diff options
Diffstat (limited to 'src/main/java/de/pixart/messenger/services/ImportBackupService.java')
-rw-r--r-- | src/main/java/de/pixart/messenger/services/ImportBackupService.java | 290 |
1 files changed, 290 insertions, 0 deletions
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 |