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.
master
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:theme="@style/Theme.Sherlock.Light.DarkActionBar">
<activity android:name=".ConversationListActivity"
android:label="@string/app_name"
<activity android:name=".RoutingActivity"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:launchMode="singleTask"
android:uiOptions="splitActionBarWhenNarrow"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
@ -62,22 +61,34 @@
</intent-filter>
</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"
android:windowSoftInputMode="stateUnchanged"
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"
android:theme="@style/Theme.Sherlock.Light.Dialog"
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"/>
<activity android:name=".PassphrasePromptActivity"
android:theme="@style/Theme.Sherlock.Light.Dialog"
android:label="@string/AndroidManifest__enter_passphrase"
android:launchMode="singleInstance"
android:windowSoftInputMode="stateVisible"
android:launchMode="singleTop"
android:theme="@style/NoAnimation.Theme.Sherlock.Light.DarkActionBar"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ContactSelectionActivity"
@ -95,7 +106,6 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseChangeActivity"
android:theme="@style/Theme.Sherlock.Light.Dialog"
android:label="@string/AndroidManifest__change_passphrase"
android:launchMode="singleInstance"
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"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
android:layout_width="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_height="wrap_content"
android:paddingLeft="16dip"
android:paddingRight="16dip"
android:orientation="vertical" >
android:layout_height="fill_parent"
android:gravity="center" >
<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
android:padding="3dip"
android:text="@string/change_passphrase_activity__old_passphrase"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<TextView style="@style/Registration.Label"
android:layout_width="fill_parent"
android:textAllCaps="true"
android:text="@string/change_passphrase_activity__new_passphrase" />
<EditText android:id="@+id/new_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true"/>
<EditText
android:id="@+id/old_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:layout_marginBottom="5dip" />
<TextView style="@style/Registration.Label"
android:layout_width="fill_parent"
android:textAllCaps="true"
android:text="@string/change_passphrase_activity__repeat_new_passphrase" />
<EditText android:id="@+id/repeat_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true"/>
<TextView
android:padding="3dip"
android:text="@string/change_passphrase_activity__new_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium" />
<LinearLayout android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dip">
<EditText
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
<Button style="@android:style/Widget.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_marginLeft="16dip"
android:layout_marginRight="15dip"
android:text="@android:string/cancel" />
<Button
android:layout_marginRight="7dip"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<Button style="@android:style/Widget.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_marginRight="16dip"
android:text="@android:string/ok" />
</TableRow>
</TableLayout>
android:textAppearance="?android:attr/textAppearanceMedium"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</ScrollView>

View File

@ -1,74 +1,87 @@
<?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_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"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dip"
android:paddingRight="16dip"
android:orientation="vertical">
<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:textAppearance="?android:attr/textAppearanceMedium"
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"
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center" >
<LinearLayout android:id="@+id/create_layout"
android:paddingRight="16dip"
android:paddingLeft="16dip"
android:paddingTop="10dip"
android:layout_width="fill_parent"
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: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"
android:layout_height="wrap_content"
android:stretchColumns="*">
<TableRow>
<Button android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@android:string/cancel"
android:id="@+id/cancel_button"
android:layout_marginRight="15dip"
android:layout_marginLeft="16dip"/>
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
android:id="@+id/ok_button"
android:layout_marginRight="16dip"/>
</TableRow>
</TableLayout>
<TextView style="@style/Registration.Label"
android:layout_width="fill_parent"
android:textAllCaps="true"
android:text="@string/create_passphrase_activity__repeat" />
<EditText android:id="@+id/passphrase_edit_repeat"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true"/>
<Button style="@android:style/Widget.Button"
android:id="@+id/ok_button"
android:text="@string/create_passphrase_activity__continue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_marginTop="20dip"
android:layout_marginBottom="20dip"/>
</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>

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"?>
<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"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:id="@+id/passphrase_edit"
android:password="true"
android:layout_margin="16dip"/>
<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">
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dip"
android:gravity="right">
<TableLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchColumns="*">
<TableRow>
<Button android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@android:string/cancel"
android:id="@+id/cancel_button"
android:layout_marginRight="15dip"
android:layout_marginLeft="16dip"/>
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
<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:orientation="vertical">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="30dip"
android:src="@drawable/padlock_prompt"/>
<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:layout_marginRight="16dip"/>
</TableRow>
</TableLayout>
</LinearLayout>
</LinearLayout>
android:text="@string/prompt_passphrase_activity__unlock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
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>
<!-- PassphraseCreateActivity -->
<string name="PassphraseCreateActivity_passphrases_dont_match_exclamation">Passphrases Don\'t Match!</string>
<string name="PassphraseCreateActivity_generating_keypair">Generating KeyPair</string>
<string name="PassphraseCreateActivity_generating_a_local_encryption_keypair">Generating a local encryption keypair...</string>
<string name="PassphraseCreateActivity_passphrases_dont_match">Passphrases don\'t match</string>
<string name="PassphraseCreateActivity_you_must_specify_a_password">You must specify a password</string>
<!-- PassphrasePromptActivity -->
<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>
<!-- ApplicationMigrationService -->
<string name="ApplicationMigrationService_migrating">Migrating</string>
<string name="ApplicationMigrationService_migrating_system_text_messages">Migrating System Text Messages</string>
<string name="ApplicationMigrationService_import_in_progress">Import in progress</string>
<string name="ApplicationMigrationService_importing_text_messages">Importing Text Messages</string>
<string name="ApplicationMigrationService_import_complete">Import complete!</string>
<!-- KeyCachingService -->
<string name="KeyCachingService_textsecure_passphrase_cached">TextSecure Passphrase Cached</string>
@ -251,9 +251,9 @@
<string name="auto_initiate_activity__initiate_exchange">Initiate Exchange</string>
<!-- change_passphrase_activity -->
<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__repeat_new_passphrase">Repeat new 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__repeat_new_passphrase">REPEAT NEW PASSPHRASE:</string>
<!-- contact_selection_group_activity -->
<!-- contact_selection_list_activity -->
@ -282,9 +282,23 @@
<string name="conversation_fragment_cab__batch_selection_mode">Batch Selection Mode</string>
<!-- 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__passphrase">Passphrase:</string>
<string name="create_passphrase_activity__repeat">Repeat:</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__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 -->
<string name="receive_key_activity__session">Session</string>

View File

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<item name="android:windowFrame">@null</item>
<item name="android:windowBackground">@android:color/transparent</item>
@ -22,4 +26,48 @@
<item name="android:textStyle">bold</item>
</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>

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.IdentityKeyUtil;
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.MemoryCleaner;
import org.thoughtcrime.securesms.util.Trimmer;
@ -138,7 +138,12 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
@Override
public boolean onOptionsItemSelected(MenuItem item) {
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;
@ -313,9 +318,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
private class ChangePassphraseClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
SharedPreferences settings = getSharedPreferences(KeyCachingService.PREFERENCES_NAME, 0);
if (settings.getBoolean("passphrase_initialized", false)) {
if (MasterSecretUtil.isPassphraseInitialized(ApplicationPreferencesActivity.this)) {
startActivity(new Intent(ApplicationPreferencesActivity.this, PassphraseChangeActivity.class));
} else {
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_session: handleVerifySession(); 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;
@ -261,6 +261,14 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
//////// Event Handlers
private void handleReturnToConversationList() {
Intent intent = new Intent(this, ConversationListActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra("master_secret", masterSecret);
startActivity(intent);
finish();
}
private void handleVerifyRecipient() {
Intent verifyIdentityIntent = new Intent(this, VerifyIdentityActivity.class);
verifyIdentityIntent.putExtra("recipient", getRecipients().getPrimaryRecipient());

View File

@ -1,25 +1,16 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.ContactsContract;
import android.util.Log;
import android.view.WindowManager;
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.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.KeyCachingService;
import org.thoughtcrime.securesms.service.SendReceiveService;
@ -36,11 +27,6 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
private ConversationListFragment fragment;
private MasterSecret masterSecret;
private ApplicationMigrationManager migrationManager;
private boolean havePromptedForPassphrase = false;
private boolean isVisible = false;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
@ -52,32 +38,6 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
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
public void onDestroy() {
Log.w("ConversationListActivity", "onDestroy...");
@ -87,47 +47,17 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
@Override
public void onMasterSecretCleared() {
this.masterSecret = null;
this.fragment.setMasterSecret(null);
this.invalidateOptionsMenu();
if (!havePromptedForPassphrase && isVisible) {
promptForPassphrase();
}
// this.fragment.setMasterSecret(null);
startActivity(new Intent(this, RoutingActivity.class));
super.onMasterSecretCleared();
}
@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
public boolean onPrepareOptionsMenu(Menu menu) {
Log.w("ConversationListActivity", "onPrepareOptionsMenu...");
MenuInflater inflater = this.getSupportMenuInflater();
menu.clear();
if (this.masterSecret == null) inflater.inflate(R.menu.text_secure_locked, menu);
else inflater.inflate(R.menu.text_secure_normal, menu);
inflater.inflate(R.menu.text_secure_normal, menu);
super.onPrepareOptionsMenu(menu);
return true;
@ -138,12 +68,11 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.menu_new_message: createConversation(-1, null, null, null, null); return true;
case R.id.menu_unlock: promptForPassphrase(); return true;
case R.id.menu_settings: handleDisplaySettings(); return true;
case R.id.menu_export: handleExportDatabase(); return true;
case R.id.menu_import: handleImportDatabase(); return true;
case R.id.menu_clear_passphrase: handleClearPassphrase(); return true;
case R.id.menu_new_message: createConversation(-1, null); return true;
case R.id.menu_settings: handleDisplaySettings(); return true;
case R.id.menu_export: handleExportDatabase(); return true;
case R.id.menu_import: handleImportDatabase(); return true;
case R.id.menu_clear_passphrase: handleClearPassphrase(); return true;
}
return false;
@ -151,39 +80,18 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
@Override
public void onCreateConversation(long threadId, Recipients recipients) {
createConversation(threadId, recipients, null, null, null);
createConversation(threadId, recipients);
}
private void createConversation(long threadId, Recipients recipients,
String text, Uri imageUri, Uri audioUri)
{
if (this.masterSecret == null) {
promptForPassphrase();
return;
}
private void createConversation(long threadId, Recipients recipients) {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
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);
}
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() {
Intent preferencesIntent = new Intent(this, ApplicationPreferencesActivity.class);
preferencesIntent.putExtra("master_secret", masterSecret);
@ -237,89 +145,17 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
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() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE);
}
this.masterSecret = (MasterSecret)getIntent().getParcelableExtra("master_secret");
this.fragment = (ConversationListFragment)this.getSupportFragmentManager()
.findFragmentById(R.id.fragment_content);
}
private boolean isDatabaseMigrated() {
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);
}
this.fragment.setMasterSecret(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.os.IBinder;
import com.actionbarsherlock.app.SherlockActivity;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.MemoryCleaner;
import com.actionbarsherlock.app.SherlockActivity;
/**
* 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);
}
protected MasterSecret getMasterSecret() {
return masterSecret;
}
protected abstract void cleanup();
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
keyCachingService = ((KeyCachingService.KeyCachingBinder)service).getService();
keyCachingService.setMasterSecret(masterSecret);
@ -55,9 +60,12 @@ public abstract class PassphraseActivity extends SherlockActivity {
MemoryCleaner.clean(masterSecret);
cleanup();
PassphraseActivity.this.setResult(RESULT_OK);
PassphraseActivity.this.finish();
}
@Override
public void onServiceDisconnected(ComponentName name) {
keyCachingService = null;
}

View File

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

View File

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

View File

@ -150,7 +150,7 @@ public class SmsMigrator {
private static void migrateConversation(Context context, MasterSecret masterSecret,
SmsMigrationProgressListener listener,
int primaryProgress,
ProgressDescription progress,
long theirThreadId, long ourThreadId)
{
SmsDatabase ourSmsDatabase = DatabaseFactory.getSmsDatabase(context);
@ -166,11 +166,7 @@ public class SmsMigrator {
getContentValuesForRow(context, masterSecret, cursor, ourThreadId, statement);
statement.execute();
double position = cursor.getPosition();
double count = cursor.getCount();
double progress = position / count;
listener.progressUpdate(primaryProgress, (int)(progress * 10000));
listener.progressUpdate(new ProgressDescription(progress, cursor.getCount(), cursor.getPosition()));
}
ourSmsDatabase.endTransaction(transaction);
@ -192,31 +188,26 @@ public class SmsMigrator {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Cursor cursor = null;
int primaryProgress = 0;
try {
Uri threadListUri = Uri.parse("content://mms-sms/conversations?simple=true");
cursor = context.getContentResolver().query(threadListUri, null, null, null, "date ASC");
while (cursor != null && cursor.moveToNext()) {
long theirThreadId = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
String theirRecipients = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
Recipients ourRecipients = getOurRecipients(context, theirRecipients);
long theirThreadId = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
String theirRecipients = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
Recipients ourRecipients = getOurRecipients(context, theirRecipients);
ProgressDescription progress = new ProgressDescription(cursor.getCount(), cursor.getPosition(), 100, 0);
if (ourRecipients != null) {
long ourThreadId = threadDatabase.getThreadIdFor(ourRecipients);
migrateConversation(context, masterSecret,
listener, primaryProgress,
listener, progress,
theirThreadId, ourThreadId);
}
double position = cursor.getPosition() + 1;
double count = cursor.getCount();
double progress = position / count;
primaryProgress = (int)(progress * 10000);
listener.progressUpdate(primaryProgress, 0);
progress.incrementPrimaryComplete();
listener.progressUpdate(progress);
}
} finally {
if (cursor != null)
@ -228,6 +219,34 @@ public class SmsMigrator {
}
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 org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RoutingActivity;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -79,7 +79,7 @@ public class MessageNotifier {
if (visibleThread == threadId) {
sendInThreadNotification(context);
} else {
Intent intent = new Intent(context, ConversationListActivity.class);
Intent intent = new Intent(context, RoutingActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra("recipients", recipients);
intent.putExtra("thread_id", threadId);
@ -187,7 +187,7 @@ public class MessageNotifier {
notificationState.getMessageCount()));
builder.setContentText(String.format(context.getString(R.string.MessageNotifier_most_recent_from_s),
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();

View File

@ -6,7 +6,7 @@ import android.content.Intent;
import android.net.Uri;
import android.text.SpannableStringBuilder;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.RoutingActivity;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Util;
@ -62,7 +62,7 @@ public class NotificationItem {
}
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);
if (recipients.getPrimaryRecipient() != null) {

View File

@ -4,41 +4,64 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
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.RoutingActivity;
import org.thoughtcrime.securesms.crypto.MasterSecret;
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
implements SmsMigrator.SmsMigrationProgressListener
{
public static final int PROGRESS_UPDATE = 1;
public static final int PROGRESS_COMPLETE = 2;
public static final String MIGRATE_DATABASE = "org.thoughtcrime.securesms.ApplicationMigration.MIGRATE_DATABSE";
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 boolean isMigrating = false;
private Handler handler = null;
private Notification notification = null;
private Handler handler = null;
private NotificationCompat.Builder notification = null;
private ImportState state = new ImportState(ImportState.STATE_IDLE, null);
@Override
public void onStart(Intent intent, int startId) {
if (intent == null) return;
public void onCreate() {
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)) {
handleDatabaseMigration((MasterSecret)intent.getParcelableExtra("master_secret"));
executor.execute(new ImportRunnable(intent));
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
unregisterCompletedReceiver();
}
@Override
@ -46,70 +69,106 @@ public class ApplicationMigrationService extends Service
return binder;
}
private void handleDatabaseMigration(final MasterSecret masterSecret) {
this.notification = initializeBackgroundNotification();
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();
public void setImportStateHandler(Handler handler) {
this.handler = handler;
}
private Notification initializeBackgroundNotification() {
Intent intent = new Intent(this, ConversationListActivity.class);
Notification notification = new Notification(R.drawable.icon,
getString(R.string.ApplicationMigrationService_migrating),
System.currentTimeMillis());
private void registerCompletedReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(COMPLETED_ACTION);
notification.flags = notification.flags | Notification.FLAG_ONGOING_EVENT;
notification.contentView = new RemoteViews(getApplicationContext().getPackageName(),
R.layout.migration_notification_progress);
registerReceiver(completedReceiver, filter);
}
notification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
notification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon);
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 unregisterCompletedReceiver() {
unregisterReceiver(completedReceiver);
}
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);
startForeground(4242, notification);
startForeground(4242, builder.build());
return notification;
return builder;
}
private synchronized void setMigrating(boolean isMigrating) {
this.isMigrating = isMigrating;
}
private class ImportRunnable implements Runnable {
private final MasterSecret masterSecret;
public synchronized boolean isMigrating() {
return isMigrating;
}
public ImportRunnable(Intent intent) {
this.masterSecret = intent.getParcelableExtra("master_secret");
Log.w("ApplicationMigrationService", "Service got mastersecret: " + masterSecret);
}
public void setHandler(Handler handler) {
this.handler = handler;
@Override
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 {
@ -118,20 +177,44 @@ public class ApplicationMigrationService extends Service
}
}
@Override
public void progressUpdate(int primaryProgress, int secondaryProgress) {
if (handler != null) {
handler.obtainMessage(PROGRESS_UPDATE, primaryProgress, secondaryProgress).sendToTarget();
}
private class CompletedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
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.contentView.setProgressBar(R.id.status_progress, 10000, primaryProgress, false);
NotificationManager notificationManager =
(NotificationManager)getApplicationContext()
.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(4242, notification);
Notification notification = builder.build();
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(31337, 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 org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.ConversationListActivity;
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.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 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 PREFERENCES_NAME = "SecureSMS-Preferences";
private PendingIntent pending;
private int activitiesRunning = 0;
@ -75,6 +74,7 @@ public class KeyCachingService extends Service {
foregroundService();
broadcastNewSecret();
startTimeoutIfAppropriate();
DecryptingQueue.schedulePendingDecrypts(this, masterSecret);
new Thread() {
@Override
@ -178,7 +178,9 @@ public class KeyCachingService extends Service {
Notification notification = new Notification(R.drawable.icon_cached,
getString(R.string.KeyCachingService_textsecure_passphrase_cached),
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);
notification.setLatestEventInfo(getApplicationContext(),
getString(R.string.KeyCachingService_passphrase_cached),