From cf81815bf67ee5ea098e863951f9b8faf098f021 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Thu, 16 Nov 2017 15:21:46 -0800 Subject: [PATCH] Add recent chats to top of share list --- res/drawable/contact_list_divider_dark.xml | 19 +++ res/drawable/contact_list_divider_light.xml | 19 +++ res/layout/contact_selection_list_divider.xml | 20 ++++ .../contact_selection_list_fragment.xml | 1 - res/values/attrs.xml | 2 + res/values/strings.xml | 2 + res/values/themes.xml | 4 + .../ContactSelectionListFragment.java | 26 +++-- .../thoughtcrime/securesms/ShareActivity.java | 3 + .../contacts/ContactSelectionListAdapter.java | 108 +++++++++++++++--- .../contacts/ContactSelectionListItem.java | 10 +- .../contacts/ContactsCursorLoader.java | 80 ++++++++----- .../securesms/contacts/ContactsDatabase.java | 17 ++- .../securesms/database/ThreadDatabase.java | 34 +++--- 14 files changed, 258 insertions(+), 87 deletions(-) create mode 100644 res/drawable/contact_list_divider_dark.xml create mode 100644 res/drawable/contact_list_divider_light.xml create mode 100644 res/layout/contact_selection_list_divider.xml diff --git a/res/drawable/contact_list_divider_dark.xml b/res/drawable/contact_list_divider_dark.xml new file mode 100644 index 000000000..09a9f2470 --- /dev/null +++ b/res/drawable/contact_list_divider_dark.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/contact_list_divider_light.xml b/res/drawable/contact_list_divider_light.xml new file mode 100644 index 000000000..984656136 --- /dev/null +++ b/res/drawable/contact_list_divider_light.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/contact_selection_list_divider.xml b/res/layout/contact_selection_list_divider.xml new file mode 100644 index 000000000..f53da3f14 --- /dev/null +++ b/res/layout/contact_selection_list_divider.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/contact_selection_list_fragment.xml b/res/layout/contact_selection_list_fragment.xml index 3d695bb13..27814414e 100644 --- a/res/layout/contact_selection_list_fragment.xml +++ b/res/layout/contact_selection_list_fragment.xml @@ -13,7 +13,6 @@ android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="4dp" android:clipToPadding="false" android:scrollbars="vertical" /> diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 305fefef5..e55e02204 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -132,6 +132,8 @@ + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 45b2a3f31..1ebf58a88 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1249,6 +1249,8 @@ Device no longer registered This is likely because you registered your phone number with Signal on a different device. Tap to re-register. No web browser installed! + Recent chats + Contacts diff --git a/res/values/themes.xml b/res/values/themes.xml index 45ad3fe0d..d03272207 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -143,6 +143,8 @@ @color/gray65 @color/black + @drawable/contact_list_divider_light + @color/gray5 @color/gray12 @@ -256,6 +258,8 @@ #66333333 @drawable/last_seen_divider_text_background_dark + @drawable/contact_list_divider_dark + #ff333333 @drawable/ic_info_outline_dark diff --git a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 211f0a860..0d6bb13d8 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -38,12 +38,13 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.ViewUtil; import java.util.LinkedList; import java.util.List; -import java.util.Map; +import java.util.Set; /** * Fragment for selecting a one or more contacts from a list. @@ -54,11 +55,13 @@ import java.util.Map; public class ContactSelectionListFragment extends Fragment implements LoaderManager.LoaderCallbacks { + @SuppressWarnings("unused") private static final String TAG = ContactSelectionListFragment.class.getSimpleName(); - public final static String DISPLAY_MODE = "display_mode"; - public final static String MULTI_SELECT = "multi_select"; - public final static String REFRESHABLE = "refreshable"; + public static final String DISPLAY_MODE = "display_mode"; + public static final String MULTI_SELECT = "multi_select"; + public static final String REFRESHABLE = "refreshable"; + public static final String RECENTS = "recents"; public final static int DISPLAY_MODE_ALL = ContactsCursorLoader.MODE_ALL; public final static int DISPLAY_MODE_PUSH_ONLY = ContactsCursorLoader.MODE_PUSH_ONLY; @@ -66,7 +69,7 @@ public class ContactSelectionListFragment extends Fragment private TextView emptyText; - private Map selectedContacts; + private Set selectedContacts; private OnContactSelectedListener onContactSelectedListener; private SwipeRefreshLayout swipeRefresh; private String cursorFilter; @@ -108,7 +111,7 @@ public class ContactSelectionListFragment extends Fragment public @NonNull List getSelectedContacts() { List selected = new LinkedList<>(); if (selectedContacts != null) { - selected.addAll(selectedContacts.values()); + selected.addAll(selectedContacts); } return selected; @@ -151,9 +154,9 @@ public class ContactSelectionListFragment extends Fragment @Override public Loader onCreateLoader(int id, Bundle args) { - return new ContactsCursorLoader(getActivity(), + return new ContactsCursorLoader(getActivity(), KeyCachingService.getMasterSecret(getContext()), getActivity().getIntent().getIntExtra(DISPLAY_MODE, DISPLAY_MODE_ALL), - cursorFilter); + cursorFilter, getActivity().getIntent().getBooleanExtra(RECENTS, false)); } @Override @@ -177,13 +180,12 @@ public class ContactSelectionListFragment extends Fragment private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener { @Override public void onItemClick(ContactSelectionListItem contact) { - - if (!isMulti() || !selectedContacts.containsKey(contact.getContactId())) { - selectedContacts.put(contact.getContactId(), contact.getNumber()); + if (!isMulti() || !selectedContacts.contains(contact.getNumber())) { + selectedContacts.add(contact.getNumber()); contact.setChecked(true); if (onContactSelectedListener != null) onContactSelectedListener.onContactSelected(contact.getNumber()); } else { - selectedContacts.remove(contact.getContactId()); + selectedContacts.remove(contact.getNumber()); contact.setChecked(false); if (onContactSelectedListener != null) onContactSelectedListener.onContactDeselected(contact.getNumber()); } diff --git a/src/org/thoughtcrime/securesms/ShareActivity.java b/src/org/thoughtcrime/securesms/ShareActivity.java index c64ceca5c..7037a8029 100644 --- a/src/org/thoughtcrime/securesms/ShareActivity.java +++ b/src/org/thoughtcrime/securesms/ShareActivity.java @@ -100,6 +100,9 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity : ContactSelectionListFragment.DISPLAY_MODE_PUSH_ONLY); } + getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false); + getIntent().putExtra(ContactSelectionListFragment.RECENTS, true); + setContentView(R.layout.share_activity); initializeToolbar(); diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java index ae6f65945..b8d0803b7 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java @@ -43,7 +43,9 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapte import org.thoughtcrime.securesms.util.Util; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * List adapter to display all contacts and their related information @@ -56,6 +58,9 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter selectedContacts = new HashMap<>(); + private final Set selectedContacts = new HashSet<>(); - public static class ViewHolder extends RecyclerView.ViewHolder { - public ViewHolder(@NonNull final View itemView, + public abstract static class ViewHolder extends RecyclerView.ViewHolder { + + public ViewHolder(View itemView) { + super(itemView); + } + + public abstract void bind(@NonNull GlideRequests glideRequests, int type, String name, String number, String label, int color, boolean multiSelect); + public abstract void unbind(@NonNull GlideRequests glideRequests); + public abstract void setChecked(boolean checked); + } + + public static class ContactViewHolder extends ViewHolder { + ContactViewHolder(@NonNull final View itemView, @Nullable final ItemClickListener clickListener) { super(itemView); @@ -80,10 +96,45 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter getSelectedContacts() { + public Set getSelectedContacts() { return selectedContacts; } @@ -169,8 +236,15 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter cursorList = new ArrayList<>(3); + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(getContext()); + ArrayList cursorList = new ArrayList<>(4); + + if (recents && TextUtils.isEmpty(filter)) { + try (Cursor recentConversations = DatabaseFactory.getThreadDatabase(getContext()).getRecentConversationList(5)) { + MatrixCursor synthesizedContacts = new MatrixCursor(CONTACT_PROJECTION); + synthesizedContacts.addRow(new Object[] {getContext().getString(R.string.ContactsCursorLoader_recent_chats), "", ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, "", ContactsDatabase.DIVIDER_TYPE}); + + ThreadDatabase.Reader reader = threadDatabase.readerFor(recentConversations, new MasterCipher(masterSecret)); + + ThreadRecord threadRecord; + + while ((threadRecord = reader.getNext()) != null) { + synthesizedContacts.addRow(new Object[] {threadRecord.getRecipient().toShortString(), + threadRecord.getRecipient().getAddress().serialize(), + ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, + "", ContactsDatabase.RECENT_TYPE}); + } + + synthesizedContacts.addRow(new Object[] {getContext().getString(R.string.ContactsCursorLoader_contacts), "", ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE, "", ContactsDatabase.DIVIDER_TYPE}); + if (synthesizedContacts.getCount() > 2) cursorList.add(synthesizedContacts); + } + } if (mode != MODE_SMS_ONLY) { cursorList.add(contactsDatabase.queryTextSecureContacts(filter)); @@ -74,14 +113,9 @@ public class ContactsCursorLoader extends CursorLoader { } if (!TextUtils.isEmpty(filter) && NumberUtil.isValidSmsOrEmail(filter)) { - MatrixCursor newNumberCursor = new MatrixCursor(new String[] {ContactsDatabase.ID_COLUMN, - ContactsDatabase.NAME_COLUMN, - ContactsDatabase.NUMBER_COLUMN, - ContactsDatabase.NUMBER_TYPE_COLUMN, - ContactsDatabase.LABEL_COLUMN, - ContactsDatabase.CONTACT_TYPE_COLUMN}, 1); + MatrixCursor newNumberCursor = new MatrixCursor(CONTACT_PROJECTION, 1); - newNumberCursor.addRow(new Object[] {-1L, getContext().getString(R.string.contact_selection_list__unknown_contact), + newNumberCursor.addRow(new Object[] {getContext().getString(R.string.contact_selection_list__unknown_contact), filter, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, "\u21e2", ContactsDatabase.NEW_TYPE}); @@ -94,19 +128,13 @@ public class ContactsCursorLoader extends CursorLoader { private @NonNull Cursor filterNonPushContacts(@NonNull Cursor cursor) { try { final long startMillis = System.currentTimeMillis(); - final MatrixCursor matrix = new MatrixCursor(new String[]{ContactsDatabase.ID_COLUMN, - ContactsDatabase.NAME_COLUMN, - ContactsDatabase.NUMBER_COLUMN, - ContactsDatabase.NUMBER_TYPE_COLUMN, - ContactsDatabase.LABEL_COLUMN, - ContactsDatabase.CONTACT_TYPE_COLUMN}); + final MatrixCursor matrix = new MatrixCursor(CONTACT_PROJECTION); while (cursor.moveToNext()) { final String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN)); final Recipient recipient = Recipient.from(getContext(), Address.fromExternal(getContext(), number), false); if (recipient.resolve().getRegistered() != RecipientDatabase.RegisteredState.REGISTERED) { - matrix.addRow(new Object[]{cursor.getLong(cursor.getColumnIndexOrThrow(ContactsDatabase.ID_COLUMN)), - cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN)), + matrix.addRow(new Object[]{cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN)), number, cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN)), cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.LABEL_COLUMN)), diff --git a/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java b/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java index 378aa5afe..ea64d7b31 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java @@ -62,16 +62,17 @@ public class ContactsDatabase { private static final String CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call"; private static final String SYNC = "__TS"; - static final String ID_COLUMN = "_id"; static final String NAME_COLUMN = "name"; static final String NUMBER_COLUMN = "number"; static final String NUMBER_TYPE_COLUMN = "number_type"; static final String LABEL_COLUMN = "label"; static final String CONTACT_TYPE_COLUMN = "contact_type"; - static final int NORMAL_TYPE = 0; - static final int PUSH_TYPE = 1; - static final int NEW_TYPE = 2; + static final int NORMAL_TYPE = 0; + static final int PUSH_TYPE = 1; + static final int NEW_TYPE = 2; + static final int RECENT_TYPE = 3; + static final int DIVIDER_TYPE = 4; private final Context context; @@ -142,8 +143,7 @@ public class ContactsDatabase { uri = uri.buildUpon().appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true").build(); } - String[] projection = new String[]{ContactsContract.CommonDataKinds.Phone._ID, - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, + String[] projection = new String[]{ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.LABEL}; @@ -151,7 +151,6 @@ public class ContactsDatabase { String sort = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; Map projectionMap = new HashMap() {{ - put(ID_COLUMN, ContactsContract.CommonDataKinds.Phone._ID); put(NAME_COLUMN, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME); put(NUMBER_COLUMN, ContactsContract.CommonDataKinds.Phone.NUMBER); put(NUMBER_TYPE_COLUMN, ContactsContract.CommonDataKinds.Phone.TYPE); @@ -180,14 +179,12 @@ public class ContactsDatabase { } @NonNull Cursor queryTextSecureContacts(String filter) { - String[] projection = new String[] {ContactsContract.Data._ID, - ContactsContract.Contacts.DISPLAY_NAME, + String[] projection = new String[] {ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.Data.DATA1}; String sort = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; Map projectionMap = new HashMap(){{ - put(ID_COLUMN, ContactsContract.Data._ID); put(NAME_COLUMN, ContactsContract.Contacts.DISPLAY_NAME); put(NUMBER_COLUMN, ContactsContract.Data.DATA1); }}; diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java index abf15b396..81eeef01b 100644 --- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -288,16 +288,6 @@ public class ThreadDatabase extends Database { }}; } -// public void setUnread(long threadId, int unreadCount) { -// ContentValues contentValues = new ContentValues(1); -// contentValues.put(READ, 0); -// contentValues.put(UNREAD_COUNT, unreadCount); -// -// SQLiteDatabase db = databaseHelper.getWritableDatabase(); -// db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""}); -// notifyConversationListListeners(); -// } - public void incrementUnread(long threadId, int amount) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " + @@ -351,7 +341,7 @@ public class ThreadDatabase extends Database { selectionArgs[i++] = DelimiterUtil.escape(address.serialize(), ' '); } - String query = createQuery(selection); + String query = createQuery(selection, 0); cursors.add(db.rawQuery(query, selectionArgs)); } @@ -360,6 +350,13 @@ public class ThreadDatabase extends Database { return cursor; } + public Cursor getRecentConversationList(int limit) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = createQuery(MESSAGE_COUNT + " != 0", limit); + + return db.rawQuery(query, null); + } + public Cursor getConversationList() { return getConversationList("0"); } @@ -370,7 +367,7 @@ public class ThreadDatabase extends Database { private Cursor getConversationList(String archived) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0"); + String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", 0); Cursor cursor = db.rawQuery(query, new String[]{archived}); setNotifyConverationListListeners(cursor); @@ -380,7 +377,7 @@ public class ThreadDatabase extends Database { public Cursor getDirectShareList() { SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = createQuery(MESSAGE_COUNT + " != 0"); + String query = createQuery(MESSAGE_COUNT + " != 0", 0); return db.rawQuery(query, null); } @@ -598,15 +595,22 @@ public class ThreadDatabase extends Database { return thumbnail != null ? thumbnail.getThumbnailUri() : null; } - private @NonNull String createQuery(@NonNull String where) { + private @NonNull String createQuery(@NonNull String where, int limit) { String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ","); - return "SELECT " + projection + " FROM " + TABLE_NAME + + String query = + "SELECT " + projection + " FROM " + TABLE_NAME + " LEFT OUTER JOIN " + RecipientDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ADDRESS + " = " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.ADDRESS + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ADDRESS + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID + " WHERE " + where + " ORDER BY " + TABLE_NAME + "." + DATE + " DESC"; + + if (limit > 0) { + query += " LIMIT " + limit; + } + + return query; } public interface ProgressListener {