diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index d715ea539..7b034a255 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -21,7 +21,9 @@ import java.io.PrintWriter; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.zip.GZIPOutputStream; @@ -50,6 +52,8 @@ public class ExportBackupService extends Service { public static final String CIPHERMODE = "AES/GCM/NoPadding"; public static final String PROVIDER = "BC"; + public static final String MIME_TYPE = "application/vnd.conversations.backup"; + private static final int NOTIFICATION_ID = 19; private static final int PAGE_SIZE = 20; private static AtomicBoolean running = new AtomicBoolean(false); @@ -213,11 +217,19 @@ public class ExportBackupService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { if (running.compareAndSet(false, true)) { new Thread(() -> { - final boolean success = export(); + boolean success; + List files; + try { + files = export(); + success = true; + } catch (Exception e) { + success = false; + files = Collections.emptyList(); + } stopForeground(true); running.set(false); if (success) { - notifySuccess(); + notifySuccess(files); } stopSelf(); }).start(); @@ -250,81 +262,97 @@ public class ExportBackupService extends Service { } } - private boolean export() { + private List export() throws Exception { NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); mBuilder.setContentTitle(getString(R.string.notification_create_backup_title)) .setSmallIcon(R.drawable.ic_archive_white_24dp) .setProgress(1, 0, false); startForeground(NOTIFICATION_ID, mBuilder.build()); - try { - int count = 0; - final int max = this.mAccounts.size(); - final SecureRandom secureRandom = new SecureRandom(); - for (Account account : this.mAccounts) { - final byte[] IV = new byte[12]; - final byte[] salt = new byte[16]; - secureRandom.nextBytes(IV); - secureRandom.nextBytes(salt); - final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt); - final Progress progress = new Progress(mBuilder, max, count); - final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb"); - if (file.getParentFile().mkdirs()) { - Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().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); - byte[] key = getKey(account.getPassword(), salt); - Log.d(Config.LOGTAG, backupFileHeader.toString()); - SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(IV); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); - CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); - - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); - PrintWriter writer = new PrintWriter(gzipOutputStream); - SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase(); - final String uuid = account.getUuid(); - accountExport(db, uuid, writer); - simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer); - messageExport(db, uuid, writer, progress); - for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { - simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer); - } - writer.flush(); - writer.close(); - Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); - count++; + int count = 0; + final int max = this.mAccounts.size(); + final SecureRandom secureRandom = new SecureRandom(); + final List files = new ArrayList<>(); + for (Account account : this.mAccounts) { + final byte[] IV = new byte[12]; + final byte[] salt = new byte[16]; + secureRandom.nextBytes(IV); + secureRandom.nextBytes(salt); + final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt); + final Progress progress = new Progress(mBuilder, max, count); + final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb"); + files.add(file); + if (file.getParentFile().mkdirs()) { + Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath()); } - return true; - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to create backup ", e); - return false; + 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); + byte[] key = getKey(account.getPassword(), salt); + Log.d(Config.LOGTAG, backupFileHeader.toString()); + SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(IV); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); + CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); + + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); + PrintWriter writer = new PrintWriter(gzipOutputStream); + SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase(); + final String uuid = account.getUuid(); + accountExport(db, uuid, writer); + simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer); + messageExport(db, uuid, writer, progress); + for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { + simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer); + } + writer.flush(); + writer.close(); + Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); + count++; } + return files; } - private void notifySuccess() { + private void notifySuccess(List files) { final String path = FileBackend.getBackupDirectory(this); - PendingIntent pendingIntent = null; + PendingIntent openFolderIntent = null; for (Intent intent : getPossibleFileOpenIntents(this, path)) { if (intent.resolveActivityInfo(getPackageManager(), 0) != null) { - pendingIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT); + openFolderIntent = PendingIntent.getActivity(this, 189, intent, PendingIntent.FLAG_UPDATE_CURRENT); break; } } + PendingIntent shareFilesIntent = null; + if (files.size() > 0) { + final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + ArrayList uris = new ArrayList<>(); + for(File file : files) { + uris.add(FileBackend.getUriForFile(this, file)); + } + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setType(MIME_TYPE); + final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files)); + shareFilesIntent = PendingIntent.getActivity(this,190, chooser, PendingIntent.FLAG_UPDATE_CURRENT); + } + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); mBuilder.setContentTitle(getString(R.string.notification_backup_created_title)) .setContentText(getString(R.string.notification_backup_created_subtitle, path)) .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this)))) .setAutoCancel(true) - .setContentIntent(pendingIntent) + .setContentIntent(openFolderIntent) .setSmallIcon(R.drawable.ic_archive_white_24dp); + + if (shareFilesIntent != null) { + mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent); + } + notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index ad5ddce89..3ea451653 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -94,6 +94,7 @@ public class ConversationAdapter extends RecyclerView.Adapter + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 419928a80..90866434e 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -868,4 +868,7 @@ This looks like a domain address Add anyway This looks like a channel address + Share backup files + Conversations backup + Event diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index e7bc3cd31..f4c870603 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -74,6 +74,7 @@ @drawable/ic_event_black_48dp @drawable/ic_archive_black_48dp @drawable/ic_book_black_48dp + @drawable/ic_backup_black_48dp @drawable/ic_help_black_48dp @drawable/ic_group_add_white_24dp @@ -187,6 +188,7 @@ @drawable/ic_event_white_48dp @drawable/ic_archive_white_48dp @drawable/ic_book_white_48dp + @drawable/ic_backup_white_48dp @drawable/ic_help_white_48dp @drawable/ic_group_add_white_24dp