diff --git a/res/drawable/conversation_list_divider_shape.xml b/res/drawable/conversation_list_divider_shape.xml index 4f0dbd5d39..12adb1cfef 100644 --- a/res/drawable/conversation_list_divider_shape.xml +++ b/res/drawable/conversation_list_divider_shape.xml @@ -8,6 +8,7 @@ + - \ No newline at end of file + diff --git a/res/drawable/conversation_list_divider_shape_dark.xml b/res/drawable/conversation_list_divider_shape_dark.xml index 51603a346e..cc259b2440 100644 --- a/res/drawable/conversation_list_divider_shape_dark.xml +++ b/res/drawable/conversation_list_divider_shape_dark.xml @@ -8,6 +8,7 @@ + - \ No newline at end of file + diff --git a/res/layout/conversation_list_fragment.xml b/res/layout/conversation_list_fragment.xml index c37ab878f5..5a20158921 100644 --- a/res/layout/conversation_list_fragment.xml +++ b/res/layout/conversation_list_fragment.xml @@ -7,31 +7,36 @@ android:layout_height="fill_parent" android:orientation="vertical"> - + android:orientation="vertical"> + + + + + + + android:id="@+id/fab" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|right" + android:layout_margin="16dp" + android:src="@drawable/ic_create_white_24dp" + android:focusable="true" + android:contentDescription="@string/conversation_list_fragment__fab_content_description" + fab:fab_colorNormal="?fab_color" + fab:fab_colorPressed="@color/textsecure_primary_dark" + fab:fab_colorRipple="@color/textsecure_primary_dark" /> diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index 861cc7c4ff..25c2c09ddb 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -46,6 +46,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; public class ConversationListActivity extends PassphraseRequiredActionBarActivity implements ConversationListFragment.ConversationSelectedListener { + private static final String TAG = ConversationListActivity.class.getSimpleName(); + private final DynamicTheme dynamicTheme = new DynamicTheme (); private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); @@ -209,15 +211,15 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit @Override public void onChange(boolean selfChange) { super.onChange(selfChange); - Log.w("ConversationListActivity", "detected android contact data changed, refreshing cache"); + Log.w(TAG, "detected android contact data changed, refreshing cache"); // TODO only clear updated recipients from cache RecipientFactory.clearCache(); ConversationListActivity.this.runOnUiThread(new Runnable() { - @Override - public void run() { - ((ConversationListAdapter)fragment.getListAdapter()).notifyDataSetChanged(); - } - }); + @Override + public void run() { + fragment.getListAdapter().notifyDataSetChanged(); + } + }); } }; diff --git a/src/org/thoughtcrime/securesms/ConversationListAdapter.java b/src/org/thoughtcrime/securesms/ConversationListAdapter.java index 7ce87e34c4..acbba4831f 100644 --- a/src/org/thoughtcrime/securesms/ConversationListAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationListAdapter.java @@ -18,14 +18,18 @@ package org.thoughtcrime.securesms; import android.content.Context; import android.database.Cursor; -import android.support.v4.widget.CursorAdapter; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; import android.view.ViewGroup; -import android.widget.AbsListView; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.ThreadRecord; @@ -39,40 +43,65 @@ import java.util.Set; * * @author Moxie Marlinspike */ -public class ConversationListAdapter extends CursorAdapter implements AbsListView.RecyclerListener { +public class ConversationListAdapter extends CursorRecyclerViewAdapter { - private final ThreadDatabase threadDatabase; - private final MasterCipher masterCipher; - private final Context context; - private final LayoutInflater inflater; + private final ThreadDatabase threadDatabase; + private final MasterCipher masterCipher; + private final Context context; + private final LayoutInflater inflater; + private final ItemClickListener clickListener; private final Set batchSet = Collections.synchronizedSet(new HashSet()); private boolean batchMode = false; - public ConversationListAdapter(Context context, Cursor cursor, MasterSecret masterSecret) { - super(context, cursor, 0); + protected static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(final @NonNull ConversationListItem itemView, + final @Nullable ItemClickListener clickListener) { + super(itemView); + itemView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (clickListener != null) clickListener.onItemClick(itemView); + } + }); + itemView.setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + if (clickListener != null) clickListener.onItemLongClick(itemView); + return true; + } + }); + } - if (masterSecret != null) this.masterCipher = new MasterCipher(masterSecret); - else this.masterCipher = null; + public ConversationListItem getItem() { + return (ConversationListItem) itemView; + } + } + public ConversationListAdapter(@NonNull Context context, + @NonNull MasterSecret masterSecret, + @Nullable Cursor cursor, + @Nullable ItemClickListener clickListener) { + super(context, cursor); + this.masterCipher = new MasterCipher(masterSecret); this.context = context; this.threadDatabase = DatabaseFactory.getThreadDatabase(context); this.inflater = LayoutInflater.from(context); + this.clickListener = clickListener; } @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return inflater.inflate(R.layout.conversation_list_item_view, parent, false); + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder((ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view, + parent, false), clickListener); } @Override - public void bindView(View view, Context context, Cursor cursor) { - if (masterCipher != null) { - ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor, masterCipher); - ThreadRecord record = reader.getCurrent(); + public void onBindViewHolder(ViewHolder viewHolder, Cursor cursor) { + ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor, masterCipher); + ThreadRecord record = reader.getCurrent(); - ((ConversationListItem)view).set(record, batchSet, batchMode); - } + viewHolder.getItem().set(record, batchSet, batchMode); } public void toggleThreadInBatchSet(long threadId) { @@ -112,8 +141,8 @@ public class ConversationListAdapter extends CursorAdapter implements AbsListVie this.notifyDataSetChanged(); } - @Override - public void onMovedToScrapHeap(View view) { - ((ConversationListItem)view).unbind(); + public interface ItemClickListener { + void onItemClick(ConversationListItem item); + void onItemLongClick(ConversationListItem item); } } diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java index 84c78b1b75..0c2d81f35a 100644 --- a/src/org/thoughtcrime/securesms/ConversationListFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationListFragment.java @@ -16,19 +16,21 @@ */ package org.thoughtcrime.securesms; -import android.app.Activity; import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.os.AsyncTask; import android.os.Bundle; -import android.support.v4.app.ListFragment; +import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; -import android.support.v4.widget.CursorAdapter; import android.support.v7.app.ActionBarActivity; import android.support.v7.view.ActionMode; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.RecyclerListener; +import android.support.v7.widget.RecyclerView.ViewHolder; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; @@ -37,13 +39,13 @@ import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ListView; import com.afollestad.materialdialogs.AlertDialogWrapper; import com.melnykov.fab.FloatingActionButton; +import org.thoughtcrime.securesms.ConversationListAdapter.ItemClickListener; import org.thoughtcrime.securesms.components.DefaultSmsReminder; +import org.thoughtcrime.securesms.components.DividerItemDecoration; import org.thoughtcrime.securesms.components.ExpiredBuildReminder; import org.thoughtcrime.securesms.components.PushRegistrationReminder; import org.thoughtcrime.securesms.components.ReminderView; @@ -57,15 +59,16 @@ import org.thoughtcrime.securesms.recipients.Recipients; import java.util.Set; -public class ConversationListFragment extends ListFragment - implements LoaderManager.LoaderCallbacks, ActionMode.Callback +public class ConversationListFragment extends Fragment + implements LoaderManager.LoaderCallbacks, ActionMode.Callback, ItemClickListener { - private MasterSecret masterSecret; - private ActionMode actionMode; - private ReminderView reminderView; - private FloatingActionButton fab; - private String queryFilter = ""; + private MasterSecret masterSecret; + private ActionMode actionMode; + private RecyclerView list; + private ReminderView reminderView; + private FloatingActionButton fab; + private String queryFilter = ""; @Override public void onCreate(Bundle icicle) { @@ -76,8 +79,12 @@ public class ConversationListFragment extends ListFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { final View view = inflater.inflate(R.layout.conversation_list_fragment, container, false); - reminderView = new ReminderView(getActivity()); + reminderView = (ReminderView) view.findViewById(R.id.reminder); + list = (RecyclerView) view.findViewById(R.id.list); fab = (FloatingActionButton) view.findViewById(R.id.fab); + list.setHasFixedSize(true); + list.setLayoutManager(new LinearLayoutManager(getActivity())); + list.addItemDecoration(new DividerItemDecoration(getActivity(), LinearLayoutManager.VERTICAL, R.attr.conversation_list_item_divider)); return view; } @@ -86,8 +93,6 @@ public class ConversationListFragment extends ListFragment super.onActivityCreated(bundle); setHasOptionsMenu(true); - getListView().setAdapter(null); - getListView().addHeaderView(reminderView); fab.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -95,7 +100,6 @@ public class ConversationListFragment extends ListFragment } }); initializeListAdapter(); - initializeBatchListener(); } @Override @@ -103,30 +107,11 @@ public class ConversationListFragment extends ListFragment super.onResume(); initializeReminders(); - ((ConversationListAdapter)getListAdapter()).notifyDataSetChanged(); + list.getAdapter().notifyDataSetChanged(); } - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - if (v instanceof ConversationListItem) { - ConversationListItem headerView = (ConversationListItem) v; - if (actionMode == null) { - handleCreateConversation(headerView.getThreadId(), headerView.getRecipients(), - headerView.getDistributionType()); - } else { - ConversationListAdapter adapter = (ConversationListAdapter)getListAdapter(); - adapter.toggleThreadInBatchSet(headerView.getThreadId()); - - if (adapter.getBatchSelections().size() == 0) { - actionMode.finish(); - } else { - actionMode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, - adapter.getBatchSelections().size())); - } - - adapter.notifyDataSetChanged(); - } - } + public ConversationListAdapter getListAdapter() { + return (ConversationListAdapter) list.getAdapter(); } public void setQueryFilter(String query) { @@ -140,22 +125,6 @@ public class ConversationListFragment extends ListFragment } } - private void initializeBatchListener() { - getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - @Override - public boolean onItemLongClick(AdapterView arg0, View v, int position, long id) { - ConversationListAdapter adapter = (ConversationListAdapter)getListAdapter(); - actionMode = ((ActionBarActivity)getActivity()).startSupportActionMode(ConversationListFragment.this); - - adapter.initializeBatchMode(true); - adapter.toggleThreadInBatchSet(((ConversationListItem) v).getThreadId()); - adapter.notifyDataSetChanged(); - - return true; - } - }); - } - private void initializeReminders() { if (ExpiredBuildReminder.isEligible(getActivity())) { reminderView.showReminder(new ExpiredBuildReminder()); @@ -171,8 +140,13 @@ public class ConversationListFragment extends ListFragment } private void initializeListAdapter() { - this.setListAdapter(new ConversationListAdapter(getActivity(), null, masterSecret)); - getListView().setRecyclerListener((ConversationListAdapter)getListAdapter()); + list.setAdapter(new ConversationListAdapter(getActivity(), masterSecret, null, this)); + list.setRecyclerListener(new RecyclerListener() { + @Override + public void onViewRecycled(ViewHolder holder) { + ((ConversationListItem)holder.itemView).unbind(); + } + }); getLoaderManager().restartLoader(0, null, this); } @@ -186,7 +160,7 @@ public class ConversationListFragment extends ListFragment alert.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - final Set selectedConversations = ((ConversationListAdapter)getListAdapter()) + final Set selectedConversations = (getListAdapter()) .getBatchSelections(); if (!selectedConversations.isEmpty()) { @@ -226,7 +200,7 @@ public class ConversationListFragment extends ListFragment } private void handleSelectAllThreads() { - ((ConversationListAdapter)this.getListAdapter()).selectAllThreads(); + getListAdapter().selectAllThreads(); actionMode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, ((ConversationListAdapter)this.getListAdapter()).getBatchSelections().size())); } @@ -242,16 +216,45 @@ public class ConversationListFragment extends ListFragment @Override public void onLoadFinished(Loader arg0, Cursor cursor) { - ((CursorAdapter)getListAdapter()).changeCursor(cursor); + getListAdapter().changeCursor(cursor); } @Override public void onLoaderReset(Loader arg0) { - ((CursorAdapter)getListAdapter()).changeCursor(null); + getListAdapter().changeCursor(null); + } + + @Override + public void onItemClick(ConversationListItem item) { + if (actionMode == null) { + handleCreateConversation(item.getThreadId(), item.getRecipients(), + item.getDistributionType()); + } else { + ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter(); + adapter.toggleThreadInBatchSet(item.getThreadId()); + + if (adapter.getBatchSelections().size() == 0) { + actionMode.finish(); + } else { + actionMode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, + adapter.getBatchSelections().size())); + } + + adapter.notifyDataSetChanged(); + } + } + + @Override + public void onItemLongClick(ConversationListItem item) { + actionMode = ((ActionBarActivity)getActivity()).startSupportActionMode(ConversationListFragment.this); + + getListAdapter().initializeBatchMode(true); + getListAdapter().toggleThreadInBatchSet(item.getThreadId()); + getListAdapter().notifyDataSetChanged(); } public interface ConversationSelectedListener { - public void onCreateConversation(long threadId, Recipients recipients, int distributionType); + void onCreateConversation(long threadId, Recipients recipients, int distributionType); } @Override @@ -282,7 +285,7 @@ public class ConversationListFragment extends ListFragment @Override public void onDestroyActionMode(ActionMode mode) { - ((ConversationListAdapter)getListAdapter()).initializeBatchMode(false); + getListAdapter().initializeBatchMode(false); actionMode = null; } diff --git a/src/org/thoughtcrime/securesms/components/DividerItemDecoration.java b/src/org/thoughtcrime/securesms/components/DividerItemDecoration.java new file mode 100644 index 0000000000..a28727e5e9 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/DividerItemDecoration.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thoughtcrime.securesms.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.AttrRes; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + private static final int DEFAULT_ATTR = android.R.attr.listDivider; + + private Drawable mDivider; + private int mOrientation; + + public DividerItemDecoration(Context context, int orientation) { + this(context, orientation, DEFAULT_ATTR); + } + + public DividerItemDecoration(Context context, int orientation, @AttrRes int attr) { + final TypedArray a = context.obtainStyledAttributes(new int[]{attr}); + mDivider = a.getDrawable(0); + a.recycle(); + setOrientation(orientation); + } + + public void setOrientation(int orientation) { + if (orientation != LinearLayoutManager.HORIZONTAL && orientation != LinearLayoutManager.VERTICAL) { + throw new IllegalArgumentException("invalid orientation"); + } + mOrientation = orientation; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent) { + if (mOrientation == LinearLayoutManager.VERTICAL) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + public void drawVertical(Canvas c, RecyclerView parent) { + final int left = parent.getPaddingLeft(); + final int right = parent.getWidth() - parent.getPaddingRight(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int top = child.getBottom() + params.bottomMargin; + final int bottom = top + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + public void drawHorizontal(Canvas c, RecyclerView parent) { + final int top = parent.getPaddingTop(); + final int bottom = parent.getHeight() - parent.getPaddingBottom(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int left = child.getRight() + params.rightMargin; + final int right = left + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + @Override + public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { + if (mOrientation == LinearLayoutManager.VERTICAL) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +}