only load partial conversation by default
Closes #4252 Fixes #3911 // FREEBIE
This commit is contained in:
parent
4a3faf9086
commit
ae97495c47
10 changed files with 224 additions and 46 deletions
9
res/layout/load_more_header.xml
Normal file
9
res/layout/load_more_header.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="?conversation_item_bubble_background"
|
||||
android:textColor="?conversation_item_sent_text_primary_color"
|
||||
android:text="@string/load_more_header__load_full_conversation" />
|
||||
|
|
@ -640,6 +640,9 @@
|
|||
<string name="import_fragment__import_a_plaintext_backup_file">
|
||||
Import a plaintext backup file. Compatible with \'SMSBackup And Restore.\'</string>
|
||||
|
||||
<!-- load_more_header -->
|
||||
<string name="load_more_header__load_full_conversation">Load full conversation...</string>
|
||||
|
||||
<!-- media_overview_activity -->
|
||||
<string name="media_overview_activity__no_images">No images</string>
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||
super.changeCursor(cursor);
|
||||
}
|
||||
|
||||
@Override public void onBindViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
@Override public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
|
||||
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
|
||||
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
|
||||
|
@ -121,7 +121,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||
viewHolder.getView().bind(masterSecret, messageRecord, locale, batchSelected, groupThread);
|
||||
}
|
||||
|
||||
@Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
@Override public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
final V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType));
|
||||
if (viewType == MESSAGE_TYPE_INCOMING || viewType == MESSAGE_TYPE_OUTGOING) {
|
||||
itemView.setOnClickListener(new OnClickListener() {
|
||||
|
@ -142,7 +142,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||
return new ViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override public void onViewRecycled(ViewHolder holder) {
|
||||
@Override public void onItemViewRecycled(ViewHolder holder) {
|
||||
holder.getView().unbind();
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.view.Menu;
|
|||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.widget.Toast;
|
||||
|
@ -58,6 +59,8 @@ public class ConversationFragment extends Fragment
|
|||
{
|
||||
private static final String TAG = ConversationFragment.class.getSimpleName();
|
||||
|
||||
private static final long PARTIAL_CONVERSATION_LIMIT = 500L;
|
||||
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener();
|
||||
|
||||
|
@ -69,6 +72,7 @@ public class ConversationFragment extends Fragment
|
|||
private ActionMode actionMode;
|
||||
private Locale locale;
|
||||
private RecyclerView list;
|
||||
private View loadMoreView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
|
@ -81,6 +85,18 @@ public class ConversationFragment extends Fragment
|
|||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
|
||||
list = ViewUtil.findById(view, android.R.id.list);
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, true);
|
||||
list.setHasFixedSize(false);
|
||||
list.setLayoutManager(layoutManager);
|
||||
|
||||
loadMoreView = inflater.inflate(R.layout.load_more_header, container, false);
|
||||
loadMoreView.setOnClickListener(new OnClickListener() {
|
||||
@Override public void onClick(View v) {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("limit", 0);
|
||||
getLoaderManager().restartLoader(0, args, ConversationFragment.this);
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
|
@ -88,13 +104,6 @@ public class ConversationFragment extends Fragment
|
|||
public void onActivityCreated(Bundle bundle) {
|
||||
super.onActivityCreated(bundle);
|
||||
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
|
||||
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||
layoutManager.setReverseLayout(true);
|
||||
list.setHasFixedSize(false);
|
||||
list.setScrollContainer(true);
|
||||
list.setLayoutManager(layoutManager);
|
||||
|
||||
initializeResources();
|
||||
initializeListAdapter();
|
||||
}
|
||||
|
@ -123,20 +132,20 @@ public class ConversationFragment extends Fragment
|
|||
initializeListAdapter();
|
||||
|
||||
if (threadId == -1) {
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
this.recipients = RecipientFactory.getRecipientsForIds(getActivity(), getActivity().getIntent().getLongArrayExtra("recipients"), true);
|
||||
this.threadId = this.getActivity().getIntent().getLongExtra("thread_id", -1);
|
||||
this.recipients = RecipientFactory.getRecipientsForIds(getActivity(), getActivity().getIntent().getLongArrayExtra("recipients"), true);
|
||||
this.threadId = this.getActivity().getIntent().getLongExtra("thread_id", -1);
|
||||
}
|
||||
|
||||
private void initializeListAdapter() {
|
||||
if (this.recipients != null && this.threadId != -1) {
|
||||
list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null,
|
||||
(!this.recipients.isSingleRecipient()) || this.recipients.isGroupRecipient()));
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,13 +325,18 @@ public class ConversationFragment extends Fragment
|
|||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return new ConversationLoader(getActivity(), threadId);
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new ConversationLoader(getActivity(), threadId, args.getLong("limit", PARTIAL_CONVERSATION_LIMIT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) {
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
if (list.getAdapter() != null) {
|
||||
if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && ((ConversationLoader)loader).hasLimit()) {
|
||||
getListAdapter().setFooterView(loadMoreView);
|
||||
} else {
|
||||
getListAdapter().setFooterView(null);
|
||||
}
|
||||
getListAdapter().changeCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,17 +95,17 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter<Conversat
|
|||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
return new ViewHolder((ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view,
|
||||
parent, false), clickListener);
|
||||
}
|
||||
|
||||
@Override public void onViewRecycled(ViewHolder holder) {
|
||||
@Override public void onItemViewRecycled(ViewHolder holder) {
|
||||
holder.getItem().unbind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor, masterCipher);
|
||||
ThreadRecord record = reader.getCurrent();
|
||||
|
||||
|
|
|
@ -57,13 +57,13 @@ public class ImageMediaAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(final ViewGroup viewGroup, final int i) {
|
||||
public ViewHolder onCreateItemViewHolder(final ViewGroup viewGroup, final int i) {
|
||||
final View view = LayoutInflater.from(getContext()).inflate(R.layout.media_overview_item, viewGroup, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final ViewHolder viewHolder, final @NonNull Cursor cursor) {
|
||||
public void onBindItemViewHolder(final ViewHolder viewHolder, final @NonNull Cursor cursor) {
|
||||
final ThumbnailView imageView = viewHolder.imageView;
|
||||
final ImageRecord imageRecord = ImageRecord.from(cursor);
|
||||
|
||||
|
|
|
@ -20,17 +20,34 @@ import android.content.Context;
|
|||
import android.database.Cursor;
|
||||
import android.database.DataSetObserver;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.util.VisibleForTesting;
|
||||
|
||||
/**
|
||||
* RecyclerView.Adapter that manages a Cursor, comparable to the CursorAdapter usable in ListView/GridView.
|
||||
*/
|
||||
public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
|
||||
private final Context context;
|
||||
public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
private final Context context;
|
||||
private final DataSetObserver observer = new AdapterDataSetObserver();
|
||||
|
||||
private Cursor cursor;
|
||||
private boolean valid;
|
||||
@VisibleForTesting final static int HEADER_TYPE = Integer.MIN_VALUE;
|
||||
@VisibleForTesting final static int FOOTER_TYPE = Integer.MIN_VALUE + 1;
|
||||
|
||||
private Cursor cursor;
|
||||
private boolean valid;
|
||||
private @Nullable View header;
|
||||
private @Nullable View footer;
|
||||
|
||||
private static class HeaderFooterViewHolder extends RecyclerView.ViewHolder {
|
||||
public HeaderFooterViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
protected CursorRecyclerViewAdapter(Context context, Cursor cursor) {
|
||||
this.context = context;
|
||||
|
@ -39,18 +56,32 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
|||
valid = true;
|
||||
cursor.registerDataSetObserver(observer);
|
||||
}
|
||||
|
||||
setHasStableIds(false);
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
protected @NonNull Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public Cursor getCursor() {
|
||||
public @Nullable Cursor getCursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public void setHeaderView(@Nullable View header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
public void setFooterView(@Nullable View footer) {
|
||||
this.footer = footer;
|
||||
}
|
||||
|
||||
public boolean hasHeaderView() {
|
||||
return header != null;
|
||||
}
|
||||
|
||||
public boolean hasFooterView() {
|
||||
return footer != null;
|
||||
}
|
||||
|
||||
public void changeCursor(Cursor cursor) {
|
||||
Cursor old = swapCursor(cursor);
|
||||
if (old != null) {
|
||||
|
@ -78,29 +109,50 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
|||
return oldCursor;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return isActiveCursor() ? cursor.getCount() : 0;
|
||||
if (!isActiveCursor()) return 0;
|
||||
|
||||
return cursor.getCount()
|
||||
+ (hasHeaderView() ? 1 : 0)
|
||||
+ (hasFooterView() ? 1 : 0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return isActiveCursor() && cursor.moveToPosition(position)
|
||||
? cursor.getLong(cursor.getColumnIndexOrThrow("_id"))
|
||||
: 0;
|
||||
public final void onViewRecycled(ViewHolder holder) {
|
||||
if (!(holder instanceof HeaderFooterViewHolder)) {
|
||||
onItemViewRecycled((VH)holder);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void onBindViewHolder(VH viewHolder, @NonNull Cursor cursor);
|
||||
public void onItemViewRecycled(VH holder) {}
|
||||
|
||||
@Override public final ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case HEADER_TYPE: return new HeaderFooterViewHolder(header);
|
||||
case FOOTER_TYPE: return new HeaderFooterViewHolder(footer);
|
||||
default: return onCreateItemViewHolder(parent, viewType);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onBindViewHolder(VH viewHolder, int position) {
|
||||
moveToPositionOrThrow(position);
|
||||
onBindViewHolder(viewHolder, cursor);
|
||||
public final void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (!isHeaderPosition(position) && !isFooterPosition(position)) {
|
||||
moveToPositionOrThrow(getCursorPosition(position));
|
||||
onBindItemViewHolder((VH)viewHolder, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor);
|
||||
|
||||
@Override public int getItemViewType(int position) {
|
||||
moveToPositionOrThrow(position);
|
||||
if (isHeaderPosition(position)) return HEADER_TYPE;
|
||||
if (isFooterPosition(position)) return FOOTER_TYPE;
|
||||
moveToPositionOrThrow(getCursorPosition(position));
|
||||
return getItemViewType(cursor);
|
||||
}
|
||||
|
||||
|
@ -125,6 +177,18 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
|||
return valid && cursor != null;
|
||||
}
|
||||
|
||||
private boolean isFooterPosition(int position) {
|
||||
return hasFooterView() && position == getItemCount() - 1;
|
||||
}
|
||||
|
||||
private boolean isHeaderPosition(int position) {
|
||||
return hasHeaderView() && position == 0;
|
||||
}
|
||||
|
||||
private int getCursorPosition(int position) {
|
||||
return hasHeaderView() ? position - 1 : position;
|
||||
}
|
||||
|
||||
private class AdapterDataSetObserver extends DataSetObserver {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
|
|
|
@ -42,7 +42,7 @@ public class MmsSmsDatabase extends Database {
|
|||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public Cursor getConversation(long threadId) {
|
||||
public Cursor getConversation(long threadId, long limit) {
|
||||
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
|
||||
MmsSmsColumns.THREAD_ID,
|
||||
SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT,
|
||||
|
@ -60,12 +60,16 @@ public class MmsSmsDatabase extends Database {
|
|||
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
|
||||
Cursor cursor = queryTables(projection, selection, selection, order, null, null);
|
||||
Cursor cursor = queryTables(projection, selection, selection, order, null, limit > 0 ? String.valueOf(limit) : null);
|
||||
setNotifyConverationListeners(cursor, threadId);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public Cursor getConversation(long threadId) {
|
||||
return getConversation(threadId, 0);
|
||||
}
|
||||
|
||||
public Cursor getIdentityConflictMessagesForThread(long threadId) {
|
||||
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
|
||||
MmsSmsColumns.THREAD_ID,
|
||||
|
|
|
@ -7,15 +7,21 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
|
||||
|
||||
public class ConversationLoader extends AbstractCursorLoader {
|
||||
private final long threadId;
|
||||
private final long threadId;
|
||||
private long limit;
|
||||
|
||||
public ConversationLoader(Context context, long threadId) {
|
||||
public ConversationLoader(Context context, long threadId, long limit) {
|
||||
super(context);
|
||||
this.threadId = threadId;
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public boolean hasLimit() {
|
||||
return limit > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId);
|
||||
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId, limit);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CursorRecyclerViewAdapterTest {
|
||||
private CursorRecyclerViewAdapter adapter;
|
||||
private Context context;
|
||||
private Cursor cursor;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = mock(Context.class);
|
||||
cursor = mock(Cursor.class);
|
||||
when(cursor.getCount()).thenReturn(100);
|
||||
when(cursor.moveToPosition(anyInt())).thenReturn(true);
|
||||
|
||||
adapter = new CursorRecyclerViewAdapter(context, cursor) {
|
||||
@Override
|
||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSanityCount() throws Exception {
|
||||
assertEquals(adapter.getItemCount(), 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeaderCount() throws Exception {
|
||||
adapter.setHeaderView(new View(context));
|
||||
assertEquals(adapter.getItemCount(), 101);
|
||||
|
||||
assertEquals(adapter.getItemViewType(0), CursorRecyclerViewAdapter.HEADER_TYPE);
|
||||
assertNotEquals(adapter.getItemViewType(1), CursorRecyclerViewAdapter.HEADER_TYPE);
|
||||
assertNotEquals(adapter.getItemViewType(100), CursorRecyclerViewAdapter.HEADER_TYPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFooterCount() throws Exception {
|
||||
adapter.setFooterView(new View(context));
|
||||
assertEquals(adapter.getItemCount(), 101);
|
||||
assertEquals(adapter.getItemViewType(100), CursorRecyclerViewAdapter.FOOTER_TYPE);
|
||||
assertNotEquals(adapter.getItemViewType(0), CursorRecyclerViewAdapter.FOOTER_TYPE);
|
||||
assertNotEquals(adapter.getItemViewType(99), CursorRecyclerViewAdapter.FOOTER_TYPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeaderFooterCount() throws Exception {
|
||||
adapter.setHeaderView(new View(context));
|
||||
adapter.setFooterView(new View(context));
|
||||
assertEquals(adapter.getItemCount(), 102);
|
||||
assertEquals(adapter.getItemViewType(101), CursorRecyclerViewAdapter.FOOTER_TYPE);
|
||||
assertEquals(adapter.getItemViewType(0), CursorRecyclerViewAdapter.HEADER_TYPE);
|
||||
assertNotEquals(adapter.getItemViewType(1), CursorRecyclerViewAdapter.HEADER_TYPE);
|
||||
assertNotEquals(adapter.getItemViewType(100), CursorRecyclerViewAdapter.FOOTER_TYPE);
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue