From e2f7c1529a9864c51bcb5c3932c6bca9245dd8c7 Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Tue, 1 Apr 2014 16:40:16 -0700 Subject: [PATCH] group and contact list fixes 1) Updating a group without changing the avatar will keep that avatar 2) Prohibit adding non-push users to an existing push group 3) Add Android contacts to the same database. Takes a small amount more time and memory, but allows queries to not be a hack, and enables us to dedupe numbers in JB and higher devices. // FREEBIE --- res/layout/new_conversation_activity.xml | 4 - .../push_contact_selection_list_activity.xml | 28 ++++-- res/values/strings.xml | 3 +- .../securesms/GroupCreateActivity.java | 19 +++- .../securesms/NewConversationActivity.java | 12 +-- .../PushContactSelectionActivity.java | 3 +- .../PushContactSelectionListFragment.java | 40 +++++++- .../securesms/contacts/ContactAccessor.java | 8 +- .../contacts/ContactSelectionListAdapter.java | 1 + .../contacts/ContactsCursorLoader.java | 13 ++- .../securesms/contacts/ContactsDatabase.java | 96 +++++++++++++++---- 11 files changed, 171 insertions(+), 56 deletions(-) diff --git a/res/layout/new_conversation_activity.xml b/res/layout/new_conversation_activity.xml index e289f89b17..e3bf93e1f4 100644 --- a/res/layout/new_conversation_activity.xml +++ b/res/layout/new_conversation_activity.xml @@ -5,10 +5,6 @@ android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android"> - - + + + android:layout_width="match_parent" + android:layout_height="match_parent" /> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center|center_vertical" + android:layout_marginTop="15dp" + android:text="@string/contact_selection_group_activity__finding_contacts" + android:textSize="20sp" /> diff --git a/res/values/strings.xml b/res/values/strings.xml index f1a6f219ce..fc0c87bb7a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -147,7 +147,6 @@ New MMS Group You have selected a contact that doesn\'t support TextSecure groups, so this group will be MMS. You\'re not registered for using the data channel, so TextSecure groups are disabled. - You\'re not the owner of this group, so you cannot edit the title or picture. An unexpected error happened that has made group creation fail. You need at least one person in your group! One of the members of your group has a number that can\'t be read correctly. Please fix or remove that contact and try again. @@ -155,6 +154,7 @@ Group Avatar Create Group Creating %1$s… + Cannot add non-TextSecure contacts to an existing TextSecure group Import System SMS Database? @@ -765,6 +765,7 @@ Unselect All TEXTSECURE USERS ALL CONTACTS + New message to... Finished diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index f83750854f..c0191edc9c 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -162,9 +162,17 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv } private void addSelectedContact(Recipient contact) { + final boolean isPushUser = isActiveInDirectory(this, contact); + if (existingContacts != null && !isPushUser) { + Toast.makeText(getApplicationContext(), + R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group, + Toast.LENGTH_LONG).show(); + return; + } + if (!selectedContacts.contains(contact) && (existingContacts == null || !existingContacts.contains(contact))) selectedContacts.add(contact); - if (!isActiveInDirectory(this, contact)) { + if (!isPushUser) { disableWhisperGroupUi(R.string.GroupCreateActivity_contacts_dont_support_push); getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_mms_title); } @@ -375,6 +383,7 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv @Override public void onClick(View v) { Intent intent = new Intent(GroupCreateActivity.this, PushContactSelectionActivity.class); + if (existingContacts != null) intent.putExtra(PushContactSelectionActivity.PUSH_ONLY_EXTRA, true); startActivityForResult(intent, PICK_CONTACT); } } @@ -533,9 +542,13 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv @Override protected Pair doInBackground(Void... params) { byte[] avatarBytes = null; - if (avatarBmp != null) { + final Bitmap bitmap; + if (avatarBmp == null) bitmap = existingAvatarBmp; + else bitmap = avatarBmp; + + if (bitmap != null) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); - avatarBmp.compress(Bitmap.CompressFormat.PNG, 100, stream); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); avatarBytes = stream.toByteArray(); } final String name = (groupName.getText() != null) ? groupName.getText().toString() : null; diff --git a/src/org/thoughtcrime/securesms/NewConversationActivity.java b/src/org/thoughtcrime/securesms/NewConversationActivity.java index 5e871d1c5d..57ca481d78 100644 --- a/src/org/thoughtcrime/securesms/NewConversationActivity.java +++ b/src/org/thoughtcrime/securesms/NewConversationActivity.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; +import android.widget.EditText; import android.widget.Toast; import com.actionbarsherlock.app.ActionBar; @@ -62,7 +63,6 @@ public class NewConversationActivity extends PassphraseRequiredSherlockFragmentA private final DynamicTheme dynamicTheme = new DynamicTheme(); private MasterSecret masterSecret; - private SingleRecipientPanel recipientsPanel; private PushContactSelectionListFragment contactsFragment; @Override @@ -106,7 +106,6 @@ public class NewConversationActivity extends PassphraseRequiredSherlockFragmentA } private void initializeResources() { - recipientsPanel = (SingleRecipientPanel) findViewById(R.id.recipients); contactsFragment = (PushContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment); contactsFragment.setOnContactSelectedListener(new PushContactSelectionListFragment.OnContactSelectedListener() { @Override @@ -116,15 +115,6 @@ public class NewConversationActivity extends PassphraseRequiredSherlockFragmentA openNewConversation(recipients); } }); - - recipientsPanel.setPanelChangeListener(new SingleRecipientPanel.RecipientsPanelChangedListener() { - @Override - public void onRecipientsPanelUpdate(Recipients recipients) { - Log.i(TAG, "Choosing contact from autocompletion."); - openNewConversation(recipients); - } - }); - } private void handleSelectionFinished() { diff --git a/src/org/thoughtcrime/securesms/PushContactSelectionActivity.java b/src/org/thoughtcrime/securesms/PushContactSelectionActivity.java index dcd38dee37..e61281a899 100644 --- a/src/org/thoughtcrime/securesms/PushContactSelectionActivity.java +++ b/src/org/thoughtcrime/securesms/PushContactSelectionActivity.java @@ -56,7 +56,8 @@ import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; * */ public class PushContactSelectionActivity extends PassphraseRequiredSherlockFragmentActivity { - private final static String TAG = "ContactSelectActivity"; + private final static String TAG = "ContactSelectActivity"; + public final static String PUSH_ONLY_EXTRA = "push_only"; private final DynamicTheme dynamicTheme = new DynamicTheme(); diff --git a/src/org/thoughtcrime/securesms/PushContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/PushContactSelectionListFragment.java index 6bbbc8e595..87f15a2f19 100644 --- a/src/org/thoughtcrime/securesms/PushContactSelectionListFragment.java +++ b/src/org/thoughtcrime/securesms/PushContactSelectionListFragment.java @@ -24,11 +24,14 @@ 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.text.Editable; +import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; +import android.widget.EditText; import android.widget.TextView; import org.thoughtcrime.securesms.contacts.ContactAccessor; @@ -36,6 +39,7 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.DataHolder; +import org.thoughtcrime.securesms.contacts.ContactsDatabase; import java.util.LinkedList; import java.util.List; @@ -61,6 +65,8 @@ public class PushContactSelectionListFragment extends Fragment private OnContactSelectedListener onContactSelectedListener; private boolean multi = false; private StickyListHeadersListView listView; + private EditText filterEditText; + private String cursorFilter; @Override @@ -80,6 +86,12 @@ public class PushContactSelectionListFragment extends Fragment super.onPause(); } + @Override + public void onDestroyView() { + super.onDestroyView(); + ContactsDatabase.destroyInstance(); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.push_contact_selection_list_activity, container, false); @@ -127,9 +139,27 @@ public class PushContactSelectionListFragment extends Fragment listView = (StickyListHeadersListView) getView().findViewById(android.R.id.list); listView.setFocusable(true); listView.setFastScrollEnabled(true); - listView.setFastScrollAlwaysVisible(true); listView.setDrawingListUnderStickyHeader(false); listView.setOnItemClickListener(new ListClickListener()); + filterEditText = (EditText) getView().findViewById(R.id.filter); + filterEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { + cursorFilter = charSequence.toString(); + getLoaderManager().restartLoader(0, null, PushContactSelectionListFragment.this); + } + + @Override + public void afterTextChanged(Editable editable) { + + } + }); + cursorFilter = null; } public void update() { @@ -138,13 +168,19 @@ public class PushContactSelectionListFragment extends Fragment @Override public Loader onCreateLoader(int id, Bundle args) { - return ContactAccessor.getInstance().getCursorLoaderForContacts(getActivity()); + if (getActivity().getIntent().getBooleanExtra(PushContactSelectionActivity.PUSH_ONLY_EXTRA, false)) { + return ContactAccessor.getInstance().getCursorLoaderForPushContacts(getActivity(), cursorFilter); + } else { + return ContactAccessor.getInstance().getCursorLoaderForContacts(getActivity(), cursorFilter); + } } @Override public void onLoadFinished(Loader loader, Cursor data) { ((CursorAdapter) listView.getAdapter()).changeCursor(data); emptyText.setText(R.string.contact_selection_group_activity__no_contacts); + if (data != null && data.getCount() < 40) listView.setFastScrollAlwaysVisible(false); + else listView.setFastScrollAlwaysVisible(true); } @Override diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java index 794b7b34dc..8293ffd425 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -82,8 +82,12 @@ public class ContactAccessor { null, null, null, ContactsContract.Groups.TITLE + " ASC"); } - public Loader getCursorLoaderForContacts(Context context) { - return new ContactsCursorLoader(context); + public Loader getCursorLoaderForContacts(Context context, String filter) { + return new ContactsCursorLoader(context, filter, false); + } + + public Loader getCursorLoaderForPushContacts(Context context, String filter) { + return new ContactsCursorLoader(context, filter, true); } public Cursor getCursorForContactsWithNumbers(Context context) { diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java index c86329d552..11cb8cf9d4 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java @@ -30,6 +30,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; +import android.widget.FilterQueryProvider; import android.widget.ImageView; import android.widget.TextView; diff --git a/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java b/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java index 3ad86b5d10..04dc15e531 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactsCursorLoader.java @@ -28,22 +28,25 @@ import android.support.v4.content.CursorLoader; public class ContactsCursorLoader extends CursorLoader { private final Context context; + private final String filter; + private final boolean pushOnly; private ContactsDatabase db; - public ContactsCursorLoader(Context context) { + public ContactsCursorLoader(Context context, String filter, boolean pushOnly) { super(context); - this.context = context; + this.context = context; + this.filter = filter; + this.pushOnly = pushOnly; } @Override public Cursor loadInBackground() { - db = new ContactsDatabase(context); - return db.getAllContacts(); + db = ContactsDatabase.getInstance(context); + return db.query(filter, pushOnly); } @Override public void onReset() { super.onReset(); - db.close(); } } diff --git a/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java b/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java index 7f1de7252f..322866d3d1 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactsDatabase.java @@ -20,6 +20,7 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.CursorWrapper; +import android.database.MatrixCursor; import android.database.MergeCursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; @@ -28,10 +29,15 @@ import android.provider.ContactsContract; import android.util.Log; import android.util.Pair; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.NumberUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.textsecure.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; /** * Database to supply all types of contacts that TextSecure needs to know about @@ -50,50 +56,102 @@ public class ContactsDatabase { public static final String NUMBER_COLUMN = ContactsContract.CommonDataKinds.Phone.NUMBER; public static final String TYPE_COLUMN = "type"; + private static final String FILTER_SELECTION = NAME_COLUMN + " LIKE ? OR " + NUMBER_COLUMN + " LIKE ?"; private static final String CONTACT_LIST_SORT = NAME_COLUMN + " ASC"; private static final String[] ANDROID_PROJECTION = new String[]{ID_COLUMN, NAME_COLUMN, NUMBER_TYPE_COLUMN, NUMBER_COLUMN}; + private static final String[] CONTACTS_PROJECTION = new String[]{ID_COLUMN, + NAME_COLUMN, + NUMBER_TYPE_COLUMN, + NUMBER_COLUMN, + TYPE_COLUMN}; + public static final int NORMAL_TYPE = 0; public static final int PUSH_TYPE = 1; public static final int GROUP_TYPE = 2; - public ContactsDatabase(Context context) { + private static ContactsDatabase instance = null; + + public synchronized static ContactsDatabase getInstance(Context context) { + if (instance == null) instance = new ContactsDatabase(context); + return instance; + } + + public synchronized static void destroyInstance() { + if (instance != null) instance.close(); + instance = null; + } + + private ContactsDatabase(Context context) { this.dbHelper = new DatabaseOpenHelper(context); - this.context = context; + this.context = context; } public void close() { dbHelper.close(); } - public Cursor getAllContacts() { - return query(null, null, null); - } + public Cursor query(String filter, boolean pushOnly) { + final boolean includeAndroidContacts = !pushOnly && TextSecurePreferences.isSmsNonDataOutEnabled(context); + final Cursor localCursor = queryLocalDb(filter); + final Cursor androidCursor; + final MatrixCursor newNumberCursor; - private Cursor query(String selection, String[] selectionArgs, String[] columns) { - final Cursor localCursor = queryLocalDb(selection, selectionArgs, columns); - final Cursor androidCursor; - - if (TextSecurePreferences.isSmsNonDataOutEnabled(context)) { - androidCursor = queryAndroidDb(); - } else{ - return localCursor; + if (includeAndroidContacts) { + androidCursor = queryAndroidDb(filter); + } else { + androidCursor = null; } - if (localCursor != null && androidCursor != null) return new MergeCursor(new Cursor[]{localCursor,androidCursor}); - else if (localCursor != null) return localCursor; - else if (androidCursor != null) return androidCursor; - else return null; + if (includeAndroidContacts && !Util.isEmpty(filter) && NumberUtil.isValidSmsOrEmail(filter)) { + newNumberCursor = new MatrixCursor(CONTACTS_PROJECTION, 1); + newNumberCursor.addRow(new Object[]{-1L, context.getString(R.string.contact_selection_list__unknown_contact), + 0, filter, NORMAL_TYPE}); + } else { + newNumberCursor = null; + } + + List cursors = new ArrayList(); + if (localCursor != null) cursors.add(localCursor); + if (androidCursor != null) cursors.add(androidCursor); + if (newNumberCursor != null) cursors.add(newNumberCursor); + + switch (cursors.size()) { + case 0: return null; + case 1: return cursors.get(0); + default: return new MergeCursor(cursors.toArray(new Cursor[]{})); + } } - private Cursor queryAndroidDb() { - Cursor cursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, ANDROID_PROJECTION, null, null, CONTACT_LIST_SORT); + private Cursor queryAndroidDb(String filter) { + final Uri baseUri; + if (filter != null) { + baseUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, + Uri.encode(filter)); + } else { + baseUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI; + } + Cursor cursor = context.getContentResolver().query(baseUri, ANDROID_PROJECTION, null, null, CONTACT_LIST_SORT); return new TypedCursorWrapper(cursor); } + private Cursor queryLocalDb(String filter) { + final String selection; + final String[] selectionArgs; + final String fuzzyFilter = "%" + filter + "%"; + if (!Util.isEmpty(filter)) { + selection = FILTER_SELECTION; + selectionArgs = new String[]{fuzzyFilter, fuzzyFilter}; + } else { + selection = null; + selectionArgs = null; + } + return queryLocalDb(selection, selectionArgs, null); + } + private Cursor queryLocalDb(String selection, String[] selectionArgs, String[] columns) { SQLiteDatabase localDb = dbHelper.getReadableDatabase(); final Cursor localCursor;