add ability to cancel in-progress one-off backup
This commit is contained in:
parent
45b9c4dcc9
commit
5853f57f0a
|
@ -143,7 +143,11 @@
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".services.EventReceiver"
|
android:name=".receiver.WorkManagerEventReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.SystemEventReceiver"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
@ -157,7 +161,7 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".services.UnifiedPushDistributor"
|
android:name=".receiver.UnifiedPushDistributor"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.siacs.conversations.services;
|
package eu.siacs.conversations.receiver;
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -10,9 +10,10 @@ import android.util.Log;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
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 SETTING_ENABLED_ACCOUNTS = "enabled_accounts";
|
||||||
public static final String EXTRA_NEEDS_FOREGROUND_SERVICE = "needs_foreground_service";
|
public static final String EXTRA_NEEDS_FOREGROUND_SERVICE = "needs_foreground_service";
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.siacs.conversations.services;
|
package eu.siacs.conversations.receiver;
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
|
@ -25,6 +25,7 @@ import java.util.List;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
|
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
|
||||||
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
import eu.siacs.conversations.utils.Compatibility;
|
||||||
|
|
||||||
public class UnifiedPushDistributor extends BroadcastReceiver {
|
public class UnifiedPushDistributor extends BroadcastReceiver {
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import java.util.List;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
|
import eu.siacs.conversations.receiver.SystemEventReceiver;
|
||||||
import eu.siacs.conversations.ui.ConversationsActivity;
|
import eu.siacs.conversations.ui.ConversationsActivity;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
import eu.siacs.conversations.utils.Compatibility;
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ public class ContactChooserTargetService extends ChooserTargetService implements
|
||||||
@Override
|
@Override
|
||||||
public List<ChooserTarget> onGetChooserTargets(
|
public List<ChooserTarget> onGetChooserTargets(
|
||||||
final ComponentName targetActivityName, final IntentFilter matchedFilter) {
|
final ComponentName targetActivityName, final IntentFilter matchedFilter) {
|
||||||
if (!EventReceiver.hasEnabledAccounts(this)) {
|
if (!SystemEventReceiver.hasEnabledAccounts(this)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
final Intent intent = new Intent(this, XmppConnectionService.class);
|
final Intent intent = new Intent(this, XmppConnectionService.class);
|
||||||
|
|
|
@ -28,6 +28,7 @@ import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.parser.AbstractParser;
|
import eu.siacs.conversations.parser.AbstractParser;
|
||||||
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
|
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
|
||||||
|
import eu.siacs.conversations.receiver.UnifiedPushDistributor;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
|
@ -128,6 +128,7 @@ import eu.siacs.conversations.parser.PresenceParser;
|
||||||
import eu.siacs.conversations.persistance.DatabaseBackend;
|
import eu.siacs.conversations.persistance.DatabaseBackend;
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
|
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
|
||||||
|
import eu.siacs.conversations.receiver.SystemEventReceiver;
|
||||||
import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
|
import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
|
||||||
import eu.siacs.conversations.ui.ConversationsActivity;
|
import eu.siacs.conversations.ui.ConversationsActivity;
|
||||||
import eu.siacs.conversations.ui.RtpSessionActivity;
|
import eu.siacs.conversations.ui.RtpSessionActivity;
|
||||||
|
@ -678,7 +679,7 @@ public class XmppConnectionService extends Service {
|
||||||
@Override
|
@Override
|
||||||
public int onStartCommand(final Intent intent, int flags, int startId) {
|
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||||
final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
|
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) {
|
if (needsForegroundService) {
|
||||||
Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")");
|
Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")");
|
||||||
toggleForegroundService(true);
|
toggleForegroundService(true);
|
||||||
|
@ -1286,7 +1287,7 @@ public class XmppConnectionService extends Service {
|
||||||
this.accounts = databaseBackend.getAccounts();
|
this.accounts = databaseBackend.getAccounts();
|
||||||
final SharedPreferences.Editor editor = getPreferences().edit();
|
final SharedPreferences.Editor editor = getPreferences().edit();
|
||||||
final boolean hasEnabledAccounts = hasEnabledAccounts();
|
final boolean hasEnabledAccounts = hasEnabledAccounts();
|
||||||
editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
|
editor.putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
|
||||||
editor.apply();
|
editor.apply();
|
||||||
toggleSetProfilePictureActivity(hasEnabledAccounts);
|
toggleSetProfilePictureActivity(hasEnabledAccounts);
|
||||||
reconfigurePushDistributor();
|
reconfigurePushDistributor();
|
||||||
|
@ -1582,7 +1583,7 @@ public class XmppConnectionService extends Service {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final long triggerAtMillis = SystemClock.elapsedRealtime() + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000);
|
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);
|
intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE);
|
||||||
try {
|
try {
|
||||||
final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, s()
|
final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, s()
|
||||||
|
@ -1604,7 +1605,7 @@ public class XmppConnectionService extends Service {
|
||||||
if (alarmManager == null) {
|
if (alarmManager == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Intent intent = new Intent(this, EventReceiver.class);
|
final Intent intent = new Intent(this, SystemEventReceiver.class);
|
||||||
intent.setAction(ACTION_PING);
|
intent.setAction(ACTION_PING);
|
||||||
try {
|
try {
|
||||||
final PendingIntent pendingIntent =
|
final PendingIntent pendingIntent =
|
||||||
|
@ -1623,7 +1624,7 @@ public class XmppConnectionService extends Service {
|
||||||
if (alarmManager == null) {
|
if (alarmManager == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Intent intent = new Intent(this, EventReceiver.class);
|
final Intent intent = new Intent(this, SystemEventReceiver.class);
|
||||||
intent.setAction(ACTION_IDLE_PING);
|
intent.setAction(ACTION_IDLE_PING);
|
||||||
try {
|
try {
|
||||||
final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, s()
|
final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, s()
|
||||||
|
@ -2573,7 +2574,7 @@ public class XmppConnectionService extends Service {
|
||||||
|
|
||||||
private void syncEnabledAccountSetting() {
|
private void syncEnabledAccountSetting() {
|
||||||
final boolean hasEnabledAccounts = hasEnabledAccounts();
|
final boolean hasEnabledAccounts = hasEnabledAccounts();
|
||||||
getPreferences().edit().putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
|
getPreferences().edit().putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
|
||||||
toggleSetProfilePictureActivity(hasEnabledAccounts);
|
toggleSetProfilePictureActivity(hasEnabledAccounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class BackupSettingsFragment extends XmppPreferenceFragment {
|
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 static final String RECURRING_BACKUP = "recurring_backup";
|
||||||
|
|
||||||
private final ActivityResultLauncher<String> requestStorageForBackupLauncher =
|
private final ActivityResultLauncher<String> requestStorageForBackupLauncher =
|
||||||
|
|
|
@ -13,7 +13,7 @@ import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.services.UnifiedPushDistributor;
|
import eu.siacs.conversations.receiver.UnifiedPushDistributor;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package eu.siacs.conversations.utils;
|
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.annotation.SuppressLint;
|
||||||
import android.app.ActivityOptions;
|
import android.app.ActivityOptions;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.content.pm.ServiceInfo;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -21,6 +22,7 @@ import androidx.work.WorkerParameters;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.gson.stream.JsonWriter;
|
import com.google.gson.stream.JsonWriter;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
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.entities.Message;
|
||||||
import eu.siacs.conversations.persistance.DatabaseBackend;
|
import eu.siacs.conversations.persistance.DatabaseBackend;
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
|
import eu.siacs.conversations.receiver.WorkManagerEventReceiver;
|
||||||
import eu.siacs.conversations.utils.BackupFileHeader;
|
import eu.siacs.conversations.utils.BackupFileHeader;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
import eu.siacs.conversations.utils.Compatibility;
|
||||||
|
|
||||||
|
@ -65,7 +68,7 @@ import javax.crypto.spec.SecretKeySpec;
|
||||||
public class ExportBackupWorker extends Worker {
|
public class ExportBackupWorker extends Worker {
|
||||||
|
|
||||||
private static final SimpleDateFormat DATE_FORMAT =
|
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 KEYTYPE = "AES";
|
||||||
public static final String CIPHERMODE = "AES/GCM/NoPadding";
|
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 NOTIFICATION_ID = 19;
|
||||||
private static final int BACKUP_CREATED_NOTIFICATION_ID = 23;
|
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;
|
private final boolean recurringBackup;
|
||||||
|
|
||||||
public ExportBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
public ExportBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||||
|
@ -99,9 +107,12 @@ public class ExportBackupWorker extends Worker {
|
||||||
| NoSuchProviderException e) {
|
| NoSuchProviderException e) {
|
||||||
Log.d(Config.LOGTAG, "could not create backup", e);
|
Log.d(Config.LOGTAG, "could not create backup", e);
|
||||||
return Result.failure();
|
return Result.failure();
|
||||||
|
} finally {
|
||||||
|
getApplicationContext()
|
||||||
|
.getSystemService(NotificationManager.class)
|
||||||
|
.cancel(NOTIFICATION_ID);
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG, "done creating " + files.size() + " backup files");
|
Log.d(Config.LOGTAG, "done creating " + files.size() + " backup files");
|
||||||
getApplicationContext().getSystemService(NotificationManager.class).cancel(NOTIFICATION_ID);
|
|
||||||
if (files.isEmpty() || recurringBackup) {
|
if (files.isEmpty() || recurringBackup) {
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
@ -113,13 +124,7 @@ public class ExportBackupWorker extends Worker {
|
||||||
@Override
|
@Override
|
||||||
public ForegroundInfo getForegroundInfo() {
|
public ForegroundInfo getForegroundInfo() {
|
||||||
Log.d(Config.LOGTAG, "getForegroundInfo()");
|
Log.d(Config.LOGTAG, "getForegroundInfo()");
|
||||||
final var context = getApplicationContext();
|
final NotificationCompat.Builder notification = getNotification();
|
||||||
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);
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||||
return new ForegroundInfo(
|
return new ForegroundInfo(
|
||||||
NOTIFICATION_ID,
|
NOTIFICATION_ID,
|
||||||
|
@ -144,10 +149,13 @@ public class ExportBackupWorker extends Worker {
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
final int max = accounts.size();
|
final int max = accounts.size();
|
||||||
final SecureRandom secureRandom = new SecureRandom();
|
final ImmutableList.Builder<File> files = new ImmutableList.Builder<>();
|
||||||
final List<File> files = new ArrayList<>();
|
|
||||||
Log.d(Config.LOGTAG, "starting backup for " + max + " accounts");
|
Log.d(Config.LOGTAG, "starting backup for " + max + " accounts");
|
||||||
for (final Account account : 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();
|
final String password = account.getPassword();
|
||||||
if (Strings.nullToEmpty(password).trim().isEmpty()) {
|
if (Strings.nullToEmpty(password).trim().isEmpty()) {
|
||||||
Log.d(
|
Log.d(
|
||||||
|
@ -155,84 +163,140 @@ public class ExportBackupWorker extends Worker {
|
||||||
String.format(
|
String.format(
|
||||||
"skipping backup for %s because password is empty. unable to encrypt",
|
"skipping backup for %s because password is empty. unable to encrypt",
|
||||||
account.getJid().asBareJid()));
|
account.getJid().asBareJid()));
|
||||||
|
count++;
|
||||||
continue;
|
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 =
|
final String filename =
|
||||||
String.format(
|
String.format(
|
||||||
"%s.%s.ceb",
|
"%s.%s.ceb",
|
||||||
account.getJid().asBareJid().toEscapedString(),
|
account.getJid().asBareJid().toEscapedString(),
|
||||||
DATE_FORMAT.format(new Date()));
|
DATE_FORMAT.format(new Date()));
|
||||||
final File file = new File(FileBackend.getBackupDirectory(context), filename);
|
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);
|
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++;
|
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) {
|
private void mediaScannerScanFile(final File file) {
|
||||||
|
@ -313,7 +377,7 @@ public class ExportBackupWorker extends Worker {
|
||||||
final String uuid,
|
final String uuid,
|
||||||
final JsonWriter writer,
|
final JsonWriter writer,
|
||||||
final Progress progress)
|
final Progress progress)
|
||||||
throws IOException {
|
throws IOException, WorkStoppedException {
|
||||||
final var notificationManager =
|
final var notificationManager =
|
||||||
getApplicationContext().getSystemService(NotificationManager.class);
|
getApplicationContext().getSystemService(NotificationManager.class);
|
||||||
try (final Cursor cursor =
|
try (final Cursor cursor =
|
||||||
|
@ -322,9 +386,11 @@ public class ExportBackupWorker extends Worker {
|
||||||
new String[] {uuid})) {
|
new String[] {uuid})) {
|
||||||
final int size = cursor != null ? cursor.getCount() : 0;
|
final int size = cursor != null ? cursor.getCount() : 0;
|
||||||
Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid);
|
Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid);
|
||||||
|
long lastUpdate = 0;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int p = 0;
|
int p = Integer.MIN_VALUE;
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
throwIfWorkStopped();
|
||||||
writer.beginObject();
|
writer.beginObject();
|
||||||
writer.name("table");
|
writer.name("table");
|
||||||
writer.value(Message.TABLENAME);
|
writer.value(Message.TABLENAME);
|
||||||
|
@ -339,8 +405,9 @@ public class ExportBackupWorker extends Worker {
|
||||||
writer.endObject();
|
writer.endObject();
|
||||||
writer.endObject();
|
writer.endObject();
|
||||||
final int percentage = i * 100 / size;
|
final int percentage = i * 100 / size;
|
||||||
if (p < percentage) {
|
if (p < percentage && (SystemClock.elapsedRealtime() - lastUpdate) > 2_000) {
|
||||||
p = percentage;
|
p = percentage;
|
||||||
|
lastUpdate = SystemClock.elapsedRealtime();
|
||||||
notificationManager.notify(NOTIFICATION_ID, progress.build(p));
|
notificationManager.notify(NOTIFICATION_ID, progress.build(p));
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
|
@ -377,13 +444,7 @@ public class ExportBackupWorker extends Worker {
|
||||||
final Intent chooser =
|
final Intent chooser =
|
||||||
Intent.createChooser(intent, context.getString(R.string.share_backup_files));
|
Intent.createChooser(intent, context.getString(R.string.share_backup_files));
|
||||||
final var shareFilesIntent =
|
final var shareFilesIntent =
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(context, 190, chooser, PENDING_INTENT_FLAGS);
|
||||||
context,
|
|
||||||
190,
|
|
||||||
chooser,
|
|
||||||
s()
|
|
||||||
? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
: PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, "backup");
|
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, "backup");
|
||||||
mBuilder.setContentTitle(context.getString(R.string.notification_backup_created_title))
|
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)) {
|
for (final Intent intent : getPossibleFileOpenIntents(context, path)) {
|
||||||
if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null) {
|
if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null) {
|
||||||
return Optional.of(
|
return Optional.of(
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(context, 189, intent, PENDING_INTENT_FLAGS));
|
||||||
context,
|
|
||||||
189,
|
|
||||||
intent,
|
|
||||||
s()
|
|
||||||
? PendingIntent.FLAG_IMMUTABLE
|
|
||||||
| PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
: PendingIntent.FLAG_UPDATE_CURRENT));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
|
@ -474,4 +528,6 @@ public class ExportBackupWorker extends Worker {
|
||||||
return notification.build();
|
return notification.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class WorkStoppedException extends Exception {}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue