diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 746a8b6dca..866d913625 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -134,6 +134,10 @@ android:windowSoftInputMode="stateHidden" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + + + + + + + \ No newline at end of file diff --git a/res/layout/group_create_activity.xml b/res/layout/group_create_activity.xml index e03bf239ff..4da504a35c 100644 --- a/res/layout/group_create_activity.xml +++ b/res/layout/group_create_activity.xml @@ -17,15 +17,16 @@ android:layout_height="106dp" android:orientation="horizontal" android:gravity="center_vertical" - android:paddingLeft="18dp" + android:paddingLeft="14dp" android:paddingRight="18dp" - android:paddingTop="18dp"> + android:paddingTop="14dp"> @@ -68,8 +69,9 @@ @@ -78,11 +80,11 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" - android:background="#ff444444" + android:background="#dd222222" android:textColor="#ffeeeeee" android:fontFamily="sans-serif-light" android:padding="8dp" - android:textSize="16dp" + android:textSize="16sp" android:text="@string/GroupCreateActivity_contacts_dont_support_push" /> @@ -91,7 +93,9 @@ + android:layout_height="wrap_content" + android:paddingLeft="15dp" + android:paddingRight="15dp" /> + + + + + + \ No newline at end of file diff --git a/res/layout/single_contact_selection_list_activity.xml b/res/layout/single_contact_selection_list_activity.xml new file mode 100644 index 0000000000..873d70593c --- /dev/null +++ b/res/layout/single_contact_selection_list_activity.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/res/layout/single_contact_selection_list_item.xml b/res/layout/single_contact_selection_list_item.xml new file mode 100644 index 0000000000..463a44bafc --- /dev/null +++ b/res/layout/single_contact_selection_list_item.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 03b445ecce..93ef5a516f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -118,10 +118,9 @@ New Group + Group Name New MMS Group - You have selected contacts that - don\'t support TextSecure groups, so this will be an unencrypted MMS group. - + You have selected a contact that doesn\'t support TextSecure groups, so this group will be MMS. Import System SMS Database? @@ -377,7 +376,10 @@ No contacts. - Finding contacts... + Finding contacts… + + + Type a name to filter… Select for @@ -569,6 +571,7 @@ Create Passphrase Enter Passphrase Select Contacts + Select Contact TextSecure Detected Public Identity Key Change Passphrase diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 4364a41fda..afbe4516fb 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -16,7 +16,6 @@ */ package org.thoughtcrime.securesms; -import android.app.Activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; @@ -167,13 +166,13 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi @Override protected void onStart() { super.onStart(); - - if (!isExistingConversation()) - initializeRecipientsInput(); } @Override protected void onResume() { + if (recipients == null || recipients.isEmpty()) + initializeRecipientsInput(); + super.onResume(); dynamicTheme.onResume(this); dynamicLanguage.onResume(this); @@ -207,28 +206,30 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi Log.w("ComposeMessageActivity", "onActivityResult called: " + resultCode + " , " + data); super.onActivityResult(reqCode, resultCode, data); - if (data == null || resultCode != Activity.RESULT_OK) - return; - switch (reqCode) { case PICK_CONTACT: - List contacts = data.getParcelableArrayListExtra("contacts"); - - if (contacts != null) - recipientsPanel.addContacts(contacts); - + if (resultCode == RESULT_OK) { + List contacts = data.getParcelableArrayListExtra("contacts"); + if (contacts != null) { + recipientsPanel.addContacts(contacts); + this.recipients = getRecipients(); + } + } else { + Log.w("ConversationActivity", "gonna have a bad time."); + finish(); + } break; case PICK_IMAGE: - addAttachmentImage(data.getData()); + if (data != null && resultCode == RESULT_OK) addAttachmentImage(data.getData()); break; case PICK_VIDEO: - addAttachmentVideo(data.getData()); + if (data != null && resultCode == RESULT_OK) addAttachmentVideo(data.getData()); break; case PICK_AUDIO: - addAttachmentAudio(data.getData()); + if (data != null && resultCode == RESULT_OK) addAttachmentAudio(data.getData()); break; case PICK_CONTACT_INFO: - addContactInfo(data.getData()); + if (data != null && resultCode == RESULT_OK) addContactInfo(data.getData()); break; } } @@ -649,16 +650,9 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi } private void initializeRecipientsInput() { - recipientsPanel.setVisibility(View.VISIBLE); - - if (this.recipients != null) { - recipientsPanel.addRecipients(this.recipients); - } else { - InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); - input.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); - } + Intent intent = new Intent(ConversationActivity.this, SingleContactSelectionActivity.class); + startActivityForResult(intent, PICK_CONTACT); } - private void initializeReceivers() { securityUpdateReceiver = new BroadcastReceiver() { @Override @@ -846,7 +840,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi if (isExistingConversation()) return this.recipients; else return recipientsPanel.getRecipients(); } catch (RecipientFormattingException rfe) { - Log.w("ConversationActivity", rfe); + Log.d("ConversationActivity", "Empty list of recipients retrieved from RecipientsPanel."); return null; } } diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index ec88fad683..17bd94804b 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -171,7 +171,7 @@ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActiv @Override public void afterTextChanged(Editable editable) { if (editable.length() > 0) - getSupportActionBar().setTitle(editable); + getSupportActionBar().setTitle(getString(R.string.GroupCreateActivity_actionbar_title) + ": " + editable.toString()); else getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_title); } diff --git a/src/org/thoughtcrime/securesms/SingleContactSelectionActivity.java b/src/org/thoughtcrime/securesms/SingleContactSelectionActivity.java new file mode 100644 index 0000000000..92c3c0dbc7 --- /dev/null +++ b/src/org/thoughtcrime/securesms/SingleContactSelectionActivity.java @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms; + +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.widget.CursorAdapter; +import android.widget.EditText; +import android.widget.Filter; +import android.widget.FilterQueryProvider; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +import org.thoughtcrime.securesms.contacts.ContactAccessor; +import org.thoughtcrime.securesms.util.ActionBarUtil; +import org.thoughtcrime.securesms.util.DynamicTheme; + +import java.util.ArrayList; +import java.util.List; + +import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; + +/** + * Activity container for selecting a list of contacts. Provides a tab frame for + * contact, group, and "recent contact" activity tabs. Used by ComposeMessageActivity + * when selecting a list of contacts to address a message to. + * + * @author Moxie Marlinspike + * + */ +public class SingleContactSelectionActivity extends PassphraseRequiredSherlockFragmentActivity { + private final String TAG = "SingleContactSelectionActivity"; + private final DynamicTheme dynamicTheme = new DynamicTheme(); + + @Override + protected void onCreate(Bundle icicle) { + dynamicTheme.onCreate(this); + super.onCreate(icicle); + + final ActionBar actionBar = this.getSupportActionBar(); + ActionBarUtil.initializeDefaultActionBar(this, actionBar); + actionBar.setDisplayHomeAsUpEnabled(true); + + setContentView(R.layout.single_contact_selection_activity); + final SingleContactSelectionListFragment listFragment = (SingleContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment); + listFragment.setOnContactSelectedListener(new SingleContactSelectionListFragment.OnContactSelectedListener() { + @Override + public void onContactSelected(ContactData contactData) { + Intent resultIntent = getIntent(); + ArrayList contactList = new ArrayList(); + contactList.add(contactData); + resultIntent.putParcelableArrayListExtra("contacts", contactList); + setResult(RESULT_OK, resultIntent); + finish(); + } + }); + + + EditText filter = (EditText)findViewById(R.id.filter); + filter.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) { + + } + + @Override + public void afterTextChanged(Editable editable) { + final CursorAdapter adapter = (CursorAdapter)listFragment.getListView().getAdapter(); + Log.i(TAG, "new text change: " + editable); + Filter filter = adapter.getFilter(); + + if (filter != null) { filter.filter(editable.toString()); adapter.notifyDataSetChanged(); } + else { Log.w(TAG, "filter was null, bad time."); } + + + } + }); + } + + @Override + public void onResume() { + super.onResume(); + dynamicTheme.onResume(this); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + setResult(RESULT_CANCELED); + finish(); + return true; + } + + return false; + } + +} diff --git a/src/org/thoughtcrime/securesms/SingleContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/SingleContactSelectionListFragment.java new file mode 100644 index 0000000000..93e2b8f82f --- /dev/null +++ b/src/org/thoughtcrime/securesms/SingleContactSelectionListFragment.java @@ -0,0 +1,298 @@ +/** + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms; + + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.database.Cursor; +import android.database.MergeCursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CursorAdapter; +import android.widget.FilterQueryProvider; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.actionbarsherlock.app.SherlockListFragment; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +import org.thoughtcrime.securesms.contacts.ContactAccessor; +import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; +import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * Activity for selecting a list of contacts. Displayed inside + * a PushContactSelectionActivity tab frame, and ultimately called by + * ComposeMessageActivity for selecting a list of destination contacts. + * + * @author Moxie Marlinspike + * + */ + +public class SingleContactSelectionListFragment extends SherlockListFragment + implements LoaderManager.LoaderCallbacks +{ + private final String TAG = "SingleContactSelectionListFragment"; + private final HashMap selectedContacts = new HashMap(); + private static LayoutInflater li; + private OnContactSelectedListener onContactSelectedListener; + + @Override + public void onActivityCreated(Bundle icicle) { + super.onCreate(icicle); + li = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + initializeResources(); + initializeCursor(); + } + + public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) { + this.onContactSelectedListener = onContactSelectedListener; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.single_contact_selection_list_activity, container, false); + } + + private void addSingleNumberContact(ContactData contactData) { + if (onContactSelectedListener != null) { + onContactSelectedListener.onContactSelected(contactData); + } + } + + private void addMultipleNumberContact(ContactData contactData, TextView textView) { + String[] options = new String[contactData.numbers.size()]; + int i = 0; + + for (NumberData option : contactData.numbers) { + options[i++] = option.type + " " + option.number; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.ContactSelectionlistFragment_select_for + " " + contactData.name); + builder.setMultiChoiceItems(options, null, new DiscriminatorClickedListener(contactData)); + builder.setPositiveButton(android.R.string.ok, new DiscriminatorFinishedListener(contactData, textView)); + builder.setOnCancelListener(new DiscriminatorFinishedListener(contactData, textView)); + builder.show(); + } + + private void initializeCursor() { + final ContactSelectionListAdapter listAdapter = new ContactSelectionListAdapter(getActivity(), null); + listAdapter.setFilterQueryProvider(new FilterQueryProvider() { + @Override + public Cursor runQuery(CharSequence charSequence) { + final Uri uri = ContactsContract.Contacts.CONTENT_URI; + final String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1" + + " AND " + ContactsContract.Contacts.DISPLAY_NAME + " like ?"; + + final Cursor filteredCursor = getActivity().getContentResolver().query(uri, null, selection, new String[]{"%"+charSequence.toString()+"%"}, + ContactsContract.Contacts.DISPLAY_NAME + " ASC"); + return filteredCursor; + } + }); + setListAdapter(listAdapter); + this.getLoaderManager().initLoader(0, null, this); + } + + private void initializeResources() { + this.getListView().setFocusable(true); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + ((ContactItemView)v).selected(); + } + + private class ContactSelectionListAdapter extends CursorAdapter { + + public ContactSelectionListAdapter(Context context, Cursor c) { + super(context, c); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + ContactItemView view = new ContactItemView(context); + bindView(view, context, cursor); + + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + boolean isPushUser; + try { + isPushUser = (cursor.getInt(cursor.getColumnIndexOrThrow(ContactAccessor.PUSH_COLUMN)) > 0); + } catch (IllegalArgumentException iae) { + isPushUser = false; + } + ContactData contactData = ContactAccessor.getInstance().getContactData(context, cursor); + PushContactData pushContactData = new PushContactData(contactData, isPushUser); + ((ContactItemView)view).set(pushContactData); + } + } + + private class PushContactData { + private final ContactData contactData; + private final boolean pushSupport; + public PushContactData(ContactData contactData, boolean pushSupport) { + this.contactData = contactData; + this.pushSupport = pushSupport; + } + } + + private class ContactItemView extends RelativeLayout { + private ContactData contactData; + private boolean pushSupport; + private TextView name; + private TextView number; + private TextView label; + private View pushLabel; + + public ContactItemView(Context context) { + super(context); + + li.inflate(R.layout.single_contact_selection_list_item, this, true); + + this.name = (TextView) findViewById(R.id.name); + this.number = (TextView) findViewById(R.id.number); + this.label = (TextView) findViewById(R.id.label); + this.pushLabel = findViewById(R.id.push_support_label); + } + + public void selected() { + if (contactData.numbers.size() == 1) addSingleNumberContact(contactData); + else addMultipleNumberContact(contactData, name); + } + + public void set(PushContactData pushContactData) { + this.contactData = pushContactData.contactData; + this.pushSupport = pushContactData.pushSupport; + + /*if (!pushSupport) { + this.name.setTextColor(0xa0000000); + this.number.setTextColor(0xa0000000); + this.pushLabel.setBackgroundColor(0x99000000); + } else { + this.name.setTextColor(0xff000000); + this.number.setTextColor(0xff000000); + this.pushLabel.setBackgroundColor(0xff64a926); + }*/ + + this.name.setText(contactData.name); + + if (contactData.numbers.isEmpty()) { + this.name.setEnabled(false); + this.number.setText(""); + this.label.setText(""); + } else { + this.number.setText(contactData.numbers.get(0).number); + this.label.setText(contactData.numbers.get(0).type); + } + } + } + + private class DiscriminatorFinishedListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { + private final ContactData contactData; + private final TextView textView; + + public DiscriminatorFinishedListener(ContactData contactData, TextView textView) { + this.contactData = contactData; + this.textView = textView; + } + + public void onClick(DialogInterface dialog, int which) { + ContactData selected = selectedContacts.get(contactData.id); + + if (selected.numbers.size() == 0) { + selectedContacts.remove(selected.id); + } + + if (textView == null) + ((CursorAdapter) getListView().getAdapter()).notifyDataSetChanged(); + } + + public void onCancel(DialogInterface dialog) { + onClick(dialog, 0); + } + } + + private class DiscriminatorClickedListener implements DialogInterface.OnMultiChoiceClickListener { + private final ContactData contactData; + + public DiscriminatorClickedListener(ContactData contactData) { + this.contactData = contactData; + } + + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + Log.w(TAG, "Got checked: " + isChecked); + + ContactData existing = selectedContacts.get(contactData.id); + + if (existing == null) { + Log.w(TAG, "No existing contact data, creating..."); + + if (!isChecked) + throw new AssertionError("We shouldn't be unchecking data that doesn't exist."); + + existing = new ContactData(contactData.id, contactData.name); + selectedContacts.put(existing.id, existing); + } + + NumberData selectedData = contactData.numbers.get(which); + + if (!isChecked) existing.numbers.remove(selectedData); + else existing.numbers.add(selectedData); + } + } + + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) { + return ContactAccessor.getInstance().getCursorLoaderForContactsWithNumbers(getActivity()); + } + + @Override + public void onLoadFinished(Loader arg0, Cursor cursor) { + ((CursorAdapter) getListAdapter()).changeCursor(cursor); + } + + @Override + public void onLoaderReset(Loader arg0) { + ((CursorAdapter) getListAdapter()).changeCursor(null); + } + + public interface OnContactSelectedListener { + public void onContactSelected(ContactData contactData); + } +} diff --git a/src/org/thoughtcrime/securesms/components/PushRecipientsPanel.java b/src/org/thoughtcrime/securesms/components/PushRecipientsPanel.java index 3fd1fe87e7..20fd4af6e7 100644 --- a/src/org/thoughtcrime/securesms/components/PushRecipientsPanel.java +++ b/src/org/thoughtcrime/securesms/components/PushRecipientsPanel.java @@ -79,14 +79,6 @@ public class PushRecipientsPanel extends RelativeLayout { else recipientsText.append(number + ", "); } - public void addContacts(List contacts) { - for (ContactAccessor.ContactData contact : contacts) { - for (ContactAccessor.NumberData number : contact.numbers) { - addRecipient(contact.name, number.number); - } - } - } - public void addRecipients(Recipients recipients) { List recipientList = recipients.getRecipientsList(); Iterator iterator = recipientList.iterator(); @@ -130,8 +122,8 @@ public class PushRecipientsPanel extends RelativeLayout { } private void initRecipientsEditor() { - Recipients recipients = null; - recipientsText = (RecipientsEditor)findViewById(R.id.recipients_text); + Recipients recipients; + recipientsText = (RecipientsEditor)findViewById(R.id.recipients_text); try { recipients = getRecipients(); diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java index 810e8513a1..7e19aa5c21 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -102,6 +102,7 @@ public class ContactAccessor { try { if (lookupCursor != null && lookupCursor.moveToFirst()) { cursor.addRow(new Object[]{lookupCursor.getLong(0), lookupCursor.getString(1), 1}); + Log.w("poop", "Adding matrix row for " + lookupCursor.getLong(0) + " : " + lookupCursor.getString(1)); } } finally { if (lookupCursor != null) diff --git a/src/org/thoughtcrime/securesms/mms/MmsCommunication.java b/src/org/thoughtcrime/securesms/mms/MmsCommunication.java index 83a386e734..7bc19fac19 100644 --- a/src/org/thoughtcrime/securesms/mms/MmsCommunication.java +++ b/src/org/thoughtcrime/securesms/mms/MmsCommunication.java @@ -111,7 +111,7 @@ public class MmsCommunication { Log.w("MmsCommunication", sqe); return getLocalMmsConnectionParameters(context); } catch (SecurityException se) { - Log.w("MmsCommunication", se); + Log.i("MmsCommunication", "Couldn't write APN settings, expected. msg: " + se.getMessage()); return getLocalMmsConnectionParameters(context); } catch (IllegalArgumentException iae) { Log.w("MmsCommunication", iae);