From 23aee53c7d6681adb507eaec37e2dc4d23757569 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Thu, 1 Feb 2018 16:01:24 -0800 Subject: [PATCH] Add determinte progress and foreground service for sqlcipher migration --- AndroidManifest.xml | 2 + res/layout/database_upgrade_activity.xml | 2 +- res/values/strings.xml | 1 + .../securesms/database/DatabaseFactory.java | 3 +- .../helpers/SQLCipherMigrationHelper.java | 61 +++++++++++--- .../database/helpers/SQLCipherOpenHelper.java | 4 +- .../service/GenericForegroundService.java | 82 +++++++++++++++++++ 7 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/service/GenericForegroundService.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e7aac6fda3..f0e7bfa30c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -443,6 +443,8 @@ + + diff --git a/res/layout/database_upgrade_activity.xml b/res/layout/database_upgrade_activity.xml index 5e9c1fc44e..8f797c9b20 100644 --- a/res/layout/database_upgrade_activity.xml +++ b/res/layout/database_upgrade_activity.xml @@ -34,7 +34,7 @@ android:layout_gravity="center"/> Not now Signal needs Contacts permission in order to search your contacts, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Contacts\". ENABLE SIGNAL MESSAGES + Migrating Signal database diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 26967eebce..fa105bac85 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -156,7 +156,8 @@ public class DatabaseFactory { SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyOpenHelper.getWritableDatabase(), - databaseHelper.getWritableDatabase()); + databaseHelper.getWritableDatabase(), + listener); } } diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java index 6148393d73..b7e2986194 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherMigrationHelper.java @@ -10,13 +10,16 @@ import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import com.annimon.stream.function.Function; +import com.annimon.stream.function.BiFunction; +import org.thoughtcrime.securesms.DatabaseUpgradeActivity; +import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.service.GenericForegroundService; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.InvalidMessageException; @@ -30,11 +33,13 @@ public class SQLCipherMigrationHelper { private static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000; private static final long ENCRYPTION_ASYMMETRIC_BIT = 0x40000000; - static void migratePlaintext(@NonNull android.database.sqlite.SQLiteDatabase legacyDb, + static void migratePlaintext(@NonNull Context context, + @NonNull android.database.sqlite.SQLiteDatabase legacyDb, @NonNull net.sqlcipher.database.SQLiteDatabase modernDb) { modernDb.beginTransaction(); try { + GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database)); copyTable("identities", legacyDb, modernDb, null); copyTable("push", legacyDb, modernDb, null); copyTable("groups", legacyDb, modernDb, null); @@ -43,13 +48,15 @@ public class SQLCipherMigrationHelper { modernDb.setTransactionSuccessful(); } finally { modernDb.endTransaction(); + GenericForegroundService.stopForegroundTask(context); } } public static void migrateCiphertext(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull android.database.sqlite.SQLiteDatabase legacyDb, - @NonNull net.sqlcipher.database.SQLiteDatabase modernDb) + @NonNull net.sqlcipher.database.SQLiteDatabase modernDb, + @Nullable DatabaseUpgradeActivity.DatabaseUpgradeListener listener) { MasterCipher legacyCipher = new MasterCipher(masterSecret); AsymmetricMasterCipher legacyAsymmetricCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret)); @@ -57,7 +64,10 @@ public class SQLCipherMigrationHelper { modernDb.beginTransaction(); try { - copyTable("sms", legacyDb, modernDb, (row) -> { + GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database)); + int total = 5000; + + copyTable("sms", legacyDb, modernDb, (row, progress) -> { Pair plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher, row.getAsLong("type"), row.getAsString("body")); @@ -65,10 +75,14 @@ public class SQLCipherMigrationHelper { row.put("body", plaintext.second); row.put("type", plaintext.first); + if (listener != null && (progress.first % 1000 == 0)) { + listener.setProgress(getTotalProgress(0, progress.first, progress.second), total); + } + return row; }); - copyTable("mms", legacyDb, modernDb, (row) -> { + copyTable("mms", legacyDb, modernDb, (row, progress) -> { Pair plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher, row.getAsLong("msg_box"), row.getAsString("body")); @@ -76,10 +90,14 @@ public class SQLCipherMigrationHelper { row.put("body", plaintext.second); row.put("msg_box", plaintext.first); + if (listener != null && (progress.first % 1000 == 0)) { + listener.setProgress(getTotalProgress(1000, progress.first, progress.second), total); + } + return row; }); - copyTable("part", legacyDb, modernDb, (row) -> { + copyTable("part", legacyDb, modernDb, (row, progress) -> { String fileName = row.getAsString("file_name"); String mediaKey = row.getAsString("cd"); @@ -107,11 +125,14 @@ public class SQLCipherMigrationHelper { Log.w(TAG, e); } + if (listener != null && (progress.first % 1000 == 0)) { + listener.setProgress(getTotalProgress(2000, progress.first, progress.second), total); + } return row; }); - copyTable("thread", legacyDb, modernDb, (row) -> { + copyTable("thread", legacyDb, modernDb, (row, progress) -> { Pair plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher, row.getAsLong("snippet_type"), row.getAsString("snippet")); @@ -119,11 +140,15 @@ public class SQLCipherMigrationHelper { row.put("snippet", plaintext.second); row.put("snippet_type", plaintext.first); + if (listener != null && (progress.first % 1000 == 0)) { + listener.setProgress(getTotalProgress(3000, progress.first, progress.second), total); + } + return row; }); - copyTable("drafts", legacyDb, modernDb, (row) -> { + copyTable("drafts", legacyDb, modernDb, (row, progress) -> { String draftType = row.getAsString("type"); String draft = row.getAsString("value"); @@ -134,24 +159,31 @@ public class SQLCipherMigrationHelper { Log.w(TAG, e); } + if (listener != null && (progress.first % 1000 == 0)) { + listener.setProgress(getTotalProgress(4000, progress.first, progress.second), total); + } + return row; }); + AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded()); TextSecurePreferences.setNeedsSqlCipherMigration(context, false); modernDb.setTransactionSuccessful(); } finally { modernDb.endTransaction(); + GenericForegroundService.stopForegroundTask(context); } - - AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded()); } private static void copyTable(@NonNull String tableName, @NonNull android.database.sqlite.SQLiteDatabase legacyDb, @NonNull net.sqlcipher.database.SQLiteDatabase modernDb, - @Nullable Function transformer) + @Nullable BiFunction, ContentValues> transformer) { try (Cursor cursor = legacyDb.query(tableName, null, null, null, null, null, null)) { + int count = (cursor != null) ? cursor.getCount() : 0; + int progress = 1; + while (cursor != null && cursor.moveToNext()) { ContentValues row = new ContentValues(); @@ -167,7 +199,7 @@ public class SQLCipherMigrationHelper { } if (transformer != null) { - row = transformer.apply(row); + row = transformer.apply(row, new Pair<>(progress++, count)); } modernDb.insert(tableName, null, row); @@ -194,4 +226,9 @@ public class SQLCipherMigrationHelper { return new Pair<>(type, body); } + + private static int getTotalProgress(int sectionOffset, int sectionProgress, int sectionTotal) { + double percentOfSectionComplete = ((double)sectionProgress) / ((double)sectionTotal); + return sectionOffset + (int)(((double)1000) * percentOfSectionComplete); + } } diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 0b5490ff3b..c7728a4793 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -78,11 +78,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context); android.database.sqlite.SQLiteDatabase legacyDb = legacyHelper.getWritableDatabase(); - SQLCipherMigrationHelper.migratePlaintext(legacyDb, db); + SQLCipherMigrationHelper.migratePlaintext(context, legacyDb, db); MasterSecret masterSecret = KeyCachingService.getMasterSecret(context); - if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db); + if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db, null); else TextSecurePreferences.setNeedsSqlCipherMigration(context, true); } } diff --git a/src/org/thoughtcrime/securesms/service/GenericForegroundService.java b/src/org/thoughtcrime/securesms/service/GenericForegroundService.java new file mode 100644 index 0000000000..b00bf397e4 --- /dev/null +++ b/src/org/thoughtcrime/securesms/service/GenericForegroundService.java @@ -0,0 +1,82 @@ +package org.thoughtcrime.securesms.service; + + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; + +import org.thoughtcrime.securesms.ConversationListActivity; +import org.thoughtcrime.securesms.R; + +import java.util.concurrent.atomic.AtomicInteger; + +public class GenericForegroundService extends Service { + + private static final int NOTIFICATION_ID = 827353982; + private static final String EXTRA_TITLE = "extra_title"; + + private static final String ACTION_START = "start"; + private static final String ACTION_STOP = "stop"; + + private final AtomicInteger foregroundCount = new AtomicInteger(0); + + @Override + public void onCreate() { + + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null && ACTION_START.equals(intent.getAction())) handleStart(intent); + else if (intent != null && ACTION_STOP.equals(intent.getAction())) handleStop(); + + return START_NOT_STICKY; + } + + + private void handleStart(@NonNull Intent intent) { + String title = intent.getStringExtra(EXTRA_TITLE); + assert title != null; + + if (foregroundCount.getAndIncrement() == 0) { + startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.ic_signal_grey_24dp) + .setContentTitle(title) + .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ConversationListActivity.class), 0)) + .build()); + } + } + + private void handleStop() { + if (foregroundCount.decrementAndGet() == 0) { + stopForeground(true); + stopSelf(); + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + public static void startForegroundTask(@NonNull Context context, @NonNull String task) { + Intent intent = new Intent(context, GenericForegroundService.class); + intent.setAction(ACTION_START); + intent.putExtra(EXTRA_TITLE, task); + + context.startService(intent); + } + + public static void stopForegroundTask(@NonNull Context context) { + Intent intent = new Intent(context, GenericForegroundService.class); + intent.setAction(ACTION_STOP); + + context.startService(intent); + } +}