Improve conversation open speed.
This commit is contained in:
parent
60874ba57b
commit
ec504af593
5 changed files with 65 additions and 17 deletions
|
@ -16,6 +16,7 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
@ -33,9 +34,9 @@ import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates control of voice note playback from an Activity component.
|
* Encapsulates control of voice note playback from an Activity component.
|
||||||
*
|
* <p>
|
||||||
* This class assumes that it will be created within the scope of Activity#onCreate
|
* This class assumes that it will be created within the scope of Activity#onCreate
|
||||||
*
|
* <p>
|
||||||
* The workhorse of this repository is the ProgressEventHandler, which will supply a
|
* The workhorse of this repository is the ProgressEventHandler, which will supply a
|
||||||
* steady stream of update events to the set callback.
|
* steady stream of update events to the set callback.
|
||||||
*/
|
*/
|
||||||
|
@ -54,15 +55,17 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
|
private MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState = new MutableLiveData<>(VoiceNotePlaybackState.NONE);
|
||||||
private LiveData<Optional<VoiceNotePlayerView.State>> voiceNotePlayerViewState;
|
private LiveData<Optional<VoiceNotePlayerView.State>> voiceNotePlayerViewState;
|
||||||
private VoiceNoteProximityWakeLockManager voiceNoteProximityWakeLockManager;
|
private VoiceNoteProximityWakeLockManager voiceNoteProximityWakeLockManager;
|
||||||
|
private boolean isMediaBrowserCreationPostponed;
|
||||||
|
|
||||||
private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback();
|
private final MediaControllerCompatCallback mediaControllerCompatCallback = new MediaControllerCompatCallback();
|
||||||
|
|
||||||
public VoiceNoteMediaController(@NonNull FragmentActivity activity) {
|
public VoiceNoteMediaController(@NonNull FragmentActivity activity) {
|
||||||
this.activity = activity;
|
this(activity, false);
|
||||||
this.mediaBrowser = new MediaBrowserCompat(activity,
|
}
|
||||||
new ComponentName(activity, VoiceNotePlaybackService.class),
|
|
||||||
new ConnectionCallback(),
|
public VoiceNoteMediaController(@NonNull FragmentActivity activity, boolean postponeMediaBrowserCreation) {
|
||||||
null);
|
this.activity = activity;
|
||||||
|
this.isMediaBrowserCreationPostponed = postponeMediaBrowserCreation;
|
||||||
|
|
||||||
activity.getLifecycle().addObserver(this);
|
activity.getLifecycle().addObserver(this);
|
||||||
|
|
||||||
|
@ -71,9 +74,9 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
VoiceNotePlaybackState.ClipType.Message message = (VoiceNotePlaybackState.ClipType.Message) playbackState.getClipType();
|
VoiceNotePlaybackState.ClipType.Message message = (VoiceNotePlaybackState.ClipType.Message) playbackState.getClipType();
|
||||||
LiveRecipient sender = Recipient.live(message.getSenderId());
|
LiveRecipient sender = Recipient.live(message.getSenderId());
|
||||||
LiveRecipient threadRecipient = Recipient.live(message.getThreadRecipientId());
|
LiveRecipient threadRecipient = Recipient.live(message.getThreadRecipientId());
|
||||||
LiveData<String> name = LiveDataUtil.combineLatest(sender.getLiveDataResolved(),
|
LiveData<String> name = LiveDataUtil.combineLatest(sender.getLiveDataResolved(),
|
||||||
threadRecipient.getLiveDataResolved(),
|
threadRecipient.getLiveDataResolved(),
|
||||||
(s, t) -> VoiceNoteMediaItemFactory.getTitle(activity, s, t, null));
|
(s, t) -> VoiceNoteMediaItemFactory.getTitle(activity, s, t, null));
|
||||||
|
|
||||||
return Transformations.map(name, displayName -> Optional.of(
|
return Transformations.map(name, displayName -> Optional.of(
|
||||||
new VoiceNotePlayerView.State(
|
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<VoiceNotePlaybackState> getVoiceNotePlaybackState() {
|
public LiveData<VoiceNotePlaybackState> getVoiceNotePlaybackState() {
|
||||||
return voiceNotePlaybackState;
|
return voiceNotePlaybackState;
|
||||||
}
|
}
|
||||||
|
@ -103,8 +117,22 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
return voiceNotePlayerViewState;
|
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
|
@Override
|
||||||
public void onResume(@NonNull LifecycleOwner owner) {
|
public void onResume(@NonNull LifecycleOwner owner) {
|
||||||
|
if (mediaBrowser == null && isMediaBrowserCreationPostponed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureMediaBrowser();
|
||||||
mediaBrowser.disconnect();
|
mediaBrowser.disconnect();
|
||||||
mediaBrowser.connect();
|
mediaBrowser.connect();
|
||||||
}
|
}
|
||||||
|
@ -117,7 +145,9 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
MediaControllerCompat.getMediaController(activity).unregisterCallback(mediaControllerCompatCallback);
|
MediaControllerCompat.getMediaController(activity).unregisterCallback(mediaControllerCompatCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaBrowser.disconnect();
|
if (mediaBrowser != null) {
|
||||||
|
mediaBrowser.disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
* 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.
|
* currently paused, playback will be started from the beginning.
|
||||||
*
|
*
|
||||||
* @param audioSlideUri The Uri of the desired audio slide
|
* @param audioSlideUri The Uri of the desired audio slide
|
||||||
* @param messageId The Message id of the given audio slide
|
* @param messageId The Message id of the given audio slide
|
||||||
*/
|
*/
|
||||||
public void resumePlayback(@NonNull Uri audioSlideUri, long messageId) {
|
public void resumePlayback(@NonNull Uri audioSlideUri, long messageId) {
|
||||||
if (getMediaController() == null) {
|
if (getMediaController() == null) {
|
||||||
|
@ -390,8 +420,8 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canExtractPlaybackInformationFromMetadata(@Nullable MediaMetadataCompat mediaMetadataCompat) {
|
private static boolean canExtractPlaybackInformationFromMetadata(@Nullable MediaMetadataCompat mediaMetadataCompat) {
|
||||||
return mediaMetadataCompat != null &&
|
return mediaMetadataCompat != null &&
|
||||||
mediaMetadataCompat.getDescription() != null &&
|
mediaMetadataCompat.getDescription() != null &&
|
||||||
mediaMetadataCompat.getDescription().getMediaUri() != null;
|
mediaMetadataCompat.getDescription().getMediaUri() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -254,7 +254,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||||
if (extras == null) {
|
if (extras == null) {
|
||||||
return;
|
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));
|
RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaItemFactory.EXTRA_INDIVIDUAL_RECIPIENT_ID));
|
||||||
MessageTable messageDatabase = SignalDatabase.messages();
|
MessageTable messageDatabase = SignalDatabase.messages();
|
||||||
|
|
||||||
|
|
|
@ -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.DonationPaymentComponent
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.Debouncer
|
||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||||
import org.thoughtcrime.securesms.util.views.Stub
|
import org.thoughtcrime.securesms.util.views.Stub
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
open class ConversationActivity : PassphraseRequiredActivity(), ConversationParentFragment.Callback, DonationPaymentComponent {
|
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 const val STATE_WATERMARK = "share_data_watermark"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val transitionDebouncer: Debouncer = Debouncer(150, TimeUnit.MILLISECONDS)
|
||||||
private lateinit var fragment: ConversationParentFragment
|
private lateinit var fragment: ConversationParentFragment
|
||||||
private var shareDataTimestamp: Long = -1L
|
private var shareDataTimestamp: Long = -1L
|
||||||
|
|
||||||
|
@ -35,6 +38,8 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
|
supportPostponeEnterTransition()
|
||||||
|
transitionDebouncer.publish { supportStartPostponedEnterTransition() }
|
||||||
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
|
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
|
@ -51,6 +56,11 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
transitionDebouncer.clear()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putLong(STATE_WATERMARK, shareDataTimestamp)
|
outState.putLong(STATE_WATERMARK, shareDataTimestamp)
|
||||||
|
|
|
@ -746,6 +746,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
startupStopwatch.split("first-render");
|
startupStopwatch.split("first-render");
|
||||||
startupStopwatch.stop(TAG);
|
startupStopwatch.stop(TAG);
|
||||||
SignalLocalMetrics.ConversationOpen.onRenderFinished();
|
SignalLocalMetrics.ConversationOpen.onRenderFinished();
|
||||||
|
listener.onFirstRender();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1480,6 +1481,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
|
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
|
||||||
void onCursorChanged();
|
void onCursorChanged();
|
||||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||||
|
void onFirstRender();
|
||||||
void onVoiceNotePause(@NonNull Uri uri);
|
void onVoiceNotePause(@NonNull Uri uri);
|
||||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
|
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
|
||||||
void onVoiceNoteResume(@NonNull Uri uri, long messageId);
|
void onVoiceNoteResume(@NonNull Uri uri, long messageId);
|
||||||
|
|
|
@ -512,7 +512,7 @@ public class ConversationParentFragment extends Fragment
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
voiceNoteMediaController = new VoiceNoteMediaController(requireActivity());
|
voiceNoteMediaController = new VoiceNoteMediaController(requireActivity(), true);
|
||||||
voiceRecorderWakeLock = new VoiceRecorderWakeLock(requireActivity());
|
voiceRecorderWakeLock = new VoiceRecorderWakeLock(requireActivity());
|
||||||
|
|
||||||
// TODO [alex] LargeScreenSupport -- Should be removed once we move to multi-pane layout.
|
// 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
|
@Override
|
||||||
public void onVoiceNotePause(@NonNull Uri uri) {
|
public void onVoiceNotePause(@NonNull Uri uri) {
|
||||||
voiceNoteMediaController.pausePlayback(uri);
|
voiceNoteMediaController.pausePlayback(uri);
|
||||||
|
|
Loading…
Add table
Reference in a new issue