WIP backup & restore

This commit is contained in:
Daniel Gultsch 2019-01-22 19:25:45 +01:00
parent 68565f2766
commit c9fc40dfe5
39 changed files with 1684 additions and 754 deletions

View file

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

View file

@ -0,0 +1,276 @@
package eu.siacs.conversations.services;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.os.Binder;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPInputStream;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.ManageAccountActivity;
import eu.siacs.conversations.utils.BackupFileHeader;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import static eu.siacs.conversations.services.ExportBackupService.CIPHERMODE;
import static eu.siacs.conversations.services.ExportBackupService.KEYTYPE;
import static eu.siacs.conversations.services.ExportBackupService.PROVIDER;
public class ImportBackupService extends Service {
private static final int NOTIFICATION_ID = 21;
private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
private final Set<OnBackupProcessed> mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
private static AtomicBoolean running = new AtomicBoolean(false);
private DatabaseBackend mDatabaseBackend;
private NotificationManager notificationManager;
private static int count(String input, char c) {
int count = 0;
for (char aChar : input.toCharArray()) {
if (aChar == c) {
++count;
}
}
return count;
}
@Override
public void onCreate() {
mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
return START_NOT_STICKY;
}
final String password = intent.getStringExtra("password");
final String file = intent.getStringExtra("file");
if (password == null || file == null) {
return START_NOT_STICKY;
}
Log.d(Config.LOGTAG, "on start command");
if (running.compareAndSet(false, true)) {
executor.execute(() -> {
startForegroundService();
final boolean success = importBackup(new File(file), password);
stopForeground(true);
running.set(false);
if (success) {
notifySuccess();
}
stopSelf();
});
} else {
Log.d(Config.LOGTAG, "backup already running");
}
return START_NOT_STICKY;
}
public void loadBackupFiles(OnBackupFilesLoaded onBackupFilesLoaded) {
executor.execute(() -> {
final ArrayList<BackupFile> backupFiles = new ArrayList<>();
for (String app : Arrays.asList("Conversations", "Quicksy")) {
final File directory = new File(FileBackend.getBackupDirectory(app));
if (!directory.exists() || !directory.isDirectory()) {
Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
continue;
}
for (File file : directory.listFiles()) {
if (file.isFile() && file.getName().endsWith(".ceb")) {
try {
backupFiles.add(BackupFile.read(file));
} catch (IOException e) {
Log.d(Config.LOGTAG, "unable to read backup file ", e);
}
}
}
}
onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
});
}
private void startForegroundService() {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_restore_backup_title))
.setSmallIcon(R.drawable.ic_unarchive_white_24dp)
.setProgress(1, 0, true);
startForeground(NOTIFICATION_ID, mBuilder.build());
}
private boolean importBackup(File file, String password) {
Log.d(Config.LOGTAG, "importing backup from file " + file.getAbsolutePath());
try {
SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
final FileInputStream fileInputStream = new FileInputStream(file);
final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
Log.d(Config.LOGTAG, backupFileHeader.toString());
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(backupFileHeader.getIv());
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
CipherInputStream cipherInputStream = new CipherInputStream(fileInputStream, cipher);
GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
String line;
StringBuilder multiLineQuery = null;
while ((line = reader.readLine()) != null) {
int count = count(line, '\'');
if (multiLineQuery != null) {
multiLineQuery.append(line);
if (count % 2 == 1) {
db.execSQL(multiLineQuery.toString());
multiLineQuery = null;
}
} else {
if (count % 2 == 0) {
db.execSQL(line);
} else {
multiLineQuery = new StringBuilder(line);
}
}
}
Log.d(Config.LOGTAG, "done reading file");
stopBackgroundService();
synchronized (mOnBackupProcessedListeners) {
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
l.onBackupRestored();
}
}
return true;
} catch (Exception e) {
Throwable throwable = e.getCause();
final boolean reasonWasCrypto;
if (throwable instanceof BadPaddingException) {
reasonWasCrypto = true;
} else {
reasonWasCrypto = false;
}
synchronized (mOnBackupProcessedListeners) {
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
if (reasonWasCrypto) {
l.onBackupDecryptionFailed();
} else {
l.onBackupRestoreFailed();
}
}
}
Log.d(Config.LOGTAG, "error restoring backup " + file.getAbsolutePath(), e);
return false;
}
}
private void notifySuccess() {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title))
.setContentText(getString(R.string.notification_restored_backup_subtitle))
.setAutoCancel(true)
.setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT))
.setSmallIcon(R.drawable.ic_unarchive_white_24dp);
notificationManager.notify(NOTIFICATION_ID,mBuilder.build());
}
private void stopBackgroundService() {
Intent intent = new Intent(this, XmppConnectionService.class);
stopService(intent);
}
public void removeOnBackupProcessedListener(OnBackupProcessed listener) {
synchronized (mOnBackupProcessedListeners) {
mOnBackupProcessedListeners.remove(listener);
}
}
public void addOnBackupProcessedListener(OnBackupProcessed listener) {
synchronized (mOnBackupProcessedListeners) {
mOnBackupProcessedListeners.add(listener);
}
}
@Override
public IBinder onBind(Intent intent) {
return this.binder;
}
public static class BackupFile {
private final File file;
private final BackupFileHeader header;
private BackupFile(File file, BackupFileHeader header) {
this.file = file;
this.header = header;
}
private static BackupFile read(File file) throws IOException {
final FileInputStream fileInputStream = new FileInputStream(file);
final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
fileInputStream.close();
return new BackupFile(file, backupFileHeader);
}
public BackupFileHeader getHeader() {
return header;
}
public File getFile() {
return file;
}
}
public class ImportBackupServiceBinder extends Binder {
public ImportBackupService getService() {
return ImportBackupService.this;
}
}
public interface OnBackupFilesLoaded {
void onBackupFilesLoaded(List<BackupFile> files);
}
public interface OnBackupProcessed {
void onBackupRestored();
void onBackupDecryptionFailed();
void onBackupRestoreFailed();
}
}

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -511,7 +511,11 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return bitmap; 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); Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap); Canvas canvas = new Canvas(bitmap);
final String trimmedName = name == null ? "" : name.trim(); final String trimmedName = name == null ? "" : name.trim();
@ -528,7 +532,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size); 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()); letter = letter.toUpperCase(Locale.getDefault());
Paint tilePaint = new Paint(), textPaint = new Paint(); Paint tilePaint = new Paint(), textPaint = new Paint();
tilePaint.setColor(tileColor); tilePaint.setColor(tileColor);
@ -591,7 +595,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return drawTile(canvas, name, name, left, top, right, bottom); 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) { if (name != null) {
final String letter = getFirstLetter(name); final String letter = getFirstLetter(name);
final int color = UIHelper.getColorForName(seed == null ? name : seed); final int color = UIHelper.getColorForName(seed == null ? name : seed);

View file

@ -0,0 +1,281 @@
package eu.siacs.conversations.services;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPOutputStream;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.utils.BackupFileHeader;
import eu.siacs.conversations.utils.Compatibility;
public class ExportBackupService extends Service {
public static final String KEYTYPE = "AES";
public static final String CIPHERMODE = "AES/GCM/NoPadding";
public static final String PROVIDER = "BC";
private static final int NOTIFICATION_ID = 19;
private static AtomicBoolean running = new AtomicBoolean(false);
private DatabaseBackend mDatabaseBackend;
private List<Account> mAccounts;
private NotificationManager notificationManager;
@Override
public void onCreate() {
mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
mAccounts = mDatabaseBackend.getAccounts();
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (running.compareAndSet(false, true)) {
new Thread(() -> {
export();
stopForeground(true);
running.set(false);
stopSelf();
}).start();
}
return START_NOT_STICKY;
}
private static void accountExport(SQLiteDatabase db, String uuid, PrintWriter writer) {
StringBuilder builder = new StringBuilder();
final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null);
while (accountCursor != null && accountCursor.moveToNext()) {
builder.append("INSERT INTO ").append(Account.TABLENAME).append("(");
for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
if (i != 0) {
builder.append(',');
}
builder.append(accountCursor.getColumnName(i));
}
builder.append(") VALUES(");
for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
if (i != 0) {
builder.append(',');
}
final String value = accountCursor.getString(i);
if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) {
builder.append("NULL");
} else if (value.matches("\\d+")) {
int intValue = Integer.parseInt(value);
Log.d(Config.LOGTAG,"reading int value. "+intValue);
if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) {
intValue |= 1 << Account.OPTION_DISABLED;
Log.d(Config.LOGTAG,"modified int value "+intValue);
}
builder.append(intValue);
} else {
DatabaseUtils.appendEscapedSQLString(builder, value);
}
}
builder.append(")");
builder.append(';');
builder.append('\n');
}
Log.d(Config.LOGTAG,builder.toString());
if (accountCursor != null) {
accountCursor.close();
}
writer.append(builder.toString());
}
private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
int size = cursor != null ? cursor.getCount() : 0;
Log.d(Config.LOGTAG, "exporting " + size + " messages");
int i = 0;
int p = 0;
while (cursor != null && cursor.moveToNext()) {
writer.write(cursorToString(Message.TABLENAME, cursor, 20));
if (i + 20 > size) {
i = size;
} else {
i += 20;
}
final int percentage = i * 100 / size;
if (p < percentage) {
p = percentage;
notificationManager.notify(NOTIFICATION_ID,progress.build(p));
Log.d(Config.LOGTAG, "percentage=" + p);
}
}
if (cursor != null) {
cursor.close();
}
}
private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) {
final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
writer.write(cursorToString(table, cursor, 20));
}
if (cursor != null) {
cursor.close();
}
}
private void export() {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
.setSmallIcon(R.drawable.ic_archive_white_24dp)
.setProgress(1, 0, false);
startForeground(NOTIFICATION_ID, mBuilder.build());
try {
int count = 0;
final int max = this.mAccounts.size();
final SecureRandom secureRandom = new SecureRandom();
for (Account account : this.mAccounts) {
final byte[] IV = new byte[12];
final byte[] salt = new byte[16];
secureRandom.nextBytes(IV);
secureRandom.nextBytes(salt);
final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name),account.getJid(),System.currentTimeMillis(),IV,salt);
final Progress progress = new Progress(mBuilder, max, count);
final File file = new File(FileBackend.getBackupDirectory(this)+account.getJid().asBareJid().toEscapedString()+".ceb");
if (file.getParentFile().mkdirs()) {
Log.d(Config.LOGTAG,"created backup directory "+file.getParentFile().getAbsolutePath());
}
final FileOutputStream fileOutputStream = new FileOutputStream(file);
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
backupFileHeader.write(dataOutputStream);
dataOutputStream.flush();
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
byte[] key = getKey(account.getPassword(), salt);
Log.d(Config.LOGTAG,backupFileHeader.toString());
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
PrintWriter writer = new PrintWriter(gzipOutputStream);
SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
final String uuid = account.getUuid();
accountExport(db, uuid, writer);
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
messageExport(db, uuid, writer, progress);
for(String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT,uuid,writer);
}
writer.flush();
writer.close();
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
count++;
}
} catch (Exception e) {
Log.d(Config.LOGTAG, "unable to create backup ", e);
}
}
public static byte[] getKey(String password, byte[] salt) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new AssertionError(e);
}
}
private static String cursorToString(String tablename, Cursor cursor, int max) {
StringBuilder builder = new StringBuilder();
builder.append("INSERT INTO ").append(tablename).append("(");
for (int i = 0; i < cursor.getColumnCount(); ++i) {
if (i != 0) {
builder.append(',');
}
builder.append(cursor.getColumnName(i));
}
builder.append(") VALUES");
for (int i = 0; i < max; ++i) {
if (i != 0) {
builder.append(',');
}
appendValues(cursor, builder);
if (!cursor.moveToNext()) {
break;
}
}
builder.append(';');
builder.append('\n');
return builder.toString();
}
private static void appendValues(Cursor cursor, StringBuilder builder) {
builder.append("(");
for (int i = 0; i < cursor.getColumnCount(); ++i) {
if (i != 0) {
builder.append(',');
}
final String value = cursor.getString(i);
if (value == null) {
builder.append("NULL");
} else if (value.matches("\\d+")) {
builder.append(value);
} else {
DatabaseUtils.appendEscapedSQLString(builder, value);
}
}
builder.append(")");
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private class Progress {
private final NotificationCompat.Builder builder;
private final int max;
private final int count;
private Progress(NotificationCompat.Builder builder, int max, int count) {
this.builder = builder;
this.max = max;
this.count = count;
}
private Notification build(int percentage) {
builder.setProgress(max * 100,count * 100 + percentage,false);
return builder.build();
}
}
}

View file

@ -1,148 +0,0 @@
package eu.siacs.conversations.services;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import rocks.xmpp.addr.Jid;
public class ExportLogsService extends Service {
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
private static final String DIRECTORY_STRING_FORMAT = FileBackend.getConversationsLogsDirectory() + "/logs/%s";
private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n";
private static final int NOTIFICATION_ID = 1;
private static AtomicBoolean running = new AtomicBoolean(false);
private DatabaseBackend mDatabaseBackend;
private List<Account> mAccounts;
@Override
public void onCreate() {
mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
mAccounts = mDatabaseBackend.getAccounts();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (running.compareAndSet(false, true)) {
new Thread(() -> {
export();
stopForeground(true);
running.set(false);
stopSelf();
}).start();
}
return START_NOT_STICKY;
}
private void export() {
List<Conversation> conversations = mDatabaseBackend.getConversations(Conversation.STATUS_AVAILABLE);
conversations.addAll(mDatabaseBackend.getConversations(Conversation.STATUS_ARCHIVED));
NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "export");
mBuilder.setContentTitle(getString(R.string.notification_export_logs_title))
.setSmallIcon(R.drawable.ic_import_export_white_24dp)
.setProgress(conversations.size(), 0, false);
startForeground(NOTIFICATION_ID, mBuilder.build());
int progress = 0;
for (Conversation conversation : conversations) {
writeToFile(conversation);
progress++;
mBuilder.setProgress(conversations.size(), progress, false);
if (mNotifyManager != null) {
mNotifyManager.notify(NOTIFICATION_ID, mBuilder.build());
}
}
}
private void writeToFile(Conversation conversation) {
Jid accountJid = resolveAccountUuid(conversation.getAccountUuid());
Jid contactJid = conversation.getJid();
File dir = new File(String.format(DIRECTORY_STRING_FORMAT, accountJid.asBareJid().toString()));
dir.mkdirs();
BufferedWriter bw = null;
try {
for (Message message : mDatabaseBackend.getMessagesIterable(conversation)) {
if (message == null)
continue;
if (message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) {
String date = simpleDateFormat.format(new Date(message.getTimeSent()));
if (bw == null) {
bw = new BufferedWriter(new FileWriter(
new File(dir, contactJid.asBareJid().toString() + ".txt")));
}
String jid = null;
switch (message.getStatus()) {
case Message.STATUS_RECEIVED:
jid = getMessageCounterpart(message);
break;
case Message.STATUS_SEND:
case Message.STATUS_SEND_RECEIVED:
case Message.STATUS_SEND_DISPLAYED:
jid = accountJid.asBareJid().toString();
break;
}
if (jid != null) {
String body = message.hasFileOnRemoteHost() ? message.getFileParams().url.toString() : message.getBody();
bw.write(String.format(MESSAGE_STRING_FORMAT, date, jid,
body.replace("\\\n", "\\ \n").replace("\n", "\\ \n")));
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null) {
bw.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
private Jid resolveAccountUuid(String accountUuid) {
for (Account account : mAccounts) {
if (account.getUuid().equals(accountUuid)) {
return account.getJid();
}
}
return null;
}
private String getMessageCounterpart(Message message) {
String trueCounterpart = (String) message.getContentValues().get(Message.TRUE_COUNTERPART);
if (trueCounterpart != null) {
return trueCounterpart;
} else {
return message.getCounterpart().toString();
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View file

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

View file

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

View file

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

View file

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 B

View file

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

View file

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

View file

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

View file

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

View file

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