mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 02:55:23 +00:00
924 lines
34 KiB
Java
924 lines
34 KiB
Java
/**
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.thoughtcrime.securesms;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlertDialog;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.net.Uri;
|
|
import android.os.AsyncTask;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.preference.PreferenceManager;
|
|
import android.telephony.PhoneNumberUtils;
|
|
import android.text.Editable;
|
|
import android.text.TextWatcher;
|
|
import android.util.Log;
|
|
import android.view.ContextMenu;
|
|
import android.view.KeyEvent;
|
|
import android.view.View;
|
|
import android.view.View.OnClickListener;
|
|
import android.view.View.OnKeyListener;
|
|
import android.view.WindowManager;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.widget.EditText;
|
|
import android.widget.ImageButton;
|
|
import android.widget.TextView;
|
|
import android.widget.Toast;
|
|
|
|
import com.actionbarsherlock.view.Menu;
|
|
import com.actionbarsherlock.view.MenuInflater;
|
|
import com.actionbarsherlock.view.MenuItem;
|
|
import org.thoughtcrime.securesms.components.RecipientsPanel;
|
|
import org.thoughtcrime.securesms.crypto.AuthenticityCalculator;
|
|
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
|
|
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
|
|
import org.thoughtcrime.securesms.crypto.KeyUtil;
|
|
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
import org.thoughtcrime.securesms.database.DraftDatabase;
|
|
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
|
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
|
|
import org.thoughtcrime.securesms.mms.MediaTooLargeException;
|
|
import org.thoughtcrime.securesms.mms.MmsSendHelper;
|
|
import org.thoughtcrime.securesms.mms.Slide;
|
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
|
import org.thoughtcrime.securesms.protocol.Tag;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
|
import org.thoughtcrime.securesms.recipients.Recipients;
|
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
|
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
|
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
|
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
|
import org.thoughtcrime.securesms.util.CharacterCalculator;
|
|
import org.thoughtcrime.securesms.util.EncryptedCharacterCalculator;
|
|
import org.thoughtcrime.securesms.util.InvalidMessageException;
|
|
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
|
import org.thoughtcrime.securesms.util.Util;
|
|
|
|
import java.io.IOException;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
|
|
import ws.com.google.android.mms.MmsException;
|
|
|
|
/**
|
|
* Activity for displaying a message thread, as well as
|
|
* composing/sending a new message into that thread.
|
|
*
|
|
* @author Moxie Marlinspike
|
|
*
|
|
*/
|
|
public class ConversationActivity extends PassphraseRequiredSherlockFragmentActivity
|
|
implements ConversationFragment.ConversationFragmentListener
|
|
{
|
|
|
|
public static final String RECIPIENTS_EXTRA = "recipients";
|
|
public static final String THREAD_ID_EXTRA = "thread_id";
|
|
public static final String MASTER_SECRET_EXTRA = "master_secret";
|
|
public static final String DRAFT_TEXT_EXTRA = "draft_text";
|
|
public static final String DRAFT_IMAGE_EXTRA = "draft_image";
|
|
public static final String DRAFT_AUDIO_EXTRA = "draft_audio";
|
|
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 MasterSecret masterSecret;
|
|
private RecipientsPanel recipientsPanel;
|
|
private EditText composeText;
|
|
private ImageButton addContactButton;
|
|
private ImageButton sendButton;
|
|
private TextView charactersLeft;
|
|
|
|
private AttachmentTypeSelectorAdapter attachmentAdapter;
|
|
private AttachmentManager attachmentManager;
|
|
private BroadcastReceiver securityUpdateReceiver;
|
|
|
|
private Recipients recipients;
|
|
private long threadId;
|
|
private int distributionType;
|
|
private boolean isEncryptedConversation;
|
|
private boolean isMmsEnabled = true;
|
|
|
|
private CharacterCalculator characterCalculator = new CharacterCalculator();
|
|
|
|
@Override
|
|
protected void onCreate(Bundle state) {
|
|
super.onCreate(state);
|
|
|
|
setContentView(R.layout.conversation_activity);
|
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
|
|
initializeReceivers();
|
|
initializeResources();
|
|
initializeDraft();
|
|
initializeTitleBar();
|
|
}
|
|
|
|
@Override
|
|
protected void onStart() {
|
|
super.onStart();
|
|
|
|
if (!isExistingConversation())
|
|
initializeRecipientsInput();
|
|
}
|
|
|
|
@Override
|
|
protected void onResume() {
|
|
super.onResume();
|
|
initializeSecurity();
|
|
initializeTitleBar();
|
|
initializeMmsEnabledCheck();
|
|
calculateCharactersRemaining();
|
|
|
|
MessageNotifier.setVisibleThread(threadId);
|
|
markThreadAsRead();
|
|
}
|
|
|
|
@Override
|
|
protected void onPause() {
|
|
super.onPause();
|
|
MessageNotifier.setVisibleThread(-1L);
|
|
}
|
|
|
|
@Override
|
|
protected void onDestroy() {
|
|
unregisterReceiver(securityUpdateReceiver);
|
|
saveDraft();
|
|
MemoryCleaner.clean(masterSecret);
|
|
super.onDestroy();
|
|
}
|
|
|
|
@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");
|
|
|
|
if (recipients != null)
|
|
recipientsPanel.addRecipients(recipients);
|
|
|
|
break;
|
|
case PICK_IMAGE:
|
|
addAttachmentImage(data.getData());
|
|
break;
|
|
case PICK_VIDEO:
|
|
addAttachmentVideo(data.getData());
|
|
break;
|
|
case PICK_AUDIO:
|
|
addAttachmentAudio(data.getData());
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onPrepareOptionsMenu(Menu menu) {
|
|
MenuInflater inflater = this.getSupportMenuInflater();
|
|
menu.clear();
|
|
|
|
if (isSingleConversation() && isEncryptedConversation)
|
|
{
|
|
if (isAuthenticatedSession()) {
|
|
inflater.inflate(R.menu.conversation_secure_verified, menu);
|
|
} else {
|
|
inflater.inflate(R.menu.conversation_secure_unverified, menu);
|
|
}
|
|
} else if (isSingleConversation()) {
|
|
inflater.inflate(R.menu.conversation_insecure, menu);
|
|
}
|
|
|
|
if (isSingleConversation()) {
|
|
inflater.inflate(R.menu.conversation_callable, menu);
|
|
} else if (isGroupConversation()) {
|
|
inflater.inflate(R.menu.conversation_group_options, menu);
|
|
|
|
if (distributionType == ThreadDatabase.DistributionTypes.BROADCAST)
|
|
{
|
|
menu.findItem(R.id.menu_distribution_broadcast).setChecked(true);
|
|
} else {
|
|
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
|
|
}
|
|
}
|
|
|
|
inflater.inflate(R.menu.conversation, menu);
|
|
super.onPrepareOptionsMenu(menu);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
super.onOptionsItemSelected(item);
|
|
switch (item.getItemId()) {
|
|
case R.id.menu_call: handleDial(getRecipients().getPrimaryRecipient()); return true;
|
|
case R.id.menu_delete_thread: handleDeleteThread(); return true;
|
|
case R.id.menu_add_attachment: handleAddAttachment(); return true;
|
|
case R.id.menu_start_secure_session: handleStartSecureSession(); return true;
|
|
case R.id.menu_abort_session: handleAbortSecureSession(); return true;
|
|
case R.id.menu_verify_recipient: handleVerifyRecipient(); return true;
|
|
case R.id.menu_verify_session: handleVerifySession(); return true;
|
|
case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true;
|
|
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
|
|
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
|
|
case android.R.id.home: handleReturnToConversationList(); return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void onCreateContextMenu (ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
|
if (isEncryptedConversation) {
|
|
android.view.MenuInflater inflater = getMenuInflater();
|
|
inflater.inflate(R.menu.conversation_button_context, menu);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onContextItemSelected(android.view.MenuItem item) {
|
|
switch (item.getItemId()) {
|
|
case R.id.menu_context_send_unencrypted: sendMessage(true); return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//////// Event Handlers
|
|
|
|
private void handleReturnToConversationList() {
|
|
Intent intent = new Intent(this, ConversationListActivity.class);
|
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
intent.putExtra("master_secret", masterSecret);
|
|
startActivity(intent);
|
|
finish();
|
|
}
|
|
|
|
private void handleVerifyRecipient() {
|
|
Intent verifyIdentityIntent = new Intent(this, VerifyIdentityActivity.class);
|
|
verifyIdentityIntent.putExtra("recipient", getRecipients().getPrimaryRecipient());
|
|
verifyIdentityIntent.putExtra("master_secret", masterSecret);
|
|
startActivity(verifyIdentityIntent);
|
|
}
|
|
|
|
private void handleVerifySession() {
|
|
Intent verifyKeysIntent = new Intent(this, VerifyKeysActivity.class);
|
|
verifyKeysIntent.putExtra("recipient", getRecipients().getPrimaryRecipient());
|
|
verifyKeysIntent.putExtra("master_secret", masterSecret);
|
|
startActivity(verifyKeysIntent);
|
|
}
|
|
|
|
private void handleStartSecureSession() {
|
|
final Recipient recipient = getRecipients().getPrimaryRecipient();
|
|
String recipientName = (recipient.getName() == null ? recipient.getNumber() : recipient.getName());
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
builder.setTitle(R.string.ConversationActivity_initiate_secure_session_question);
|
|
builder.setIcon(android.R.drawable.ic_dialog_info);
|
|
builder.setCancelable(true);
|
|
builder.setMessage(String.format(getString(R.string.ConversationActivity_initiate_secure_session_with_s_question),
|
|
recipientName));
|
|
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
KeyExchangeInitiator.initiate(ConversationActivity.this, masterSecret,
|
|
recipient, true);
|
|
}
|
|
});
|
|
|
|
builder.setNegativeButton(R.string.no, null);
|
|
builder.show();
|
|
}
|
|
|
|
private void handleAbortSecureSession() {
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
builder.setTitle(R.string.ConversationActivity_abort_secure_session_confirmation);
|
|
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
|
builder.setCancelable(true);
|
|
builder.setMessage(R.string.ConversationActivity_are_you_sure_that_you_want_to_abort_this_secure_session_question);
|
|
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
if (isSingleConversation()) {
|
|
KeyUtil.abortSessionFor(ConversationActivity.this, getRecipients().getPrimaryRecipient());
|
|
initializeSecurity();
|
|
initializeTitleBar();
|
|
}
|
|
}
|
|
});
|
|
builder.setNegativeButton(R.string.no, null);
|
|
builder.show();
|
|
}
|
|
|
|
private void handleDistributionBroadcastEnabled(MenuItem item) {
|
|
distributionType = ThreadDatabase.DistributionTypes.BROADCAST;
|
|
item.setChecked(true);
|
|
|
|
if (threadId != -1) {
|
|
new AsyncTask<Void, Void, Void>() {
|
|
@Override
|
|
protected Void doInBackground(Void... params) {
|
|
DatabaseFactory.getThreadDatabase(ConversationActivity.this)
|
|
.setDistributionType(threadId, ThreadDatabase.DistributionTypes.BROADCAST);
|
|
return null;
|
|
}
|
|
}.execute();
|
|
}
|
|
}
|
|
|
|
private void handleDistributionConversationEnabled(MenuItem item) {
|
|
distributionType = ThreadDatabase.DistributionTypes.CONVERSATION;
|
|
item.setChecked(true);
|
|
|
|
if (threadId != -1) {
|
|
new AsyncTask<Void, Void, Void>() {
|
|
@Override
|
|
protected Void doInBackground(Void... params) {
|
|
DatabaseFactory.getThreadDatabase(ConversationActivity.this)
|
|
.setDistributionType(threadId, ThreadDatabase.DistributionTypes.CONVERSATION);
|
|
return null;
|
|
}
|
|
}.execute();
|
|
}
|
|
}
|
|
|
|
private void handleDial(Recipient recipient) {
|
|
if (recipient == null) return;
|
|
|
|
Intent dialIntent = new Intent(Intent.ACTION_DIAL,
|
|
Uri.parse("tel:" + recipient.getNumber()));
|
|
startActivity(dialIntent);
|
|
}
|
|
|
|
private void handleDisplayGroupRecipients() {
|
|
List<String> recipientStrings = new LinkedList<String>();
|
|
|
|
for (Recipient recipient : getRecipients().getRecipientsList()) {
|
|
recipientStrings.add(recipient.toShortString());
|
|
}
|
|
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
builder.setTitle(R.string.ConversationActivity_group_conversation_recipients);
|
|
builder.setIcon(R.drawable.ic_groups_holo_dark);
|
|
builder.setCancelable(true);
|
|
builder.setItems(recipientStrings.toArray(new String[]{}), null);
|
|
builder.setPositiveButton(android.R.string.ok, null);
|
|
builder.show();
|
|
}
|
|
|
|
private void handleDeleteThread() {
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
builder.setTitle(R.string.ConversationActivity_delete_thread_confirmation);
|
|
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
|
builder.setCancelable(true);
|
|
builder.setMessage(R.string.ConversationActivity_are_you_sure_that_you_want_to_permanently_delete_this_conversation_question);
|
|
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
if (threadId > 0) {
|
|
DatabaseFactory.getThreadDatabase(ConversationActivity.this).deleteConversation(threadId);
|
|
finish();
|
|
}
|
|
}
|
|
});
|
|
|
|
builder.setNegativeButton(R.string.no, null);
|
|
builder.show();
|
|
}
|
|
|
|
private void handleAddAttachment() {
|
|
if (this.isMmsEnabled) {
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
builder.setIcon(R.drawable.ic_dialog_attach);
|
|
builder.setTitle(R.string.ConversationActivity_add_attachment);
|
|
builder.setAdapter(attachmentAdapter, new AttachmentTypeListener());
|
|
builder.show();
|
|
} else {
|
|
startActivity(new Intent(this, PromptApnActivity.class));
|
|
}
|
|
}
|
|
|
|
///// Initializers
|
|
|
|
private void initializeTitleBar() {
|
|
String title = null;
|
|
String subtitle = null;
|
|
|
|
if (isSingleConversation()) {
|
|
|
|
if (isEncryptedConversation) {
|
|
title = AuthenticityCalculator.getAuthenticatedName(this,
|
|
getRecipients().getPrimaryRecipient(),
|
|
masterSecret);
|
|
}
|
|
|
|
if (title == null || title.trim().length() == 0) {
|
|
title = getRecipients().getPrimaryRecipient().getName();
|
|
}
|
|
|
|
if (title == null || title.trim().length() == 0) {
|
|
title = getRecipients().getPrimaryRecipient().getNumber();
|
|
} else {
|
|
subtitle = getRecipients().getPrimaryRecipient().getNumber();
|
|
}
|
|
} else if (isGroupConversation()) {
|
|
title = getString(R.string.ConversationActivity_group_conversation);
|
|
subtitle = String.format(getString(R.string.ConversationActivity_d_recipients_in_group),
|
|
getRecipients().getRecipientsList().size());
|
|
} else {
|
|
title = getString(R.string.ConversationActivity_compose_message);
|
|
subtitle = "";
|
|
}
|
|
|
|
this.getSupportActionBar().setTitle(title);
|
|
|
|
if (subtitle != null)
|
|
this.getSupportActionBar().setSubtitle(PhoneNumberUtils.formatNumber(subtitle));
|
|
|
|
this.invalidateOptionsMenu();
|
|
}
|
|
|
|
private void initializeDraft() {
|
|
String draftText = getIntent().getStringExtra(DRAFT_TEXT_EXTRA);
|
|
Uri draftImage = getIntent().getParcelableExtra(DRAFT_IMAGE_EXTRA);
|
|
Uri draftAudio = getIntent().getParcelableExtra(DRAFT_AUDIO_EXTRA);
|
|
|
|
if (draftText != null) composeText.setText(draftText);
|
|
if (draftImage != null) addAttachmentImage(draftImage);
|
|
if (draftAudio != null) addAttachmentAudio(draftAudio);
|
|
|
|
if (draftText == null && draftImage == null && draftAudio == null) {
|
|
initializeDraftFromDatabase();
|
|
}
|
|
}
|
|
|
|
private void initializeDraftFromDatabase() {
|
|
new AsyncTask<Void, Void, List<Draft>>() {
|
|
@Override
|
|
protected List<Draft> doInBackground(Void... params) {
|
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
|
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
|
|
List<Draft> results = draftDatabase.getDrafts(masterCipher, threadId);
|
|
|
|
draftDatabase.clearDrafts(threadId);
|
|
|
|
return results;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(List<Draft> drafts) {
|
|
for (Draft draft : drafts) {
|
|
if (draft.getType().equals(Draft.TEXT)) composeText.setText(draft.getValue());
|
|
else if (draft.getType().equals(Draft.IMAGE)) addAttachmentImage(Uri.parse(draft.getValue()));
|
|
else if (draft.getType().equals(Draft.AUDIO)) addAttachmentAudio(Uri.parse(draft.getValue()));
|
|
else if (draft.getType().equals(Draft.VIDEO)) addAttachmentVideo(Uri.parse(draft.getValue()));
|
|
}
|
|
}
|
|
}.execute();
|
|
}
|
|
|
|
private void initializeSecurity() {
|
|
if (isSingleConversation() &&
|
|
KeyUtil.isSessionFor(this, getRecipients().getPrimaryRecipient()))
|
|
{
|
|
sendButton.setImageResource(R.drawable.ic_send_encrypted_holo_light);
|
|
this.isEncryptedConversation = true;
|
|
this.characterCalculator = new EncryptedCharacterCalculator();
|
|
} else {
|
|
sendButton.setImageResource(R.drawable.ic_send_holo_light);
|
|
this.isEncryptedConversation = false;
|
|
this.characterCalculator = new CharacterCalculator();
|
|
}
|
|
|
|
calculateCharactersRemaining();
|
|
}
|
|
|
|
private void initializeMmsEnabledCheck() {
|
|
new AsyncTask<Void, Void, Boolean>() {
|
|
@Override
|
|
protected Boolean doInBackground(Void... params) {
|
|
return MmsSendHelper.hasNecessaryApnDetails(ConversationActivity.this);
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Boolean isMmsEnabled) {
|
|
ConversationActivity.this.isMmsEnabled = isMmsEnabled;
|
|
}
|
|
}.execute();
|
|
}
|
|
|
|
private void initializeResources() {
|
|
recipientsPanel = (RecipientsPanel)findViewById(R.id.recipients);
|
|
recipients = getIntent().getParcelableExtra(RECIPIENTS_EXTRA);
|
|
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);
|
|
charactersLeft = (TextView)findViewById(R.id.space_left);
|
|
|
|
attachmentAdapter = new AttachmentTypeSelectorAdapter(this);
|
|
attachmentManager = new AttachmentManager(this);
|
|
|
|
SendButtonListener sendButtonListener = new SendButtonListener();
|
|
|
|
recipientsPanel.setPanelChangeListener(new RecipientsPanelChangeListener());
|
|
sendButton.setOnClickListener(sendButtonListener);
|
|
sendButton.setEnabled(true);
|
|
addContactButton.setOnClickListener(new AddRecipientButtonListener());
|
|
composeText.setOnKeyListener(new ComposeKeyPressedListener());
|
|
composeText.addTextChangedListener(new OnTextChangedListener());
|
|
composeText.setOnEditorActionListener(sendButtonListener);
|
|
|
|
registerForContextMenu(sendButton);
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
|
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() {
|
|
recipientsPanel.setVisibility(View.VISIBLE);
|
|
|
|
if (this.recipients != null) {
|
|
recipientsPanel.addRecipients(this.recipients);
|
|
}
|
|
}
|
|
|
|
|
|
private void initializeReceivers() {
|
|
securityUpdateReceiver = new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
if (intent.getLongExtra("thread_id", -1) == -1)
|
|
return;
|
|
|
|
if (intent.getLongExtra("thread_id", -1) == threadId) {
|
|
initializeSecurity();
|
|
initializeTitleBar();
|
|
calculateCharactersRemaining();
|
|
}
|
|
}
|
|
};
|
|
|
|
registerReceiver(securityUpdateReceiver,
|
|
new IntentFilter(KeyExchangeProcessor.SECURITY_UPDATE_EVENT),
|
|
KeyCachingService.KEY_PERMISSION, null);
|
|
}
|
|
|
|
|
|
//////// Helper Methods
|
|
|
|
private void addAttachment(int type) {
|
|
Log.w("ComposeMessageActivity", "Selected: " + type);
|
|
switch (type) {
|
|
case AttachmentTypeSelectorAdapter.ADD_IMAGE:
|
|
AttachmentManager.selectImage(this, PICK_IMAGE); break;
|
|
case AttachmentTypeSelectorAdapter.ADD_VIDEO:
|
|
AttachmentManager.selectVideo(this, PICK_VIDEO); break;
|
|
case AttachmentTypeSelectorAdapter.ADD_SOUND:
|
|
AttachmentManager.selectAudio(this, PICK_AUDIO); break;
|
|
}
|
|
}
|
|
|
|
private void addAttachmentImage(Uri imageUri) {
|
|
try {
|
|
attachmentManager.setImage(imageUri);
|
|
} catch (IOException e) {
|
|
Log.w("ConversationActivity", e);
|
|
attachmentManager.clear();
|
|
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
|
Toast.LENGTH_LONG).show();
|
|
} catch (BitmapDecodingException e) {
|
|
Log.w("ConversationActivity", e);
|
|
attachmentManager.clear();
|
|
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
|
Toast.LENGTH_LONG).show();
|
|
}
|
|
}
|
|
|
|
private void addAttachmentVideo(Uri videoUri) {
|
|
try {
|
|
attachmentManager.setVideo(videoUri);
|
|
} catch (IOException e) {
|
|
attachmentManager.clear();
|
|
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
|
Toast.LENGTH_LONG).show();
|
|
Log.w("ComposeMessageActivity", e);
|
|
} catch (MediaTooLargeException e) {
|
|
attachmentManager.clear();
|
|
Toast.makeText(this, R.string.ConversationActivity_sorry_the_selected_video_exceeds_message_size_restrictions,
|
|
Toast.LENGTH_LONG).show();
|
|
Log.w("ComposeMessageActivity", e);
|
|
}
|
|
}
|
|
|
|
private void addAttachmentAudio(Uri audioUri) {
|
|
try {
|
|
attachmentManager.setAudio(audioUri);
|
|
} catch (IOException e) {
|
|
attachmentManager.clear();
|
|
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
|
Toast.LENGTH_LONG).show();
|
|
Log.w("ComposeMessageActivity", e);
|
|
} catch (MediaTooLargeException e) {
|
|
attachmentManager.clear();
|
|
Toast.makeText(this, R.string.ConversationActivity_sorry_the_selected_audio_exceeds_message_size_restrictions,
|
|
Toast.LENGTH_LONG).show();
|
|
Log.w("ComposeMessageActivity", e);
|
|
}
|
|
}
|
|
|
|
private List<Draft> getDraftsForCurrentState() {
|
|
List<Draft> drafts = new LinkedList<Draft>();
|
|
|
|
if (!Util.isEmpty(composeText)) {
|
|
drafts.add(new Draft(Draft.TEXT, composeText.getText().toString()));
|
|
}
|
|
|
|
for (Slide slide : attachmentManager.getSlideDeck().getSlides()) {
|
|
if (slide.hasImage()) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString()));
|
|
else if (slide.hasAudio()) drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString()));
|
|
else if (slide.hasVideo()) drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString()));
|
|
}
|
|
|
|
return drafts;
|
|
}
|
|
|
|
private void saveDraft() {
|
|
if (this.threadId <= 0 || this.recipients == null || this.recipients.isEmpty())
|
|
return;
|
|
|
|
final List<Draft> drafts = getDraftsForCurrentState();
|
|
|
|
if (drafts.size() <= 0)
|
|
return;
|
|
|
|
final long thisThreadId = this.threadId;
|
|
final MasterSecret thisMasterSecret = this.masterSecret.parcelClone();
|
|
|
|
new AsyncTask<Void, Void, Void>() {
|
|
@Override
|
|
protected void onPreExecute() {
|
|
Toast.makeText(ConversationActivity.this,
|
|
R.string.ConversationActivity_saving_draft,
|
|
Toast.LENGTH_SHORT).show();
|
|
}
|
|
|
|
@Override
|
|
protected Void doInBackground(Void... params) {
|
|
MasterCipher masterCipher = new MasterCipher(thisMasterSecret);
|
|
DatabaseFactory.getDraftDatabase(ConversationActivity.this).insertDrafts(masterCipher, thisThreadId, drafts);
|
|
MemoryCleaner.clean(thisMasterSecret);
|
|
return null;
|
|
}
|
|
}.execute();
|
|
}
|
|
|
|
private void calculateCharactersRemaining() {
|
|
int charactersSpent = composeText.getText().length();
|
|
CharacterCalculator.CharacterState characterState = characterCalculator.calculateCharacters(charactersSpent);
|
|
charactersLeft.setText(characterState.charactersRemaining + "/" + characterState.maxMessageSize + " (" + characterState.messagesSpent + ")");
|
|
}
|
|
|
|
private boolean isExistingConversation() {
|
|
return this.recipients != null && this.threadId != -1;
|
|
}
|
|
|
|
private boolean isSingleConversation() {
|
|
return getRecipients() != null && getRecipients().isSingleRecipient();
|
|
}
|
|
|
|
private boolean isGroupConversation() {
|
|
return getRecipients() != null && !getRecipients().isSingleRecipient();
|
|
}
|
|
|
|
private boolean isAuthenticatedSession() {
|
|
return AuthenticityCalculator.isAuthenticated(this,
|
|
getRecipients().getPrimaryRecipient(),
|
|
masterSecret);
|
|
}
|
|
|
|
private Recipients getRecipients() {
|
|
try {
|
|
if (isExistingConversation()) return this.recipients;
|
|
else return recipientsPanel.getRecipients();
|
|
} catch (RecipientFormattingException rfe) {
|
|
Log.w("ConversationActivity", rfe);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private String getMessage() throws InvalidMessageException {
|
|
String rawText = composeText.getText().toString();
|
|
|
|
if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent())
|
|
throw new InvalidMessageException(getString(R.string.ConversationActivity_message_is_empty_exclamation));
|
|
|
|
if (!isEncryptedConversation && Tag.isTaggable(this, rawText))
|
|
rawText = Tag.getTaggedMessage(rawText);
|
|
|
|
return rawText;
|
|
}
|
|
|
|
private void markThreadAsRead() {
|
|
new AsyncTask<Long, Void, Void>() {
|
|
@Override
|
|
protected Void doInBackground(Long... params) {
|
|
DatabaseFactory.getThreadDatabase(ConversationActivity.this).setRead(params[0]);
|
|
MessageNotifier.updateNotification(ConversationActivity.this, masterSecret);
|
|
return null;
|
|
}
|
|
}.execute(threadId);
|
|
}
|
|
|
|
private void sendComplete(Recipients recipients, long threadId) {
|
|
attachmentManager.clear();
|
|
recipientsPanel.disable();
|
|
composeText.setText("");
|
|
|
|
this.recipients = recipients;
|
|
this.threadId = threadId;
|
|
|
|
if (this.recipientsPanel.getVisibility() == View.VISIBLE) {
|
|
ConversationFragment fragment
|
|
= (ConversationFragment)this.getSupportFragmentManager()
|
|
.findFragmentById(R.id.fragment_content);
|
|
|
|
fragment.reload(recipients, threadId);
|
|
|
|
this.recipientsPanel.setVisibility(View.GONE);
|
|
initializeTitleBar();
|
|
initializeSecurity();
|
|
}
|
|
}
|
|
|
|
private void sendMessage(boolean forcePlaintext) {
|
|
try {
|
|
Recipients recipients = getRecipients();
|
|
|
|
if (recipients == null)
|
|
throw new RecipientFormattingException("Badly formatted");
|
|
|
|
String body = getMessage();
|
|
long allocatedThreadId;
|
|
|
|
if (attachmentManager.isAttachmentPresent()) {
|
|
allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients,
|
|
threadId, attachmentManager.getSlideDeck(), body,
|
|
distributionType, isEncryptedConversation && !forcePlaintext);
|
|
} else if (recipients.isEmailRecipient() || !recipients.isSingleRecipient()) {
|
|
allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients,
|
|
threadId, new SlideDeck(), body, distributionType,
|
|
isEncryptedConversation && !forcePlaintext);
|
|
} else {
|
|
OutgoingTextMessage message;
|
|
|
|
if (isEncryptedConversation && !forcePlaintext) {
|
|
message = new OutgoingEncryptedMessage(recipients, body);
|
|
} else {
|
|
message = new OutgoingTextMessage(recipients, body);
|
|
}
|
|
|
|
Log.w("ConversationActivity", "Sending message...");
|
|
allocatedThreadId = MessageSender.send(ConversationActivity.this, masterSecret,
|
|
message, threadId);
|
|
}
|
|
|
|
sendComplete(recipients, allocatedThreadId);
|
|
} catch (RecipientFormattingException ex) {
|
|
Toast.makeText(ConversationActivity.this,
|
|
R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation,
|
|
Toast.LENGTH_LONG).show();
|
|
Log.w("ConversationActivity", ex);
|
|
} catch (InvalidMessageException ex) {
|
|
Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_message_is_empty_exclamation,
|
|
Toast.LENGTH_SHORT).show();
|
|
Log.w("ConversationActivity", ex);
|
|
} catch (MmsException e) {
|
|
Log.w("ComposeMessageActivity", e);
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
addAttachment(attachmentAdapter.buttonToCommand(which));
|
|
}
|
|
}
|
|
|
|
private class RecipientsPanelChangeListener implements RecipientsPanel.RecipientsPanelChangedListener {
|
|
@Override
|
|
public void onRecipientsPanelUpdate(Recipients recipients) {
|
|
initializeSecurity();
|
|
initializeTitleBar();
|
|
calculateCharactersRemaining();
|
|
}
|
|
}
|
|
|
|
private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener {
|
|
@Override
|
|
public void onClick(View v) {
|
|
sendMessage(false);
|
|
}
|
|
|
|
@Override
|
|
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
|
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
|
sendButton.performClick();
|
|
composeText.clearFocus();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private class OnTextChangedListener implements TextWatcher {
|
|
@Override
|
|
public void afterTextChanged(Editable s) {
|
|
calculateCharactersRemaining();
|
|
// if (s == null || s.length() == 0) {
|
|
// sendButton.setClickable(false);
|
|
// sendButton.setEnabled(false);
|
|
// sendButton.setColorFilter(0x66FFFFFF);
|
|
// } else {
|
|
// sendButton.setClickable(true);
|
|
// sendButton.setEnabled(true);
|
|
// sendButton.setColorFilter(null);
|
|
// }
|
|
}
|
|
@Override
|
|
public void beforeTextChanged(CharSequence s, int start, int count,int after) {}
|
|
@Override
|
|
public void onTextChanged(CharSequence s, int start, int before,int count) {}
|
|
|
|
}
|
|
|
|
private class ComposeKeyPressedListener implements OnKeyListener {
|
|
@Override
|
|
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
|
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
|
if (PreferenceManager.getDefaultSharedPreferences(ConversationActivity.this).getBoolean("pref_enter_sends", false)) {
|
|
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
|
|
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setComposeText(String text) {
|
|
this.composeText.setText(text);
|
|
}
|
|
}
|