diff --git a/res/layout/new_conversation_activity.xml b/res/layout/new_conversation_activity.xml index e289f89b1..e3bf93e1f 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 f1a6f219c..fc0c87bb7 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 f83750854..c0191edc9 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 5e871d1c5..57ca481d7 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 dcd38dee3..e61281a89 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 6bbbc8e59..87f15a2f1 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 794b7b34d..8293ffd42 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 c86329d55..11cb8cf9d 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 3ad86b5d1..04dc15e53 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 7f1de7252..322866d3d 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;