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
This commit is contained in:
Jake McGinty 2014-04-01 16:40:16 -07:00 committed by Moxie Marlinspike
parent b715debefc
commit e2f7c1529a
11 changed files with 171 additions and 56 deletions

View File

@ -5,10 +5,6 @@
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<org.thoughtcrime.securesms.components.SingleRecipientPanel android:id="@+id/recipients"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<fragment
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"

View File

@ -4,16 +4,28 @@
android:layout_height="match_parent"
android:orientation="vertical">
<EditText android:id="@+id/filter"
android:inputType="textPersonName"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="@string/recipients_panel__to"
android:paddingRight="45dip"
android:paddingLeft="15dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:textColor="?conversation_editor_text_color"
android:background="?conversation_editor_background" />
<se.emilsjolander.stickylistheaders.StickyListHeadersListView android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView android:id="@android:id/empty"
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" />
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" />
</LinearLayout>

View File

@ -147,7 +147,6 @@
<string name="GroupCreateActivity_actionbar_mms_title">New MMS Group</string>
<string name="GroupCreateActivity_contacts_dont_support_push">You have selected a contact that doesn\'t support TextSecure groups, so this group will be MMS.</string>
<string name="GroupCreateActivity_you_dont_support_push">You\'re not registered for using the data channel, so TextSecure groups are disabled.</string>
<string name="GroupCreateActivity_you_dont_own_this_group">You\'re not the owner of this group, so you cannot edit the title or picture.</string>
<string name="GroupCreateActivity_contacts_mms_exception">An unexpected error happened that has made group creation fail.</string>
<string name="GroupCreateActivity_contacts_no_members">You need at least one person in your group!</string>
<string name="GroupCreateActivity_contacts_invalid_number">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.</string>
@ -155,6 +154,7 @@
<string name="GroupCreateActivity_avatar_content_description">Group Avatar</string>
<string name="GroupCreateActivity_menu_create_title">Create Group</string>
<string name="GroupCreateActivity_creating_group">Creating %1$s&#8230;</string>
<string name="GroupCreateActivity_cannot_add_non_push_to_existing_group">Cannot add non-TextSecure contacts to an existing TextSecure group</string>
<!-- ImportFragment -->
<string name="ImportFragment_import_system_sms_database">Import System SMS Database?</string>
@ -765,6 +765,7 @@
<string name="contact_selection_list__menu_unselect_all">Unselect All</string>
<string name="contact_selection_list__header_textsecure_users">TEXTSECURE USERS</string>
<string name="contact_selection_list__header_other">ALL CONTACTS</string>
<string name="contact_selection_list__unknown_contact">New message to...</string>
<!-- contact_selection -->
<string name="contact_selection__menu_finished">Finished</string>

View File

@ -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<Long, Recipients> 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;

View File

@ -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() {

View File

@ -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();

View File

@ -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<Cursor> 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<Cursor> 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

View File

@ -82,8 +82,12 @@ public class ContactAccessor {
null, null, null, ContactsContract.Groups.TITLE + " ASC");
}
public Loader<Cursor> getCursorLoaderForContacts(Context context) {
return new ContactsCursorLoader(context);
public Loader<Cursor> getCursorLoaderForContacts(Context context, String filter) {
return new ContactsCursorLoader(context, filter, false);
}
public Loader<Cursor> getCursorLoaderForPushContacts(Context context, String filter) {
return new ContactsCursorLoader(context, filter, true);
}
public Cursor getCursorForContactsWithNumbers(Context context) {

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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<Cursor> cursors = new ArrayList<Cursor>();
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;