@@ -6,6 +6,7 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@@ -22,6 +23,8 @@ import androidx.core.app.NotificationManagerCompat;
import com.google.common.base.Charsets;
import com.google.common.base.Stopwatch;
import com.google.common.io.CountingInputStream;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.io.CipherInputStream;
@@ -40,6 +43,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -53,6 +57,10 @@ import javax.crypto.BadPaddingException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
+import eu.siacs.conversations.entities.Account;
+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.ui.ManageAccountActivity;
@@ -65,25 +73,28 @@ public class ImportBackupService extends Service {
private static final int NOTIFICATION_ID = 21;
private static final AtomicBoolean running = new AtomicBoolean(false);
private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
- private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
- private final Set<OnBackupProcessed> mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
+ private final SerialSingleThreadExecutor executor =
+ new SerialSingleThreadExecutor(getClass().getSimpleName());
+ private final Set<OnBackupProcessed> mOnBackupProcessedListeners =
+ Collections.newSetFromMap(new WeakHashMap<>());
private DatabaseBackend mDatabaseBackend;
private NotificationManager notificationManager;
- private static int count(String input, char c) {
- int count = 0;
- for (char aChar : input.toCharArray()) {
- if (aChar == c) {
- ++count;
- }
- }
- return count;
- }
+ private static final Collection<String> TABLE_ALLOW_LIST =
+ Arrays.asList(
+ Account.TABLENAME,
+ Conversation.TABLENAME,
+ Message.TABLENAME,
+ SQLiteAxolotlStore.PREKEY_TABLENAME,
+ SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ SQLiteAxolotlStore.SESSION_TABLENAME,
+ SQLiteAxolotlStore.IDENTITIES_TABLENAME);
@Override
public void onCreate() {
mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
- notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager =
+ (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
@Override
@@ -105,16 +116,17 @@ public class ImportBackupService extends Service {
return START_NOT_STICKY;
}
if (running.compareAndSet(false, true)) {
- executor.execute(() -> {
- startForegroundService();
- final boolean success = importBackup(uri, password);
- stopForeground(true);
- running.set(false);
- if (success) {
- notifySuccess();
- }
- stopSelf();
- });
+ executor.execute(
+ () -> {
+ startForegroundService();
+ final boolean success = importBackup(uri, password);
+ stopForeground(true);
+ running.set(false);
+ if (success) {
+ notifySuccess();
+ }
+ stopSelf();
+ });
} else {
Log.d(Config.LOGTAG, "backup already running");
}
@@ -126,42 +138,59 @@ public class ImportBackupService extends Service {
}
public void loadBackupFiles(final OnBackupFilesLoaded onBackupFilesLoaded) {
- executor.execute(() -> {
- final List<Jid> accounts = mDatabaseBackend.getAccountJids(false);
- final ArrayList<BackupFile> backupFiles = new ArrayList<>();
- final Set<String> apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name)));
- final List<File> directories = new ArrayList<>();
- for (final String app : apps) {
- directories.add(FileBackend.getLegacyBackupDirectory(app));
- }
- directories.add(FileBackend.getBackupDirectory(this));
- for (final File directory : directories) {
- if (!directory.exists() || !directory.isDirectory()) {
- Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
- continue;
- }
- final File[] files = directory.listFiles();
- if (files == null) {
- continue;
- }
- for (final File file : files) {
- if (file.isFile() && file.getName().endsWith(".ceb")) {
- try {
- final BackupFile backupFile = BackupFile.read(file);
- if (accounts.contains(backupFile.getHeader().getJid())) {
- Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid());
- } else {
- backupFiles.add(backupFile);
+ executor.execute(
+ () -> {
+ final List<Jid> accounts = mDatabaseBackend.getAccountJids(false);
+ final ArrayList<BackupFile> backupFiles = new ArrayList<>();
+ final Set<String> apps =
+ new HashSet<>(
+ Arrays.asList(
+ "Conversations",
+ "Quicksy",
+ getString(R.string.app_name)));
+ final List<File> directories = new ArrayList<>();
+ for (final String app : apps) {
+ directories.add(FileBackend.getLegacyBackupDirectory(app));
+ }
+ directories.add(FileBackend.getBackupDirectory(this));
+ for (final File directory : directories) {
+ if (!directory.exists() || !directory.isDirectory()) {
+ Log.d(
+ Config.LOGTAG,
+ "directory not found: " + directory.getAbsolutePath());
+ continue;
+ }
+ final File[] files = directory.listFiles();
+ if (files == null) {
+ continue;
+ }
+ for (final File file : files) {
+ if (file.isFile() && file.getName().endsWith(".ceb")) {
+ try {
+ final BackupFile backupFile = BackupFile.read(file);
+ if (accounts.contains(backupFile.getHeader().getJid())) {
+ Log.d(
+ Config.LOGTAG,
+ "skipping backup for "
+ + backupFile.getHeader().getJid());
+ } else {
+ backupFiles.add(backupFile);
+ }
+ } catch (IOException | IllegalArgumentException e) {
+ Log.d(Config.LOGTAG, "unable to read backup file ", e);
+ }
}
- } catch (IOException | IllegalArgumentException e) {
- Log.d(Config.LOGTAG, "unable to read backup file ", e);
}
}
- }
- }
- Collections.sort(backupFiles, (a, b) -> a.header.getJid().toString().compareTo(b.header.getJid().toString()));
- onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
- });
+ Collections.sort(
+ backupFiles,
+ (a, b) ->
+ a.header
+ .getJid()
+ .toString()
+ .compareTo(b.header.getJid().toString()));
+ onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
+ });
}
private void startForegroundService() {
@@ -180,14 +209,16 @@ public class ImportBackupService extends Service {
}
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
try {
- notificationManager.notify(NOTIFICATION_ID, createImportBackupNotification(max, progress));
+ notificationManager.notify(
+ NOTIFICATION_ID, createImportBackupNotification(max, progress));
} catch (final RuntimeException e) {
Log.d(Config.LOGTAG, "unable to make notification", e);
}
}
private Notification createImportBackupNotification(final int max, final int progress) {
- NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ NotificationCompat.Builder mBuilder =
+ new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.restoring_backup))
.setSmallIcon(R.drawable.ic_unarchive_white_24dp)
.setProgress(max, progress, max == 1 && progress == 0);
@@ -212,7 +243,9 @@ public class ImportBackupService extends Service {
fileSize = 0;
} else {
returnCursor.moveToFirst();
- fileSize = returnCursor.getLong(returnCursor.getColumnIndex(OpenableColumns.SIZE));
+ fileSize =
+ returnCursor.getLong(
+ returnCursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
returnCursor.close();
}
inputStream = getContentResolver().openInputStream(uri);
@@ -242,40 +275,46 @@ public class ImportBackupService extends Service {
final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
- cipher.init(false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv()));
- final CipherInputStream cipherInputStream = new CipherInputStream(countingInputStream, cipher);
+ cipher.init(
+ false,
+ new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv()));
+ final CipherInputStream cipherInputStream =
+ new CipherInputStream(countingInputStream, cipher);
final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
- final BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8));
+ final BufferedReader reader =
+ new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8));
+ final JsonReader jsonReader = new JsonReader(reader);
+ if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
+ jsonReader.beginArray();
+ } else {
+ throw new IllegalStateException("Backup file did not begin with array");
+ }
db.beginTransaction();
- String line;
- StringBuilder multiLineQuery = null;
- while ((line = reader.readLine()) != null) {
- int count = count(line, '\'');
- if (multiLineQuery != null) {
- multiLineQuery.append('\n');
- multiLineQuery.append(line);
- if (count % 2 == 1) {
- db.execSQL(multiLineQuery.toString());
- multiLineQuery = null;
- updateImportBackupNotification(fileSize, countingInputStream.getCount());
- }
- } else {
- if (count % 2 == 0) {
- db.execSQL(line);
- updateImportBackupNotification(fileSize, countingInputStream.getCount());
- } else {
- multiLineQuery = new StringBuilder(line);
- }
+ while (jsonReader.hasNext()) {
+ if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
+ importRow(db, jsonReader, backupFileHeader.getJid(), password);
+ } else if (jsonReader.peek() == JsonToken.END_ARRAY) {
+ jsonReader.endArray();
+ continue;
}
+ updateImportBackupNotification(fileSize, countingInputStream.getCount());
}
db.setTransactionSuccessful();
db.endTransaction();
final Jid jid = backupFileHeader.getJid();
- final Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain().toEscapedString()});
+ final Cursor countCursor =
+ db.rawQuery(
+ "select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?",
+ new String[] {
+ jid.getEscapedLocal(), jid.getDomain().toEscapedString()
+ });
countCursor.moveToFirst();
final int count = countCursor.getInt(0);
- Log.d(Config.LOGTAG, String.format("restored %d messages in %s", count, stopwatch.stop().toString()));
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "restored %d messages in %s", count, stopwatch.stop().toString()));
countCursor.close();
stopBackgroundService();
synchronized (mOnBackupProcessedListeners) {
@@ -286,7 +325,8 @@ public class ImportBackupService extends Service {
return true;
} catch (final Exception e) {
final Throwable throwable = e.getCause();
- final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException;
+ final boolean reasonWasCrypto =
+ throwable instanceof BadPaddingException || e instanceof ZipException;
synchronized (mOnBackupProcessedListeners) {
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
if (reasonWasCrypto) {
@@ -301,14 +341,71 @@ public class ImportBackupService extends Service {
}
}
+ private void importRow(
+ final SQLiteDatabase db,
+ final JsonReader jsonReader,
+ final Jid account,
+ final String passphrase)
+ throws IOException {
+ jsonReader.beginObject();
+ final String firstParameter = jsonReader.nextName();
+ if (!firstParameter.equals("table")) {
+ throw new IllegalStateException("Expected key 'table'");
+ }
+ final String table = jsonReader.nextString();
+ if (!TABLE_ALLOW_LIST.contains(table)) {
+ throw new IOException(String.format("%s is not recognized for import", table));
+ }
+ final ContentValues contentValues = new ContentValues();
+ final String secondParameter = jsonReader.nextName();
+ if (!secondParameter.equals("values")) {
+ throw new IllegalStateException("Expected key 'values'");
+ }
+ jsonReader.beginObject();
+ while (jsonReader.peek() != JsonToken.END_OBJECT) {
+ final String name = jsonReader.nextName();
+ if (jsonReader.peek() == JsonToken.NULL) {
+ jsonReader.nextNull();
+ contentValues.putNull(name);
+ } else if (jsonReader.peek() == JsonToken.NUMBER) {
+ contentValues.put(name, jsonReader.nextLong());
+ } else {
+ contentValues.put(name, jsonReader.nextString());
+ }
+ }
+ jsonReader.endObject();
+ jsonReader.endObject();
+ if (Account.TABLENAME.equals(table)) {
+ final Jid jid =
+ Jid.of(
+ contentValues.getAsString(Account.USERNAME),
+ contentValues.getAsString(Account.SERVER),
+ null);
+ final String password = contentValues.getAsString(Account.PASSWORD);
+ if (jid.equals(account) && passphrase.equals(password)) {
+ Log.d(Config.LOGTAG, "jid and password from backup header had matching row");
+ } else {
+ throw new IOException("jid or password in table did not match backup");
+ }
+ }
+ db.insert(table, null, contentValues);
+ }
+
private void notifySuccess() {
- NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ NotificationCompat.Builder mBuilder =
+ new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title))
.setContentText(getString(R.string.notification_restored_backup_subtitle))
.setAutoCancel(true)
- .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), s()
- ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
- : PendingIntent.FLAG_UPDATE_CURRENT))
+ .setContentIntent(
+ PendingIntent.getActivity(
+ this,
+ 145,
+ new Intent(this, ManageAccountActivity.class),
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT))
.setSmallIcon(R.drawable.ic_unarchive_white_24dp);
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
}
@@ -391,4 +488,4 @@ public class ImportBackupService extends Service {
return ImportBackupService.this;
}
}
-}
+}
@@ -19,11 +19,15 @@ import androidx.core.app.NotificationCompat;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
+import com.google.gson.stream.JsonWriter;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
@@ -61,16 +65,16 @@ public class ExportBackupService extends Service {
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 final AtomicBoolean RUNNING = new AtomicBoolean(false);
private DatabaseBackend mDatabaseBackend;
private List<Account> mAccounts;
private NotificationManager notificationManager;
- private static List<Intent> getPossibleFileOpenIntents(final Context context, final String path) {
+ private static List<Intent> getPossibleFileOpenIntents(
+ final Context context, final String path) {
- //http://www.openintents.org/action/android-intent-action-view/file-directory
- //do not use 'vnd.android.document/directory' since this will trigger system file manager
+ // http://www.openintents.org/action/android-intent-action-view/file-directory
+ // do not use 'vnd.android.document/directory' since this will trigger system file manager
final Intent openIntent = new Intent(Intent.ACTION_VIEW);
openIntent.addCategory(Intent.CATEGORY_DEFAULT);
if (Compatibility.runsAndTargetsTwentyFour(context)) {
@@ -83,134 +87,95 @@ public class ExportBackupService extends Service {
final Intent amazeIntent = new Intent(Intent.ACTION_VIEW);
amazeIntent.setDataAndType(Uri.parse("com.amaze.filemanager:" + path), "resource/folder");
- //will open a file manager at root and user can navigate themselves
+ // will open a file manager at root and user can navigate themselves
final Intent systemFallBack = new Intent(Intent.ACTION_VIEW);
systemFallBack.addCategory(Intent.CATEGORY_DEFAULT);
- systemFallBack.setData(Uri.parse("content://com.android.externalstorage.documents/root/primary"));
+ systemFallBack.setData(
+ Uri.parse("content://com.android.externalstorage.documents/root/primary"));
return Arrays.asList(openIntent, amazeIntent, systemFallBack);
}
- private static void accountExport(final SQLiteDatabase db, final String uuid, final PrintWriter writer) {
- final StringBuilder builder = new StringBuilder();
- final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null);
+ private static void accountExport(
+ final SQLiteDatabase db, final String uuid, final JsonWriter writer)
+ throws IOException {
+ final Cursor accountCursor =
+ db.query(
+ Account.TABLENAME,
+ null,
+ Account.UUID + "=?",
+ new String[] {uuid},
+ null,
+ null,
+ null);
while (accountCursor != null && accountCursor.moveToNext()) {
- builder.append("INSERT INTO ").append(Account.TABLENAME).append("(");
+ writer.beginObject();
+ writer.name("table");
+ writer.value(Account.TABLENAME);
+ writer.name("values");
+ writer.beginObject();
for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
- if (i != 0) {
- builder.append(',');
- }
- builder.append(accountCursor.getColumnName(i));
- }
- builder.append(") VALUES(");
- for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
- if (i != 0) {
- builder.append(',');
- }
+ final String name = accountCursor.getColumnName(i);
+ writer.name(name);
final String value = accountCursor.getString(i);
if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) {
- builder.append("NULL");
- } else if (Account.OPTIONS.equals(accountCursor.getColumnName(i)) && value.matches("\\d+")) {
+ writer.nullValue();
+ } else if (Account.OPTIONS.equals(accountCursor.getColumnName(i))
+ && value.matches("\\d+")) {
int intValue = Integer.parseInt(value);
intValue |= 1 << Account.OPTION_DISABLED;
- builder.append(intValue);
+ writer.value(intValue);
} else {
- appendEscapedSQLString(builder, value);
+ writer.value(value);
}
}
- builder.append(")");
- builder.append(';');
- builder.append('\n');
+ writer.endObject();
+ writer.endObject();
}
if (accountCursor != null) {
accountCursor.close();
}
- writer.append(builder.toString());
}
- private static void appendEscapedSQLString(final StringBuilder sb, final String sqlString) {
- DatabaseUtils.appendEscapedSQLString(sb, CharMatcher.is('\u0000').removeFrom(sqlString));
- }
-
- private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) {
- final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null);
+ private static void simpleExport(
+ final SQLiteDatabase db,
+ final String table,
+ final String column,
+ final String uuid,
+ final JsonWriter writer)
+ throws IOException {
+ final Cursor cursor =
+ db.query(table, null, column + "=?", new String[] {uuid}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
- writer.write(cursorToString(table, cursor, PAGE_SIZE));
+ writer.beginObject();
+ writer.name("table");
+ writer.value(table);
+ writer.name("values");
+ writer.beginObject();
+ for (int i = 0; i < cursor.getColumnCount(); ++i) {
+ final String name = cursor.getColumnName(i);
+ writer.name(name);
+ final String value = cursor.getString(i);
+ writer.value(value);
+ }
+ writer.endObject();
+ writer.endObject();
}
if (cursor != null) {
cursor.close();
}
}
- public static byte[] getKey(final String password, final byte[] salt) throws InvalidKeySpecException {
+ public static byte[] getKey(final String password, final byte[] salt)
+ throws InvalidKeySpecException {
final SecretKeyFactory factory;
try {
factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
- return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
- }
-
- private static String cursorToString(final String table, final Cursor cursor, final int max) {
- return cursorToString(table, cursor, max, false);
- }
-
- private static String cursorToString(final String table, final Cursor cursor, int max, boolean ignore) {
- final boolean identities = SQLiteAxolotlStore.IDENTITIES_TABLENAME.equals(table);
- StringBuilder builder = new StringBuilder();
- builder.append("INSERT ");
- if (ignore) {
- builder.append("OR IGNORE ");
- }
- builder.append("INTO ").append(table).append("(");
- int skipColumn = -1;
- for (int i = 0; i < cursor.getColumnCount(); ++i) {
- final String name = cursor.getColumnName(i);
- if (identities && SQLiteAxolotlStore.TRUSTED.equals(name)) {
- skipColumn = i;
- continue;
- }
- if (i != 0) {
- builder.append(',');
- }
- builder.append(name);
- }
- builder.append(") VALUES");
- for (int i = 0; i < max; ++i) {
- if (i != 0) {
- builder.append(',');
- }
- appendValues(cursor, builder, skipColumn);
- if (i < max - 1 && !cursor.moveToNext()) {
- break;
- }
- }
- builder.append(';');
- builder.append('\n');
- return builder.toString();
- }
-
- private static void appendValues(final Cursor cursor, final StringBuilder builder, final int skipColumn) {
- builder.append("(");
- for (int i = 0; i < cursor.getColumnCount(); ++i) {
- if (i == skipColumn) {
- continue;
- }
- if (i != 0) {
- builder.append(',');
- }
- final String value = cursor.getString(i);
- if (value == null) {
- builder.append("NULL");
- } else if (value.matches("[0-9]+")) {
- builder.append(value);
- } else {
- appendEscapedSQLString(builder, value);
- }
- }
- builder.append(")");
-
+ return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128))
+ .getEncoded();
}
@Override
@@ -223,49 +188,69 @@ public class ExportBackupService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (RUNNING.compareAndSet(false, true)) {
- new Thread(() -> {
- boolean success;
- List<File> files;
- try {
- files = export();
- success = true;
- } catch (final Exception e) {
- Log.d(Config.LOGTAG, "unable to create backup", e);
- success = false;
- files = Collections.emptyList();
- }
- stopForeground(true);
- RUNNING.set(false);
- if (success) {
- notifySuccess(files);
- }
- stopSelf();
- }).start();
+ new Thread(
+ () -> {
+ boolean success;
+ List<File> files;
+ try {
+ files = export();
+ success = true;
+ } catch (final Exception e) {
+ Log.d(Config.LOGTAG, "unable to create backup", e);
+ success = false;
+ files = Collections.emptyList();
+ }
+ stopForeground(true);
+ RUNNING.set(false);
+ if (success) {
+ notifySuccess(files);
+ }
+ stopSelf();
+ })
+ .start();
return START_STICKY;
} else {
- Log.d(Config.LOGTAG, "ExportBackupService. ignoring start command because already running");
+ Log.d(
+ Config.LOGTAG,
+ "ExportBackupService. ignoring start command because already running");
}
return START_NOT_STICKY;
}
- private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
- Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
+ private void messageExport(
+ final SQLiteDatabase db,
+ final String uuid,
+ final JsonWriter writer,
+ final Progress progress)
+ throws IOException {
+ Cursor cursor =
+ db.rawQuery(
+ "select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?",
+ new String[] {uuid});
int size = cursor != null ? cursor.getCount() : 0;
Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid);
int i = 0;
int p = 0;
while (cursor != null && cursor.moveToNext()) {
- writer.write(cursorToString(Message.TABLENAME, cursor, PAGE_SIZE, false));
- if (i + PAGE_SIZE > size) {
- i = size;
- } else {
- i += PAGE_SIZE;
+ writer.beginObject();
+ writer.name("table");
+ writer.value(Message.TABLENAME);
+ writer.name("values");
+ writer.beginObject();
+ for (int j = 0; j < cursor.getColumnCount(); ++j) {
+ final String name = cursor.getColumnName(j);
+ writer.name(name);
+ final String value = cursor.getString(j);
+ writer.value(value);
}
+ writer.endObject();
+ writer.endObject();
final int percentage = i * 100 / size;
if (p < percentage) {
p = percentage;
notificationManager.notify(NOTIFICATION_ID, progress.build(p));
}
+ i++;
}
if (cursor != null) {
cursor.close();
@@ -273,7 +258,8 @@ public class ExportBackupService extends Service {
}
private List<File> export() throws Exception {
- NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ 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);
@@ -286,17 +272,34 @@ public class ExportBackupService extends Service {
for (final Account account : this.mAccounts) {
final String password = account.getPassword();
if (Strings.nullToEmpty(password).trim().isEmpty()) {
- Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid()));
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "skipping backup for %s because password is empty. unable to encrypt",
+ account.getJid().asBareJid()));
continue;
}
- Log.d(Config.LOGTAG, String.format("exporting data for account %s (%s)", account.getJid().asBareJid(), account.getUuid()));
+ 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(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, 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");
+ final File file =
+ new File(
+ FileBackend.getBackupDirectory(this),
+ account.getJid().asBareJid().toEscapedString() + ".ceb");
files.add(file);
final File directory = file.getParentFile();
if (directory != null && directory.mkdirs()) {
@@ -307,25 +310,38 @@ public class ExportBackupService extends Service {
backupFileHeader.write(dataOutputStream);
dataOutputStream.flush();
- final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
+ 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);
-
- GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
- PrintWriter writer = new PrintWriter(gzipOutputStream);
- SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
+ 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 = 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);
+ 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);
}
- writer.flush();
- writer.close();
+ jsonWriter.endArray();
+ jsonWriter.flush();
+ jsonWriter.close();
mediaScannerScanFile(file);
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
count++;
@@ -346,9 +362,15 @@ public class ExportBackupService extends Service {
for (final Intent intent : getPossibleFileOpenIntents(this, path)) {
if (intent.resolveActivityInfo(getPackageManager(), 0) != null) {
- openFolderIntent = PendingIntent.getActivity(this, 189, intent, s()
- ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
- : PendingIntent.FLAG_UPDATE_CURRENT);
+ openFolderIntent =
+ PendingIntent.getActivity(
+ this,
+ 189,
+ intent,
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
break;
}
}
@@ -363,22 +385,39 @@ public class ExportBackupService extends Service {
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, s()
- ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
- : PendingIntent.FLAG_UPDATE_CURRENT);
+ final Intent chooser =
+ Intent.createChooser(intent, getString(R.string.share_backup_files));
+ shareFilesIntent =
+ PendingIntent.getActivity(
+ this,
+ 190,
+ chooser,
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
}
- NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ 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).getAbsolutePath())))
+ .setStyle(
+ new NotificationCompat.BigTextStyle()
+ .bigText(
+ getString(
+ R.string.notification_backup_created_subtitle,
+ FileBackend.getBackupDirectory(this)
+ .getAbsolutePath())))
.setAutoCancel(true)
.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);
+ mBuilder.addAction(
+ R.drawable.ic_share_white_24dp,
+ getString(R.string.share_backup_files),
+ shareFilesIntent);
}
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());