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.net.Uri; import android.os.Binder; import android.os.IBinder; import android.util.Log; import androidx.core.app.NotificationCompat; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; 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 java.util.zip.ZipException; 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 Uri data = intent.getData(); final Uri uri; if (data == null) { final String file = intent.getStringExtra("file"); uri = file == null ? null : Uri.fromFile(new File(file)); } else { uri = data; } if (password == null || password.isEmpty() || uri == null) { 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(); }); } else { Log.d(Config.LOGTAG, "backup already running"); } return START_NOT_STICKY; } public boolean getLoadingState() { return running.get(); } public void loadBackupFiles(OnBackupFilesLoaded onBackupFilesLoaded) { executor.execute(() -> { List accounts = mDatabaseBackend.getAccountJids(false); final ArrayList backupFiles = new ArrayList<>(); final Set apps = new HashSet<>(Arrays.asList(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); } catch (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); }); } private void startForegroundService() { NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); mBuilder.setContentTitle(getString(R.string.restoring_backup)) .setSmallIcon(R.drawable.ic_unarchive_white_24dp) .setProgress(1, 0, true); startForeground(NOTIFICATION_ID, mBuilder.build()); } private boolean importBackup(Uri uri, String password) { Log.d(Config.LOGTAG, "importing backup from " + uri); try { SQLiteDatabase db = mDatabaseBackend.getWritableDatabase(); final InputStream inputStream; if ("file".equals(uri.getScheme())) { inputStream = new FileInputStream(new File(uri.getPath())); } else { inputStream = getContentResolver().openInputStream(uri); } final DataInputStream dataInputStream = new DataInputStream(inputStream); final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); Log.d(Config.LOGTAG, backupFileHeader.toString()); if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) { synchronized (mOnBackupProcessedListeners) { for (OnBackupProcessed l : mOnBackupProcessedListeners) { l.onAccountAlreadySetup(); } } return false; } 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(inputStream, 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 = throwable instanceof BadPaddingException || e instanceof ZipException; synchronized (mOnBackupProcessedListeners) { for (OnBackupProcessed l : mOnBackupProcessedListeners) { if (reasonWasCrypto) { l.onBackupDecryptionFailed(); } else { l.onBackupRestoreFailed(); } } } Log.d(Config.LOGTAG, "error restoring backup " + uri, 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(); void onAccountAlreadySetup(); } public static class BackupFile { private final Uri uri; private final BackupFileHeader header; private BackupFile(Uri uri, BackupFileHeader header) { this.uri = uri; 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(Uri.fromFile(file), backupFileHeader); } public static BackupFile read(final Context context, final Uri uri) throws IOException { final InputStream inputStream = context.getContentResolver().openInputStream(uri); if (inputStream == null) { throw new FileNotFoundException(); } final DataInputStream dataInputStream = new DataInputStream(inputStream); BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); inputStream.close(); return new BackupFile(uri, backupFileHeader); } public BackupFileHeader getHeader() { return header; } public Uri getUri() { return uri; } } public class ImportBackupServiceBinder extends Binder { public ImportBackupService getService() { return ImportBackupService.this; } } }