WIP backup & restore
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -198,8 +198,10 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
case R.id.action_import_backup:
|
||||||
|
startActivity(new Intent(this, ImportBackupActivity.class));
|
||||||
break;
|
break;
|
||||||
case R.id.action_disable_all:
|
case R.id.action_disable_all:
|
||||||
disableAllAccounts();
|
disableAllAccounts();
|
||||||
|
@ -314,7 +316,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,13 +327,13 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,18 @@ 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 {
|
||||||
|
@ -77,6 +81,21 @@ public class WelcomeActivity extends XmppActivity {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.welcome_menu, menu);
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
if (item.getItemId() == R.id.action_import_backup) {
|
||||||
|
startActivity(new Intent(this, ImportBackupActivity.class));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
public void addInviteUri(Intent intent) {
|
public void addInviteUri(Intent intent) {
|
||||||
StartConversationActivity.addInviteUri(intent, getIntent());
|
StartConversationActivity.addInviteUri(intent, getIntent());
|
||||||
}
|
}
|
||||||
|
@ -85,7 +104,7 @@ public class WelcomeActivity extends XmppActivity {
|
||||||
Intent intent = new Intent(activity, WelcomeActivity.class);
|
Intent intent = new Intent(activity, WelcomeActivity.class);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
activity.startActivity(intent);
|
activity.startActivity(intent);
|
||||||
activity.overridePendingTransition(0,0);
|
activity.overridePendingTransition(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
src/conversations/res/drawable-hdpi/ic_unarchive_white_24dp.png
Normal file
After Width: | Height: | Size: 258 B |
BIN
src/conversations/res/drawable-mdpi/ic_unarchive_white_24dp.png
Normal file
After Width: | Height: | Size: 181 B |
BIN
src/conversations/res/drawable-xhdpi/ic_unarchive_white_24dp.png
Normal file
After Width: | Height: | Size: 273 B |
After Width: | Height: | Size: 391 B |
After Width: | Height: | Size: 503 B |
32
src/conversations/res/layout/activity_import_backup.xml
Normal 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>
|
47
src/conversations/res/layout/dialog_enter_password.xml
Normal 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>
|
|
@ -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"
|
8
src/conversations/res/menu/welcome_menu.xml
Normal 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>
|
|
@ -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">
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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");
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -43,45 +43,45 @@ public class AccountAdapter extends ArrayAdapter<Account> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
final ViewHolder viewHolder;
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
LayoutInflater inflater = (LayoutInflater) getContext()
|
AccountRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.account_row, parent, false);
|
||||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
view = binding.getRoot();
|
||||||
view = inflater.inflate(R.layout.account_row, parent, false);
|
viewHolder = new ViewHolder(binding);
|
||||||
}
|
view.setTag(viewHolder);
|
||||||
TextView jid = view.findViewById(R.id.account_jid);
|
|
||||||
if (Config.DOMAIN_LOCK != null) {
|
|
||||||
jid.setText(account.getJid().getLocal());
|
|
||||||
} else {
|
} else {
|
||||||
jid.setText(account.getJid().asBareJid().toString());
|
viewHolder = (ViewHolder) view.getTag();
|
||||||
}
|
}
|
||||||
TextView statusView = view.findViewById(R.id.account_status);
|
if (Config.DOMAIN_LOCK != null) {
|
||||||
ImageView imageView = view.findViewById(R.id.account_image);
|
viewHolder.binding.accountJid.setText(account.getJid().getLocal());
|
||||||
loadAvatar(account, imageView);
|
} else {
|
||||||
statusView.setText(getContext().getString(account.getStatus().getReadableId()));
|
viewHolder.binding.accountJid.setText(account.getJid().asBareJid().toString());
|
||||||
|
}
|
||||||
|
loadAvatar(account, viewHolder.binding.accountImage);
|
||||||
|
viewHolder.binding.accountStatus.setText(getContext().getString(account.getStatus().getReadableId()));
|
||||||
switch (account.getStatus()) {
|
switch (account.getStatus()) {
|
||||||
case ONLINE:
|
case ONLINE:
|
||||||
statusView.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline));
|
viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline));
|
||||||
break;
|
break;
|
||||||
case DISABLED:
|
case DISABLED:
|
||||||
case CONNECTING:
|
case CONNECTING:
|
||||||
statusView.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary));
|
viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
statusView.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError));
|
viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError));
|
||||||
break;
|
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);
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,14 @@ public class AccountAdapter extends ArrayAdapter<Account> {
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class ViewHolder {
|
||||||
|
private final AccountRowBinding binding;
|
||||||
|
|
||||||
|
private ViewHolder(AccountRowBinding binding) {
|
||||||
|
this.binding = binding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class BitmapWorkerTask extends AsyncTask<Account, Void, Bitmap> {
|
class BitmapWorkerTask extends AsyncTask<Account, Void, Bitmap> {
|
||||||
private final WeakReference<ImageView> imageViewReference;
|
private final WeakReference<ImageView> imageViewReference;
|
||||||
private Account account = null;
|
private Account account = null;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
BIN
src/main/res/drawable-hdpi/ic_archive_white_24dp.png
Normal file
After Width: | Height: | Size: 247 B |
Before Width: | Height: | Size: 300 B |
BIN
src/main/res/drawable-mdpi/ic_archive_white_24dp.png
Normal file
After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 226 B |
BIN
src/main/res/drawable-xhdpi/ic_archive_white_24dp.png
Normal file
After Width: | Height: | Size: 267 B |
Before Width: | Height: | Size: 330 B |
BIN
src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png
Normal file
After Width: | Height: | Size: 390 B |
Before Width: | Height: | Size: 414 B |
BIN
src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.png
Normal file
After Width: | Height: | Size: 489 B |
Before Width: | Height: | Size: 502 B |
|
@ -1,9 +1,11 @@
|
||||||
<?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">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?android:attr/activatedBackgroundIndicator"
|
android:background="?android:selectableItemBackground"
|
||||||
android:paddingLeft="8dp"
|
android:paddingLeft="8dp"
|
||||||
android:paddingBottom="8dp"
|
android:paddingBottom="8dp"
|
||||||
android:paddingTop="8dp">
|
android:paddingTop="8dp">
|
||||||
|
@ -32,15 +34,14 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:scrollHorizontally="false"
|
android:scrollHorizontally="false"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="@style/TextAppearance.Conversations.Subhead"/>
|
android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/account_status"
|
android:id="@+id/account_status"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/account_status_unknown"
|
android:text="@string/account_status_unknown"
|
||||||
android:textAppearance="@style/TextAppearance.Conversations.Body2"
|
android:textAppearance="@style/TextAppearance.Conversations.Body2" />
|
||||||
/>
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<android.support.v7.widget.SwitchCompat
|
<android.support.v7.widget.SwitchCompat
|
||||||
|
@ -50,6 +51,7 @@
|
||||||
android:layout_alignParentRight="true"
|
android:layout_alignParentRight="true"
|
||||||
android:layout_centerVertical="true"
|
android:layout_centerVertical="true"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:focusable="false"/>
|
android:focusable="false" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
</layout>
|
|
@ -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"
|
||||||
|
|
|
@ -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 you’ve lost the original device.</string>
|
||||||
|
<string name="unable_to_restore_backup">Unable to restore backup</string>
|
||||||
|
<string name="unable_to_decrypt_backup">Unable to decrypt backup</string>
|
||||||
|
<string name="backup_channel_name"><![CDATA[Backup & Restore]]></string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|