From 12dac6ccc3401249f903cf596648df2ffd1d761c Mon Sep 17 00:00:00 2001 From: Santoso Wijaya Date: Thu, 27 Feb 2014 22:44:02 -0800 Subject: [PATCH] ShareActivity, destruction of RecipientsPanel // FREEBIE --- AndroidManifest.xml | 15 +- res/drawable/share_list_divider_shape.xml | 13 ++ .../share_list_divider_shape_dark.xml | 13 ++ res/layout/conversation_activity.xml | 6 - res/layout/share_activity.xml | 13 ++ res/layout/share_fragment.xml | 17 ++ res/layout/share_list_item_view.xml | 44 +++++ res/menu/share.xml | 8 + res/values/attrs.xml | 2 + res/values/strings.xml | 6 +- res/values/themes.xml | 4 + .../securesms/ConversationActivity.java | 73 +------- .../securesms/ConversationFragment.java | 6 +- .../securesms/ConversationListActivity.java | 4 +- .../securesms/GroupCreateActivity.java | 23 ++- .../securesms/NewConversationActivity.java | 9 +- .../securesms/RoutingActivity.java | 46 +++-- .../thoughtcrime/securesms/ShareActivity.java | 173 ++++++++++++++++++ .../thoughtcrime/securesms/ShareFragment.java | 110 +++++++++++ .../securesms/ShareListAdapter.java | 75 ++++++++ .../thoughtcrime/securesms/ShareListItem.java | 155 ++++++++++++++++ .../securesms/components/RecipientsPanel.java | 172 ----------------- .../util/EncryptedCharacterCalculator.java | 3 - 23 files changed, 717 insertions(+), 273 deletions(-) create mode 100644 res/drawable/share_list_divider_shape.xml create mode 100644 res/drawable/share_list_divider_shape_dark.xml create mode 100644 res/layout/share_activity.xml create mode 100644 res/layout/share_fragment.xml create mode 100644 res/layout/share_list_item_view.xml create mode 100644 res/menu/share.xml create mode 100644 src/org/thoughtcrime/securesms/ShareActivity.java create mode 100644 src/org/thoughtcrime/securesms/ShareFragment.java create mode 100644 src/org/thoughtcrime/securesms/ShareListAdapter.java create mode 100644 src/org/thoughtcrime/securesms/ShareListItem.java delete mode 100644 src/org/thoughtcrime/securesms/components/RecipientsPanel.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4ed58e13c9..983d6ec189 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -71,7 +71,7 @@ - + @@ -93,11 +93,18 @@ - + + @@ -126,7 +133,7 @@ diff --git a/res/drawable/share_list_divider_shape.xml b/res/drawable/share_list_divider_shape.xml new file mode 100644 index 0000000000..aa46fbd9f0 --- /dev/null +++ b/res/drawable/share_list_divider_shape.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/share_list_divider_shape_dark.xml b/res/drawable/share_list_divider_shape_dark.xml new file mode 100644 index 0000000000..d33a7d39ea --- /dev/null +++ b/res/drawable/share_list_divider_shape_dark.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml index 95d339b634..0057771baf 100644 --- a/res/layout/conversation_activity.xml +++ b/res/layout/conversation_activity.xml @@ -6,12 +6,6 @@ android:background="?conversation_background" android:orientation="vertical"> - - + + + + + + diff --git a/res/layout/share_fragment.xml b/res/layout/share_fragment.xml new file mode 100644 index 0000000000..10f692b5a1 --- /dev/null +++ b/res/layout/share_fragment.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/res/layout/share_list_item_view.xml b/res/layout/share_list_item_view.xml new file mode 100644 index 0000000000..f9e7cf08c6 --- /dev/null +++ b/res/layout/share_list_item_view.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/res/menu/share.xml b/res/menu/share.xml new file mode 100644 index 0000000000..658c80ab4a --- /dev/null +++ b/res/menu/share.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 4379a43aae..a295288deb 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -9,6 +9,8 @@ + + diff --git a/res/values/strings.xml b/res/values/strings.xml index fc0c87bb7a..ad08d51d57 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -84,13 +84,12 @@ Sorry, the selected audio exceeds message size restrictions. Recipient is not a valid SMS or email address! Message is empty! - FWD Group Conversation Recipients Group Conversation Unnamed Group %d members 1 member - Saving draft... + Saved draft Invalid recipient! Calls Not Supported This device does not appear to support dial actions. @@ -116,6 +115,9 @@ Key exchange message... + + Share with + Export To SD Card? This diff --git a/res/values/themes.xml b/res/values/themes.xml index 2dbc94b835..8215f68a53 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -14,6 +14,8 @@ #ff999999 @drawable/conversation_list_divider_shape + @drawable/share_list_divider_shape + @drawable/actionbar_icon_holo_light @drawable/divet_lower_right_dark @@ -87,6 +89,8 @@ #ffdddddd @drawable/conversation_list_divider_shape_dark + @drawable/share_list_divider_shape_dark + #99ffffff #ffeeeeee #44eeeeee diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index ace667525c..ddaf32c6ca 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -58,7 +58,6 @@ import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.components.EmojiDrawer; import org.thoughtcrime.securesms.components.EmojiToggle; -import org.thoughtcrime.securesms.components.RecipientsPanel; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator; @@ -139,21 +138,18 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi public static final String DRAFT_VIDEO_EXTRA = "draft_video"; public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type"; - private static final int PICK_CONTACT = 1; - private static final int PICK_IMAGE = 2; - private static final int PICK_VIDEO = 3; - private static final int PICK_AUDIO = 4; - private static final int PICK_CONTACT_INFO = 5; - private static final int GROUP_EDIT = 6; + private static final int PICK_IMAGE = 1; + private static final int PICK_VIDEO = 2; + private static final int PICK_AUDIO = 3; + private static final int PICK_CONTACT_INFO = 4; + private static final int GROUP_EDIT = 5; private static final int SEND_ATTRIBUTES[] = new int[]{R.attr.conversation_send_button_push, R.attr.conversation_send_button_sms_secure, R.attr.conversation_send_button_sms_insecure}; private MasterSecret masterSecret; - private RecipientsPanel recipientsPanel; private EditText composeText; - private ImageButton addContactButton; private ImageButton sendButton; private TextView charactersLeft; @@ -200,7 +196,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi @Override protected void onResume() { - initializeRecipientsInput(); super.onResume(); dynamicTheme.onResume(this); @@ -241,11 +236,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi if (data == null || resultCode != RESULT_OK) return; switch (reqCode) { - case PICK_CONTACT: - Recipients recipients = data.getParcelableExtra("recipients"); - if (recipients != null) - recipientsPanel.addRecipients(recipients); - break; case PICK_IMAGE: addAttachmentImage(data.getData()); break; @@ -723,12 +713,10 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi } private void initializeResources() { - recipientsPanel = (RecipientsPanel)findViewById(R.id.recipients); recipients = RecipientFactory.getRecipientsForIds(this, getIntent().getStringExtra(RECIPIENTS_EXTRA), true); threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1); distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT); - addContactButton = (ImageButton)findViewById(R.id.contacts_button); sendButton = (ImageButton)findViewById(R.id.send_button); composeText = (EditText)findViewById(R.id.embedded_text_editor); masterSecret = getIntent().getParcelableExtra(MASTER_SECRET_EXTRA); @@ -746,10 +734,8 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi SendButtonListener sendButtonListener = new SendButtonListener(); ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); - recipientsPanel.setPanelChangeListener(new RecipientsPanelChangeListener()); sendButton.setOnClickListener(sendButtonListener); sendButton.setEnabled(true); - addContactButton.setOnClickListener(new AddRecipientButtonListener()); composeText.setOnKeyListener(composeKeyPressedListener); composeText.addTextChangedListener(composeKeyPressedListener); composeText.setOnEditorActionListener(sendButtonListener); @@ -769,23 +755,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && TextSecurePreferences.isScreenSecurityEnabled(this)) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); } - - if (getIntent().getStringExtra("forwarded_message") != null) { - composeText.setText(getString(R.string.ConversationActivity_forward_message_prefix) + ": " + - getIntent().getStringExtra("forwarded_message")); - } - - } - - private void initializeRecipientsInput() { - if (recipients == null || recipients.isEmpty()) { - recipientsPanel.setVisibility(View.VISIBLE); - } else if (recipients != null) { - recipientsPanel.addRecipients(this.recipients); - } else { - InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); - input.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); - } } private void initializeReceivers() { @@ -945,9 +914,10 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi new AsyncTask() { @Override - protected void onPreExecute() { + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); Toast.makeText(ConversationActivity.this, - R.string.ConversationActivity_saving_draft, + R.string.ConversationActivity_saved_draft, Toast.LENGTH_SHORT).show(); } @@ -1004,13 +974,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi } private Recipients getRecipients() { - try { - if (isExistingConversation()) return this.recipients; - else return recipientsPanel.getRecipients(); - } catch (RecipientFormattingException rfe) { - Log.d(TAG, "Empty list of recipients retrieved from RecipientsPanel."); - return null; - } + return this.recipients; } private String getMessage() throws InvalidMessageException { @@ -1038,7 +1002,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi private void sendComplete(Recipients recipients, long threadId, boolean refreshFragment) { attachmentManager.clear(); - recipientsPanel.disable(); composeText.setText(""); this.recipients = recipients; @@ -1049,7 +1012,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi if (refreshFragment) { fragment.reload(recipients, threadId); - this.recipientsPanel.setVisibility(View.GONE); initializeTitleBar(); initializeSecurity(); } @@ -1114,14 +1076,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi // Listeners - private class AddRecipientButtonListener implements OnClickListener { - @Override - public void onClick(View v) { - Intent intent = new Intent(ConversationActivity.this, ContactSelectionActivity.class); - startActivityForResult(intent, PICK_CONTACT); - } - } - private class AttachmentTypeListener implements DialogInterface.OnClickListener { @Override public void onClick(DialogInterface dialog, int which) { @@ -1129,15 +1083,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi } } - private class RecipientsPanelChangeListener implements RecipientsPanel.RecipientsPanelChangedListener { - @Override - public void onRecipientsPanelUpdate(Recipients recipients) { - initializeSecurity(); - initializeTitleBar(); - calculateCharactersRemaining(); - } - } - private class EmojiToggleListener implements OnClickListener { @Override public void onClick(View v) { diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index 8f2208a8cb..abae3631ff 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -184,9 +184,9 @@ public class ConversationFragment extends SherlockListFragment } private void handleForwardMessage(MessageRecord message) { - Intent composeIntent = new Intent(getActivity(), ConversationActivity.class); - composeIntent.putExtra("forwarded_message", message.getDisplayBody().toString()); - composeIntent.putExtra("master_secret", masterSecret); + Intent composeIntent = new Intent(getActivity(), ShareActivity.class); + composeIntent.putExtra(ConversationActivity.DRAFT_TEXT_EXTRA, message.getDisplayBody().toString()); + composeIntent.putExtra(ShareActivity.MASTER_SECRET_EXTRA, masterSecret); startActivity(composeIntent); } diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index ca4e7c06a1..a8598b04e2 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -64,7 +64,7 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment setContentView(R.layout.conversation_list_activity); - ActionBarUtil.initializeDefaultActionBar(this, getSupportActionBar(), "TextSecure"); + ActionBarUtil.initializeDefaultActionBar(this, getSupportActionBar(), R.string.app_name); initializeNavigationDrawer(); initializeSenderReceiverService(); @@ -287,7 +287,7 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment this.drawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout); this.drawerList = (ListView)findViewById(R.id.left_drawer); - this.masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret"); + this.masterSecret = getIntent().getParcelableExtra("master_secret"); this.fragment = (ConversationListFragment)this.getSupportFragmentManager() .findFragmentById(R.id.fragment_content); diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index c0191edc9c..474faf7c9d 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -1,3 +1,20 @@ +/** + * Copyright (C) 2014 Open 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.Activity; @@ -67,7 +84,11 @@ import ws.com.google.android.mms.MmsException; import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent.GroupContext; - +/** + * Activity to create and update groups + * + * @author Jake McGinty + */ public class GroupCreateActivity extends PassphraseRequiredSherlockFragmentActivity { private final static String TAG = GroupCreateActivity.class.getSimpleName(); diff --git a/src/org/thoughtcrime/securesms/NewConversationActivity.java b/src/org/thoughtcrime/securesms/NewConversationActivity.java index 57ca481d78..64bd8a9d9f 100644 --- a/src/org/thoughtcrime/securesms/NewConversationActivity.java +++ b/src/org/thoughtcrime/securesms/NewConversationActivity.java @@ -19,16 +19,12 @@ package org.thoughtcrime.securesms; 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; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; -import org.thoughtcrime.securesms.components.SingleRecipientPanel; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.ThreadDatabase; @@ -40,7 +36,6 @@ import org.thoughtcrime.securesms.util.ActionBarUtil; import org.thoughtcrime.securesms.util.DirectoryHelper; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.NumberUtil; -import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.crypto.MasterSecret; @@ -159,6 +154,10 @@ public class NewConversationActivity extends PassphraseRequiredSherlockFragmentA Intent intent = new Intent(this, ConversationActivity.class); intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.toIdString()); intent.putExtra(ConversationActivity.MASTER_SECRET_EXTRA, masterSecret); + intent.putExtra(ConversationActivity.DRAFT_TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.DRAFT_TEXT_EXTRA)); + intent.putExtra(ConversationActivity.DRAFT_AUDIO_EXTRA, getIntent().getParcelableExtra(ConversationActivity.DRAFT_AUDIO_EXTRA)); + intent.putExtra(ConversationActivity.DRAFT_VIDEO_EXTRA, getIntent().getParcelableExtra(ConversationActivity.DRAFT_VIDEO_EXTRA)); + intent.putExtra(ConversationActivity.DRAFT_IMAGE_EXTRA, getIntent().getParcelableExtra(ConversationActivity.DRAFT_IMAGE_EXTRA)); long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipients); intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread); intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT); diff --git a/src/org/thoughtcrime/securesms/RoutingActivity.java b/src/org/thoughtcrime/securesms/RoutingActivity.java index 83c3675df5..711d237e0a 100644 --- a/src/org/thoughtcrime/securesms/RoutingActivity.java +++ b/src/org/thoughtcrime/securesms/RoutingActivity.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms; import android.content.Intent; import android.net.Uri; +import android.os.Build; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -126,16 +127,25 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity { } private void handleDisplayConversationOrList() { - ConversationParameters parameters = getConversationParameters(); - - Intent intent; - - if (isShareAction() || parameters.recipients != null) { + final ConversationParameters parameters = getConversationParameters(); + + final Intent intent; + if (isShareAction()) { + intent = getShareIntent(parameters); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + } + } else if (parameters.recipients != null) { intent = getConversationIntent(parameters); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); } else { intent = getConversationListIntent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + intent.addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME); + } } - startActivity(intent); finish(); } @@ -153,6 +163,20 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity { return intent; } + private Intent getShareIntent(ConversationParameters parameters) { + Intent intent = new Intent(this, ShareActivity.class); + intent.putExtra("master_secret", masterSecret); + + if (parameters != null) { + intent.putExtra(ConversationActivity.DRAFT_TEXT_EXTRA, parameters.draftText); + intent.putExtra(ConversationActivity.DRAFT_IMAGE_EXTRA, parameters.draftImage); + intent.putExtra(ConversationActivity.DRAFT_AUDIO_EXTRA, parameters.draftAudio); + intent.putExtra(ConversationActivity.DRAFT_VIDEO_EXTRA, parameters.draftVideo); + } + + return intent; + } + private Intent getConversationListIntent() { Intent intent = new Intent(this, ConversationListActivity.class); intent.putExtra("master_secret", masterSecret); @@ -197,8 +221,8 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity { } private ConversationParameters getConversationParametersForSendAction() { - Recipients recipients = null; - long threadId = getIntent().getLongExtra("thread_id", -1); + Recipients recipients; + long threadId = getIntent().getLongExtra("thread_id", -1); try { String data = getIntent().getData().getSchemeSpecificPart(); @@ -220,11 +244,11 @@ public class RoutingActivity extends PassphraseRequiredSherlockActivity { if ("text/plain".equals(type)) { draftText = getIntent().getStringExtra(Intent.EXTRA_TEXT); - } else if (type.startsWith("image/")) { + } else if (type != null && type.startsWith("image/")) { draftImage = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); - } else if (type.startsWith("audio/")) { + } else if (type != null && type.startsWith("audio/")) { draftAudio = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); - } else if (type.startsWith("video/")) { + } else if (type != null && type.startsWith("video/")) { draftVideo = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); } diff --git a/src/org/thoughtcrime/securesms/ShareActivity.java b/src/org/thoughtcrime/securesms/ShareActivity.java new file mode 100644 index 0000000000..cba5b018c2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/ShareActivity.java @@ -0,0 +1,173 @@ +/** + * Copyright (C) 2014 Open 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.ContentObserver; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.ListView; + +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.service.DirectoryRefreshListener; +import org.thoughtcrime.securesms.util.ActionBarUtil; +import org.thoughtcrime.securesms.util.DynamicLanguage; +import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.MemoryCleaner; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.textsecure.crypto.MasterSecret; + +/** + * An activity to quickly share content with contacts + * + * @author Jake McGinty + */ +public class ShareActivity extends PassphraseRequiredSherlockFragmentActivity + implements ShareFragment.ConversationSelectedListener + { + public final static String MASTER_SECRET_EXTRA = "master_secret"; + + private final DynamicTheme dynamicTheme = new DynamicTheme (); + private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); + + private ShareFragment fragment; + private MasterSecret masterSecret; + + @Override + public void onCreate(Bundle icicle) { + dynamicTheme.onCreate(this); + dynamicLanguage.onCreate(this); + super.onCreate(icicle); + + setContentView(R.layout.share_activity); + ActionBarUtil.initializeDefaultActionBar(this, getSupportActionBar(), R.string.ShareActivity_share_with); + initializeResources(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @Override + public void onResume() { + super.onResume(); + dynamicTheme.onResume(this); + dynamicLanguage.onResume(this); + } + + @Override + public void onPause() { + super.onPause(); + if (!isFinishing()) finish(); + } + + @Override + public void onDestroy() { + MemoryCleaner.clean(masterSecret); + super.onDestroy(); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuInflater inflater = this.getSupportMenuInflater(); + menu.clear(); + + inflater.inflate(R.menu.share, menu); + super.onPrepareOptionsMenu(menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + switch (item.getItemId()) { + case R.id.menu_new_message: handleNewConversation(); return true; + case android.R.id.home: finish(); return true; + } + return false; + } + + @Override + public void onMasterSecretCleared() { + startActivity(new Intent(this, RoutingActivity.class)); + super.onMasterSecretCleared(); + } + + private void handleNewConversation() { + Intent intent = getBaseShareIntent(NewConversationActivity.class); + startActivity(intent); + } + + @Override + public void onCreateConversation(long threadId, Recipients recipients, int distributionType) { + createConversation(threadId, recipients, distributionType); + } + + private void createConversation(long threadId, Recipients recipients, int distributionType) { + final Intent intent = getBaseShareIntent(ConversationActivity.class); + intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.toIdString()); + intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); + intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType); + + startActivity(intent); + } + + private void initializeResources() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && TextSecurePreferences.isScreenSecurityEnabled(this)) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE); + } + + this.masterSecret = getIntent().getParcelableExtra(MASTER_SECRET_EXTRA); + + this.fragment = (ShareFragment)this.getSupportFragmentManager() + .findFragmentById(R.id.fragment_content); + + this.fragment.setMasterSecret(masterSecret); + } + + private Intent getBaseShareIntent(final Class target) { + final Intent intent = new Intent(this, target); + final Intent originalIntent = getIntent(); + final String draftText = originalIntent.getStringExtra(ConversationActivity.DRAFT_TEXT_EXTRA); + final Uri draftImage = originalIntent.getParcelableExtra(ConversationActivity.DRAFT_IMAGE_EXTRA); + final Uri draftAudio = originalIntent.getParcelableExtra(ConversationActivity.DRAFT_AUDIO_EXTRA); + final Uri draftVideo = originalIntent.getParcelableExtra(ConversationActivity.DRAFT_VIDEO_EXTRA); + + intent.putExtra(ConversationActivity.DRAFT_TEXT_EXTRA, draftText); + intent.putExtra(ConversationActivity.DRAFT_IMAGE_EXTRA, draftImage); + intent.putExtra(ConversationActivity.DRAFT_AUDIO_EXTRA, draftAudio); + intent.putExtra(ConversationActivity.DRAFT_VIDEO_EXTRA, draftVideo); + intent.putExtra(NewConversationActivity.MASTER_SECRET_EXTRA, masterSecret); + + return intent; + } +} diff --git a/src/org/thoughtcrime/securesms/ShareFragment.java b/src/org/thoughtcrime/securesms/ShareFragment.java new file mode 100644 index 0000000000..320a8c6248 --- /dev/null +++ b/src/org/thoughtcrime/securesms/ShareFragment.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2014 Open 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.Activity; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +import com.actionbarsherlock.app.SherlockListFragment; + +import org.thoughtcrime.securesms.database.loaders.ConversationListLoader; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.whispersystems.textsecure.crypto.MasterSecret; + +/** + * A fragment to select and share to open conversations + * + * @author Jake McGinty + */ +public class ShareFragment extends SherlockListFragment implements LoaderManager.LoaderCallbacks { + + private ConversationSelectedListener listener; + private MasterSecret masterSecret; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { + return inflater.inflate(R.layout.share_fragment, container, false); + } + + @Override + public void onActivityCreated(Bundle bundle) { + super.onActivityCreated(bundle); + + initializeListAdapter(); + getLoaderManager().initLoader(0, null, this); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + this.listener = (ConversationSelectedListener) activity; + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + if (v instanceof ShareListItem) { + ShareListItem headerView = (ShareListItem) v; + + handleCreateConversation(headerView.getThreadId(), headerView.getRecipients(), + headerView.getDistributionType()); + } + } + + public void setMasterSecret(MasterSecret masterSecret) { + if (this.masterSecret != masterSecret) { + this.masterSecret = masterSecret; + initializeListAdapter(); + } + } + + private void initializeListAdapter() { + this.setListAdapter(new ShareListAdapter(getActivity(), null, masterSecret)); + getListView().setRecyclerListener((ShareListAdapter) getListAdapter()); + getLoaderManager().restartLoader(0, null, this); + } + + private void handleCreateConversation(long threadId, Recipients recipients, int distributionType) { + listener.onCreateConversation(threadId, recipients, distributionType); + } + + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) { + return new ConversationListLoader(getActivity(), null); + } + + @Override + public void onLoadFinished(Loader arg0, Cursor cursor) { + ((CursorAdapter)getListAdapter()).changeCursor(cursor); + } + + @Override + public void onLoaderReset(Loader arg0) { + ((CursorAdapter)getListAdapter()).changeCursor(null); + } + + public interface ConversationSelectedListener { + public void onCreateConversation(long threadId, Recipients recipients, int distributionType); + } +} diff --git a/src/org/thoughtcrime/securesms/ShareListAdapter.java b/src/org/thoughtcrime/securesms/ShareListAdapter.java new file mode 100644 index 0000000000..100a67b2cc --- /dev/null +++ b/src/org/thoughtcrime/securesms/ShareListAdapter.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2014 Open 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.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.model.ThreadRecord; +import org.whispersystems.textsecure.crypto.MasterCipher; +import org.whispersystems.textsecure.crypto.MasterSecret; + +/** + * A CursorAdapter for building a list of open conversations + * + * @author Jake McGinty + */ +public class ShareListAdapter extends CursorAdapter implements AbsListView.RecyclerListener { + + private final ThreadDatabase threadDatabase; + private final MasterCipher masterCipher; + private final Context context; + private final LayoutInflater inflater; + + public ShareListAdapter(Context context, Cursor cursor, MasterSecret masterSecret) { + super(context, cursor, 0); + + if (masterSecret != null) this.masterCipher = new MasterCipher(masterSecret); + else this.masterCipher = null; + + this.context = context; + this.threadDatabase = DatabaseFactory.getThreadDatabase(context); + this.inflater = LayoutInflater.from(context); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return inflater.inflate(R.layout.share_list_item_view, parent, false); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (masterCipher != null) { + ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor, masterCipher); + ThreadRecord record = reader.getCurrent(); + + ((ShareListItem)view).set(record); + } + } + + @Override + public void onMovedToScrapHeap(View view) { + ((ShareListItem)view).unbind(); + } +} diff --git a/src/org/thoughtcrime/securesms/ShareListItem.java b/src/org/thoughtcrime/securesms/ShareListItem.java new file mode 100644 index 0000000000..119529ba9c --- /dev/null +++ b/src/org/thoughtcrime/securesms/ShareListItem.java @@ -0,0 +1,155 @@ +/** + * Copyright (C) 2014 Open 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.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.provider.Contacts.Intents; +import android.provider.ContactsContract.QuickContact; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.StyleSpan; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import org.thoughtcrime.securesms.database.model.ThreadRecord; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.BitmapUtil; +import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.Emoji; + +import java.util.Set; + +/** + * A simple view to show the recipients of an open conversation + * + * @author Jake McGinty + */ +public class ShareListItem extends RelativeLayout + implements Recipient.RecipientModifiedListener +{ + private final static String TAG = ShareListItem.class.getSimpleName(); + + private Context context; + private Recipients recipients; + private long threadId; + private TextView fromView; + + private ImageView contactPhotoImage; + + private final Handler handler = new Handler(); + private int distributionType; + + public ShareListItem(Context context) { + super(context); + this.context = context; + } + + public ShareListItem(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + } + + @Override + protected void onFinishInflate() { + this.fromView = (TextView) findViewById(R.id.from); + this.contactPhotoImage = (ImageView) findViewById(R.id.contact_photo_image); + } + + public void set(ThreadRecord thread) { + this.recipients = thread.getRecipients(); + this.threadId = thread.getThreadId(); + this.distributionType = thread.getDistributionType(); + + this.recipients.addListener(this); + this.fromView.setText(formatFrom(recipients)); + + setBackground(); + setContactPhoto(this.recipients.getPrimaryRecipient()); + } + + public void unbind() { + if (this.recipients != null) this.recipients.removeListener(this); + } + + private void setContactPhoto(final Recipient recipient) { + if (recipient == null) return; + contactPhotoImage.setImageBitmap(BitmapUtil.getCircleCroppedBitmap(recipient.getContactPhoto())); + } + + private void setBackground() { + int[] attributes = new int[]{R.attr.conversation_list_item_background_read}; + TypedArray drawables = context.obtainStyledAttributes(attributes); + + setBackgroundDrawable(drawables.getDrawable(0)); + + drawables.recycle(); + } + + private CharSequence formatFrom(Recipients from) { + final String fromString; + final boolean isUnnamedGroup = from.isGroupRecipient() && TextUtils.isEmpty(from.getPrimaryRecipient().getName()); + if (isUnnamedGroup) { + fromString = context.getString(R.string.ConversationActivity_unnamed_group); + } else { + fromString = from.toShortString(); + } + SpannableStringBuilder builder = new SpannableStringBuilder(fromString); + + final int typeface; + if (isUnnamedGroup) typeface = Typeface.ITALIC; + else typeface = Typeface.NORMAL; + + builder.setSpan(new StyleSpan(typeface), 0, builder.length(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + + return builder; + } + + public Recipients getRecipients() { + return recipients; + } + + public long getThreadId() { + return threadId; + } + + public int getDistributionType() { + return distributionType; + } + + @Override + public void onModified(Recipient recipient) { + handler.post(new Runnable() { + @Override + public void run() { + fromView.setText(formatFrom(recipients)); + setContactPhoto(recipients.getPrimaryRecipient()); + } + }); + } +} diff --git a/src/org/thoughtcrime/securesms/components/RecipientsPanel.java b/src/org/thoughtcrime/securesms/components/RecipientsPanel.java deleted file mode 100644 index 40f29c5206..0000000000 --- a/src/org/thoughtcrime/securesms/components/RecipientsPanel.java +++ /dev/null @@ -1,172 +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.components; - -import android.content.Context; -import android.os.Build; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.RelativeLayout; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.contacts.ContactAccessor; -import org.thoughtcrime.securesms.contacts.RecipientsAdapter; -import org.thoughtcrime.securesms.contacts.RecipientsEditor; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientFactory; -import org.thoughtcrime.securesms.recipients.RecipientFormattingException; -import org.thoughtcrime.securesms.recipients.Recipients; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** - * Panel component combining both an editable field with a button for - * a list-based contact selector. - * - * @author Moxie Marlinspike - */ -public class RecipientsPanel extends RelativeLayout { - - private RecipientsPanelChangedListener panelChangeListener; - private RecipientsEditor recipientsText; - private View panel; - - private static final int RECIPIENTS_MAX_LENGTH = 312; - - public RecipientsPanel(Context context) { - super(context); - initialize(); - } - - public RecipientsPanel(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(); - } - - public RecipientsPanel(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(); - } - - public void addRecipient(String name, String number) { - if (name != null) recipientsText.append(sanitizeRecipientName(name) + "< " + number + ">, "); - 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) { - Set panelRecipients; - - try { - panelRecipients = new HashSet(getRecipients().getRecipientsList()); - } catch (RecipientFormattingException e) { - Log.w("RecipientsPanel", e); - panelRecipients = new HashSet(); - } - - List recipientList = recipients.getRecipientsList(); - Iterator iterator = recipientList.iterator(); - - while (iterator.hasNext()) { - Recipient recipient = iterator.next(); - if (!panelRecipients.contains(recipient)) { - addRecipient(recipient.getName(), recipient.getNumber()); - } - } - } - - public Recipients getRecipients() throws RecipientFormattingException { - String rawText = recipientsText.getText().toString(); - Recipients recipients = RecipientFactory.getRecipientsFromString(getContext(), rawText, false); - - if (recipients.isEmpty()) - throw new RecipientFormattingException("Recipient List Is Empty!"); - - return recipients; - } - - public void disable() { - recipientsText.setText(""); - panel.setVisibility(View.GONE); - } - - public void setPanelChangeListener(RecipientsPanelChangedListener panelChangeListener) { - this.panelChangeListener = panelChangeListener; - } - - private void initialize() { - LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.recipients_panel, this, true); - - View imageButton = findViewById(R.id.contacts_button); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) - ((MarginLayoutParams) imageButton.getLayoutParams()).topMargin = 0; - - panel = findViewById(R.id.recipients_panel); - initRecipientsEditor(); - } - - private void initRecipientsEditor() { - Recipients recipients = null; - recipientsText = (RecipientsEditor)findViewById(R.id.recipients_text); - - try { - recipients = getRecipients(); - } catch (RecipientFormattingException e) { - recipients = new Recipients( new LinkedList() ); - } - - recipientsText.setAdapter(new RecipientsAdapter(this.getContext())); - recipientsText.populate(recipients); - recipientsText.setOnFocusChangeListener(new FocusChangedListener()); - } - - private static String sanitizeRecipientName(String name) { - return name.replaceAll("[,<>]", ""); - } - - private class FocusChangedListener implements View.OnFocusChangeListener { - public void onFocusChange(View v, boolean hasFocus) { - if (!hasFocus && (panelChangeListener != null)) { - try { - panelChangeListener.onRecipientsPanelUpdate(getRecipients()); - } catch (RecipientFormattingException rfe) { - panelChangeListener.onRecipientsPanelUpdate(null); - } - } - } - } - - public interface RecipientsPanelChangedListener { - public void onRecipientsPanelUpdate(Recipients recipients); - } - -} diff --git a/src/org/thoughtcrime/securesms/util/EncryptedCharacterCalculator.java b/src/org/thoughtcrime/securesms/util/EncryptedCharacterCalculator.java index 00ce9b054f..9761d96f57 100644 --- a/src/org/thoughtcrime/securesms/util/EncryptedCharacterCalculator.java +++ b/src/org/thoughtcrime/securesms/util/EncryptedCharacterCalculator.java @@ -16,8 +16,6 @@ */ package org.thoughtcrime.securesms.util; -import android.util.Log; - import org.thoughtcrime.securesms.sms.SmsTransportDetails; public class EncryptedCharacterCalculator extends CharacterCalculator { @@ -37,7 +35,6 @@ public class EncryptedCharacterCalculator extends CharacterCalculator { spilloverMessagesSpent++; int charactersRemaining = (SmsTransportDetails.MULTI_MESSAGE_MAX_BYTES * spilloverMessagesSpent) - spillover; - Log.w("EncryptedCharacterCalculator", "charactersRemaining: " + charactersRemaining); return new CharacterState(spilloverMessagesSpent+1, charactersRemaining, SmsTransportDetails.MULTI_MESSAGE_MAX_BYTES); }