From f2d502518ea3de673c7f0ebf425f53295f620f2f Mon Sep 17 00:00:00 2001 From: Christian Schneppe Date: Sat, 26 Jan 2019 15:07:28 +0100 Subject: rework backup & restore use the implementation from Conversations --- .../pixart/messenger/services/AlarmReceiver.java | 2 +- .../pixart/messenger/services/AvatarService.java | 84 +++--- .../messenger/services/ExportBackupService.java | 287 ++++++++++++++++++++ .../messenger/services/ExportLogsService.java | 248 ------------------ .../messenger/services/ImportBackupService.java | 290 +++++++++++++++++++++ .../messenger/services/NotificationService.java | 1 - 6 files changed, 619 insertions(+), 293 deletions(-) create mode 100644 src/main/java/de/pixart/messenger/services/ExportBackupService.java delete mode 100644 src/main/java/de/pixart/messenger/services/ExportLogsService.java create mode 100644 src/main/java/de/pixart/messenger/services/ImportBackupService.java (limited to 'src/main/java/de/pixart/messenger/services') 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 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 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 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 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 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 accounts = mDatabaseBackend.getAccountJids(false); + final ArrayList backupFiles = new ArrayList<>(); + final Set 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 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); -- cgit v1.2.3