Transition conversation loading from a Loader to a Repository.

This commit is contained in:
Greyson Parrelli 2020-04-24 17:43:49 -04:00
parent 2b65916344
commit e1a90bcb00
5 changed files with 348 additions and 200 deletions

View file

@ -0,0 +1,82 @@
package org.thoughtcrime.securesms.conversation;
import android.database.Cursor;
import androidx.annotation.NonNull;
public final class ConversationData {
private final Cursor cursor;
private final int offset;
private final int limit;
private final long lastSeen;
private final int previousOffset;
private final boolean firstLoad;
private final boolean hasSent;
private final boolean isMessageRequestAccepted;
private final boolean hasPreMessageRequestMessages;
public ConversationData(Cursor cursor,
int offset,
int limit,
long lastSeen,
int previousOffset,
boolean firstLoad,
boolean hasSent,
boolean isMessageRequestAccepted,
boolean hasPreMessageRequestMessages)
{
this.cursor = cursor;
this.offset = offset;
this.limit = limit;
this.lastSeen = lastSeen;
this.previousOffset = previousOffset;
this.firstLoad = firstLoad;
this.hasSent = hasSent;
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.hasPreMessageRequestMessages = hasPreMessageRequestMessages;
}
public @NonNull Cursor getCursor() {
return cursor;
}
public boolean hasLimit() {
return limit > 0;
}
public int getLimit() {
return limit;
}
public boolean hasOffset() {
return offset > 0;
}
public int getOffset() {
return offset;
}
public int getPreviousOffset() {
return previousOffset;
}
public long getLastSeen() {
return lastSeen;
}
public boolean isFirstLoad() {
return firstLoad;
}
public boolean hasSent() {
return hasSent;
}
public boolean isMessageRequestAccepted() {
return isMessageRequestAccepted;
}
public boolean hasPreMessageRequestMessages() {
return hasPreMessageRequestMessages;
}
}

View file

@ -51,6 +51,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.text.HtmlCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
@ -81,7 +82,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
@ -135,9 +135,7 @@ import java.util.Locale;
import java.util.Set;
@SuppressLint("StaticFieldLeak")
public class ConversationFragment extends Fragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
public class ConversationFragment extends Fragment {
private static final String TAG = ConversationFragment.class.getSimpleName();
private static final String KEY_LIMIT = "limit";
@ -152,11 +150,6 @@ public class ConversationFragment extends Fragment
private LiveRecipient recipient;
private long threadId;
private long lastSeen;
private int startingPosition;
private int previousOffset;
private int activeOffset;
private boolean firstLoad;
private boolean isReacting;
private ActionMode actionMode;
private Locale locale;
@ -172,6 +165,7 @@ public class ConversationFragment extends Fragment
private ConversationBannerView conversationBanner;
private ConversationBannerView emptyConversationBanner;
private MessageRequestViewModel messageRequestViewModel;
private ConversationViewModel conversationViewModel;
@Override
public void onCreate(Bundle icicle) {
@ -214,6 +208,9 @@ public class ConversationFragment extends Fragment
setupListLayoutListeners();
this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class);
conversationViewModel.getConversation().observe(this, this::presentConversation);
return view;
}
@ -295,16 +292,16 @@ public class ConversationFragment extends Fragment
initializeListAdapter();
if (threadId == -1) {
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
conversationViewModel.refreshConversation();
}
}
public void reloadList() {
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
conversationViewModel.refreshConversation();
}
public void moveToLastSeen() {
if (lastSeen <= 0) {
if (conversationViewModel.getLastSeen() <= 0) {
Log.i(TAG, "No need to move to last seen.");
return;
}
@ -314,7 +311,7 @@ public class ConversationFragment extends Fragment
return;
}
int position = getListAdapter().findLastSeenPosition(lastSeen);
int position = getListAdapter().findLastSeenPosition(conversationViewModel.getLastSeen());
scrollToLastSeenPosition(position);
}
@ -403,13 +400,17 @@ public class ConversationFragment extends Fragment
private void initializeResources() {
long oldThreadId = threadId;
long lastSeen = this.getActivity().getIntent().getLongExtra(ConversationActivity.LAST_SEEN_EXTRA, -1);
int startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
int limit = getArguments() != null ? getArguments().getInt(KEY_LIMIT, PARTIAL_CONVERSATION_LIMIT) : PARTIAL_CONVERSATION_LIMIT;
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
this.lastSeen = this.getActivity().getIntent().getLongExtra(ConversationActivity.LAST_SEEN_EXTRA, -1);
this.startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
this.firstLoad = true;
this.unknownSenderView = new UnknownSenderView(getActivity(), recipient.get(), threadId);
conversationViewModel.onConversationDataAvailable(recipient.get(), threadId, lastSeen, startingPosition, limit);
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
list.addOnScrollListener(scrollListener);
@ -425,8 +426,7 @@ public class ConversationFragment extends Fragment
list.setAdapter(adapter);
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
setLastSeen(lastSeen);
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
setLastSeen(conversationViewModel.getLastSeen());
emptyConversationBanner.setVisibility(View.GONE);
} else if (FeatureFlags.messageRequests() && threadId == -1) {
@ -436,9 +436,7 @@ public class ConversationFragment extends Fragment
private void initializeLoadMoreView(ViewSwitcher loadMoreView) {
loadMoreView.setOnClickListener(v -> {
Bundle args = new Bundle();
args.putInt(KEY_LIMIT, 0);
getLoaderManager().restartLoader(0, args, ConversationFragment.this);
conversationViewModel.onLoadMoreClicked();
loadMoreView.showNext();
loadMoreView.setOnClickListener(null);
});
@ -565,7 +563,8 @@ public class ConversationFragment extends Fragment
}
public void setLastSeen(long lastSeen) {
this.lastSeen = lastSeen;
conversationViewModel.onLastSeenChanged(lastSeen);
if (lastSeenDecoration != null) {
list.removeItemDecoration(lastSeenDecoration);
}
@ -827,109 +826,12 @@ public class ConversationFragment extends Fragment
});
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
Log.i(TAG, "onCreateLoader");
int limit = args.getInt(KEY_LIMIT, PARTIAL_CONVERSATION_LIMIT);
int offset = 0;
if (limit != 0 && startingPosition >= limit) {
offset = Math.max(startingPosition - (limit / 2) + 1, 0);
startingPosition -= offset - 1;
}
return new ConversationLoader(getActivity(), threadId, offset, limit, lastSeen);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> cursorLoader, Cursor cursor) {
int count = cursor.getCount();
ConversationLoader loader = (ConversationLoader) cursorLoader;
ConversationAdapter adapter = getListAdapter();
if (adapter == null) {
return;
}
if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && loader.hasLimit()) {
adapter.setFooterView(topLoadMoreView);
} else if (FeatureFlags.messageRequests()) {
adapter.setFooterView(conversationBanner);
} else {
adapter.setFooterView(null);
}
if (lastSeen == -1) {
setLastSeen(loader.getLastSeen());
}
if (FeatureFlags.messageRequests() && !loader.hasPreMessageRequestMessages()) {
clearHeaderIfNotTyping(adapter);
} else {
if (!loader.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
adapter.setHeaderView(unknownSenderView);
} else {
clearHeaderIfNotTyping(adapter);
}
}
if (loader.hasOffset()) {
adapter.setHeaderView(bottomLoadMoreView);
}
if (firstLoad || loader.hasOffset()) {
previousOffset = loader.getOffset();
}
activeOffset = loader.getOffset();
adapter.changeCursor(cursor);
listener.onCursorChanged();
int lastSeenPosition = adapter.findLastSeenPosition(lastSeen);
if (isTypingIndicatorShowing()) {
lastSeenPosition = Math.max(lastSeenPosition - 1, 0);
}
if (firstLoad) {
if (startingPosition >= 0) {
scrollToStartingPosition(startingPosition);
} else if (loader.isMessageRequestAccepted()) {
scrollToLastSeenPosition(lastSeenPosition);
} else if (FeatureFlags.messageRequests()) {
list.post(() -> getListLayoutManager().scrollToPosition(adapter.getItemCount() - 1));
}
firstLoad = false;
} else if (previousOffset > 0) {
int scrollPosition = previousOffset + getListLayoutManager().findFirstVisibleItemPosition();
scrollPosition = Math.min(scrollPosition, count - 1);
View firstView = list.getLayoutManager().getChildAt(scrollPosition);
int pixelOffset = (firstView == null) ? 0 : (firstView.getBottom() - list.getPaddingBottom());
getListLayoutManager().scrollToPositionWithOffset(scrollPosition, pixelOffset);
previousOffset = 0;
}
if (lastSeenPosition <= 0) {
setLastSeen(0);
}
}
private void clearHeaderIfNotTyping(ConversationAdapter adapter) {
if (adapter.getHeaderView() != typingView) {
adapter.setHeaderView(null);
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> arg0) {
if (list.getAdapter() != null) {
getListAdapter().changeCursor(null);
listener.onCursorChanged();
}
}
public long stageOutgoingMessage(OutgoingMediaMessage message) {
MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(getContext()).readerFor(message, threadId).getCurrent();
@ -960,6 +862,73 @@ public class ConversationFragment extends Fragment
}
}
private void presentConversation(@NonNull ConversationData conversation) {
Cursor cursor = conversation.getCursor();
int count = cursor.getCount();
ConversationAdapter adapter = getListAdapter();
if (adapter == null) {
return;
}
if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && conversation.hasLimit()) {
adapter.setFooterView(topLoadMoreView);
} else if (FeatureFlags.messageRequests()) {
adapter.setFooterView(conversationBanner);
} else {
adapter.setFooterView(null);
}
if (conversationViewModel.getLastSeen() == -1) {
setLastSeen(conversation.getLastSeen());
}
if (FeatureFlags.messageRequests() && !conversation.hasPreMessageRequestMessages()) {
clearHeaderIfNotTyping(adapter);
} else {
if (!conversation.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
adapter.setHeaderView(unknownSenderView);
} else {
clearHeaderIfNotTyping(adapter);
}
}
if (conversation.hasOffset()) {
adapter.setHeaderView(bottomLoadMoreView);
}
adapter.changeCursor(cursor);
listener.onCursorChanged();
int lastSeenPosition = adapter.findLastSeenPosition(conversationViewModel.getLastSeen());
if (isTypingIndicatorShowing()) {
lastSeenPosition = Math.max(lastSeenPosition - 1, 0);
}
if (conversation.isFirstLoad()) {
if (conversationViewModel.getStartingPosition() >= 0) {
scrollToStartingPosition(conversationViewModel.getStartingPosition());
} else if (conversation.isMessageRequestAccepted()) {
scrollToLastSeenPosition(lastSeenPosition);
} else if (FeatureFlags.messageRequests()) {
list.post(() -> getListLayoutManager().scrollToPosition(adapter.getItemCount() - 1));
}
} else if (conversation.getPreviousOffset() > 0) {
int scrollPosition = conversation.getPreviousOffset() + getListLayoutManager().findFirstVisibleItemPosition();
scrollPosition = Math.min(scrollPosition, count - 1);
View firstView = list.getLayoutManager().getChildAt(scrollPosition);
int pixelOffset = (firstView == null) ? 0 : (firstView.getBottom() - list.getPaddingBottom());
getListLayoutManager().scrollToPositionWithOffset(scrollPosition, pixelOffset);
}
if (lastSeenPosition <= 0) {
setLastSeen(0);
}
}
private void scrollToStartingPosition(final int startingPosition) {
list.post(() -> {
list.getLayoutManager().scrollToPosition(startingPosition);
@ -1004,6 +973,8 @@ public class ConversationFragment extends Fragment
}
private void moveToMessagePosition(int position, @Nullable Runnable onMessageNotFound) {
int activeOffset = conversationViewModel.getActiveOffset();
Log.d(TAG, "Moving to message position: " + position + " activeOffset: " + activeOffset + " cursorCount: " + getListAdapter().getCursorCount());
if (position >= activeOffset && position >= 0 && position < getListAdapter().getCursorCount()) {
@ -1018,9 +989,7 @@ public class ConversationFragment extends Fragment
} else {
Log.i(TAG, "Message was outside of the loaded range. Need to restart the loader.");
firstLoad = true;
startingPosition = position;
getLoaderManager().restartLoader(0, Bundle.EMPTY, ConversationFragment.this);
conversationViewModel.onMoveJumpToMessageOutOfRange(position);
}
}

View file

@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.DatabaseFactory;
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;
public class ConversationRepository {
private final Context context;
private final Executor executor;
public ConversationRepository() {
this.context = ApplicationDependencies.getApplication();
this.executor = SignalExecutors.BOUNDED;
}
public void getConversationData(long threadId,
int offset,
int limit,
long lastSeen,
int previousOffset,
boolean firstLoad,
@NonNull Callback<ConversationData> callback)
{
executor.execute(() -> callback.onComplete(getConversationDataInternal(threadId, offset, limit, lastSeen, previousOffset, firstLoad)));
}
private @NonNull ConversationData getConversationDataInternal(long threadId, int offset, int limit, long lastSeen, int previousOffset, boolean firstLoad) {
Pair<Long, Boolean> lastSeenAndHasSent = DatabaseFactory.getThreadDatabase(context).getLastSeenAndHasSent(threadId);
boolean hasSent = lastSeenAndHasSent.second();
if (lastSeen == -1) {
lastSeen = lastSeenAndHasSent.first();
}
boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
boolean hasPreMessageRequestMessages = RecipientUtil.isPreMessageRequestThread(context, threadId);
Cursor cursor = DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId, offset, limit);
return new ConversationData(cursor, offset, limit, lastSeen, previousOffset, firstLoad, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages);
}
interface Callback<E> {
void onComplete(@NonNull E result);
}
}

View file

@ -1,6 +1,11 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.content.Context;
import android.database.ContentObservable;
import android.database.ContentObserver;
import android.database.Cursor;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
@ -8,32 +13,139 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaRepository;
import org.thoughtcrime.securesms.pin.PinState;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.List;
class ConversationViewModel extends ViewModel {
private final Context context;
private final MediaRepository mediaRepository;
private final MutableLiveData<List<Media>> recentMedia;
private static final String TAG = Log.tag(ConversationViewModel.class);
private static final int NO_LIMIT = 0;
private final Application context;
private final MediaRepository mediaRepository;
private final ConversationRepository conversationRepository;
private final MutableLiveData<List<Media>> recentMedia;
private final MutableLiveData<ConversationData> conversation;
private final ContentObserver contentObserver;
private Recipient recipient;
private long threadId;
private boolean firstLoad;
private int requestedLimit;
private long lastSeen;
private int startingPosition;
private int previousOffset;
private boolean contentObserverRegistered;
private ConversationViewModel() {
this.context = ApplicationDependencies.getApplication();
this.mediaRepository = new MediaRepository();
this.recentMedia = new MutableLiveData<>();
this.context = ApplicationDependencies.getApplication();
this.mediaRepository = new MediaRepository();
this.conversationRepository = new ConversationRepository();
this.recentMedia = new MutableLiveData<>();
this.conversation = new MutableLiveData<>();
this.contentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
ConversationData data = conversation.getValue();
if (data != null) {
conversationRepository.getConversationData(threadId, data.getOffset(), data.getLimit(), data.getLastSeen(), data.getPreviousOffset(), data.isFirstLoad(), conversation::postValue);
} else {
Log.w(TAG, "Got a content change, but have no previous data?");
}
}
};
}
void onAttachmentKeyboardOpen() {
mediaRepository.getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, recentMedia::postValue);
}
void onConversationDataAvailable(Recipient recipient, long threadId, long lastSeen, int startingPosition, int limit) {
this.recipient = recipient;
this.threadId = threadId;
this.lastSeen = lastSeen;
this.startingPosition = startingPosition;
this.requestedLimit = limit;
this.firstLoad = true;
if (!contentObserverRegistered) {
context.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadId), true, contentObserver);
contentObserverRegistered = true;
}
refreshConversation();
}
void refreshConversation() {
int limit = requestedLimit;
int offset = 0;
if (requestedLimit != NO_LIMIT && startingPosition >= requestedLimit) {
offset = Math.max(startingPosition - (requestedLimit / 2) + 1, 0);
startingPosition -= offset - 1;
}
conversationRepository.getConversationData(threadId, offset, limit, lastSeen, previousOffset, firstLoad, conversation::postValue);
if (firstLoad) {
firstLoad = false;
}
previousOffset = offset;
}
void onLoadMoreClicked() {
requestedLimit = 0;
refreshConversation();
}
void onMoveJumpToMessageOutOfRange(int startingPosition) {
this.firstLoad = true;
this.startingPosition = startingPosition;
refreshConversation();
}
void onLastSeenChanged(long lastSeen) {
this.lastSeen = lastSeen;
}
@NonNull LiveData<List<Media>> getRecentMedia() {
return recentMedia;
}
@NonNull LiveData<ConversationData> getConversation() {
return conversation;
}
long getLastSeen() {
return lastSeen;
}
int getStartingPosition() {
return startingPosition;
}
int getActiveOffset() {
ConversationData data = conversation.getValue();
return data != null ? data.getOffset() : 0;
}
@Override
protected void onCleared() {
context.getContentResolver().unregisterContentObserver(contentObserver);
contentObserverRegistered = false;
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {

View file

@ -1,72 +0,0 @@
package org.thoughtcrime.securesms.database.loaders;
import android.content.Context;
import android.database.Cursor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
import org.whispersystems.libsignal.util.Pair;
public class ConversationLoader extends AbstractCursorLoader {
private final long threadId;
private int offset;
private int limit;
private long lastSeen;
private boolean hasSent;
private boolean isMessageRequestAccepted;
private boolean hasPreMessageRequestMessages;
public ConversationLoader(Context context, long threadId, int offset, int limit, long lastSeen) {
super(context);
this.threadId = threadId;
this.offset = offset;
this.limit = limit;
this.lastSeen = lastSeen;
this.hasSent = true;
}
public boolean hasLimit() {
return limit > 0;
}
public boolean hasOffset() {
return offset > 0;
}
public int getOffset() {
return offset;
}
public long getLastSeen() {
return lastSeen;
}
public boolean hasSent() {
return hasSent;
}
public boolean isMessageRequestAccepted() {
return isMessageRequestAccepted;
}
public boolean hasPreMessageRequestMessages() {
return hasPreMessageRequestMessages;
}
@Override
public Cursor getCursor() {
Pair<Long, Boolean> lastSeenAndHasSent = DatabaseFactory.getThreadDatabase(context).getLastSeenAndHasSent(threadId);
this.hasSent = lastSeenAndHasSent.second();
if (lastSeen == -1) {
this.lastSeen = lastSeenAndHasSent.first();
}
this.isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
this.hasPreMessageRequestMessages = RecipientUtil.isPreMessageRequestThread(context, threadId);
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId, offset, limit);
}
}