New design for Conversation Activity.

1) Move to Fragments for the list view.
2) Switch to CursorLoader from my jankey self-managed cursor.
3) Add session security logic to the ActionBar.
4) Fix colors to be less ugly.
This commit is contained in:
Moxie Marlinspike 2012-07-19 14:22:03 -07:00
parent 3d9475676f
commit b377fe84df
32 changed files with 1265 additions and 1175 deletions

View File

@ -30,7 +30,7 @@
<activity android:name=".AutoInitiateActivity" android:theme="@android:style/Theme.Dialog" android:label="TextSecure Messaging Detected" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"></activity>
<activity android:name=".ApplicationPreferencesActivity" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"></activity>
<activity android:name=".ComposeMessageActivity" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"></activity>
<activity android:name=".ConversationActivity" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"></activity>
<activity android:name=".PassphraseCreateActivity" android:theme="@android:style/Theme.Dialog" android:label="Create Passphrase" android:launchMode="singleInstance" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"></activity>
<activity android:name=".PassphrasePromptActivity" android:theme="@android:style/Theme.Dialog" android:label="Enter Passphrase" android:launchMode="singleInstance" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"></activity>
<activity android:name=".PassphraseChangeActivity" android:theme="@android:style/Theme.Dialog" android:label="Change Passphrase" android:launchMode="singleInstance" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"></activity>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -1,274 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (C) 2008 Esmertec AG.
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<RelativeLayout android:layout_width="fill_parent"
android:layout_height="43dip"
android:background="@drawable/iphone_bar_top">
<ImageView
android:id="@+id/secure_indicator"
android:visibility="gone"
android:paddingLeft="5.0dip"
android:paddingRight="2.0dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_lock_iphone"
android:adjustViewBounds="false"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true" />
<!-- <ImageView-->
<!-- android:id="@+id/secure_indicator_yellow"-->
<!-- android:visibility="gone"-->
<!-- android:paddingLeft="5.0dip" -->
<!-- android:paddingRight="2.0dip" -->
<!-- android:layout_width="wrap_content" -->
<!-- android:layout_height="wrap_content" -->
<!-- android:src="@drawable/ic_lock_iphone_yellow2" -->
<!-- android:adjustViewBounds="false" -->
<!-- android:layout_alignParentLeft="true" -->
<!-- android:layout_centerVertical="true" />-->
<ImageView
android:id="@+id/secure_indicator_red"
android:visibility="gone"
android:paddingLeft="5.0dip"
android:paddingRight="2.0dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_lock_iphone_red2"
android:adjustViewBounds="false"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true" />
<!-- <ImageView-->
<!-- android:id="@+id/secure_indicator_green"-->
<!-- android:visibility="gone"-->
<!-- android:paddingLeft="5.0dip" -->
<!-- android:paddingRight="2.0dip" -->
<!-- android:layout_width="wrap_content" -->
<!-- android:layout_height="wrap_content" -->
<!-- android:src="@drawable/ic_lock_iphone_red2" -->
<!-- android:adjustViewBounds="false" -->
<!-- android:layout_alignParentLeft="true" -->
<!-- android:layout_centerVertical="true" />-->
<!-- <ImageView-->
<!-- android:id="@+id/secure_indicator_question"-->
<!-- android:visibility="gone"-->
<!-- android:paddingLeft="0.0dip"-->
<!-- android:paddingRight="2.0dip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:src="@drawable/ic_lock_iphone_question"-->
<!-- android:adjustViewBounds="false"-->
<!-- android:layout_toRightOf="@id/secure_indicator_green"-->
<!-- android:layout_centerVertical="true" />-->
<TextView
android:id="@+id/title_bar"
android:textSize="18.0dip"
android:textStyle="bold"
android:textColor="#ffffffff"
android:singleLine="true"
style="?android:windowTitleStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:gravity="center" />
<!-- <ImageView-->
<!-- android:id="@+id/settings_button"-->
<!-- android:paddingLeft="2.0dip" -->
<!-- android:paddingRight="5.0dip" -->
<!-- android:layout_width="wrap_content" -->
<!-- android:layout_height="wrap_content" -->
<!-- android:src="@drawable/ic_settings_iphone" -->
<!-- android:adjustViewBounds="false" -->
<!-- android:layout_alignParentRight="true" -->
<!-- android:layout_centerVertical="true" />-->
</RelativeLayout>
<org.thoughtcrime.securesms.components.RecipientsPanel
android:id="@+id/recipients"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"
/>
<LinearLayout
android:id="@+id/layout_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="@drawable/white_background"
android:gravity="bottom">
<ListView
android:id="@+id/conversation"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1.0"
android:listSelector="@drawable/chat_history_selector"
android:drawSelectorOnTop="true"
android:transcriptMode="alwaysScroll"
android:scrollbarAlwaysDrawVerticalTrack="true"
android:scrollbarStyle="insideInset"
android:stackFromBottom="true"
android:visibility="gone"
android:fadingEdge="none"
android:layout_marginBottom="1dip"
android:cacheColorHint="@android:color/white"
android:dividerHeight="0px" android:divider="#ffffffff"/>
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:paddingBottom="5dip"
android:background="@drawable/bottombar_landscape_565">
<LinearLayout
android:id="@+id/attachment_editor"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<ImageView
android:id="@+id/attachment_thumbnail"
android:layout_width="fill_parent"
android:layout_height="150dip"
android:layout_weight="1" />
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center_vertical">
<!-- <Button-->
<!-- android:id="@+id/view_image_button"-->
<!-- style="?android:attr/buttonStyleSmall"-->
<!-- android:layout_width="100dip"-->
<!-- android:layout_height="50dip"-->
<!-- android:text="View" />-->
<!-- -->
<!-- <Button-->
<!-- android:id="@+id/replace_image_button"-->
<!-- style="?android:attr/buttonStyleSmall"-->
<!-- android:layout_width="100dip"-->
<!-- android:layout_height="50dip"-->
<!-- android:text="Replace" />-->
<Button
android:id="@+id/remove_image_button"
style="?android:attr/buttonStyleSmall"
android:layout_width="100dip"
android:layout_height="50dip"
android:text="Remove" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/bottom_panel"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="5dip"
android:paddingLeft="5dip"
android:paddingRight="5dip"
>
<RelativeLayout
android:id="@+id/editor_with_counter"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:addStatesFromChildren="true"
android:background="@android:drawable/edit_text">
<EditText
android:id="@+id/embedded_text_editor"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:autoText="true"
android:capitalize="sentences"
android:nextFocusRight="@+id/send_button"
android:hint="Type to compose"
android:maxLines="4"
android:inputType="textShortMessage|textAutoCorrect|textCapSentences|textMultiLine"
android:imeOptions="actionSend|flagNoEnterAction"
android:background="@null"
android:maxLength="1000"
/>
<TextView
android:id="@+id/text_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#88000000"
android:textColor="#ffffffff"
android:textSize="11sp"
android:textStyle="bold"
android:paddingLeft="3dip"
android:paddingRight="3dip"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:visibility="gone"
/>
</RelativeLayout>
<Button android:id="@+id/send_button"
android:layout_marginLeft="5dip"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
style="?android:attr/buttonStyle"
android:layout_gravity="center_vertical"
android:nextFocusLeft="@+id/embedded_text_editor"
android:text="Send"
/>
</LinearLayout>
<TextView android:id="@+id/space_left"
android:paddingLeft="5dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="160/160 (1)"
/>
</LinearLayout>
</ScrollView>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,210 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (C) 2008 Esmertec AG.
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.RecipientsPanel
android:id="@+id/recipients"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"
/>
<RelativeLayout
android:id="@+id/layout_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="@drawable/white_background"
android:gravity="bottom">
<fragment
android:id="@+id/fragment_content"
android:name="org.thoughtcrime.securesms.ConversationFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/bottom_container" />
<!-- <ListView -->
<!-- android:id="@+id/conversation" -->
<!-- android:layout_width="fill_parent" -->
<!-- android:layout_height="fill_parent" -->
<!-- android:layout_weight="1.0" -->
<!-- android:listSelector="@drawable/chat_history_selector" -->
<!-- android:drawSelectorOnTop="true" -->
<!-- android:transcriptMode="alwaysScroll" -->
<!-- android:scrollbarAlwaysDrawVerticalTrack="true" -->
<!-- android:scrollbarStyle="insideInset" -->
<!-- android:stackFromBottom="true" -->
<!-- android:visibility="gone" -->
<!-- android:fadingEdge="none" -->
<!-- android:layout_marginBottom="1dip" -->
<!-- android:cacheColorHint="@android:color/white" -->
<!-- android:dividerHeight="0px" android:divider="#ffffffff"/> -->
<ScrollView android:id="@id/bottom_container"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:paddingBottom="5dip"
android:background="#fff">
<LinearLayout
android:id="@+id/attachment_editor"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<ImageView
android:id="@+id/attachment_thumbnail"
android:layout_width="fill_parent"
android:layout_height="150dip"
android:layout_weight="1" />
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center_vertical">
<!-- <Button-->
<!-- android:id="@+id/view_image_button"-->
<!-- style="?android:attr/buttonStyleSmall"-->
<!-- android:layout_width="100dip"-->
<!-- android:layout_height="50dip"-->
<!-- android:text="View" />-->
<!-- -->
<!-- <Button-->
<!-- android:id="@+id/replace_image_button"-->
<!-- style="?android:attr/buttonStyleSmall"-->
<!-- android:layout_width="100dip"-->
<!-- android:layout_height="50dip"-->
<!-- android:text="Replace" />-->
<Button
android:id="@+id/remove_image_button"
style="?android:attr/buttonStyleSmall"
android:layout_width="100dip"
android:layout_height="50dip"
android:text="Remove" />
</LinearLayout>
</LinearLayout>
<View android:background="#eeeeee"
android:layout_width="match_parent"
android:layout_height="1dp" />
<LinearLayout
android:id="@+id/bottom_panel"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="5dip"
android:paddingLeft="5dip"
android:paddingRight="5dip"
>
<EditText
android:id="@+id/embedded_text_editor"
android:textColor="@android:color/black"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:autoText="true"
android:capitalize="sentences"
android:nextFocusRight="@+id/send_button"
android:hint="Type message"
android:maxLines="4"
android:inputType="textShortMessage|textAutoCorrect|textCapSentences|textMultiLine"
android:imeOptions="actionSend|flagNoEnterAction"
android:maxLength="1000"
/>
<!-- <RelativeLayout -->
<!-- android:id="@+id/editor_with_counter" -->
<!-- android:layout_width="0dip" -->
<!-- android:layout_height="wrap_content" -->
<!-- android:layout_weight="1.0" -->
<!-- android:addStatesFromChildren="true" -->
<!-- android:background="@android:drawable/edit_text"> -->
<!-- <EditText -->
<!-- android:id="@+id/embedded_text_editor" -->
<!-- android:layout_width="fill_parent" -->
<!-- android:layout_height="wrap_content" -->
<!-- android:autoText="true" -->
<!-- android:capitalize="sentences" -->
<!-- android:nextFocusRight="@+id/send_button" -->
<!-- android:hint="Type to compose" -->
<!-- android:maxLines="4" -->
<!-- android:inputType="textShortMessage|textAutoCorrect|textCapSentences|textMultiLine" -->
<!-- android:imeOptions="actionSend|flagNoEnterAction" -->
<!-- android:background="@null" -->
<!-- android:maxLength="1000" -->
<!-- /> -->
<!-- <TextView -->
<!-- android:id="@+id/text_counter" -->
<!-- android:layout_width="wrap_content" -->
<!-- android:layout_height="wrap_content" -->
<!-- android:background="#88000000" -->
<!-- android:textColor="#ffffffff" -->
<!-- android:textSize="11sp" -->
<!-- android:textStyle="bold" -->
<!-- android:paddingLeft="3dip" -->
<!-- android:paddingRight="3dip" -->
<!-- android:layout_alignParentRight="true" -->
<!-- android:layout_alignParentTop="true" -->
<!-- android:visibility="gone" -->
<!-- /> -->
<!-- </RelativeLayout> -->
<Button android:id="@+id/send_button"
android:layout_marginLeft="5dip"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
style="?android:attr/buttonStyle"
android:layout_gravity="center_vertical"
android:nextFocusLeft="@+id/embedded_text_editor"
android:text="Send"
/>
</LinearLayout>
<TextView android:id="@+id/space_left"
android:paddingLeft="5dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="160/160 (1)"
/>
</LinearLayout>
</ScrollView>
</RelativeLayout>
</LinearLayout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:listSelector="@drawable/chat_history_selector"
android:drawSelectorOnTop="true"
android:transcriptMode="alwaysScroll"
android:scrollbarAlwaysDrawVerticalTrack="true"
android:scrollbarStyle="insideInset"
android:stackFromBottom="true"
android:fadingEdge="none"
android:layout_marginBottom="1dip"/>
</LinearLayout>

17
res/menu/conversation.xml Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="Call"
android:id="@+id/menu_call"
android:icon="@drawable/ic_menu_call"
android:showAsAction="ifRoom" />
<item android:title="Add attachment"
android:id="@+id/menu_add_attachment"
android:icon="@drawable/ic_menu_attach" />
<item android:title="Delete thread"
android:id="@+id/menu_delete_thread"
android:icon="@android:drawable/ic_menu_delete" />
</menu>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="Send unencrypted"
android:id="@+id/menu_context_send_unencrypted" />
</menu>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="Copy text"
android:id="@+id/menu_context_copy" />
<item android:title="Delete message"
android:id="@+id/menu_context_delete_message" />
<item android:title="Message details"
android:id="@+id/menu_context_details" />
<item android:title="Forward message"
android:id="@+id/menu_context_forward" />
</menu>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="Security"
android:id="@+id/menu_security"
android:icon="@drawable/ic_menu_lock_holo_dark"
android:showAsAction="ifRoom">
<menu>
<item android:title="Start Secure Session"
android:id="@+id/menu_start_secure_session" />
</menu>
</item>
</menu>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="Security"
android:id="@+id/menu_security"
android:icon="@drawable/ic_menu_lock_unverified_holo_dark"
android:showAsAction="ifRoom">
<menu>
<item android:title="Verify Session"
android:id="@+id/menu_verify_session" />
<item android:title="Verify Recipient"
android:id="@+id/menu_verify_recipient"/>
<item android:title="Abort Secure Session"
android:id="@+id/menu_abort_session"/>
</menu>
</item>
</menu>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="Security"
android:id="@+id/menu_security"
android:icon="@drawable/ic_menu_lock_unverified_holo_dark"
android:showAsAction="ifRoom">
<menu>
<item android:title="Verify Session"
android:id="@+id/menu_verify_session" />
<item android:title="Verify Recipient"
android:id="@+id/menu_verify_recipient"/>
<item android:title="Abort Secure Session"
android:id="@+id/menu_abort_session"/>
</menu>
</item>
</menu>

View File

@ -1,830 +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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.thoughtcrime.securesms.components.RecipientsPanel;
import org.thoughtcrime.securesms.crypto.AuthenticityCalculator;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
import org.thoughtcrime.securesms.crypto.KeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageRecord;
import org.thoughtcrime.securesms.database.SessionRecord;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.SlideDeck;
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.service.MessageNotifier;
import org.thoughtcrime.securesms.sms.MessageSender;
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 ws.com.google.android.mms.MmsException;
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.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.text.ClipboardManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.Window;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.CursorAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
/**
* Activity for displaying a message thread, as well as
* composing/sending a new message into that thread.
*
* @author Moxie Marlinspike
*
*/
public class ComposeMessageActivity extends Activity {
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 MENU_OPTION_CALL = 2;
// private static final int MENU_OPTION_VIEW_CONTACT = 3;
private static final int MENU_OPTION_VERIFY_KEYS = 4;
private static final int MENU_OPTION_DELETE_THREAD = 5;
private static final int MENU_OPTION_START_SESSION = 6;
private static final int MENU_OPTION_DELETE_KEYS = 7;
private static final int MENU_OPTION_ADD_ATTACHMENT = 8;
private static final int MENU_OPTION_DETAILS = 9;
private static final int MENU_OPTION_VERIFY_IDENTITY = 10;
private static final int MENU_OPTION_REDECRYPT = 11;
private static final int MENU_OPTION_COPY = 100;
private static final int MENU_OPTION_DELETE = 101;
private static final int MENU_OPTION_FORWARD = 102;
private static final int MENU_OPTION_SEND_CLEARTEXT = 103;
private static final int MENU_OPTION_SEND_DELAYED = 104;
private static final int MESSAGE_ITEM_GROUP = 0;
private static final int SEND_BUTTON_GROUP = 1;
private MasterSecret masterSecret;
private ConversationAdapter conversationAdapter;
private ListView conversationView;
private RecipientsPanel recipientsPanel;
private EditText composeText;
private ImageButton addContactButton;
private Button sendButton;
private TextView charactersLeft;
private TextView titleBar;
private View greyLock;
private View redLock;
private AttachmentTypeSelectorAdapter attachmentAdapter;
private AttachmentManager attachmentManager;
private KillActivityReceiver killActivityReceiver;
private SecurityUpdateReceiver securityUpdateReceiver;
private Recipients recipients;
private long threadId;
private boolean sendEncrypted;
private CharacterCalculator characterCalculator = new CharacterCalculator();
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
Log.w("ComposeMessageActivity", "onCreate called...");
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.compose_message_activity);
initializeReceivers();
initializeResources();
initializeTitleBar();
initializeColors();
}
@Override
protected void onResume() {
super.onResume();
Log.w("ComposeMessageActivity", "onResume called...");
initializeSecurity(recipients);
initializeTitleBar();
calculateCharactersRemaining();
}
@Override
protected void onStart() {
super.onStart();
Log.w("ComposeMessageActivity", "onStart called...");
if (isExistingConversation()) initializeConversationAdapter();
else initializeRecipientsInput();
registerPassphraseActivityStarted();
}
@Override
protected void onStop() {
super.onStop();
Log.w("ComposeMessageActivity", "onStop called...");
if (this.conversationAdapter != null)
this.conversationAdapter.close();
registerPassphraseActivityStopped();
}
@Override
protected void onDestroy() {
unregisterReceiver(killActivityReceiver);
unregisterReceiver(securityUpdateReceiver);
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 = (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) {
super.onPrepareOptionsMenu(menu);
menu.clear();
if (recipients != null && recipients.isSingleRecipient())
menu.add(0, MENU_OPTION_CALL, Menu.NONE, "Call").setIcon(android.R.drawable.ic_menu_call);
menu.add(0, MENU_OPTION_DELETE_THREAD, Menu.NONE, "Delete Thread").setIcon(android.R.drawable.ic_menu_delete);
menu.add(0, MENU_OPTION_ADD_ATTACHMENT, Menu.NONE, "Add Attachment").setIcon(R.drawable.ic_menu_attachment);
if (recipients != null && recipients.isSingleRecipient() && SessionRecord.hasSession(this, recipients.getPrimaryRecipient())) {
SubMenu secureSettingsMenu = menu.addSubMenu("Secure Session Options").setIcon(android.R.drawable.ic_menu_more);
secureSettingsMenu.add(0, MENU_OPTION_VERIFY_KEYS, Menu.NONE, "Verify Secure Session").setIcon(R.drawable.ic_lock_message_sms);
secureSettingsMenu.add(0, MENU_OPTION_VERIFY_IDENTITY, Menu.NONE, "Verify Recipient Identity").setIcon(android.R.drawable.ic_menu_zoom);
secureSettingsMenu.add(0, MENU_OPTION_DELETE_KEYS, Menu.NONE, "Abort Secure Session").setIcon(android.R.drawable.ic_menu_revert);
} else if (recipients != null && recipients.isSingleRecipient()) {
menu.add(0, MENU_OPTION_START_SESSION, Menu.NONE, "Start Secure Session").setIcon(R.drawable.ic_lock_message_sms);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case MENU_OPTION_CALL: dial(recipients.getPrimaryRecipient()); return true;
case MENU_OPTION_DELETE_THREAD: deleteThread(); return true;
case MENU_OPTION_VERIFY_KEYS: verifyKeys(); return true;
case MENU_OPTION_START_SESSION: initiateSecureSession(); return true;
case MENU_OPTION_DELETE_KEYS: abortSecureSession(); return true;
case MENU_OPTION_ADD_ATTACHMENT: addAttachment(); return true;
case MENU_OPTION_VERIFY_IDENTITY: verifyIdentity(); return true;
}
return false;
}
@Override
public void onCreateContextMenu (ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
if (v == sendButton) createSendButtonContextMenu(menu);
else createMessageItemContextMenu(menu);
}
private void createSendButtonContextMenu(ContextMenu menu) {
if (sendEncrypted)
menu.add(SEND_BUTTON_GROUP, MENU_OPTION_SEND_CLEARTEXT, Menu.NONE, "Send unencrypted");
}
private void createMessageItemContextMenu(ContextMenu menu) {
menu.add(MESSAGE_ITEM_GROUP, MENU_OPTION_COPY, Menu.NONE, "Copy text");
menu.add(MESSAGE_ITEM_GROUP, MENU_OPTION_DELETE, Menu.NONE, "Delete");
menu.add(MESSAGE_ITEM_GROUP, MENU_OPTION_DETAILS, Menu.NONE, "Message Details");
menu.add(MESSAGE_ITEM_GROUP, MENU_OPTION_FORWARD, Menu.NONE, "Forward message");
Cursor cursor = ((CursorAdapter)conversationAdapter).getCursor();
ConversationItem conversationItem = (ConversationItem)(conversationAdapter.newView(this, cursor, null));
MessageRecord messageRecord = conversationItem.getMessageRecord();
if (messageRecord.isFailedDecryptType())
menu.add(MESSAGE_ITEM_GROUP, MENU_OPTION_REDECRYPT, Menu.NONE, "Attempt decrypt again");
}
@Override
public boolean onContextItemSelected(MenuItem item) {
if (item.getGroupId() == MESSAGE_ITEM_GROUP) return onMessageContextItemSelected(item);
else if (item.getGroupId() == SEND_BUTTON_GROUP) return onSendContextItemSelected(item);
return false;
}
private boolean onSendContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_OPTION_SEND_CLEARTEXT: sendMessage(false); return true;
}
return false;
}
private boolean onMessageContextItemSelected(MenuItem item) {
Cursor cursor = ((CursorAdapter)conversationAdapter).getCursor();
ConversationItem conversationItem = (ConversationItem)(conversationAdapter.newView(this, cursor, null));
String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
MessageRecord messageRecord = conversationItem.getMessageRecord();
switch(item.getItemId()) {
case MENU_OPTION_COPY: copyMessageToClipboard(messageRecord); return true;
case MENU_OPTION_DELETE: deleteMessage(messageRecord); return true;
case MENU_OPTION_DETAILS: displayMessageDetails(messageRecord); return true;
case MENU_OPTION_REDECRYPT: redecryptMessage(messageRecord, address, body); return true;
case MENU_OPTION_FORWARD: forwardMessage(messageRecord); return true;
}
return false;
}
private void verifyIdentity() {
Intent verifyIdentityIntent = new Intent(this, VerifyIdentityActivity.class);
verifyIdentityIntent.putExtra("recipient", recipients.getPrimaryRecipient());
verifyIdentityIntent.putExtra("master_secret", masterSecret);
startActivity(verifyIdentityIntent);
}
private void verifyKeys() {
Intent verifyKeysIntent = new Intent(this, VerifyKeysActivity.class);
verifyKeysIntent.putExtra("recipient", recipients.getPrimaryRecipient());
verifyKeysIntent.putExtra("master_secret", masterSecret);
startActivity(verifyKeysIntent);
}
private void initiateSecureSession() {
Recipient recipient = recipients.getPrimaryRecipient();
String recipientName = (recipient.getName() == null ? recipient.getNumber() : recipient.getName());
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Initiate Secure Session?");
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setCancelable(true);
builder.setMessage("Initiate secure session with " + recipientName + "?");
builder.setPositiveButton(R.string.yes, new InitiateSecureSessionListener());
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void abortSecureSession() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Abort Secure Session Confirmation");
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setCancelable(true);
builder.setMessage("Are you sure that you want to abort this secure session?");
builder.setPositiveButton(R.string.yes, new AbortSessionListener());
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void dial(Recipient recipient) {
if (recipient == null) {
// XXX toast?
return;
}
Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.getNumber()));
startActivity(dialIntent);
}
private void displayMessageDetails(MessageRecord messageRecord) {
String sender = messageRecord.getRecipients().getPrimaryRecipient().getNumber();
String transport = messageRecord.isMms() ? "mms" : "sms";
long date = messageRecord.getDate();
SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE MMM d, yyyy 'at' hh:mm:ss a zzz");
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Message Details");
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setCancelable(false);
builder.setMessage("Sender: " + sender + "\nTransport: " + transport.toUpperCase() + "\nSent/Received: " + dateFormatter.format(new Date(date)));
builder.setPositiveButton("Ok", null);
builder.show();
}
private void forwardMessage(MessageRecord messageRecord) {
Intent composeIntent = new Intent(this, ComposeMessageActivity.class);
composeIntent.putExtra("forwarded_message", messageRecord.getBody());
composeIntent.putExtra("master_secret", masterSecret);
startActivity(composeIntent);
}
private void redecryptMessage(MessageRecord messageRecord, String address, String body) {
DatabaseFactory.getEncryptingSmsDatabase(this).markAsDecrypting(messageRecord.getId());
DecryptingQueue.scheduleDecryption(this, masterSecret, messageRecord.getId(), address, body);
}
private void copyMessageToClipboard(MessageRecord messageRecord) {
String body = messageRecord.getBody();
if (body == null) return;
ClipboardManager clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
clipboard.setText(body);
}
private void deleteMessage(MessageRecord messageRecord) {
long messageId = messageRecord.getId();
String transport = messageRecord.isMms() ? "mms" : "sms";
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Delete Message Confirmation");
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setCancelable(true);
builder.setMessage("Are you sure that you want to permanently delete this message?");
builder.setPositiveButton(R.string.yes, new DeleteMessageListener(messageId, transport));
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void deleteThread() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Delete Thread Confirmation");
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setCancelable(true);
builder.setMessage("Are you sure that you want to permanently delete this conversation?");
builder.setPositiveButton(R.string.yes, new DeleteThreadListener());
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void addAttachment() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.drawable.ic_dialog_attach);
builder.setTitle("Add attachment");
builder.setAdapter(attachmentAdapter, new AttachmentTypeListener());
builder.show();
}
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) {
attachmentManager.clear();
Toast.makeText(this, "Sorry, there was an error setting your attachment.", Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
}
}
private void addAttachmentVideo(Uri videoUri) {
try {
attachmentManager.setVideo(videoUri);
} catch (IOException e) {
attachmentManager.clear();
Toast.makeText(this, "Sorry, there was an error setting your attachment.", Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
} catch (MediaTooLargeException e) {
attachmentManager.clear();
Toast.makeText(this, "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, "Sorry, there was an error setting your attachment.", Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
} catch (MediaTooLargeException e) {
attachmentManager.clear();
Toast.makeText(this, "Sorry, the selected audio exceeds message size restrictions.", Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
}
}
private void initializeReceivers() {
killActivityReceiver = new KillActivityReceiver();
securityUpdateReceiver = new SecurityUpdateReceiver();
registerReceiver(killActivityReceiver, new IntentFilter(KeyCachingService.PASSPHRASE_EXPIRED_EVENT), KeyCachingService.KEY_PERMISSION, null);
registerReceiver(securityUpdateReceiver, new IntentFilter(KeyExchangeProcessor.SECURITY_UPDATE_EVENT), KeyCachingService.KEY_PERMISSION, null);
}
private void registerPassphraseActivityStarted() {
Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(KeyCachingService.ACTIVITY_START_EVENT);
startService(intent);
}
private void registerPassphraseActivityStopped() {
Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(KeyCachingService.ACTIVITY_STOP_EVENT);
startService(intent);
}
private void initializeSecurity(Recipients recipients) {
if (recipients != null && recipients.isSingleRecipient() && KeyUtil.isSessionFor(this, recipients.getPrimaryRecipient())) {
sendButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_lock_small, 0);
sendButton.setCompoundDrawablePadding(10);
this.sendEncrypted = true;
this.characterCalculator = new EncryptedCharacterCalculator();
} else {
sendButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
this.sendEncrypted = false;
this.characterCalculator = new CharacterCalculator();
}
calculateCharactersRemaining();
}
private void initializeResources() {
recipientsPanel = (RecipientsPanel)findViewById(R.id.recipients);
conversationView = (ListView)findViewById(R.id.conversation);
recipients = getIntent().getParcelableExtra("recipients");
threadId = getIntent().getLongExtra("thread_id", -1);
addContactButton = (ImageButton)findViewById(R.id.contacts_button);
sendButton = (Button)findViewById(R.id.send_button);
composeText = (EditText)findViewById(R.id.embedded_text_editor);
masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret");
charactersLeft = (TextView)findViewById(R.id.space_left);
titleBar = (TextView)findViewById(R.id.title_bar);
greyLock = findViewById(R.id.secure_indicator);
redLock = findViewById(R.id.secure_indicator_red);
attachmentAdapter = new AttachmentTypeSelectorAdapter(this);
attachmentManager = new AttachmentManager(this);
AuthenticityCheckListener authenticityListener = new AuthenticityCheckListener();
SendButtonListener sendButtonListener = new SendButtonListener();
recipientsPanel.setPanelChangeListener(new RecipientsPanelChangeListener());
sendButton.setOnClickListener(sendButtonListener);
addContactButton.setOnClickListener(new AddRecipientButtonListener());
composeText.setOnKeyListener(new ComposeKeyPressedListener());
composeText.addTextChangedListener(new OnTextChangedListener());
composeText.setOnEditorActionListener(sendButtonListener);
greyLock.setOnClickListener(authenticityListener);
redLock.setOnClickListener(authenticityListener);
this.registerForContextMenu(conversationView);
this.registerForContextMenu(sendButton);
if (getIntent().getStringExtra("forwarded_message") != null)
composeText.setText("FWD: " + getIntent().getStringExtra("forwarded_message"));
}
private void initializeTitleBarSecurity() {
redLock.setVisibility(View.GONE);
greyLock.setVisibility(View.GONE);
if (recipients != null && recipients.isSingleRecipient() && KeyUtil.isSessionFor(this, recipients.getPrimaryRecipient())) {
Recipient recipient = recipients.getPrimaryRecipient();
AuthenticityCalculator.setAuthenticityStatus(this, recipient, masterSecret, greyLock, redLock, titleBar);
}
}
private void initializeTitleBar() {
if (recipients != null && recipients.isSingleRecipient()) {
String name = recipients.getPrimaryRecipient().getName();
if (name == null || name.trim().length() == 0)
name = recipients.getPrimaryRecipient().getNumber();
titleBar.setText(name);
} else {
titleBar.setText("Compose Message");
}
initializeTitleBarSecurity();
}
private void initializeColors() {
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(ApplicationPreferencesActivity.DARK_CONVERSATION_PREF, false)) {
((LinearLayout)findViewById(R.id.layout_container)).setBackgroundDrawable(getResources().getDrawable(R.drawable.softgrey_background));
}
}
private void calculateCharactersRemaining() {
int charactersSpent = composeText.getText().length();
CharacterCalculator.CharacterState characterState = characterCalculator.calculateCharacters(charactersSpent);
charactersLeft.setText(characterState.charactersRemaining + "/" + characterState.maxMessageSize + " (" + characterState.messagesSpent + ")");
}
private void initializeRecipientsInput() {
recipientsPanel.setVisibility(View.VISIBLE);
if (this.recipients != null) {
recipientsPanel.addRecipients(this.recipients);
}
}
private void initializeConversationAdapter() {
Cursor cursor = DatabaseFactory.getMmsSmsDatabase(this).getConversation(threadId);
conversationAdapter = new ConversationAdapter(recipients, threadId, this, cursor, masterSecret, new FailedIconClickHandler());
conversationView.setAdapter(conversationAdapter);
conversationView.setItemsCanFocus(true);
conversationView.setVisibility(View.VISIBLE);
recipientsPanel.setVisibility(View.GONE);
composeText.requestFocus();
}
private boolean isExistingConversation() {
return this.recipients != null && this.threadId != -1;
}
private Recipients getRecipients() throws RecipientFormattingException {
if (isExistingConversation()) return this.recipients;
else return recipientsPanel.getRecipients();
}
private String getMessage() throws InvalidMessageException {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
String rawText = composeText.getText().toString();
if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent())
throw new InvalidMessageException("Message is empty!");
if (!sendEncrypted && sp.getBoolean(ApplicationPreferencesActivity.WHITESPACE_PREF, true) && rawText.length() <= 145)
rawText = rawText + " ";
return rawText;
}
private void sendComplete(Recipients recipients, long threadId) {
attachmentManager.clear();
recipientsPanel.disable();
composeText.setText("");
this.recipients = recipients;
this.threadId = threadId;
if (this.conversationAdapter == null) {
initializeConversationAdapter();
this.recipientsPanel.setVisibility(View.GONE);
initializeTitleBar();
}
}
private void sendMessage(boolean sendEncrypted) {
try {
Recipients recipients = getRecipients();
String message = getMessage();
long allocatedThreadId;
if (attachmentManager.isAttachmentPresent()) {
allocatedThreadId = MessageSender.sendMms(ComposeMessageActivity.this, masterSecret, recipients,
threadId, attachmentManager.getSlideDeck(), message,
sendEncrypted);
} else if (recipients.isEmailRecipient()) {
allocatedThreadId = MessageSender.sendMms(ComposeMessageActivity.this, masterSecret, recipients,
threadId, new SlideDeck(), message, sendEncrypted);
} else {
allocatedThreadId = MessageSender.send(ComposeMessageActivity.this, masterSecret, recipients,
threadId, message, sendEncrypted);
}
sendComplete(recipients, allocatedThreadId);
MessageNotifier.updateNotification(ComposeMessageActivity.this, false);
} catch (RecipientFormattingException ex) {
Toast.makeText(ComposeMessageActivity.this, "Recipient is not a valid SMS or email address!", Toast.LENGTH_LONG).show();
Log.w("compose", ex);
} catch (InvalidMessageException ex) {
Toast.makeText(ComposeMessageActivity.this, "Message is empty!", Toast.LENGTH_SHORT).show();
Log.w("compose", ex);
} catch (MmsException e) {
Log.w("ComposeMessageActivity", e);
}
}
// Listeners
private class KillActivityReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
finish();
}
}
private class SecurityUpdateReceiver extends 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(recipients);
initializeTitleBar();
calculateCharactersRemaining();
}
}
}
private class InitiateSecureSessionListener implements DialogInterface.OnClickListener {
public void onClick(DialogInterface dialogInterface, int which) {
KeyExchangeInitiator.initiate(ComposeMessageActivity.this, masterSecret, recipients.getPrimaryRecipient(), true);
}
}
private class FailedIconClickHandler extends Handler {
@Override
public void handleMessage(android.os.Message message) {
String failedMessageText = (String)message.obj;
ComposeMessageActivity.this.composeText.setText(failedMessageText);
}
}
private class OnTextChangedListener implements TextWatcher {
public void afterTextChanged(Editable s) {
calculateCharactersRemaining();
}
public void beforeTextChanged(CharSequence s, int start, int count,int after) {}
public void onTextChanged(CharSequence s, int start, int before,int count) {}
}
private class ComposeKeyPressedListener implements OnKeyListener {
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
if (PreferenceManager.getDefaultSharedPreferences(ComposeMessageActivity.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;
}
}
private class AuthenticityCheckListener implements OnClickListener {
public void onClick(View clicked) {
String message = null;
if (clicked == greyLock) message = "This session is verified to be authentic.";
else if (clicked == redLock) message = "Warning, this session has not yet been verified to be authentic. You should verify your session or the identity key of the person you are communicating with.";
AlertDialog.Builder builder = new AlertDialog.Builder(ComposeMessageActivity.this);
builder.setTitle("Authenticity");
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setCancelable(false);
builder.setMessage(message);
builder.setPositiveButton("Ok", null);
builder.show();
}
}
private class DeleteThreadListener implements DialogInterface.OnClickListener {
public void onClick(DialogInterface dialog, int which) {
if (threadId > 0) {
DatabaseFactory.getThreadDatabase(ComposeMessageActivity.this).deleteConversation(threadId);
finish();
}
}
};
private class DeleteMessageListener implements DialogInterface.OnClickListener {
private final long messageId;
private final String transport;
public DeleteMessageListener(long messageId, String transport) {
this.messageId = messageId;
this.transport = transport;
}
public void onClick(DialogInterface dialog, int which) {
Log.w("ComposeMessageActivity", "Calling delete on: " + messageId);
if (transport.equals("mms"))
DatabaseFactory.getMmsDatabase(ComposeMessageActivity.this).delete(messageId);
else
DatabaseFactory.getSmsDatabase(ComposeMessageActivity.this).deleteMessage(messageId);
}
}
private class AbortSessionListener implements DialogInterface.OnClickListener {
public void onClick(DialogInterface dialog, int which) {
if (recipients != null && recipients.isSingleRecipient()) {
KeyUtil.abortSessionFor(ComposeMessageActivity.this, recipients.getPrimaryRecipient());
initializeSecurity(recipients);
initializeTitleBar();
}
}
}
private class AddRecipientButtonListener implements OnClickListener {
public void onClick(View v) {
Intent intent = new Intent(ComposeMessageActivity.this, ContactSelectionActivity.class);
startActivityForResult(intent, PICK_CONTACT);
}
};
private class AttachmentTypeListener implements DialogInterface.OnClickListener {
public void onClick(DialogInterface dialog, int which) {
addAttachment(attachmentAdapter.buttonToCommand(which));
}
}
private class RecipientsPanelChangeListener implements RecipientsPanel.RecipientsPanelChangedListener {
public void onRecipientsPanelUpdate(Recipients recipients) {
initializeSecurity(recipients);
}
}
private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener {
public void onClick(View v) {
sendMessage(sendEncrypted);
}
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEND) {
sendButton.performClick();
composeText.clearFocus();
return true;
}
return false;
}
};
}

View File

@ -0,0 +1,664 @@
/**
* 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.content.SharedPreferences;
import android.net.Uri;
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.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragmentActivity;
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.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.SlideDeck;
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.service.MessageNotifier;
import org.thoughtcrime.securesms.sms.MessageSender;
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 ws.com.google.android.mms.MmsException;
import java.io.IOException;
/**
* Activity for displaying a message thread, as well as
* composing/sending a new message into that thread.
*
* @author Moxie Marlinspike
*
*/
public class ConversationActivity extends SherlockFragmentActivity {
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 Button sendButton;
private TextView charactersLeft;
private AttachmentTypeSelectorAdapter attachmentAdapter;
private AttachmentManager attachmentManager;
private BroadcastReceiver killActivityReceiver;
private BroadcastReceiver securityUpdateReceiver;
private Recipients recipients;
private long threadId;
private boolean sendEncrypted;
private CharacterCalculator characterCalculator = new CharacterCalculator();
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.conversation_activity);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
initializeReceivers();
initializeResources();
initializeTitleBar();
}
@Override
protected void onResume() {
super.onResume();
initializeSecurity(recipients);
initializeTitleBar();
calculateCharactersRemaining();
}
@Override
protected void onStart() {
super.onStart();
if (!isExistingConversation())
initializeRecipientsInput();
registerPassphraseActivityStarted();
}
@Override
protected void onStop() {
super.onStop();
registerPassphraseActivityStopped();
}
@Override
protected void onDestroy() {
unregisterReceiver(killActivityReceiver);
unregisterReceiver(securityUpdateReceiver);
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 = (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 (isSingleExistingConversation() && sendEncrypted)
{
if (isAuthenticatedSession()) {
inflater.inflate(R.menu.conversation_secure_verified, menu);
} else {
inflater.inflate(R.menu.conversation_secure_unverified, menu);
}
} else if (isSingleExistingConversation()) {
inflater.inflate(R.menu.conversation_insecure, menu);
}
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(recipients.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;
}
return false;
}
@Override
public void onCreateContextMenu (ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
if (sendEncrypted) {
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(false); return true;
}
return false;
}
//////// Event Handlers
private void handleVerifyRecipient() {
Intent verifyIdentityIntent = new Intent(this, VerifyIdentityActivity.class);
verifyIdentityIntent.putExtra("recipient", recipients.getPrimaryRecipient());
verifyIdentityIntent.putExtra("master_secret", masterSecret);
startActivity(verifyIdentityIntent);
}
private void handleVerifySession() {
Intent verifyKeysIntent = new Intent(this, VerifyKeysActivity.class);
verifyKeysIntent.putExtra("recipient", recipients.getPrimaryRecipient());
verifyKeysIntent.putExtra("master_secret", masterSecret);
startActivity(verifyKeysIntent);
}
private void handleStartSecureSession() {
Recipient recipient = recipients.getPrimaryRecipient();
String recipientName = (recipient.getName() == null ? recipient.getNumber() : recipient.getName());
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Initiate Secure Session?");
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setCancelable(true);
builder.setMessage("Initiate secure session with " + recipientName + "?");
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
KeyExchangeInitiator.initiate(ConversationActivity.this, masterSecret,
recipients.getPrimaryRecipient(), true);
}
});
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void handleAbortSecureSession() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Abort Secure Session Confirmation");
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setCancelable(true);
builder.setMessage("Are you sure that you want to abort this secure session?");
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (recipients != null && recipients.isSingleRecipient()) {
KeyUtil.abortSessionFor(ConversationActivity.this, recipients.getPrimaryRecipient());
initializeSecurity(recipients);
initializeTitleBar();
}
}
});
builder.setNegativeButton(R.string.no, null);
builder.show();
}
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 handleDeleteThread() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Delete Thread Confirmation");
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setCancelable(true);
builder.setMessage("Are you sure that you want to permanently delete this conversation?");
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (threadId > 0) {
DatabaseFactory.getThreadDatabase(ConversationActivity.this).deleteConversation(threadId);
finish();
}
}
});
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void handleAddAttachment() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.drawable.ic_dialog_attach);
builder.setTitle("Add attachment");
builder.setAdapter(attachmentAdapter, new AttachmentTypeListener());
builder.show();
}
///// Initializers
private void initializeTitleBar() {
String title = null;
String subtitle = null;
if (isSingleExistingConversation()) {
if (sendEncrypted) {
title = AuthenticityCalculator.getAuthenticatedName(this,
recipients.getPrimaryRecipient(),
masterSecret);
}
if (title == null || title.trim().length() == 0) {
title = recipients.getPrimaryRecipient().getName();
}
if (title == null || title.trim().length() == 0) {
title = recipients.getPrimaryRecipient().getNumber();
} else {
subtitle = recipients.getPrimaryRecipient().getNumber();
}
} else {
title = "Compose Message";
}
this.getSupportActionBar().setTitle(title);
if (subtitle != null)
this.getSupportActionBar().setSubtitle(PhoneNumberUtils.formatNumber(subtitle));
this.invalidateOptionsMenu();
}
private void initializeSecurity(Recipients recipients) {
if (isSingleExistingConversation() &&
KeyUtil.isSessionFor(this, recipients.getPrimaryRecipient()))
{
sendButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_lock_small, 0);
sendButton.setCompoundDrawablePadding(10);
this.sendEncrypted = true;
this.characterCalculator = new EncryptedCharacterCalculator();
} else {
sendButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
this.sendEncrypted = false;
this.characterCalculator = new CharacterCalculator();
}
calculateCharactersRemaining();
}
private void initializeResources() {
recipientsPanel = (RecipientsPanel)findViewById(R.id.recipients);
recipients = getIntent().getParcelableExtra("recipients");
threadId = getIntent().getLongExtra("thread_id", -1);
addContactButton = (ImageButton)findViewById(R.id.contacts_button);
sendButton = (Button)findViewById(R.id.send_button);
composeText = (EditText)findViewById(R.id.embedded_text_editor);
masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret");
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);
addContactButton.setOnClickListener(new AddRecipientButtonListener());
composeText.setOnKeyListener(new ComposeKeyPressedListener());
composeText.addTextChangedListener(new OnTextChangedListener());
composeText.setOnEditorActionListener(sendButtonListener);
registerForContextMenu(sendButton);
if (getIntent().getStringExtra("forwarded_message") != null)
composeText.setText("FWD: " + getIntent().getStringExtra("forwarded_message"));
}
private void initializeRecipientsInput() {
recipientsPanel.setVisibility(View.VISIBLE);
if (this.recipients != null) {
recipientsPanel.addRecipients(this.recipients);
}
}
private void initializeReceivers() {
killActivityReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
finish();
}
};
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(recipients);
initializeTitleBar();
calculateCharactersRemaining();
}
}
};
registerReceiver(killActivityReceiver,
new IntentFilter(KeyCachingService.PASSPHRASE_EXPIRED_EVENT),
KeyCachingService.KEY_PERMISSION, null);
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) {
attachmentManager.clear();
Toast.makeText(this, "Sorry, there was an error setting your attachment.",
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
}
}
private void addAttachmentVideo(Uri videoUri) {
try {
attachmentManager.setVideo(videoUri);
} catch (IOException e) {
attachmentManager.clear();
Toast.makeText(this, "Sorry, there was an error setting your attachment.",
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
} catch (MediaTooLargeException e) {
attachmentManager.clear();
Toast.makeText(this, "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, "Sorry, there was an error setting your attachment.",
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
} catch (MediaTooLargeException e) {
attachmentManager.clear();
Toast.makeText(this, "Sorry, the selected audio exceeds message size restrictions.",
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
}
}
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 isSingleExistingConversation() {
return this.recipients != null && this.recipients.isSingleRecipient();
}
private boolean isAuthenticatedSession() {
return AuthenticityCalculator.isAuthenticated(this,
recipients.getPrimaryRecipient(),
masterSecret);
}
private Recipients getRecipients() throws RecipientFormattingException {
if (isExistingConversation()) return this.recipients;
else return recipientsPanel.getRecipients();
}
private String getMessage() throws InvalidMessageException {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
String rawText = composeText.getText().toString();
if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent())
throw new InvalidMessageException("Message is empty!");
if (!sendEncrypted && sp.getBoolean(ApplicationPreferencesActivity.WHITESPACE_PREF, true) && rawText.length() <= 145)
rawText = rawText + " ";
return rawText;
}
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) {
///XXX call down to fragment! ??
// initializeConversationAdapter();
this.recipientsPanel.setVisibility(View.GONE);
initializeTitleBar();
}
}
private void sendMessage(boolean sendEncrypted) {
try {
Recipients recipients = getRecipients();
String message = getMessage();
long allocatedThreadId;
if (attachmentManager.isAttachmentPresent()) {
allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients,
threadId, attachmentManager.getSlideDeck(), message,
sendEncrypted);
} else if (recipients.isEmailRecipient()) {
allocatedThreadId = MessageSender.sendMms(ConversationActivity.this, masterSecret, recipients,
threadId, new SlideDeck(), message, sendEncrypted);
} else {
allocatedThreadId = MessageSender.send(ConversationActivity.this, masterSecret, recipients,
threadId, message, sendEncrypted);
}
sendComplete(recipients, allocatedThreadId);
MessageNotifier.updateNotification(ConversationActivity.this, false);
} catch (RecipientFormattingException ex) {
Toast.makeText(ConversationActivity.this, "Recipient is not a valid SMS or email address!", Toast.LENGTH_LONG).show();
Log.w("compose", ex);
} catch (InvalidMessageException ex) {
Toast.makeText(ConversationActivity.this, "Message is empty!", Toast.LENGTH_SHORT).show();
Log.w("compose", ex);
} catch (MmsException e) {
Log.w("ComposeMessageActivity", e);
}
}
private void registerPassphraseActivityStarted() {
Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(KeyCachingService.ACTIVITY_START_EVENT);
startService(intent);
}
private void registerPassphraseActivityStopped() {
Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(KeyCachingService.ACTIVITY_STOP_EVENT);
startService(intent);
}
// Listeners
private class AddRecipientButtonListener implements OnClickListener {
public void onClick(View v) {
Intent intent = new Intent(ConversationActivity.this, ContactSelectionActivity.class);
startActivityForResult(intent, PICK_CONTACT);
}
};
private class AttachmentTypeListener implements DialogInterface.OnClickListener {
public void onClick(DialogInterface dialog, int which) {
addAttachment(attachmentAdapter.buttonToCommand(which));
}
}
private class RecipientsPanelChangeListener implements RecipientsPanel.RecipientsPanelChangedListener {
public void onRecipientsPanelUpdate(Recipients recipients) {
initializeSecurity(recipients);
}
}
private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener {
public void onClick(View v) {
sendMessage(sendEncrypted);
}
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 {
public void afterTextChanged(Editable s) {
calculateCharactersRemaining();
}
public void beforeTextChanged(CharSequence s, int start, int count,int after) {}
public void onTextChanged(CharSequence s, int start, int before,int count) {}
}
private class ComposeKeyPressedListener implements OnKeyListener {
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;
}
}
}

View File

@ -1,6 +1,6 @@
/**
/**
* Copyright (C) 2011 Whisper Systems
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@ -10,13 +10,20 @@
* 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 java.util.LinkedHashMap;
import android.content.Context;
import android.database.Cursor;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -33,40 +40,34 @@ import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.MessageNotifier;
import ws.com.google.android.mms.MmsException;
import android.content.Context;
import android.database.Cursor;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import java.util.LinkedHashMap;
/**
* A cursor adapter for a conversation thread. Ultimately
* used by ComposeMessageActivity to display a conversation
* thread in a ListActivity.
*
*
* @author Moxie Marlinspike
*
*/
public class ConversationAdapter extends CursorAdapter {
private static final int MAX_CACHE_SIZE = 40;
private final TouchListener touchListener = new TouchListener();
private final LinkedHashMap<String,MessageRecord> messageRecordCache;
private final Handler failedIconClickHandler;
private final long threadId;
private final long threadId;
private final Context context;
private final Recipients recipients;
private final MasterSecret masterSecret;
private final MasterCipher masterCipher;
private boolean dataChanged;
public ConversationAdapter(Recipients recipients, long threadId, Context context, Cursor c, MasterSecret masterSecret, Handler failedIconClickHandler) {
super(context, c);
public ConversationAdapter(Recipients recipients, long threadId, Context context, MasterSecret masterSecret, Handler failedIconClickHandler) {
super(context, null);
this.context = context;
this.recipients = recipients;
this.threadId = threadId;
@ -75,14 +76,14 @@ public class ConversationAdapter extends CursorAdapter {
this.dataChanged = false;
this.failedIconClickHandler = failedIconClickHandler;
this.messageRecordCache = initializeCache();
DatabaseFactory.getThreadDatabase(context).setRead(threadId);
MessageNotifier.updateNotification(context, false);
}
private Recipient buildRecipient(String address) {
Recipient recipient;
try {
if (address == null) recipient = recipients.getPrimaryRecipient();
else recipient = RecipientFactory.getRecipientsFromString(context, address).getPrimaryRecipient();
@ -90,17 +91,17 @@ public class ConversationAdapter extends CursorAdapter {
Log.w("ConversationAdapter", e);
recipient = new Recipient("Unknown", "Unknown", null);
}
return recipient;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
String type = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.TRANSPORT));
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
((ConversationItem)view).set(masterSecret, messageRecord, failedIconClickHandler);
((ConversationItem)view).set(masterSecret, messageRecord, failedIconClickHandler);
view.setOnTouchListener(touchListener);
}
@ -111,12 +112,12 @@ public class ConversationAdapter extends CursorAdapter {
return view;
}
private MessageRecord getNewMmsMessageRecord(long messageId, Cursor cursor) {
MessageRecord messageRecord = getNewSmsMessageRecord(messageId, cursor);
long mmsType = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_TYPE));
long mmsBox = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
try {
return MmsFactory.getMms(context, masterSecret, messageRecord, mmsType, mmsBox);
} catch (MmsException me) {
@ -124,64 +125,65 @@ public class ConversationAdapter extends CursorAdapter {
return messageRecord;
}
}
private MessageRecord getNewSmsMessageRecord(long messageId, Cursor cursor) {
long date = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.DATE));
long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE));
String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
Recipient recipient = buildRecipient(address);
MessageRecord messageRecord = new MessageRecord(messageId, recipients, date, type, threadId);
messageRecord.setMessageRecipient(recipient);
setBody(cursor, messageRecord);
return messageRecord;
}
private MessageRecord getMessageRecord(long messageId, Cursor cursor, String type) {
if (messageRecordCache.containsKey(type + messageId))
return messageRecordCache.get(type + messageId);
MessageRecord messageRecord;
if (type.equals("mms")) messageRecord = getNewMmsMessageRecord(messageId, cursor);
else messageRecord = getNewSmsMessageRecord(messageId, cursor);
messageRecordCache.put(type + messageId, messageRecord);
messageRecordCache.put(type + messageId, messageRecord);
return messageRecord;
}
protected void setBody(Cursor cursor, MessageRecord message) {
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
if (body == null)
message.setBody("");
else
MessageDisplayHelper.setDecryptedMessageBody(body, message, masterCipher);
}
@Override
protected void onContentChanged() {
super.onContentChanged();
messageRecordCache.clear();
DatabaseFactory.getThreadDatabase(context).setRead(threadId);
this.dataChanged = true;
}
public void close() {
this.getCursor().close();
}
private class TouchListener implements View.OnTouchListener {
public boolean onTouch(View v, MotionEvent event) {
if (ConversationAdapter.this.dataChanged) {
ConversationAdapter.this.dataChanged = false;
MessageNotifier.updateNotification(context, false);
}
return false;
}
}
}
private LinkedHashMap<String,MessageRecord> initializeCache() {
return new LinkedHashMap<String,MessageRecord>() {
@Override
@ -190,6 +192,6 @@ public class ConversationAdapter extends CursorAdapter {
}
};
}
}

View File

@ -0,0 +1,176 @@
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.text.ClipboardManager;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import com.actionbarsherlock.app.SherlockListFragment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageRecord;
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.sql.Date;
import java.text.SimpleDateFormat;
public class ConversationFragment extends SherlockListFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
private MasterSecret masterSecret;
private Recipients recipients;
private long threadId;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
return inflater.inflate(R.layout.conversation_fragment, container, false);
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
initializeResources();
initializeListAdapter();
registerForContextMenu(getListView());
getLoaderManager().initLoader(0, null, this);
}
@Override
public void onCreateContextMenu (ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
android.view.MenuInflater inflater = this.getSherlockActivity().getMenuInflater();
menu.clear();
inflater.inflate(R.menu.conversation_context, menu);
}
@Override
public boolean onContextItemSelected(android.view.MenuItem item) {
Cursor cursor = ((CursorAdapter)getListAdapter()).getCursor();
ConversationItem conversationItem = (ConversationItem)(((ConversationAdapter)getListAdapter()).newView(getActivity(), cursor, null));
MessageRecord messageRecord = conversationItem.getMessageRecord();
switch(item.getItemId()) {
case R.id.menu_context_copy: handleCopyMessage(messageRecord); return true;
case R.id.menu_context_delete_message: handleDeleteMessage(messageRecord); return true;
case R.id.menu_context_details: handleDisplayDetails(messageRecord); return true;
case R.id.menu_context_forward: handleForwardMessage(messageRecord); return true;
}
return false;
}
private void handleCopyMessage(MessageRecord message) {
String body = message.getBody();
if (body == null) return;
ClipboardManager clipboard = (ClipboardManager)getActivity()
.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setText(body);
}
private void handleDeleteMessage(MessageRecord message) {
final long messageId = message.getId();
final String transport = message.isMms() ? "mms" : "sms";
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("Delete Message Confirmation");
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setCancelable(true);
builder.setMessage("Are you sure that you want to permanently delete this message?");
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (transport.equals("mms")) {
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageId);
} else {
DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageId);
}
}
});
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void handleDisplayDetails(MessageRecord message) {
String sender = message.getRecipients().getPrimaryRecipient().getNumber();
String transport = message.isMms() ? "mms" : "sms";
long date = message.getDate();
SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE MMM d, yyyy 'at' hh:mm:ss a zzz");
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("Message Details");
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setCancelable(false);
builder.setMessage("Sender: " + sender + "\nTransport: " + transport.toUpperCase() +
"\nSent/Received: " + dateFormatter.format(new Date(date)));
builder.setPositiveButton("Ok", null);
builder.show();
}
private void handleForwardMessage(MessageRecord message) {
Intent composeIntent = new Intent(getActivity(), ConversationActivity.class);
composeIntent.putExtra("forwarded_message", message.getBody());
composeIntent.putExtra("master_secret", masterSecret);
startActivity(composeIntent);
}
private void initializeResources() {
this.masterSecret = (MasterSecret)this.getActivity().getIntent()
.getParcelableExtra("master_secret");
this.recipients = this.getActivity().getIntent().getParcelableExtra("recipients");
this.threadId = this.getActivity().getIntent().getLongExtra("thread_id", -1);
}
private void initializeListAdapter() {
if (this.recipients != null && this.threadId != -1) {
this.setListAdapter(new ConversationAdapter(recipients, threadId, getActivity(), masterSecret, new FailedIconClickHandler()));
}
}
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
if (this.threadId != -1) {
return new ConversationLoader(getActivity(), threadId);
} else {
return null;
}
}
@Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) {
((CursorAdapter)getListAdapter()).changeCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> arg0) {
((CursorAdapter)getListAdapter()).changeCursor(null);
}
private class FailedIconClickHandler extends Handler {
@Override
public void handleMessage(android.os.Message message) {
assert(false);
// String failedMessageText = (String)message.obj;
// ConversationActivity.this.composeText.setText(failedMessageText);
}
}
}

View File

@ -145,7 +145,7 @@ public class ConversationListActivity extends SherlockFragmentActivity
return;
}
Intent intent = new Intent(this, ComposeMessageActivity.class);
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra("recipients", recipients);
intent.putExtra("thread_id", threadId);
intent.putExtra("master_secret", masterSecret);

View File

@ -1,6 +1,6 @@
/**
/**
* Copyright (C) 2011 Whisper Systems
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@ -10,42 +10,53 @@
* 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.crypto;
import android.content.Context;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.SessionRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import android.content.Context;
import android.view.View;
import android.widget.TextView;
public class AuthenticityCalculator {
private static void setAuthentictyForIdentity(Context context, MasterSecret masterSecret, IdentityKey identityKey, View grey, View red, TextView titleBar)
private static boolean isAuthenticatedIdentity(Context context,
MasterSecret masterSecret,
IdentityKey identityKey)
{
String identityName = DatabaseFactory.getIdentityDatabase(context).getNameForIdentity(masterSecret, identityKey);
if (identityName == null) {
red.setVisibility(View.VISIBLE);
return;
}
grey.setVisibility(View.VISIBLE);
titleBar.setText(identityName);
String identityName = DatabaseFactory.getIdentityDatabase(context)
.getNameForIdentity(masterSecret, identityKey);
if (identityName == null) return false;
else return true;
}
public static void setAuthenticityStatus(Context context, Recipient recipient, MasterSecret masterSecret, View grey, View red, TextView titleBar)
public static String getAuthenticatedName(Context context,
Recipient recipient,
MasterSecret masterSecret)
{
SessionRecord session = new SessionRecord(context, masterSecret, recipient);
if (session.isVerifiedSession()) grey.setVisibility(View.VISIBLE);
else if (session.getIdentityKey() != null) setAuthentictyForIdentity(context, masterSecret, session.getIdentityKey(), grey, red, titleBar);
else red.setVisibility(View.VISIBLE);
return DatabaseFactory.getIdentityDatabase(context)
.getNameForIdentity(masterSecret, session.getIdentityKey());
}
public static boolean isAuthenticated(Context context,
Recipient recipient,
MasterSecret masterSecret)
{
SessionRecord session = new SessionRecord(context, masterSecret, recipient);
if (session.isVerifiedSession()) {
return true;
} else if (session.getIdentityKey() != null) {
return isAuthenticatedIdentity(context, masterSecret, session.getIdentityKey());
}
return false;
}
}

View File

@ -0,0 +1,23 @@
package org.thoughtcrime.securesms.database.loaders;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.CursorLoader;
import org.thoughtcrime.securesms.database.DatabaseFactory;
public class ConversationLoader extends CursorLoader {
private final Context context;
private final long threadId;
public ConversationLoader(Context context, long threadId) {
super(context);
this.context = context.getApplicationContext();
this.threadId = threadId;
}
@Override
public Cursor loadInBackground() {
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId);
}
}