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.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.keyvalue.SignalStore;
|
||||
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.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;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final MainNavigator navigator = new MainNavigator(this);
|
||||
|
||||
private VoiceNoteMediaController mediaController;
|
||||
|
||||
public static @NonNull Intent clearTop(@NonNull Context context) {
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
|
||||
|
@ -40,6 +44,7 @@ public class MainActivity extends PassphraseRequiredActivity {
|
|||
super.onCreate(savedInstanceState, ready);
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
mediaController = new VoiceNoteMediaController(this);
|
||||
navigator.onCreate(savedInstanceState);
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
|
@ -109,4 +114,9 @@ public class MainActivity extends PassphraseRequiredActivity {
|
|||
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) {
|
||||
onDuration(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getTrackDuration());
|
||||
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset());
|
||||
onProgress(voiceNotePlaybackState.getUri(),
|
||||
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
|
||||
voiceNotePlaybackState.getPlayheadPositionMillis());
|
||||
onSpeedChanged(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getSpeed());
|
||||
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isPlaying(), voiceNotePlaybackState.isAutoReset());
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!isTarget(uri)) {
|
||||
private void onStart(@NonNull Uri uri, boolean statePlaying, boolean autoReset) {
|
||||
if (!isTarget(uri) || !statePlaying) {
|
||||
if (hasAudioUri()) {
|
||||
onStop(audioSlide.getUri(), autoReset);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.voice;
|
|||
|
||||
import android.content.ComponentName;
|
||||
import android.media.AudioManager;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
|
@ -9,19 +10,28 @@ import android.os.Looper;
|
|||
import android.os.Message;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -42,10 +52,11 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||
|
||||
private static final String TAG = Log.tag(VoiceNoteMediaController.class);
|
||||
|
||||
private MediaBrowserCompat mediaBrowser;
|
||||
private AppCompatActivity activity;
|
||||
private ProgressEventHandler progressEventHandler;
|
||||
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
|
||||
private MediaBrowserCompat mediaBrowser;
|
||||
private AppCompatActivity activity;
|
||||
private ProgressEventHandler progressEventHandler;
|
||||
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
|
||||
private LiveData<Optional<VoiceNotePlayerView.State>> voiceNotePlayerViewState;
|
||||
|
||||
private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback();
|
||||
|
||||
|
@ -57,12 +68,44 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||
null);
|
||||
|
||||
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() {
|
||||
return voiceNotePlaybackState;
|
||||
}
|
||||
|
||||
public LiveData<Optional<VoiceNotePlayerView.State>> getVoiceNotePlayerViewState() {
|
||||
return voiceNotePlayerViewState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
mediaBrowser.connect();
|
||||
|
@ -94,6 +137,14 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||
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() {
|
||||
return MediaControllerCompat.getMediaController(activity);
|
||||
}
|
||||
|
@ -215,6 +266,17 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||
|
||||
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());
|
||||
} catch (RemoteException 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 final MediaControllerCompat mediaController;
|
||||
|
@ -238,38 +401,14 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
|
||||
if (isPlayerActive(mediaController.getPlaybackState()) &&
|
||||
mediaMetadataCompat != null &&
|
||||
mediaMetadataCompat.getDescription() != null &&
|
||||
mediaMetadataCompat.getDescription().getMediaUri() != null)
|
||||
{
|
||||
VoiceNotePlaybackState newPlaybackState = constructPlaybackState(mediaController, voiceNotePlaybackState.getValue());
|
||||
|
||||
Uri mediaUri = Objects.requireNonNull(mediaMetadataCompat.getDescription().getMediaUri());
|
||||
boolean autoReset = Objects.equals(mediaUri, VoiceNotePlaybackPreparer.NEXT_URI) || Objects.equals(mediaUri, VoiceNotePlaybackPreparer.END_URI);
|
||||
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 (newPlaybackState != null) {
|
||||
voiceNotePlaybackState.postValue(newPlaybackState);
|
||||
}
|
||||
|
||||
if (isPlayerActive(mediaController.getPlaybackState())) {
|
||||
sendEmptyMessageDelayed(0, 50);
|
||||
} else {
|
||||
voiceNotePlaybackState.postValue(VoiceNotePlaybackState.NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -281,6 +420,10 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||
notifyProgressEventHandler();
|
||||
} else {
|
||||
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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
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_COLOR = "voice.note.extra.COLOR";
|
||||
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);
|
||||
|
||||
|
@ -110,19 +112,11 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
|||
extras.putLong(EXTRA_THREAD_ID, threadId);
|
||||
extras.putLong(EXTRA_COLOR, threadRecipient.getChatColors().asSingleColor());
|
||||
extras.putLong(EXTRA_MESSAGE_ID, messageId);
|
||||
extras.putLong(EXTRA_MESSAGE_TIMESTAMP, dateReceived);
|
||||
|
||||
NotificationPrivacyPreference preference = SignalStore.settings().getMessageNotificationsPrivacy();
|
||||
|
||||
String title;
|
||||
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 title = getTitle(context, sender, threadRecipient, preference);
|
||||
|
||||
String subtitle = null;
|
||||
if (preference.isDisplayContact()) {
|
||||
|
@ -139,4 +133,22 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
|||
.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
|
||||
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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 {
|
||||
@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.VoiceNoteMediaController;
|
||||
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.ContactData;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
|
@ -380,6 +381,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
private MenuItem searchViewItem;
|
||||
private MessageRequestsBottomView messageRequestBottomView;
|
||||
private ConversationReactionDelegate reactionDelegate;
|
||||
private Stub<VoiceNotePlayerView> voiceNotePlayerViewStub;
|
||||
|
||||
private AttachmentManager attachmentManager;
|
||||
private AudioRecorder audioRecorder;
|
||||
|
@ -2011,6 +2013,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
|
||||
wallpaper = findViewById(R.id.conversation_wallpaper);
|
||||
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 inlineAttachmentButton = findViewById(R.id.inline_attachment_button);
|
||||
|
@ -2080,6 +2083,18 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
reactionDelegate.setOnReactionSelectedListener(this);
|
||||
|
||||
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) {
|
||||
|
@ -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) {
|
||||
if (!Util.isEmpty(viewModel.getArgs().getDraftText()) ||
|
||||
viewModel.getArgs().getMedia() != null ||
|
||||
|
|
|
@ -27,6 +27,7 @@ import android.content.res.TypedArray;
|
|||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
@ -57,6 +58,8 @@ import androidx.constraintlayout.widget.ConstraintLayout;
|
|||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
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.UnauthorizedReminder;
|
||||
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.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.database.DatabaseFactory;
|
||||
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.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
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.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
|
@ -188,6 +194,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private Drawable archiveDrawable;
|
||||
private AppForegroundObserver.Listener appForegroundObserver;
|
||||
private VoiceNoteMediaControllerOwner mediaControllerOwner;
|
||||
private Stub<VoiceNotePlayerView> voiceNotePlayerViewStub;
|
||||
|
||||
private Stopwatch startupStopwatch;
|
||||
|
||||
|
@ -195,6 +203,17 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
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
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
@ -223,6 +242,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar));
|
||||
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
|
||||
paymentNotificationView = new Stub<>(view.findViewById(R.id.payments_notification));
|
||||
voiceNotePlayerViewStub = new Stub<>(view.findViewById(R.id.voice_note_player));
|
||||
|
||||
Toolbar toolbar = getToolbar(view);
|
||||
toolbar.setVisibility(View.VISIBLE);
|
||||
|
@ -257,6 +277,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
initializeListAdapters();
|
||||
initializeTypingObserver();
|
||||
initializeSearchListener();
|
||||
initializeVoiceNotePlayer();
|
||||
|
||||
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() {
|
||||
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
|
||||
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_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
|
||||
android:id="@+id/unverified_banner_stub"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -9,11 +9,19 @@
|
|||
android:layout_height="match_parent"
|
||||
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
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@null"
|
||||
android:background="@color/signal_background_primary"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:theme="?attr/actionBarStyle"
|
||||
android:visibility="gone"
|
||||
|
@ -174,7 +182,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="reminder,payments_notification" />
|
||||
app:constraint_referenced_ids="reminder,payments_notification,voice_note_player" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
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_hint">@color/core_grey_25</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>
|
||||
|
|
|
@ -152,4 +152,7 @@
|
|||
<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_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>
|
||||
|
|
|
@ -3606,6 +3606,9 @@
|
|||
<string name="PlaybackSpeedToggleTextView__1x">1x</string>
|
||||
<string name="PlaybackSpeedToggleTextView__2x">2x</string>
|
||||
|
||||
<!-- VoiceNotePlayerView -->
|
||||
<string name="VoiceNotePlayerView__s_dot_s">%1$s · %2$s</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
Ładowanie…
Reference in New Issue