diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java index 617d22b04..ff341da3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNoteMediaController.java @@ -16,6 +16,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -33,9 +34,9 @@ import java.util.Optional; /** * Encapsulates control of voice note playback from an Activity component. - * + *

* This class assumes that it will be created within the scope of Activity#onCreate - * + *

* The workhorse of this repository is the ProgressEventHandler, which will supply a * steady stream of update events to the set callback. */ @@ -54,15 +55,17 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { private MutableLiveData voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE); private LiveData> voiceNotePlayerViewState; private VoiceNoteProximityWakeLockManager voiceNoteProximityWakeLockManager; + private boolean isMediaBrowserCreationPostponed; private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback(); public VoiceNoteMediaController(@NonNull FragmentActivity activity) { - this.activity = activity; - this.mediaBrowser = new MediaBrowserCompat(activity, - new ComponentName(activity, VoiceNotePlaybackService.class), - new ConnectionCallback(), - null); + this(activity, false); + } + + public VoiceNoteMediaController(@NonNull FragmentActivity activity, boolean postponeMediaBrowserCreation) { + this.activity = activity; + this.isMediaBrowserCreationPostponed = postponeMediaBrowserCreation; activity.getLifecycle().addObserver(this); @@ -71,9 +74,9 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { VoiceNotePlaybackState.ClipType.Message message = (VoiceNotePlaybackState.ClipType.Message) playbackState.getClipType(); LiveRecipient sender = Recipient.live(message.getSenderId()); LiveRecipient threadRecipient = Recipient.live(message.getThreadRecipientId()); - LiveData name = LiveDataUtil.combineLatest(sender.getLiveDataResolved(), - threadRecipient.getLiveDataResolved(), - (s, t) -> VoiceNoteMediaItemFactory.getTitle(activity, s, t, null)); + LiveData name = LiveDataUtil.combineLatest(sender.getLiveDataResolved(), + threadRecipient.getLiveDataResolved(), + (s, t) -> VoiceNoteMediaItemFactory.getTitle(activity, s, t, null)); return Transformations.map(name, displayName -> Optional.of( new VoiceNotePlayerView.State( @@ -95,6 +98,17 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { }); } + public void ensureMediaBrowser() { + if (mediaBrowser != null) { + return; + } + + mediaBrowser = new MediaBrowserCompat(activity, + new ComponentName(activity, VoiceNotePlaybackService.class), + new ConnectionCallback(), + null); + } + public LiveData getVoiceNotePlaybackState() { return voiceNotePlaybackState; } @@ -103,8 +117,22 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { return voiceNotePlayerViewState; } + public void finishPostpone() { + isMediaBrowserCreationPostponed = false; + if (activity != null && mediaBrowser == null && activity.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { + ensureMediaBrowser(); + mediaBrowser.disconnect(); + mediaBrowser.connect(); + } + } + @Override public void onResume(@NonNull LifecycleOwner owner) { + if (mediaBrowser == null && isMediaBrowserCreationPostponed) { + return; + } + + ensureMediaBrowser(); mediaBrowser.disconnect(); mediaBrowser.connect(); } @@ -117,7 +145,9 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { MediaControllerCompat.getMediaController(activity).unregisterCallback(mediaControllerCompatCallback); } - mediaBrowser.disconnect(); + if (mediaBrowser != null) { + mediaBrowser.disconnect(); + } } @Override @@ -201,8 +231,8 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { * Tells the Media service to resume playback of a given audio slide. If the audio slide is not * currently paused, playback will be started from the beginning. * - * @param audioSlideUri The Uri of the desired audio slide - * @param messageId The Message id of the given audio slide + * @param audioSlideUri The Uri of the desired audio slide + * @param messageId The Message id of the given audio slide */ public void resumePlayback(@NonNull Uri audioSlideUri, long messageId) { if (getMediaController() == null) { @@ -390,8 +420,8 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver { } private static boolean canExtractPlaybackInformationFromMetadata(@Nullable MediaMetadataCompat mediaMetadataCompat) { - return mediaMetadataCompat != null && - mediaMetadataCompat.getDescription() != null && + return mediaMetadataCompat != null && + mediaMetadataCompat.getDescription() != null && mediaMetadataCompat.getDescription().getMediaUri() != null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java index b9a17ec69..bffc9ab95 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java @@ -254,7 +254,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { if (extras == null) { return; } - long messageId = extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID); + long messageId = extras.getLong(VoiceNoteMediaItemFactory.EXTRA_MESSAGE_ID); RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaItemFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID)); MessageTable messageDatabase = SignalDatabase.messages(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt index b88fdd5d8..e109b5bf3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt @@ -16,9 +16,11 @@ import org.thoughtcrime.securesms.components.reminder.ReminderView import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.Debouncer import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import org.thoughtcrime.securesms.util.DynamicTheme import org.thoughtcrime.securesms.util.views.Stub +import java.util.concurrent.TimeUnit open class ConversationActivity : PassphraseRequiredActivity(), ConversationParentFragment.Callback, DonationPaymentComponent { @@ -26,6 +28,7 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare private const val STATE_WATERMARK = "share_data_watermark" } + private val transitionDebouncer: Debouncer = Debouncer(150, TimeUnit.MILLISECONDS) private lateinit var fragment: ConversationParentFragment private var shareDataTimestamp: Long = -1L @@ -35,6 +38,8 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare } override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + supportPostponeEnterTransition() + transitionDebouncer.publish { supportStartPostponedEnterTransition() } window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) if (savedInstanceState != null) { @@ -51,6 +56,11 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare } } + override fun onDestroy() { + super.onDestroy() + transitionDebouncer.clear() + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(STATE_WATERMARK, shareDataTimestamp) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 2d89f4bd0..a05ebac07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -746,6 +746,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect startupStopwatch.split("first-render"); startupStopwatch.stop(TAG); SignalLocalMetrics.ConversationOpen.onRenderFinished(); + listener.onFirstRender(); }); } }); @@ -1480,6 +1481,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect @NonNull ConversationReactionOverlay.OnHideListener onHideListener); void onCursorChanged(); void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord); + void onFirstRender(); void onVoiceNotePause(@NonNull Uri uri); void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress); void onVoiceNoteResume(@NonNull Uri uri, long messageId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index 8b0a90e3d..1b7ae9a63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -512,7 +512,7 @@ public class ConversationParentFragment extends Fragment return; } - voiceNoteMediaController = new VoiceNoteMediaController(requireActivity()); + voiceNoteMediaController = new VoiceNoteMediaController(requireActivity(), true); voiceRecorderWakeLock = new VoiceRecorderWakeLock(requireActivity()); // TODO [alex] LargeScreenSupport -- Should be removed once we move to multi-pane layout. @@ -4040,6 +4040,12 @@ public class ConversationParentFragment extends Fragment } } + @Override + public void onFirstRender() { + requireActivity().supportStartPostponedEnterTransition(); + voiceNoteMediaController.finishPostpone(); + } + @Override public void onVoiceNotePause(@NonNull Uri uri) { voiceNoteMediaController.pausePlayback(uri);