From 5b2a399392dbe47c00379c254396cfcbad100703 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 11 Jun 2020 16:55:34 -0400 Subject: [PATCH] Return to previous scroll position when returning to a conversation. --- .../conversation/ConversationAdapter.java | 4 ++ .../conversation/ConversationData.java | 25 ++++++--- .../conversation/ConversationFragment.java | 30 ++++++++--- .../conversation/ConversationRepository.java | 20 ++++--- .../conversation/ConversationViewModel.java | 6 ++- .../securesms/database/MmsSmsDatabase.java | 4 +- .../securesms/database/ThreadDatabase.java | 54 +++++++++++++++---- .../database/helpers/SQLCipherOpenHelper.java | 7 ++- 8 files changed, 115 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index 9e5891fefb9..ee2182f0896 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -510,6 +510,10 @@ private static MessageDigest getMessageDigestOrThrow() { } } + public @Nullable MessageRecord getLastVisibleMessageRecord(int position) { + return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0)); + } + static class ConversationViewHolder extends RecyclerView.ViewHolder { public ConversationViewHolder(final @NonNull V itemView) { super(itemView); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java index 5e6c0a4572d..4a72eb03012 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java @@ -7,6 +7,7 @@ final class ConversationData { private final long threadId; private final long lastSeen; private final int lastSeenPosition; + private final int lastScrolledPosition; private final boolean hasSent; private final boolean isMessageRequestAccepted; private final boolean hasPreMessageRequestMessages; @@ -15,18 +16,20 @@ final class ConversationData { ConversationData(long threadId, long lastSeen, int lastSeenPosition, + int lastScrolledPosition, boolean hasSent, boolean isMessageRequestAccepted, boolean hasPreMessageRequestMessages, int jumpToPosition) { - this.threadId = threadId; - this.lastSeen = lastSeen; - this.lastSeenPosition = lastSeenPosition; - this.hasSent = hasSent; - this.isMessageRequestAccepted = isMessageRequestAccepted; - this.hasPreMessageRequestMessages = hasPreMessageRequestMessages; - this.jumpToPosition = jumpToPosition; + this.threadId = threadId; + this.lastSeen = lastSeen; + this.lastSeenPosition = lastSeenPosition; + this.lastScrolledPosition = lastScrolledPosition; + this.hasSent = hasSent; + this.isMessageRequestAccepted = isMessageRequestAccepted; + this.hasPreMessageRequestMessages = hasPreMessageRequestMessages; + this.jumpToPosition = jumpToPosition; } public long getThreadId() { @@ -41,6 +44,10 @@ int getLastSeenPosition() { return lastSeenPosition; } + int getLastScrolledPosition() { + return lastScrolledPosition; + } + boolean hasSent() { return hasSent; } @@ -57,6 +64,10 @@ boolean shouldJumpToMessage() { return jumpToPosition >= 0; } + boolean shouldScrollToLastSeen() { + return lastSeenPosition > 0; + } + int getJumpToPosition() { return jumpToPosition; } 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 9d2621ddbe4..b14d1a63fce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -284,6 +284,23 @@ public void onStart() { initializeTypingObserver(); } + @Override + public void onPause() { + super.onPause(); + int lastVisiblePosition = getListLayoutManager().findLastVisibleItemPosition(); + int firstVisiblePosition = getListLayoutManager().findFirstCompletelyVisibleItemPosition(); + + final long lastVisibleMessageTimestamp; + if (firstVisiblePosition != 0 && lastVisiblePosition != RecyclerView.NO_POSITION) { + MessageRecord message = getListAdapter().getLastVisibleMessageRecord(lastVisiblePosition); + + lastVisibleMessageTimestamp = message != null ? message.getDateReceived() : 0; + } else { + lastVisibleMessageTimestamp = 0; + } + SignalExecutors.BOUNDED.submit(() -> DatabaseFactory.getThreadDatabase(requireContext()).setLastScrolled(threadId, lastVisibleMessageTimestamp)); + } + @Override public void onStop() { super.onStop(); @@ -312,7 +329,7 @@ public void moveToLastSeen() { } int position = getListAdapter().getAdapterPositionForMessagePosition(conversationViewModel.getLastSeenPosition()); - scrollToLastSeenPosition(position); + scrollToPosition(position); } private void initializeMessageRequestViewModel() { @@ -886,12 +903,13 @@ private void presentConversationMetadata(@NonNull ConversationData conversation) listener.onCursorChanged(); - int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition()); + int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition()); + int lastScrolledPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastScrolledPosition()); if (conversation.shouldJumpToMessage()) { scrollToStartingPosition(conversation.getJumpToPosition()); } else if (conversation.isMessageRequestAccepted()) { - scrollToLastSeenPosition(lastSeenPosition); + scrollToPosition(conversation.shouldScrollToLastSeen() ? lastSeenPosition : lastScrolledPosition); } else if (FeatureFlags.messageRequests()) { list.post(() -> getListLayoutManager().scrollToPosition(adapter.getItemCount() - 1)); } @@ -904,9 +922,9 @@ private void scrollToStartingPosition(int startingPosition) { }); } - private void scrollToLastSeenPosition(int lastSeenPosition) { - if (lastSeenPosition > 0) { - list.post(() -> getListLayoutManager().scrollToPositionWithOffset(lastSeenPosition, list.getHeight())); + private void scrollToPosition(int position) { + if (position > 0) { + list.post(() -> getListLayoutManager().scrollToPositionWithOffset(position, list.getHeight())); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java index 2fa390b91f4..3e71b1fccb8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java @@ -7,10 +7,10 @@ import androidx.lifecycle.MutableLiveData; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; -import org.whispersystems.libsignal.util.Pair; import java.util.concurrent.Executor; @@ -35,23 +35,29 @@ LiveData getConversationData(long threadId, int jumpToPosition } private @NonNull ConversationData getConversationDataInternal(long threadId, int jumpToPosition) { - Pair lastSeenAndHasSent = DatabaseFactory.getThreadDatabase(context).getLastSeenAndHasSent(threadId); + ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId); - long lastSeen = lastSeenAndHasSent.first(); - boolean hasSent = lastSeenAndHasSent.second(); - int lastSeenPosition = 0; + long lastSeen = metadata.getLastSeen(); + boolean hasSent = metadata.hasSent(); + int lastSeenPosition = 0; + long lastScrolled = metadata.getLastScrolled(); + int lastScrolledPosition = 0; boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId); boolean hasPreMessageRequestMessages = RecipientUtil.isPreMessageRequestThread(context, threadId); if (lastSeen > 0) { - lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionForLastSeen(threadId, lastSeen); + lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastSeen); } if (lastSeenPosition <= 0) { lastSeen = 0; } - return new ConversationData(threadId, lastSeen, lastSeenPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition); + if (lastSeen == 0 && lastScrolled > 0) { + lastScrolledPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastScrolled); + } + + return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java index d2624a234a5..cbc6a3ebf5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -70,11 +70,13 @@ private ConversationViewModel() { final int startPosition; if (data.shouldJumpToMessage()) { startPosition = data.getJumpToPosition(); - } else { + } else if (data.shouldScrollToLastSeen()) { startPosition = data.getLastSeenPosition(); + } else { + startPosition = data.getLastScrolledPosition(); } - Log.d(TAG, "Starting at position " + startPosition + " :: " + data.getJumpToPosition() + " :: " + data.getLastSeenPosition()); + Log.d(TAG, "Starting at position startPosition: " + startPosition + " jumpToPosition: " + jumpToPosition + " lastSeenPosition: " + data.getLastSeenPosition() + " lastScrolledPosition: " + data.getLastScrolledPosition()); return Transformations.map(new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationDataSource.EXECUTOR) .setInitialLoadKey(Math.max(startPosition, 0)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 5d401f75fd2..040837df0da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -124,9 +124,9 @@ public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { return new Pair<>(id, latestQuit); } - public int getMessagePositionForLastSeen(long threadId, long lastSeen) { + public int getMessagePositionOnOrAfterTimestamp(long threadId, long timestamp) { String[] projection = new String[] { "COUNT(*)" }; - String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + lastSeen; + String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " >= " + timestamp; try (Cursor cursor = queryTables(projection, selection, null, null)) { if (cursor != null && cursor.moveToNext()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index a62cb98b5f2..6249f5d9f56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -90,6 +90,7 @@ public class ThreadDatabase extends Database { public static final String EXPIRES_IN = "expires_in"; public static final String LAST_SEEN = "last_seen"; public static final String HAS_SENT = "has_sent"; + private static final String LAST_SCROLLED = "last_scrolled"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " + @@ -111,7 +112,8 @@ public class ThreadDatabase extends Database { LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " + READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + - UNREAD_COUNT + " INTEGER DEFAULT 0);"; + UNREAD_COUNT + " INTEGER DEFAULT 0, " + + LAST_SCROLLED + " INTEGER DEFAULT 0);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");", @@ -120,7 +122,7 @@ public class ThreadDatabase extends Database { private static final String[] THREAD_PROJECTION = { ID, DATE, MESSAGE_COUNT, RECIPIENT_ID, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, TYPE, ERROR, SNIPPET_TYPE, - SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT + SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, LAST_SCROLLED }; private static final List TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION) @@ -618,18 +620,26 @@ public void setLastSeen(long threadId) { notifyConversationListListeners(); } - public Pair getLastSeenAndHasSent(long threadId) { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); + public void setLastScrolled(long threadId, long lastScrolledTimestamp) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + ContentValues contentValues = new ContentValues(1); - try { + contentValues.put(LAST_SCROLLED, lastScrolledTimestamp); + + db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)}); + } + + public ConversationMetadata getConversationMetadata(long threadId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + + try (Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT, LAST_SCROLLED}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { - return new Pair<>(cursor.getLong(0), cursor.getLong(1) == 1); + return new ConversationMetadata(cursor.getLong(cursor.getColumnIndexOrThrow(LAST_SEEN)), + cursor.getLong(cursor.getColumnIndexOrThrow(HAS_SENT)) == 1, + cursor.getLong(cursor.getColumnIndexOrThrow(LAST_SCROLLED))); } - return new Pair<>(-1L, false); - } finally { - if (cursor != null) cursor.close(); + return new ConversationMetadata(-1L, false, -1); } } @@ -1060,4 +1070,28 @@ public int serialize() { return value; } } + + public static class ConversationMetadata { + private final long lastSeen; + private final boolean hasSent; + private final long lastScrolled; + + public ConversationMetadata(long lastSeen, boolean hasSent, long lastScrolled) { + this.lastSeen = lastSeen; + this.hasSent = hasSent; + this.lastScrolled = lastScrolled; + } + + public long getLastSeen() { + return lastSeen; + } + + public boolean hasSent() { + return hasSent; + } + + public long getLastScrolled() { + return lastScrolled; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 3636d111081..146a43a0e0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -134,8 +134,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int SERVER_TIMESTAMP = 59; private static final int REMOTE_DELETE = 60; private static final int COLOR_MIGRATION = 61; + private static final int LAST_SCROLLED = 62; - private static final int DATABASE_VERSION = 61; + private static final int DATABASE_VERSION = 62; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -906,6 +907,10 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } } + if (oldVersion < LAST_SCROLLED) { + db.execSQL("ALTER TABLE thread ADD COLUMN last_scrolled INTEGER DEFAULT 0"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction();