diff --git a/res/layout/load_more_header.xml b/res/layout/load_more_header.xml
new file mode 100644
index 0000000000..99cae04828
--- /dev/null
+++ b/res/layout/load_more_header.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c1beed93c1..709931fdbe 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -640,6 +640,9 @@
Import a plaintext backup file. Compatible with \'SMSBackup And Restore.\'
+
+ Load full conversation...
+
No images
diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java
index 730c1499f8..94d310385e 100644
--- a/src/org/thoughtcrime/securesms/ConversationAdapter.java
+++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java
@@ -113,7 +113,7 @@ public class ConversationAdapter
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
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
return new ViewHolder(itemView);
}
- @Override public void onViewRecycled(ViewHolder holder) {
+ @Override public void onItemViewRecycled(ViewHolder holder) {
holder.getView().unbind();
}
diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java
index b9de105cf3..4a81544bcd 100644
--- a/src/org/thoughtcrime/securesms/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/ConversationFragment.java
@@ -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 onCreateLoader(int arg0, Bundle arg1) {
- return new ConversationLoader(getActivity(), threadId);
+ public Loader onCreateLoader(int id, Bundle args) {
+ return new ConversationLoader(getActivity(), threadId, args.getLong("limit", PARTIAL_CONVERSATION_LIMIT));
}
@Override
- public void onLoadFinished(Loader arg0, Cursor cursor) {
+ public void onLoadFinished(Loader 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);
}
}
diff --git a/src/org/thoughtcrime/securesms/ConversationListAdapter.java b/src/org/thoughtcrime/securesms/ConversationListAdapter.java
index 0fb908b7e9..ceba38875f 100644
--- a/src/org/thoughtcrime/securesms/ConversationListAdapter.java
+++ b/src/org/thoughtcrime/securesms/ConversationListAdapter.java
@@ -95,17 +95,17 @@ public class ConversationListAdapter extends CursorRecyclerViewAdapter {
}
@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);
diff --git a/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java b/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java
index 6855cd8bc6..749b3a0ca9 100644
--- a/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java
+++ b/src/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java
@@ -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 extends RecyclerView.Adapter {
- private final Context context;
+public abstract class CursorRecyclerViewAdapter extends RecyclerView.Adapter {
+ 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 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,
diff --git a/src/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java b/src/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java
index 8b0c2f67d5..292ff3e6f1 100644
--- a/src/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java
+++ b/src/org/thoughtcrime/securesms/database/loaders/ConversationLoader.java
@@ -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);
}
}
diff --git a/test/unitTest/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.java b/test/unitTest/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.java
new file mode 100644
index 0000000000..ec61223c36
--- /dev/null
+++ b/test/unitTest/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapterTest.java
@@ -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);
+ }
+}
+