From 5853f57f0aa2324ef0932abf9dcb2bf2d902a112 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 3 May 2024 08:51:09 +0200 Subject: [PATCH] add ability to cancel in-progress one-off backup --- src/main/AndroidManifest.xml | 8 +- .../SystemEventReceiver.java} | 5 +- .../UnifiedPushDistributor.java | 3 +- .../receiver/WorkManagerEventReceiver.java | 32 +++ .../services/ContactChooserTargetService.java | 3 +- .../services/UnifiedPushBroker.java | 1 + .../services/XmppConnectionService.java | 13 +- .../settings/BackupSettingsFragment.java | 2 +- .../fragment/settings/UpSettingsFragment.java | 2 +- .../conversations/utils/Compatibility.java | 2 +- .../worker/ExportBackupWorker.java | 248 +++++++++++------- 11 files changed, 208 insertions(+), 111 deletions(-) rename src/main/java/eu/siacs/conversations/{services/EventReceiver.java => receiver/SystemEventReceiver.java} (89%) rename src/main/java/eu/siacs/conversations/{services => receiver}/UnifiedPushDistributor.java (99%) create mode 100644 src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 3d0ce91b0..749b593c6 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -143,7 +143,11 @@ + + @@ -157,7 +161,7 @@ diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/receiver/SystemEventReceiver.java similarity index 89% rename from src/main/java/eu/siacs/conversations/services/EventReceiver.java rename to src/main/java/eu/siacs/conversations/receiver/SystemEventReceiver.java index b189e9a9e..3efa9b67e 100644 --- a/src/main/java/eu/siacs/conversations/services/EventReceiver.java +++ b/src/main/java/eu/siacs/conversations/receiver/SystemEventReceiver.java @@ -1,4 +1,4 @@ -package eu.siacs.conversations.services; +package eu.siacs.conversations.receiver; import android.content.BroadcastReceiver; import android.content.Context; @@ -10,9 +10,10 @@ import android.util.Log; import com.google.common.base.Strings; import eu.siacs.conversations.Config; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.Compatibility; -public class EventReceiver extends BroadcastReceiver { +public class SystemEventReceiver extends BroadcastReceiver { public static final String SETTING_ENABLED_ACCOUNTS = "enabled_accounts"; public static final String EXTRA_NEEDS_FOREGROUND_SERVICE = "needs_foreground_service"; diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java b/src/main/java/eu/siacs/conversations/receiver/UnifiedPushDistributor.java similarity index 99% rename from src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java rename to src/main/java/eu/siacs/conversations/receiver/UnifiedPushDistributor.java index f51de2432..ace71ddb5 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java +++ b/src/main/java/eu/siacs/conversations/receiver/UnifiedPushDistributor.java @@ -1,4 +1,4 @@ -package eu.siacs.conversations.services; +package eu.siacs.conversations.receiver; import android.app.PendingIntent; import android.content.BroadcastReceiver; @@ -25,6 +25,7 @@ import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.persistance.UnifiedPushDatabase; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.Compatibility; public class UnifiedPushDistributor extends BroadcastReceiver { diff --git a/src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java b/src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java new file mode 100644 index 000000000..71ec74f53 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java @@ -0,0 +1,32 @@ +package eu.siacs.conversations.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.work.WorkManager; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.ui.fragment.settings.BackupSettingsFragment; + +public class WorkManagerEventReceiver extends BroadcastReceiver { + + public static final String ACTION_STOP_BACKUP = "eu.siacs.conversations.receiver.STOP_BACKUP"; + + @Override + public void onReceive(final Context context, final Intent intent) { + final var action = Strings.nullToEmpty(intent == null ? null : intent.getAction()); + if (action.equals(ACTION_STOP_BACKUP)) { + stopBackup(context); + } + } + + private void stopBackup(final Context context) { + Log.d(Config.LOGTAG, "trying to stop one-off backup worker"); + final var workManager = WorkManager.getInstance(context); + workManager.cancelUniqueWork(BackupSettingsFragment.CREATE_ONE_OFF_BACKUP); + } +} diff --git a/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java b/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java index c68b502af..63a9afc3f 100644 --- a/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java +++ b/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java @@ -21,6 +21,7 @@ import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.receiver.SystemEventReceiver; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.utils.Compatibility; @@ -44,7 +45,7 @@ public class ContactChooserTargetService extends ChooserTargetService implements @Override public List onGetChooserTargets( final ComponentName targetActivityName, final IntentFilter matchedFilter) { - if (!EventReceiver.hasEnabledAccounts(this)) { + if (!SystemEventReceiver.hasEnabledAccounts(this)) { return Collections.emptyList(); } final Intent intent = new Intent(this, XmppConnectionService.class); diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index bfa1785f1..4aab05cee 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -28,6 +28,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.persistance.UnifiedPushDatabase; +import eu.siacs.conversations.receiver.UnifiedPushDistributor; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b12eaffff..ce45d9083 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -128,6 +128,7 @@ import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.UnifiedPushDatabase; +import eu.siacs.conversations.receiver.SystemEventReceiver; import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.RtpSessionActivity; @@ -678,7 +679,7 @@ public class XmppConnectionService extends Service { @Override public int onStartCommand(final Intent intent, int flags, int startId) { final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction()); - final boolean needsForegroundService = intent != null && intent.getBooleanExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false); + final boolean needsForegroundService = intent != null && intent.getBooleanExtra(SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false); if (needsForegroundService) { Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")"); toggleForegroundService(true); @@ -1286,7 +1287,7 @@ public class XmppConnectionService extends Service { this.accounts = databaseBackend.getAccounts(); final SharedPreferences.Editor editor = getPreferences().edit(); final boolean hasEnabledAccounts = hasEnabledAccounts(); - editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); + editor.putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); editor.apply(); toggleSetProfilePictureActivity(hasEnabledAccounts); reconfigurePushDistributor(); @@ -1582,7 +1583,7 @@ public class XmppConnectionService extends Service { return; } final long triggerAtMillis = SystemClock.elapsedRealtime() + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000); - final Intent intent = new Intent(this, EventReceiver.class); + final Intent intent = new Intent(this, SystemEventReceiver.class); intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE); try { final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, s() @@ -1604,7 +1605,7 @@ public class XmppConnectionService extends Service { if (alarmManager == null) { return; } - final Intent intent = new Intent(this, EventReceiver.class); + final Intent intent = new Intent(this, SystemEventReceiver.class); intent.setAction(ACTION_PING); try { final PendingIntent pendingIntent = @@ -1623,7 +1624,7 @@ public class XmppConnectionService extends Service { if (alarmManager == null) { return; } - final Intent intent = new Intent(this, EventReceiver.class); + final Intent intent = new Intent(this, SystemEventReceiver.class); intent.setAction(ACTION_IDLE_PING); try { final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, s() @@ -2573,7 +2574,7 @@ public class XmppConnectionService extends Service { private void syncEnabledAccountSetting() { final boolean hasEnabledAccounts = hasEnabledAccounts(); - getPreferences().edit().putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); + getPreferences().edit().putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); toggleSetProfilePictureActivity(hasEnabledAccounts); } diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java index debf929aa..f78577656 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java @@ -36,7 +36,7 @@ import java.util.concurrent.TimeUnit; public class BackupSettingsFragment extends XmppPreferenceFragment { - private static final String CREATE_ONE_OFF_BACKUP = "create_one_off_backup"; + public static final String CREATE_ONE_OFF_BACKUP = "create_one_off_backup"; private static final String RECURRING_BACKUP = "recurring_backup"; private final ActivityResultLauncher requestStorageForBackupLauncher = diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/UpSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/UpSettingsFragment.java index 7ab15e21e..771acbbe1 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/UpSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/UpSettingsFragment.java @@ -13,7 +13,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import eu.siacs.conversations.R; -import eu.siacs.conversations.services.UnifiedPushDistributor; +import eu.siacs.conversations.receiver.UnifiedPushDistributor; import eu.siacs.conversations.xmpp.Jid; import java.net.URI; diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index eaf89d121..26a2331cc 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -1,6 +1,6 @@ package eu.siacs.conversations.utils; -import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; +import static eu.siacs.conversations.receiver.SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; import android.annotation.SuppressLint; import android.app.ActivityOptions; diff --git a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java index 167a1f240..75ef9036c 100644 --- a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java +++ b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java @@ -11,6 +11,7 @@ import android.content.pm.ServiceInfo; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import android.os.SystemClock; import android.util.Log; import androidx.annotation.NonNull; @@ -21,6 +22,7 @@ import androidx.work.WorkerParameters; import com.google.common.base.Optional; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.gson.stream.JsonWriter; import eu.siacs.conversations.Config; @@ -31,6 +33,7 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.receiver.WorkManagerEventReceiver; import eu.siacs.conversations.utils.BackupFileHeader; import eu.siacs.conversations.utils.Compatibility; @@ -65,7 +68,7 @@ import javax.crypto.spec.SecretKeySpec; public class ExportBackupWorker extends Worker { private static final SimpleDateFormat DATE_FORMAT = - new SimpleDateFormat("yyyy-MM-dd", Locale.US); + new SimpleDateFormat("yyyy-MM-dd-HH-mm", Locale.US); public static final String KEYTYPE = "AES"; public static final String CIPHERMODE = "AES/GCM/NoPadding"; @@ -76,6 +79,11 @@ public class ExportBackupWorker extends Worker { private static final int NOTIFICATION_ID = 19; private static final int BACKUP_CREATED_NOTIFICATION_ID = 23; + private static final int PENDING_INTENT_FLAGS = + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT; + private final boolean recurringBackup; public ExportBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { @@ -99,9 +107,12 @@ public class ExportBackupWorker extends Worker { | NoSuchProviderException e) { Log.d(Config.LOGTAG, "could not create backup", e); return Result.failure(); + } finally { + getApplicationContext() + .getSystemService(NotificationManager.class) + .cancel(NOTIFICATION_ID); } Log.d(Config.LOGTAG, "done creating " + files.size() + " backup files"); - getApplicationContext().getSystemService(NotificationManager.class).cancel(NOTIFICATION_ID); if (files.isEmpty() || recurringBackup) { return Result.success(); } @@ -113,13 +124,7 @@ public class ExportBackupWorker extends Worker { @Override public ForegroundInfo getForegroundInfo() { Log.d(Config.LOGTAG, "getForegroundInfo()"); - final var context = getApplicationContext(); - final NotificationCompat.Builder notification = - new NotificationCompat.Builder(context, "backup"); - notification - .setContentTitle(context.getString(R.string.notification_create_backup_title)) - .setSmallIcon(R.drawable.ic_archive_24dp) - .setProgress(1, 0, false); + final NotificationCompat.Builder notification = getNotification(); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { return new ForegroundInfo( NOTIFICATION_ID, @@ -144,10 +149,13 @@ public class ExportBackupWorker extends Worker { int count = 0; final int max = accounts.size(); - final SecureRandom secureRandom = new SecureRandom(); - final List files = new ArrayList<>(); + final ImmutableList.Builder files = new ImmutableList.Builder<>(); Log.d(Config.LOGTAG, "starting backup for " + max + " accounts"); for (final Account account : accounts) { + if (isStopped()) { + Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have"); + return files.build(); + } final String password = account.getPassword(); if (Strings.nullToEmpty(password).trim().isEmpty()) { Log.d( @@ -155,84 +163,140 @@ public class ExportBackupWorker extends Worker { String.format( "skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid())); + count++; continue; } - Log.d( - Config.LOGTAG, - String.format( - "exporting data for account %s (%s)", - account.getJid().asBareJid(), account.getUuid())); - final byte[] IV = new byte[12]; - final byte[] salt = new byte[16]; - secureRandom.nextBytes(IV); - secureRandom.nextBytes(salt); - final BackupFileHeader backupFileHeader = - new BackupFileHeader( - context.getString(R.string.app_name), - account.getJid(), - System.currentTimeMillis(), - IV, - salt); - final NotificationCompat.Builder notification = - new NotificationCompat.Builder(context, "backup"); - notification - .setContentTitle(context.getString(R.string.notification_create_backup_title)) - .setSmallIcon(R.drawable.ic_archive_24dp) - .setProgress(1, 0, false); - final Progress progress = new Progress(notification, max, count); final String filename = String.format( "%s.%s.ceb", account.getJid().asBareJid().toEscapedString(), DATE_FORMAT.format(new Date())); final File file = new File(FileBackend.getBackupDirectory(context), filename); + try { + export(database, account, password, file, max, count); + } catch (final WorkStoppedException e) { + if (file.delete()) { + Log.d( + Config.LOGTAG, + "deleted in progress backup file " + file.getAbsolutePath()); + } + Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have"); + return files.build(); + } files.add(file); - final File directory = file.getParentFile(); - if (directory != null && directory.mkdirs()) { - Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); - } - final FileOutputStream fileOutputStream = new FileOutputStream(file); - final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); - backupFileHeader.write(dataOutputStream); - dataOutputStream.flush(); - - final Cipher cipher = - Compatibility.twentyEight() - ? Cipher.getInstance(CIPHERMODE) - : Cipher.getInstance(CIPHERMODE, PROVIDER); - final byte[] key = getKey(password, salt); - SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(IV); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); - CipherOutputStream cipherOutputStream = - new CipherOutputStream(fileOutputStream, cipher); - - final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); - final JsonWriter jsonWriter = - new JsonWriter( - new OutputStreamWriter(gzipOutputStream, StandardCharsets.UTF_8)); - jsonWriter.beginArray(); - final SQLiteDatabase db = database.getReadableDatabase(); - final String uuid = account.getUuid(); - accountExport(db, uuid, jsonWriter); - simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, jsonWriter); - messageExport(db, uuid, jsonWriter, progress); - for (final String table : - Arrays.asList( - SQLiteAxolotlStore.PREKEY_TABLENAME, - SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { - simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, jsonWriter); - } - jsonWriter.endArray(); - jsonWriter.flush(); - jsonWriter.close(); - mediaScannerScanFile(file); - Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); count++; } - return files; + return files.build(); + } + + private void export( + final DatabaseBackend database, + final Account account, + final String password, + final File file, + final int max, + final int count) + throws IOException, + InvalidKeySpecException, + InvalidAlgorithmParameterException, + InvalidKeyException, + NoSuchPaddingException, + NoSuchAlgorithmException, + NoSuchProviderException, + WorkStoppedException { + final var context = getApplicationContext(); + final SecureRandom secureRandom = new SecureRandom(); + Log.d( + Config.LOGTAG, + String.format( + "exporting data for account %s (%s)", + account.getJid().asBareJid(), account.getUuid())); + final byte[] IV = new byte[12]; + final byte[] salt = new byte[16]; + secureRandom.nextBytes(IV); + secureRandom.nextBytes(salt); + final BackupFileHeader backupFileHeader = + new BackupFileHeader( + context.getString(R.string.app_name), + account.getJid(), + System.currentTimeMillis(), + IV, + salt); + final var notification = getNotification(); + if (!recurringBackup) { + final var cancel = new Intent(context, WorkManagerEventReceiver.class); + cancel.setAction(WorkManagerEventReceiver.ACTION_STOP_BACKUP); + final var cancelPendingIntent = + PendingIntent.getBroadcast(context, 197, cancel, PENDING_INTENT_FLAGS); + notification.addAction( + new NotificationCompat.Action.Builder( + R.drawable.ic_cancel_24dp, + context.getString(R.string.cancel), + cancelPendingIntent) + .build()); + } + final Progress progress = new Progress(notification, max, count); + final File directory = file.getParentFile(); + if (directory != null && directory.mkdirs()) { + Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); + } + final FileOutputStream fileOutputStream = new FileOutputStream(file); + final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); + backupFileHeader.write(dataOutputStream); + dataOutputStream.flush(); + + final Cipher cipher = + Compatibility.twentyEight() + ? Cipher.getInstance(CIPHERMODE) + : Cipher.getInstance(CIPHERMODE, PROVIDER); + final byte[] key = getKey(password, salt); + SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(IV); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); + CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); + + final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); + final JsonWriter jsonWriter = + new JsonWriter(new OutputStreamWriter(gzipOutputStream, StandardCharsets.UTF_8)); + jsonWriter.beginArray(); + final SQLiteDatabase db = database.getReadableDatabase(); + final String uuid = account.getUuid(); + accountExport(db, uuid, jsonWriter); + simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, jsonWriter); + messageExport(db, uuid, jsonWriter, progress); + for (final String table : + Arrays.asList( + SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { + throwIfWorkStopped(); + simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, jsonWriter); + } + jsonWriter.endArray(); + jsonWriter.flush(); + jsonWriter.close(); + mediaScannerScanFile(file); + Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); + } + + private NotificationCompat.Builder getNotification() { + final var context = getApplicationContext(); + final NotificationCompat.Builder notification = + new NotificationCompat.Builder(context, "backup"); + notification + .setContentTitle(context.getString(R.string.notification_create_backup_title)) + .setSmallIcon(R.drawable.ic_archive_24dp) + .setProgress(1, 0, false); + notification.setOngoing(true); + notification.setLocalOnly(true); + return notification; + } + + private void throwIfWorkStopped() throws WorkStoppedException { + if (isStopped()) { + throw new WorkStoppedException(); + } } private void mediaScannerScanFile(final File file) { @@ -313,7 +377,7 @@ public class ExportBackupWorker extends Worker { final String uuid, final JsonWriter writer, final Progress progress) - throws IOException { + throws IOException, WorkStoppedException { final var notificationManager = getApplicationContext().getSystemService(NotificationManager.class); try (final Cursor cursor = @@ -322,9 +386,11 @@ public class ExportBackupWorker extends Worker { new String[] {uuid})) { final int size = cursor != null ? cursor.getCount() : 0; Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid); + long lastUpdate = 0; int i = 0; - int p = 0; + int p = Integer.MIN_VALUE; while (cursor != null && cursor.moveToNext()) { + throwIfWorkStopped(); writer.beginObject(); writer.name("table"); writer.value(Message.TABLENAME); @@ -339,8 +405,9 @@ public class ExportBackupWorker extends Worker { writer.endObject(); writer.endObject(); final int percentage = i * 100 / size; - if (p < percentage) { + if (p < percentage && (SystemClock.elapsedRealtime() - lastUpdate) > 2_000) { p = percentage; + lastUpdate = SystemClock.elapsedRealtime(); notificationManager.notify(NOTIFICATION_ID, progress.build(p)); } i++; @@ -377,13 +444,7 @@ public class ExportBackupWorker extends Worker { final Intent chooser = Intent.createChooser(intent, context.getString(R.string.share_backup_files)); final var shareFilesIntent = - PendingIntent.getActivity( - context, - 190, - chooser, - s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent.getActivity(context, 190, chooser, PENDING_INTENT_FLAGS); NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, "backup"); mBuilder.setContentTitle(context.getString(R.string.notification_backup_created_title)) @@ -418,14 +479,7 @@ public class ExportBackupWorker extends Worker { for (final Intent intent : getPossibleFileOpenIntents(context, path)) { if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null) { return Optional.of( - PendingIntent.getActivity( - context, - 189, - intent, - s() - ? PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getActivity(context, 189, intent, PENDING_INTENT_FLAGS)); } } return Optional.absent(); @@ -474,4 +528,6 @@ public class ExportBackupWorker extends Worker { return notification.build(); } } + + private static class WorkStoppedException extends Exception {} }