Add inline voice note player to conversation and conversation list.

fork-5.53.8
Alex Hart 2021-07-07 14:23:37 -03:00
rodzic 1bb87834d8
commit 06b64fe619
16 zmienionych plików z 667 dodań i 54 usunięć

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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);
}
}
}
}

Wyświetl plik

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.components.voice
interface VoiceNoteMediaControllerOwner {
val voiceNoteMediaController: VoiceNoteMediaController
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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 ||

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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"

Wyświetl plik

@ -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"

Wyświetl plik

@ -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" />

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>