diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d57253f3fd..746a8b6dca 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -101,6 +101,10 @@ android:windowSoftInputMode="stateUnchanged" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/push_contact_selection_activity.xml b/res/layout/push_contact_selection_activity.xml new file mode 100644 index 0000000000..9836f8c47c --- /dev/null +++ b/res/layout/push_contact_selection_activity.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/push_contact_selection_list_activity.xml b/res/layout/push_contact_selection_list_activity.xml new file mode 100644 index 0000000000..f25dd83a65 --- /dev/null +++ b/res/layout/push_contact_selection_list_activity.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/res/layout/push_contact_selection_list_item.xml b/res/layout/push_contact_selection_list_item.xml new file mode 100644 index 0000000000..928d599f5e --- /dev/null +++ b/res/layout/push_contact_selection_list_item.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + diff --git a/res/layout/selected_recipient_list_item.xml b/res/layout/selected_recipient_list_item.xml new file mode 100644 index 0000000000..4ef65831f9 --- /dev/null +++ b/res/layout/selected_recipient_list_item.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/group_create.xml b/res/menu/group_create.xml new file mode 100644 index 0000000000..b9a28fedb0 --- /dev/null +++ b/res/menu/group_create.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/res/menu/text_secure_normal.xml b/res/menu/text_secure_normal.xml index 1150efb128..1cc156505e 100644 --- a/res/menu/text_secure_normal.xml +++ b/res/menu/text_secure_normal.xml @@ -5,6 +5,10 @@ android:icon="?attr/menu_new_conversation_icon" android:showAsAction="always" /> + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index d562942f94..e9b232c2ab 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -33,6 +33,7 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index c467194aec..abbef3a19e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -370,7 +370,8 @@ No contacts. - + Finding contacts... + Select for diff --git a/res/values/themes.xml b/res/values/themes.xml index a8874d72a6..3224caf01b 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -27,6 +27,7 @@ @drawable/ic_ime_dark @drawable/ic_action_new_holo_light + @drawable/ic_action_add_group_holo_light @drawable/ic_menu_search_holo_light @drawable/ic_menu_call_holo_light @drawable/ic_menu_unlock_holo_light @@ -75,6 +76,7 @@ @drawable/ic_ime_light @drawable/ic_action_new_holo_dark + @drawable/ic_action_add_group_holo_dark @drawable/ic_menu_search_holo_dark @drawable/ic_menu_call_holo_dark @drawable/ic_menu_unlock_holo_dark diff --git a/src/org/thoughtcrime/securesms/ContactSelectionActivity.java b/src/org/thoughtcrime/securesms/ContactSelectionActivity.java index 834b06c1f3..e131252dca 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionActivity.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionActivity.java @@ -19,7 +19,6 @@ package org.thoughtcrime.securesms; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentTransaction; import android.support.v4.view.ViewPager; diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index a1dd2adb27..8c28c56ccc 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -659,7 +659,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi } } - private void initializeReceivers() { securityUpdateReceiver = new BroadcastReceiver() { @Override diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index c4e608cb9b..3e0103b180 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -140,6 +140,7 @@ public class ConversationFragment extends SherlockListFragment long dateReceived = message.getDateReceived(); long dateSent = message.getDateSent(); + SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE MMM d, yyyy 'at' hh:mm:ss a zzz"); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.ConversationFragment_message_details); diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index 4661b0bdb5..d0f0335378 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -139,6 +139,7 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment switch (item.getItemId()) { case R.id.menu_new_message: createConversation(-1, null, defaultType); return true; + case R.id.menu_new_group: createGroup(); return true; case R.id.menu_settings: handleDisplaySettings(); return true; case R.id.menu_clear_passphrase: handleClearPassphrase(); return true; case R.id.menu_mark_all_read: handleMarkAllRead(); return true; @@ -153,6 +154,11 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment createConversation(threadId, recipients, distributionType); } + private void createGroup() { + Intent intent = new Intent(this, GroupCreateActivity.class); + startActivity(intent); + } + private void createConversation(long threadId, Recipients recipients, int distributionType) { Intent intent = new Intent(this, ConversationActivity.class); intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients); diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java new file mode 100644 index 0000000000..61fafba25c --- /dev/null +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -0,0 +1,151 @@ +package org.thoughtcrime.securesms; + +import android.app.Activity; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.Log; +import android.view.View; +import android.widget.ListView; + +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.ActionBarUtil; +import org.thoughtcrime.securesms.util.DynamicLanguage; +import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + + +public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActivity { + + private final DynamicTheme dynamicTheme = new DynamicTheme(); + private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); + + private static final int PICK_CONTACT = 1; + private static final int SELECT_PHOTO = 100; + private ListView lv; + + private Set selectedContacts; + + @Override + public void onCreate(Bundle state) { + dynamicTheme.onCreate(this); + dynamicLanguage.onCreate(this); + super.onCreate(state); + + setContentView(R.layout.group_create_activity); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + ActionBarUtil.initializeDefaultActionBar(this, getSupportActionBar(), "New Group"); + + selectedContacts = new HashSet(); + initializeResources(); + } + + @Override + public void onResume() { + super.onResume(); + dynamicTheme.onResume(this); + } + + private void initializeResources() { + lv = (ListView) findViewById(R.id.selected_contacts_list); + lv.setAdapter(new SelectedRecipientsAdapter(this, android.R.id.text1, new ArrayList())); + (findViewById(R.id.add_people_button)).setOnClickListener(new AddRecipientButtonListener()); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuInflater inflater = this.getSupportMenuInflater(); + menu.clear(); + + inflater.inflate(R.menu.group_create, menu); + super.onPrepareOptionsMenu(menu); + return true; + } + + private List selectedContactsAsIdArray() { + final List ids = new ArrayList(); + for (Recipient recipient : selectedContacts) { + ids.add(String.valueOf(recipient.getCanonicalAddress(this))); + } + return ids; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + switch (item.getItemId()) { + case android.R.id.home: + case R.id.menu_create_group: + finish(); // TODO not this + return true; + } + + return false; + } + + @Override + public void onActivityResult(int reqCode, int resultCode, Intent data) { + 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: + Recipients recipients = data.getParcelableExtra("recipients"); + for (Recipient recipient : recipients.getRecipientsList()) { + if (!selectedContacts.contains(recipient)) { + Log.w("poop", "contains that shit."); + selectedContacts.add(recipient); + } + } + SelectedRecipientsAdapter adapter = (SelectedRecipientsAdapter)lv.getAdapter(); + adapter.clear(); + Iterator selectedContactsIter = selectedContacts.iterator(); + while (selectedContactsIter.hasNext()) { + adapter.add(selectedContactsIter.next()); + } + break; + case SELECT_PHOTO: + if(resultCode == RESULT_OK){ + Uri selectedImage = data.getData(); + String[] filePathColumn = {MediaStore.Images.Media.DATA}; + + Cursor cursor = getContentResolver().query( + selectedImage, filePathColumn, null, null, null); + cursor.moveToFirst(); + + int columnIndex = cursor.getColumnIndex(filePathColumn[0]); + String filePath = cursor.getString(columnIndex); + cursor.close(); + + Bitmap selectedBitmap = BitmapFactory.decodeFile(filePath); + break; + } + } + } + + private class AddRecipientButtonListener implements View.OnClickListener { + @Override + public void onClick(View v) { + Intent intent = new Intent(GroupCreateActivity.this, PushContactSelectionActivity.class); + startActivityForResult(intent, PICK_CONTACT); + } + } +} diff --git a/src/org/thoughtcrime/securesms/PushContactSelectionActivity.java b/src/org/thoughtcrime/securesms/PushContactSelectionActivity.java new file mode 100644 index 0000000000..65f84f2075 --- /dev/null +++ b/src/org/thoughtcrime/securesms/PushContactSelectionActivity.java @@ -0,0 +1,94 @@ +/** + * 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.os.Bundle; + +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.ActionBarUtil; +import org.thoughtcrime.securesms.util.DynamicTheme; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +/** + * 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 PushContactSelectionActivity extends PassphraseRequiredSherlockFragmentActivity { + + private final DynamicTheme dynamicTheme = new DynamicTheme(); + + private Recipients recipients; + + @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.push_contact_selection_activity); + } + + @Override + public void onResume() { + super.onResume(); + dynamicTheme.onResume(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = this.getSupportMenuInflater(); + inflater.inflate(R.menu.contact_selection, menu); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_selection_finished: + case android.R.id.home: + handleSelectionFinished(); return true; + } + + return false; + } + + private void handleSelectionFinished() { + PushContactSelectionListFragment contactsFragment = (PushContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment); + recipients = contactsFragment.getSelectedContacts(); + + Intent resultIntent = getIntent(); + resultIntent.putExtra("recipients", this.recipients); + + setResult(RESULT_OK, resultIntent); + + finish(); + } + +} diff --git a/src/org/thoughtcrime/securesms/PushContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/PushContactSelectionListFragment.java new file mode 100644 index 0000000000..f666125f45 --- /dev/null +++ b/src/org/thoughtcrime/securesms/PushContactSelectionListFragment.java @@ -0,0 +1,336 @@ +/** + * 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.os.Bundle; +import android.support.v4.app.LoaderManager; +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.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 org.thoughtcrime.securesms.contacts.PushFilterCursorWrapper; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; + +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 PushContactSelectionListFragment extends SherlockListFragment + implements LoaderManager.LoaderCallbacks +{ + + private final HashMap selectedContacts = new HashMap(); + private static LayoutInflater li; + + @Override + public void onActivityCreated(Bundle icicle) { + super.onCreate(icicle); + li = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + initializeResources(); + initializeCursor(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.push_contact_selection_list_activity, container, false); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.contact_selection_list, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + switch (item.getItemId()) { + case R.id.menu_select_all: handleSelectAll(); return true; + case R.id.menu_unselect_all: handleUnselectAll(); return true; + } + + super.onOptionsItemSelected(item); + return false; + } + + public Recipients getSelectedContacts() { + List recipientList = new LinkedList(); + + for (ContactData contactData : selectedContacts.values()) { + for (NumberData numberData : contactData.numbers) { + recipientList.add(new Recipient(contactData.name, numberData.number, null, null)); + } + } + + return new Recipients(recipientList); + } + + + private void handleUnselectAll() { + selectedContacts.clear(); + ((CursorAdapter)getListView().getAdapter()).notifyDataSetChanged(); + } + + private void handleSelectAll() { + selectedContacts.clear(); + + Cursor cursor = null; + + try { + cursor = ContactAccessor.getInstance().getCursorForContactsWithNumbers(getActivity()); + + while (cursor != null && cursor.moveToNext()) { + ContactData contactData = ContactAccessor.getInstance().getContactData(getActivity(), cursor); + + if (contactData.numbers.isEmpty()) continue; + else if (contactData.numbers.size() == 1) addSingleNumberContact(contactData); + else addMultipleNumberContact(contactData, null, null); + } + } finally { + if (cursor != null) + cursor.close(); + } + + ((CursorAdapter)getListView().getAdapter()).notifyDataSetChanged(); + } + + private void addSingleNumberContact(ContactData contactData) { + selectedContacts.put(contactData.id, contactData); + } + + private void removeContact(ContactData contactData) { + selectedContacts.remove(contactData.id); + } + + private void addMultipleNumberContact(ContactData contactData, TextView textView, CheckBox checkBox) { + 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, checkBox)); + builder.setOnCancelListener(new DiscriminatorFinishedListener(contactData, textView, checkBox)); + builder.show(); + } + + private void initializeCursor() { + setListAdapter(new ContactSelectionListAdapter(getActivity(), null)); + 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) { + PushFilterCursorWrapper wrappedCursor = (PushFilterCursorWrapper) cursor; + boolean isPushUser = wrappedCursor.getPushCount() > wrappedCursor.getPosition(); + ContactData contactData = ContactAccessor.getInstance().getContactData(context, cursor); + ((ContactItemView)view).set(contactData, isPushUser); + } + } + + private class ContactItemView extends RelativeLayout { + private ContactData contactData; + private CheckBox checkBox; + private TextView name; + private TextView number; + private TextView label; + + public ContactItemView(Context context) { + super(context); + + li.inflate(R.layout.push_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.checkBox = (CheckBox) findViewById(R.id.check_box); + } + + public void selected() { + + checkBox.toggle(); + + if (checkBox.isChecked()) { + if (contactData.numbers.size() == 1) addSingleNumberContact(contactData); + else addMultipleNumberContact(contactData, name, checkBox); + } else { + removeContact(contactData); + } + } + + public void set(ContactData contactData, boolean isPushUser) { + this.contactData = contactData; + + if (!isPushUser) { + this.name.setTextColor(0xa0000000); + this.number.setTextColor(0xa0000000); + this.checkBox.setVisibility(View.GONE); + } else { + this.name.setTextColor(0xff000000); + this.number.setTextColor(0xff000000); + this.checkBox.setVisibility(View.VISIBLE); + } + + if (selectedContacts.containsKey(contactData.id)) + this.checkBox.setChecked(true); + else + this.checkBox.setChecked(false); + + 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; + private final CheckBox checkBox; + + public DiscriminatorFinishedListener(ContactData contactData, TextView textView, CheckBox checkBox) { + this.contactData = contactData; + this.textView = textView; + this.checkBox = checkBox; + } + + public void onClick(DialogInterface dialog, int which) { + ContactData selected = selectedContacts.get(contactData.id); + + if (selected == null && textView != null) { + if (textView != null) checkBox.setChecked(false); + } else if (selected.numbers.size() == 0) { + selectedContacts.remove(selected.id); + if (textView != null) checkBox.setChecked(false); + } + + 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("ContactSelectionListActivity", "Got checked: " + isChecked); + + ContactData existing = selectedContacts.get(contactData.id); + + if (existing == null) { + Log.w("ContactSelectionListActivity", "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(new PushFilterCursorWrapper(cursor, getActivity())); + } + + @Override + public void onLoaderReset(Loader arg0) { + ((CursorAdapter) getListAdapter()).changeCursor(null); + } +} diff --git a/src/org/thoughtcrime/securesms/contacts/PushFilterCursorWrapper.java b/src/org/thoughtcrime/securesms/contacts/PushFilterCursorWrapper.java new file mode 100644 index 0000000000..6725eb4da2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/contacts/PushFilterCursorWrapper.java @@ -0,0 +1,106 @@ +package org.thoughtcrime.securesms.contacts; + +import android.content.Context; +import android.database.Cursor; +import android.database.CursorWrapper; +import android.os.Debug; +import android.provider.ContactsContract; +import android.util.Log; + +import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.textsecure.util.InvalidNumberException; + +import java.util.List; + +public class PushFilterCursorWrapper extends CursorWrapper { + private int[] pushIndex; + private int[] normalIndex; + private int count = 0; + private int pushCount = 0; + private int pos = 0; + + private final ContactAccessor contactAccessor = ContactAccessor.getInstance(); + + /* + * Don't know of a better way to do this without a large filtering through the entire dataset at first + */ + public PushFilterCursorWrapper(Cursor cursor, Context context) { + super(cursor); + this.count = super.getCount(); + this.pushIndex = new int[this.count]; + this.normalIndex = new int[this.count]; + int pushPos = 0; + int normalPos = 0; + for (int i = 0; i < this.count; i++) { + super.moveToPosition(i); + + + List numbers = contactAccessor.getContactData(context, cursor).numbers; + if (numbers.size() > 0) { + try { + if (Util.isPushTransport(context, Util.canonicalizeNumber(context, numbers.get(0).number))) + this.pushIndex[pushPos++] = i; + else + this.normalIndex[normalPos++] = i; + } catch (InvalidNumberException ine) { + } + } + } + this.pushCount = pushPos; + super.moveToFirst(); + } + + @Override + public boolean move(int offset) { + return this.moveToPosition(this.pos + offset); + } + + @Override + public boolean moveToNext() { + return this.moveToPosition(this.pos + 1); + } + + @Override + public boolean moveToPrevious() { + return this.moveToPosition(this.pos - 1); + } + + @Override + public boolean moveToFirst() { + return this.moveToPosition(0); + } + + @Override + public boolean moveToLast() { + return this.moveToPosition(this.count - 1); + } + + private int getPostFilteredPosition(int preFilteredPosition) { + return preFilteredPosition < this.pushCount + ? this.pushIndex[preFilteredPosition] + : this.normalIndex[preFilteredPosition - pushCount]; + } + + @Override + public boolean moveToPosition(int position) { + if (position >= this.count || position < 0) + return false; + pos = position; + return super.moveToPosition(getPostFilteredPosition(position)); + } + + @Override + public int getCount() { + return this.count; + } + + public int getPushCount() { + return this.pushCount; + } + + @Override + public int getPosition() { + return this.pos; + } + +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java index 9715f429f0..f420f947cf 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java @@ -24,15 +24,25 @@ import android.os.Parcelable; import android.util.Log; import org.thoughtcrime.securesms.database.CanonicalAddressDatabase; +import org.thoughtcrime.securesms.push.PushServiceSocketFactory; import org.thoughtcrime.securesms.recipients.RecipientProvider.RecipientDetails; +import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.textsecure.directory.Directory; +import org.whispersystems.textsecure.directory.NotInDirectoryException; +import org.whispersystems.textsecure.push.ContactTokenDetails; +import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.util.FutureTaskListener; +import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.ListenableFutureTask; import org.whispersystems.textsecure.storage.CanonicalRecipientAddress; +import java.io.IOException; import java.util.HashSet; public class Recipient implements Parcelable, CanonicalRecipientAddress { + private final static String TAG = "Recipient"; + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public Recipient createFromParcel(Parcel in) { return new Recipient(in); @@ -152,7 +162,31 @@ public class Recipient implements Parcelable, CanonicalRecipientAddress { return CanonicalAddressDatabase.getInstance(context).getCanonicalAddress(getNumber()); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || ((Object) this).getClass() != o.getClass()) return false; // the Object casting is due to an Android Studio bug... + + Recipient recipient = (Recipient) o; + + if (contactUri != null ? !contactUri.equals(recipient.contactUri) : recipient.contactUri != null) + return false; + if (name != null ? !name.equals(recipient.name) : recipient.name != null) return false; + if (number != null ? !number.equals(recipient.number) : recipient.number != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = number != null ? number.hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (contactUri != null ? contactUri.hashCode() : 0); + return result; + } + public static interface RecipientModifiedListener { public void onModified(Recipient recipient); } + } diff --git a/src/org/thoughtcrime/securesms/util/SelectedRecipientsAdapter.java b/src/org/thoughtcrime/securesms/util/SelectedRecipientsAdapter.java new file mode 100644 index 0000000000..5b64589808 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/SelectedRecipientsAdapter.java @@ -0,0 +1,72 @@ +package org.thoughtcrime.securesms.util; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageButton; +import android.widget.TextView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.recipients.Recipient; + +import java.util.ArrayList; +import java.util.List; + +public class SelectedRecipientsAdapter extends ArrayAdapter { + + private ArrayList recipients; + + public SelectedRecipientsAdapter(Context context, int textViewResourceId) { + super(context, textViewResourceId); + } + + public SelectedRecipientsAdapter(Context context, int resource, ArrayList recipients) { + super(context, resource, recipients); + this.recipients = recipients; + } + + @Override + public View getView(final int position, final View convertView, final ViewGroup parent) { + + View v = convertView; + + if (v == null) { + + LayoutInflater vi; + vi = LayoutInflater.from(getContext()); + v = vi.inflate(R.layout.selected_recipient_list_item, null); + + } + + Recipient p = getItem(position); + + if (p != null) { + + TextView name = (TextView) v.findViewById(R.id.name); + TextView phone = (TextView) v.findViewById(R.id.phone); + ImageButton delete = (ImageButton) v.findViewById(R.id.delete); + + if (name != null) { + name.setText(p.getName()); + } + if (phone != null) { + + phone.setText(p.getNumber()); + } + if (delete != null) { + delete.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + recipients.remove(position); + SelectedRecipientsAdapter.this.notifyDataSetChanged(); + } + }); + } + } + + return v; + + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java index 03e8626b49..ae09e985fb 100644 --- a/src/org/thoughtcrime/securesms/util/Util.java +++ b/src/org/thoughtcrime/securesms/util/Util.java @@ -28,6 +28,11 @@ import android.os.Build; import android.provider.Telephony; import org.thoughtcrime.securesms.mms.MmsRadio; +import org.thoughtcrime.securesms.push.PushServiceSocketFactory; +import org.whispersystems.textsecure.directory.Directory; +import org.whispersystems.textsecure.directory.NotInDirectoryException; +import org.whispersystems.textsecure.push.ContactTokenDetails; +import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.util.InvalidNumberException; import org.whispersystems.textsecure.util.PhoneNumberFormatter; @@ -151,6 +156,31 @@ public class Util { (context.getPackageName().equals(Telephony.Sms.getDefaultSmsPackage(context))); } + public static boolean isPushTransport(Context context, String destination) { + Directory directory = Directory.getInstance(context); + + try { + return directory.isActiveNumber(destination); + } catch (NotInDirectoryException e) { + try { + PushServiceSocket socket = PushServiceSocketFactory.create(context); + String contactToken = directory.getToken(destination); + ContactTokenDetails registeredUser = socket.getContactTokenDetails(contactToken); + + if (registeredUser == null) { + registeredUser = new ContactTokenDetails(contactToken); + directory.setToken(registeredUser, false); + return false; + } else { + directory.setToken(registeredUser, true); + return true; + } + } catch (IOException e1) { + Log.w("UniversalTransport", e1); + return false; + } + } + } // public static Bitmap loadScaledBitmap(InputStream src, int targetWidth, int targetHeight) { // return BitmapFactory.decodeStream(src); //// BitmapFactory.Options options = new BitmapFactory.Options();