diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2477ed09b..2c83c8ee9 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -79,7 +79,6 @@ - diff --git a/res/drawable-hdpi/ic_menu_selectall_holo_dark.png b/res/drawable-hdpi/ic_menu_selectall_holo_dark.png new file mode 100644 index 000000000..c2cec7ff1 Binary files /dev/null and b/res/drawable-hdpi/ic_menu_selectall_holo_dark.png differ diff --git a/res/drawable-mdpi/ic_menu_selectall_holo_dark.png b/res/drawable-mdpi/ic_menu_selectall_holo_dark.png new file mode 100644 index 000000000..da64c7556 Binary files /dev/null and b/res/drawable-mdpi/ic_menu_selectall_holo_dark.png differ diff --git a/res/drawable-xhdpi/ic_menu_selectall_holo_dark.png b/res/drawable-xhdpi/ic_menu_selectall_holo_dark.png new file mode 100644 index 000000000..8eef37dd4 Binary files /dev/null and b/res/drawable-xhdpi/ic_menu_selectall_holo_dark.png differ diff --git a/res/layout/conversation_fragment_cab.xml b/res/layout/conversation_fragment_cab.xml new file mode 100644 index 000000000..5f3dfbd9d --- /dev/null +++ b/res/layout/conversation_fragment_cab.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/res/layout/conversation_header_view.xml b/res/layout/conversation_header_view.xml index edd390067..0238d7bc1 100644 --- a/res/layout/conversation_header_view.xml +++ b/res/layout/conversation_header_view.xml @@ -1,5 +1,5 @@ - - + diff --git a/res/menu/conversation_list_batch.xml b/res/menu/conversation_list_batch.xml index 222b64059..c2c4d43c7 100644 --- a/res/menu/conversation_list_batch.xml +++ b/res/menu/conversation_list_batch.xml @@ -1,8 +1,5 @@ - + android:icon="@drawable/ic_menu_selectall_holo_dark" /> - + + + \ No newline at end of file diff --git a/res/menu/conversation_list_context.xml b/res/menu/conversation_list_context.xml deleted file mode 100644 index 3123065bb..000000000 --- a/res/menu/conversation_list_context.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/res/menu/conversation_list_locked.xml b/res/menu/conversation_list_locked.xml deleted file mode 100644 index 2ef6795b9..000000000 --- a/res/menu/conversation_list_locked.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/ConversationListAdapter.java b/src/org/thoughtcrime/securesms/ConversationListAdapter.java index 2c18fe859..dfb6c947d 100644 --- a/src/org/thoughtcrime/securesms/ConversationListAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationListAdapter.java @@ -1,6 +1,6 @@ -/** +/** * 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 @@ -10,15 +10,17 @@ * 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 java.util.Collections; -import java.util.HashSet; -import java.util.Set; +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageRecord; @@ -27,32 +29,30 @@ import org.thoughtcrime.securesms.protocol.Prefix; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; -import android.content.Context; -import android.database.Cursor; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CursorAdapter; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; /** * A CursorAdapter for building a list of conversation threads. - * + * * @author Moxie Marlinspike */ public class ConversationListAdapter extends CursorAdapter { - + private final Context context; - + private final Set batchSet = Collections.synchronizedSet(new HashSet()); private boolean batchMode = false; - + public ConversationListAdapter(Context context, Cursor cursor) { super(context, cursor); this.context = context; } - + @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - ConversationHeaderView view = new ConversationHeaderView(context, batchSet); + ConversationListItem view = new ConversationListItem(context, batchSet); bindView(view, context, cursor); return view; @@ -63,20 +63,20 @@ public class ConversationListAdapter extends CursorAdapter { long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID)); String recipientId = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_IDS)); Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientId); - + long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE)); long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT)); long read = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.READ)); - + MessageRecord message = new MessageRecord(-1, recipients, date, count, read == 1, threadId); setBody(cursor, message); - - ((ConversationHeaderView)view).set(message, batchMode); + + ((ConversationListItem)view).set(message, batchMode); } - + protected void filterBody(MessageRecord message, String body) { if (body == null) body = "(No subject)"; - + if (body.startsWith(Prefix.SYMMETRIC_ENCRYPT) || body.startsWith(Prefix.ASYMMETRIC_ENCRYPT) || body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT)) { message.setBody("Encrypted message, enter passphrase... "); message.setEmphasis(true); @@ -90,12 +90,16 @@ public class ConversationListAdapter extends CursorAdapter { protected void setBody(Cursor cursor, MessageRecord message) { filterBody(message, cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET))); - } - + } + + public void addToBatchSet(long threadId) { + batchSet.add(threadId); + } + public Set getBatchSelections() { return batchSet; } - + public void initializeBatchMode(boolean toggle) { this.batchMode = toggle; unselectAllThreads(); @@ -103,12 +107,12 @@ public class ConversationListAdapter extends CursorAdapter { public void unselectAllThreads() { this.batchSet.clear(); - this.notifyDataSetInvalidated(); + this.notifyDataSetChanged(); } - + public void selectAllThreads() { Cursor cursor = DatabaseFactory.getThreadDatabase(context).getConversationList(); - + try { while (cursor != null && cursor.moveToNext()) { this.batchSet.add(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID))); @@ -117,7 +121,7 @@ public class ConversationListAdapter extends CursorAdapter { if (cursor != null) cursor.close(); } - - this.notifyDataSetInvalidated(); + + this.notifyDataSetChanged(); } } diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java index 21969b262..36f31f513 100644 --- a/src/org/thoughtcrime/securesms/ConversationListFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationListFragment.java @@ -23,35 +23,33 @@ import android.database.Cursor; import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; -import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.CursorAdapter; import android.widget.ListView; import com.actionbarsherlock.app.SherlockListFragment; +import com.actionbarsherlock.view.ActionMode; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.loaders.ConversationListLoader; -import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; import java.util.Set; public class ConversationListFragment extends SherlockListFragment - implements LoaderManager.LoaderCallbacks + implements LoaderManager.LoaderCallbacks, ActionMode.Callback { private ConversationSelectedListener listener; private MasterSecret masterSecret; - private boolean isBatchMode = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { @@ -64,7 +62,7 @@ public class ConversationListFragment extends SherlockListFragment setHasOptionsMenu(true); initializeListAdapter(); - registerForContextMenu(getListView()); + initializeBatchListener(); getLoaderManager().initLoader(0, null, this); } @@ -77,57 +75,18 @@ public class ConversationListFragment extends SherlockListFragment @Override public void onPrepareOptionsMenu(Menu menu) { - MenuInflater inflater = this.getSherlockActivity().getSupportMenuInflater(); - - if (this.isBatchMode) inflater.inflate(R.menu.conversation_list_batch, menu); - else if (this.masterSecret == null) inflater.inflate(R.menu.conversation_list_locked, menu); - else inflater.inflate(R.menu.conversation_list, menu); + if (this.masterSecret != null) { + MenuInflater inflater = this.getSherlockActivity().getSupportMenuInflater(); + inflater.inflate(R.menu.conversation_list, menu); + } super.onPrepareOptionsMenu(menu); } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - - switch (item.getItemId()) { - case R.id.menu_batch_mode: handleSwitchBatchMode(true); return true; - case R.id.menu_delete_selected: handleDeleteAllSelected(); return true; - case R.id.menu_select_all: handleSelectAllThreads(); return true; - case R.id.menu_unselect_all: handleUnselectAllThreads(); return true; - case R.id.menu_normal_mode: handleSwitchBatchMode(false); return true; - } - - return false; - } - - @Override - public void onCreateContextMenu (ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - android.view.MenuInflater inflater = this.getSherlockActivity().getMenuInflater(); - menu.clear(); - - inflater.inflate(R.menu.conversation_list_context, menu); - } - - @Override - public boolean onContextItemSelected(android.view.MenuItem item) { - Cursor cursor = ((CursorAdapter)this.getListAdapter()).getCursor(); - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID)); - String recipientId = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_IDS)); - Recipients recipients = RecipientFactory.getRecipientsForIds(getActivity(), recipientId); - - switch(item.getItemId()) { - case R.id.menu_context_view: handleCreateConversation(threadId, recipients); return true; - case R.id.menu_context_delete: handleDeleteThread(threadId); return true; - } - - return false; - } - @Override public void onListItemClick(ListView l, View v, int position, long id) { - if (v instanceof ConversationHeaderView) { - ConversationHeaderView headerView = (ConversationHeaderView) v; + if (v instanceof ConversationListItem) { + ConversationListItem headerView = (ConversationListItem) v; handleCreateConversation(headerView.getThreadId(), headerView.getRecipients()); } } @@ -137,6 +96,22 @@ public class ConversationListFragment extends SherlockListFragment initializeListAdapter(); } + private void initializeBatchListener() { + getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView arg0, View v, int position, long id) { + ConversationListAdapter adapter = (ConversationListAdapter)getListAdapter(); + getSherlockActivity().startActionMode(ConversationListFragment.this); + + adapter.initializeBatchMode(true); + adapter.addToBatchSet(((ConversationListItem)v).getThreadId()); + adapter.notifyDataSetChanged(); + + return true; + } + }); + } + private void initializeListAdapter() { if (this.masterSecret == null) { this.setListAdapter(new ConversationListAdapter(getActivity(), null)); @@ -147,12 +122,6 @@ public class ConversationListFragment extends SherlockListFragment getLoaderManager().restartLoader(0, null, this); } - private void handleSwitchBatchMode(boolean batchMode) { - this.isBatchMode = batchMode; - ((ConversationListAdapter)this.getListAdapter()).initializeBatchMode(batchMode); - this.getSherlockActivity().invalidateOptionsMenu(); - } - private void handleDeleteAllSelected() { AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); alert.setIcon(android.R.drawable.ic_dialog_alert); @@ -176,24 +145,6 @@ public class ConversationListFragment extends SherlockListFragment alert.show(); } - private void handleDeleteThread(final long threadId) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle("Delete Thread Confirmation"); - builder.setIcon(android.R.drawable.ic_dialog_alert); - builder.setCancelable(true); - builder.setMessage("Are you sure that you want to permanently delete this conversation?"); - builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (threadId > 0) { - DatabaseFactory.getThreadDatabase(getActivity()).deleteConversation(threadId); - } - } - }); - builder.setNegativeButton(R.string.no, null); - builder.show(); - } - private void handleSelectAllThreads() { ((ConversationListAdapter)this.getListAdapter()).selectAllThreads(); } @@ -225,6 +176,39 @@ public class ConversationListFragment extends SherlockListFragment public void onCreateConversation(long threadId, Recipients recipients); } + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = getSherlockActivity().getSupportMenuInflater(); + inflater.inflate(R.menu.conversation_list_batch, menu); + + LayoutInflater layoutInflater = getSherlockActivity().getLayoutInflater(); + View actionModeView = layoutInflater.inflate(R.layout.conversation_fragment_cab, null); + + mode.setCustomView(actionModeView); + + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_select_all: handleSelectAllThreads(); return true; + case R.id.menu_delete_selected: handleDeleteAllSelected(); return true; + } + + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + ((ConversationListAdapter)getListAdapter()).initializeBatchMode(false); + } + } diff --git a/src/org/thoughtcrime/securesms/ConversationHeaderView.java b/src/org/thoughtcrime/securesms/ConversationListItem.java similarity index 95% rename from src/org/thoughtcrime/securesms/ConversationHeaderView.java rename to src/org/thoughtcrime/securesms/ConversationListItem.java index c4db90d67..05757b2f4 100644 --- a/src/org/thoughtcrime/securesms/ConversationHeaderView.java +++ b/src/org/thoughtcrime/securesms/ConversationListItem.java @@ -45,7 +45,7 @@ import java.util.Set; * @author Moxie Marlinspike */ -public class ConversationHeaderView extends RelativeLayout { +public class ConversationListItem extends RelativeLayout { private Set selectedThreads; private Recipients recipients; @@ -58,14 +58,14 @@ public class ConversationHeaderView extends RelativeLayout { private CheckBox checkbox; private QuickContactBadge contactPhoto; - public ConversationHeaderView(Context context, boolean first) { + public ConversationListItem(Context context, boolean first) { this(context, (Set)null); this.first = true; contactPhoto.setVisibility(View.GONE); } - public ConversationHeaderView(Context context, Set selectedThreads) { + public ConversationListItem(Context context, Set selectedThreads) { super(context); LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -82,7 +82,7 @@ public class ConversationHeaderView extends RelativeLayout { intializeListeners(); } - public ConversationHeaderView(Context context, AttributeSet attrs) { + public ConversationListItem(Context context, AttributeSet attrs) { super(context, attrs); } diff --git a/src/org/thoughtcrime/securesms/SendKeyActivity.java b/src/org/thoughtcrime/securesms/SendKeyActivity.java deleted file mode 100644 index 18d5fdd7c..000000000 --- a/src/org/thoughtcrime/securesms/SendKeyActivity.java +++ /dev/null @@ -1,148 +0,0 @@ -/** - * 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 org.thoughtcrime.securesms.components.RecipientsPanel; -import org.thoughtcrime.securesms.contacts.ContactAccessor; -import org.thoughtcrime.securesms.contacts.NameAndNumber; -import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator; -import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientFormattingException; -import org.thoughtcrime.securesms.recipients.Recipients; -import org.thoughtcrime.securesms.util.MemoryCleaner; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.provider.Contacts.People; -import android.util.Log; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.ImageButton; - -/** - * Distinct activity for selecting a contact to initiate a key exchange with. - * Can ordinarily be done through ComposeMessageActivity, this allows for - * initiating key exchanges outside of that context. - * - * @author Moxie Marlinspike - * - */ -public class SendKeyActivity extends Activity { - - private static final int PICK_CONTACT = 1; - - private MasterSecret masterSecret; - private RecipientsPanel recipientsPanel; - private ImageButton addContactButton; - private Button sendButton; - private Button cancelButton; - - @Override - protected void onCreate(Bundle state) { - super.onCreate(state); - setContentView(R.layout.send_key_activity); - - initializeResources(); - initializeDefaults(); - } - - @Override - protected void onDestroy() { - MemoryCleaner.clean(masterSecret); - super.onDestroy(); - } - - @Override - public void onActivityResult(int reqCode, int resultCode, Intent data) { - super.onActivityResult(reqCode, resultCode, data); - - switch (reqCode) { - case (PICK_CONTACT): - if (resultCode == Activity.RESULT_OK) { - if (data.getData() == null) - return; - - NameAndNumber nameAndNumber = ContactAccessor.getInstance().getNameAndNumberFromContact(this, data.getData()); - - if (nameAndNumber != null) - recipientsPanel.addRecipient(nameAndNumber.name, nameAndNumber.number); - } - break; - } - } - - private void initializeDefaults() { - String name = getIntent().getStringExtra("name"); - String number = getIntent().getStringExtra("number"); - - if (number != null) - recipientsPanel.addRecipient(name, number); - } - - private void initializeResources() { - recipientsPanel = (RecipientsPanel)findViewById(R.id.key_recipients); - addContactButton = (ImageButton)findViewById(R.id.contacts_button); - sendButton = (Button)findViewById(R.id.send_key_button); - cancelButton = (Button)findViewById(R.id.cancel_key_button); - masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret"); - - Recipient defaultRecipient = (Recipient)getIntent().getParcelableExtra("recipient"); - - if (defaultRecipient != null) { - recipientsPanel.addRecipient(defaultRecipient.getName(), defaultRecipient.getNumber()); - } - - sendButton.setOnClickListener(new SendButtonListener()); - cancelButton.setOnClickListener(new CancelButtonListener()); - addContactButton.setOnClickListener(new AddRecipientButtonListener()); - } - - // Listeners - - private class AddRecipientButtonListener implements OnClickListener { - public void onClick(View v) { - Intent intent = new Intent(Intent.ACTION_PICK, People.CONTENT_URI); - startActivityForResult(intent, PICK_CONTACT); - } - } - - private class CancelButtonListener implements OnClickListener { - public void onClick(View v) { - SendKeyActivity.this.finish(); - } - } - - private class SendButtonListener implements OnClickListener { - public void onClick(View v) { - try { - Recipients recipients = recipientsPanel.getRecipients(); - - if (recipients.isEmpty()) - return; - - KeyExchangeInitiator.initiate(SendKeyActivity.this, masterSecret, recipients.getPrimaryRecipient(), true); - SendKeyActivity.this.finish(); - } catch (RecipientFormattingException ex) { - Log.w("compose", ex); - //alert - } - } - } -}