package org.thoughtcrime.securesms.jobs; import android.Manifest; import androidx.annotation.NonNull; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.backup.BackupPassphrase; import org.thoughtcrime.securesms.backup.FullBackupExporter; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.NoExternalStorageException; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraint; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.service.GenericForegroundService; import org.thoughtcrime.securesms.service.NotificationController; import org.thoughtcrime.securesms.util.BackupUtil; import org.thoughtcrime.securesms.util.StorageUtil; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; public final class LocalBackupJob extends BaseJob { public static final String KEY = "LocalBackupJob"; private static final String TAG = Log.tag(LocalBackupJob.class); private static final String QUEUE = "__LOCAL_BACKUP__"; public static final String TEMP_BACKUP_FILE_PREFIX = ".backup"; public static final String TEMP_BACKUP_FILE_SUFFIX = ".tmp"; public static void enqueue(boolean force) { JobManager jobManager = ApplicationDependencies.getJobManager(); Parameters.Builder parameters = new Parameters.Builder() .setQueue(QUEUE) .setMaxInstances(1) .setMaxAttempts(3); if (force) { jobManager.cancelAllInQueue(QUEUE); } else { parameters.addConstraint(ChargingConstraint.KEY); } jobManager.add(new LocalBackupJob(parameters.build())); } private LocalBackupJob(@NonNull Job.Parameters parameters) { super(parameters); } @Override public @NonNull Data serialize() { return Data.EMPTY; } @Override public @NonNull String getFactoryKey() { return KEY; } @Override public void onRun() throws NoExternalStorageException, IOException { Log.i(TAG, "Executing backup job..."); if (!Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { throw new IOException("No external storage permission!"); } try (NotificationController notification = GenericForegroundService.startForegroundTask(context, context.getString(R.string.LocalBackupJob_creating_backup), NotificationChannels.BACKUPS, R.drawable.ic_signal_backup)) { notification.setIndeterminateProgress(); String backupPassword = BackupPassphrase.get(context); File backupDirectory = StorageUtil.getBackupDirectory(); String timestamp = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(new Date()); String fileName = String.format("signal-%s.backup", timestamp); File backupFile = new File(backupDirectory, fileName); deleteOldTemporaryBackups(backupDirectory); if (backupFile.exists()) { throw new IOException("Backup file already exists?"); } if (backupPassword == null) { throw new IOException("Backup password is null"); } File tempFile = File.createTempFile(TEMP_BACKUP_FILE_PREFIX, TEMP_BACKUP_FILE_SUFFIX, backupDirectory); try { FullBackupExporter.export(context, AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), DatabaseFactory.getBackupDatabase(context), tempFile, backupPassword); if (!tempFile.renameTo(backupFile)) { Log.w(TAG, "Failed to rename temp file"); throw new IOException("Renaming temporary backup file failed!"); } } finally { if (tempFile.exists()) { if (tempFile.delete()) { Log.w(TAG, "Backup failed. Deleted temp file"); } else { Log.w(TAG, "Backup failed. Failed to delete temp file " + tempFile); } } } BackupUtil.deleteOldBackups(); } } private static void deleteOldTemporaryBackups(@NonNull File backupDirectory) { for (File file : backupDirectory.listFiles()) { if (file.isFile()) { String name = file.getName(); if (name.startsWith(TEMP_BACKUP_FILE_PREFIX) && name.endsWith(TEMP_BACKUP_FILE_SUFFIX)) { if (file.delete()) { Log.w(TAG, "Deleted old temporary backup file"); } else { Log.w(TAG, "Could not delete old temporary backup file"); } } } } } @Override public boolean onShouldRetry(@NonNull Exception e) { return false; } @Override public void onFailure() { } public static class Factory implements Job.Factory { @Override public @NonNull LocalBackupJob create(@NonNull Parameters parameters, @NonNull Data data) { return new LocalBackupJob(parameters); } } }