aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/de/pixart/messenger/services
diff options
context:
space:
mode:
authorChristian Schneppe <christian@pix-art.de>2019-01-26 15:07:28 +0100
committerChristian Schneppe <christian@pix-art.de>2019-01-26 15:07:28 +0100
commitf2d502518ea3de673c7f0ebf425f53295f620f2f (patch)
tree2db8f4e334d51b59c35105bc1871b102f4bb34d3 /src/main/java/de/pixart/messenger/services
parent2773c19c429c4bcb99fd0144cd1b3e2346cab962 (diff)
rework backup & restore
use the implementation from Conversations
Diffstat (limited to 'src/main/java/de/pixart/messenger/services')
-rw-r--r--src/main/java/de/pixart/messenger/services/AlarmReceiver.java2
-rw-r--r--src/main/java/de/pixart/messenger/services/AvatarService.java84
-rw-r--r--src/main/java/de/pixart/messenger/services/ExportBackupService.java287
-rw-r--r--src/main/java/de/pixart/messenger/services/ExportLogsService.java248
-rw-r--r--src/main/java/de/pixart/messenger/services/ImportBackupService.java290
-rw-r--r--src/main/java/de/pixart/messenger/services/NotificationService.java1
6 files changed, 619 insertions, 293 deletions
diff --git a/src/main/java/de/pixart/messenger/services/AlarmReceiver.java b/src/main/java/de/pixart/messenger/services/AlarmReceiver.java
index eb38da8b3..887a59684 100644
--- a/src/main/java/de/pixart/messenger/services/AlarmReceiver.java
+++ b/src/main/java/de/pixart/messenger/services/AlarmReceiver.java
@@ -15,7 +15,7 @@ public class AlarmReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
if (intent.getAction().contains("exportlogs")) {
Log.d(Config.LOGTAG, "Received alarm broadcast to export logs");
- Compatibility.startService(context, new Intent(context, ExportLogsService.class));
+ Compatibility.startService(context, new Intent(context, ExportBackupService.class));
}
}
}
diff --git a/src/main/java/de/pixart/messenger/services/AvatarService.java b/src/main/java/de/pixart/messenger/services/AvatarService.java
index 93d24a63f..fc6235a55 100644
--- a/src/main/java/de/pixart/messenger/services/AvatarService.java
+++ b/src/main/java/de/pixart/messenger/services/AvatarService.java
@@ -51,6 +51,8 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
private static final int TRANSPARENT = 0x00000000;
private static final int PLACEHOLDER_COLOR = 0xFF202020;
+ public static final int SYSTEM_UI_AVATAR_SIZE = 48;
+
private static final String PREFIX_CONTACT = "contact";
private static final String PREFIX_CONVERSATION = "conversation";
private static final String PREFIX_ACCOUNT = "account";
@@ -65,19 +67,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
this.mXmppConnectionService = service;
}
- private static String getFirstLetter(String name) {
- for (Character c : name.toCharArray()) {
- if (Character.isLetterOrDigit(c)) {
- return c.toString();
- }
- }
- return "X";
- }
-
- private static String emptyOnNull(@Nullable Jid value) {
- return value == null ? "" : value.toString();
- }
-
public static int getSystemUiAvatarSize(final Context context) {
return (int) (SYSTEM_UI_AVATAR_SIZE * context.getResources().getDisplayMetrics().density);
}
@@ -91,15 +80,14 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (avatar != null || cachedOnly) {
return avatar;
}
- if (contact.getAvatarFilename() != null) {
+ if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy()) {
avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatarFilename(), size);
}
if (avatar == null && contact.getProfilePhoto() != null) {
- try {
- avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
- } catch (Exception e) {
- e.printStackTrace();
- }
+ avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
+ }
+ if (avatar == null && contact.getAvatarFilename() != null) {
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatarFilename(), size);
}
if (avatar == null) {
avatar = get(contact.getDisplayName(), contact.getJid().asBareJid().toString(), size, false);
@@ -123,6 +111,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
final Paint paint = new Paint();
+
drawAvatar(bitmap, canvas, paint);
if (withIcon) {
drawIcon(canvas, paint);
@@ -318,7 +307,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (bitmap != null || cachedOnly) {
return bitmap;
}
+
bitmap = mXmppConnectionService.getFileBackend().getAvatar(mucOptions.getAvatar(), size);
+
if (bitmap == null) {
final List<MucOptions.User> users = mucOptions.getUsersRelevantForNameAndAvatar();
if (users.size() == 0) {
@@ -328,7 +319,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
bitmap = getImpl(users, size);
}
}
+
this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+
return bitmap;
}
@@ -488,10 +481,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
}
}
- /*public Bitmap get(String name, int size) {
- return get(name,null, size,false);
- }*/
-
public void clear(MucOptions.User user) {
synchronized (this.sizes) {
for (Integer size : sizes) {
@@ -510,6 +499,10 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
+ String.valueOf(size);
}
+ /*public Bitmap get(String name, int size) {
+ return get(name,null, size,false);
+ }*/
+
public Bitmap get(final String name, String seed, final int size, boolean cachedOnly) {
final String KEY = key(seed == null ? name : name + "\0" + seed, size);
Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
@@ -521,7 +514,11 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return bitmap;
}
- private Bitmap getImpl(final String name, final String seed, final int size) {
+ public static Bitmap get(final Jid jid, final int size) {
+ return getImpl(jid.asBareJid().toEscapedString(), null, size);
+ }
+
+ private static Bitmap getImpl(final String name, final String seed, final int size) {
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
final String trimmedName = name == null ? "" : name.trim();
@@ -538,7 +535,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size);
}
- private boolean drawTile(Canvas canvas, String letter, int tileColor, int left, int top, int right, int bottom) {
+ private static boolean drawTile(Canvas canvas, String letter, int tileColor, int left, int top, int right, int bottom) {
letter = letter.toUpperCase(Locale.getDefault());
Paint tilePaint = new Paint(), textPaint = new Paint();
tilePaint.setColor(tileColor);
@@ -561,14 +558,12 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
Contact contact = user.getContact();
if (contact != null) {
Uri uri = null;
- if (contact.getAvatarFilename() != null) {
- try {
- uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename());
- } catch (Exception e) {
- e.printStackTrace();
- }
+ if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy()) {
+ uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename());
} else if (contact.getProfilePhoto() != null) {
uri = Uri.parse(contact.getProfilePhoto());
+ } else if (contact.getAvatarFilename() != null) {
+ uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename());
}
if (drawTile(canvas, uri, left, top, right, bottom)) {
return true;
@@ -578,16 +573,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (drawTile(canvas, uri, left, top, right, bottom)) {
return true;
}
- } else if (user.getAvatar() != null) {
- Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(user.getAvatar());
- if (drawTile(canvas, uri, left, top, right, bottom)) {
- return true;
- }
- } else if (user.getAvatar() != null) {
- Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(user.getAvatar());
- if (drawTile(canvas, uri, left, top, right, bottom)) {
- return true;
- }
}
if (contact != null) {
String seed = contact.getJid().asBareJid().toString();
@@ -613,7 +598,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return drawTile(canvas, name, name, left, top, right, bottom);
}
- private boolean drawTile(Canvas canvas, String name, String seed, int left, int top, int right, int bottom) {
+ private static boolean drawTile(Canvas canvas, String name, String seed, int left, int top, int right, int bottom) {
if (name != null) {
final String letter = getFirstLetter(name);
final int color = UIHelper.getColorForName(seed == null ? name : seed);
@@ -623,6 +608,15 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return false;
}
+ private static String getFirstLetter(String name) {
+ for (Character c : name.toCharArray()) {
+ if (Character.isLetterOrDigit(c)) {
+ return c.toString();
+ }
+ }
+ return "X";
+ }
+
private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) {
if (uri != null) {
Bitmap bitmap = mXmppConnectionService.getFileBackend()
@@ -651,4 +645,8 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
}
}
}
-}
+
+ private static String emptyOnNull(@Nullable Jid value) {
+ return value == null ? "" : value.toString();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/services/ExportBackupService.java b/src/main/java/de/pixart/messenger/services/ExportBackupService.java
new file mode 100644
index 000000000..36b926f6a
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/services/ExportBackupService.java
@@ -0,0 +1,287 @@
+package de.pixart.messenger.services;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.zip.GZIPOutputStream;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import de.pixart.messenger.Config;
+import de.pixart.messenger.R;
+import de.pixart.messenger.crypto.axolotl.SQLiteAxolotlStore;
+import de.pixart.messenger.entities.Account;
+import de.pixart.messenger.entities.Conversation;
+import de.pixart.messenger.entities.Message;
+import de.pixart.messenger.persistance.DatabaseBackend;
+import de.pixart.messenger.persistance.FileBackend;
+import de.pixart.messenger.utils.BackupFileHeader;
+import de.pixart.messenger.utils.Compatibility;
+
+public class ExportBackupService extends Service {
+
+ public static final String KEYTYPE = "AES";
+ public static final String CIPHERMODE = "AES/GCM/NoPadding";
+ public static final String PROVIDER = "BC";
+
+ private static final int NOTIFICATION_ID = 19;
+ private static final int PAGE_SIZE = 20;
+ private static AtomicBoolean running = new AtomicBoolean(false);
+ private DatabaseBackend mDatabaseBackend;
+ private List<Account> mAccounts;
+ private NotificationManager notificationManager;
+
+ private static void accountExport(SQLiteDatabase db, String uuid, PrintWriter writer) {
+ final StringBuilder builder = new StringBuilder();
+ 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("(");
+ 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 value = accountCursor.getString(i);
+ if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) {
+ builder.append("NULL");
+ } else if (value.matches("\\d+")) {
+ int intValue = Integer.parseInt(value);
+ if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) {
+ intValue |= 1 << Account.OPTION_DISABLED;
+ }
+ builder.append(intValue);
+ } else {
+ DatabaseUtils.appendEscapedSQLString(builder, value);
+ }
+ }
+ builder.append(")");
+ builder.append(';');
+ builder.append('\n');
+ }
+ if (accountCursor != null) {
+ accountCursor.close();
+ }
+ writer.append(builder.toString());
+ }
+
+ 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);
+ while (cursor != null && cursor.moveToNext()) {
+ writer.write(cursorToString(table, cursor, PAGE_SIZE));
+ }
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ public static byte[] getKey(String password, byte[] salt) {
+ try {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static String cursorToString(String tablename, Cursor cursor, int max) {
+ return cursorToString(tablename, cursor, max, false);
+ }
+
+ private static String cursorToString(String tablename, Cursor cursor, int max, boolean ignore) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("INSERT ");
+ if (ignore) {
+ builder.append("OR IGNORE ");
+ }
+ builder.append("INTO ").append(tablename).append("(");
+ for (int i = 0; i < cursor.getColumnCount(); ++i) {
+ if (i != 0) {
+ builder.append(',');
+ }
+ builder.append(cursor.getColumnName(i));
+ }
+ builder.append(") VALUES");
+ for (int i = 0; i < max; ++i) {
+ if (i != 0) {
+ builder.append(',');
+ }
+ appendValues(cursor, builder);
+ if (i < max - 1 && !cursor.moveToNext()) {
+ break;
+ }
+ }
+ builder.append(';');
+ builder.append('\n');
+ return builder.toString();
+ }
+
+ private static void appendValues(Cursor cursor, StringBuilder builder) {
+ builder.append("(");
+ for (int i = 0; i < cursor.getColumnCount(); ++i) {
+ if (i != 0) {
+ builder.append(',');
+ }
+ final String value = cursor.getString(i);
+ if (value == null) {
+ builder.append("NULL");
+ } else if (value.matches("\\d+")) {
+ builder.append(value);
+ } else {
+ DatabaseUtils.appendEscapedSQLString(builder, value);
+ }
+ }
+ builder.append(")");
+
+ }
+
+ @Override
+ public void onCreate() {
+ mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
+ mAccounts = mDatabaseBackend.getAccounts();
+ notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (running.compareAndSet(false, true)) {
+ new Thread(() -> {
+ export();
+ stopForeground(true);
+ running.set(false);
+ stopSelf();
+ }).start();
+ return START_STICKY;
+ }
+ 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});
+ int size = cursor != null ? cursor.getCount() : 0;
+ Log.d(Config.LOGTAG, "exporting " + size + " messages");
+ 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;
+ }
+ final int percentage = i * 100 / size;
+ if (p < percentage) {
+ p = percentage;
+ notificationManager.notify(NOTIFICATION_ID, progress.build(p));
+ }
+ }
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ private void export() {
+ 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() + 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++;
+ }
+ } catch (Exception e) {
+ Log.d(Config.LOGTAG, "unable to create backup ", e);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private class Progress {
+ private final NotificationCompat.Builder builder;
+ private final int max;
+ private final int count;
+
+ private Progress(NotificationCompat.Builder builder, int max, int count) {
+ this.builder = builder;
+ this.max = max;
+ this.count = count;
+ }
+
+ private Notification build(int percentage) {
+ builder.setProgress(max * 100, count * 100 + percentage, false);
+ return builder.build();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/services/ExportLogsService.java b/src/main/java/de/pixart/messenger/services/ExportLogsService.java
deleted file mode 100644
index a607fcb66..000000000
--- a/src/main/java/de/pixart/messenger/services/ExportLogsService.java
+++ /dev/null
@@ -1,248 +0,0 @@
-package de.pixart.messenger.services;
-
-import android.app.NotificationManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.IBinder;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.preference.PreferenceManager;
-import android.support.annotation.BoolRes;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import javax.crypto.NoSuchPaddingException;
-
-import de.pixart.messenger.Config;
-import de.pixart.messenger.R;
-import de.pixart.messenger.entities.Account;
-import de.pixart.messenger.entities.Conversation;
-import de.pixart.messenger.entities.Message;
-import de.pixart.messenger.persistance.DatabaseBackend;
-import de.pixart.messenger.persistance.FileBackend;
-import de.pixart.messenger.utils.EncryptDecryptFile;
-import de.pixart.messenger.utils.WakeLockHelper;
-import rocks.xmpp.addr.Jid;
-
-import static de.pixart.messenger.services.NotificationService.BACKUP_CHANNEL_ID;
-import static de.pixart.messenger.services.NotificationService.NOTIFICATION_ID;
-import static de.pixart.messenger.ui.SettingsActivity.USE_MULTI_ACCOUNTS;
-
-public class ExportLogsService extends XmppConnectionService {
-
- private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
- private static final String DIRECTORY_STRING_FORMAT = FileBackend.getAppLogsDirectory() + "%s";
- private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n";
- private static AtomicBoolean running = new AtomicBoolean(false);
- boolean ReadableLogsEnabled = false;
- private DatabaseBackend mDatabaseBackend;
- private List<Account> mAccounts;
- private WakeLock wakeLock;
- private PowerManager pm;
-
- @Override
- public void onCreate() {
- mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
- mAccounts = mDatabaseBackend.getAccounts();
- final SharedPreferences ReadableLogs = PreferenceManager.getDefaultSharedPreferences(this);
- ReadableLogsEnabled = ReadableLogs.getBoolean("export_plain_text_logs", getResources().getBoolean(R.bool.plain_text_logs));
- pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Config.LOGTAG + ": ExportLogsService");
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- if (running.compareAndSet(false, true)) {
- new Thread(() -> {
- export();
- WakeLockHelper.release(wakeLock);
- stopForeground(true);
- running.set(false);
- stopSelf();
- }).start();
- }
- return START_NOT_STICKY;
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- stopForeground(true);
- }
-
- private void export() {
- wakeLock.acquire();
- NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), BACKUP_CHANNEL_ID);
- mBuilder.setContentTitle(getString(R.string.notification_export_logs_title));
- mBuilder.setSmallIcon(R.drawable.ic_import_export_white_24dp);
- mBuilder.setProgress(0, 0, true);
- startForeground(NOTIFICATION_ID, mBuilder.build());
- List<Conversation> conversations = mDatabaseBackend.getConversations(Conversation.STATUS_AVAILABLE);
- conversations.addAll(mDatabaseBackend.getConversations(Conversation.STATUS_ARCHIVED));
- if (mAccounts.size() >= 1) {
- if (ReadableLogsEnabled) {
- for (Conversation conversation : conversations) {
- writeToFile(conversation);
- }
- }
- try {
- ExportDatabase();
- } catch (IOException e) {
- e.printStackTrace();
- }
- } else {
- Log.d(Config.LOGTAG, "ExportLogsService: no accounts, aborting export");
- }
- }
-
- private void writeToFile(Conversation conversation) {
- Jid accountJid = resolveAccountUuid(conversation.getAccountUuid());
- Jid contactJid = conversation.getJid();
-
- File dir = new File(String.format(DIRECTORY_STRING_FORMAT, accountJid.asBareJid().toString()));
- dir.mkdirs();
-
- BufferedWriter bw = null;
- try {
- for (Message message : mDatabaseBackend.getMessagesIterable(conversation)) {
- if (message == null)
- continue;
- if (message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) {
- String date = simpleDateFormat.format(new Date(message.getTimeSent()));
- if (bw == null) {
- bw = new BufferedWriter(new FileWriter(
- new File(dir, contactJid.asBareJid().toString() + ".txt")));
- }
- String jid = null;
- switch (message.getStatus()) {
- case Message.STATUS_RECEIVED:
- jid = getMessageCounterpart(message);
- break;
- case Message.STATUS_SEND:
- case Message.STATUS_SEND_RECEIVED:
- case Message.STATUS_SEND_DISPLAYED:
- jid = accountJid.asBareJid().toString();
- break;
- }
- if (jid != null) {
- String body = message.hasFileOnRemoteHost() ? message.getFileParams().url.toString() : message.getBody();
- bw.write(String.format(MESSAGE_STRING_FORMAT, date, jid,
- body.replace("\\\n", "\\ \n").replace("\n", "\\ \n")));
- }
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (bw != null) {
- bw.close();
- }
- } catch (IOException e1) {
- e1.printStackTrace();
- }
- }
- }
-
- private Jid resolveAccountUuid(String accountUuid) {
- for (Account account : mAccounts) {
- if (account.getUuid().equals(accountUuid)) {
- return account.getJid();
- }
- }
- return null;
- }
-
- private String getMessageCounterpart(Message message) {
- String trueCounterpart = (String) message.getContentValues().get(Message.TRUE_COUNTERPART);
- if (trueCounterpart != null) {
- return trueCounterpart;
- } else {
- return message.getCounterpart().toString();
- }
- }
-
- public void ExportDatabase() throws IOException {
- Log.d(Config.LOGTAG, "ExportLogsService: start creating backup");
- Account mAccount = mAccounts.get(0);
- String EncryptionKey = null;
- // Get hold of the db:
- FileInputStream InputFile = new FileInputStream(this.getDatabasePath(DatabaseBackend.DATABASE_NAME));
- // Set the output folder on the SDcard
- File directory = new File(FileBackend.getBackupDirectory());
- // Create the folder if it doesn't exist:
- if (!directory.exists()) {
- boolean directory_created = directory.mkdirs();
- Log.d(Config.LOGTAG, "ExportLogsService: backup directory created " + directory_created);
- }
- //Delete old database export file
- File temp_db_file = new File(directory + "/database.bak");
- if (temp_db_file.exists()) {
- Log.d(Config.LOGTAG, "ExportLogsService: Delete temp database backup file from " + temp_db_file.toString());
- boolean temp_db_file_deleted = temp_db_file.delete();
- Log.d(Config.LOGTAG, "ExportLogsService: old backup file deleted " + temp_db_file_deleted);
- }
- // Set the output file stream up:
- FileOutputStream OutputFile = new FileOutputStream(directory.getPath() + "/database.db.crypt");
-
- if (mAccounts.size() == 1 && !multipleAccounts()) {
- EncryptionKey = mAccount.getPassword(); //get account password
- } else {
- SharedPreferences multiaccount_prefs = getApplicationContext().getSharedPreferences(USE_MULTI_ACCOUNTS, Context.MODE_PRIVATE);
- EncryptionKey = multiaccount_prefs.getString("BackupPW", null);
- }
- if (EncryptionKey == null) {
- Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: failed to write encryted backup to sdcard because of missing password");
- return;
- }
-
- // encrypt database from the input file to the output file
- try {
- EncryptDecryptFile.encrypt(InputFile, OutputFile, EncryptionKey);
- Log.d(Config.LOGTAG, "ExportLogsService: starting encrypted output to " + OutputFile.toString());
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: encryption failed with " + e);
- e.printStackTrace();
- } catch (InvalidKeyException e) {
- Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: encryption failed (invalid key) with " + e);
- e.printStackTrace();
- } catch (IOException e) {
- Log.d(Config.LOGTAG, "ExportLogsService: Database exporter: encryption failed (IO) with " + e);
- e.printStackTrace();
- } finally {
- Log.d(Config.LOGTAG, "ExportLogsService: backup job finished");
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- public SharedPreferences getPreferences() {
- return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- }
-
- public boolean getBooleanPreference(String name, @BoolRes int res) {
- return getPreferences().getBoolean(name, getResources().getBoolean(res));
- }
-
- public boolean multipleAccounts() {
- return getBooleanPreference("enable_multi_accounts", R.bool.enable_multi_accounts);
- }
-}
diff --git a/src/main/java/de/pixart/messenger/services/ImportBackupService.java b/src/main/java/de/pixart/messenger/services/ImportBackupService.java
new file mode 100644
index 000000000..e73b1972f
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/services/ImportBackupService.java
@@ -0,0 +1,290 @@
+package de.pixart.messenger.services;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Binder;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.zip.GZIPInputStream;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+
+import de.pixart.messenger.Config;
+import de.pixart.messenger.R;
+import de.pixart.messenger.persistance.DatabaseBackend;
+import de.pixart.messenger.persistance.FileBackend;
+import de.pixart.messenger.ui.ManageAccountActivity;
+import de.pixart.messenger.utils.BackupFileHeader;
+import de.pixart.messenger.utils.Compatibility;
+import de.pixart.messenger.utils.SerialSingleThreadExecutor;
+import rocks.xmpp.addr.Jid;
+
+import static de.pixart.messenger.services.ExportBackupService.CIPHERMODE;
+import static de.pixart.messenger.services.ExportBackupService.KEYTYPE;
+import static de.pixart.messenger.services.ExportBackupService.PROVIDER;
+
+public class ImportBackupService extends Service {
+
+ private static final int NOTIFICATION_ID = 21;
+ private static 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 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;
+ }
+
+ @Override
+ public void onCreate() {
+ mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
+ notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent == null) {
+ return START_NOT_STICKY;
+ }
+ final String password = intent.getStringExtra("password");
+ final String file = intent.getStringExtra("file");
+ if (password == null || file == null) {
+ return START_NOT_STICKY;
+ }
+ if (running.compareAndSet(false, true)) {
+ executor.execute(() -> {
+ startForegroundService();
+ final boolean success = importBackup(new File(file), password);
+ stopForeground(true);
+ running.set(false);
+ if (success) {
+ notifySuccess();
+ }
+ stopSelf();
+ });
+ } else {
+ Log.d(Config.LOGTAG, "backup already running");
+ }
+ return START_NOT_STICKY;
+ }
+
+ public void loadBackupFiles(OnBackupFilesLoaded onBackupFilesLoaded) {
+ executor.execute(() -> {
+ 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)));
+ for (String app : apps) {
+ final File directory = new File(FileBackend.getBackupDirectory());
+ if (!directory.exists() || !directory.isDirectory()) {
+ Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
+ continue;
+ }
+ for (File file : directory.listFiles()) {
+ 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 e) {
+ Log.d(Config.LOGTAG, "unable to read backup file ", e);
+ }
+ }
+ }
+ }
+ onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
+ });
+ }
+
+ private void startForegroundService() {
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ mBuilder.setContentTitle(getString(R.string.notification_restore_backup_title))
+ .setSmallIcon(R.drawable.ic_unarchive_white_24dp)
+ .setProgress(1, 0, true);
+ startForeground(NOTIFICATION_ID, mBuilder.build());
+ }
+
+ private boolean importBackup(File file, String password) {
+ Log.d(Config.LOGTAG, "importing backup from file " + file.getAbsolutePath());
+ try {
+ SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
+ final FileInputStream fileInputStream = new FileInputStream(file);
+ final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
+ BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ Log.d(Config.LOGTAG, backupFileHeader.toString());
+
+ final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
+ byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
+ SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
+ IvParameterSpec ivSpec = new IvParameterSpec(backupFileHeader.getIv());
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+ CipherInputStream cipherInputStream = new CipherInputStream(fileInputStream, cipher);
+
+ GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
+ 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;
+ }
+ } else {
+ if (count % 2 == 0) {
+ db.execSQL(line);
+ } else {
+ multiLineQuery = new StringBuilder(line);
+ }
+ }
+ }
+ final Jid jid = backupFileHeader.getJid();
+ 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()});
+ countCursor.moveToFirst();
+ int count = countCursor.getInt(0);
+ Log.d(Config.LOGTAG, "restored " + count + " messages");
+ countCursor.close();
+ stopBackgroundService();
+ synchronized (mOnBackupProcessedListeners) {
+ for (OnBackupProcessed l : mOnBackupProcessedListeners) {
+ l.onBackupRestored();
+ }
+ }
+ return true;
+ } catch (Exception e) {
+ Throwable throwable = e.getCause();
+ final boolean reasonWasCrypto;
+ if (throwable instanceof BadPaddingException) {
+ reasonWasCrypto = true;
+ } else {
+ reasonWasCrypto = false;
+ }
+ synchronized (mOnBackupProcessedListeners) {
+ for (OnBackupProcessed l : mOnBackupProcessedListeners) {
+ if (reasonWasCrypto) {
+ l.onBackupDecryptionFailed();
+ } else {
+ l.onBackupRestoreFailed();
+ }
+ }
+ }
+ Log.d(Config.LOGTAG, "error restoring backup " + file.getAbsolutePath(), e);
+ return false;
+ }
+ }
+
+ private void notifySuccess() {
+ 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), PendingIntent.FLAG_UPDATE_CURRENT))
+ .setSmallIcon(R.drawable.ic_unarchive_white_24dp);
+ notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
+ }
+
+ private void stopBackgroundService() {
+ Intent intent = new Intent(this, XmppConnectionService.class);
+ stopService(intent);
+ }
+
+ public void removeOnBackupProcessedListener(OnBackupProcessed listener) {
+ synchronized (mOnBackupProcessedListeners) {
+ mOnBackupProcessedListeners.remove(listener);
+ }
+ }
+
+ public void addOnBackupProcessedListener(OnBackupProcessed listener) {
+ synchronized (mOnBackupProcessedListeners) {
+ mOnBackupProcessedListeners.add(listener);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return this.binder;
+ }
+
+ public interface OnBackupFilesLoaded {
+ void onBackupFilesLoaded(List<BackupFile> files);
+ }
+
+ public interface OnBackupProcessed {
+ void onBackupRestored();
+
+ void onBackupDecryptionFailed();
+
+ void onBackupRestoreFailed();
+ }
+
+ public static class BackupFile {
+ private final File file;
+ private final BackupFileHeader header;
+
+ private BackupFile(File file, BackupFileHeader header) {
+ this.file = file;
+ this.header = header;
+ }
+
+ private static BackupFile read(File file) throws IOException {
+ final FileInputStream fileInputStream = new FileInputStream(file);
+ final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
+ BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+ fileInputStream.close();
+ return new BackupFile(file, backupFileHeader);
+ }
+
+ public BackupFileHeader getHeader() {
+ return header;
+ }
+
+ public File getFile() {
+ return file;
+ }
+ }
+
+ public class ImportBackupServiceBinder extends Binder {
+ public ImportBackupService getService() {
+ return ImportBackupService.this;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/services/NotificationService.java b/src/main/java/de/pixart/messenger/services/NotificationService.java
index 8ded39d50..c3f97a7fe 100644
--- a/src/main/java/de/pixart/messenger/services/NotificationService.java
+++ b/src/main/java/de/pixart/messenger/services/NotificationService.java
@@ -1047,7 +1047,6 @@ public class NotificationService {
Notification AppUpdateNotification(PendingIntent intent, String version, String filesize) {
Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
- mBuilder.setContentText(mXmppConnectionService.getString(R.string.notification_export_logs_title));
mBuilder.setContentText(String.format(mXmppConnectionService.getString(R.string.update_available), version, filesize));
mBuilder.setSmallIcon(R.drawable.ic_update_notification);
mBuilder.setContentIntent(intent);