Improve passphrase and onboarding UI. Abstract out routing.

1) Update the create, prompt, and change passphrase activities.
   They are no longer dialog themed, and should look a little
   less ugly.

2) Update the import DB activity to be less ugly and more robust.

3) Abstract all of the state handling stuff out of
   ConversationListActivity.  This is now handled by RoutingActivity,
   which all launch intents move through.
This commit is contained in:
Moxie Marlinspike 2013-02-17 11:42:30 -08:00
parent 9e3da08d45
commit 5eb04328d3
30 changed files with 1177 additions and 718 deletions

View File

@ -35,10 +35,9 @@
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Sherlock.Light.DarkActionBar"> android:theme="@style/Theme.Sherlock.Light.DarkActionBar">
<activity android:name=".ConversationListActivity" <activity android:name=".RoutingActivity"
android:label="@string/app_name" android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:launchMode="singleTask" android:launchMode="singleTask"
android:uiOptions="splitActionBarWhenNarrow"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter> <intent-filter>
@ -62,22 +61,34 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ConversationListActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:uiOptions="splitActionBarWhenNarrow"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ConversationActivity" <activity android:name=".ConversationActivity"
android:windowSoftInputMode="stateUnchanged" android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseCreateActivity" <activity android:name=".PassphraseCreateActivity"
android:theme="@style/Theme.Sherlock.Light.Dialog"
android:label="@string/AndroidManifest__create_passphrase" android:label="@string/AndroidManifest__create_passphrase"
android:launchMode="singleInstance" android:windowSoftInputMode="stateUnchanged"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphrasePromptActivity" <activity android:name=".PassphrasePromptActivity"
android:theme="@style/Theme.Sherlock.Light.Dialog"
android:label="@string/AndroidManifest__enter_passphrase" android:label="@string/AndroidManifest__enter_passphrase"
android:launchMode="singleInstance" android:launchMode="singleTop"
android:windowSoftInputMode="stateVisible" android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ContactSelectionActivity" <activity android:name=".ContactSelectionActivity"
@ -95,7 +106,6 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseChangeActivity" <activity android:name=".PassphraseChangeActivity"
android:theme="@style/Theme.Sherlock.Light.Dialog"
android:label="@string/AndroidManifest__change_passphrase" android:label="@string/AndroidManifest__change_passphrase"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap android:src="@drawable/background_pattern" android:tileMode="repeat"
xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@ -1,90 +1,81 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:layout_height="fill_parent" > android:fillViewport="true"
android:background="@drawable/background_pattern_repeat">
<LinearLayout <FrameLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="fill_parent"
android:paddingLeft="16dip" android:gravity="center" >
android:paddingRight="16dip"
android:orientation="vertical" > <LinearLayout android:paddingRight="16dip"
android:paddingLeft="16dip"
android:paddingTop="10dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible"
android:orientation="vertical">
<TextView style="@style/Registration.Label"
android:layout_width="fill_parent"
android:textAllCaps="true"
android:text="@string/change_passphrase_activity__old_passphrase" />
<EditText android:id="@+id/old_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:layout_marginBottom="10dip"
android:singleLine="true"/>
<TextView <TextView style="@style/Registration.Label"
android:padding="3dip" android:layout_width="fill_parent"
android:text="@string/change_passphrase_activity__old_passphrase" android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceMedium" android:text="@string/change_passphrase_activity__new_passphrase" />
android:layout_width="fill_parent"
android:layout_height="wrap_content"/> <EditText android:id="@+id/new_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true"/>
<EditText <TextView style="@style/Registration.Label"
android:id="@+id/old_passphrase" android:layout_width="fill_parent"
android:layout_width="fill_parent" android:textAllCaps="true"
android:layout_height="wrap_content" android:text="@string/change_passphrase_activity__repeat_new_passphrase" />
android:inputType="textPassword"
android:layout_marginBottom="5dip" /> <EditText android:id="@+id/repeat_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true"/>
<TextView <LinearLayout android:orientation="horizontal"
android:padding="3dip" android:layout_width="fill_parent"
android:text="@string/change_passphrase_activity__new_passphrase" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_marginTop="30dip">
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText <Button style="@android:style/Widget.Button"
android:id="@+id/new_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:layout_marginBottom="5dip" />
<TextView
android:padding="3dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/change_passphrase_activity__repeat_new_passphrase"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/repeat_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:layout_marginBottom="5dip" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dip"
android:gravity="right"
android:orientation="horizontal" >
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchColumns="*"
tools:ignore="UselessParent" >
<TableRow>
<Button
android:id="@+id/cancel_button" android:id="@+id/cancel_button"
android:layout_width="wrap_content" android:text="@android:string/cancel"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="16dip" android:layout_marginRight="7dip"
android:layout_marginRight="15dip" android:textAppearance="?android:attr/textAppearanceMedium"/>
android:text="@android:string/cancel" />
<Button style="@android:style/Widget.Button"
<Button
android:id="@+id/ok_button" android:id="@+id/ok_button"
android:layout_width="wrap_content" android:text="@android:string/ok"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginRight="16dip" android:textAppearance="?android:attr/textAppearanceMedium"/>
android:text="@android:string/ok" /> </LinearLayout>
</TableRow>
</TableLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </FrameLayout>
</ScrollView> </ScrollView>

View File

@ -1,74 +1,87 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView android:id="@+id/ScrollView" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android"> android:fillViewport="true"
android:background="@drawable/background_pattern_repeat">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="fill_parent"
android:paddingLeft="16dip" android:gravity="center" >
android:paddingRight="16dip"
android:orientation="vertical"> <LinearLayout android:id="@+id/create_layout"
android:paddingRight="16dip"
<TextView android:text="@string/create_passphrase_activity__please_choose_a_passphrase_that_will_be_used_to_locally_encrypt_your_data_this_should_be_a_strong_passphrase" android:paddingLeft="16dip"
android:textAppearance="?android:attr/textAppearanceMedium" android:paddingTop="10dip"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
android:layout_marginBottom="10dip"/>
<TextView android:text="@string/create_passphrase_activity__passphrase"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="5dip"/>
<EditText android:id="@+id/passphrase_edit"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:inputType="textPassword"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/repeat_label"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_marginBottom="5dip"
android:layout_marginTop="10dip"
android:text="@string/create_passphrase_activity__repeat"/>
<EditText
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:inputType="textPassword"
android:id="@+id/passphrase_edit_repeat"/>
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_gravity="center"
android:visibility="visible"
android:orientation="vertical">
<TextView style="@style/Registration.Description"
android:layout_width="fill_parent"
android:layout_marginBottom="16dip" android:layout_marginBottom="16dip"
android:gravity="right"> android:layout_marginTop="16dip"
android:text="@string/create_passphrase_activity__please_choose_a_passphrase_that_will_be_used_to_locally_encrypt_your_data_this_should_be_a_strong_passphrase"/>
<TextView style="@style/Registration.Label"
android:layout_width="fill_parent"
android:textAllCaps="true"
android:text="@string/create_passphrase_activity__passphrase" />
<EditText android:id="@+id/passphrase_edit"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:layout_marginBottom="10dip"
android:singleLine="true"/>
<TableLayout android:layout_width="wrap_content" <TextView style="@style/Registration.Label"
android:layout_height="wrap_content" android:layout_width="fill_parent"
android:stretchColumns="*"> android:textAllCaps="true"
<TableRow> android:text="@string/create_passphrase_activity__repeat" />
<Button android:layout_height="wrap_content"
android:layout_width="wrap_content" <EditText android:id="@+id/passphrase_edit_repeat"
android:text="@android:string/cancel" android:layout_width="fill_parent"
android:id="@+id/cancel_button" android:layout_height="wrap_content"
android:layout_marginRight="15dip" android:inputType="textPassword"
android:layout_marginLeft="16dip"/> android:singleLine="true"/>
<Button android:layout_width="wrap_content" <Button style="@android:style/Widget.Button"
android:layout_height="wrap_content" android:id="@+id/ok_button"
android:text="@android:string/ok" android:text="@string/create_passphrase_activity__continue"
android:id="@+id/ok_button" android:layout_width="wrap_content"
android:layout_marginRight="16dip"/> android:layout_height="wrap_content"
</TableRow> android:layout_gravity="right"
</TableLayout> android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_marginTop="20dip"
android:layout_marginBottom="20dip"/>
</LinearLayout> </LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/progress_layout"
android:paddingRight="16dip"
android:paddingLeft="16dip"
android:paddingTop="10dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:orientation="vertical">
<TextView style="@style/Registration.BigLabel"
android:layout_width="fill_parent"
android:layout_marginBottom="16dip"
android:layout_marginTop="16dip"
android:gravity="center"
android:text="@string/create_passphrase_activity__generating_secrets"/>
<ProgressBar android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="center"/>
</LinearLayout>
</FrameLayout>
</ScrollView> </ScrollView>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fillViewport="true"
android:background="@drawable/background_pattern_repeat">
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center" >
<LinearLayout android:id="@+id/prompt_layout"
android:paddingRight="16dip"
android:paddingLeft="16dip"
android:paddingTop="10dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:orientation="vertical">
<TextView style="@style/Registration.Description"
android:layout_width="fill_parent"
android:layout_marginBottom="16dip"
android:text="@string/database_migration_activity__would_you_like_to_import_your_existing_text_messages"/>
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/import_database"/>
<TextView style="@style/Registration.Description"
android:layout_width="fill_parent"
android:layout_marginBottom="16dip"
android:layout_marginTop="16dip"
android:text="@string/database_migration_activity__the_default_system_database_will_not_be_modified"/>
<LinearLayout android:orientation="horizontal"
android:gravity="center"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dip"
android:layout_marginRight="20dip">
<Button style="@android:style/Widget.Button"
android:id="@+id/skip_button"
android:text="@string/database_migration_activity__skip"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginRight="7dip"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<Button style="@android:style/Widget.Button"
android:id="@+id/import_button"
android:text="@string/database_migration_activity__import"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:textAppearance="?android:attr/textAppearanceMedium"/>
</LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/progress_layout"
android:paddingRight="16dip"
android:paddingLeft="16dip"
android:paddingTop="10dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible"
android:orientation="vertical">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/import_database"/>
<TextView style="@style/Registration.Description"
android:layout_width="fill_parent"
android:layout_marginTop="34dip"
android:layout_marginBottom="16dip"
android:text="@string/database_migration_activity__this_could_take_a_moment_please_be_patient"/>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dip"
android:layout_weight="1.0" >
<TextView
android:id="@+id/import_status"
style="@style/Registration.Constant"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="0/100"
android:textSize="12.0sp" />
<TextView
style="@style/Registration.Constant"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:text="@string/database_migration_activity__importing"
android:textAllCaps="true"
android:textSize="12.0sp"
android:textStyle="normal" />
</RelativeLayout>
<ProgressBar
android:id="@+id/import_progress"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="14.0dip"
android:layout_marginTop="2.0dip" />
</LinearLayout>
</FrameLayout>
</ScrollView>

View File

@ -1,39 +1,58 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText android:inputType="textPassword" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content" android:layout_width="fill_parent"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:id="@+id/passphrase_edit" android:fillViewport="true"
android:password="true" android:background="@drawable/background_pattern_repeat">
android:layout_margin="16dip"/>
<LinearLayout android:layout_width="fill_parent" <FrameLayout
android:layout_height="wrap_content" android:layout_width="fill_parent"
android:orientation="horizontal" android:layout_height="fill_parent"
android:layout_marginBottom="16dip" android:gravity="center" >
android:gravity="right">
<LinearLayout android:id="@+id/prompt_layout"
<TableLayout android:layout_width="wrap_content" android:paddingRight="16dip"
android:layout_height="wrap_content" android:paddingLeft="16dip"
android:stretchColumns="*"> android:paddingTop="10dip"
<TableRow> android:layout_width="fill_parent"
<Button android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="wrap_content" android:layout_gravity="center"
android:text="@android:string/cancel" android:orientation="vertical">
android:id="@+id/cancel_button"
android:layout_marginRight="15dip" <ImageView android:layout_width="wrap_content"
android:layout_marginLeft="16dip"/> android:layout_height="wrap_content"
android:layout_gravity="center"
<Button android:layout_width="wrap_content" android:layout_marginBottom="30dip"
android:layout_height="wrap_content" android:src="@drawable/padlock_prompt"/>
android:text="@android:string/ok"
<TextView style="@style/Registration.Label"
android:layout_width="fill_parent"
android:textAllCaps="true"
android:text="@string/prompt_passphrase_activity__textsecure_passphrase" />
<EditText android:id="@+id/passphrase_edit"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:layout_marginBottom="10dip"
android:singleLine="true"/>
<LinearLayout android:orientation="horizontal"
android:gravity="right"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dip"
android:layout_marginRight="20dip">
<Button style="@android:style/Widget.Button"
android:id="@+id/ok_button" android:id="@+id/ok_button"
android:layout_marginRight="16dip"/> android:text="@string/prompt_passphrase_activity__unlock"
</TableRow> android:layout_width="wrap_content"
</TableLayout> android:layout_height="wrap_content"
</LinearLayout> android:layout_gravity="right"
</LinearLayout> android:textAppearance="?android:attr/textAppearanceMedium"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</ScrollView>

View File

@ -118,9 +118,8 @@
<string name="PassphraseChangeActivity_incorrect_old_passphrase_exclamation">Incorrect old passphrase!</string> <string name="PassphraseChangeActivity_incorrect_old_passphrase_exclamation">Incorrect old passphrase!</string>
<!-- PassphraseCreateActivity --> <!-- PassphraseCreateActivity -->
<string name="PassphraseCreateActivity_passphrases_dont_match_exclamation">Passphrases Don\'t Match!</string> <string name="PassphraseCreateActivity_passphrases_dont_match">Passphrases don\'t match</string>
<string name="PassphraseCreateActivity_generating_keypair">Generating KeyPair</string> <string name="PassphraseCreateActivity_you_must_specify_a_password">You must specify a password</string>
<string name="PassphraseCreateActivity_generating_a_local_encryption_keypair">Generating a local encryption keypair...</string>
<!-- PassphrasePromptActivity --> <!-- PassphrasePromptActivity -->
<string name="PassphrasePromptActivity_invalid_passphrase_exclamation">Invalid Passphrase!</string> <string name="PassphrasePromptActivity_invalid_passphrase_exclamation">Invalid Passphrase!</string>
@ -225,8 +224,9 @@
<string name="MmsSender_currently_unable_to_send_your_mms_message">Currently unable to send your MMS message. It will be sent once service becomes available.</string> <string name="MmsSender_currently_unable_to_send_your_mms_message">Currently unable to send your MMS message. It will be sent once service becomes available.</string>
<!-- ApplicationMigrationService --> <!-- ApplicationMigrationService -->
<string name="ApplicationMigrationService_migrating">Migrating</string> <string name="ApplicationMigrationService_import_in_progress">Import in progress</string>
<string name="ApplicationMigrationService_migrating_system_text_messages">Migrating System Text Messages</string> <string name="ApplicationMigrationService_importing_text_messages">Importing Text Messages</string>
<string name="ApplicationMigrationService_import_complete">Import complete!</string>
<!-- KeyCachingService --> <!-- KeyCachingService -->
<string name="KeyCachingService_textsecure_passphrase_cached">TextSecure Passphrase Cached</string> <string name="KeyCachingService_textsecure_passphrase_cached">TextSecure Passphrase Cached</string>
@ -251,9 +251,9 @@
<string name="auto_initiate_activity__initiate_exchange">Initiate Exchange</string> <string name="auto_initiate_activity__initiate_exchange">Initiate Exchange</string>
<!-- change_passphrase_activity --> <!-- change_passphrase_activity -->
<string name="change_passphrase_activity__old_passphrase">Old passphrase:</string> <string name="change_passphrase_activity__old_passphrase">OLD PASSPHRASE:</string>
<string name="change_passphrase_activity__new_passphrase">New passphrase:</string> <string name="change_passphrase_activity__new_passphrase">NEW PASSPHRASE:</string>
<string name="change_passphrase_activity__repeat_new_passphrase">Repeat new passphrase:</string> <string name="change_passphrase_activity__repeat_new_passphrase">REPEAT NEW PASSPHRASE:</string>
<!-- contact_selection_group_activity --> <!-- contact_selection_group_activity -->
<!-- contact_selection_list_activity --> <!-- contact_selection_list_activity -->
@ -282,9 +282,23 @@
<string name="conversation_fragment_cab__batch_selection_mode">Batch Selection Mode</string> <string name="conversation_fragment_cab__batch_selection_mode">Batch Selection Mode</string>
<!-- create_passphrase_activity --> <!-- create_passphrase_activity -->
<string name="create_passphrase_activity__please_choose_a_passphrase_that_will_be_used_to_locally_encrypt_your_data_this_should_be_a_strong_passphrase">Please choose a passphrase that will be used to locally encrypt your data. This should be a strong passphrase.</string> <string name="create_passphrase_activity__please_choose_a_passphrase_that_will_be_used_to_locally_encrypt_your_data_this_should_be_a_strong_passphrase">Please choose a passphrase that will be used to locally encrypt your data.\n\nThis should be a strong passphrase.</string>
<string name="create_passphrase_activity__passphrase">Passphrase:</string> <string name="create_passphrase_activity__passphrase">PASSPHRASE:</string>
<string name="create_passphrase_activity__repeat">Repeat:</string> <string name="create_passphrase_activity__repeat">REPEAT:</string>
<string name="create_passphrase_activity__continue">Continue</string>
<string name="create_passphrase_activity__generating_secrets">GENERATING SECRETS</string>
<!-- database_migration_activity -->
<string name="database_migration_activity__would_you_like_to_import_your_existing_text_messages">Would you like to import your existing text messages into TextSecure\\\'s encrypted database?</string>
<string name="database_migration_activity__the_default_system_database_will_not_be_modified">The default system database will not be modified or altered in any way.</string>
<string name="database_migration_activity__skip">Skip</string>
<string name="database_migration_activity__import">Import</string>
<string name="database_migration_activity__this_could_take_a_moment_please_be_patient">This could take a moment. Please be patient, we\'ll notify you when the import is complete.</string>
<string name="database_migration_activity__importing">IMPORTING</string>
<!-- prompt_passphrase_activity -->
<string name="prompt_passphrase_activity__textsecure_passphrase">TEXTSECURE PASSPHRASE</string>
<string name="prompt_passphrase_activity__unlock">Unlock</string>
<!-- receive_key_activity --> <!-- receive_key_activity -->
<string name="receive_key_activity__session">Session</string> <string name="receive_key_activity__session">Session</string>

View File

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="NoAnimation.Theme.Sherlock.Light.DarkActionBar" parent="@style/Theme.Sherlock.Light.DarkActionBar">
<item name="android:windowAnimationStyle">@null</item>
</style>
<style name="transparent_progress"> <style name="transparent_progress">
<item name="android:windowFrame">@null</item> <item name="android:windowFrame">@null</item>
<item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowBackground">@android:color/transparent</item>
@ -22,4 +26,48 @@
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
</style> </style>
<style name="Registration.Description" parent="@android:style/TextAppearance">
<item name="android:textSize">16.0sp</item>
<item name="android:typeface">sans</item>
<item name="android:textStyle">normal</item>
<item name="android:textColor">#ff333333</item>
<item name="android:gravity">left</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:shadowColor">#ffffff</item>
<item name="android:shadowDx">1.0</item>
<item name="android:shadowDy">1.0</item>
<item name="android:shadowRadius">0.0</item>
<item name="android:lineSpacingMultiplier">1.25</item>
</style>
<style name="Registration.Label" parent="@android:style/TextAppearance">
<item name="android:textSize">12.0sp</item>
<item name="android:typeface">sans</item>
<item name="android:textStyle">normal</item>
<item name="android:textColor">#ff808080</item>
<item name="android:gravity">left</item>
<item name="android:layout_gravity">left</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:shadowColor">#ffffff</item>
<item name="android:shadowDx">1.0</item>
<item name="android:shadowDy">1.0</item>
<item name="android:shadowRadius">0.0</item>
<item name="android:lineSpacingMultiplier">1.25</item>
</style>
<style name="Registration.BigLabel" parent="@style/Registration.Label">
<item name="android:textSize">20sp</item>
</style>
<style name="Registration.Constant" parent="@android:style/TextAppearance">
<item name="android:typeface">sans</item>
<item name="android:textStyle">normal</item>
<item name="android:textColor">#ff808080</item>
<item name="android:shadowColor">#ffffff</item>
<item name="android:shadowDx">1.0</item>
<item name="android:shadowDy">1.0</item>
<item name="android:shadowRadius">0.0</item>
<item name="android:lineSpacingMultiplier">1.25</item>
</style>
</resources> </resources>

View File

@ -1,121 +0,0 @@
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
public class ApplicationMigrationManager extends Handler {
private ProgressDialog progressDialog;
private ApplicationMigrationListener listener;
private final Context context;
private final MasterSecret masterSecret;
public ApplicationMigrationManager(Context context,
MasterSecret masterSecret)
{
this.masterSecret = masterSecret;
this.context = context;
}
public void setMigrationListener(ApplicationMigrationListener listener) {
this.listener = listener;
}
private void displayMigrationProgress() {
progressDialog = new ProgressDialog(context);
progressDialog.setTitle(context.getString(R.string.ApplicationMigrationManager_migrating_database));
progressDialog.setMessage(context.getString(R.string.ApplicationMigrationManager_migrating_text_message_database));
progressDialog.setMax(10000);
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(false);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.show();
}
public void migrate() {
context.bindService(new Intent(context, ApplicationMigrationService.class),
serviceConnection, Context.BIND_AUTO_CREATE);
}
private void displayMigrationPrompt() {
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(context);
alertBuilder.setTitle(R.string.ApplicationMigrationManager_copy_system_text_message_database_question);
alertBuilder.setMessage(R.string.ApplicationMigrationManager_copy_system_text_message_database_explanation);
alertBuilder.setCancelable(false);
alertBuilder.setPositiveButton(R.string.ApplicationMigrationManager_copy,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
displayMigrationProgress();
Intent intent = new Intent(context, ApplicationMigrationService.class);
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
intent.putExtra("master_secret", masterSecret);
context.startService(intent);
}
});
alertBuilder.setNegativeButton(R.string.ApplicationMigrationManager_dont_copy,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE)
.edit()
.putBoolean("migrated", true).commit();
listener.applicationMigrationComplete();
}
});
alertBuilder.create().show();
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case ApplicationMigrationService.PROGRESS_UPDATE:
if (progressDialog != null) {
progressDialog.setProgress(message.arg1);
progressDialog.setSecondaryProgress(message.arg2);
}
break;
case ApplicationMigrationService.PROGRESS_COMPLETE:
if (progressDialog != null) {
progressDialog.dismiss();
}
if (listener != null) {
listener.applicationMigrationComplete();
}
break;
}
}
public static interface ApplicationMigrationListener {
public void applicationMigrationComplete();
}
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
ApplicationMigrationService applicationMigrationService
= ((ApplicationMigrationService.ApplicationMigrationBinder)service).getService();
if (applicationMigrationService.isMigrating()) displayMigrationProgress();
else displayMigrationPrompt();
applicationMigrationService.setHandler(ApplicationMigrationManager.this);
}
public void onServiceDisconnected(ComponentName name) {}
};
}

View File

@ -35,7 +35,7 @@ import org.thoughtcrime.securesms.contacts.ContactIdentityManager;
import org.thoughtcrime.securesms.crypto.IdentityKey; import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.Trimmer; import org.thoughtcrime.securesms.util.Trimmer;
@ -138,7 +138,12 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home: finish(); return true; case android.R.id.home:
Intent intent = new Intent(this, ConversationListActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
return true;
} }
return false; return false;
@ -313,9 +318,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
private class ChangePassphraseClickListener implements Preference.OnPreferenceClickListener { private class ChangePassphraseClickListener implements Preference.OnPreferenceClickListener {
@Override @Override
public boolean onPreferenceClick(Preference preference) { public boolean onPreferenceClick(Preference preference) {
SharedPreferences settings = getSharedPreferences(KeyCachingService.PREFERENCES_NAME, 0); if (MasterSecretUtil.isPassphraseInitialized(ApplicationPreferencesActivity.this)) {
if (settings.getBoolean("passphrase_initialized", false)) {
startActivity(new Intent(ApplicationPreferencesActivity.this, PassphraseChangeActivity.class)); startActivity(new Intent(ApplicationPreferencesActivity.this, PassphraseChangeActivity.class));
} else { } else {
Toast.makeText(ApplicationPreferencesActivity.this, Toast.makeText(ApplicationPreferencesActivity.this,

View File

@ -236,7 +236,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
case R.id.menu_verify_recipient: handleVerifyRecipient(); return true; case R.id.menu_verify_recipient: handleVerifyRecipient(); return true;
case R.id.menu_verify_session: handleVerifySession(); return true; case R.id.menu_verify_session: handleVerifySession(); return true;
case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true; case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true;
case android.R.id.home: finish(); return true; case android.R.id.home: handleReturnToConversationList(); return true;
} }
return false; return false;
@ -261,6 +261,14 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
//////// Event Handlers //////// 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() { private void handleVerifyRecipient() {
Intent verifyIdentityIntent = new Intent(this, VerifyIdentityActivity.class); Intent verifyIdentityIntent = new Intent(this, VerifyIdentityActivity.class);
verifyIdentityIntent.putExtra("recipient", getRecipients().getPrimaryRecipient()); verifyIdentityIntent.putExtra("recipient", getRecipients().getPrimaryRecipient());

View File

@ -1,25 +1,16 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.util.Log; import android.util.Log;
import android.view.WindowManager; import android.view.WindowManager;
import org.thoughtcrime.securesms.ApplicationExportManager.ApplicationExportListener; import org.thoughtcrime.securesms.ApplicationExportManager.ApplicationExportListener;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.service.SendReceiveService;
@ -36,11 +27,6 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
private ConversationListFragment fragment; private ConversationListFragment fragment;
private MasterSecret masterSecret; private MasterSecret masterSecret;
private ApplicationMigrationManager migrationManager;
private boolean havePromptedForPassphrase = false;
private boolean isVisible = false;
@Override @Override
public void onCreate(Bundle icicle) { public void onCreate(Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
@ -52,32 +38,6 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
initializeContactUpdatesReceiver(); initializeContactUpdatesReceiver();
} }
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
this.setIntent(intent);
}
@Override
public void onResume() {
super.onResume();
isVisible = true;
}
@Override
public void onPause() {
super.onPause();
isVisible = false;
}
@Override
public void onStop() {
super.onStop();
havePromptedForPassphrase = false;
}
@Override @Override
public void onDestroy() { public void onDestroy() {
Log.w("ConversationListActivity", "onDestroy..."); Log.w("ConversationListActivity", "onDestroy...");
@ -87,47 +47,17 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
@Override @Override
public void onMasterSecretCleared() { public void onMasterSecretCleared() {
this.masterSecret = null; // this.fragment.setMasterSecret(null);
this.fragment.setMasterSecret(null); startActivity(new Intent(this, RoutingActivity.class));
this.invalidateOptionsMenu(); super.onMasterSecretCleared();
if (!havePromptedForPassphrase && isVisible) {
promptForPassphrase();
}
} }
@Override
public void onNewMasterSecret(MasterSecret masterSecret) {
this.masterSecret = masterSecret;
if (masterSecret != null) {
if (!IdentityKeyUtil.hasIdentityKey(this)) {
new Thread(new IdentityKeyInitializer()).start();
}
if (!MasterSecretUtil.hasAsymmericMasterSecret(this)) {
new Thread(new AsymmetricMasteSecretInitializer()).start();
}
if (!isDatabaseMigrated()) initializeDatabaseMigration();
else DecryptingQueue.schedulePendingDecrypts(this, masterSecret);
}
this.fragment.setMasterSecret(masterSecret);
this.invalidateOptionsMenu();
this.havePromptedForPassphrase = false;
createConversationIfNecessary(this.getIntent());
}
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
Log.w("ConversationListActivity", "onPrepareOptionsMenu...");
MenuInflater inflater = this.getSupportMenuInflater(); MenuInflater inflater = this.getSupportMenuInflater();
menu.clear(); menu.clear();
if (this.masterSecret == null) inflater.inflate(R.menu.text_secure_locked, menu); inflater.inflate(R.menu.text_secure_normal, menu);
else inflater.inflate(R.menu.text_secure_normal, menu);
super.onPrepareOptionsMenu(menu); super.onPrepareOptionsMenu(menu);
return true; return true;
@ -138,12 +68,11 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
super.onOptionsItemSelected(item); super.onOptionsItemSelected(item);
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_new_message: createConversation(-1, null, null, null, null); return true; case R.id.menu_new_message: createConversation(-1, null); return true;
case R.id.menu_unlock: promptForPassphrase(); return true; case R.id.menu_settings: handleDisplaySettings(); return true;
case R.id.menu_settings: handleDisplaySettings(); return true; case R.id.menu_export: handleExportDatabase(); return true;
case R.id.menu_export: handleExportDatabase(); return true; case R.id.menu_import: handleImportDatabase(); return true;
case R.id.menu_import: handleImportDatabase(); return true; case R.id.menu_clear_passphrase: handleClearPassphrase(); return true;
case R.id.menu_clear_passphrase: handleClearPassphrase(); return true;
} }
return false; return false;
@ -151,39 +80,18 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
@Override @Override
public void onCreateConversation(long threadId, Recipients recipients) { public void onCreateConversation(long threadId, Recipients recipients) {
createConversation(threadId, recipients, null, null, null); createConversation(threadId, recipients);
} }
private void createConversation(long threadId, Recipients recipients, private void createConversation(long threadId, Recipients recipients) {
String text, Uri imageUri, Uri audioUri)
{
if (this.masterSecret == null) {
promptForPassphrase();
return;
}
Intent intent = new Intent(this, ConversationActivity.class); Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients); intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.MASTER_SECRET_EXTRA, masterSecret); intent.putExtra(ConversationActivity.MASTER_SECRET_EXTRA, masterSecret);
intent.putExtra(ConversationActivity.DRAFT_TEXT_EXTRA, text);
intent.putExtra(ConversationActivity.DRAFT_IMAGE_EXTRA, imageUri);
intent.putExtra(ConversationActivity.DRAFT_AUDIO_EXTRA, audioUri);
startActivity(intent); startActivity(intent);
} }
private void promptForPassphrase() {
havePromptedForPassphrase = true;
if (hasSelectedPassphrase()) startActivity(new Intent(this, PassphrasePromptActivity.class));
else startActivity(new Intent(this, PassphraseCreateActivity.class));
}
private boolean hasSelectedPassphrase() {
SharedPreferences settings = getSharedPreferences(KeyCachingService.PREFERENCES_NAME, 0);
return settings.getBoolean("passphrase_initialized", false);
}
private void handleDisplaySettings() { private void handleDisplaySettings() {
Intent preferencesIntent = new Intent(this, ApplicationPreferencesActivity.class); Intent preferencesIntent = new Intent(this, ApplicationPreferencesActivity.class);
preferencesIntent.putExtra("master_secret", masterSecret); preferencesIntent.putExtra("master_secret", masterSecret);
@ -237,89 +145,17 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
startService(mmsSenderIntent); startService(mmsSenderIntent);
} }
private void initializeDatabaseMigration() {
if (migrationManager == null) {
migrationManager = new ApplicationMigrationManager(this, masterSecret);
ApplicationMigrationManager.ApplicationMigrationListener listener =
new ApplicationMigrationManager.ApplicationMigrationListener() {
@Override
public void applicationMigrationComplete() {
if (masterSecret != null)
DecryptingQueue.schedulePendingDecrypts(ConversationListActivity.this,
masterSecret);
}
};
migrationManager.setMigrationListener(listener);
migrationManager.migrate();
}
}
private void initializeResources() { private void initializeResources() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE); WindowManager.LayoutParams.FLAG_SECURE);
} }
this.masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret");
this.fragment = (ConversationListFragment)this.getSupportFragmentManager() this.fragment = (ConversationListFragment)this.getSupportFragmentManager()
.findFragmentById(R.id.fragment_content); .findFragmentById(R.id.fragment_content);
}
private boolean isDatabaseMigrated() { this.fragment.setMasterSecret(masterSecret);
return this.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE)
.getBoolean("migrated", false);
}
private void createConversationIfNecessary(Intent intent) {
long thread = intent.getLongExtra("thread_id", -1L);
String type = intent.getType();
Recipients recipients = null;
String draftText = null;
Uri draftImage = null;
Uri draftAudio = null;
if (Intent.ACTION_SENDTO.equals(intent.getAction())) {
try {
recipients = RecipientFactory.getRecipientsFromString(this, intent.getData().getSchemeSpecificPart(), false);
thread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipients);
} catch (RecipientFormattingException rfe) {
recipients = null;
}
} else if (Intent.ACTION_SEND.equals(intent.getAction())) {
if ("text/plain".equals(type)) {
draftText = intent.getStringExtra(Intent.EXTRA_TEXT);
} else if (type.startsWith("image/")) {
draftImage = intent.getParcelableExtra(Intent.EXTRA_STREAM);
} else if (type.startsWith("audio/")) {
draftAudio = intent.getParcelableExtra(Intent.EXTRA_STREAM);
}
} else {
recipients = intent.getParcelableExtra("recipients");
}
if (recipients != null || Intent.ACTION_SEND.equals(intent.getAction())) {
createConversation(thread, recipients, draftText, draftImage, draftAudio);
intent.putExtra("thread_id", -1L);
intent.putExtra("recipients", (Parcelable)null);
intent.putExtra(Intent.EXTRA_TEXT, (String)null);
intent.putExtra(Intent.EXTRA_STREAM, (Parcelable)null);
intent.setAction(null);
}
}
private class IdentityKeyInitializer implements Runnable {
@Override
public void run() {
IdentityKeyUtil.generateIdentityKeys(ConversationListActivity.this, masterSecret);
}
}
private class AsymmetricMasteSecretInitializer implements Runnable {
@Override
public void run() {
MasterSecretUtil.generateAsymmetricMasterSecret(ConversationListActivity.this, masterSecret);
}
} }
} }

View File

@ -0,0 +1,189 @@
package org.thoughtcrime.securesms;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
import org.thoughtcrime.securesms.service.ApplicationMigrationService.ImportState;
public class DatabaseMigrationActivity extends PassphraseRequiredSherlockActivity {
private final ImportServiceConnection serviceConnection = new ImportServiceConnection();
private final ImportStateHandler importStateHandler = new ImportStateHandler();
private final BroadcastReceiver completedReceiver = new NullReceiver();
private LinearLayout promptLayout;
private LinearLayout progressLayout;
private Button skipButton;
private Button importButton;
private ProgressBar progress;
private TextView progressLabel;
private ApplicationMigrationService importService;
private boolean isVisible = false;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.database_migration_activity);
initializeResources();
initializeServiceBinding();
}
@Override
public void onResume() {
super.onResume();
isVisible = true;
registerForCompletedNotification();
}
@Override
public void onPause() {
super.onPause();
isVisible = false;
unregisterForCompletedNotification();
}
@Override
public void onDestroy() {
super.onDestroy();
shutdownServiceBinding();
}
private void initializeServiceBinding() {
Intent intent = new Intent(this, ApplicationMigrationService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
private void initializeResources() {
this.promptLayout = (LinearLayout)findViewById(R.id.prompt_layout);
this.progressLayout = (LinearLayout)findViewById(R.id.progress_layout);
this.skipButton = (Button) findViewById(R.id.skip_button);
this.importButton = (Button) findViewById(R.id.import_button);
this.progress = (ProgressBar) findViewById(R.id.import_progress);
this.progressLabel = (TextView) findViewById(R.id.import_status);
this.progressLayout.setVisibility(View.GONE);
this.promptLayout.setVisibility(View.GONE);
this.importButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(DatabaseMigrationActivity.this, ApplicationMigrationService.class);
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
intent.putExtra("master_secret", getIntent().getParcelableExtra("master_secret"));
startService(intent);
promptLayout.setVisibility(View.GONE);
progressLayout.setVisibility(View.VISIBLE);
}
});
this.skipButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ApplicationMigrationService.setDatabaseImported(DatabaseMigrationActivity.this);
handleImportComplete();
}
});
}
private void registerForCompletedNotification() {
IntentFilter filter = new IntentFilter();
filter.addAction(ApplicationMigrationService.COMPLETED_ACTION);
filter.setPriority(1000);
registerReceiver(completedReceiver, filter);
}
private void unregisterForCompletedNotification() {
unregisterReceiver(completedReceiver);
}
private void shutdownServiceBinding() {
unbindService(serviceConnection);
}
private void handleStateIdle() {
this.promptLayout.setVisibility(View.VISIBLE);
this.progressLayout.setVisibility(View.GONE);
}
private void handleStateProgress(ProgressDescription update) {
this.promptLayout.setVisibility(View.GONE);
this.progressLayout.setVisibility(View.VISIBLE);
this.progressLabel.setText(update.primaryComplete + "/" + update.primaryTotal);
double max = this.progress.getMax();
double primaryTotal = update.primaryTotal;
double primaryComplete = update.primaryComplete;
double secondaryTotal = update.secondaryTotal;
double secondaryComplete = update.secondaryComplete;
this.progress.setProgress((int)Math.round((primaryComplete / primaryTotal) * max));
this.progress.setSecondaryProgress((int)Math.round((secondaryComplete / secondaryTotal) * max));
}
private void handleImportComplete() {
if (isVisible) {
if (getIntent().hasExtra("next_intent")) {
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
} else {
startActivity(new Intent(this, ConversationListActivity.class));
}
}
finish();
}
private class ImportStateHandler extends Handler {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case ImportState.STATE_IDLE: handleStateIdle(); break;
case ImportState.STATE_MIGRATING_IN_PROGRESS: handleStateProgress((ProgressDescription)message.obj); break;
case ImportState.STATE_MIGRATING_COMPLETE: handleImportComplete(); break;
}
}
}
private class ImportServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
importService = ((ApplicationMigrationService.ApplicationMigrationBinder)service).getService();
importService.setImportStateHandler(importStateHandler);
ImportState state = importService.getState();
importStateHandler.obtainMessage(state.state, state.progress).sendToTarget();
}
@Override
public void onServiceDisconnected(ComponentName name) {
importService.setImportStateHandler(null);
}
}
private class NullReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
abortBroadcast();
}
}
}

View File

@ -22,12 +22,12 @@ import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.os.IBinder; import android.os.IBinder;
import com.actionbarsherlock.app.SherlockActivity;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.MemoryCleaner;
import com.actionbarsherlock.app.SherlockActivity;
/** /**
* Base Activity for changing/prompting local encryption passphrase. * Base Activity for changing/prompting local encryption passphrase.
* *
@ -44,9 +44,14 @@ public abstract class PassphraseActivity extends SherlockActivity {
bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE); bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE);
} }
protected MasterSecret getMasterSecret() {
return masterSecret;
}
protected abstract void cleanup(); protected abstract void cleanup();
private ServiceConnection serviceConnection = new ServiceConnection() { private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) { public void onServiceConnected(ComponentName className, IBinder service) {
keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService(); keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService();
keyCachingService.setMasterSecret(masterSecret); keyCachingService.setMasterSecret(masterSecret);
@ -55,9 +60,12 @@ public abstract class PassphraseActivity extends SherlockActivity {
MemoryCleaner.clean(masterSecret); MemoryCleaner.clean(masterSecret);
cleanup(); cleanup();
PassphraseActivity.this.setResult(RESULT_OK);
PassphraseActivity.this.finish(); PassphraseActivity.this.finish();
} }
@Override
public void onServiceDisconnected(ComponentName name) { public void onServiceDisconnected(ComponentName name) {
keyCachingService = null; keyCachingService = null;
} }

View File

@ -16,18 +16,19 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.app.ProgressDialog;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast; import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.MemoryCleaner;
import org.thoughtcrime.securesms.util.Util;
/** /**
* Activity for creating a user's local encryption passphrase. * Activity for creating a user's local encryption passphrase.
@ -37,10 +38,12 @@ import org.thoughtcrime.securesms.util.MemoryCleaner;
public class PassphraseCreateActivity extends PassphraseActivity { public class PassphraseCreateActivity extends PassphraseActivity {
private LinearLayout createLayout;
private LinearLayout progressLayout;
private EditText passphraseEdit; private EditText passphraseEdit;
private EditText passphraseRepeatEdit; private EditText passphraseRepeatEdit;
private Button okButton; private Button okButton;
private Button cancelButton;
public PassphraseCreateActivity() { } public PassphraseCreateActivity() { }
@ -54,10 +57,11 @@ public class PassphraseCreateActivity extends PassphraseActivity {
} }
private void initializeResources() { private void initializeResources() {
this.passphraseEdit = (EditText) findViewById(R.id.passphrase_edit); this.createLayout = (LinearLayout)findViewById(R.id.create_layout);
this.passphraseRepeatEdit = (EditText) findViewById(R.id.passphrase_edit_repeat); this.progressLayout = (LinearLayout)findViewById(R.id.progress_layout);
this.okButton = (Button) findViewById(R.id.ok_button); this.passphraseEdit = (EditText) findViewById(R.id.passphrase_edit);
this.cancelButton = (Button) findViewById(R.id.cancel_button); this.passphraseRepeatEdit = (EditText) findViewById(R.id.passphrase_edit_repeat);
this.okButton = (Button) findViewById(R.id.ok_button);
this.okButton.setOnClickListener(new View.OnClickListener() { this.okButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
@ -65,45 +69,36 @@ public class PassphraseCreateActivity extends PassphraseActivity {
verifyAndSavePassphrases(); verifyAndSavePassphrases();
} }
}); });
this.cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
} }
private void verifyAndSavePassphrases() { private void verifyAndSavePassphrases() {
if (Util.isEmpty(this.passphraseEdit) || Util.isEmpty(this.passphraseRepeatEdit)) {
Toast.makeText(this, R.string.PassphraseCreateActivity_you_must_specify_a_password, Toast.LENGTH_SHORT).show();
return;
}
String passphrase = this.passphraseEdit.getText().toString(); String passphrase = this.passphraseEdit.getText().toString();
String passphraseRepeat = this.passphraseRepeatEdit.getText().toString(); String passphraseRepeat = this.passphraseRepeatEdit.getText().toString();
if (!passphrase.equals(passphraseRepeat)) { if (!passphrase.equals(passphraseRepeat)) {
Toast.makeText(getApplicationContext(), Toast.makeText(this, R.string.PassphraseCreateActivity_passphrases_dont_match, Toast.LENGTH_SHORT).show();
R.string.PassphraseCreateActivity_passphrases_dont_match_exclamation,
Toast.LENGTH_SHORT).show();
this.passphraseEdit.setText(""); this.passphraseEdit.setText("");
this.passphraseRepeatEdit.setText(""); this.passphraseRepeatEdit.setText("");
} else { return;
// We do this, but the edit boxes are basically impossible to clean up.
MemoryCleaner.clean(passphraseRepeat);
new SecretGenerator().execute(passphrase);
} }
// We do this, but the edit boxes are basically impossible to clean up.
MemoryCleaner.clean(passphraseRepeat);
new SecretGenerator().execute(passphrase);
} }
private class SecretGenerator extends AsyncTask<String, Void, Void> { private class SecretGenerator extends AsyncTask<String, Void, Void> {
private ProgressDialog progressDialog;
private MasterSecret masterSecret; private MasterSecret masterSecret;
@Override @Override
protected void onPreExecute() { protected void onPreExecute() {
progressDialog = new ProgressDialog(PassphraseCreateActivity.this); createLayout.setVisibility(View.GONE);
progressDialog.setTitle(R.string.PassphraseCreateActivity_generating_keypair); progressLayout.setVisibility(View.VISIBLE);
progressDialog.setMessage(getString(R.string.PassphraseCreateActivity_generating_a_local_encryption_keypair));
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.show();
} }
@Override @Override
@ -123,7 +118,6 @@ public class PassphraseCreateActivity extends PassphraseActivity {
@Override @Override
protected void onPostExecute(Void param) { protected void onPostExecute(Void param) {
progressDialog.dismiss();
setMasterSecret(masterSecret); setMasterSecret(masterSecret);
} }
} }

View File

@ -38,7 +38,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private EditText passphraseText; private EditText passphraseText;
private Button okButton; private Button okButton;
private Button cancelButton;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -51,13 +50,12 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private void initializeResources() { private void initializeResources() {
passphraseText = (EditText)findViewById(R.id.passphrase_edit); passphraseText = (EditText)findViewById(R.id.passphrase_edit);
okButton = (Button)findViewById(R.id.ok_button); okButton = (Button)findViewById(R.id.ok_button);
cancelButton = (Button)findViewById(R.id.cancel_button);
okButton.setOnClickListener(new OkButtonClickListener()); okButton.setOnClickListener(new OkButtonClickListener());
cancelButton.setOnClickListener(new CancelButtonClickListener());
} }
private class OkButtonClickListener implements OnClickListener { private class OkButtonClickListener implements OnClickListener {
@Override
public void onClick(View v) { public void onClick(View v) {
try { try {
Editable text = passphraseText.getText(); Editable text = passphraseText.getText();
@ -74,16 +72,9 @@ public class PassphrasePromptActivity extends PassphraseActivity {
} }
} }
private class CancelButtonClickListener implements OnClickListener {
public void onClick(View v) {
finish();
}
}
@Override @Override
protected void cleanup() { protected void cleanup() {
this.passphraseText = null; this.passphraseText = null;
System.gc(); System.gc();
} }
} }

View File

@ -0,0 +1,218 @@
package org.thoughtcrime.securesms;
import android.content.Intent;
import android.net.Uri;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
public class RoutingActivity extends PassphraseRequiredSherlockActivity {
private static final int STATE_CREATE_PASSPHRASE = 1;
private static final int STATE_PROMPT_PASSPHRASE = 2;
private static final int STATE_IMPORT_DATABASE = 3;
private static final int STATE_CONVERSATION_OR_LIST = 4;
private MasterSecret masterSecret = null;
private boolean isVisible = false;
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
}
@Override
public void onResume() {
this.isVisible = true;
super.onResume();
}
@Override
public void onPause() {
this.isVisible = false;
super.onPause();
}
@Override
public void onNewMasterSecret(MasterSecret masterSecret) {
this.masterSecret = masterSecret;
if (isVisible) {
routeApplicationState();
}
}
@Override
public void onMasterSecretCleared() {
this.masterSecret = null;
if (isVisible) {
routeApplicationState();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_CANCELED)
finish();
}
private void routeApplicationState() {
int state = getApplicationState();
switch (state) {
case STATE_CREATE_PASSPHRASE: handleCreatePassphrase(); break;
case STATE_PROMPT_PASSPHRASE: handlePromptPassphrase(); break;
case STATE_IMPORT_DATABASE: handleImportDatabase(); break;
case STATE_CONVERSATION_OR_LIST: handleDisplayConversationOrList(); break;
}
}
private void handleCreatePassphrase() {
Intent intent = new Intent(this, PassphraseCreateActivity.class);
startActivityForResult(intent, 1);
}
private void handlePromptPassphrase() {
Intent intent = new Intent(this, PassphrasePromptActivity.class);
startActivityForResult(intent, 2);
}
private void handleImportDatabase() {
Intent intent = new Intent(this, DatabaseMigrationActivity.class);
intent.putExtra("master_secret", masterSecret);
intent.putExtra("next_intent", getConversationListIntent());
startActivity(intent);
finish();
}
private void handleDisplayConversationOrList() {
ConversationParameters parameters = getConversationParameters();
Intent intent;
if (isShareAction() || parameters.recipients != null) {
intent = getConversationIntent(parameters);
} else {
intent = getConversationListIntent();
}
startActivity(intent);
finish();
}
private Intent getConversationIntent(ConversationParameters parameters) {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, parameters.recipients);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, parameters.thread);
intent.putExtra(ConversationActivity.MASTER_SECRET_EXTRA, masterSecret);
intent.putExtra(ConversationActivity.DRAFT_TEXT_EXTRA, parameters.draftText);
intent.putExtra(ConversationActivity.DRAFT_IMAGE_EXTRA, parameters.draftImage);
intent.putExtra(ConversationActivity.DRAFT_AUDIO_EXTRA, parameters.draftAudio);
return intent;
}
private Intent getConversationListIntent() {
Intent intent = new Intent(this, ConversationListActivity.class);
intent.putExtra("master_secret", masterSecret);
return intent;
}
private int getApplicationState() {
if (!MasterSecretUtil.isPassphraseInitialized(this))
return STATE_CREATE_PASSPHRASE;
if (masterSecret == null)
return STATE_PROMPT_PASSPHRASE;
if (!ApplicationMigrationService.isDatabaseImported(this))
return STATE_IMPORT_DATABASE;
return STATE_CONVERSATION_OR_LIST;
}
private ConversationParameters getConversationParameters() {
if (isSendAction()) {
return getConversationParametersForSendAction();
} else if (isShareAction()) {
return getConversationParametersForShareAction();
} else {
return getConversationParametersForInternalAction();
}
}
private ConversationParameters getConversationParametersForSendAction() {
Recipients recipients = null;
long threadId = getIntent().getLongExtra("thread_id", -1);
try {
String data = getIntent().getData().getSchemeSpecificPart();
recipients = RecipientFactory.getRecipientsFromString(this, data, false);
threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipients);
} catch (RecipientFormattingException rfe) {
recipients = null;
}
return new ConversationParameters(threadId, recipients, null, null, null);
}
private ConversationParameters getConversationParametersForShareAction() {
String type = getIntent().getType();
String draftText = null;
Uri draftImage = null;
Uri draftAudio = null;
if ("text/plain".equals(type)) {
draftText = getIntent().getStringExtra(Intent.EXTRA_TEXT);
} else if (type.startsWith("image/")) {
draftImage = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
} else if (type.startsWith("audio/")) {
draftAudio = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
}
return new ConversationParameters(-1, null, draftText, draftImage, draftAudio);
}
private ConversationParameters getConversationParametersForInternalAction() {
long threadId = getIntent().getLongExtra("thread_id", -1);
Recipients recipients = getIntent().getParcelableExtra("recipients");
return new ConversationParameters(threadId, recipients, null, null, null);
}
private boolean isShareAction() {
return Intent.ACTION_SEND.equals(getIntent().getAction());
}
private boolean isSendAction() {
return Intent.ACTION_SENDTO.equals(getIntent().getAction());
}
private static class ConversationParameters {
public final long thread;
public final Recipients recipients;
public final String draftText;
public final Uri draftImage;
public final Uri draftAudio;
public ConversationParameters(long thread, Recipients recipients,
String draftText, Uri draftImage, Uri draftAudio)
{
this.thread = thread;
this.recipients = recipients;
this.draftText = draftText;
this.draftImage = draftImage;
this.draftAudio = draftAudio;
}
}
}

View File

@ -1,6 +1,6 @@
/** /**
* Copyright (C) 2011 Whisper Systems * Copyright (C) 2011 Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@ -10,12 +10,21 @@
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.thoughtcrime.securesms.util.Base64;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -31,38 +40,29 @@ import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec; import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.thoughtcrime.securesms.util.Base64;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;
/** /**
* Helper class for generating and securely storing a MasterSecret. * Helper class for generating and securely storing a MasterSecret.
* *
* @author Moxie Marlinspike * @author Moxie Marlinspike
*/ */
public class MasterSecretUtil { public class MasterSecretUtil {
public static final String PREFERENCES_NAME = "SecureSMS-Preferences"; public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
public static final String ASYMMETRIC_LOCAL_PUBLIC = "asymmetric_master_secret_public"; public static final String ASYMMETRIC_LOCAL_PUBLIC = "asymmetric_master_secret_public";
public static MasterSecret changeMasterSecretPassphrase(Context context, String originalPassphrase, String newPassphrase) throws InvalidPassphraseException { public static MasterSecret changeMasterSecretPassphrase(Context context, String originalPassphrase, String newPassphrase) throws InvalidPassphraseException {
try { try {
MasterSecret masterSecret = getMasterSecret(context, originalPassphrase); MasterSecret masterSecret = getMasterSecret(context, originalPassphrase);
byte[] combinedSecrets = combineSecrets(masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded()); byte[] combinedSecrets = combineSecrets(masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded());
encryptWithPassphraseAndSave(context, combinedSecrets, newPassphrase); encryptWithPassphraseAndSave(context, combinedSecrets, newPassphrase);
return masterSecret; return masterSecret;
} catch (GeneralSecurityException gse) { } catch (GeneralSecurityException gse) {
throw new AssertionError(gse); throw new AssertionError(gse);
} }
} }
public static MasterSecret getMasterSecret(Context context, String passphrase) throws InvalidPassphraseException { public static MasterSecret getMasterSecret(Context context, String passphrase) throws InvalidPassphraseException {
try { try {
byte[] encryptedAndMacdMasterSecret = retrieve(context, "master_secret"); byte[] encryptedAndMacdMasterSecret = retrieve(context, "master_secret");
@ -70,7 +70,7 @@ public class MasterSecretUtil {
byte[] combinedSecrets = decryptWithPassphrase(context, encryptedMasterSecret, passphrase); byte[] combinedSecrets = decryptWithPassphrase(context, encryptedMasterSecret, passphrase);
byte[] encryptionSecret = getEncryptionSecret(combinedSecrets); byte[] encryptionSecret = getEncryptionSecret(combinedSecrets);
byte[] macSecret = getMacSecret(combinedSecrets); byte[] macSecret = getMacSecret(combinedSecrets);
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"), return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
new SecretKeySpec(macSecret, "HmacSHA1")); new SecretKeySpec(macSecret, "HmacSHA1"));
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
@ -80,18 +80,18 @@ public class MasterSecretUtil {
Log.w("keyutil", e); Log.w("keyutil", e);
return null; //XXX return null; //XXX
} }
} }
public static AsymmetricMasterSecret getAsymmetricMasterSecret(Context context, MasterSecret masterSecret) { public static AsymmetricMasterSecret getAsymmetricMasterSecret(Context context, MasterSecret masterSecret) {
try { try {
PublicKey publicKey = new PublicKey(retrieve(context, ASYMMETRIC_LOCAL_PUBLIC)); PublicKey publicKey = new PublicKey(retrieve(context, ASYMMETRIC_LOCAL_PUBLIC));
ECPrivateKeyParameters privateKey = null; ECPrivateKeyParameters privateKey = null;
if (masterSecret != null) { if (masterSecret != null) {
MasterCipher masterCipher = new MasterCipher(masterSecret); MasterCipher masterCipher = new MasterCipher(masterSecret);
privateKey = masterCipher.decryptKey(retrieve(context, "asymmetric_master_secret_private")); privateKey = masterCipher.decryptKey(retrieve(context, "asymmetric_master_secret_private"));
} }
return new AsymmetricMasterSecret(publicKey, privateKey); return new AsymmetricMasterSecret(publicKey, privateKey);
} catch (InvalidKeyException ike) { } catch (InvalidKeyException ike) {
throw new AssertionError(ike); throw new AssertionError(ike);
@ -99,93 +99,98 @@ public class MasterSecretUtil {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
public static AsymmetricMasterSecret generateAsymmetricMasterSecret(Context context, MasterSecret masterSecret) { public static AsymmetricMasterSecret generateAsymmetricMasterSecret(Context context, MasterSecret masterSecret) {
MasterCipher masterCipher = new MasterCipher(masterSecret); MasterCipher masterCipher = new MasterCipher(masterSecret);
AsymmetricCipherKeyPair ackp = KeyUtil.generateKeyPair(); AsymmetricCipherKeyPair ackp = KeyUtil.generateKeyPair();
KeyPair keyPair = new KeyPair(31337, ackp, masterSecret); KeyPair keyPair = new KeyPair(31337, ackp, masterSecret);
PublicKey publicKey = keyPair.getPublicKey(); PublicKey publicKey = keyPair.getPublicKey();
ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters)ackp.getPrivate(); ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters)ackp.getPrivate();
save(context, ASYMMETRIC_LOCAL_PUBLIC, publicKey.serialize()); save(context, ASYMMETRIC_LOCAL_PUBLIC, publicKey.serialize());
save(context, "asymmetric_master_secret_private", masterCipher.encryptKey(privateKey)); save(context, "asymmetric_master_secret_private", masterCipher.encryptKey(privateKey));
return new AsymmetricMasterSecret(publicKey, privateKey); return new AsymmetricMasterSecret(publicKey, privateKey);
} }
public static MasterSecret generateMasterSecret(Context context, String passphrase) { public static MasterSecret generateMasterSecret(Context context, String passphrase) {
try { try {
byte[] encryptionSecret = generateEncryptionSecret(); byte[] encryptionSecret = generateEncryptionSecret();
byte[] macSecret = generateMacSecret(); byte[] macSecret = generateMacSecret();
byte[] masterSecret = combineSecrets(encryptionSecret, macSecret); byte[] masterSecret = combineSecrets(encryptionSecret, macSecret);
encryptWithPassphraseAndSave(context, masterSecret, passphrase); encryptWithPassphraseAndSave(context, masterSecret, passphrase);
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"), return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
new SecretKeySpec(macSecret, "HmacSHA1")); new SecretKeySpec(macSecret, "HmacSHA1"));
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
Log.w("keyutil", e); Log.w("keyutil", e);
return null; return null;
} }
} }
public static boolean hasAsymmericMasterSecret(Context context) { public static boolean hasAsymmericMasterSecret(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0); SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
return settings.contains(ASYMMETRIC_LOCAL_PUBLIC); return settings.contains(ASYMMETRIC_LOCAL_PUBLIC);
} }
public static boolean isPassphraseInitialized(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, 0);
return preferences.getBoolean("passphrase_initialized", false);
}
private static void encryptWithPassphraseAndSave(Context context, byte[] masterSecret, String passphrase) throws GeneralSecurityException { private static void encryptWithPassphraseAndSave(Context context, byte[] masterSecret, String passphrase) throws GeneralSecurityException {
byte[] encryptedMasterSecret = encryptWithPassphrase(context, masterSecret, passphrase); byte[] encryptedMasterSecret = encryptWithPassphrase(context, masterSecret, passphrase);
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(context, encryptedMasterSecret, passphrase); byte[] encryptedAndMacdMasterSecret = macWithPassphrase(context, encryptedMasterSecret, passphrase);
save(context, "master_secret", encryptedAndMacdMasterSecret); save(context, "master_secret", encryptedAndMacdMasterSecret);
save(context, "passphrase_initialized", true); save(context, "passphrase_initialized", true);
} }
private static byte[] getEncryptionSecret(byte[] combinedSecrets) { private static byte[] getEncryptionSecret(byte[] combinedSecrets) {
byte[] encryptionSecret = new byte[16]; byte[] encryptionSecret = new byte[16];
System.arraycopy(combinedSecrets, 0, encryptionSecret, 0, encryptionSecret.length); System.arraycopy(combinedSecrets, 0, encryptionSecret, 0, encryptionSecret.length);
return encryptionSecret; return encryptionSecret;
} }
private static byte[] getMacSecret(byte[] combinedSecrets) { private static byte[] getMacSecret(byte[] combinedSecrets) {
byte[] macSecret = new byte[20]; byte[] macSecret = new byte[20];
System.arraycopy(combinedSecrets, 16, macSecret, 0, macSecret.length); System.arraycopy(combinedSecrets, 16, macSecret, 0, macSecret.length);
return macSecret; return macSecret;
} }
private static byte[] combineSecrets(byte[] encryptionSecret, byte[] macSecret) { private static byte[] combineSecrets(byte[] encryptionSecret, byte[] macSecret) {
byte[] combinedSecret = new byte[encryptionSecret.length + macSecret.length]; byte[] combinedSecret = new byte[encryptionSecret.length + macSecret.length];
System.arraycopy(encryptionSecret, 0, combinedSecret, 0, encryptionSecret.length); System.arraycopy(encryptionSecret, 0, combinedSecret, 0, encryptionSecret.length);
System.arraycopy(macSecret, 0, combinedSecret, encryptionSecret.length, macSecret.length); System.arraycopy(macSecret, 0, combinedSecret, encryptionSecret.length, macSecret.length);
return combinedSecret; return combinedSecret;
} }
private static void save(Context context, String key, byte[] value) { private static void save(Context context, String key, byte[] value) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0); SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
Editor editor = settings.edit(); Editor editor = settings.edit();
editor.putString(key, Base64.encodeBytes(value)); editor.putString(key, Base64.encodeBytes(value));
editor.commit(); editor.commit();
} }
private static void save(Context context, String key, boolean value) { private static void save(Context context, String key, boolean value) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0); SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
Editor editor = settings.edit(); Editor editor = settings.edit();
editor.putBoolean(key, value); editor.putBoolean(key, value);
editor.commit(); editor.commit();
} }
private static byte[] retrieve(Context context, String key) throws IOException { private static byte[] retrieve(Context context, String key) throws IOException {
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0); SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
String encodedValue = settings.getString(key, ""); String encodedValue = settings.getString(key, "");
if (encodedValue == "") return null; if (encodedValue == "") return null;
else return Base64.decode(encodedValue); else return Base64.decode(encodedValue);
} }
private static byte[] generateEncryptionSecret() { private static byte[] generateEncryptionSecret() {
try { try {
KeyGenerator generator = KeyGenerator.getInstance("AES"); KeyGenerator generator = KeyGenerator.getInstance("AES");
@ -196,9 +201,9 @@ public class MasterSecretUtil {
} catch (NoSuchAlgorithmException ex) { } catch (NoSuchAlgorithmException ex) {
Log.w("keyutil", ex); Log.w("keyutil", ex);
return null; return null;
} }
} }
private static byte[] generateMacSecret() { private static byte[] generateMacSecret() {
try { try {
KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1"); KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1");
@ -208,42 +213,42 @@ public class MasterSecretUtil {
return null; return null;
} }
} }
private static byte[] generateSalt() throws NoSuchAlgorithmException { private static byte[] generateSalt() throws NoSuchAlgorithmException {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
byte[] salt = new byte[8]; byte[] salt = new byte[8];
random.nextBytes(salt); random.nextBytes(salt);
return salt; return salt;
} }
private static SecretKey getKeyFromPassphrase(String passphrase, byte[] salt) throws GeneralSecurityException { private static SecretKey getKeyFromPassphrase(String passphrase, byte[] salt) throws GeneralSecurityException {
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, 100); PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, 100);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC"); SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
return skf.generateSecret(keyspec); return skf.generateSecret(keyspec);
} }
private static Cipher getCipherFromPassphrase(String passphrase, byte[] salt, int opMode) throws GeneralSecurityException { private static Cipher getCipherFromPassphrase(String passphrase, byte[] salt, int opMode) throws GeneralSecurityException {
SecretKey key = getKeyFromPassphrase(passphrase, salt); SecretKey key = getKeyFromPassphrase(passphrase, salt);
Cipher cipher = Cipher.getInstance(key.getAlgorithm()); Cipher cipher = Cipher.getInstance(key.getAlgorithm());
cipher.init(opMode, key, new PBEParameterSpec(salt, 100)); cipher.init(opMode, key, new PBEParameterSpec(salt, 100));
return cipher; return cipher;
} }
private static byte[] encryptWithPassphrase(Context context, byte[] data, String passphrase) throws NoSuchAlgorithmException, GeneralSecurityException { private static byte[] encryptWithPassphrase(Context context, byte[] data, String passphrase) throws NoSuchAlgorithmException, GeneralSecurityException {
byte[] encryptionSalt = generateSalt(); byte[] encryptionSalt = generateSalt();
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, Cipher.ENCRYPT_MODE); Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, Cipher.ENCRYPT_MODE);
byte[] cipherText = cipher.doFinal(data); byte[] cipherText = cipher.doFinal(data);
save(context, "encryption_salt", encryptionSalt); save(context, "encryption_salt", encryptionSalt);
return cipherText; return cipherText;
} }
private static byte[] decryptWithPassphrase(Context context, byte[] data, String passphrase) throws GeneralSecurityException, IOException { private static byte[] decryptWithPassphrase(Context context, byte[] data, String passphrase) throws GeneralSecurityException, IOException {
byte[] encryptionSalt = retrieve(context, "encryption_salt"); byte[] encryptionSalt = retrieve(context, "encryption_salt");
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, Cipher.DECRYPT_MODE); Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, Cipher.DECRYPT_MODE);
return cipher.doFinal(data); return cipher.doFinal(data);
} }
private static Mac getMacForPassphrase(String passphrase, byte[] salt) throws GeneralSecurityException { private static Mac getMacForPassphrase(String passphrase, byte[] salt) throws GeneralSecurityException {
@ -255,32 +260,32 @@ public class MasterSecretUtil {
return hmac; return hmac;
} }
private static byte[] verifyMac(Context context, byte[] encryptedAndMacdData, String passphrase) throws InvalidPassphraseException, GeneralSecurityException, IOException { private static byte[] verifyMac(Context context, byte[] encryptedAndMacdData, String passphrase) throws InvalidPassphraseException, GeneralSecurityException, IOException {
byte[] macSalt = retrieve(context, "mac_salt"); byte[] macSalt = retrieve(context, "mac_salt");
Mac hmac = getMacForPassphrase(passphrase, macSalt); Mac hmac = getMacForPassphrase(passphrase, macSalt);
byte[] encryptedData = new byte[encryptedAndMacdData.length - hmac.getMacLength()]; byte[] encryptedData = new byte[encryptedAndMacdData.length - hmac.getMacLength()];
System.arraycopy(encryptedAndMacdData, 0, encryptedData, 0, encryptedData.length); System.arraycopy(encryptedAndMacdData, 0, encryptedData, 0, encryptedData.length);
byte[] givenMac = new byte[hmac.getMacLength()]; byte[] givenMac = new byte[hmac.getMacLength()];
System.arraycopy(encryptedAndMacdData, encryptedAndMacdData.length-hmac.getMacLength(), givenMac, 0, givenMac.length); System.arraycopy(encryptedAndMacdData, encryptedAndMacdData.length-hmac.getMacLength(), givenMac, 0, givenMac.length);
byte[] localMac = hmac.doFinal(encryptedData); byte[] localMac = hmac.doFinal(encryptedData);
if (Arrays.equals(givenMac, localMac)) return encryptedData; if (Arrays.equals(givenMac, localMac)) return encryptedData;
else throw new InvalidPassphraseException("MAC Error"); else throw new InvalidPassphraseException("MAC Error");
} }
private static byte[] macWithPassphrase(Context context, byte[] data, String passphrase) throws GeneralSecurityException { private static byte[] macWithPassphrase(Context context, byte[] data, String passphrase) throws GeneralSecurityException {
byte[] macSalt = generateSalt(); byte[] macSalt = generateSalt();
Mac hmac = getMacForPassphrase(passphrase, macSalt); Mac hmac = getMacForPassphrase(passphrase, macSalt);
byte[] mac = hmac.doFinal(data); byte[] mac = hmac.doFinal(data);
byte[] result = new byte[data.length + mac.length]; byte[] result = new byte[data.length + mac.length];
System.arraycopy(data, 0, result, 0, data.length); System.arraycopy(data, 0, result, 0, data.length);
System.arraycopy(mac, 0, result, data.length, mac.length); System.arraycopy(mac, 0, result, data.length, mac.length);
save(context, "mac_salt", macSalt); save(context, "mac_salt", macSalt);
return result; return result;
} }

View File

@ -150,7 +150,7 @@ public class SmsMigrator {
private static void migrateConversation(Context context, MasterSecret masterSecret, private static void migrateConversation(Context context, MasterSecret masterSecret,
SmsMigrationProgressListener listener, SmsMigrationProgressListener listener,
int primaryProgress, ProgressDescription progress,
long theirThreadId, long ourThreadId) long theirThreadId, long ourThreadId)
{ {
SmsDatabase ourSmsDatabase = DatabaseFactory.getSmsDatabase(context); SmsDatabase ourSmsDatabase = DatabaseFactory.getSmsDatabase(context);
@ -166,11 +166,7 @@ public class SmsMigrator {
getContentValuesForRow(context, masterSecret, cursor, ourThreadId, statement); getContentValuesForRow(context, masterSecret, cursor, ourThreadId, statement);
statement.execute(); statement.execute();
double position = cursor.getPosition(); listener.progressUpdate(new ProgressDescription(progress, cursor.getCount(), cursor.getPosition()));
double count = cursor.getCount();
double progress = position / count;
listener.progressUpdate(primaryProgress, (int)(progress * 10000));
} }
ourSmsDatabase.endTransaction(transaction); ourSmsDatabase.endTransaction(transaction);
@ -192,31 +188,26 @@ public class SmsMigrator {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Cursor cursor = null; Cursor cursor = null;
int primaryProgress = 0;
try { try {
Uri threadListUri = Uri.parse("content://mms-sms/conversations?simple=true"); Uri threadListUri = Uri.parse("content://mms-sms/conversations?simple=true");
cursor = context.getContentResolver().query(threadListUri, null, null, null, "date ASC"); cursor = context.getContentResolver().query(threadListUri, null, null, null, "date ASC");
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
long theirThreadId = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); long theirThreadId = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
String theirRecipients = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids")); String theirRecipients = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
Recipients ourRecipients = getOurRecipients(context, theirRecipients); Recipients ourRecipients = getOurRecipients(context, theirRecipients);
ProgressDescription progress = new ProgressDescription(cursor.getCount(), cursor.getPosition(), 100, 0);
if (ourRecipients != null) { if (ourRecipients != null) {
long ourThreadId = threadDatabase.getThreadIdFor(ourRecipients); long ourThreadId = threadDatabase.getThreadIdFor(ourRecipients);
migrateConversation(context, masterSecret, migrateConversation(context, masterSecret,
listener, primaryProgress, listener, progress,
theirThreadId, ourThreadId); theirThreadId, ourThreadId);
} }
double position = cursor.getPosition() + 1; progress.incrementPrimaryComplete();
double count = cursor.getCount(); listener.progressUpdate(progress);
double progress = position / count;
primaryProgress = (int)(progress * 10000);
listener.progressUpdate(primaryProgress, 0);
} }
} finally { } finally {
if (cursor != null) if (cursor != null)
@ -228,6 +219,34 @@ public class SmsMigrator {
} }
public interface SmsMigrationProgressListener { public interface SmsMigrationProgressListener {
public void progressUpdate(int primaryProgress, int secondaryProgress); public void progressUpdate(ProgressDescription description);
} }
public static class ProgressDescription {
public final int primaryTotal;
public int primaryComplete;
public final int secondaryTotal;
public final int secondaryComplete;
public ProgressDescription(int primaryTotal, int primaryComplete,
int secondaryTotal, int secondaryComplete)
{
this.primaryTotal = primaryTotal;
this.primaryComplete = primaryComplete;
this.secondaryTotal = secondaryTotal;
this.secondaryComplete = secondaryComplete;
}
public ProgressDescription(ProgressDescription that, int secondaryTotal, int secondaryComplete) {
this.primaryComplete = that.primaryComplete;
this.primaryTotal = that.primaryTotal;
this.secondaryComplete = secondaryComplete;
this.secondaryTotal = secondaryTotal;
}
public void incrementPrimaryComplete() {
primaryComplete += 1;
}
}
} }

View File

@ -37,8 +37,8 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RoutingActivity;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -79,7 +79,7 @@ public class MessageNotifier {
if (visibleThread == threadId) { if (visibleThread == threadId) {
sendInThreadNotification(context); sendInThreadNotification(context);
} else { } else {
Intent intent = new Intent(context, ConversationListActivity.class); Intent intent = new Intent(context, RoutingActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra("recipients", recipients); intent.putExtra("recipients", recipients);
intent.putExtra("thread_id", threadId); intent.putExtra("thread_id", threadId);
@ -187,7 +187,7 @@ public class MessageNotifier {
notificationState.getMessageCount())); notificationState.getMessageCount()));
builder.setContentText(String.format(context.getString(R.string.MessageNotifier_most_recent_from_s), builder.setContentText(String.format(context.getString(R.string.MessageNotifier_most_recent_from_s),
notifications.get(0).getRecipientName())); notifications.get(0).getRecipientName()));
builder.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0)); builder.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, RoutingActivity.class), 0));
InboxStyle style = new InboxStyle(); InboxStyle style = new InboxStyle();

View File

@ -6,7 +6,7 @@ import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.RoutingActivity;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -62,7 +62,7 @@ public class NotificationItem {
} }
public PendingIntent getPendingIntent(Context context) { public PendingIntent getPendingIntent(Context context) {
Intent intent = new Intent(context, ConversationListActivity.class); Intent intent = new Intent(context, RoutingActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
if (recipients.getPrimaryRecipient() != null) { if (recipients.getPrimaryRecipient() != null) {

View File

@ -4,41 +4,64 @@ import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.BitmapFactory;
import android.os.Binder; import android.os.Binder;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.PowerManager; import android.os.PowerManager;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
import android.widget.RemoteViews; import android.support.v4.app.NotificationCompat;
import android.util.Log;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RoutingActivity;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.SmsMigrator; import org.thoughtcrime.securesms.database.SmsMigrator;
import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class ApplicationMigrationService extends Service public class ApplicationMigrationService extends Service
implements SmsMigrator.SmsMigrationProgressListener implements SmsMigrator.SmsMigrationProgressListener
{ {
public static final int PROGRESS_UPDATE = 1; public static final String MIGRATE_DATABASE = "org.thoughtcrime.securesms.ApplicationMigration.MIGRATE_DATABSE";
public static final int PROGRESS_COMPLETE = 2; public static final String COMPLETED_ACTION = "org.thoughtcrime.securesms.ApplicationMigrationService.COMPLETED";
private static final String PREFERENCES_NAME = "SecureSMS";
private static final String DATABASE_MIGRATED = "migrated";
public static final String MIGRATE_DATABASE = "org.thoughtcrime.securesms.ApplicationMigration.MIGRATE_DATABSE"; private final BroadcastReceiver completedReceiver = new CompletedReceiver();
private final Binder binder = new ApplicationMigrationBinder();
private final Executor executor = Executors.newSingleThreadExecutor();
private final Binder binder = new ApplicationMigrationBinder(); private Handler handler = null;
private boolean isMigrating = false; private NotificationCompat.Builder notification = null;
private Handler handler = null; private ImportState state = new ImportState(ImportState.STATE_IDLE, null);
private Notification notification = null;
@Override @Override
public void onStart(Intent intent, int startId) { public void onCreate() {
if (intent == null) return; registerCompletedReceiver();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) return START_NOT_STICKY;
if (intent.getAction() != null && intent.getAction().equals(MIGRATE_DATABASE)) { if (intent.getAction() != null && intent.getAction().equals(MIGRATE_DATABASE)) {
handleDatabaseMigration((MasterSecret)intent.getParcelableExtra("master_secret")); executor.execute(new ImportRunnable(intent));
} }
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
unregisterCompletedReceiver();
} }
@Override @Override
@ -46,70 +69,106 @@ public class ApplicationMigrationService extends Service
return binder; return binder;
} }
private void handleDatabaseMigration(final MasterSecret masterSecret) { public void setImportStateHandler(Handler handler) {
this.notification = initializeBackgroundNotification(); this.handler = handler;
final PowerManager power = (PowerManager)getSystemService(Context.POWER_SERVICE);
final WakeLock wakeLock = power.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Migration");
new Thread() {
@Override
public void run() {
try {
wakeLock.acquire();
setMigrating(true);
SmsMigrator.migrateDatabase(ApplicationMigrationService.this,
masterSecret,
ApplicationMigrationService.this);
setMigrating(false);
if (handler != null) {
handler.obtainMessage(PROGRESS_COMPLETE).sendToTarget();
}
stopForeground(true);
} finally {
wakeLock.release();
stopService(new Intent(ApplicationMigrationService.this,
ApplicationMigrationService.class));
}
}
}.start();
} }
private Notification initializeBackgroundNotification() { private void registerCompletedReceiver() {
Intent intent = new Intent(this, ConversationListActivity.class); IntentFilter filter = new IntentFilter();
Notification notification = new Notification(R.drawable.icon, filter.addAction(COMPLETED_ACTION);
getString(R.string.ApplicationMigrationService_migrating),
System.currentTimeMillis());
notification.flags = notification.flags | Notification.FLAG_ONGOING_EVENT; registerReceiver(completedReceiver, filter);
notification.contentView = new RemoteViews(getApplicationContext().getPackageName(), }
R.layout.migration_notification_progress);
notification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0); private void unregisterCompletedReceiver() {
notification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon); unregisterReceiver(completedReceiver);
notification.contentView.setTextViewText(R.id.status_text, }
getString(R.string.ApplicationMigrationService_migrating_system_text_messages));
notification.contentView.setProgressBar(R.id.status_progress, 10000, 0, false); private void notifyImportComplete() {
Intent intent = new Intent();
intent.setAction(COMPLETED_ACTION);
sendOrderedBroadcast(intent, null);
}
@Override
public void progressUpdate(ProgressDescription progress) {
setState(new ImportState(ImportState.STATE_MIGRATING_IN_PROGRESS, progress));
}
public ImportState getState() {
return state;
}
private void setState(ImportState state) {
this.state = state;
if (handler != null) {
handler.obtainMessage(state.state, state.progress).sendToTarget();
}
if (state.progress != null && state.progress.secondaryComplete == 0) {
updateBackgroundNotification(state.progress.primaryTotal, state.progress.primaryComplete);
}
}
private void updateBackgroundNotification(int total, int complete) {
notification.setProgress(total, complete, false);
((NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE))
.notify(4242, notification.build());
}
private NotificationCompat.Builder initializeBackgroundNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.drawable.icon_notification);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon_notification));
builder.setContentTitle(getString(R.string.ApplicationMigrationService_importing_text_messages));
builder.setContentText(getString(R.string.ApplicationMigrationService_import_in_progress));
builder.setOngoing(true);
builder.setProgress(100, 0, false);
builder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, RoutingActivity.class), 0));
stopForeground(true); stopForeground(true);
startForeground(4242, notification); startForeground(4242, builder.build());
return notification; return builder;
} }
private synchronized void setMigrating(boolean isMigrating) { private class ImportRunnable implements Runnable {
this.isMigrating = isMigrating; private final MasterSecret masterSecret;
}
public synchronized boolean isMigrating() { public ImportRunnable(Intent intent) {
return isMigrating; this.masterSecret = intent.getParcelableExtra("master_secret");
} Log.w("ApplicationMigrationService", "Service got mastersecret: " + masterSecret);
}
public void setHandler(Handler handler) { @Override
this.handler = handler; public void run() {
notification = initializeBackgroundNotification();
PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Migration");
try {
wakeLock.acquire();
setState(new ImportState(ImportState.STATE_MIGRATING_BEGIN, null));
SmsMigrator.migrateDatabase(ApplicationMigrationService.this,
masterSecret,
ApplicationMigrationService.this);
setState(new ImportState(ImportState.STATE_MIGRATING_COMPLETE, null));
setDatabaseImported(ApplicationMigrationService.this);
stopForeground(true);
notifyImportComplete();
stopSelf();
} finally {
wakeLock.release();
}
}
} }
public class ApplicationMigrationBinder extends Binder { public class ApplicationMigrationBinder extends Binder {
@ -118,20 +177,44 @@ public class ApplicationMigrationService extends Service
} }
} }
@Override private class CompletedReceiver extends BroadcastReceiver {
public void progressUpdate(int primaryProgress, int secondaryProgress) { @Override
if (handler != null) { public void onReceive(Context context, Intent intent) {
handler.obtainMessage(PROGRESS_UPDATE, primaryProgress, secondaryProgress).sendToTarget(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
} builder.setSmallIcon(R.drawable.icon_notification);
builder.setContentTitle("Import Complete");
builder.setContentText("TextSecure system database import is complete.");
builder.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, RoutingActivity.class), 0));
builder.setWhen(System.currentTimeMillis());
builder.setDefaults(Notification.DEFAULT_VIBRATE);
builder.setAutoCancel(true);
if (notification != null && secondaryProgress == 0) { Notification notification = builder.build();
notification.contentView.setProgressBar(R.id.status_progress, 10000, primaryProgress, false); ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(31337, notification);
NotificationManager notificationManager =
(NotificationManager)getApplicationContext()
.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(4242, notification);
} }
} }
public static class ImportState {
public static final int STATE_IDLE = 0;
public static final int STATE_MIGRATING_BEGIN = 1;
public static final int STATE_MIGRATING_IN_PROGRESS = 2;
public static final int STATE_MIGRATING_COMPLETE = 3;
public int state;
public ProgressDescription progress;
public ImportState(int state, ProgressDescription progress) {
this.state = state;
this.progress = progress;
}
}
public static boolean isDatabaseImported(Context context) {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
.getBoolean(DATABASE_MIGRATED, false);
}
public static void setDatabaseImported(Context context) {
context.getSharedPreferences(PREFERENCES_NAME, 0).edit().putBoolean(DATABASE_MIGRATED, true).commit();
}
} }

View File

@ -32,8 +32,9 @@ import android.util.Log;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RoutingActivity;
import org.thoughtcrime.securesms.crypto.DecryptingQueue;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
@ -54,8 +55,6 @@ public class KeyCachingService extends Service {
public static final String CLEAR_KEY_ACTION = "org.thoughtcrime.securesms.service.action.CLEAR_KEY"; public static final String CLEAR_KEY_ACTION = "org.thoughtcrime.securesms.service.action.CLEAR_KEY";
public static final String ACTIVITY_START_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_START_EVENT"; public static final String ACTIVITY_START_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_START_EVENT";
public static final String ACTIVITY_STOP_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_STOP_EVENT"; public static final String ACTIVITY_STOP_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_STOP_EVENT";
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
private PendingIntent pending; private PendingIntent pending;
private int activitiesRunning = 0; private int activitiesRunning = 0;
@ -75,6 +74,7 @@ public class KeyCachingService extends Service {
foregroundService(); foregroundService();
broadcastNewSecret(); broadcastNewSecret();
startTimeoutIfAppropriate(); startTimeoutIfAppropriate();
DecryptingQueue.schedulePendingDecrypts(this, masterSecret);
new Thread() { new Thread() {
@Override @Override
@ -178,7 +178,9 @@ public class KeyCachingService extends Service {
Notification notification = new Notification(R.drawable.icon_cached, Notification notification = new Notification(R.drawable.icon_cached,
getString(R.string.KeyCachingService_textsecure_passphrase_cached), getString(R.string.KeyCachingService_textsecure_passphrase_cached),
System.currentTimeMillis()); System.currentTimeMillis());
Intent intent = new Intent(this, ConversationListActivity.class); Intent intent = new Intent(this, RoutingActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent launchIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0); PendingIntent launchIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
notification.setLatestEventInfo(getApplicationContext(), notification.setLatestEventInfo(getApplicationContext(),
getString(R.string.KeyCachingService_passphrase_cached), getString(R.string.KeyCachingService_passphrase_cached),