diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml index 317ec1737..587422280 100644 --- a/res/layout/conversation_activity.xml +++ b/res/layout/conversation_activity.xml @@ -21,7 +21,6 @@ android:layout_height="match_parent" android:layout_above="@+id/bottom_container" /> - - + + + + diff --git a/res/layout/transport_selection_list_item.xml b/res/layout/transport_selection_list_item.xml new file mode 100644 index 000000000..310c9ab80 --- /dev/null +++ b/res/layout/transport_selection_list_item.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/res/values/arrays.xml b/res/values/arrays.xml index c9de19381..a85647fe8 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -142,4 +142,46 @@ 3000,3000 custom + + + @string/ConversationActivity_transport_insecure_sms + @string/ConversationActivity_transport_secure_sms + @string/ConversationActivity_transport_textsecure + + + + @string/ConversationActivity_transport_insecure_mms + @string/ConversationActivity_transport_secure_mms + @string/ConversationActivity_transport_textsecure + + + + @string/conversation_activity__type_message_sms_insecure + @string/conversation_activity__type_message_sms_secure + @string/conversation_activity__type_message_push + + + + @string/conversation_activity__type_message_mms_insecure + @string/conversation_activity__type_message_mms_secure + @string/conversation_activity__type_message_push + + + + insecure_sms + secure_sms + textsecure + + + + @drawable/ic_send_sms_insecure + @drawable/ic_send_sms_secure + @drawable/ic_send_push + + + + @drawable/ic_send_sms_insecure_dark + @drawable/ic_send_sms_secure + @drawable/ic_send_push + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index fc86871c2..8ad1b43a6 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -29,6 +29,8 @@ + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index bf061aade..8896b5f40 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -6,6 +6,9 @@ 2dp 8dp 12sp + 200sp + 0dp + 1dp 3dp 2dp 50dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 7753ab402..64b638dea 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -101,6 +101,11 @@ This device does not appear to support dial actions. Leave group? Are you sure you want to leave this group? + Insecure SMS + Insecure MMS + Secure SMS + Secure MMS + TextSecure Message details diff --git a/res/values/themes.xml b/res/values/themes.xml index 4bb571337..919446e66 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -45,6 +45,8 @@ @drawable/ic_send_sms_secure @drawable/ic_send_sms_insecure @drawable/ic_delivery_delivered + @array/transport_selection_icons_light + @color/white @drawable/ic_emoji_dark @drawable/ic_ime_dark @@ -139,6 +141,8 @@ @drawable/ic_send_sms_secure @drawable/ic_send_sms_insecure_dark @drawable/ic_delivery_delivered_dark + @array/transport_selection_icons_dark + @color/black @drawable/ic_emoji_light @drawable/ic_ime_light diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index fd16b9661..6cc154c18 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -23,7 +23,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; @@ -34,18 +33,15 @@ import android.provider.ContactsContract; import android.telephony.PhoneNumberUtils; import android.text.Editable; import android.text.InputType; -import android.text.Spannable; -import android.text.SpannableString; import android.text.TextUtils; import android.text.TextWatcher; -import android.text.style.RelativeSizeSpan; import android.util.Log; -import android.view.ContextMenu; import android.view.ContextThemeWrapper; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; @@ -53,7 +49,6 @@ import android.view.View.OnKeyListener; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; -import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; @@ -61,6 +56,7 @@ import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.components.EmojiDrawer; import org.thoughtcrime.securesms.components.EmojiToggle; +import org.thoughtcrime.securesms.components.SendButton; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator; @@ -113,6 +109,7 @@ import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.api.push.PushAddress; import java.io.IOException; +import java.util.LinkedList; import java.util.List; import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; @@ -147,14 +144,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity 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 EditText composeText; - private ImageButton sendButton; - private TextView charactersLeft; + private MasterSecret masterSecret; + private EditText composeText; + private SendButton sendButton; + private TextView charactersLeft; private AttachmentTypeSelectorAdapter attachmentAdapter; private AttachmentManager attachmentManager; @@ -317,49 +310,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity return false; } - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - if (isEncryptedConversation && isSingleConversation()) { - SessionStore sessionStore = new TextSecureSessionStore(this, masterSecret); - Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient(); - boolean isPushDestination = DirectoryHelper.isPushDestination(this, getRecipients()); - boolean isSecureDestination = isSingleConversation() && sessionStore.containsSession(primaryRecipient.getRecipientId(), - PushAddress.DEFAULT_DEVICE_ID); - - getMenuInflater().inflate(R.menu.conversation_button_context, menu); - - if (attachmentManager.isAttachmentPresent()) { - menu.removeItem(R.id.menu_context_send_encrypted_sms); - menu.removeItem(R.id.menu_context_send_unencrypted_sms); - } else { - menu.removeItem(R.id.menu_context_send_encrypted_mms); - menu.removeItem(R.id.menu_context_send_unencrypted_mms); - } - - if (!isPushDestination) { - menu.removeItem(R.id.menu_context_send_push); - } - - if (!isSecureDestination) { - menu.removeItem(R.id.menu_context_send_encrypted_mms); - menu.removeItem(R.id.menu_context_send_encrypted_sms); - } - } - } - - @Override - public boolean onContextItemSelected(android.view.MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_context_send_push: sendMessage(false, false); return true; - case R.id.menu_context_send_encrypted_mms: - case R.id.menu_context_send_encrypted_sms: sendMessage(false, true); return true; - case R.id.menu_context_send_unencrypted_mms: - case R.id.menu_context_send_unencrypted_sms: sendMessage(true, true); return true; - } - - return false; - } - @Override public void onBackPressed() { if (emojiDrawer.getVisibility() == View.VISIBLE) { @@ -700,7 +650,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void initializeSecurity() { - TypedArray drawables = obtainStyledAttributes(SEND_ATTRIBUTES); SessionStore sessionStore = new TextSecureSessionStore(this, masterSecret); Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient(); boolean isPushDestination = DirectoryHelper.isPushDestination(this, getRecipients()); @@ -715,27 +664,21 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity this.characterCalculator = new CharacterCalculator(); } - if (isPushDestination) { - sendButton.setImageDrawable(drawables.getDrawable(0)); - setComposeHint(getString(R.string.conversation_activity__type_message_push)); - } else if (isSecureDestination) { - sendButton.setImageDrawable(drawables.getDrawable(1)); - setComposeHint(attachmentManager.isAttachmentPresent() ? - getString(R.string.conversation_activity__type_message_mms_secure) : - getString(R.string.conversation_activity__type_message_sms_secure)); - } else { - sendButton.setImageDrawable(drawables.getDrawable(2)); - setComposeHint((attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient()) ? - getString(R.string.conversation_activity__type_message_mms_insecure) : - getString(R.string.conversation_activity__type_message_sms_insecure)); - } + sendButton.initializeAvailableTransports(!recipients.isSingleRecipient() || attachmentManager.isAttachmentPresent()); + if (!isPushDestination ) sendButton.disableTransport("textsecure"); + if (!isSecureDestination) sendButton.disableTransport("secure_sms"); - drawables.recycle(); + if (isPushDestination) { + sendButton.setDefaultTransport("textsecure"); + } else if (isSecureDestination) { + sendButton.setDefaultTransport("secure_sms"); + } else { + sendButton.setDefaultTransport("insecure_sms"); + } calculateCharactersRemaining(); } - private void initializeMmsEnabledCheck() { new AsyncTask() { @Override @@ -763,7 +706,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1); distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT); - sendButton = (ImageButton)findViewById(R.id.send_button); + sendButton = (SendButton)findViewById(R.id.send_button); composeText = (EditText)findViewById(R.id.embedded_text_editor); masterSecret = getIntent().getParcelableExtra(MASTER_SECRET_EXTRA); charactersLeft = (TextView)findViewById(R.id.space_left); @@ -782,6 +725,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity sendButton.setOnClickListener(sendButtonListener); sendButton.setEnabled(true); + sendButton.setComposeTextView(composeText); composeText.setOnKeyListener(composeKeyPressedListener); composeText.addTextChangedListener(composeKeyPressedListener); composeText.setOnEditorActionListener(sendButtonListener); @@ -796,8 +740,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity initializeTitleBar(); } }); - - registerForContextMenu(sendButton); } private void initializeReceivers() { @@ -1056,20 +998,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity fragment.scrollToBottom(); } - - private void sendMessage(boolean forcePlaintext, boolean forceSms) { + private void sendMessage() { try { final Recipients recipients = getRecipients(); - if (recipients == null) + if (recipients == null) { throw new RecipientFormattingException("Badly formatted"); + } if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) { handleManualMmsRequired(); } else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) { - sendMediaMessage(forcePlaintext, forceSms); + sendMediaMessage(sendButton.getSelectedTransport().isForcedPlaintext(), sendButton.getSelectedTransport().isForcedSms()); } else { - sendTextMessage(forcePlaintext, forceSms); + sendTextMessage(sendButton.getSelectedTransport().isForcedPlaintext(), sendButton.getSelectedTransport().isForcedSms()); } } catch (RecipientFormattingException ex) { Toast.makeText(ConversationActivity.this, @@ -1171,7 +1113,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener { @Override public void onClick(View v) { - sendMessage(false, false); + sendMessage(); } @Override @@ -1239,17 +1181,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity this.composeText.setText(text); } - private void setComposeHint(String hint){ - if (hint == null) { - this.composeText.setHint(null); - } else { - SpannableString span = new SpannableString(hint); - span.setSpan(new RelativeSizeSpan(0.8f), 0, hint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); - this.composeText.setHint(span); - this.sendButton.setContentDescription(hint); - } - } - @Override public void onAttachmentChanged() { initializeSecurity(); diff --git a/src/org/thoughtcrime/securesms/TransportOption.java b/src/org/thoughtcrime/securesms/TransportOption.java new file mode 100644 index 000000000..98c75316f --- /dev/null +++ b/src/org/thoughtcrime/securesms/TransportOption.java @@ -0,0 +1,24 @@ +package org.thoughtcrime.securesms; + +public class TransportOption { + public int drawable; + public String text; + public String key; + public String composeHint; + + public TransportOption(String key, int drawable, String text, String composeHint) { + this.key = key; + this.drawable = drawable; + this.text = text; + this.composeHint = composeHint; + } + + public boolean isForcedPlaintext() { + return key.equals("insecure_sms"); + } + + public boolean isForcedSms() { + return key.equals("insecure_sms") || key.equals("secure_sms"); + } +} + diff --git a/src/org/thoughtcrime/securesms/TransportOptions.java b/src/org/thoughtcrime/securesms/TransportOptions.java new file mode 100644 index 000000000..268d7cc6c --- /dev/null +++ b/src/org/thoughtcrime/securesms/TransportOptions.java @@ -0,0 +1,148 @@ +package org.thoughtcrime.securesms; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.BitmapDrawable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.RelativeSizeSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ListView; +import android.widget.PopupWindow; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class TransportOptions { + private static final String TAG = TransportOptions.class.getSimpleName(); + + private final Context context; + private PopupWindow transportPopup; + private final List enabledTransports = new ArrayList(); + private final Map transportMetadata = new HashMap(); + private String selectedTransport; + private boolean transportOverride = false; + private OnTransportChangedListener listener; + + public TransportOptions(Context context) { + this.context = context; + } + + private void initializeTransportPopup() { + if (transportPopup == null) { + final View selectionMenu = LayoutInflater.from(context).inflate(R.layout.transport_selection, null); + final ListView list = (ListView) selectionMenu.findViewById(R.id.transport_selection_list); + + final TransportOptionsAdapter adapter = new TransportOptionsAdapter(context, enabledTransports, transportMetadata); + + list.setAdapter(adapter); + transportPopup = new PopupWindow(selectionMenu); + transportPopup.setFocusable(true); + transportPopup.setBackgroundDrawable(new BitmapDrawable(context.getResources(), "")); + transportPopup.setOutsideTouchable(true); + transportPopup.setWindowLayoutMode(0, WindowManager.LayoutParams.WRAP_CONTENT); + transportPopup.setWidth(context.getResources().getDimensionPixelSize(R.dimen.transport_selection_popup_width)); + list.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + transportOverride = true; + setTransport((TransportOption) adapter.getItem(position)); + transportPopup.dismiss(); + } + }); + } else { + final ListView list = (ListView) transportPopup.getContentView().findViewById(R.id.transport_selection_list); + final TransportOptionsAdapter adapter = (TransportOptionsAdapter) list.getAdapter(); + adapter.setEnabledTransports(enabledTransports); + adapter.notifyDataSetInvalidated(); + } + } + + public void initializeAvailableTransports(boolean isMediaMessage) { + String[] entryArray = (isMediaMessage) + ? context.getResources().getStringArray(R.array.transport_selection_entries_media) + : context.getResources().getStringArray(R.array.transport_selection_entries_text); + + String[] composeHintArray = (isMediaMessage) + ? context.getResources().getStringArray(R.array.transport_selection_entries_compose_media) + : context.getResources().getStringArray(R.array.transport_selection_entries_compose_text); + + final String[] valuesArray = context.getResources().getStringArray(R.array.transport_selection_values); + + final int[] attrs = new int[]{R.attr.conversation_transport_indicators}; + final TypedArray iconArray = context.obtainStyledAttributes(attrs); + final int iconArrayResource = iconArray.getResourceId(0, -1); + final TypedArray icons = context.getResources().obtainTypedArray(iconArrayResource); + + enabledTransports.clear(); + for (int i=0; i getEnabledTransports() { + return enabledTransports; + } + + private void updateViews() { + if (selectedTransport == null) return; + + if (listener != null) { + listener.onChange(getSelectedTransport()); + } + } + + public void setOnTransportChangedListener(OnTransportChangedListener listener) { + this.listener = listener; + } + + public interface OnTransportChangedListener { + public void onChange(TransportOption newTransport); + } +} diff --git a/src/org/thoughtcrime/securesms/TransportOptionsAdapter.java b/src/org/thoughtcrime/securesms/TransportOptionsAdapter.java new file mode 100644 index 000000000..b9435fea2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/TransportOptionsAdapter.java @@ -0,0 +1,71 @@ +package org.thoughtcrime.securesms; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.List; +import java.util.Map; + +public class TransportOptionsAdapter extends BaseAdapter { + private final Context context; + private final LayoutInflater inflater; + private List enabledTransports; + private final Map transportMetadata; + + public TransportOptionsAdapter(final Context context, + final List enabledTransports, + final Map transportMetadata) { + super(); + this.context = context; + this.inflater = LayoutInflater.from(context); + this.enabledTransports = enabledTransports; + this.transportMetadata = transportMetadata; + } + + public TransportOptionsAdapter(final Context context, + final Map transportMetadata) { + this(context, null, transportMetadata); + } + + public void setEnabledTransports(final List enabledTransports) { + this.enabledTransports = enabledTransports; + } + + @Override + public int getCount() { + return enabledTransports == null ? 0 : enabledTransports.size(); + } + + @Override + public Object getItem(int position) { + return transportMetadata.get(enabledTransports.get(position)); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final View view; + if (convertView == null) { + view = inflater.inflate(R.layout.transport_selection_list_item, parent, false); + } else { + view = convertView; + } + + TransportOption transport = (TransportOption) getItem(position); + final ImageView imageView = (ImageView)view.findViewById(R.id.icon); + final TextView textView = (TextView) view.findViewById(R.id.text); + + imageView.setImageResource(transport.drawable); + textView.setText(transport.text); + return view; + } +} diff --git a/src/org/thoughtcrime/securesms/components/SendButton.java b/src/org/thoughtcrime/securesms/components/SendButton.java new file mode 100644 index 000000000..fc44e9cec --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/SendButton.java @@ -0,0 +1,90 @@ +package org.thoughtcrime.securesms.components; + +import android.content.Context; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.RelativeSizeSpan; +import android.util.AttributeSet; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageButton; + +import org.thoughtcrime.securesms.TransportOption; +import org.thoughtcrime.securesms.TransportOptions; +import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener; + +public class SendButton extends ImageButton { + private TransportOptions transportOptions; + private EditText composeText; + + @SuppressWarnings("unused") + public SendButton(Context context) { + super(context); + initialize(); + } + + @SuppressWarnings("unused") + public SendButton(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + @SuppressWarnings("unused") + public SendButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initialize(); + } + + private void initialize() { + transportOptions = new TransportOptions(getContext()); + transportOptions.setOnTransportChangedListener(new OnTransportChangedListener() { + @Override + public void onChange(TransportOption newTransport) { + setImageResource(newTransport.drawable); + setContentDescription(newTransport.composeHint); + if (composeText != null) setComposeTextHint(newTransport.composeHint); + } + }); + + setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + if (transportOptions.getEnabledTransports().size() > 1) { + transportOptions.showPopup(SendButton.this); + return true; + } + return false; + } + }); + } + + public void setComposeTextView(EditText composeText) { + this.composeText = composeText; + } + + public TransportOption getSelectedTransport() { + return transportOptions.getSelectedTransport(); + } + + public void initializeAvailableTransports(boolean isMediaMessage) { + transportOptions.initializeAvailableTransports(isMediaMessage); + } + + public void disableTransport(String transport) { + transportOptions.disableTransport(transport); + } + + public void setDefaultTransport(String transport) { + transportOptions.setDefaultTransport(transport); + } + + private void setComposeTextHint(String hint) { + if (hint == null) { + this.composeText.setHint(null); + } else { + SpannableString span = new SpannableString(hint); + span.setSpan(new RelativeSizeSpan(0.8f), 0, hint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + this.composeText.setHint(span); + } + } +}