mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 08:23:00 +01:00
Fix all media storage overview performance.
This commit is contained in:
committed by
Cody Henthorne
parent
a8ba0dccca
commit
7a2eca3bd5
@@ -297,7 +297,8 @@ class AttachmentTable(
|
||||
"CREATE INDEX IF NOT EXISTS $DATA_FILE_INDEX ON $TABLE_NAME ($DATA_FILE);",
|
||||
"CREATE INDEX IF NOT EXISTS attachment_archive_transfer_state ON $TABLE_NAME ($ARCHIVE_TRANSFER_STATE);",
|
||||
"CREATE INDEX IF NOT EXISTS attachment_remote_digest_index ON $TABLE_NAME ($REMOTE_DIGEST);",
|
||||
"CREATE INDEX IF NOT EXISTS attachment_metadata_id ON $TABLE_NAME ($METADATA_ID);"
|
||||
"CREATE INDEX IF NOT EXISTS attachment_metadata_id ON $TABLE_NAME ($METADATA_ID);",
|
||||
"CREATE INDEX IF NOT EXISTS attachment_media_overview_size ON $TABLE_NAME ($DATA_SIZE DESC, $DISPLAY_ORDER DESC) WHERE $QUOTE = 0 AND $STICKER_PACK_ID IS NULL AND $DATA_FILE IS NOT NULL"
|
||||
)
|
||||
|
||||
private val DATA_FILE_INFO_PROJECTION = arrayOf(
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireString
|
||||
@@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.util.MediaUtil.SlideType
|
||||
class MediaTable internal constructor(context: Context?, databaseHelper: SignalDatabase?) : DatabaseTable(context, databaseHelper) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MediaTable::class)
|
||||
const val ALL_THREADS = -1
|
||||
private const val THREAD_RECIPIENT_ID = "THREAD_RECIPIENT_ID"
|
||||
private val BASE_MEDIA_QUERY = """
|
||||
@@ -64,16 +66,12 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID},
|
||||
${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} as $THREAD_RECIPIENT_ID,
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.LINK_PREVIEWS}
|
||||
FROM
|
||||
${AttachmentTable.TABLE_NAME}
|
||||
LEFT JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
|
||||
LEFT JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} = ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}
|
||||
WHERE
|
||||
${AttachmentTable.MESSAGE_ID} IN (
|
||||
SELECT ${MessageTable.ID}
|
||||
FROM ${MessageTable.TABLE_NAME}
|
||||
WHERE ${MessageTable.THREAD_ID} __EQUALITY__ ?
|
||||
) AND
|
||||
FROM
|
||||
${AttachmentTable.TABLE_NAME} __INDEX_HINT__
|
||||
LEFT JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
|
||||
LEFT JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} = ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}
|
||||
WHERE
|
||||
__THREAD_FILTER__ AND
|
||||
(%s) AND
|
||||
${MessageTable.VIEW_ONCE} = 0 AND
|
||||
${MessageTable.STORY_TYPE} = 0 AND
|
||||
@@ -216,7 +214,25 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
)
|
||||
|
||||
private fun applyEqualityOperator(threadId: Long, query: String): String {
|
||||
return query.replace("__EQUALITY__", if (threadId == ALL_THREADS.toLong()) "!=" else "=")
|
||||
val isAllThreads = threadId == ALL_THREADS.toLong()
|
||||
return query
|
||||
.replace(
|
||||
"__THREAD_FILTER__",
|
||||
if (isAllThreads) {
|
||||
"${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} != ?"
|
||||
} else {
|
||||
"${AttachmentTable.MESSAGE_ID} IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.THREAD_ID} = ?)"
|
||||
}
|
||||
)
|
||||
.replace("__EQUALITY__", if (isAllThreads) "!=" else "=")
|
||||
.replace("__INDEX_HINT__", "")
|
||||
}
|
||||
|
||||
private fun applyIndexHint(query: String, threadId: Long, sorting: Sorting): String {
|
||||
if (threadId == ALL_THREADS.toLong() && sorting == Sorting.Largest) {
|
||||
return query.replace("__INDEX_HINT__", "INDEXED BY attachment_media_overview_size")
|
||||
}
|
||||
return query.replace("__INDEX_HINT__", "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,15 +248,27 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
return readableDatabase.rawQuery(query, args)
|
||||
}
|
||||
|
||||
fun getDocumentMediaForThread(threadId: Long, sorting: Sorting): Cursor {
|
||||
val query = sorting.applyToQuery(applyEqualityOperator(threadId, DOCUMENT_MEDIA_QUERY))
|
||||
@JvmOverloads
|
||||
fun getDocumentMediaForThread(threadId: Long, sorting: Sorting, limit: Int = 0): Cursor {
|
||||
var query = sorting.applyToQuery(applyEqualityOperator(threadId, DOCUMENT_MEDIA_QUERY))
|
||||
val args = arrayOf(threadId.toString() + "")
|
||||
|
||||
if (limit > 0) {
|
||||
query = "$query LIMIT $limit"
|
||||
}
|
||||
|
||||
return readableDatabase.rawQuery(query, args)
|
||||
}
|
||||
|
||||
fun getAudioMediaForThread(threadId: Long, sorting: Sorting): Cursor {
|
||||
val query = sorting.applyToQuery(applyEqualityOperator(threadId, AUDIO_MEDIA_QUERY))
|
||||
@JvmOverloads
|
||||
fun getAudioMediaForThread(threadId: Long, sorting: Sorting, limit: Int = 0): Cursor {
|
||||
var query = sorting.applyToQuery(applyEqualityOperator(threadId, AUDIO_MEDIA_QUERY))
|
||||
val args = arrayOf(threadId.toString() + "")
|
||||
|
||||
if (limit > 0) {
|
||||
query = "$query LIMIT $limit"
|
||||
}
|
||||
|
||||
return readableDatabase.rawQuery(query, args)
|
||||
}
|
||||
|
||||
@@ -255,9 +283,15 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
return readableDatabase.rawQuery(query, args)
|
||||
}
|
||||
|
||||
fun getAllMediaForThread(threadId: Long, sorting: Sorting): Cursor {
|
||||
val query = sorting.applyToQuery(applyEqualityOperator(threadId, ALL_MEDIA_QUERY))
|
||||
@JvmOverloads
|
||||
fun getAllMediaForThread(threadId: Long, sorting: Sorting, limit: Int = 0): Cursor {
|
||||
var query = sorting.applyToQuery(applyEqualityOperator(threadId, applyIndexHint(ALL_MEDIA_QUERY, threadId, sorting)))
|
||||
val args = arrayOf(threadId.toString() + "")
|
||||
|
||||
if (limit > 0) {
|
||||
query = "$query LIMIT $limit"
|
||||
}
|
||||
|
||||
return readableDatabase.rawQuery(query, args)
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V306_AddRemoteDelet
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V308_AddBackRemoteDeletedColumn
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V309_GroupTerminatedColumnMigration
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V310_AddStarredColumn
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V311_AddAttachmentMediaOverviewSizeIndex
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -333,10 +334,11 @@ object SignalDatabaseMigrations {
|
||||
// 307 to V307_RemoveRemoteDeletedColumn - Removed due to unsolvable OOM crashes. [TODO]: Attempt to fix in the future
|
||||
308 to V308_AddBackRemoteDeletedColumn,
|
||||
309 to V309_GroupTerminatedColumnMigration,
|
||||
310 to V310_AddStarredColumn
|
||||
310 to V310_AddStarredColumn,
|
||||
311 to V311_AddAttachmentMediaOverviewSizeIndex
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 310
|
||||
const val DATABASE_VERSION = 311
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds a partial index on attachment (data_size, display_order) to speed up
|
||||
* the "all threads" media overview sorted by largest.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V311_AddAttachmentMediaOverviewSizeIndex : SignalDatabaseMigration {
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_media_overview_size ON attachment (data_size DESC, display_order DESC) WHERE quote = 0 AND sticker_pack_id IS NULL AND data_file IS NOT NULL")
|
||||
}
|
||||
}
|
||||
@@ -30,16 +30,19 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader<GroupedThrea
|
||||
private final MediaLoader.MediaType mediaType;
|
||||
private final MediaTable.Sorting sorting;
|
||||
private final long threadId;
|
||||
private final int limit;
|
||||
|
||||
public GroupedThreadMediaLoader(@NonNull Context context,
|
||||
long threadId,
|
||||
@NonNull MediaLoader.MediaType mediaType,
|
||||
@NonNull MediaTable.Sorting sorting)
|
||||
@NonNull MediaTable.Sorting sorting,
|
||||
int limit)
|
||||
{
|
||||
super(context);
|
||||
this.threadId = threadId;
|
||||
this.mediaType = mediaType;
|
||||
this.sorting = sorting;
|
||||
this.limit = limit;
|
||||
this.observer = () -> ThreadUtil.runOnMain(this::onContentChanged);
|
||||
|
||||
onContentChanged();
|
||||
@@ -73,7 +76,7 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader<GroupedThrea
|
||||
|
||||
AppDependencies.getDatabaseObserver().registerAttachmentUpdatedObserver(observer);
|
||||
|
||||
try (Cursor cursor = ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting)) {
|
||||
try (Cursor cursor = ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting, limit)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
mediaGrouping.add(MediaTable.MediaRecord.from(cursor));
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public final class RecipientMediaLoader extends MediaLoader {
|
||||
|
||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.resolved(recipientId));
|
||||
|
||||
return ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting);
|
||||
return ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,21 +27,22 @@ public final class ThreadMediaLoader extends MediaLoader {
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
return createThreadMediaCursor(context, threadId, mediaType, sorting);
|
||||
return createThreadMediaCursor(context, threadId, mediaType, sorting, 0);
|
||||
}
|
||||
|
||||
static Cursor createThreadMediaCursor(@NonNull Context context,
|
||||
long threadId,
|
||||
@NonNull MediaType mediaType,
|
||||
@NonNull MediaTable.Sorting sorting) {
|
||||
@NonNull MediaTable.Sorting sorting,
|
||||
int limit) {
|
||||
MediaTable mediaDatabase = SignalDatabase.media();
|
||||
|
||||
switch (mediaType) {
|
||||
case GALLERY : return mediaDatabase.getGalleryMediaForThread(threadId, sorting);
|
||||
case DOCUMENT: return mediaDatabase.getDocumentMediaForThread(threadId, sorting);
|
||||
case AUDIO : return mediaDatabase.getAudioMediaForThread(threadId, sorting);
|
||||
case GALLERY : return mediaDatabase.getGalleryMediaForThread(threadId, sorting, limit);
|
||||
case DOCUMENT: return mediaDatabase.getDocumentMediaForThread(threadId, sorting, limit);
|
||||
case AUDIO : return mediaDatabase.getAudioMediaForThread(threadId, sorting, limit);
|
||||
case LINK : return mediaDatabase.getLinkMediaForThread(threadId, sorting);
|
||||
case ALL : return mediaDatabase.getAllMediaForThread(threadId, sorting);
|
||||
case ALL : return mediaDatabase.getAllMediaForThread(threadId, sorting, limit);
|
||||
default : throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,9 +70,11 @@ public final class MediaOverviewPageFragment extends LoggingFragment
|
||||
|
||||
private static final String TAG = Log.tag(MediaOverviewPageFragment.class);
|
||||
|
||||
private static final String THREAD_ID_EXTRA = "thread_id";
|
||||
private static final String MEDIA_TYPE_EXTRA = "media_type";
|
||||
private static final String GRID_MODE = "grid_mode";
|
||||
private static final String THREAD_ID_EXTRA = "thread_id";
|
||||
private static final String MEDIA_TYPE_EXTRA = "media_type";
|
||||
private static final String GRID_MODE = "grid_mode";
|
||||
private static final int INITIAL_LOAD_LIMIT = 500;
|
||||
private static final int LOAD_MORE_THRESHOLD = 50;
|
||||
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
private MediaTable.Sorting sorting = MediaTable.Sorting.Newest;
|
||||
@@ -88,6 +90,10 @@ public final class MediaOverviewPageFragment extends LoggingFragment
|
||||
private VoiceNoteMediaController voiceNoteMediaController;
|
||||
private SignalBottomActionBar bottomActionBar;
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
private boolean pendingLoad = true;
|
||||
private int loadLimit;
|
||||
private boolean allLoaded;
|
||||
private boolean loadingMore;
|
||||
|
||||
public static @NonNull Fragment newInstance(long threadId,
|
||||
@NonNull MediaLoader.MediaType mediaType,
|
||||
@@ -115,7 +121,8 @@ public final class MediaOverviewPageFragment extends LoggingFragment
|
||||
|
||||
if (threadId == Long.MIN_VALUE) throw new AssertionError();
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||
loadLimit = threadId == MediaTable.ALL_THREADS ? INITIAL_LOAD_LIMIT : 0;
|
||||
allLoaded = loadLimit == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,6 +132,15 @@ public final class MediaOverviewPageFragment extends LoggingFragment
|
||||
voiceNoteMediaController = new VoiceNoteMediaController(requireActivity(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (pendingLoad) {
|
||||
pendingLoad = false;
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
lifecycleDisposable = new LifecycleDisposable();
|
||||
@@ -151,6 +167,22 @@ public final class MediaOverviewPageFragment extends LoggingFragment
|
||||
this.recyclerView.setHasFixedSize(true);
|
||||
this.recyclerView.addItemDecoration(new MediaGridDividerDecoration(spans, ViewUtil.dpToPx(4), adapter));
|
||||
this.recyclerView.addItemDecoration(new BottomOffsetDecoration(ViewUtil.dpToPx(160)));
|
||||
this.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) {
|
||||
if (allLoaded || loadingMore) return;
|
||||
|
||||
int scrollRange = rv.computeVerticalScrollRange();
|
||||
int scrollOffset = rv.computeVerticalScrollOffset();
|
||||
int scrollExtent = rv.computeVerticalScrollExtent();
|
||||
|
||||
if (scrollRange > 0 && scrollOffset + scrollExtent * 2 >= scrollRange) {
|
||||
loadingMore = true;
|
||||
loadLimit *= 2;
|
||||
LoaderManager.getInstance(MediaOverviewPageFragment.this).restartLoader(0, null, MediaOverviewPageFragment.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
MediaOverviewViewModel viewModel = MediaOverviewViewModel.getMediaOverviewViewModel(requireActivity());
|
||||
|
||||
@@ -158,8 +190,14 @@ public final class MediaOverviewPageFragment extends LoggingFragment
|
||||
.observe(getViewLifecycleOwner(), sorting -> {
|
||||
if (sorting != null) {
|
||||
this.sorting = sorting;
|
||||
this.loadLimit = threadId == MediaTable.ALL_THREADS ? INITIAL_LOAD_LIMIT : 0;
|
||||
this.allLoaded = loadLimit == 0;
|
||||
adapter.setShowFileSizes(sorting.isRelatedToFileSize());
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
if (isResumed()) {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
} else {
|
||||
pendingLoad = true;
|
||||
}
|
||||
updateMultiSelect();
|
||||
}
|
||||
});
|
||||
@@ -196,7 +234,7 @@ public final class MediaOverviewPageFragment extends LoggingFragment
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> onCreateLoader(int i, Bundle bundle) {
|
||||
return new GroupedThreadMediaLoader(requireContext(), threadId, mediaType, sorting);
|
||||
return new GroupedThreadMediaLoader(requireContext(), threadId, mediaType, sorting, loadLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -204,6 +242,15 @@ public final class MediaOverviewPageFragment extends LoggingFragment
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).setMedia(groupedThreadMedia);
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged();
|
||||
|
||||
if (loadLimit > 0) {
|
||||
int totalMediaItems = 0;
|
||||
for (int i = 0; i < groupedThreadMedia.getSectionCount(); i++) {
|
||||
totalMediaItems += groupedThreadMedia.getSectionItemCount(i);
|
||||
}
|
||||
allLoaded = totalMediaItems < loadLimit;
|
||||
}
|
||||
|
||||
loadingMore = false;
|
||||
noMedia.setVisibility(recyclerView.getAdapter().getItemCount() > 0 ? View.GONE : View.VISIBLE);
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user