Enhanced import/export support.

1) Allow imports from the stock SMS database at any time.

2) Provide plaintext export support, in a format compatible with
   the "SMS Backup And Restore" app.

3) Fix the DB weirdness on encrypted restore that previously
   required killing the app.
This commit is contained in:
Moxie Marlinspike 2013-06-24 21:02:30 -07:00
parent 903ab92f5f
commit aa25f94291
37 changed files with 1343 additions and 199 deletions

View File

@ -68,12 +68,15 @@
</intent-filter>
</activity>
<activity android:name=".ImportExportActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PromptApnActivity"
android:label="Configure MMS Settings"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ConversationListActivity"
android:label="@string/app_name"
android:launchMode="singleTask"

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:drawable/list_selector_background"
android:state_pressed="true" />
<item android:drawable="@drawable/card" />
</selector>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffeaeaea"
android:layout_gravity="center_vertical"
android:gravity="center_vertical">
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_gravity="center_vertical"
android:padding="8dip"
android:background="#ffeaeaea">
<LinearLayout android:id="@+id/export_encrypted_backup"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
android:background="@drawable/clickable_card"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dip"
android:src="@drawable/encrypted_backup"/>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Registration.Description"
android:text="@string/export_fragment__export_encrypted_backup"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/export_fragment__export_an_encrypted_backup_to_the_sd_card"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/export_plaintext_backup"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
android:background="@drawable/clickable_card"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dip"
android:src="@drawable/plaintext_backup"/>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Registration.Description"
android:text="@string/export_fragment__export_plaintext_backup"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/export_fragment__export_a_plaintext_backup_compatible_with"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/import_export_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffeaeaea"
android:layout_gravity="center_vertical"
android:gravity="center_vertical">
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_gravity="center_vertical"
android:padding="8dip"
android:background="#ffeaeaea">
<LinearLayout android:id="@+id/import_sms"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/clickable_card"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dip"
android:src="@drawable/stock_sms"/>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Registration.Description"
android:text="@string/import_fragment__import_system_sms_database"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/import_fragment__import_the_database_from_the_default_system"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/import_encrypted_backup"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
android:background="@drawable/clickable_card"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dip"
android:src="@drawable/encrypted_backup"/>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Registration.Description"
android:text="@string/import_fragment__import_encrypted_backup"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/import_fragment__restore_a_previously_exported_encrypted_textsecure_backup"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/import_plaintext_backup"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
android:background="@drawable/clickable_card"
android:orientation="vertical">
<LinearLayout android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip">
<ImageView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dip"
android:src="@drawable/plaintext_backup"/>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Registration.Description"
android:text="@string/import_fragment__import_plaintext_backup"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/import_fragment__import_a_plaintext_backup_file"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -13,19 +13,6 @@
android:id="@+id/menu_mark_all_read"
android:icon="@android:drawable/ic_menu_set_as" />
<item android:title="@string/text_secure_normal__menu_import_export"
android:icon="@android:drawable/ic_menu_save">
<menu>
<item android:title="@string/text_secure_normal__menu_import"
android:id="@+id/menu_import"
android:icon="@android:drawable/ic_menu_revert" />
<item android:title="@string/text_secure_normal__menu_export"
android:id="@+id/menu_export"
android:icon="@android:drawable/ic_menu_save" />
</menu>
</item>
<item android:title="@string/text_secure_normal__menu_settings"
android:id="@+id/menu_settings"
android:icon="@android:drawable/ic_menu_preferences" />

View File

@ -104,7 +104,62 @@
<!-- ConversationListItem -->
<string name="ConversationListItem_key_exchange_message">Key exchange message...</string>
<!-- ExportFragment -->
<string name="ExportFragment_export_to_sd_card">Export To SD Card?</string>
<string name="ExportFragment_this_will_export_your_encrypted_keys_settings_and_messages">This
will export your encrypted keys, settings, and messages to the SD card.
</string>
<string name="ExportFragment_export">Export</string>
<string name="ExportFragment_export_plaintext_to_sd_card">Export Plaintext To SD Card?</string>
<string name="ExportFragment_warning_this_will_export_the_plaintext_contents">Warning, this will
export the plaintext contents of your TextSecure messages to the SD card.
</string>
<string name="ExportFragment_cancel">Cancel</string>
<string name="ExportFragment_exporting">Exporting</string>
<string name="ExportFragment_exporting_plaintext_to_sd_card">Exporting plaintext to SD card...
</string>
<string name="ExportFragment_error_unable_to_write_to_sd_card">Error, unable to write to SD
card!
</string>
<string name="ExportFragment_error_while_writing_to_sd_card">Error while writing to SD card.
</string>
<string name="ExportFragment_success">Success!</string>
<string name="ExportFragment_exporting_keys_settings_and_messages">Exporting encrypted keys,
settings, and messages...
</string>
<!-- ImportFragment -->
<string name="ImportFragment_import_system_sms_database">Import System SMS Database?</string>
<string name="ImportFragment_this_will_import_messages_from_the_system">This will import
messages from the system\'s default SMS database to TextSecure. If you\'ve previously
imported the system\'s SMS database, importing again will result in duplicated messages.
</string>
<string name="ImportFragment_import">Import</string>
<string name="ImportFragment_cancel">Cancel</string>
<string name="ImportFragment_restore_encrypted_backup">Restore Encrypted Backup?</string>
<string name="ImportFragment_restoring_an_encrypted_backup_will_completely_replace_your_existing_keys">
Restoring an encrypted backup will completely replace your existing keys, preferences, and
messages. You will lose any information that\'s in your current TextSecure install but not
in the backup.
</string>
<string name="ImportFragment_restore">Restore</string>
<string name="ImportFragment_import_plaintext_backup">Import Plaintext Backup?</string>
<string name="ImportFragment_this_will_import_messages_from_a_plaintext_backup">This will import
messages from a plaintext backup. If you\'ve previously imported the system\'s SMS database,
importing again will result in duplicated messages.
</string>
<string name="ImportFragment_importing">Importing</string>
<string name="ImportFragment_import_plaintext_backup_elipse">Import plaintext backup...</string>
<string name="ImportFragment_no_plaintext_backup_found">No plaintext backup found!</string>
<string name="ImportFragment_error_importing_backup">Error importing backup!</string>
<string name="ImportFragment_import_complete">Import complete!</string>
<string name="ImportFragment_restoring">Restoring</string>
<string name="ImportFragment_restoring_encrypted_backup">Restoring encrypted backup...</string>
<string name="ImportFragment_no_encrypted_backup_found">No encrypted backup found!</string>
<string name="ImportFragment_restore_complete">Restore complete!</string>
<!-- KeyScanningActivity -->
<string name="KeyScanningActivity_no_scanned_key_found_exclamation">No scanned key found!</string>
@ -322,6 +377,26 @@
<!-- database_upgrade_activity -->
<string name="database_upgrade_activity__updating_database">Updating Database...</string>
<string name="export_fragment__export_encrypted_backup">Export Encrypted Backup</string>
<string name="export_fragment__export_an_encrypted_backup_to_the_sd_card">Export an encrypted
backup to the SD card.
</string>
<string name="export_fragment__export_plaintext_backup">Export Plaintext Backup</string>
<string name="export_fragment__export_a_plaintext_backup_compatible_with">
Export a plaintext backup compatible with \'SMSBackup And Restore\' to the SD card.</string>
<string name="import_fragment__import_system_sms_database">Import System SMS Database</string>
<string name="import_fragment__import_the_database_from_the_default_system">Import the database
from the default system messenger app.
</string>
<string name="import_fragment__import_encrypted_backup">Import Encrypted Backup</string>
<string name="import_fragment__restore_a_previously_exported_encrypted_textsecure_backup">
Restore a previously exported encrypted TextSecure backup.
</string>
<string name="import_fragment__import_plaintext_backup">Import Plaintext Backup</string>
<string name="import_fragment__import_a_plaintext_backup_file">
Import a plaintext backup file. Compatible with \'SMSBackup And Restore.\'</string>
<!-- prompt_passphrase_activity -->
<string name="prompt_passphrase_activity__textsecure_passphrase">TEXTSECURE PASSPHRASE</string>
<string name="prompt_passphrase_activity__unlock">Unlock</string>

View File

@ -20,8 +20,8 @@
</style>
<style name="TextSecure.LightTheme.NavigationDrawer" parent="@style/TextSecure.LightTheme">
<item name="android:homeAsUpIndicator">@drawable/ic_drawer</item>
<item name="homeAsUpIndicator">@drawable/ic_drawer</item>
<item name="navigation_drawer_background">@color/abs__background_holo_light</item>
<item name="navigation_drawer_text_color">#ff333333</item>
<item name="navigation_drawer_icons">@array/navigation_drawer_icons_light</item>
@ -49,6 +49,7 @@
</style>
<style name="TextSecure.DarkTheme.NavigationDrawer" parent="@style/TextSecure.DarkTheme">
<item name="android:homeAsUpIndicator">@drawable/ic_drawer</item>
<item name="homeAsUpIndicator">@drawable/ic_drawer</item>
<item name="navigation_drawer_background">#ff333333</item>
<item name="navigation_drawer_text_color">#ffdddddd</item>

View File

@ -1,147 +0,0 @@
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import org.thoughtcrime.securesms.database.ApplicationExporter;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import java.io.IOException;
public class ApplicationExportManager extends Handler implements Runnable {
private static final int ERROR_NO_SD = 0;
private static final int ERROR_IO = 1;
private static final int COMPLETE = 2;
private static final int TASK_EXPORT = 0;
private static final int TASK_IMPORT = 1;
private int task;
private ProgressDialog progressDialog;
private ApplicationExportListener listener;
private final Context context;
public ApplicationExportManager(Context context) {
this.context = context;
}
public void setListener(ApplicationExportListener listener) {
this.listener = listener;
}
public void importDatabase() {
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(context);
alertBuilder.setTitle(R.string.ApplicationExportManager_import_database_and_settings_title);
alertBuilder.setMessage(R.string.ApplicationExportManager_import_database_and_settings_message);
alertBuilder.setCancelable(false);
alertBuilder.setPositiveButton(R.string.ApplicationExportManager_import, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
task = TASK_IMPORT;
progressDialog = new ProgressDialog(context);
progressDialog.setTitle(context.getString(R.string.ApplicationExportManager_importing_database_and_keys));
progressDialog.setMessage(context
.getString(R.string.ApplicationExportManager_importing_your_sms_database_keys_and_settings));
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.show();
if (listener != null)
listener.onPrepareForImport();
new Thread(ApplicationExportManager.this).start();
}
});
alertBuilder.setNegativeButton(android.R.string.cancel, null);
alertBuilder.create().show();
}
public void exportDatabase() {
Log.w("ApplicationExportManager", "Context: " + context);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(context);
alertBuilder.setTitle(R.string.ApplicationExportManager_export_database_question);
alertBuilder.setMessage(R.string.ApplicationExportManager_export_textsecure_database_keys_and_settings_prompt);
alertBuilder.setCancelable(false);
alertBuilder.setPositiveButton(R.string.ApplicationExportManager_export, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
task = TASK_EXPORT;
progressDialog = new ProgressDialog(context);
progressDialog.setTitle(context.getString(R.string.ApplicationExportManager_exporting_database_and_keys));
progressDialog.setMessage(context
.getString(R.string.ApplicationExportManager_exporting_your_sms_database_keys_and_settings));
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.show();
new Thread(ApplicationExportManager.this).start();
}
});
alertBuilder.setNegativeButton(android.R.string.cancel, null);
alertBuilder.create().show();
}
public void run() {
try {
switch (task) {
case TASK_EXPORT: ApplicationExporter.exportToSd(context); break;
case TASK_IMPORT: ApplicationExporter.importFromSd(context); break;
}
} catch (NoExternalStorageException e) {
Log.w("SecureSMS", e);
this.obtainMessage(ERROR_NO_SD).sendToTarget();
return;
} catch (IOException e) {
Log.w("SecureSMS", e);
this.obtainMessage(ERROR_IO).sendToTarget();
return;
}
this.obtainMessage(COMPLETE).sendToTarget();
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case ERROR_NO_SD:
Toast.makeText(context, R.string.ApplicationExportManager_no_sd_card_found_exclamation,
Toast.LENGTH_LONG).show();
break;
case ERROR_IO:
Toast.makeText(context, R.string.ApplicationExportManager_error_exporting_to_sd_exclamation,
Toast.LENGTH_LONG).show();
break;
case COMPLETE:
switch (task) {
case TASK_IMPORT:
Toast.makeText(context, R.string.ApplicationExportManager_import_successful_exclamation,
Toast.LENGTH_LONG).show();
break;
case TASK_EXPORT:
Toast.makeText(context, R.string.ApplicationExportManager_export_successful_exclamation,
Toast.LENGTH_LONG).show();
break;
}
break;
}
progressDialog.dismiss();
}
public interface ApplicationExportListener {
public void onPrepareForImport();
}
}

View File

@ -19,7 +19,6 @@ import android.widget.SimpleAdapter;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import org.thoughtcrime.securesms.ApplicationExportManager.ApplicationExportListener;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -100,7 +99,8 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
Intent intent;
if (selected.equals("import_export")) {
intent = new Intent();
intent = new Intent(this, ImportExportActivity.class);
intent.putExtra("master_secret", masterSecret);
} else if (selected.equals("my_identity_key")) {
intent = new Intent(this, ViewIdentityActivity.class);
intent.putExtra("identity_key", IdentityKeyUtil.getIdentityKey(this));
@ -126,8 +126,6 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
switch (item.getItemId()) {
case R.id.menu_new_message: createConversation(-1, null, defaultType); 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_mark_all_read: handleMarkAllRead(); return true;
case android.R.id.home: handleNavigationDrawerToggle(); return true;
@ -165,25 +163,6 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
startActivity(preferencesIntent);
}
private void handleExportDatabase() {
ApplicationExportManager exportManager = new ApplicationExportManager(this);
exportManager.exportDatabase();
}
private void handleImportDatabase() {
ApplicationExportManager exportManager = new ApplicationExportManager(this);
ApplicationExportListener listener = new ApplicationExportManager.ApplicationExportListener() {
@Override
public void onPrepareForImport() {
onMasterSecretCleared();
handleClearPassphrase();
}
};
exportManager.setListener(listener);
exportManager.importDatabase();
}
private void handleClearPassphrase() {
Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(KeyCachingService.CLEAR_KEY_ACTION);

View File

@ -65,6 +65,11 @@ public class DatabaseMigrationActivity extends PassphraseRequiredSherlockActivit
shutdownServiceBinding();
}
@Override
public void onBackPressed() {
}
private void initializeServiceBinding() {
Intent intent = new Intent(this, ApplicationMigrationService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

View File

@ -0,0 +1,197 @@
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.EncryptedBackupExporter;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.database.PlaintextBackupExporter;
import java.io.IOException;
public class ExportFragment extends SherlockFragment {
private static final int SUCCESS = 0;
private static final int NO_SD_CARD = 1;
private static final int IO_ERROR = 2;
private MasterSecret masterSecret;
public void setMasterSecret(MasterSecret masterSecret) {
this.masterSecret = masterSecret;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
View layout = inflater.inflate(R.layout.export_fragment, container, false);
View exportEncryptedView = layout.findViewById(R.id.export_encrypted_backup);
View exportPlaintextView = layout.findViewById(R.id.export_plaintext_backup);
exportEncryptedView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleExportEncryptedBackup();
}
});
exportPlaintextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleExportPlaintextBackup();
}
});
return layout;
}
private void handleExportEncryptedBackup() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setTitle(getActivity().getString(R.string.ExportFragment_export_to_sd_card));
builder.setMessage(getActivity().getString(R.string.ExportFragment_this_will_export_your_encrypted_keys_settings_and_messages));
builder.setPositiveButton(getActivity().getString(R.string.ExportFragment_export), new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new ExportEncryptedTask().execute();
}
});
builder.setNegativeButton(getActivity().getString(R.string.ExportFragment_cancel), null);
builder.show();
}
private void handleExportPlaintextBackup() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(getActivity().getString(R.string.ExportFragment_export_plaintext_to_sd_card));
builder.setMessage(getActivity().getString(R.string.ExportFragment_warning_this_will_export_the_plaintext_contents));
builder.setPositiveButton(getActivity().getString(R.string.ExportFragment_export), new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new ExportPlaintextTask().execute();
}
});
builder.setNegativeButton(getActivity().getString(R.string.ExportFragment_cancel), null);
builder.show();
}
private class ExportPlaintextTask extends AsyncTask<Void, Void, Integer> {
private ProgressDialog dialog;
@Override
protected void onPreExecute() {
dialog = ProgressDialog.show(getActivity(),
getActivity().getString(R.string.ExportFragment_exporting),
getActivity().getString(R.string.ExportFragment_exporting_plaintext_to_sd_card),
true, false);
}
@Override
protected Integer doInBackground(Void... params) {
try {
PlaintextBackupExporter.exportPlaintextToSd(getActivity(), masterSecret);
return SUCCESS;
} catch (NoExternalStorageException e) {
Log.w("ExportFragment", e);
return NO_SD_CARD;
} catch (IOException e) {
Log.w("ExportFragment", e);
return IO_ERROR;
}
}
@Override
protected void onPostExecute(Integer result) {
Context context = getActivity();
if (dialog != null)
dialog.dismiss();
if (context == null)
return;
switch (result) {
case NO_SD_CARD:
Toast.makeText(context,
context.getString(R.string.ExportFragment_error_unable_to_write_to_sd_card),
Toast.LENGTH_LONG).show();
break;
case IO_ERROR:
Toast.makeText(context,
context.getString(R.string.ExportFragment_error_while_writing_to_sd_card),
Toast.LENGTH_LONG).show();
break;
case SUCCESS:
Toast.makeText(context,
context.getString(R.string.ExportFragment_success),
Toast.LENGTH_LONG).show();
break;
}
}
}
private class ExportEncryptedTask extends AsyncTask<Void, Void, Integer> {
private ProgressDialog dialog;
@Override
protected void onPreExecute() {
dialog = ProgressDialog.show(getActivity(),
getActivity().getString(R.string.ExportFragment_exporting),
getActivity().getString(R.string.ExportFragment_exporting_keys_settings_and_messages),
true, false);
}
@Override
protected void onPostExecute(Integer result) {
Context context = getActivity();
if (dialog != null) dialog.dismiss();
if (context == null) return;
switch (result) {
case NO_SD_CARD:
Toast.makeText(context,
context.getString(R.string.ExportFragment_error_unable_to_write_to_sd_card),
Toast.LENGTH_LONG).show();
break;
case IO_ERROR:
Toast.makeText(context,
context.getString(R.string.ExportFragment_error_while_writing_to_sd_card),
Toast.LENGTH_LONG).show();
break;
case SUCCESS:
Toast.makeText(context,
context.getString(R.string.ExportFragment_success),
Toast.LENGTH_LONG).show();
break;
}
}
@Override
protected Integer doInBackground(Void... params) {
try {
EncryptedBackupExporter.exportToSd(getActivity());
return SUCCESS;
} catch (NoExternalStorageException e) {
Log.w("ExportFragment", e);
return NO_SD_CARD;
} catch (IOException e) {
Log.w("ExportFragment", e);
return IO_ERROR;
}
}
}
}

View File

@ -0,0 +1,109 @@
package org.thoughtcrime.securesms;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.ViewPager;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.view.MenuItem;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class ImportExportActivity extends PassphraseRequiredSherlockFragmentActivity {
private TabPagerAdapter tabPagerAdapter;
private ViewPager viewPager;
private MasterSecret masterSecret;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.import_export_activity);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
initializeResources();
initializeViewPager();
initializeTabs();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
}
return false;
}
private void initializeResources() {
this.masterSecret = getIntent().getParcelableExtra("master_secret");
this.viewPager = (ViewPager) findViewById(R.id.import_export_pager);
this.tabPagerAdapter = new TabPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(tabPagerAdapter);
}
private void initializeViewPager() {
viewPager.setAdapter(tabPagerAdapter);
viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
getSupportActionBar().setSelectedNavigationItem(position);
}
});
}
private void initializeTabs() {
final ActionBar actionBar = getSupportActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
ActionBar.TabListener tabListener = new ActionBar.TabListener() {
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
viewPager.setCurrentItem(tab.getPosition());
}
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {}
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {}
};
actionBar.addTab(actionBar.newTab().setText("Import").setTabListener(tabListener));
actionBar.addTab(actionBar.newTab().setText("Export").setTabListener(tabListener));
}
private class TabPagerAdapter extends FragmentStatePagerAdapter {
private final ImportFragment importFragment;
private final ExportFragment exportFragment;
public TabPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
this.importFragment = new ImportFragment();
this.exportFragment = new ExportFragment();
this.importFragment.setMasterSecret(masterSecret);
this.exportFragment.setMasterSecret(masterSecret);
}
@Override
public Fragment getItem(int i) {
if (i == 0) return importFragment;
else return exportFragment;
}
@Override
public int getCount() {
return 2;
}
@Override
public CharSequence getPageTitle(int i) {
if (i == 0) return "Import";
else return "Export";
}
}
}

View File

@ -0,0 +1,248 @@
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockFragment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.EncryptedBackupExporter;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.database.PlaintextBackupImporter;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
import org.thoughtcrime.securesms.service.KeyCachingService;
import java.io.IOException;
public class ImportFragment extends SherlockFragment {
private static final int SUCCESS = 0;
private static final int NO_SD_CARD = 1;
private static final int ERROR_IO = 2;
private MasterSecret masterSecret;
private ProgressDialog progressDialog;
public void setMasterSecret(MasterSecret masterSecret) {
this.masterSecret = masterSecret;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
View layout = inflater.inflate(R.layout.import_fragment, container, false);
View importSmsView = layout.findViewById(R.id.import_sms );
View importEncryptedView = layout.findViewById(R.id.import_encrypted_backup);
View importPlaintextView = layout.findViewById(R.id.import_plaintext_backup);
importSmsView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleImportSms();
}
});
importEncryptedView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleImportEncryptedBackup();
}
});
importPlaintextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleImportPlaintextBackup();
}
});
return layout;
}
@Override
public void onDestroy() {
super.onDestroy();
if (progressDialog != null && progressDialog.isShowing()) {
progressDialog.dismiss();
progressDialog = null;
}
}
private void handleImportSms() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setTitle(getActivity().getString(R.string.ImportFragment_import_system_sms_database));
builder.setMessage(getActivity().getString(R.string.ImportFragment_this_will_import_messages_from_the_system));
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(getActivity(), ApplicationMigrationService.class);
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
intent.putExtra("master_secret", masterSecret);
getActivity().startService(intent);
Intent nextIntent = new Intent(getActivity(), ConversationListActivity.class);
intent.putExtra("master_secret", masterSecret);
Intent activityIntent = new Intent(getActivity(), DatabaseMigrationActivity.class);
activityIntent.putExtra("master_secret", masterSecret);
activityIntent.putExtra("next_intent", nextIntent);
getActivity().startActivity(activityIntent);
}
});
builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
builder.show();
}
private void handleImportEncryptedBackup() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(getActivity().getString(R.string.ImportFragment_restore_encrypted_backup));
builder.setMessage(getActivity().getString(R.string.ImportFragment_restoring_an_encrypted_backup_will_completely_replace_your_existing_keys));
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_restore), new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new ImportEncryptedBackupTask().execute();
}
});
builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
builder.show();
}
private void handleImportPlaintextBackup() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(getActivity().getString(R.string.ImportFragment_import_plaintext_backup));
builder.setMessage(getActivity().getString(R.string.ImportFragment_this_will_import_messages_from_a_plaintext_backup));
builder.setPositiveButton(getActivity().getString(R.string.ImportFragment_import), new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new ImportPlaintextBackupTask().execute();
}
});
builder.setNegativeButton(getActivity().getString(R.string.ImportFragment_cancel), null);
builder.show();
}
private class ImportPlaintextBackupTask extends AsyncTask<Void, Void, Integer> {
@Override
protected void onPreExecute() {
progressDialog = ProgressDialog.show(getActivity(),
getActivity().getString(R.string.ImportFragment_importing),
getActivity().getString(R.string.ImportFragment_import_plaintext_backup_elipse),
true, false);
}
protected void onPostExecute(Integer result) {
Context context = getActivity();
if (progressDialog != null)
progressDialog.dismiss();
if (context == null)
return;
switch (result) {
case NO_SD_CARD:
Toast.makeText(context,
context.getString(R.string.ImportFragment_no_plaintext_backup_found),
Toast.LENGTH_LONG).show();
break;
case ERROR_IO:
Toast.makeText(context,
context.getString(R.string.ImportFragment_error_importing_backup),
Toast.LENGTH_LONG).show();
break;
case SUCCESS:
Toast.makeText(context,
context.getString(R.string.ImportFragment_import_complete),
Toast.LENGTH_LONG).show();
break;
}
}
@Override
protected Integer doInBackground(Void... params) {
try {
PlaintextBackupImporter.importPlaintextFromSd(getActivity(), masterSecret);
return SUCCESS;
} catch (NoExternalStorageException e) {
Log.w("ImportFragment", e);
return NO_SD_CARD;
} catch (IOException e) {
Log.w("ImportFragment", e);
return ERROR_IO;
}
}
}
private class ImportEncryptedBackupTask extends AsyncTask<Void, Void, Integer> {
@Override
protected void onPreExecute() {
progressDialog = ProgressDialog.show(getActivity(),
getActivity().getString(R.string.ImportFragment_restoring),
getActivity().getString(R.string.ImportFragment_restoring_encrypted_backup),
true, false);
}
protected void onPostExecute(Integer result) {
Context context = getActivity();
if (progressDialog != null)
progressDialog.dismiss();
if (context == null)
return;
switch (result) {
case NO_SD_CARD:
Toast.makeText(context,
context.getString(R.string.ImportFragment_no_encrypted_backup_found),
Toast.LENGTH_LONG).show();
break;
case ERROR_IO:
Toast.makeText(context,
context.getString(R.string.ImportFragment_error_importing_backup),
Toast.LENGTH_LONG).show();
break;
case SUCCESS:
DatabaseFactory.getInstance(context).reset(context);
Intent intent = new Intent(context, KeyCachingService.class);
intent.setAction(KeyCachingService.CLEAR_KEY_ACTION);
context.startService(intent);
Toast.makeText(context,
context.getString(R.string.ImportFragment_restore_complete),
Toast.LENGTH_LONG).show();
}
}
@Override
protected Integer doInBackground(Void... params) {
try {
EncryptedBackupExporter.importFromSd(getActivity());
return SUCCESS;
} catch (NoExternalStorageException e) {
Log.w("ImportFragment", e);
return NO_SD_CARD;
} catch (IOException e) {
Log.w("ImportFragment", e);
return ERROR_IO;
}
}
}
}

View File

@ -44,7 +44,7 @@ public class CanonicalAddressDatabase {
private static final Object lock = new Object();
private static CanonicalAddressDatabase instance;
private final DatabaseHelper databaseHelper;
private DatabaseHelper databaseHelper;
private final Map<String,Long> addressCache = Collections.synchronizedMap(new HashMap<String,Long>());
private final Map<String,String> idCache = Collections.synchronizedMap(new HashMap<String,String>());
@ -63,6 +63,13 @@ public class CanonicalAddressDatabase {
fillCache();
}
public void reset(Context context) {
DatabaseHelper old = this.databaseHelper;
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
old.close();
fillCache();
}
private void fillCache() {
Cursor cursor = null;

View File

@ -29,7 +29,7 @@ public abstract class Database {
private static final String CONVERSATION_URI = "content://textsecure/thread/";
private static final String CONVERSATION_LIST_URI = "content://textsecure/conversation-list";
protected final SQLiteOpenHelper databaseHelper;
protected SQLiteOpenHelper databaseHelper;
protected final Context context;
public Database(Context context, SQLiteOpenHelper databaseHelper) {
@ -58,4 +58,8 @@ public abstract class Database {
cursor.setNotificationUri(context.getContentResolver(), Uri.parse(CONVERSATION_LIST_URI));
}
public void reset(SQLiteOpenHelper databaseHelper) {
this.databaseHelper = databaseHelper;
}
}

View File

@ -59,7 +59,7 @@ public class DatabaseFactory {
private static DatabaseFactory instance;
private static EncryptingPartDatabase encryptingPartInstance;
private final DatabaseHelper databaseHelper;
private DatabaseHelper databaseHelper;
private final SmsDatabase sms;
private final EncryptingSmsDatabase encryptingSms;
@ -146,10 +146,22 @@ public class DatabaseFactory {
this.draftDatabase = new DraftDatabase(context, databaseHelper);
}
public void close() {
databaseHelper.close();
address.close();
instance = null;
public void reset(Context context) {
DatabaseHelper old = this.databaseHelper;
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms.reset(databaseHelper);
this.encryptingSms.reset(databaseHelper);
this.mms.reset(databaseHelper);
this.part.reset(databaseHelper);
this.thread.reset(databaseHelper);
this.mmsAddress.reset(databaseHelper);
this.mmsSmsDatabase.reset(databaseHelper);
this.identityDatabase.reset(databaseHelper);
this.draftDatabase.reset(databaseHelper);
old.close();
this.address.reset(context);
}
public void onApplicationLevelUpgrade(Context context, MasterSecret masterSecret, int fromVersion,

View File

@ -26,7 +26,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class ApplicationExporter {
public class EncryptedBackupExporter {
public static void exportToSd(Context context) throws NoExternalStorageException, IOException {
verifyExternalStorageForExport();
@ -71,7 +71,7 @@ public class ApplicationExporter {
destination.close();
}
} catch (IOException ioe) {
Log.w("ApplicationExporter", ioe);
Log.w("EncryptedBackupExporter", ioe);
}
}
@ -95,7 +95,7 @@ public class ApplicationExporter {
}
}
} else {
Log.w("ApplicationExporter", "Could not find directory: " + directory.getAbsolutePath());
Log.w("EncryptedBackupExporter", "Could not find directory: " + directory.getAbsolutePath());
}
}

View File

@ -102,6 +102,11 @@ public class EncryptingSmsDatabase extends SmsDatabase {
Types.ENCRYPTION_SYMMETRIC_BIT);
}
public Reader getMessages(MasterSecret masterSecret, int skip, int limit) {
Cursor cursor = super.getMessages(skip, limit);
return new DecryptingReader(masterSecret, cursor);
}
public Reader getOutgoingMessages(MasterSecret masterSecret) {
Cursor cursor = super.getOutgoingMessages();
return new DecryptingReader(masterSecret, cursor);

View File

@ -61,6 +61,10 @@ public interface MmsSmsColumns {
(type & BASE_TYPE_MASK) == BASE_SENDING_TYPE;
}
public static boolean isInboxType(long type) {
return (type & BASE_TYPE_MASK) == BASE_INBOX_TYPE;
}
public static boolean isSecureType(long type) {
return (type & SECURE_MESSAGE_BIT) != 0;
}
@ -112,6 +116,14 @@ public interface MmsSmsColumns {
return BASE_INBOX_TYPE;
}
public static int translateToSystemBaseType(long type) {
if (isInboxType(type)) return 1;
else if (isOutgoingMessageType(type)) return 2;
else if (isFailedMessageType(type)) return 5;
return 1;
}
//
//

View File

@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.os.Environment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import java.io.File;
import java.io.IOException;
public class PlaintextBackupExporter {
public static void exportPlaintextToSd(Context context, MasterSecret masterSecret)
throws NoExternalStorageException, IOException
{
verifyExternalStorageForPlaintextExport();
exportPlaintext(context, masterSecret);
}
private static void verifyExternalStorageForPlaintextExport() throws NoExternalStorageException {
if (!Environment.getExternalStorageDirectory().canWrite())
throw new NoExternalStorageException();
}
private static String getPlaintextExportDirectoryPath() {
File sdDirectory = Environment.getExternalStorageDirectory();
return sdDirectory.getAbsolutePath() + File.separator + "TextSecurePlaintextBackup.xml";
}
private static void exportPlaintext(Context context, MasterSecret masterSecret)
throws IOException
{
int count = DatabaseFactory.getSmsDatabase(context).getMessageCount();
XmlBackup.Writer writer = new XmlBackup.Writer(getPlaintextExportDirectoryPath(), count);
SmsMessageRecord record;
EncryptingSmsDatabase.Reader reader = null;
int skip = 0;
int ROW_LIMIT = 500;
do {
if (reader != null)
reader.close();
reader = DatabaseFactory.getEncryptingSmsDatabase(context).getMessages(masterSecret, skip, ROW_LIMIT);
while ((record = reader.getNext()) != null) {
XmlBackup.XmlBackupItem item =
new XmlBackup.XmlBackupItem(0, record.getIndividualRecipient().getNumber(),
record.getDateReceived(),
MmsSmsColumns.Types.translateToSystemBaseType(record.getType()),
null, record.getDisplayBody().toString(), null,
1, record.getDeliveryStatus());
writer.writeItem(item);
}
skip += ROW_LIMIT;
} while (reader.getCount() > 0);
writer.close();
}
}

View File

@ -0,0 +1,126 @@
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.os.Environment;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class PlaintextBackupImporter {
public static void importPlaintextFromSd(Context context, MasterSecret masterSecret)
throws NoExternalStorageException, IOException
{
Log.w("PlaintextBackupImporter", "Importing plaintext...");
verifyExternalStorageForPlaintextImport();
importPlaintext(context, masterSecret);
}
private static void verifyExternalStorageForPlaintextImport() throws NoExternalStorageException {
if (!Environment.getExternalStorageDirectory().canRead() ||
!(new File(getPlaintextExportDirectoryPath()).exists()))
throw new NoExternalStorageException();
}
private static String getPlaintextExportDirectoryPath() {
File sdDirectory = Environment.getExternalStorageDirectory();
return sdDirectory.getAbsolutePath() + File.separator + "TextSecurePlaintextBackup.xml";
}
private static void importPlaintext(Context context, MasterSecret masterSecret)
throws IOException
{
Log.w("PlaintextBackupImporter", "importPlaintext()");
SmsDatabase db = DatabaseFactory.getSmsDatabase(context);
SQLiteDatabase transaction = db.beginTransaction();
try {
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
XmlBackup backup = new XmlBackup(getPlaintextExportDirectoryPath());
MasterCipher masterCipher = new MasterCipher(masterSecret);
Set<Long> modifiedThreads = new HashSet<Long>();
XmlBackup.XmlBackupItem item;
while ((item = backup.getNext()) != null) {
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, item.getAddress(), false);
long threadId = threads.getThreadIdFor(recipients);
SQLiteStatement statement = db.createInsertStatement(transaction);
if (item.getAddress() == null || item.getAddress().equals("null"))
continue;
addStringToStatement(statement, 1, item.getAddress());
addNullToStatement(statement, 2);
addLongToStatement(statement, 3, item.getDate());
addLongToStatement(statement, 4, item.getDate());
addLongToStatement(statement, 5, item.getProtocol());
addLongToStatement(statement, 6, item.getRead());
addLongToStatement(statement, 7, item.getStatus());
addTranslatedTypeToStatement(statement, 8, item.getType());
addNullToStatement(statement, 9);
addStringToStatement(statement, 10, item.getSubject());
addEncryptedStingToStatement(masterCipher, statement, 11, item.getBody());
addStringToStatement(statement, 12, item.getServiceCenter());
addLongToStatement(statement, 13, threadId);
modifiedThreads.add(threadId);
statement.execute();
} catch (RecipientFormattingException rfe) {
Log.w("PlaintextBackupImporter", rfe);
}
}
for (long threadId : modifiedThreads) {
threads.update(threadId);
}
Log.w("PlaintextBackupImporter", "Exited loop");
} catch (XmlPullParserException e) {
Log.w("PlaintextBackupImporter", e);
throw new IOException("XML Parsing error!");
} finally {
db.endTransaction(transaction);
}
}
private static void addEncryptedStingToStatement(MasterCipher masterCipher, SQLiteStatement statement, int index, String value) {
if (value == null || value.equals("null")) {
statement.bindNull(index);
} else {
statement.bindString(index, masterCipher.encryptBody(value));
}
}
private static void addTranslatedTypeToStatement(SQLiteStatement statement, int index, int type) {
statement.bindLong(index, SmsDatabase.Types.translateFromSystemBaseType(type) | SmsDatabase.Types.ENCRYPTION_SYMMETRIC_BIT);
}
private static void addStringToStatement(SQLiteStatement statement, int index, String value) {
if (value == null || value.equals("null")) statement.bindNull(index);
else statement.bindString(index, value);
}
private static void addNullToStatement(SQLiteStatement statement, int index) {
statement.bindNull(index);
}
private static void addLongToStatement(SQLiteStatement statement, int index, long value) {
statement.bindLong(index, value);
}
}

View File

@ -121,6 +121,21 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
}
}
public int getMessageCount() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, null, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) return cursor.getInt(0);
else return 0;
} finally {
if (cursor != null)
cursor.close();
}
}
public int getMessageCountForThread(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
@ -290,6 +305,11 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
return messageIds;
}
Cursor getMessages(int skip, int limit) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, MESSAGE_PROJECTION, null, null, null, null, ID, skip + "," + limit);
}
Cursor getOutgoingMessages() {
String outgoingSelection = TYPE + " & " + Types.BASE_TYPE_MASK + " = " + Types.BASE_OUTBOX_TYPE;
SQLiteDatabase db = databaseHelper.getReadableDatabase();
@ -415,6 +435,11 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
return getCurrent();
}
public int getCount() {
if (cursor == null) return 0;
else return cursor.getCount();
}
public SmsMessageRecord getCurrent() {
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
String address = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));

View File

@ -191,8 +191,8 @@ public class SmsMigrator {
MasterSecret masterSecret,
SmsMigrationProgressListener listener)
{
if (context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).getBoolean("migrated", false))
return;
// if (context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).getBoolean("migrated", false))
// return;
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Cursor cursor = null;

View File

@ -0,0 +1,184 @@
package org.thoughtcrime.securesms.database;
import android.util.Log;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
public class XmlBackup {
private static final String PROTOCOL = "protocol";
private static final String ADDRESS = "address";
private static final String DATE = "date";
private static final String TYPE = "type";
private static final String SUBJECT = "subject";
private static final String BODY = "body";
private static final String SERVICE_CENTER = "service_center";
private static final String READ = "read";
private static final String STATUS = "status";
private final XmlPullParser parser;
public XmlBackup(String path) throws XmlPullParserException, FileNotFoundException {
this.parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(new FileInputStream(path), null);
}
public XmlBackupItem getNext() throws IOException, XmlPullParserException {
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (!name.equals("sms")) {
continue;
}
int attributeCount = parser.getAttributeCount();
if (attributeCount <= 0) {
continue;
}
XmlBackupItem item = new XmlBackupItem();
for (int i=0;i<attributeCount;i++) {
String attributeName = parser.getAttributeName(i);
if (attributeName.equals(PROTOCOL )) item.protocol = Integer.parseInt(parser.getAttributeValue(i));
else if (attributeName.equals(ADDRESS )) item.address = parser.getAttributeValue(i);
else if (attributeName.equals(DATE )) item.date = Long.parseLong(parser.getAttributeValue(i));
else if (attributeName.equals(TYPE )) item.type = Integer.parseInt(parser.getAttributeValue(i));
else if (attributeName.equals(SUBJECT )) item.subject = parser.getAttributeValue(i);
else if (attributeName.equals(BODY )) item.body = parser.getAttributeValue(i);
else if (attributeName.equals(SERVICE_CENTER)) item.serviceCenter = parser.getAttributeValue(i);
else if (attributeName.equals(READ )) item.read = Integer.parseInt(parser.getAttributeValue(i));
else if (attributeName.equals(STATUS )) item.status = Integer.parseInt(parser.getAttributeValue(i));
}
return item;
}
return null;
}
public static class XmlBackupItem {
private int protocol;
private String address;
private long date;
private int type;
private String subject;
private String body;
private String serviceCenter;
private int read;
private int status;
public XmlBackupItem() {}
public XmlBackupItem(int protocol, String address, long date, int type, String subject,
String body, String serviceCenter, int read, int status)
{
this.protocol = protocol;
this.address = address;
this.date = date;
this.type = type;
this.subject = subject;
this.body = body;
this.serviceCenter = serviceCenter;
this.read = read;
this.status = status;
}
public int getProtocol() {
return protocol;
}
public String getAddress() {
return address;
}
public long getDate() {
return date;
}
public int getType() {
return type;
}
public String getSubject() {
return subject;
}
public String getBody() {
return body;
}
public String getServiceCenter() {
return serviceCenter;
}
public int getRead() {
return read;
}
public int getStatus() {
return status;
}
}
public static class Writer {
private BufferedWriter writer;
private XmlSerializer serializer;
public Writer(String path, int count) throws IOException {
this.writer = new BufferedWriter(new FileWriter(path));
this.serializer = Xml.newSerializer();
this.serializer.setOutput(writer);
this.serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
this.serializer.startDocument("UTF-8", true);
this.serializer.startTag("", "smses");
this.serializer.attribute("", "count", count+"");
}
public void writeItem(XmlBackupItem item) throws IOException {
this.serializer.startTag("", "sms");
this.serializer.attribute("", "protocol", item.getProtocol() + "");
this.serializer.attribute("", "address", item.getAddress());
this.serializer.attribute("", "date", item.getDate()+"");
this.serializer.attribute("", "type", item.getType()+"");
this.serializer.attribute("", "subject", item.getSubject()+"");
try {
this.serializer.attribute("", "body", item.getBody()+"");
} catch (IllegalArgumentException ise) {
// XXX - Fucking Android. Their serializer includes a bug that doesn't
// handle some unicode characters correctly.
Log.w("XmlBackup", ise);
}
this.serializer.attribute("", "toa", null+"");
this.serializer.attribute("", "sc_toa", null+"");
this.serializer.attribute("", "service_center", item.getServiceCenter()+"");
this.serializer.attribute("", "read", item.getRead()+"");
this.serializer.attribute("" , "status", item.getStatus()+"");
this.serializer.attribute("", "locked", "0");
this.serializer.endTag("", "sms");
}
public void close() throws IOException {
this.serializer.endTag("", "smses");
this.serializer.endDocument();
}
}
}