kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add inline voice note player to conversation and conversation list.
rodzic
1bb87834d8
commit
06b64fe619
|
@ -9,6 +9,8 @@ import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||||
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog;
|
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.util.AppStartup;
|
import org.thoughtcrime.securesms.util.AppStartup;
|
||||||
|
@ -17,13 +19,15 @@ import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
|
||||||
public class MainActivity extends PassphraseRequiredActivity {
|
public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
|
||||||
|
|
||||||
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
|
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||||
private final MainNavigator navigator = new MainNavigator(this);
|
private final MainNavigator navigator = new MainNavigator(this);
|
||||||
|
|
||||||
|
private VoiceNoteMediaController mediaController;
|
||||||
|
|
||||||
public static @NonNull Intent clearTop(@NonNull Context context) {
|
public static @NonNull Intent clearTop(@NonNull Context context) {
|
||||||
Intent intent = new Intent(context, MainActivity.class);
|
Intent intent = new Intent(context, MainActivity.class);
|
||||||
|
|
||||||
|
@ -40,6 +44,7 @@ public class MainActivity extends PassphraseRequiredActivity {
|
||||||
super.onCreate(savedInstanceState, ready);
|
super.onCreate(savedInstanceState, ready);
|
||||||
setContentView(R.layout.main_activity);
|
setContentView(R.layout.main_activity);
|
||||||
|
|
||||||
|
mediaController = new VoiceNoteMediaController(this);
|
||||||
navigator.onCreate(savedInstanceState);
|
navigator.onCreate(savedInstanceState);
|
||||||
|
|
||||||
handleGroupLinkInIntent(getIntent());
|
handleGroupLinkInIntent(getIntent());
|
||||||
|
@ -109,4 +114,9 @@ public class MainActivity extends PassphraseRequiredActivity {
|
||||||
CommunicationActions.handlePotentialProxyLinkUrl(this, data.toString());
|
CommunicationActions.handlePotentialProxyLinkUrl(this, data.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
|
||||||
|
return mediaController;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,11 +232,11 @@ public final class AudioView extends FrameLayout {
|
||||||
|
|
||||||
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
|
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
|
||||||
onDuration(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getTrackDuration());
|
onDuration(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getTrackDuration());
|
||||||
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset());
|
|
||||||
onProgress(voiceNotePlaybackState.getUri(),
|
onProgress(voiceNotePlaybackState.getUri(),
|
||||||
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
|
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
|
||||||
voiceNotePlaybackState.getPlayheadPositionMillis());
|
voiceNotePlaybackState.getPlayheadPositionMillis());
|
||||||
onSpeedChanged(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getSpeed());
|
onSpeedChanged(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getSpeed());
|
||||||
|
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isPlaying(), voiceNotePlaybackState.isAutoReset());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onDuration(@NonNull Uri uri, long durationMillis) {
|
private void onDuration(@NonNull Uri uri, long durationMillis) {
|
||||||
|
@ -245,8 +245,8 @@ public final class AudioView extends FrameLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onStart(@NonNull Uri uri, boolean autoReset) {
|
private void onStart(@NonNull Uri uri, boolean statePlaying, boolean autoReset) {
|
||||||
if (!isTarget(uri)) {
|
if (!isTarget(uri) || !statePlaying) {
|
||||||
if (hasAudioUri()) {
|
if (hasAudioUri()) {
|
||||||
onStop(audioSlide.getUri(), autoReset);
|
onStop(audioSlide.getUri(), autoReset);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.voice;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
|
import android.media.session.PlaybackState;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
@ -9,19 +10,28 @@ import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.support.v4.media.MediaBrowserCompat;
|
import android.support.v4.media.MediaBrowserCompat;
|
||||||
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
import android.support.v4.media.MediaMetadataCompat;
|
import android.support.v4.media.MediaMetadataCompat;
|
||||||
import android.support.v4.media.session.MediaControllerCompat;
|
import android.support.v4.media.session.MediaControllerCompat;
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import android.support.v4.media.session.PlaybackStateCompat;
|
import android.support.v4.media.session.PlaybackStateCompat;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Transformations;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@ -46,6 +56,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
private AppCompatActivity activity;
|
private AppCompatActivity activity;
|
||||||
private ProgressEventHandler progressEventHandler;
|
private ProgressEventHandler progressEventHandler;
|
||||||
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
|
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
|
||||||
|
private LiveData<Optional<VoiceNotePlayerView.State>> voiceNotePlayerViewState;
|
||||||
|
|
||||||
private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback();
|
private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback();
|
||||||
|
|
||||||
|
@ -57,12 +68,44 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
null);
|
null);
|
||||||
|
|
||||||
activity.getLifecycle().addObserver(this);
|
activity.getLifecycle().addObserver(this);
|
||||||
|
|
||||||
|
voiceNotePlayerViewState = Transformations.switchMap(voiceNotePlaybackState, playbackState -> {
|
||||||
|
if (playbackState.getClipType() instanceof VoiceNotePlaybackState.ClipType.Message) {
|
||||||
|
VoiceNotePlaybackState.ClipType.Message message = (VoiceNotePlaybackState.ClipType.Message) playbackState.getClipType();
|
||||||
|
LiveRecipient sender = Recipient.live(message.getSenderId());
|
||||||
|
LiveRecipient threadRecipient = Recipient.live(message.getThreadRecipientId());
|
||||||
|
LiveData<String> name = LiveDataUtil.combineLatest(sender.getLiveDataResolved(),
|
||||||
|
threadRecipient.getLiveDataResolved(),
|
||||||
|
(s, t) -> VoiceNoteMediaDescriptionCompatFactory.getTitle(activity, s, t, null));
|
||||||
|
|
||||||
|
return Transformations.map(name, displayName -> Optional.of(
|
||||||
|
new VoiceNotePlayerView.State(
|
||||||
|
playbackState.getUri(),
|
||||||
|
message.getMessageId(),
|
||||||
|
message.getThreadId(),
|
||||||
|
!playbackState.isPlaying(),
|
||||||
|
message.getSenderId(),
|
||||||
|
message.getThreadRecipientId(),
|
||||||
|
message.getMessagePosition(),
|
||||||
|
message.getTimestamp(),
|
||||||
|
displayName,
|
||||||
|
playbackState.getPlayheadPositionMillis(),
|
||||||
|
playbackState.getTrackDuration(),
|
||||||
|
playbackState.getSpeed())));
|
||||||
|
} else {
|
||||||
|
return new DefaultValueLiveData<>(Optional.absent());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<VoiceNotePlaybackState> getVoiceNotePlaybackState() {
|
public LiveData<VoiceNotePlaybackState> getVoiceNotePlaybackState() {
|
||||||
return voiceNotePlaybackState;
|
return voiceNotePlaybackState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<Optional<VoiceNotePlayerView.State>> getVoiceNotePlayerViewState() {
|
||||||
|
return voiceNotePlayerViewState;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart(@NonNull LifecycleOwner owner) {
|
public void onStart(@NonNull LifecycleOwner owner) {
|
||||||
mediaBrowser.connect();
|
mediaBrowser.connect();
|
||||||
|
@ -94,6 +137,14 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
playbackStateCompat.getState() == PlaybackStateCompat.STATE_PLAYING;
|
playbackStateCompat.getState() == PlaybackStateCompat.STATE_PLAYING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isPlayerPaused(@NonNull PlaybackStateCompat playbackStateCompat) {
|
||||||
|
return playbackStateCompat.getState() == PlaybackStateCompat.STATE_PAUSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPlayerStopped(@NonNull PlaybackStateCompat playbackStateCompat) {
|
||||||
|
return playbackStateCompat.getState() <= PlaybackStateCompat.STATE_STOPPED;
|
||||||
|
}
|
||||||
|
|
||||||
private @NonNull MediaControllerCompat getMediaController() {
|
private @NonNull MediaControllerCompat getMediaController() {
|
||||||
return MediaControllerCompat.getMediaController(activity);
|
return MediaControllerCompat.getMediaController(activity);
|
||||||
}
|
}
|
||||||
|
@ -215,6 +266,17 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
|
|
||||||
mediaController.registerCallback(mediaControllerCompatCallback);
|
mediaController.registerCallback(mediaControllerCompatCallback);
|
||||||
|
|
||||||
|
if (Objects.equals(voiceNotePlaybackState.getValue(), VoiceNotePlaybackState.NONE)) {
|
||||||
|
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
|
||||||
|
if (canExtractPlaybackInformationFromMetadata(mediaMetadataCompat)) {
|
||||||
|
VoiceNotePlaybackState newState = extractStateFromMetadata(mediaController, mediaMetadataCompat, null);
|
||||||
|
|
||||||
|
if (newState != null) {
|
||||||
|
voiceNotePlaybackState.postValue(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mediaControllerCompatCallback.onPlaybackStateChanged(mediaController.getPlaybackState());
|
mediaControllerCompatCallback.onPlaybackStateChanged(mediaController.getPlaybackState());
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
Log.w(TAG, "onConnected: Failed to set media controller", e);
|
Log.w(TAG, "onConnected: Failed to set media controller", e);
|
||||||
|
@ -222,6 +284,107 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean canExtractPlaybackInformationFromMetadata(@Nullable MediaMetadataCompat mediaMetadataCompat) {
|
||||||
|
return mediaMetadataCompat != null &&
|
||||||
|
mediaMetadataCompat.getDescription() != null &&
|
||||||
|
mediaMetadataCompat.getDescription().getMediaUri() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable VoiceNotePlaybackState extractStateFromMetadata(@NonNull MediaControllerCompat mediaController,
|
||||||
|
@NonNull MediaMetadataCompat mediaMetadataCompat,
|
||||||
|
@Nullable VoiceNotePlaybackState previousState)
|
||||||
|
{
|
||||||
|
Uri mediaUri = Objects.requireNonNull(mediaMetadataCompat.getDescription().getMediaUri());
|
||||||
|
boolean autoReset = Objects.equals(mediaUri, VoiceNotePlaybackPreparer.NEXT_URI) || Objects.equals(mediaUri, VoiceNotePlaybackPreparer.END_URI);
|
||||||
|
long position = mediaController.getPlaybackState().getPosition();
|
||||||
|
long duration = mediaMetadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
||||||
|
Bundle extras = mediaController.getExtras();
|
||||||
|
float speed = extras != null ? extras.getFloat(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, 1f) : 1f;
|
||||||
|
|
||||||
|
if (previousState != null && Objects.equals(mediaUri, previousState.getUri())) {
|
||||||
|
if (position < 0 && previousState.getPlayheadPositionMillis() >= 0) {
|
||||||
|
position = previousState.getPlayheadPositionMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration <= 0 && previousState.getTrackDuration() > 0) {
|
||||||
|
duration = previousState.getTrackDuration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration > 0 && position >= 0 && position <= duration) {
|
||||||
|
return new VoiceNotePlaybackState(mediaUri,
|
||||||
|
position,
|
||||||
|
duration,
|
||||||
|
autoReset,
|
||||||
|
speed,
|
||||||
|
isPlayerActive(mediaController.getPlaybackState()),
|
||||||
|
getClipType(mediaMetadataCompat.getBundle()));
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable VoiceNotePlaybackState constructPlaybackState(@NonNull MediaControllerCompat mediaController,
|
||||||
|
@Nullable VoiceNotePlaybackState previousState)
|
||||||
|
{
|
||||||
|
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
|
||||||
|
if (isPlayerActive(mediaController.getPlaybackState()) &&
|
||||||
|
canExtractPlaybackInformationFromMetadata(mediaMetadataCompat))
|
||||||
|
{
|
||||||
|
return extractStateFromMetadata(mediaController, mediaMetadataCompat, previousState);
|
||||||
|
} else if (isPlayerPaused(mediaController.getPlaybackState()) &&
|
||||||
|
mediaMetadataCompat != null)
|
||||||
|
{
|
||||||
|
long position = mediaController.getPlaybackState().getPosition();
|
||||||
|
long duration = mediaMetadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
||||||
|
|
||||||
|
if (previousState != null && position < duration) {
|
||||||
|
return previousState.asPaused();
|
||||||
|
} else {
|
||||||
|
return VoiceNotePlaybackState.NONE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return VoiceNotePlaybackState.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NonNull VoiceNotePlaybackState.ClipType getClipType(@Nullable Bundle mediaExtras) {
|
||||||
|
long messageId = -1L;
|
||||||
|
RecipientId senderId = RecipientId.UNKNOWN;
|
||||||
|
long messagePosition = -1L;
|
||||||
|
long threadId = -1L;
|
||||||
|
RecipientId threadRecipientId = RecipientId.UNKNOWN;
|
||||||
|
long timestamp = -1L;
|
||||||
|
|
||||||
|
if (mediaExtras != null) {
|
||||||
|
messageId = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID, -1L);
|
||||||
|
messagePosition = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION, -1L);
|
||||||
|
threadId = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID, -1L);
|
||||||
|
timestamp = mediaExtras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_TIMESTAMP, -1L);
|
||||||
|
|
||||||
|
String serializedSenderId = mediaExtras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID);
|
||||||
|
if (serializedSenderId != null) {
|
||||||
|
senderId = RecipientId.from(serializedSenderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
String serializedThreadRecipientId = mediaExtras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID);
|
||||||
|
if (serializedThreadRecipientId != null) {
|
||||||
|
threadRecipientId = RecipientId.from(serializedThreadRecipientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageId != -1L) {
|
||||||
|
return new VoiceNotePlaybackState.ClipType.Message(messageId,
|
||||||
|
senderId,
|
||||||
|
threadRecipientId,
|
||||||
|
messagePosition,
|
||||||
|
threadId,
|
||||||
|
timestamp);
|
||||||
|
} else {
|
||||||
|
return VoiceNotePlaybackState.ClipType.Draft.INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class ProgressEventHandler extends Handler {
|
private static class ProgressEventHandler extends Handler {
|
||||||
|
|
||||||
private final MediaControllerCompat mediaController;
|
private final MediaControllerCompat mediaController;
|
||||||
|
@ -238,38 +401,14 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(@NonNull Message msg) {
|
public void handleMessage(@NonNull Message msg) {
|
||||||
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
|
VoiceNotePlaybackState newPlaybackState = constructPlaybackState(mediaController, voiceNotePlaybackState.getValue());
|
||||||
if (isPlayerActive(mediaController.getPlaybackState()) &&
|
|
||||||
mediaMetadataCompat != null &&
|
|
||||||
mediaMetadataCompat.getDescription() != null &&
|
|
||||||
mediaMetadataCompat.getDescription().getMediaUri() != null)
|
|
||||||
{
|
|
||||||
|
|
||||||
Uri mediaUri = Objects.requireNonNull(mediaMetadataCompat.getDescription().getMediaUri());
|
if (newPlaybackState != null) {
|
||||||
boolean autoReset = Objects.equals(mediaUri, VoiceNotePlaybackPreparer.NEXT_URI) || Objects.equals(mediaUri, VoiceNotePlaybackPreparer.END_URI);
|
voiceNotePlaybackState.postValue(newPlaybackState);
|
||||||
VoiceNotePlaybackState previousState = voiceNotePlaybackState.getValue();
|
|
||||||
long position = mediaController.getPlaybackState().getPosition();
|
|
||||||
long duration = mediaMetadataCompat.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
|
||||||
Bundle extras = mediaController.getExtras();
|
|
||||||
float speed = extras != null ? extras.getFloat(VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED, 1f) : 1f;
|
|
||||||
|
|
||||||
if (previousState != null && Objects.equals(mediaUri, previousState.getUri())) {
|
|
||||||
if (position < 0 && previousState.getPlayheadPositionMillis() >= 0) {
|
|
||||||
position = previousState.getPlayheadPositionMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration <= 0 && previousState.getTrackDuration() > 0) {
|
|
||||||
duration = previousState.getTrackDuration();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration > 0 && position >= 0 && position <= duration) {
|
|
||||||
voiceNotePlaybackState.postValue(new VoiceNotePlaybackState(mediaUri, position, duration, autoReset, speed));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPlayerActive(mediaController.getPlaybackState())) {
|
||||||
sendEmptyMessageDelayed(0, 50);
|
sendEmptyMessageDelayed(0, 50);
|
||||||
} else {
|
|
||||||
voiceNotePlaybackState.postValue(VoiceNotePlaybackState.NONE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,6 +420,10 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
notifyProgressEventHandler();
|
notifyProgressEventHandler();
|
||||||
} else {
|
} else {
|
||||||
clearProgressEventHandler();
|
clearProgressEventHandler();
|
||||||
|
|
||||||
|
if (isPlayerStopped(state)) {
|
||||||
|
voiceNotePlaybackState.postValue(VoiceNotePlaybackState.NONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package org.thoughtcrime.securesms.components.voice
|
||||||
|
|
||||||
|
interface VoiceNoteMediaControllerOwner {
|
||||||
|
val voiceNoteMediaController: VoiceNoteMediaController
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import android.os.Bundle;
|
||||||
import android.support.v4.media.MediaDescriptionCompat;
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
@ -33,6 +34,7 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||||
public static final String EXTRA_THREAD_ID = "voice.note.extra.THREAD_ID";
|
public static final String EXTRA_THREAD_ID = "voice.note.extra.THREAD_ID";
|
||||||
public static final String EXTRA_COLOR = "voice.note.extra.COLOR";
|
public static final String EXTRA_COLOR = "voice.note.extra.COLOR";
|
||||||
public static final String EXTRA_MESSAGE_ID = "voice.note.extra.MESSAGE_ID";
|
public static final String EXTRA_MESSAGE_ID = "voice.note.extra.MESSAGE_ID";
|
||||||
|
public static final String EXTRA_MESSAGE_TIMESTAMP = "voice.note.extra.MESSAGE_TIMESTAMP";
|
||||||
|
|
||||||
private static final String TAG = Log.tag(VoiceNoteMediaDescriptionCompatFactory.class);
|
private static final String TAG = Log.tag(VoiceNoteMediaDescriptionCompatFactory.class);
|
||||||
|
|
||||||
|
@ -110,19 +112,11 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||||
extras.putLong(EXTRA_THREAD_ID, threadId);
|
extras.putLong(EXTRA_THREAD_ID, threadId);
|
||||||
extras.putLong(EXTRA_COLOR, threadRecipient.getChatColors().asSingleColor());
|
extras.putLong(EXTRA_COLOR, threadRecipient.getChatColors().asSingleColor());
|
||||||
extras.putLong(EXTRA_MESSAGE_ID, messageId);
|
extras.putLong(EXTRA_MESSAGE_ID, messageId);
|
||||||
|
extras.putLong(EXTRA_MESSAGE_TIMESTAMP, dateReceived);
|
||||||
|
|
||||||
NotificationPrivacyPreference preference = SignalStore.settings().getMessageNotificationsPrivacy();
|
NotificationPrivacyPreference preference = SignalStore.settings().getMessageNotificationsPrivacy();
|
||||||
|
|
||||||
String title;
|
String title = getTitle(context, sender, threadRecipient, preference);
|
||||||
if (preference.isDisplayContact() && threadRecipient.isGroup()) {
|
|
||||||
title = context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__s_to_s,
|
|
||||||
sender.getDisplayName(context),
|
|
||||||
threadRecipient.getDisplayName(context));
|
|
||||||
} else if (preference.isDisplayContact()) {
|
|
||||||
title = sender.getDisplayName(context);
|
|
||||||
} else {
|
|
||||||
title = context.getString(R.string.MessageNotifier_signal_message);
|
|
||||||
}
|
|
||||||
|
|
||||||
String subtitle = null;
|
String subtitle = null;
|
||||||
if (preference.isDisplayContact()) {
|
if (preference.isDisplayContact()) {
|
||||||
|
@ -139,4 +133,22 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @NonNull String getTitle(@NonNull Context context, @NonNull Recipient sender, @NonNull Recipient threadRecipient, @Nullable NotificationPrivacyPreference notificationPrivacyPreference) {
|
||||||
|
NotificationPrivacyPreference preference;
|
||||||
|
if (notificationPrivacyPreference == null) {
|
||||||
|
preference = new NotificationPrivacyPreference("all");
|
||||||
|
} else {
|
||||||
|
preference = notificationPrivacyPreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preference.isDisplayContact() && threadRecipient.isGroup()) {
|
||||||
|
return context.getString(R.string.VoiceNoteMediaDescriptionCompatFactory__s_to_s,
|
||||||
|
sender.getDisplayName(context),
|
||||||
|
threadRecipient.getDisplayName(context));
|
||||||
|
} else if (preference.isDisplayContact()) {
|
||||||
|
return sender.getDisplayName(context);
|
||||||
|
} else {
|
||||||
|
return context.getString(R.string.MessageNotifier_signal_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.components.voice
|
package org.thoughtcrime.securesms.components.voice
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain-level state object representing the state of the currently playing voice note.
|
* Domain-level state object representing the state of the currently playing voice note.
|
||||||
|
@ -29,11 +30,37 @@ data class VoiceNotePlaybackState(
|
||||||
/**
|
/**
|
||||||
* @return The current playback speed factor
|
* @return The current playback speed factor
|
||||||
*/
|
*/
|
||||||
val speed: Float
|
val speed: Float,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Whether we are playing or paused
|
||||||
|
*/
|
||||||
|
val isPlaying: Boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Information about the type this clip represents.
|
||||||
|
*/
|
||||||
|
val clipType: ClipType
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
@JvmField
|
@JvmField
|
||||||
val NONE = VoiceNotePlaybackState(Uri.EMPTY, 0, 0, false, 1f)
|
val NONE = VoiceNotePlaybackState(Uri.EMPTY, 0, 0, false, 1f, false, ClipType.Idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun asPaused(): VoiceNotePlaybackState {
|
||||||
|
return copy(isPlaying = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ClipType {
|
||||||
|
data class Message(
|
||||||
|
val messageId: Long,
|
||||||
|
val senderId: RecipientId,
|
||||||
|
val threadRecipientId: RecipientId,
|
||||||
|
val messagePosition: Long,
|
||||||
|
val threadId: Long,
|
||||||
|
val timestamp: Long
|
||||||
|
) : ClipType()
|
||||||
|
object Draft : ClipType()
|
||||||
|
object Idle : ClipType()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,199 @@
|
||||||
|
package org.thoughtcrime.securesms.components.voice
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.airbnb.lottie.LottieAnimationView
|
||||||
|
import com.airbnb.lottie.LottieProperty
|
||||||
|
import com.airbnb.lottie.SimpleColorFilter
|
||||||
|
import com.airbnb.lottie.model.KeyPath
|
||||||
|
import com.airbnb.lottie.value.LottieValueCallback
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.PlaybackSpeedToggleTextView
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
private const val ANIMATE_DURATION: Long = 150L
|
||||||
|
private const val TO_PAUSE = 1
|
||||||
|
private const val TO_PLAY = -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a bar at the top of Conversation list and in a conversation to allow
|
||||||
|
* playback manipulation of voice notes.
|
||||||
|
*/
|
||||||
|
class VoiceNotePlayerView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val playPauseToggleView: LottieAnimationView
|
||||||
|
private val infoView: TextView
|
||||||
|
private val speedView: PlaybackSpeedToggleTextView
|
||||||
|
private val closeButton: View
|
||||||
|
|
||||||
|
private var lastState: State? = null
|
||||||
|
private var playerVisible: Boolean = false
|
||||||
|
private var lottieDirection: Int = 0
|
||||||
|
|
||||||
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(context, R.layout.voice_note_player_view, this)
|
||||||
|
|
||||||
|
playPauseToggleView = findViewById(R.id.voice_note_player_play_pause_toggle)
|
||||||
|
infoView = findViewById(R.id.voice_note_player_info)
|
||||||
|
speedView = findViewById(R.id.voice_note_player_speed)
|
||||||
|
closeButton = findViewById(R.id.voice_note_player_close)
|
||||||
|
|
||||||
|
val speedTouchTarget: View = findViewById(R.id.voice_note_player_speed_touch_target)
|
||||||
|
speedTouchTarget.setOnClickListener {
|
||||||
|
speedView.performClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
speedView.playbackSpeedListener = object : PlaybackSpeedToggleTextView.PlaybackSpeedListener {
|
||||||
|
override fun onPlaybackSpeedChanged(speed: Float) {
|
||||||
|
lastState?.let {
|
||||||
|
listener?.onSpeedChangeRequested(it.uri, speed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeButton.setOnClickListener {
|
||||||
|
lastState?.let {
|
||||||
|
listener?.onCloseRequested(it.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playPauseToggleView.setOnClickListener {
|
||||||
|
lastState?.let {
|
||||||
|
if (it.isPaused) {
|
||||||
|
if (it.playbackPosition >= it.playbackDuration) {
|
||||||
|
listener?.onPlay(it.uri, it.messageId, 0.0)
|
||||||
|
} else {
|
||||||
|
listener?.onPlay(it.uri, it.messageId, it.playbackPosition.toDouble() / it.playbackDuration)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listener?.onPause(it.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
playPauseToggleView.addValueCallback(
|
||||||
|
KeyPath("**"),
|
||||||
|
LottieProperty.COLOR_FILTER,
|
||||||
|
LottieValueCallback(SimpleColorFilter(ContextCompat.getColor(context, R.color.signal_icon_tint_primary)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (background != null) {
|
||||||
|
background.colorFilter = SimpleColorFilter(ContextCompat.getColor(context, R.color.voice_note_player_view_background))
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnClickListener {
|
||||||
|
lastState?.let {
|
||||||
|
listener?.onNavigateToMessage(it.threadId, it.threadRecipientId, it.senderId, it.messageTimestamp, it.messagePositionInThread)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setState(state: State) {
|
||||||
|
this.lastState = state
|
||||||
|
|
||||||
|
if (state.isPaused) {
|
||||||
|
animateToggleToPlay()
|
||||||
|
} else {
|
||||||
|
animateToggleToPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
infoView.text = context.getString(R.string.VoiceNotePlayerView__s_dot_s, state.name, formatDuration(state.playbackDuration))
|
||||||
|
speedView.setCurrentSpeed(state.playbackSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show() {
|
||||||
|
if (!playerVisible) {
|
||||||
|
visibility = VISIBLE
|
||||||
|
|
||||||
|
val animation = AnimationUtils.loadAnimation(context, R.anim.slide_from_top)
|
||||||
|
animation.duration = ANIMATE_DURATION
|
||||||
|
|
||||||
|
startAnimation(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
playerVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hide() {
|
||||||
|
if (playerVisible) {
|
||||||
|
val animation = AnimationUtils.loadAnimation(context, R.anim.slide_to_top)
|
||||||
|
animation.duration = ANIMATE_DURATION
|
||||||
|
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||||
|
override fun onAnimationStart(animation: Animation?) = Unit
|
||||||
|
override fun onAnimationRepeat(animation: Animation?) = Unit
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animation?) {
|
||||||
|
visibility = GONE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
startAnimation(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
playerVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDuration(duration: Long): String {
|
||||||
|
val secs = TimeUnit.MILLISECONDS.toSeconds(duration)
|
||||||
|
|
||||||
|
return resources.getString(R.string.AudioView_duration, secs / 60, secs % 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateToggleToPlay() {
|
||||||
|
startLottieAnimation(TO_PLAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateToggleToPause() {
|
||||||
|
startLottieAnimation(TO_PAUSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLottieAnimation(direction: Int) {
|
||||||
|
if (lottieDirection == direction) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lottieDirection = direction
|
||||||
|
playPauseToggleView.pauseAnimation()
|
||||||
|
playPauseToggleView.speed = (direction * 2).toFloat()
|
||||||
|
playPauseToggleView.resumeAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val uri: Uri,
|
||||||
|
val messageId: Long,
|
||||||
|
val threadId: Long,
|
||||||
|
val isPaused: Boolean,
|
||||||
|
val senderId: RecipientId,
|
||||||
|
val threadRecipientId: RecipientId,
|
||||||
|
val messagePositionInThread: Long,
|
||||||
|
val messageTimestamp: Long,
|
||||||
|
val name: String,
|
||||||
|
val playbackPosition: Long,
|
||||||
|
val playbackDuration: Long,
|
||||||
|
val playbackSpeed: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onPlay(uri: Uri, messageId: Long, position: Double)
|
||||||
|
fun onPause(uri: Uri)
|
||||||
|
fun onCloseRequested(uri: Uri)
|
||||||
|
fun onSpeedChangeRequested(uri: Uri, speed: Float)
|
||||||
|
fun onNavigateToMessage(threadId: Long, threadRecipientId: RecipientId, senderId: RecipientId, messageSentAt: Long, messagePositionInThread: Long)
|
||||||
|
}
|
||||||
|
}
|
|
@ -135,6 +135,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.ConversationS
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||||
|
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||||
|
@ -380,6 +381,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
private MenuItem searchViewItem;
|
private MenuItem searchViewItem;
|
||||||
private MessageRequestsBottomView messageRequestBottomView;
|
private MessageRequestsBottomView messageRequestBottomView;
|
||||||
private ConversationReactionDelegate reactionDelegate;
|
private ConversationReactionDelegate reactionDelegate;
|
||||||
|
private Stub<VoiceNotePlayerView> voiceNotePlayerViewStub;
|
||||||
|
|
||||||
private AttachmentManager attachmentManager;
|
private AttachmentManager attachmentManager;
|
||||||
private AudioRecorder audioRecorder;
|
private AudioRecorder audioRecorder;
|
||||||
|
@ -2011,6 +2013,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
|
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
|
||||||
wallpaper = findViewById(R.id.conversation_wallpaper);
|
wallpaper = findViewById(R.id.conversation_wallpaper);
|
||||||
wallpaperDim = findViewById(R.id.conversation_wallpaper_dim);
|
wallpaperDim = findViewById(R.id.conversation_wallpaper_dim);
|
||||||
|
voiceNotePlayerViewStub = ViewUtil.findStubById(this, R.id.voice_note_player_stub);
|
||||||
|
|
||||||
ImageButton quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
ImageButton quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
||||||
ImageButton inlineAttachmentButton = findViewById(R.id.inline_attachment_button);
|
ImageButton inlineAttachmentButton = findViewById(R.id.inline_attachment_button);
|
||||||
|
@ -2080,6 +2083,18 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
reactionDelegate.setOnReactionSelectedListener(this);
|
reactionDelegate.setOnReactionSelectedListener(this);
|
||||||
|
|
||||||
joinGroupCallButton.setOnClickListener(v -> handleVideo(getRecipient()));
|
joinGroupCallButton.setOnClickListener(v -> handleVideo(getRecipient()));
|
||||||
|
|
||||||
|
voiceNoteMediaController.getVoiceNotePlayerViewState().observe(this, state -> {
|
||||||
|
if (state.isPresent()) {
|
||||||
|
if (!voiceNotePlayerViewStub.resolved()) {
|
||||||
|
voiceNotePlayerViewStub.get().setListener(new VoiceNotePlayerViewListener());
|
||||||
|
}
|
||||||
|
voiceNotePlayerViewStub.get().show();
|
||||||
|
voiceNotePlayerViewStub.get().setState(state.get());
|
||||||
|
} else if (voiceNotePlayerViewStub.resolved()) {
|
||||||
|
voiceNotePlayerViewStub.get().hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateWallpaper(@Nullable ChatWallpaper chatWallpaper) {
|
private void updateWallpaper(@Nullable ChatWallpaper chatWallpaper) {
|
||||||
|
@ -3984,6 +3999,39 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class VoiceNotePlayerViewListener implements VoiceNotePlayerView.Listener {
|
||||||
|
@Override
|
||||||
|
public void onCloseRequested(@NonNull Uri uri) {
|
||||||
|
voiceNoteMediaController.stopPlaybackAndReset(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSpeedChangeRequested(@NonNull Uri uri, float speed) {
|
||||||
|
voiceNoteMediaController.setPlaybackSpeed(uri, speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlay(@NonNull Uri uri, long messageId, double position) {
|
||||||
|
voiceNoteMediaController.startSinglePlayback(uri, messageId, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause(@NonNull Uri uri) {
|
||||||
|
voiceNoteMediaController.pausePlayback(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNavigateToMessage(long threadId, @NonNull RecipientId threadRecipientId, @NonNull RecipientId senderId, long messageTimestamp, long messagePositionInThread) {
|
||||||
|
if (threadId != ConversationActivity.this.threadId) {
|
||||||
|
startActivity(ConversationIntents.createBuilder(ConversationActivity.this, threadRecipientId, threadId)
|
||||||
|
.withStartingPosition((int) messagePositionInThread)
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
|
fragment.jumpToMessage(senderId, messageTimestamp, () -> { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void presentMessageRequestState(@Nullable MessageRequestViewModel.MessageData messageData) {
|
private void presentMessageRequestState(@Nullable MessageRequestViewModel.MessageData messageData) {
|
||||||
if (!Util.isEmpty(viewModel.getArgs().getDraftText()) ||
|
if (!Util.isEmpty(viewModel.getArgs().getDraftText()) ||
|
||||||
viewModel.getArgs().getMedia() != null ||
|
viewModel.getArgs().getMedia() != null ||
|
||||||
|
|
|
@ -27,6 +27,7 @@ import android.content.res.TypedArray;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -57,6 +58,8 @@ import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.constraintlayout.widget.ConstraintSet;
|
import androidx.constraintlayout.widget.ConstraintSet;
|
||||||
import androidx.core.content.res.ResourcesCompat;
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
@ -89,11 +92,11 @@ import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
||||||
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
||||||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||||
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||||
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||||
|
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationFragment;
|
import org.thoughtcrime.securesms.conversation.ConversationFragment;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
||||||
import org.thoughtcrime.securesms.search.MessageResult;
|
|
||||||
import org.thoughtcrime.securesms.search.SearchResult;
|
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
|
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
|
||||||
|
@ -119,6 +122,9 @@ import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsPar
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.search.MessageResult;
|
||||||
|
import org.thoughtcrime.securesms.search.SearchResult;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||||
|
@ -188,6 +194,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
private SnapToTopDataObserver snapToTopDataObserver;
|
private SnapToTopDataObserver snapToTopDataObserver;
|
||||||
private Drawable archiveDrawable;
|
private Drawable archiveDrawable;
|
||||||
private AppForegroundObserver.Listener appForegroundObserver;
|
private AppForegroundObserver.Listener appForegroundObserver;
|
||||||
|
private VoiceNoteMediaControllerOwner mediaControllerOwner;
|
||||||
|
private Stub<VoiceNotePlayerView> voiceNotePlayerViewStub;
|
||||||
|
|
||||||
private Stopwatch startupStopwatch;
|
private Stopwatch startupStopwatch;
|
||||||
|
|
||||||
|
@ -195,6 +203,17 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
return new ConversationListFragment();
|
return new ConversationListFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(@NonNull Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
|
||||||
|
if (context instanceof VoiceNoteMediaControllerOwner) {
|
||||||
|
mediaControllerOwner = (VoiceNoteMediaControllerOwner) context;
|
||||||
|
} else {
|
||||||
|
throw new ClassCastException("Expected context to be a Listener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle icicle) {
|
public void onCreate(Bundle icicle) {
|
||||||
super.onCreate(icicle);
|
super.onCreate(icicle);
|
||||||
|
@ -223,6 +242,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar));
|
searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar));
|
||||||
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
|
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
|
||||||
paymentNotificationView = new Stub<>(view.findViewById(R.id.payments_notification));
|
paymentNotificationView = new Stub<>(view.findViewById(R.id.payments_notification));
|
||||||
|
voiceNotePlayerViewStub = new Stub<>(view.findViewById(R.id.voice_note_player));
|
||||||
|
|
||||||
Toolbar toolbar = getToolbar(view);
|
Toolbar toolbar = getToolbar(view);
|
||||||
toolbar.setVisibility(View.VISIBLE);
|
toolbar.setVisibility(View.VISIBLE);
|
||||||
|
@ -257,6 +277,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
initializeListAdapters();
|
initializeListAdapters();
|
||||||
initializeTypingObserver();
|
initializeTypingObserver();
|
||||||
initializeSearchListener();
|
initializeSearchListener();
|
||||||
|
initializeVoiceNotePlayer();
|
||||||
|
|
||||||
RatingManager.showRatingDialogIfNecessary(requireContext());
|
RatingManager.showRatingDialogIfNecessary(requireContext());
|
||||||
|
|
||||||
|
@ -507,6 +528,21 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeVoiceNotePlayer() {
|
||||||
|
mediaControllerOwner.getVoiceNoteMediaController().getVoiceNotePlayerViewState().observe(getViewLifecycleOwner(), state -> {
|
||||||
|
if (state.isPresent()) {
|
||||||
|
if (!voiceNotePlayerViewStub.resolved()) {
|
||||||
|
voiceNotePlayerViewStub.get().setListener(new VoiceNotePlayerViewListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
voiceNotePlayerViewStub.get().setState(state.get());
|
||||||
|
voiceNotePlayerViewStub.get().show();
|
||||||
|
} else if (voiceNotePlayerViewStub.resolved()) {
|
||||||
|
voiceNotePlayerViewStub.get().hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void initializeListAdapters() {
|
private void initializeListAdapters() {
|
||||||
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
|
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
|
||||||
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
|
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
|
||||||
|
@ -1282,6 +1318,36 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class VoiceNotePlayerViewListener implements VoiceNotePlayerView.Listener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCloseRequested(@NonNull Uri uri) {
|
||||||
|
if (voiceNotePlayerViewStub.resolved()) {
|
||||||
|
mediaControllerOwner.getVoiceNoteMediaController().stopPlaybackAndReset(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSpeedChangeRequested(@NonNull Uri uri, float speed) {
|
||||||
|
mediaControllerOwner.getVoiceNoteMediaController().setPlaybackSpeed(uri, speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlay(@NonNull Uri uri, long messageId, double position) {
|
||||||
|
mediaControllerOwner.getVoiceNoteMediaController().startSinglePlayback(uri, messageId, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause(@NonNull Uri uri) {
|
||||||
|
mediaControllerOwner.getVoiceNoteMediaController().pausePlayback(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNavigateToMessage(long threadId, @NonNull RecipientId threadRecipientId, @NonNull RecipientId senderId, long messageSentAt, long messagePositionInThread) {
|
||||||
|
MainNavigator.get(requireActivity()).goToConversation(threadRecipientId, threadId, ThreadDatabase.DistributionTypes.DEFAULT, (int) messagePositionInThread);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -205,6 +205,13 @@
|
||||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline">
|
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline">
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/voice_note_player_stub"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inflatedId="@+id/voice_note_player"
|
||||||
|
android:layout="@layout/voice_note_player_stub" />
|
||||||
|
|
||||||
<ViewStub
|
<ViewStub
|
||||||
android:id="@+id/unverified_banner_stub"
|
android:id="@+id/unverified_banner_stub"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -9,11 +9,19 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?android:windowBackground">
|
android:background="?android:windowBackground">
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/voice_note_player"
|
||||||
|
android:inflatedId="@+id/voice_note_player"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout="@layout/voice_note_player_stub"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar_barrier" />
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
|
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:background="@null"
|
android:background="@color/signal_background_primary"
|
||||||
android:minHeight="?attr/actionBarSize"
|
android:minHeight="?attr/actionBarSize"
|
||||||
android:theme="?attr/actionBarStyle"
|
android:theme="?attr/actionBarStyle"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
@ -174,7 +182,7 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:barrierDirection="bottom"
|
app:barrierDirection="bottom"
|
||||||
app:constraint_referenced_ids="reminder,payments_notification" />
|
app:constraint_referenced_ids="reminder,payments_notification,voice_note_player" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/list"
|
android:id="@+id/list"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/selectable_background" />
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:background="@color/voice_note_player_view_background"
|
||||||
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||||
|
|
||||||
|
<com.airbnb.lottie.LottieAnimationView
|
||||||
|
android:id="@+id/voice_note_player_play_pause_toggle"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:padding="10dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/voice_note_player_info"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/voice_note_player_info"
|
||||||
|
app:lottie_colorFilter="@color/signal_icon_tint_primary"
|
||||||
|
app:lottie_rawRes="@raw/lottie_play_pause"
|
||||||
|
tools:background="@color/signal_alert_primary" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||||
|
android:id="@+id/voice_note_player_info"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="36dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
|
android:textColor="@color/signal_icon_tint_primary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/voice_note_player_speed"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/voice_note_player_play_pause_toggle"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Miles Morales · 5:20" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/voice_note_player_speed_touch_target"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/voice_note_player_close"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.PlaybackSpeedToggleTextView
|
||||||
|
android:id="@+id/voice_note_player_speed"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/round_background"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:gravity="center"
|
||||||
|
android:lineSpacingExtra="1sp"
|
||||||
|
android:minWidth="30dp"
|
||||||
|
android:minHeight="20dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
|
||||||
|
android:textColor="@color/signal_text_secondary"
|
||||||
|
app:backgroundTint="@color/voice_note_player_speed_background_tint"
|
||||||
|
tools:text="1x" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/voice_note_player_close"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/voice_note_player_info"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/voice_note_player_info"
|
||||||
|
app:srcCompat="@drawable/ic_x_20"
|
||||||
|
app:tint="@color/signal_icon_tint_secondary" />
|
||||||
|
|
||||||
|
</merge>
|
|
@ -152,4 +152,7 @@
|
||||||
<color name="react_with_any_search_background">@color/core_grey_65</color>
|
<color name="react_with_any_search_background">@color/core_grey_65</color>
|
||||||
<color name="react_with_any_search_hint">@color/core_grey_25</color>
|
<color name="react_with_any_search_hint">@color/core_grey_25</color>
|
||||||
<color name="react_with_any_customize_background">@color/core_grey_65</color>
|
<color name="react_with_any_customize_background">@color/core_grey_65</color>
|
||||||
|
|
||||||
|
<color name="voice_note_player_view_background">@color/core_grey_80</color>
|
||||||
|
<color name="voice_note_player_speed_background_tint">@color/core_grey_65</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -152,4 +152,7 @@
|
||||||
<color name="react_with_any_search_background">@color/core_grey_05</color>
|
<color name="react_with_any_search_background">@color/core_grey_05</color>
|
||||||
<color name="react_with_any_search_hint">@color/core_grey_60</color>
|
<color name="react_with_any_search_hint">@color/core_grey_60</color>
|
||||||
<color name="react_with_any_customize_background">@color/core_white</color>
|
<color name="react_with_any_customize_background">@color/core_white</color>
|
||||||
|
|
||||||
|
<color name="voice_note_player_view_background">@color/core_grey_02</color>
|
||||||
|
<color name="voice_note_player_speed_background_tint">@color/transparent_black_08</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -3606,6 +3606,9 @@
|
||||||
<string name="PlaybackSpeedToggleTextView__1x">1x</string>
|
<string name="PlaybackSpeedToggleTextView__1x">1x</string>
|
||||||
<string name="PlaybackSpeedToggleTextView__2x">2x</string>
|
<string name="PlaybackSpeedToggleTextView__2x">2x</string>
|
||||||
|
|
||||||
|
<!-- VoiceNotePlayerView -->
|
||||||
|
<string name="VoiceNotePlayerView__s_dot_s">%1$s · %2$s</string>
|
||||||
|
|
||||||
<!-- EOF -->
|
<!-- EOF -->
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Ładowanie…
Reference in New Issue