diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml
index d987d9062..2c01f6d6c 100644
--- a/src/conversations/AndroidManifest.xml
+++ b/src/conversations/AndroidManifest.xml
@@ -16,6 +16,10 @@
android:name=".ui.MagicCreateActivity"
android:label="@string/create_account"
android:launchMode="singleTask"/>
+
diff --git a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java
new file mode 100644
index 000000000..d7deb599c
--- /dev/null
+++ b/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 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 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 files);
+ }
+
+ public interface OnBackupProcessed {
+ void onBackupRestored();
+ void onBackupDecryptionFailed();
+ void onBackupRestoreFailed();
+ }
+}
\ No newline at end of file
diff --git a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java
new file mode 100644
index 000000000..153fc76a3
--- /dev/null
+++ b/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 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();
+ });
+ }
+}
diff --git a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java
index e1330fe88..c0d2d7eff 100644
--- a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java
+++ b/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";
+ private final String STATE_SELECTED_ACCOUNT = "selected_account";
- protected Account selectedAccount = null;
- protected Jid selectedAccountJid = null;
+ protected Account selectedAccount = null;
+ protected Jid selectedAccountJid = null;
- protected final List accountList = new ArrayList<>();
- protected ListView accountListView;
- protected AccountAdapter mAccountAdapter;
- protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false);
+ protected final List accountList = new ArrayList<>();
+ protected ListView accountListView;
+ protected AccountAdapter mAccountAdapter;
+ protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false);
- protected Pair mPostponedActivityResult = null;
+ protected Pair mPostponedActivityResult = null;
- @Override
- public void onAccountUpdate() {
- refreshUi();
- }
+ @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 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) {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(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;
- }
- }
- }
+ 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);
- }
+ 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
+ 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 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
+ 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
+ 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);
+ @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 (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;
- }
+ 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 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 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 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);
- }
- }
+ @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 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 publishAvatar(Account account) {
+ Intent intent = new Intent(getApplicationContext(),
+ PublishProfilePictureActivity.class);
+ intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString());
+ startActivity(intent);
+ }
- private void disableAllAccounts() {
- List list = new ArrayList<>();
- synchronized (this.accountList) {
- for (Account account : this.accountList) {
- if (account.isEnabled()) {
- list.add(account);
- }
- }
- }
- for (Account account : list) {
- disableAccount(account);
- }
- }
+ private void disableAllAccounts() {
+ List 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 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 boolean accountsLeftToEnable() {
+ synchronized (this.accountList) {
+ for (Account account : this.accountList) {
+ if (!account.isEnabled()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
- private void enableAllAccounts() {
- List 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 enableAllAccounts() {
+ List 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 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 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 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();
- }
+ 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
+ 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 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 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());
- }
+ @Override
+ public void informUser(final int r) {
+ runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show());
+ }
}
diff --git a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java
index 4eb614d55..332927550 100644
--- a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java
+++ b/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
+ protected void refreshUiReal() {
- }
+ }
- @Override
- void onBackendConnected() {
+ @Override
+ void onBackendConnected() {
- }
+ }
- @Override
- public void onStart() {
- super.onStart();
- final int theme = findTheme();
- if (this.mTheme != theme) {
- recreate();
- }
- }
+ @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
+ 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 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
+ 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 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());
- }
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.welcome_menu, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
- 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
+ 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);
+ }
}
diff --git a/src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java b/src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java
new file mode 100644
index 000000000..870340d7b
--- /dev/null
+++ b/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 {
+
+ private OnItemClickedListener listener;
+
+ private final List 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 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 {
+ private final WeakReference 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 bitmapWorkerTaskReference;
+
+ AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+ }
+
+ BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/conversations/res/drawable-hdpi/ic_unarchive_white_24dp.png b/src/conversations/res/drawable-hdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..18730f12f
Binary files /dev/null and b/src/conversations/res/drawable-hdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/conversations/res/drawable-mdpi/ic_unarchive_white_24dp.png b/src/conversations/res/drawable-mdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..8ec62cd34
Binary files /dev/null and b/src/conversations/res/drawable-mdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/conversations/res/drawable-xhdpi/ic_unarchive_white_24dp.png b/src/conversations/res/drawable-xhdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..a0a1509a1
Binary files /dev/null and b/src/conversations/res/drawable-xhdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/conversations/res/drawable-xxhdpi/ic_unarchive_white_24dp.png b/src/conversations/res/drawable-xxhdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..20d015751
Binary files /dev/null and b/src/conversations/res/drawable-xxhdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/conversations/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png b/src/conversations/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png
new file mode 100644
index 000000000..a789520ba
Binary files /dev/null and b/src/conversations/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png differ
diff --git a/src/conversations/res/layout/activity_import_backup.xml b/src/conversations/res/layout/activity_import_backup.xml
new file mode 100644
index 000000000..bc5ccecc1
--- /dev/null
+++ b/src/conversations/res/layout/activity_import_backup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/conversations/res/layout/dialog_enter_password.xml b/src/conversations/res/layout/dialog_enter_password.xml
new file mode 100644
index 000000000..e2fc38ff4
--- /dev/null
+++ b/src/conversations/res/layout/dialog_enter_password.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/menu/manageaccounts.xml b/src/conversations/res/menu/manageaccounts.xml
similarity index 88%
rename from src/main/res/menu/manageaccounts.xml
rename to src/conversations/res/menu/manageaccounts.xml
index 724b9a75d..5a26beaf9 100644
--- a/src/main/res/menu/manageaccounts.xml
+++ b/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"/>
+
-
+
+
+
\ No newline at end of file
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index b0623dd76..a1966a902 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -245,7 +245,8 @@
-
+
+
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
index eb84fb64a..2adec05ae 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/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) {
diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java
index bf37dca4f..5dc6996c7 100644
--- a/src/main/java/eu/siacs/conversations/services/AvatarService.java
+++ b/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);
diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java
new file mode 100644
index 000000000..63fc66e51
--- /dev/null
+++ b/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 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
deleted file mode 100644
index 79ed84496..000000000
--- a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
+++ /dev/null
@@ -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 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 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;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index e7fb1ba68..e2a2ba673 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/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");
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index 43fc763ad..73d7bd38a 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/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) {
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
index 8ee20c688..7c2342ce0 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
+++ b/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 {
- private XmppActivity activity;
- private boolean showStateButton;
+ private XmppActivity activity;
+ private boolean showStateButton;
- public AccountAdapter(XmppActivity activity, List objects, boolean showStateButton) {
- super(activity, 0, objects);
- this.activity = activity;
- this.showStateButton = showStateButton;
- }
+ public AccountAdapter(XmppActivity activity, List objects, boolean showStateButton) {
+ super(activity, 0, objects);
+ this.activity = activity;
+ this.showStateButton = showStateButton;
+ }
- public AccountAdapter(XmppActivity activity, List objects) {
- super(activity, 0, objects);
- this.activity = activity;
- this.showStateButton = true;
- }
+ public AccountAdapter(XmppActivity activity, List 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;
- }
+ @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;
+ }
- class BitmapWorkerTask extends AsyncTask {
- private final WeakReference imageViewReference;
- private Account account = null;
+ private static class ViewHolder {
+ private final AccountRowBinding binding;
- public BitmapWorkerTask(ImageView imageView) {
- imageViewReference = new WeakReference<>(imageView);
- }
+ private ViewHolder(AccountRowBinding binding) {
+ this.binding = binding;
+ }
+ }
- @Override
- protected Bitmap doInBackground(Account... params) {
- this.account = params[0];
- return activity.avatarService().get(this.account, activity.getPixel(48), isCancelled());
- }
+ class BitmapWorkerTask extends AsyncTask {
+ private final WeakReference imageViewReference;
+ private Account account = null;
- @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 BitmapWorkerTask(ImageView imageView) {
+ imageViewReference = new WeakReference<>(imageView);
+ }
- 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) {
- }
- }
- }
- }
+ @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 interface OnTglAccountState {
+ void onClickTglAccountState(Account account, boolean state);
+ }
- public static boolean cancelPotentialWork(Account account, ImageView imageView) {
- final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+ 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;
- }
+ 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;
- }
+ 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 bitmapWorkerTaskReference;
+ static class AsyncDrawable extends BitmapDrawable {
+ private final WeakReference bitmapWorkerTaskReference;
- public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
- super(res, bitmap);
- bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
- }
+ public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+ }
- public BitmapWorkerTask getBitmapWorkerTask() {
- return bitmapWorkerTaskReference.get();
- }
- }
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java b/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java
new file mode 100644
index 000000000..bc86a5d24
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/main/res/drawable-hdpi/ic_archive_white_24dp.png b/src/main/res/drawable-hdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..bb72e890f
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_archive_white_24dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png b/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png
deleted file mode 100644
index 705a4cc70..000000000
Binary files a/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/ic_archive_white_24dp.png b/src/main/res/drawable-mdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..f6aa3f966
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_archive_white_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png b/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png
deleted file mode 100644
index 5f6e11bc8..000000000
Binary files a/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png and /dev/null differ
diff --git a/src/main/res/drawable-xhdpi/ic_archive_white_24dp.png b/src/main/res/drawable-xhdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..3513bd9fe
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_archive_white_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png b/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png
deleted file mode 100644
index fb82f4208..000000000
Binary files a/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png and /dev/null differ
diff --git a/src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..00e04e42b
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png
deleted file mode 100644
index c34970372..000000000
Binary files a/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png and /dev/null differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.png
new file mode 100644
index 000000000..34cd3fd80
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png
deleted file mode 100644
index 06b27ea19..000000000
Binary files a/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png and /dev/null differ
diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml
index 22482b0d3..4cd44b0aa 100644
--- a/src/main/res/layout/account_row.xml
+++ b/src/main/res/layout/account_row.xml
@@ -1,55 +1,57 @@
-
+
+
+
+
+
+
+
+
+ android:scrollHorizontally="false"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
-
+
+
-
-
-
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:padding="16dp"
+ android:focusable="false" />
-
-
-
-
-
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/src/main/res/layout/dialog_quickedit.xml b/src/main/res/layout/dialog_quickedit.xml
index 00189eec3..f0b71fc1b 100644
--- a/src/main/res/layout/dialog_quickedit.xml
+++ b/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">
Try again
Keep service in foreground
Prevents the operating system from killing your connection
- Export history
- Write conversations history logs to SD card
- Writing logs to SD card
+ Create backup
+ Write backup files to %s
+ Creating backup files
+ Restoring backup
+ Your backup has been restored
+ Do not forget to enable the account.
Choose file
Receiving %1$s (%2$d%% completed)
Download %s
@@ -747,7 +750,6 @@
Video compression
View media
Media browser
- History export
File omitted due to security violation.
Video Quality
Lower quality means smaller files
@@ -811,4 +813,11 @@
Open with…
Conversations profile picture
Choose account
+ Restore backup
+ Restore
+ Enter your password for the account %s to restore the backup.
+ 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.
+ Unable to restore backup
+ Unable to decrypt backup
+
diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml
index 70841d057..971fb8ef9 100644
--- a/src/main/res/values/themes.xml
+++ b/src/main/res/values/themes.xml
@@ -95,7 +95,6 @@
- @drawable/ic_search_white_24dp
- @drawable/ic_lock_open_white_24dp
- @drawable/ic_settings_black_24dp
- - @drawable/ic_import_export_white_24dp
- @drawable/ic_share_white_24dp
- @drawable/ic_qr_code_scan_white_24dp
- @drawable/ic_scroll_to_end_black
@@ -208,7 +207,6 @@
- @drawable/ic_search_white_24dp
- @drawable/ic_lock_open_white_24dp
- @drawable/ic_settings_white_24dp
- - @drawable/ic_import_export_white_24dp
- @drawable/ic_share_white_24dp
- @drawable/ic_qr_code_scan_white_24dp
- @drawable/ic_scroll_to_end_white
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
index ab5d21263..4b93528f4 100644
--- a/src/main/res/xml/preferences.xml
+++ b/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" />
+ android:key="create_backup"
+ android:summary="@string/pref_create_backup_summary"
+ android:title="@string/pref_create_backup" />