WIP backup & restore

Daniel Gultsch created

Change summary

src/conversations/AndroidManifest.xml                                           |   4 
src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java | 276 
src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java      | 125 
src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java     | 718 
src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java           | 163 
src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java | 169 
src/conversations/res/drawable-hdpi/ic_unarchive_white_24dp.png                 |   0 
src/conversations/res/drawable-mdpi/ic_unarchive_white_24dp.png                 |   0 
src/conversations/res/drawable-xhdpi/ic_unarchive_white_24dp.png                |   0 
src/conversations/res/drawable-xxhdpi/ic_unarchive_white_24dp.png               |   0 
src/conversations/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png              |   0 
src/conversations/res/layout/activity_import_backup.xml                         |  32 
src/conversations/res/layout/dialog_enter_password.xml                          |  47 
src/conversations/res/menu/manageaccounts.xml                                   |   4 
src/conversations/res/menu/welcome_menu.xml                                     |   8 
src/main/AndroidManifest.xml                                                    |   3 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java               |   8 
src/main/java/eu/siacs/conversations/services/AvatarService.java                |  10 
src/main/java/eu/siacs/conversations/services/ExportBackupService.java          | 281 
src/main/java/eu/siacs/conversations/services/ExportLogsService.java            | 148 
src/main/java/eu/siacs/conversations/services/NotificationService.java          |   6 
src/main/java/eu/siacs/conversations/ui/SettingsActivity.java                   |  26 
src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java             | 316 
src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java                |  85 
src/main/res/drawable-hdpi/ic_archive_white_24dp.png                            |   0 
src/main/res/drawable-hdpi/ic_import_export_white_24dp.png                      |   0 
src/main/res/drawable-mdpi/ic_archive_white_24dp.png                            |   0 
src/main/res/drawable-mdpi/ic_import_export_white_24dp.png                      |   0 
src/main/res/drawable-xhdpi/ic_archive_white_24dp.png                           |   0 
src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png                     |   0 
src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png                          |   0 
src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png                    |   0 
src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.png                         |   0 
src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png                   |   0 
src/main/res/layout/account_row.xml                                             |  94 
src/main/res/layout/dialog_quickedit.xml                                        |   4 
src/main/res/values/strings.xml                                                 |  17 
src/main/res/values/themes.xml                                                  |   2 
src/main/res/xml/preferences.xml                                                |   6 
39 files changed, 1,741 insertions(+), 811 deletions(-)

Detailed changes

src/conversations/AndroidManifest.xml πŸ”—

@@ -16,6 +16,10 @@
             android:name=".ui.MagicCreateActivity"
             android:label="@string/create_account"
             android:launchMode="singleTask"/>
+        <activity
+            android:name=".ui.ImportBackupActivity"
+            android:label="@string/restore_backup"
+            android:launchMode="singleTask" />
 
     </application>
 </manifest>

src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java πŸ”—

@@ -0,0 +1,276 @@
+package eu.siacs.conversations.services;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+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.List;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.zip.GZIPInputStream;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import eu.siacs.conversations.utils.BackupFileHeader;
+import eu.siacs.conversations.utils.Compatibility;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+
+import static eu.siacs.conversations.services.ExportBackupService.CIPHERMODE;
+import static eu.siacs.conversations.services.ExportBackupService.KEYTYPE;
+import static eu.siacs.conversations.services.ExportBackupService.PROVIDER;
+
+public class ImportBackupService extends Service {
+
+    private static final int NOTIFICATION_ID = 21;
+
+    private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
+    private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
+
+    private final Set<OnBackupProcessed> mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
+
+    private static AtomicBoolean running = new AtomicBoolean(false);
+    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;
+        }
+        Log.d(Config.LOGTAG, "on start command");
+        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(() -> {
+            final ArrayList<BackupFile> backupFiles = new ArrayList<>();
+            for (String app : Arrays.asList("Conversations", "Quicksy")) {
+                final File directory = new File(FileBackend.getBackupDirectory(app));
+                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 {
+                            backupFiles.add(BackupFile.read(file));
+                        } 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(line);
+                    if (count % 2 == 1) {
+                        db.execSQL(multiLineQuery.toString());
+                        multiLineQuery = null;
+                    }
+                } else {
+                    if (count % 2 == 0) {
+                        db.execSQL(line);
+                    } else {
+                        multiLineQuery = new StringBuilder(line);
+                    }
+                }
+            }
+            Log.d(Config.LOGTAG, "done reading file");
+            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 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;
+        }
+    }
+
+    public interface OnBackupFilesLoaded {
+        void onBackupFilesLoaded(List<BackupFile> files);
+    }
+
+    public interface OnBackupProcessed {
+        void onBackupRestored();
+        void onBackupDecryptionFailed();
+        void onBackupRestoreFailed();
+    }
+}

src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java πŸ”—

@@ -0,0 +1,125 @@
+package eu.siacs.conversations.ui;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.databinding.DataBindingUtil;
+import android.databinding.ViewDataBinding;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.widget.Toast;
+
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityImportBackupBinding;
+import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
+import eu.siacs.conversations.services.ImportBackupService;
+import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
+
+public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
+
+    private ActivityImportBackupBinding binding;
+
+    private BackupFileAdapter backupFileAdapter;
+    private ImportBackupService service;
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup);
+        setSupportActionBar((Toolbar) binding.toolbar);
+        configureActionBar(getSupportActionBar());
+        this.backupFileAdapter = new BackupFileAdapter();
+        this.binding.list.setAdapter(this.backupFileAdapter);
+        this.backupFileAdapter.setOnItemClickedListener(this);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (this.service != null) {
+            this.service.removeOnBackupProcessedListener(this);
+        }
+        unbindService(this);
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+        ImportBackupService.ImportBackupServiceBinder binder = (ImportBackupService.ImportBackupServiceBinder) service;
+        this.service = binder.getService();
+        this.service.addOnBackupProcessedListener(this);
+        this.service.loadBackupFiles(this);
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        this.service = null;
+    }
+
+    @Override
+    public void onBackupFilesLoaded(final List<ImportBackupService.BackupFile> files) {
+        runOnUiThread(() -> {
+            backupFileAdapter.setFiles(files);
+        });
+    }
+
+    @Override
+    public void onClick(ImportBackupService.BackupFile backupFile) {
+        final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false);
+        Log.d(Config.LOGTAG, "attempting to import " + backupFile.getFile().getAbsolutePath());
+        enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString()));
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setView(enterPasswordBinding.getRoot());
+        builder.setTitle(R.string.enter_password);
+        builder.setNegativeButton(R.string.cancel, null);
+        builder.setPositiveButton(R.string.restore, (dialog, which) -> {
+            final String password = enterPasswordBinding.accountPassword.getEditableText().toString();
+            Intent intent = new Intent(this, ImportBackupService.class);
+            intent.putExtra("password", password);
+            intent.putExtra("file", backupFile.getFile().getAbsolutePath());
+            ContextCompat.startForegroundService(this, intent);
+        });
+        builder.setCancelable(false);
+        builder.create().show();
+    }
+
+    @Override
+    public void onBackupRestored() {
+        runOnUiThread(() -> {
+            Intent intent = new Intent(this, ConversationActivity.class);
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            startActivity(intent);
+            finish();
+        });
+    }
+
+    @Override
+    public void onBackupDecryptionFailed() {
+        runOnUiThread(()-> {
+            Snackbar.make(binding.coordinator,R.string.unable_to_decrypt_backup,Snackbar.LENGTH_LONG).show();
+        });
+    }
+
+    @Override
+    public void onBackupRestoreFailed() {
+        runOnUiThread(()-> {
+            Snackbar.make(binding.coordinator,R.string.unable_to_restore_backup,Snackbar.LENGTH_LONG).show();
+        });
+    }
+}

src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java πŸ”—

@@ -37,362 +37,364 @@ import rocks.xmpp.addr.Jid;
 
 public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState {
 
-	private final String STATE_SELECTED_ACCOUNT = "selected_account";
-
-	protected Account selectedAccount = null;
-	protected Jid selectedAccountJid = null;
-
-	protected final List<Account> accountList = new ArrayList<>();
-	protected ListView accountListView;
-	protected AccountAdapter mAccountAdapter;
-	protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false);
-
-	protected Pair<Integer, Intent> mPostponedActivityResult = null;
-
-	@Override
-	public void onAccountUpdate() {
-		refreshUi();
-	}
-
-	@Override
-	protected void refreshUiReal() {
-		synchronized (this.accountList) {
-			accountList.clear();
-			accountList.addAll(xmppConnectionService.getAccounts());
-		}
-		ActionBar actionBar = getSupportActionBar();
-		if (actionBar != null) {
-			actionBar.setHomeButtonEnabled(this.accountList.size() > 0);
-			actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0);
-		}
-		invalidateOptionsMenu();
-		mAccountAdapter.notifyDataSetChanged();
-	}
-
-	@Override
-	protected void onCreate(Bundle savedInstanceState) {
-
-		super.onCreate(savedInstanceState);
-
-		setContentView(R.layout.activity_manage_accounts);
-		setSupportActionBar(findViewById(R.id.toolbar));
-		configureActionBar(getSupportActionBar());
-		if (savedInstanceState != null) {
-			String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT);
-			if (jid != null) {
-				try {
-					this.selectedAccountJid = Jid.of(jid);
-				} catch (IllegalArgumentException e) {
-					this.selectedAccountJid = null;
-				}
-			}
-		}
-
-		accountListView = findViewById(R.id.account_list);
-		this.mAccountAdapter = new AccountAdapter(this, accountList);
-		accountListView.setAdapter(this.mAccountAdapter);
-		accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position)));
-		registerForContextMenu(accountListView);
-	}
-
-	@Override
-	protected void onStart() {
-		super.onStart();
-		final int theme = findTheme();
-		if (this.mTheme != theme) {
-			recreate();
-		}
-	}
-
-	@Override
-	public void onSaveInstanceState(final Bundle savedInstanceState) {
-		if (selectedAccount != null) {
-			savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toString());
-		}
-		super.onSaveInstanceState(savedInstanceState);
-	}
-
-	@Override
-	public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
-		super.onCreateContextMenu(menu, v, menuInfo);
-		ManageAccountActivity.this.getMenuInflater().inflate(
-				R.menu.manageaccounts_context, menu);
-		AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
-		this.selectedAccount = accountList.get(acmi.position);
-		if (this.selectedAccount.isEnabled()) {
-			menu.findItem(R.id.mgmt_account_enable).setVisible(false);
-			menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp());
-		} else {
-			menu.findItem(R.id.mgmt_account_disable).setVisible(false);
-			menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
-			menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
-		}
-		menu.setHeaderTitle(this.selectedAccount.getJid().asBareJid().toString());
-	}
-
-	@Override
-	void onBackendConnected() {
-		if (selectedAccountJid != null) {
-			this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid);
-		}
-		refreshUiReal();
-		if (this.mPostponedActivityResult != null) {
-			this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
-		}
-		if (Config.X509_VERIFICATION && this.accountList.size() == 0) {
-			if (mInvokedAddAccount.compareAndSet(false, true)) {
-				addAccountFromKey();
-			}
-		}
-	}
-
-	@Override
-	public boolean onCreateOptionsMenu(Menu menu) {
-		getMenuInflater().inflate(R.menu.manageaccounts, menu);
-		MenuItem enableAll = menu.findItem(R.id.action_enable_all);
-		MenuItem addAccount = menu.findItem(R.id.action_add_account);
-		MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert);
-
-		if (Config.X509_VERIFICATION) {
-			addAccount.setVisible(false);
-			addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
-		}
-
-		if (!accountsLeftToEnable()) {
-			enableAll.setVisible(false);
-		}
-		MenuItem disableAll = menu.findItem(R.id.action_disable_all);
-		if (!accountsLeftToDisable()) {
-			disableAll.setVisible(false);
-		}
-		return true;
-	}
-
-	@Override
-	public boolean onContextItemSelected(MenuItem item) {
-		switch (item.getItemId()) {
-			case R.id.mgmt_account_publish_avatar:
-				publishAvatar(selectedAccount);
-				return true;
-			case R.id.mgmt_account_disable:
-				disableAccount(selectedAccount);
-				return true;
-			case R.id.mgmt_account_enable:
-				enableAccount(selectedAccount);
-				return true;
-			case R.id.mgmt_account_delete:
-				deleteAccount(selectedAccount);
-				return true;
-			case R.id.mgmt_account_announce_pgp:
-				publishOpenPGPPublicKey(selectedAccount);
-				return true;
-			default:
-				return super.onContextItemSelected(item);
-		}
-	}
-
-	@Override
-	public boolean onOptionsItemSelected(MenuItem item) {
-		if (MenuDoubleTabUtil.shouldIgnoreTap()) {
-			return false;
-		}
-		switch (item.getItemId()) {
-			case R.id.action_add_account:
-				startActivity(new Intent(getApplicationContext(),
-						EditAccountActivity.class));
-				break;
-			case R.id.action_disable_all:
-				disableAllAccounts();
-				break;
-			case R.id.action_enable_all:
-				enableAllAccounts();
-				break;
-			case R.id.action_add_account_with_cert:
-				addAccountFromKey();
-				break;
-			default:
-				break;
-		}
-		return super.onOptionsItemSelected(item);
-	}
-
-	@Override
-	public boolean onNavigateUp() {
-		if (xmppConnectionService.getConversations().size() == 0) {
-			Intent contactsIntent = new Intent(this,
-					StartConversationActivity.class);
-			contactsIntent.setFlags(
-					// if activity exists in stack, pop the stack and go back to it
-					Intent.FLAG_ACTIVITY_CLEAR_TOP |
-							// otherwise, make a new task for it
-							Intent.FLAG_ACTIVITY_NEW_TASK |
-							// don't use the new activity animation; finish
-							// animation runs instead
-							Intent.FLAG_ACTIVITY_NO_ANIMATION);
-			startActivity(contactsIntent);
-			finish();
-			return true;
-		} else {
-			return super.onNavigateUp();
-		}
-	}
-
-	@Override
-	public void onClickTglAccountState(Account account, boolean enable) {
-		if (enable) {
-			enableAccount(account);
-		} else {
-			disableAccount(account);
-		}
-	}
-
-	private void addAccountFromKey() {
-		try {
-			KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
-		} catch (ActivityNotFoundException e) {
-			Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
-		}
-	}
-
-	private void publishAvatar(Account account) {
-		Intent intent = new Intent(getApplicationContext(),
-				PublishProfilePictureActivity.class);
-		intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString());
-		startActivity(intent);
-	}
-
-	private void disableAllAccounts() {
-		List<Account> list = new ArrayList<>();
-		synchronized (this.accountList) {
-			for (Account account : this.accountList) {
-				if (account.isEnabled()) {
-					list.add(account);
-				}
-			}
-		}
-		for (Account account : list) {
-			disableAccount(account);
-		}
-	}
-
-	private boolean accountsLeftToDisable() {
-		synchronized (this.accountList) {
-			for (Account account : this.accountList) {
-				if (account.isEnabled()) {
-					return true;
-				}
-			}
-			return false;
-		}
-	}
-
-	private boolean accountsLeftToEnable() {
-		synchronized (this.accountList) {
-			for (Account account : this.accountList) {
-				if (!account.isEnabled()) {
-					return true;
-				}
-			}
-			return false;
-		}
-	}
-
-	private void enableAllAccounts() {
-		List<Account> list = new ArrayList<>();
-		synchronized (this.accountList) {
-			for (Account account : this.accountList) {
-				if (!account.isEnabled()) {
-					list.add(account);
-				}
-			}
-		}
-		for (Account account : list) {
-			enableAccount(account);
-		}
-	}
-
-	private void disableAccount(Account account) {
-		account.setOption(Account.OPTION_DISABLED, true);
-		if (!xmppConnectionService.updateAccount(account)) {
-			Toast.makeText(this,R.string.unable_to_update_account,Toast.LENGTH_SHORT).show();
-		}
-	}
-
-	private void enableAccount(Account account) {
-		account.setOption(Account.OPTION_DISABLED, false);
-		final XmppConnection connection = account.getXmppConnection();
-		if (connection != null) {
-			connection.resetEverything();
-		}
-		if (!xmppConnectionService.updateAccount(account)) {
-			Toast.makeText(this,R.string.unable_to_update_account,Toast.LENGTH_SHORT).show();
-		}
-	}
-
-	private void publishOpenPGPPublicKey(Account account) {
-		if (ManageAccountActivity.this.hasPgp()) {
-			announcePgp(selectedAccount, null,null, onOpenPGPKeyPublished);
-		} else {
-			this.showInstallPgpDialog();
-		}
-	}
-
-	private void deleteAccount(final Account account) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(this);
-		builder.setTitle(getString(R.string.mgmt_account_are_you_sure));
-		builder.setIconAttribute(android.R.attr.alertDialogIcon);
-		builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text));
-		builder.setPositiveButton(getString(R.string.delete),
-				(dialog, which) -> {
-					xmppConnectionService.deleteAccount(account);
-					selectedAccount = null;
-					if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
-						WelcomeActivity.launch(this);
-					}
-				});
-		builder.setNegativeButton(getString(R.string.cancel), null);
-		builder.create().show();
-	}
-
-	@Override
-	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-		super.onActivityResult(requestCode, resultCode, data);
-		if (resultCode == RESULT_OK) {
-			if (xmppConnectionServiceBound) {
-				if (requestCode == REQUEST_CHOOSE_PGP_ID) {
-					if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
-						selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
-						announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
-					} else {
-						choosePgpSignId(selectedAccount);
-					}
-				} else if (requestCode == REQUEST_ANNOUNCE_PGP) {
-					announcePgp(selectedAccount, null, data, onOpenPGPKeyPublished);
-				}
-				this.mPostponedActivityResult = null;
-			} else {
-				this.mPostponedActivityResult = new Pair<>(requestCode, data);
-			}
-		}
-	}
-
-	@Override
-	public void alias(String alias) {
-		if (alias != null) {
-			xmppConnectionService.createAccountFromKey(alias, this);
-		}
-	}
-
-	@Override
-	public void onAccountCreated(Account account) {
-		Intent intent = new Intent(this, EditAccountActivity.class);
-		intent.putExtra("jid", account.getJid().asBareJid().toString());
-		intent.putExtra("init", true);
-		startActivity(intent);
-	}
-
-	@Override
-	public void informUser(final int r) {
-		runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show());
-	}
+    private final String STATE_SELECTED_ACCOUNT = "selected_account";
+
+    protected Account selectedAccount = null;
+    protected Jid selectedAccountJid = null;
+
+    protected final List<Account> accountList = new ArrayList<>();
+    protected ListView accountListView;
+    protected AccountAdapter mAccountAdapter;
+    protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false);
+
+    protected Pair<Integer, Intent> mPostponedActivityResult = null;
+
+    @Override
+    public void onAccountUpdate() {
+        refreshUi();
+    }
+
+    @Override
+    protected void refreshUiReal() {
+        synchronized (this.accountList) {
+            accountList.clear();
+            accountList.addAll(xmppConnectionService.getAccounts());
+        }
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setHomeButtonEnabled(this.accountList.size() > 0);
+            actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0);
+        }
+        invalidateOptionsMenu();
+        mAccountAdapter.notifyDataSetChanged();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.activity_manage_accounts);
+        setSupportActionBar(findViewById(R.id.toolbar));
+        configureActionBar(getSupportActionBar());
+        if (savedInstanceState != null) {
+            String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT);
+            if (jid != null) {
+                try {
+                    this.selectedAccountJid = Jid.of(jid);
+                } catch (IllegalArgumentException e) {
+                    this.selectedAccountJid = null;
+                }
+            }
+        }
+
+        accountListView = findViewById(R.id.account_list);
+        this.mAccountAdapter = new AccountAdapter(this, accountList);
+        accountListView.setAdapter(this.mAccountAdapter);
+        accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position)));
+        registerForContextMenu(accountListView);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        final int theme = findTheme();
+        if (this.mTheme != theme) {
+            recreate();
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(final Bundle savedInstanceState) {
+        if (selectedAccount != null) {
+            savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toString());
+        }
+        super.onSaveInstanceState(savedInstanceState);
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+        super.onCreateContextMenu(menu, v, menuInfo);
+        ManageAccountActivity.this.getMenuInflater().inflate(
+                R.menu.manageaccounts_context, menu);
+        AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
+        this.selectedAccount = accountList.get(acmi.position);
+        if (this.selectedAccount.isEnabled()) {
+            menu.findItem(R.id.mgmt_account_enable).setVisible(false);
+            menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp());
+        } else {
+            menu.findItem(R.id.mgmt_account_disable).setVisible(false);
+            menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
+            menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
+        }
+        menu.setHeaderTitle(this.selectedAccount.getJid().asBareJid().toString());
+    }
+
+    @Override
+    void onBackendConnected() {
+        if (selectedAccountJid != null) {
+            this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid);
+        }
+        refreshUiReal();
+        if (this.mPostponedActivityResult != null) {
+            this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
+        }
+        if (Config.X509_VERIFICATION && this.accountList.size() == 0) {
+            if (mInvokedAddAccount.compareAndSet(false, true)) {
+                addAccountFromKey();
+            }
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.manageaccounts, menu);
+        MenuItem enableAll = menu.findItem(R.id.action_enable_all);
+        MenuItem addAccount = menu.findItem(R.id.action_add_account);
+        MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert);
+
+        if (Config.X509_VERIFICATION) {
+            addAccount.setVisible(false);
+            addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+        }
+
+        if (!accountsLeftToEnable()) {
+            enableAll.setVisible(false);
+        }
+        MenuItem disableAll = menu.findItem(R.id.action_disable_all);
+        if (!accountsLeftToDisable()) {
+            disableAll.setVisible(false);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.mgmt_account_publish_avatar:
+                publishAvatar(selectedAccount);
+                return true;
+            case R.id.mgmt_account_disable:
+                disableAccount(selectedAccount);
+                return true;
+            case R.id.mgmt_account_enable:
+                enableAccount(selectedAccount);
+                return true;
+            case R.id.mgmt_account_delete:
+                deleteAccount(selectedAccount);
+                return true;
+            case R.id.mgmt_account_announce_pgp:
+                publishOpenPGPPublicKey(selectedAccount);
+                return true;
+            default:
+                return super.onContextItemSelected(item);
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
+            return false;
+        }
+        switch (item.getItemId()) {
+            case R.id.action_add_account:
+                startActivity(new Intent(this, EditAccountActivity.class));
+                break;
+            case R.id.action_import_backup:
+                startActivity(new Intent(this, ImportBackupActivity.class));
+                break;
+            case R.id.action_disable_all:
+                disableAllAccounts();
+                break;
+            case R.id.action_enable_all:
+                enableAllAccounts();
+                break;
+            case R.id.action_add_account_with_cert:
+                addAccountFromKey();
+                break;
+            default:
+                break;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public boolean onNavigateUp() {
+        if (xmppConnectionService.getConversations().size() == 0) {
+            Intent contactsIntent = new Intent(this,
+                    StartConversationActivity.class);
+            contactsIntent.setFlags(
+                    // if activity exists in stack, pop the stack and go back to it
+                    Intent.FLAG_ACTIVITY_CLEAR_TOP |
+                            // otherwise, make a new task for it
+                            Intent.FLAG_ACTIVITY_NEW_TASK |
+                            // don't use the new activity animation; finish
+                            // animation runs instead
+                            Intent.FLAG_ACTIVITY_NO_ANIMATION);
+            startActivity(contactsIntent);
+            finish();
+            return true;
+        } else {
+            return super.onNavigateUp();
+        }
+    }
+
+    @Override
+    public void onClickTglAccountState(Account account, boolean enable) {
+        if (enable) {
+            enableAccount(account);
+        } else {
+            disableAccount(account);
+        }
+    }
+
+    private void addAccountFromKey() {
+        try {
+            KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    private void publishAvatar(Account account) {
+        Intent intent = new Intent(getApplicationContext(),
+                PublishProfilePictureActivity.class);
+        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString());
+        startActivity(intent);
+    }
+
+    private void disableAllAccounts() {
+        List<Account> list = new ArrayList<>();
+        synchronized (this.accountList) {
+            for (Account account : this.accountList) {
+                if (account.isEnabled()) {
+                    list.add(account);
+                }
+            }
+        }
+        for (Account account : list) {
+            disableAccount(account);
+        }
+    }
+
+    private boolean accountsLeftToDisable() {
+        synchronized (this.accountList) {
+            for (Account account : this.accountList) {
+                if (account.isEnabled()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    private boolean accountsLeftToEnable() {
+        synchronized (this.accountList) {
+            for (Account account : this.accountList) {
+                if (!account.isEnabled()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    private void enableAllAccounts() {
+        List<Account> list = new ArrayList<>();
+        synchronized (this.accountList) {
+            for (Account account : this.accountList) {
+                if (!account.isEnabled()) {
+                    list.add(account);
+                }
+            }
+        }
+        for (Account account : list) {
+            enableAccount(account);
+        }
+    }
+
+    private void disableAccount(Account account) {
+        account.setOption(Account.OPTION_DISABLED, true);
+        if (!xmppConnectionService.updateAccount(account)) {
+            Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    private void enableAccount(Account account) {
+        account.setOption(Account.OPTION_DISABLED, false);
+        final XmppConnection connection = account.getXmppConnection();
+        if (connection != null) {
+            connection.resetEverything();
+        }
+        if (!xmppConnectionService.updateAccount(account)) {
+            Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    private void publishOpenPGPPublicKey(Account account) {
+        if (ManageAccountActivity.this.hasPgp()) {
+            announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
+        } else {
+            this.showInstallPgpDialog();
+        }
+    }
+
+    private void deleteAccount(final Account account) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setTitle(getString(R.string.mgmt_account_are_you_sure));
+        builder.setIconAttribute(android.R.attr.alertDialogIcon);
+        builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text));
+        builder.setPositiveButton(getString(R.string.delete),
+                (dialog, which) -> {
+                    xmppConnectionService.deleteAccount(account);
+                    selectedAccount = null;
+                    if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
+                        WelcomeActivity.launch(this);
+                    }
+                });
+        builder.setNegativeButton(getString(R.string.cancel), null);
+        builder.create().show();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (resultCode == RESULT_OK) {
+            if (xmppConnectionServiceBound) {
+                if (requestCode == REQUEST_CHOOSE_PGP_ID) {
+                    if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
+                        selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
+                        announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished);
+                    } else {
+                        choosePgpSignId(selectedAccount);
+                    }
+                } else if (requestCode == REQUEST_ANNOUNCE_PGP) {
+                    announcePgp(selectedAccount, null, data, onOpenPGPKeyPublished);
+                }
+                this.mPostponedActivityResult = null;
+            } else {
+                this.mPostponedActivityResult = new Pair<>(requestCode, data);
+            }
+        }
+    }
+
+    @Override
+    public void alias(String alias) {
+        if (alias != null) {
+            xmppConnectionService.createAccountFromKey(alias, this);
+        }
+    }
+
+    @Override
+    public void onAccountCreated(Account account) {
+        Intent intent = new Intent(this, EditAccountActivity.class);
+        intent.putExtra("jid", account.getJid().asBareJid().toString());
+        intent.putExtra("init", true);
+        startActivity(intent);
+    }
+
+    @Override
+    public void informUser(final int r) {
+        runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show());
+    }
 }

src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java πŸ”—

@@ -3,89 +3,108 @@ package eu.siacs.conversations.ui;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
 import android.widget.Button;
 
 import java.util.List;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.ImportBackupService;
 import eu.siacs.conversations.utils.XmppUri;
 
 public class WelcomeActivity extends XmppActivity {
 
-	@Override
-	protected void refreshUiReal() {
-
-	}
-
-	@Override
-	void onBackendConnected() {
-
-	}
-
-	@Override
-	public void onStart() {
-		super.onStart();
-		final int theme = findTheme();
-		if (this.mTheme != theme) {
-			recreate();
-		}
-	}
-
-	@Override
-	public void onNewIntent(Intent intent) {
-		if (intent != null) {
-			setIntent(intent);
-		}
-	}
-
-	@Override
-	protected void onCreate(final Bundle savedInstanceState) {
-		if (getResources().getBoolean(R.bool.portrait_only)) {
-			setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
-		}
-		super.onCreate(savedInstanceState);
-		setContentView(R.layout.welcome);
-		setSupportActionBar(findViewById(R.id.toolbar));
-		final ActionBar ab = getSupportActionBar();
-		if (ab != null) {
-			ab.setDisplayShowHomeEnabled(false);
-			ab.setDisplayHomeAsUpEnabled(false);
-		}
-		final Button createAccount = findViewById(R.id.create_account);
-		createAccount.setOnClickListener(v -> {
-			final Intent intent = new Intent(WelcomeActivity.this, MagicCreateActivity.class);
-			intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
-			addInviteUri(intent);
-			startActivity(intent);
-		});
-		final Button useOwnProvider = findViewById(R.id.use_own_provider);
-		useOwnProvider.setOnClickListener(v -> {
-			List<Account> accounts = xmppConnectionService.getAccounts();
-			Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
-			if (accounts.size() == 1) {
-				intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
-				intent.putExtra("init", true);
-			} else if (accounts.size() >= 1) {
-				intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
-			}
-			addInviteUri(intent);
-			startActivity(intent);
-		});
-
-	}
-
-	public void addInviteUri(Intent intent) {
-		StartConversationActivity.addInviteUri(intent, getIntent());
-	}
-
-	public static void launch(AppCompatActivity activity) {
-		Intent intent = new Intent(activity, WelcomeActivity.class);
-		intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-		activity.startActivity(intent);
-		activity.overridePendingTransition(0,0);
-	}
+    @Override
+    protected void refreshUiReal() {
+
+    }
+
+    @Override
+    void onBackendConnected() {
+
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        final int theme = findTheme();
+        if (this.mTheme != theme) {
+            recreate();
+        }
+    }
+
+    @Override
+    public void onNewIntent(Intent intent) {
+        if (intent != null) {
+            setIntent(intent);
+        }
+    }
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        if (getResources().getBoolean(R.bool.portrait_only)) {
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        }
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.welcome);
+        setSupportActionBar(findViewById(R.id.toolbar));
+        final ActionBar ab = getSupportActionBar();
+        if (ab != null) {
+            ab.setDisplayShowHomeEnabled(false);
+            ab.setDisplayHomeAsUpEnabled(false);
+        }
+        final Button createAccount = findViewById(R.id.create_account);
+        createAccount.setOnClickListener(v -> {
+            final Intent intent = new Intent(WelcomeActivity.this, MagicCreateActivity.class);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+            addInviteUri(intent);
+            startActivity(intent);
+        });
+        final Button useOwnProvider = findViewById(R.id.use_own_provider);
+        useOwnProvider.setOnClickListener(v -> {
+            List<Account> accounts = xmppConnectionService.getAccounts();
+            Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
+            if (accounts.size() == 1) {
+                intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
+                intent.putExtra("init", true);
+            } else if (accounts.size() >= 1) {
+                intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
+            }
+            addInviteUri(intent);
+            startActivity(intent);
+        });
+
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.welcome_menu, menu);
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.action_import_backup) {
+            startActivity(new Intent(this, ImportBackupActivity.class));
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    public void addInviteUri(Intent intent) {
+        StartConversationActivity.addInviteUri(intent, getIntent());
+    }
+
+    public static void launch(AppCompatActivity activity) {
+        Intent intent = new Intent(activity, WelcomeActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        activity.startActivity(intent);
+        activity.overridePendingTransition(0, 0);
+    }
 
 }

src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java πŸ”—

@@ -0,0 +1,169 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.content.res.Resources;
+import android.databinding.DataBindingUtil;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.text.format.DateUtils;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.AccountRowBinding;
+import eu.siacs.conversations.services.AvatarService;
+import eu.siacs.conversations.services.ImportBackupService;
+import eu.siacs.conversations.utils.BackupFileHeader;
+import eu.siacs.conversations.utils.UIHelper;
+import rocks.xmpp.addr.Jid;
+
+public class BackupFileAdapter extends RecyclerView.Adapter<BackupFileAdapter.BackupFileViewHolder> {
+
+    private OnItemClickedListener listener;
+
+    private final List<ImportBackupService.BackupFile> files = new ArrayList<>();
+
+
+    @NonNull
+    @Override
+    public BackupFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
+        return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.account_row, viewGroup, false));
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull BackupFileViewHolder backupFileViewHolder, int position) {
+        final ImportBackupService.BackupFile backupFile = files.get(position);
+        final BackupFileHeader header = backupFile.getHeader();
+        backupFileViewHolder.binding.accountJid.setText(header.getJid().asBareJid().toString());
+        backupFileViewHolder.binding.accountStatus.setText(String.format("%s Β· %s",header.getApp(), DateUtils.formatDateTime(backupFileViewHolder.binding.getRoot().getContext(), header.getTimestamp(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)));
+        backupFileViewHolder.binding.tglAccountStatus.setVisibility(View.GONE);
+        backupFileViewHolder.binding.getRoot().setOnClickListener(v -> {
+            if (listener != null) {
+                listener.onClick(backupFile);
+            }
+        });
+        loadAvatar(header.getJid(), backupFileViewHolder.binding.accountImage);
+    }
+
+    @Override
+    public int getItemCount() {
+        return files.size();
+    }
+
+    public void setFiles(List<ImportBackupService.BackupFile> files) {
+        this.files.clear();
+        this.files.addAll(files);
+        notifyDataSetChanged();
+    }
+
+    public void setOnItemClickedListener(OnItemClickedListener listener) {
+        this.listener = listener;
+    }
+
+    static class BackupFileViewHolder extends RecyclerView.ViewHolder {
+        private final AccountRowBinding binding;
+
+        BackupFileViewHolder(AccountRowBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+        }
+
+    }
+
+    public interface OnItemClickedListener {
+        void onClick(ImportBackupService.BackupFile backupFile);
+    }
+
+    static class BitmapWorkerTask extends AsyncTask<Jid, Void, Bitmap> {
+        private final WeakReference<ImageView> imageViewReference;
+        private Jid jid  = null;
+        private final int size;
+
+        BitmapWorkerTask(ImageView imageView) {
+            imageViewReference = new WeakReference<>(imageView);
+            DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics();
+		this.size = ((int) (48 * metrics.density));
+        }
+
+        @Override
+        protected Bitmap doInBackground(Jid... params) {
+            this.jid = params[0];
+            return AvatarService.get(this.jid, size);
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap bitmap) {
+            if (bitmap != null && !isCancelled()) {
+                final ImageView imageView = imageViewReference.get();
+                if (imageView != null) {
+                    imageView.setImageBitmap(bitmap);
+                    imageView.setBackgroundColor(0x00000000);
+                }
+            }
+        }
+    }
+
+    private void loadAvatar(Jid jid, ImageView imageView) {
+        if (cancelPotentialWork(jid, imageView)) {
+            imageView.setBackgroundColor(UIHelper.getColorForName(jid.asBareJid().toString()));
+            imageView.setImageDrawable(null);
+            final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+            final AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getContext().getResources(), null, task);
+            imageView.setImageDrawable(asyncDrawable);
+            try {
+                task.execute(jid);
+            } catch (final RejectedExecutionException ignored) {
+            }
+        }
+    }
+
+    private static boolean cancelPotentialWork(Jid jid, ImageView imageView) {
+        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+        if (bitmapWorkerTask != null) {
+            final Jid oldJid = bitmapWorkerTask.jid;
+            if (oldJid == null || jid != oldJid) {
+                bitmapWorkerTask.cancel(true);
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+        if (imageView != null) {
+            final Drawable drawable = imageView.getDrawable();
+            if (drawable instanceof AsyncDrawable) {
+                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+                return asyncDrawable.getBitmapWorkerTask();
+            }
+        }
+        return null;
+    }
+
+    static class AsyncDrawable extends BitmapDrawable {
+        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+        AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+            super(res, bitmap);
+            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+        }
+
+        BitmapWorkerTask getBitmapWorkerTask() {
+            return bitmapWorkerTaskReference.get();
+        }
+    }
+
+}

src/conversations/res/layout/activity_import_backup.xml πŸ”—

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:background="?attr/color_background_primary"
+        android:orientation="vertical">
+
+        <include
+            android:id="@+id/toolbar"
+            layout="@layout/toolbar" />
+
+        <android.support.design.widget.CoordinatorLayout
+            android:id="@+id/coordinator"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="?attr/color_background_primary">
+
+            <android.support.v7.widget.RecyclerView
+                android:id="@+id/list"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="?attr/color_background_primary"
+                android:orientation="vertical"
+                app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
+        </android.support.design.widget.CoordinatorLayout>
+
+    </LinearLayout>
+</layout>

src/conversations/res/layout/dialog_enter_password.xml πŸ”—

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:padding="?dialogPreferredPadding">
+
+        <TextView
+            android:id="@+id/explain"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/enter_password_to_restore"
+            android:textAppearance="@style/TextAppearance.Conversations.Body2"/>
+
+        <TextView
+            android:layout_marginTop="?TextSizeBody1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/restore_warning"
+            android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+
+        <android.support.design.widget.TextInputLayout
+            android:id="@+id/account_password_layout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            app:passwordToggleDrawable="@drawable/visibility_toggle_drawable"
+            app:passwordToggleEnabled="true"
+            app:passwordToggleTint="?android:textColorSecondary"
+            app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"
+            app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error">
+
+        <eu.siacs.conversations.ui.widget.TextInputEditText
+            android:id="@+id/account_password"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/password"
+            android:inputType="textPassword"
+            android:textColor="?attr/edit_text_color"
+            style="@style/Widget.Conversations.EditText"/>
+
+        </android.support.design.widget.TextInputLayout>
+    </LinearLayout>
+</layout>

src/main/res/menu/manageaccounts.xml β†’ src/conversations/res/menu/manageaccounts.xml πŸ”—

@@ -7,6 +7,10 @@
 		android:icon="?attr/icon_add_person"
 		app:showAsAction="always"
 		android:title="@string/action_add_account"/>
+	<item
+		android:id="@+id/action_import_backup"
+		app:showAsAction="never"
+		android:title="@string/restore_backup"/>
 	<item
 		android:id="@+id/action_add_account_with_cert"
 		app:showAsAction="never"

src/conversations/res/menu/welcome_menu.xml πŸ”—

@@ -0,0 +1,8 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_import_backup"
+        app:showAsAction="never"
+        android:title="@string/restore_backup"/>
+</menu>

src/main/AndroidManifest.xml πŸ”—

@@ -245,7 +245,8 @@
         <activity android:name=".ui.MediaBrowserActivity"
             android:label="@string/media_browser"/>
 
-        <service android:name=".services.ExportLogsService"/>
+        <service android:name=".services.ExportBackupService"/>
+        <service android:name=".services.ImportBackupService"/>
         <service
             android:name=".services.ContactChooserTargetService"
             android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">

src/main/java/eu/siacs/conversations/persistance/FileBackend.java πŸ”—

@@ -150,8 +150,12 @@ public class FileBackend {
         return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + context.getString(R.string.app_name) + "/Media/";
     }
 
-    public static String getConversationsLogsDirectory() {
-        return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Conversations/";
+    public static String getBackupDirectory(Context context) {
+        return getBackupDirectory(context.getString(R.string.app_name));
+    }
+
+    public static String getBackupDirectory(String app) {
+        return Environment.getExternalStorageDirectory().getAbsolutePath() + "/"+app+"/Backup/";
     }
 
     private static Bitmap rotate(Bitmap bitmap, int degree) {

src/main/java/eu/siacs/conversations/services/AvatarService.java πŸ”—

@@ -511,7 +511,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();
@@ -528,7 +532,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);
@@ -591,7 +595,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);

src/main/java/eu/siacs/conversations/services/ExportBackupService.java πŸ”—

@@ -0,0 +1,281 @@
+package eu.siacs.conversations.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 eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.utils.BackupFileHeader;
+import eu.siacs.conversations.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 AtomicBoolean running = new AtomicBoolean(false);
+    private DatabaseBackend mDatabaseBackend;
+    private List<Account> mAccounts;
+    private NotificationManager notificationManager;
+
+    @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_NOT_STICKY;
+    }
+
+    private static void accountExport(SQLiteDatabase db, String uuid, PrintWriter writer) {
+        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);
+                    Log.d(Config.LOGTAG,"reading int value. "+intValue);
+                    if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) {
+                        intValue |= 1 << Account.OPTION_DISABLED;
+                        Log.d(Config.LOGTAG,"modified int value "+intValue);
+                    }
+                    builder.append(intValue);
+                } else {
+                    DatabaseUtils.appendEscapedSQLString(builder, value);
+                }
+            }
+            builder.append(")");
+            builder.append(';');
+            builder.append('\n');
+        }
+        Log.d(Config.LOGTAG,builder.toString());
+        if (accountCursor != null) {
+            accountCursor.close();
+        }
+        writer.append(builder.toString());
+    }
+
+    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, 20));
+            if (i + 20 > size) {
+                i = size;
+            } else {
+                i += 20;
+            }
+            final int percentage = i * 100 / size;
+            if (p < percentage) {
+                p = percentage;
+                notificationManager.notify(NOTIFICATION_ID,progress.build(p));
+                Log.d(Config.LOGTAG, "percentage=" + p);
+            }
+        }
+        if (cursor != null) {
+            cursor.close();
+        }
+    }
+
+    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, 20));
+        }
+        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(this)+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);
+        }
+    }
+
+    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) {
+        StringBuilder builder = new StringBuilder();
+        builder.append("INSERT 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 (!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 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();
+        }
+    }
+}

src/main/java/eu/siacs/conversations/services/ExportLogsService.java πŸ”—

@@ -1,148 +0,0 @@
-package eu.siacs.conversations.services;
-
-import android.app.NotificationManager;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-import android.support.v4.app.NotificationCompat;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.persistance.DatabaseBackend;
-import eu.siacs.conversations.persistance.FileBackend;
-import rocks.xmpp.addr.Jid;
-
-public class ExportLogsService extends Service {
-
-    private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
-    private static final String DIRECTORY_STRING_FORMAT = FileBackend.getConversationsLogsDirectory() + "/logs/%s";
-    private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n";
-    private static final int NOTIFICATION_ID = 1;
-    private static AtomicBoolean running = new AtomicBoolean(false);
-    private DatabaseBackend mDatabaseBackend;
-    private List<Account> mAccounts;
-
-    @Override
-    public void onCreate() {
-        mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
-        mAccounts = mDatabaseBackend.getAccounts();
-    }
-
-    @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_NOT_STICKY;
-    }
-
-    private void export() {
-        List<Conversation> conversations = mDatabaseBackend.getConversations(Conversation.STATUS_AVAILABLE);
-        conversations.addAll(mDatabaseBackend.getConversations(Conversation.STATUS_ARCHIVED));
-        NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "export");
-        mBuilder.setContentTitle(getString(R.string.notification_export_logs_title))
-                .setSmallIcon(R.drawable.ic_import_export_white_24dp)
-                .setProgress(conversations.size(), 0, false);
-        startForeground(NOTIFICATION_ID, mBuilder.build());
-
-        int progress = 0;
-        for (Conversation conversation : conversations) {
-            writeToFile(conversation);
-            progress++;
-            mBuilder.setProgress(conversations.size(), progress, false);
-            if (mNotifyManager != null) {
-                mNotifyManager.notify(NOTIFICATION_ID, mBuilder.build());
-            }
-        }
-    }
-
-    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();
-        }
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return null;
-    }
-}

src/main/java/eu/siacs/conversations/services/NotificationService.java πŸ”—

@@ -112,6 +112,8 @@ public class NotificationService {
             return;
         }
 
+        notificationManager.deleteNotificationChannel("export");
+
         notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information)));
         notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages)));
         final NotificationChannel foregroundServiceChannel = new NotificationChannel("foreground",
@@ -136,8 +138,8 @@ public class NotificationService {
         videoCompressionChannel.setGroup("status");
         notificationManager.createNotificationChannel(videoCompressionChannel);
 
-        final NotificationChannel exportChannel = new NotificationChannel("export",
-                c.getString(R.string.export_channel_name),
+        final NotificationChannel exportChannel = new NotificationChannel("backup",
+                c.getString(R.string.backup_channel_name),
                 NotificationManager.IMPORTANCE_LOW);
         exportChannel.setShowBadge(false);
         exportChannel.setGroup("status");

src/main/java/eu/siacs/conversations/ui/SettingsActivity.java πŸ”—

@@ -21,8 +21,6 @@ import android.preference.PreferenceManager;
 import android.preference.PreferenceScreen;
 import android.provider.MediaStore;
 import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
 import android.widget.Toast;
 
 import java.io.File;
@@ -36,7 +34,8 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.OmemoSetting;
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.services.ExportLogsService;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.ExportBackupService;
 import eu.siacs.conversations.services.MemorizingTrustManager;
 import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.ui.util.StyledAttributes;
@@ -59,7 +58,7 @@ public class SettingsActivity extends XmppActivity implements
 	public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
 	public static final String OMEMO_SETTING = "omemo";
 
-	public static final int REQUEST_WRITE_LOGS = 0xbf8701;
+	public static final int REQUEST_CREATE_BACKUP = 0xbf8701;
 	private SettingsFragment mSettingsFragment;
 
 	@Override
@@ -219,11 +218,12 @@ public class SettingsActivity extends XmppActivity implements
 			});
 		}
 
-		final Preference exportLogsPreference = mSettingsFragment.findPreference("export_logs");
-		if (exportLogsPreference != null) {
-			exportLogsPreference.setOnPreferenceClickListener(preference -> {
-				if (hasStoragePermission(REQUEST_WRITE_LOGS)) {
-					startExport();
+		final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup");
+		if (createBackupPreference != null) {
+			createBackupPreference.setSummary(getString(R.string.pref_create_backup_summary, FileBackend.getBackupDirectory(this)));
+			createBackupPreference.setOnPreferenceClickListener(preference -> {
+				if (hasStoragePermission(REQUEST_CREATE_BACKUP)) {
+					createBackup();
 				}
 				return true;
 			});
@@ -399,16 +399,16 @@ public class SettingsActivity extends XmppActivity implements
 	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
 		if (grantResults.length > 0)
 			if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-				if (requestCode == REQUEST_WRITE_LOGS) {
-					startExport();
+				if (requestCode == REQUEST_CREATE_BACKUP) {
+					createBackup();
 				}
 			} else {
 				Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
 			}
 	}
 
-	private void startExport() {
-		ContextCompat.startForegroundService(this, new Intent(this, ExportLogsService.class));
+	private void createBackup() {
+		ContextCompat.startForegroundService(this, new Intent(this, ExportBackupService.class));
 	}
 
 	private void displayToast(final String msg) {

src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java πŸ”—

@@ -1,18 +1,17 @@
 package eu.siacs.conversations.ui.adapter;
 
-import android.content.Context;
 import android.content.res.Resources;
+import android.databinding.DataBindingUtil;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
-import android.support.v7.widget.SwitchCompat;
+import android.support.annotation.NonNull;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.ImageView;
-import android.widget.TextView;
 
 import java.lang.ref.WeakReference;
 import java.util.List;
@@ -20,6 +19,7 @@ import java.util.concurrent.RejectedExecutionException;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.AccountRowBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.StyledAttributes;
@@ -27,155 +27,163 @@ import eu.siacs.conversations.utils.UIHelper;
 
 public class AccountAdapter extends ArrayAdapter<Account> {
 
-	private XmppActivity activity;
-	private boolean showStateButton;
-
-	public AccountAdapter(XmppActivity activity, List<Account> objects, boolean showStateButton) {
-		super(activity, 0, objects);
-		this.activity = activity;
-		this.showStateButton = showStateButton;
-	}
-
-	public AccountAdapter(XmppActivity activity, List<Account> objects) {
-		super(activity, 0, objects);
-		this.activity = activity;
-		this.showStateButton = true;
-	}
-
-	@Override
-	public View getView(int position, View view, ViewGroup parent) {
-		final Account account = getItem(position);
-		if (view == null) {
-			LayoutInflater inflater = (LayoutInflater) getContext()
-					.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-			view = inflater.inflate(R.layout.account_row, parent, false);
-		}
-		TextView jid = view.findViewById(R.id.account_jid);
-		if (Config.DOMAIN_LOCK != null) {
-			jid.setText(account.getJid().getLocal());
-		} else {
-			jid.setText(account.getJid().asBareJid().toString());
-		}
-		TextView statusView = view.findViewById(R.id.account_status);
-		ImageView imageView = view.findViewById(R.id.account_image);
-		loadAvatar(account, imageView);
-		statusView.setText(getContext().getString(account.getStatus().getReadableId()));
-		switch (account.getStatus()) {
-			case ONLINE:
-				statusView.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline));
-				break;
-			case DISABLED:
-			case CONNECTING:
-				statusView.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary));
-				break;
-			default:
-				statusView.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError));
-				break;
-		}
-		final SwitchCompat tglAccountState = view.findViewById(R.id.tgl_account_status);
-		final boolean isDisabled = (account.getStatus() == Account.State.DISABLED);
-		tglAccountState.setOnCheckedChangeListener(null);
-		tglAccountState.setChecked(!isDisabled);
-		if (this.showStateButton) {
-			tglAccountState.setVisibility(View.VISIBLE);
-		} else {
-			tglAccountState.setVisibility(View.GONE);
-		}
-		tglAccountState.setOnCheckedChangeListener((compoundButton, b) -> {
-			if (b == isDisabled && activity instanceof OnTglAccountState) {
-				((OnTglAccountState) activity).onClickTglAccountState(account, b);
-			}
-		});
-		return view;
-	}
-
-	class BitmapWorkerTask extends AsyncTask<Account, Void, Bitmap> {
-		private final WeakReference<ImageView> imageViewReference;
-		private Account account = null;
-
-		public BitmapWorkerTask(ImageView imageView) {
-			imageViewReference = new WeakReference<>(imageView);
-		}
-
-		@Override
-		protected Bitmap doInBackground(Account... params) {
-			this.account = params[0];
-			return activity.avatarService().get(this.account, activity.getPixel(48), isCancelled());
-		}
-
-		@Override
-		protected void onPostExecute(Bitmap bitmap) {
-			if (bitmap != null && !isCancelled()) {
-				final ImageView imageView = imageViewReference.get();
-				if (imageView != null) {
-					imageView.setImageBitmap(bitmap);
-					imageView.setBackgroundColor(0x00000000);
-				}
-			}
-		}
-	}
-
-	public void loadAvatar(Account account, ImageView imageView) {
-		if (cancelPotentialWork(account, imageView)) {
-			final Bitmap bm = activity.avatarService().get(account, activity.getPixel(48), true);
-			if (bm != null) {
-				cancelPotentialWork(account, imageView);
-				imageView.setImageBitmap(bm);
-				imageView.setBackgroundColor(0x00000000);
-			} else {
-				imageView.setBackgroundColor(UIHelper.getColorForName(account.getJid().asBareJid().toString()));
-				imageView.setImageDrawable(null);
-				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
-				final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
-				imageView.setImageDrawable(asyncDrawable);
-				try {
-					task.execute(account);
-				} catch (final RejectedExecutionException ignored) {
-				}
-			}
-		}
-	}
-
-
-	public interface OnTglAccountState {
-		void onClickTglAccountState(Account account, boolean state);
-	}
-
-	public static boolean cancelPotentialWork(Account account, ImageView imageView) {
-		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
-
-		if (bitmapWorkerTask != null) {
-			final Account oldAccount = bitmapWorkerTask.account;
-			if (oldAccount == null || account != oldAccount) {
-				bitmapWorkerTask.cancel(true);
-			} else {
-				return false;
-			}
-		}
-		return true;
-	}
-
-	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
-		if (imageView != null) {
-			final Drawable drawable = imageView.getDrawable();
-			if (drawable instanceof AsyncDrawable) {
-				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
-				return asyncDrawable.getBitmapWorkerTask();
-			}
-		}
-		return null;
-	}
-
-	static class AsyncDrawable extends BitmapDrawable {
-		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
-
-		public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
-			super(res, bitmap);
-			bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
-		}
-
-		public BitmapWorkerTask getBitmapWorkerTask() {
-			return bitmapWorkerTaskReference.get();
-		}
-	}
+    private XmppActivity activity;
+    private boolean showStateButton;
+
+    public AccountAdapter(XmppActivity activity, List<Account> objects, boolean showStateButton) {
+        super(activity, 0, objects);
+        this.activity = activity;
+        this.showStateButton = showStateButton;
+    }
+
+    public AccountAdapter(XmppActivity activity, List<Account> objects) {
+        super(activity, 0, objects);
+        this.activity = activity;
+        this.showStateButton = true;
+    }
+
+    @Override
+    public View getView(int position, View view, @NonNull ViewGroup parent) {
+        final Account account = getItem(position);
+        final ViewHolder viewHolder;
+        if (view == null) {
+            AccountRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.account_row, parent, false);
+            view = binding.getRoot();
+            viewHolder = new ViewHolder(binding);
+            view.setTag(viewHolder);
+        } else {
+            viewHolder = (ViewHolder) view.getTag();
+        }
+        if (Config.DOMAIN_LOCK != null) {
+            viewHolder.binding.accountJid.setText(account.getJid().getLocal());
+        } else {
+            viewHolder.binding.accountJid.setText(account.getJid().asBareJid().toString());
+        }
+        loadAvatar(account, viewHolder.binding.accountImage);
+        viewHolder.binding.accountStatus.setText(getContext().getString(account.getStatus().getReadableId()));
+        switch (account.getStatus()) {
+            case ONLINE:
+                viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline));
+                break;
+            case DISABLED:
+            case CONNECTING:
+                viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary));
+                break;
+            default:
+                viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError));
+                break;
+        }
+        final boolean isDisabled = (account.getStatus() == Account.State.DISABLED);
+        viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener(null);
+        viewHolder.binding.tglAccountStatus.setChecked(!isDisabled);
+        if (this.showStateButton) {
+            viewHolder.binding.tglAccountStatus.setVisibility(View.VISIBLE);
+        } else {
+            viewHolder.binding.tglAccountStatus.setVisibility(View.GONE);
+        }
+        viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener((compoundButton, b) -> {
+            if (b == isDisabled && activity instanceof OnTglAccountState) {
+                ((OnTglAccountState) activity).onClickTglAccountState(account, b);
+            }
+        });
+        return view;
+    }
+
+    private static class ViewHolder {
+        private final AccountRowBinding binding;
+
+        private ViewHolder(AccountRowBinding binding) {
+            this.binding = binding;
+        }
+    }
+
+    class BitmapWorkerTask extends AsyncTask<Account, Void, Bitmap> {
+        private final WeakReference<ImageView> imageViewReference;
+        private Account account = null;
+
+        public BitmapWorkerTask(ImageView imageView) {
+            imageViewReference = new WeakReference<>(imageView);
+        }
+
+        @Override
+        protected Bitmap doInBackground(Account... params) {
+            this.account = params[0];
+            return activity.avatarService().get(this.account, activity.getPixel(48), isCancelled());
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap bitmap) {
+            if (bitmap != null && !isCancelled()) {
+                final ImageView imageView = imageViewReference.get();
+                if (imageView != null) {
+                    imageView.setImageBitmap(bitmap);
+                    imageView.setBackgroundColor(0x00000000);
+                }
+            }
+        }
+    }
+
+    public void loadAvatar(Account account, ImageView imageView) {
+        if (cancelPotentialWork(account, imageView)) {
+            final Bitmap bm = activity.avatarService().get(account, activity.getPixel(48), true);
+            if (bm != null) {
+                cancelPotentialWork(account, imageView);
+                imageView.setImageBitmap(bm);
+                imageView.setBackgroundColor(0x00000000);
+            } else {
+                imageView.setBackgroundColor(UIHelper.getColorForName(account.getJid().asBareJid().toString()));
+                imageView.setImageDrawable(null);
+                final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+                final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
+                imageView.setImageDrawable(asyncDrawable);
+                try {
+                    task.execute(account);
+                } catch (final RejectedExecutionException ignored) {
+                }
+            }
+        }
+    }
+
+
+    public interface OnTglAccountState {
+        void onClickTglAccountState(Account account, boolean state);
+    }
+
+    public static boolean cancelPotentialWork(Account account, ImageView imageView) {
+        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+        if (bitmapWorkerTask != null) {
+            final Account oldAccount = bitmapWorkerTask.account;
+            if (oldAccount == null || account != oldAccount) {
+                bitmapWorkerTask.cancel(true);
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+        if (imageView != null) {
+            final Drawable drawable = imageView.getDrawable();
+            if (drawable instanceof AsyncDrawable) {
+                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+                return asyncDrawable.getBitmapWorkerTask();
+            }
+        }
+        return null;
+    }
+
+    static class AsyncDrawable extends BitmapDrawable {
+        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+        public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+            super(res, bitmap);
+            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+        }
+
+        public BitmapWorkerTask getBitmapWorkerTask() {
+            return bitmapWorkerTaskReference.get();
+        }
+    }
 }

src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java πŸ”—

@@ -0,0 +1,85 @@
+package eu.siacs.conversations.utils;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import rocks.xmpp.addr.Jid;
+
+public class BackupFileHeader {
+
+    private static final int VERSION = 1;
+
+    private String app;
+    private Jid jid;
+    private long timestamp;
+    private byte[] iv;
+    private byte[] salt;
+
+
+    @Override
+    public String toString() {
+        return "BackupFileHeader{" +
+                "app='" + app + '\'' +
+                ", jid=" + jid +
+                ", timestamp=" + timestamp +
+                ", iv=" + CryptoHelper.bytesToHex(iv) +
+                ", salt=" + CryptoHelper.bytesToHex(salt) +
+                '}';
+    }
+
+    public BackupFileHeader(String app, Jid jid, long timestamp, byte[] iv, byte[] salt) {
+        this.app = app;
+        this.jid = jid;
+        this.timestamp = timestamp;
+        this.iv = iv;
+        this.salt = salt;
+    }
+
+    public void write(DataOutputStream dataOutputStream) throws IOException {
+        dataOutputStream.writeInt(VERSION);
+        dataOutputStream.writeUTF(app);
+        dataOutputStream.writeUTF(jid.asBareJid().toEscapedString());
+        dataOutputStream.writeLong(timestamp);
+        dataOutputStream.write(iv);
+        dataOutputStream.write(salt);
+    }
+
+    public static BackupFileHeader read(DataInputStream inputStream) throws IOException {
+        final int version = inputStream.readInt();
+        if (version > VERSION) {
+            throw new IllegalArgumentException("Backup File version was "+version+" but app only supports up to version "+VERSION);
+        }
+        String app = inputStream.readUTF();
+        String jid = inputStream.readUTF();
+        long timestamp = inputStream.readLong();
+        byte[] iv = new byte[12];
+        inputStream.readFully(iv);
+        byte[] salt = new byte[16];
+        inputStream.readFully(salt);
+
+        return new BackupFileHeader(app,Jid.of(jid),timestamp,iv,salt);
+
+    }
+
+    public byte[] getSalt() {
+        return salt;
+    }
+
+    public byte[] getIv() {
+        return iv;
+    }
+
+    public Jid getJid() {
+        return jid;
+    }
+
+    public String getApp() {
+        return app;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+}

src/main/res/layout/account_row.xml πŸ”—

@@ -1,55 +1,57 @@
 <?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                xmlns:app="http://schemas.android.com/apk/res-auto"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="?android:attr/activatedBackgroundIndicator"
-                android:paddingLeft="8dp"
-                android:paddingBottom="8dp"
-                android:paddingTop="8dp">
-
-    <com.makeramen.roundedimageview.RoundedImageView
-        android:id="@+id/account_image"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
-        android:layout_alignParentLeft="true"
-        android:contentDescription="@string/account_image_description"
-        app:riv_corner_radius="2dp" />
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
-    <LinearLayout
-        android:layout_width="fill_parent"
+    <RelativeLayout
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_centerVertical="true"
-        android:layout_toRightOf="@+id/account_image"
-        android:orientation="vertical"
-        android:paddingLeft="@dimen/avatar_item_distance"
-        android:layout_toLeftOf="@+id/tgl_account_status"
-        android:layout_toStartOf="@+id/tgl_account_status">
+        android:background="?android:selectableItemBackground"
+        android:paddingLeft="8dp"
+        android:paddingBottom="8dp"
+        android:paddingTop="8dp">
 
-        <TextView
-            android:id="@+id/account_jid"
-            android:layout_width="wrap_content"
+        <com.makeramen.roundedimageview.RoundedImageView
+            android:id="@+id/account_image"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_alignParentLeft="true"
+            android:contentDescription="@string/account_image_description"
+            app:riv_corner_radius="2dp" />
+
+        <LinearLayout
+            android:layout_width="fill_parent"
             android:layout_height="wrap_content"
-            android:scrollHorizontally="false"
-            android:singleLine="true"
-            android:textAppearance="@style/TextAppearance.Conversations.Subhead"/>
+            android:layout_centerVertical="true"
+            android:layout_toRightOf="@+id/account_image"
+            android:orientation="vertical"
+            android:paddingLeft="@dimen/avatar_item_distance"
+            android:layout_toLeftOf="@+id/tgl_account_status"
+            android:layout_toStartOf="@+id/tgl_account_status">
+
+            <TextView
+                android:id="@+id/account_jid"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:scrollHorizontally="false"
+                android:singleLine="true"
+                android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
+
+            <TextView
+                android:id="@+id/account_status"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/account_status_unknown"
+                android:textAppearance="@style/TextAppearance.Conversations.Body2" />
+        </LinearLayout>
 
-        <TextView
-            android:id="@+id/account_status"
+        <android.support.v7.widget.SwitchCompat
+            android:id="@+id/tgl_account_status"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="@string/account_status_unknown"
-            android:textAppearance="@style/TextAppearance.Conversations.Body2"
-             />
-    </LinearLayout>
-
-    <android.support.v7.widget.SwitchCompat
-        android:id="@+id/tgl_account_status"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentRight="true"
-        android:layout_centerVertical="true"
-        android:padding="16dp"
-        android:focusable="false"/>
+            android:layout_alignParentRight="true"
+            android:layout_centerVertical="true"
+            android:padding="16dp"
+            android:focusable="false" />
 
-</RelativeLayout>
+    </RelativeLayout>
+</layout>

src/main/res/layout/dialog_quickedit.xml πŸ”—

@@ -6,9 +6,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="vertical"
-        android:paddingLeft="?attr/dialog_horizontal_padding"
-        android:paddingRight="?attr/dialog_horizontal_padding"
-        android:paddingTop="?attr/dialog_vertical_padding">
+        android:padding="?dialogPreferredPadding">
 
         <android.support.design.widget.TextInputLayout
             android:id="@+id/input_layout"

src/main/res/values/strings.xml πŸ”—

@@ -314,9 +314,12 @@
     <string name="try_again">Try again</string>
     <string name="pref_keep_foreground_service">Keep service in foreground</string>
     <string name="pref_keep_foreground_service_summary">Prevents the operating system from killing your connection</string>
-    <string name="pref_export_logs">Export history</string>
-    <string name="pref_export_logs_summary">Write conversations history logs to SD card</string>
-    <string name="notification_export_logs_title">Writing logs to SD card</string>
+    <string name="pref_create_backup">Create backup</string>
+    <string name="pref_create_backup_summary">Write backup files to %s</string>
+    <string name="notification_create_backup_title">Creating backup files</string>
+    <string name="notification_restore_backup_title">Restoring backup</string>
+    <string name="notification_restored_backup_title">Your backup has been restored</string>
+    <string name="notification_restored_backup_subtitle">Do not forget to enable the account.</string>
     <string name="choose_file">Choose file</string>
     <string name="receiving_x_file">Receiving %1$s (%2$d%% completed)</string>
     <string name="download_x_file">Download %s</string>
@@ -747,7 +750,6 @@
     <string name="video_compression_channel_name">Video compression</string>
     <string name="view_media">View media</string>
     <string name="media_browser">Media browser</string>
-    <string name="export_channel_name">History export</string>
     <string name="security_violation_not_attaching_file">File omitted due to security violation.</string>
     <string name="pref_video_compression">Video Quality</string>
     <string name="pref_video_compression_summary">Lower quality means smaller files</string>
@@ -811,4 +813,11 @@
     <string name="open_with">Open with…</string>
     <string name="set_profile_picture">Conversations profile picture</string>
     <string name="choose_account">Choose account</string>
+    <string name="restore_backup">Restore backup</string>
+    <string name="restore">Restore</string>
+    <string name="enter_password_to_restore">Enter your password for the account %s to restore the backup.</string>
+    <string name="restore_warning">Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device.</string>
+    <string name="unable_to_restore_backup">Unable to restore backup</string>
+    <string name="unable_to_decrypt_backup">Unable to decrypt backup</string>
+    <string name="backup_channel_name"><![CDATA[Backup & Restore]]></string>
 </resources>

src/main/res/values/themes.xml πŸ”—

@@ -95,7 +95,6 @@
         <item type="reference" name="icon_search">@drawable/ic_search_white_24dp</item>
         <item type="reference" name="icon_secure">@drawable/ic_lock_open_white_24dp</item>
         <item type="reference" name="icon_settings">@drawable/ic_settings_black_24dp</item>
-        <item type="reference" name="icon_import_export">@drawable/ic_import_export_white_24dp</item>
         <item type="reference" name="icon_share">@drawable/ic_share_white_24dp</item>
         <item type="reference" name="icon_scan_qr_code">@drawable/ic_qr_code_scan_white_24dp</item>
         <item type="reference" name="icon_scroll_down">@drawable/ic_scroll_to_end_black</item>
@@ -208,7 +207,6 @@
         <item type="reference" name="icon_search">@drawable/ic_search_white_24dp</item>
         <item type="reference" name="icon_secure">@drawable/ic_lock_open_white_24dp</item>
         <item type="reference" name="icon_settings">@drawable/ic_settings_white_24dp</item>
-        <item type="reference" name="icon_import_export">@drawable/ic_import_export_white_24dp</item>
         <item type="reference" name="icon_share">@drawable/ic_share_white_24dp</item>
         <item type="reference" name="icon_scan_qr_code">@drawable/ic_qr_code_scan_white_24dp</item>
         <item type="reference" name="icon_scroll_down">@drawable/ic_scroll_to_end_white</item>

src/main/res/xml/preferences.xml πŸ”—

@@ -330,9 +330,9 @@
                     android:summary="@string/pref_keep_foreground_service_summary"
                     android:title="@string/pref_keep_foreground_service" />
                 <Preference
-                    android:key="export_logs"
-                    android:summary="@string/pref_export_logs_summary"
-                    android:title="@string/pref_export_logs" />
+                    android:key="create_backup"
+                    android:summary="@string/pref_create_backup_summary"
+                    android:title="@string/pref_create_backup" />
             </PreferenceCategory>
         </PreferenceScreen>