From f3693c966afe1030fcfaf74714c3a3d7e2faed20 Mon Sep 17 00:00:00 2001 From: Clark Date: Tue, 28 Feb 2023 11:39:30 -0500 Subject: [PATCH] Improve conversation list cold start performance. --- .../securesms/ApplicationContext.java | 4 ++- .../thoughtcrime/securesms/MainActivity.java | 22 +++++++++++++ .../ConversationListFragment.java | 33 ++++++++++++++----- .../securesms/database/ThreadTable.kt | 20 +++++++++-- .../dependencies/ApplicationDependencies.java | 3 +- .../securesms/emoji/EmojiFiles.kt | 20 ++++++++--- .../securesms/util/JsonUtils.java | 8 ++++- 7 files changed, 92 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index c8b4fe289..7452f3629 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -73,6 +73,7 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.SignalGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideModule; import org.thoughtcrime.securesms.providers.BlobProvider; @@ -176,6 +177,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr .addBlocking("feature-flags", FeatureFlags::init) .addBlocking("ring-rtc", this::initializeRingRtc) .addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents())) + .addNonBlocking(() -> GlideApp.get(this)) .addNonBlocking(this::checkIsGooglePayReady) .addNonBlocking(this::cleanAvatarStorage) .addNonBlocking(this::initializeRevealableMessageManager) @@ -212,6 +214,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr .addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary) .addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved()) .addPostRender(() -> SignalDatabase.groupCallRings().removeOldRings()) + .addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp()) .execute(); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); @@ -231,7 +234,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr SignalExecutors.BOUNDED.execute(() -> { FeatureFlags.refreshIfNecessary(); - ApplicationDependencies.getRecipientCache().warmUp(); RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this); executePendingContactSync(); KeyCachingService.onAppForegrounded(this); diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java index b83d63e04..0f1b75c70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; +import android.view.ViewTreeObserver; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -37,6 +38,8 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot private VoiceNoteMediaController mediaController; private ConversationListTabsViewModel conversationListTabsViewModel; + private boolean onFirstRender = false; + public static @NonNull Intent clearTop(@NonNull Context context) { Intent intent = new Intent(context, MainActivity.class); @@ -53,6 +56,21 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot super.onCreate(savedInstanceState, ready); setContentView(R.layout.main_activity); + final View content = findViewById(android.R.id.content); + content.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + // Use pre draw listener to delay drawing frames till conversation list is ready + if (onFirstRender) { + content.getViewTreeObserver().removeOnPreDrawListener(this); + return true; + } else { + return false; + } + } + }); + mediaController = new VoiceNoteMediaController(this, true); @@ -158,6 +176,10 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot } } + public void onFirstRender() { + onFirstRender = true; + } + @Override public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() { return mediaController; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 05c57c3ae..5d58908eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -82,6 +82,7 @@ import org.signal.core.util.Stopwatch; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.MainActivity; import org.thoughtcrime.securesms.MainFragment; import org.thoughtcrime.securesms.MainNavigator; import org.thoughtcrime.securesms.MuteDialog; @@ -850,7 +851,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode } private void updateSearchToolbarHint(@NonNull ConversationFilterRequest conversationFilterRequest) { - requireCallback().getSearchToolbar().get().setSearchInputHint( + Stub searchToolbar = requireCallback().getSearchToolbar(); + if (!searchToolbar.resolved()) { + return; + } + searchToolbar.get().setSearchInputHint( conversationFilterRequest.getFilter() == ConversationFilter.OFF ? R.string.SearchToolbar_search : R.string.SearchToolbar_search_unread_chats ); } @@ -887,13 +892,14 @@ public class ConversationListFragment extends MainFragment implements ActionMode startupStopwatch.split("data-set"); SignalLocalMetrics.ColdStart.onConversationListDataLoaded(); defaultAdapter.unregisterAdapterDataObserver(this); - list.post(() -> { - AppStartup.getInstance().onCriticalRenderEventEnd(); - startupStopwatch.split("first-render"); - startupStopwatch.stop(TAG); - mediaControllerOwner.getVoiceNoteMediaController().finishPostpone(); - if (getContext() != null) { - ConversationFragment.prepare(getContext()); + if (requireActivity() instanceof MainActivity) { + ((MainActivity) requireActivity()).onFirstRender(); + } + list.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + list.removeOnLayoutChangeListener(this); + list.post(ConversationListFragment.this::onFirstRender); } }); } @@ -968,6 +974,17 @@ public class ConversationListFragment extends MainFragment implements ActionMode }); } + private void onFirstRender() { + AppStartup.getInstance().onCriticalRenderEventEnd(); + startupStopwatch.split("first-render"); + startupStopwatch.stop(TAG); + mediaControllerOwner.getVoiceNoteMediaController().finishPostpone(); + requireCallback().getSearchToolbar().get(); + if (getContext() != null) { + ConversationFragment.prepare(getContext()); + } + } + private void onConversationListChanged(@NonNull List conversations) { LinearLayoutManager layoutManager = (LinearLayoutManager) list.getLayoutManager(); int firstVisibleItem = layoutManager != null ? layoutManager.findFirstCompletelyVisibleItemPosition() : -1; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index 0137b2c6f..9fb17ebdf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -8,6 +8,7 @@ import android.database.MergeCursor import android.net.Uri import androidx.core.content.contentValuesOf import com.fasterxml.jackson.annotation.JsonProperty +import org.json.JSONObject import org.jsoup.helper.StringUtil import org.signal.core.util.CursorUtil import org.signal.core.util.SqlUtil @@ -59,6 +60,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.JsonUtils +import org.thoughtcrime.securesms.util.JsonUtils.SaneJSONObject import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.isScheduled import org.whispersystems.signalservice.api.push.ServiceId @@ -1743,9 +1745,21 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa val extraString = cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET_EXTRAS)) val extra: Extra? = if (extraString != null) { try { - JsonUtils.fromJson(extraString, Extra::class.java) - } catch (e: IOException) { - Log.w(TAG, "Failed to decode extras!") + val jsonObject = SaneJSONObject(JSONObject(extraString)) + Extra( + isViewOnce = jsonObject.getBoolean("isRevealable"), + isSticker = jsonObject.getBoolean("isSticker"), + stickerEmoji = jsonObject.getString("stickerEmoji"), + isAlbum = jsonObject.getBoolean("isAlbum"), + isRemoteDelete = jsonObject.getBoolean("isRemoteDelete"), + isMessageRequestAccepted = jsonObject.getBoolean("isMessageRequestAccepted"), + isGv2Invite = jsonObject.getBoolean("isGv2Invite"), + groupAddedBy = jsonObject.getString("groupAddedBy"), + individualRecipientId = jsonObject.getString("individualRecipientId")!!, + bodyRanges = jsonObject.getString("bodyRanges"), + isScheduled = jsonObject.getBoolean("isScheduled") + ) + } catch (exception: Exception) { null } } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 96f9262bd..2cf2f1a89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -87,6 +87,7 @@ public class ApplicationDependencies { private static final Object LOCK = new Object(); private static final Object FRAME_RATE_TRACKER_LOCK = new Object(); private static final Object JOB_MANAGER_LOCK = new Object(); + private static final Object SIGNAL_HTTP_CLIENT_LOCK = new Object(); private static Application application; private static Provider provider; @@ -544,7 +545,7 @@ public class ApplicationDependencies { public static @NonNull OkHttpClient getSignalOkHttpClient() { if (signalOkHttpClient == null) { - synchronized (LOCK) { + synchronized (SIGNAL_HTTP_CLIENT_LOCK) { if (signalOkHttpClient == null) { try { OkHttpClient baseClient = ApplicationDependencies.getOkHttpClient(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiFiles.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiFiles.kt index 646a3ccf1..0838206e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiFiles.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiFiles.kt @@ -4,6 +4,7 @@ import android.content.Context import android.net.Uri import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.registerKotlinModule @@ -142,7 +143,6 @@ object EmojiFiles { private fun getDirectory(context: Context): File = File(context.getEmojiDirectory(), this.uuid.toString()).apply { mkdir() } companion object { - private val objectMapper = ObjectMapper().registerKotlinModule() @JvmStatic @@ -150,7 +150,12 @@ object EmojiFiles { fun readVersion(context: Context, skipValidation: Boolean = false): Version? { val version = try { getInputStream(context, context.getVersionFile()).use { - objectMapper.readValue(it, Version::class.java) + val tree: JsonNode = objectMapper.readTree(it) + Version( + version = tree["version"].asInt(), + uuid = objectMapper.convertValue(tree["uuid"], UUID::class.java), + density = tree["density"].asText() + ) } } catch (e: Exception) { Log.w(TAG, "Could not read current emoji version from disk.", e) @@ -222,8 +227,15 @@ object EmojiFiles { @JvmStatic fun read(context: Context, version: Version): NameCollection { try { - getInputStream(context, context.getNameFile(version.uuid)).use { - return objectMapper.readValue(it) + getInputStream(context, context.getNameFile(version.uuid)).use { inputStream -> + val tree: JsonNode = objectMapper.readTree(inputStream) + val elements = tree["names"].elements().asSequence().map { + Name( + name = it["name"].asText(), + uuid = objectMapper.convertValue(it["uuid"], UUID::class.java) + ) + }.toList() + return NameCollection(objectMapper.convertValue(tree["versionUuid"], UUID::class.java), elements) } } catch (e: Exception) { return NameCollection(version.uuid, listOf()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/JsonUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/JsonUtils.java index 0e6d66d42..f14d69724 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/JsonUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/JsonUtils.java @@ -11,6 +11,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import javax.annotation.Nullable; + public class JsonUtils { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -54,7 +56,7 @@ public class JsonUtils { this.delegate = delegate; } - public String getString(String name) throws JSONException { + public @Nullable String getString(String name) throws JSONException { if (delegate.isNull(name)) return null; else return delegate.getString(name); } @@ -63,6 +65,10 @@ public class JsonUtils { return delegate.getLong(name); } + public boolean getBoolean(String name) throws JSONException { + return delegate.getBoolean(name); + } + public boolean isNull(String name) { return delegate.isNull(name); }