allow backup to be restored from selected file
This commit is contained in:
parent
b68851b719
commit
603e1b35a5
|
@ -7,6 +7,7 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.support.v4.app.NotificationCompat;
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
@ -16,18 +17,20 @@ import java.io.BufferedReader;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
|
import java.util.zip.ZipException;
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
@ -81,14 +84,22 @@ public class ImportBackupService extends Service {
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
final String password = intent.getStringExtra("password");
|
final String password = intent.getStringExtra("password");
|
||||||
|
final Uri data = intent.getData();
|
||||||
|
final Uri uri;
|
||||||
|
if (data == null) {
|
||||||
final String file = intent.getStringExtra("file");
|
final String file = intent.getStringExtra("file");
|
||||||
if (password == null || file == null) {
|
uri = file == null ? null : Uri.fromFile(new File(file));
|
||||||
|
} else {
|
||||||
|
uri = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password == null || uri == null) {
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
if (running.compareAndSet(false, true)) {
|
if (running.compareAndSet(false, true)) {
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
startForegroundService();
|
startForegroundService();
|
||||||
final boolean success = importBackup(new File(file), password);
|
final boolean success = importBackup(uri, password);
|
||||||
stopForeground(true);
|
stopForeground(true);
|
||||||
running.set(false);
|
running.set(false);
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -122,7 +133,7 @@ public class ImportBackupService extends Service {
|
||||||
try {
|
try {
|
||||||
final BackupFile backupFile = BackupFile.read(file);
|
final BackupFile backupFile = BackupFile.read(file);
|
||||||
if (accounts.contains(backupFile.getHeader().getJid())) {
|
if (accounts.contains(backupFile.getHeader().getJid())) {
|
||||||
Log.d(Config.LOGTAG,"skipping backup for "+backupFile.getHeader().getJid());
|
Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid());
|
||||||
} else {
|
} else {
|
||||||
backupFiles.add(backupFile);
|
backupFiles.add(backupFile);
|
||||||
}
|
}
|
||||||
|
@ -145,21 +156,43 @@ public class ImportBackupService extends Service {
|
||||||
startForeground(NOTIFICATION_ID, mBuilder.build());
|
startForeground(NOTIFICATION_ID, mBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean importBackup(File file, String password) {
|
private boolean importBackup(Uri uri, String password) {
|
||||||
Log.d(Config.LOGTAG, "importing backup from file " + file.getAbsolutePath());
|
Log.d(Config.LOGTAG, "importing backup from " + uri);
|
||||||
|
if (password == null || password.isEmpty()) {
|
||||||
|
synchronized (mOnBackupProcessedListeners) {
|
||||||
|
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
|
||||||
|
l.onBackupDecryptionFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
|
SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
|
||||||
final FileInputStream fileInputStream = new FileInputStream(file);
|
final InputStream inputStream;
|
||||||
final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
|
if ("file".equals(uri.getScheme())) {
|
||||||
BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
|
inputStream = new FileInputStream(new File(uri.getPath()));
|
||||||
|
} else {
|
||||||
|
inputStream = getContentResolver().openInputStream(uri);
|
||||||
|
}
|
||||||
|
final DataInputStream dataInputStream = new DataInputStream(inputStream);
|
||||||
|
final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
|
||||||
Log.d(Config.LOGTAG, backupFileHeader.toString());
|
Log.d(Config.LOGTAG, backupFileHeader.toString());
|
||||||
|
|
||||||
|
if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) {
|
||||||
|
synchronized (mOnBackupProcessedListeners) {
|
||||||
|
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
|
||||||
|
l.onAccountAlreadySetup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||||
byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
|
byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
|
||||||
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(backupFileHeader.getIv());
|
IvParameterSpec ivSpec = new IvParameterSpec(backupFileHeader.getIv());
|
||||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||||
CipherInputStream cipherInputStream = new CipherInputStream(fileInputStream, cipher);
|
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
|
||||||
|
|
||||||
GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
|
GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
|
||||||
|
@ -197,12 +230,7 @@ public class ImportBackupService extends Service {
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Throwable throwable = e.getCause();
|
Throwable throwable = e.getCause();
|
||||||
final boolean reasonWasCrypto;
|
final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException;
|
||||||
if (throwable instanceof BadPaddingException) {
|
|
||||||
reasonWasCrypto = true;
|
|
||||||
} else {
|
|
||||||
reasonWasCrypto = false;
|
|
||||||
}
|
|
||||||
synchronized (mOnBackupProcessedListeners) {
|
synchronized (mOnBackupProcessedListeners) {
|
||||||
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
|
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
|
||||||
if (reasonWasCrypto) {
|
if (reasonWasCrypto) {
|
||||||
|
@ -212,7 +240,7 @@ public class ImportBackupService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG, "error restoring backup " + file.getAbsolutePath(), e);
|
Log.d(Config.LOGTAG, "error restoring backup " + uri, e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,14 +287,16 @@ public class ImportBackupService extends Service {
|
||||||
void onBackupDecryptionFailed();
|
void onBackupDecryptionFailed();
|
||||||
|
|
||||||
void onBackupRestoreFailed();
|
void onBackupRestoreFailed();
|
||||||
|
|
||||||
|
void onAccountAlreadySetup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BackupFile {
|
public static class BackupFile {
|
||||||
private final File file;
|
private final Uri uri;
|
||||||
private final BackupFileHeader header;
|
private final BackupFileHeader header;
|
||||||
|
|
||||||
private BackupFile(File file, BackupFileHeader header) {
|
private BackupFile(Uri uri, BackupFileHeader header) {
|
||||||
this.file = file;
|
this.uri = uri;
|
||||||
this.header = header;
|
this.header = header;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,15 +305,26 @@ public class ImportBackupService extends Service {
|
||||||
final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
|
final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
|
||||||
BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
|
BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
|
||||||
fileInputStream.close();
|
fileInputStream.close();
|
||||||
return new BackupFile(file, backupFileHeader);
|
return new BackupFile(Uri.fromFile(file), backupFileHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BackupFile read(final Context context, final Uri uri) throws IOException {
|
||||||
|
final InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||||
|
if (inputStream == null) {
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
final DataInputStream dataInputStream = new DataInputStream(inputStream);
|
||||||
|
BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
|
||||||
|
inputStream.close();
|
||||||
|
return new BackupFile(uri, backupFileHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BackupFileHeader getHeader() {
|
public BackupFileHeader getHeader() {
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getFile() {
|
public Uri getUri() {
|
||||||
return file;
|
return uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.databinding.DataBindingUtil;
|
import android.databinding.DataBindingUtil;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
|
@ -13,8 +15,11 @@ import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
|
@ -23,6 +28,7 @@ import eu.siacs.conversations.databinding.ActivityImportBackupBinding;
|
||||||
import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
|
import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
|
||||||
import eu.siacs.conversations.services.ImportBackupService;
|
import eu.siacs.conversations.services.ImportBackupService;
|
||||||
import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
|
import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
|
||||||
|
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
|
||||||
import eu.siacs.conversations.utils.ThemeHelper;
|
import eu.siacs.conversations.utils.ThemeHelper;
|
||||||
|
|
||||||
public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
|
public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
|
||||||
|
@ -32,6 +38,8 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
private BackupFileAdapter backupFileAdapter;
|
private BackupFileAdapter backupFileAdapter;
|
||||||
private ImportBackupService service;
|
private ImportBackupService service;
|
||||||
|
|
||||||
|
private boolean mLoadingState = false;
|
||||||
|
|
||||||
private int mTheme;
|
private int mTheme;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -47,6 +55,14 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
this.backupFileAdapter.setOnItemClickedListener(this);
|
this.backupFileAdapter.setOnItemClickedListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.import_backup, menu);
|
||||||
|
final MenuItem openBackup = menu.findItem(R.id.action_open_backup_file);
|
||||||
|
openBackup.setVisible(!this.mLoadingState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
@ -87,9 +103,22 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(ImportBackupService.BackupFile backupFile) {
|
public void onClick(final ImportBackupService.BackupFile backupFile) {
|
||||||
|
showEnterPasswordDialog(backupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openBackupFileFromUri(final Uri uri) {
|
||||||
|
try {
|
||||||
|
final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
|
||||||
|
showEnterPasswordDialog(backupFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showEnterPasswordDialog(final ImportBackupService.BackupFile backupFile) {
|
||||||
final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false);
|
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());
|
Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri());
|
||||||
enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString()));
|
enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString()));
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
builder.setView(enterPasswordBinding.getRoot());
|
builder.setView(enterPasswordBinding.getRoot());
|
||||||
|
@ -97,9 +126,16 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
builder.setNegativeButton(R.string.cancel, null);
|
builder.setNegativeButton(R.string.cancel, null);
|
||||||
builder.setPositiveButton(R.string.restore, (dialog, which) -> {
|
builder.setPositiveButton(R.string.restore, (dialog, which) -> {
|
||||||
final String password = enterPasswordBinding.accountPassword.getEditableText().toString();
|
final String password = enterPasswordBinding.accountPassword.getEditableText().toString();
|
||||||
|
final Uri uri = backupFile.getUri();
|
||||||
Intent intent = new Intent(this, ImportBackupService.class);
|
Intent intent = new Intent(this, ImportBackupService.class);
|
||||||
|
intent.setAction(Intent.ACTION_SEND);
|
||||||
intent.putExtra("password", password);
|
intent.putExtra("password", password);
|
||||||
intent.putExtra("file", backupFile.getFile().getAbsolutePath());
|
if ("file".equals(uri.getScheme())) {
|
||||||
|
intent.putExtra("file", uri.getPath());
|
||||||
|
} else {
|
||||||
|
intent.setData(uri);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
}
|
||||||
setLoadingState(true);
|
setLoadingState(true);
|
||||||
ContextCompat.startForegroundService(this, intent);
|
ContextCompat.startForegroundService(this, intent);
|
||||||
});
|
});
|
||||||
|
@ -108,10 +144,29 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setLoadingState(final boolean loadingState) {
|
private void setLoadingState(final boolean loadingState) {
|
||||||
binding.coordinator.setVisibility(loadingState ? View.GONE :View.VISIBLE);
|
binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE);
|
||||||
binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE);
|
binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE);
|
||||||
setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup);
|
setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup);
|
||||||
configureActionBar(getSupportActionBar(),!loadingState);
|
configureActionBar(getSupportActionBar(), !loadingState);
|
||||||
|
this.mLoadingState = loadingState;
|
||||||
|
invalidateOptionsMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
if (requestCode == 0xbac) {
|
||||||
|
openBackupFileFromUri(intent.getData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccountAlreadySetup() {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
setLoadingState(false);
|
||||||
|
Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG).show();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -126,17 +181,33 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackupDecryptionFailed() {
|
public void onBackupDecryptionFailed() {
|
||||||
runOnUiThread(()-> {
|
runOnUiThread(() -> {
|
||||||
setLoadingState(false);
|
setLoadingState(false);
|
||||||
Snackbar.make(binding.coordinator,R.string.unable_to_decrypt_backup,Snackbar.LENGTH_LONG).show();
|
Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackupRestoreFailed() {
|
public void onBackupRestoreFailed() {
|
||||||
runOnUiThread(()-> {
|
runOnUiThread(() -> {
|
||||||
setLoadingState(false);
|
setLoadingState(false);
|
||||||
Snackbar.make(binding.coordinator,R.string.unable_to_restore_backup,Snackbar.LENGTH_LONG).show();
|
Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.action_open_backup_file:
|
||||||
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
|
intent.setType("*/*");
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
|
||||||
|
}
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png
Normal file
BIN
src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 353 B |
BIN
src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png
Normal file
BIN
src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 242 B |
BIN
src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png
Normal file
BIN
src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 417 B |
BIN
src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png
Normal file
BIN
src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 610 B |
BIN
src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png
Normal file
BIN
src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 789 B |
11
src/main/res/menu/import_backup.xml
Normal file
11
src/main/res/menu/import_backup.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-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_open_backup_file"
|
||||||
|
android:icon="?attr/ic_cloud_download"
|
||||||
|
app:showAsAction="always"
|
||||||
|
android:title="@string/open_backup"/>
|
||||||
|
|
||||||
|
</menu>
|
|
@ -43,6 +43,9 @@
|
||||||
<attr name="ic_attach_photo" format="reference"/>
|
<attr name="ic_attach_photo" format="reference"/>
|
||||||
<attr name="ic_attach_record" format="reference"/>
|
<attr name="ic_attach_record" format="reference"/>
|
||||||
|
|
||||||
|
|
||||||
|
<attr name="ic_cloud_download" format="reference"/>
|
||||||
|
|
||||||
<attr name="message_bubble_received_monochrome" format="reference"/>
|
<attr name="message_bubble_received_monochrome" format="reference"/>
|
||||||
<attr name="message_bubble_sent" format="reference"/>
|
<attr name="message_bubble_sent" format="reference"/>
|
||||||
<attr name="message_bubble_received_green" format="reference"/>
|
<attr name="message_bubble_received_green" format="reference"/>
|
||||||
|
|
|
@ -871,4 +871,7 @@
|
||||||
<string name="share_backup_files">Share backup files</string>
|
<string name="share_backup_files">Share backup files</string>
|
||||||
<string name="conversations_backup">Conversations backup</string>
|
<string name="conversations_backup">Conversations backup</string>
|
||||||
<string name="event">Event</string>
|
<string name="event">Event</string>
|
||||||
|
<string name="open_backup">Open backup</string>
|
||||||
|
<string name="not_a_backup_file">The file you selected is not a Conversations backup file</string>
|
||||||
|
<string name="account_already_setup">This account has already been setup</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -98,6 +98,7 @@
|
||||||
<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_share">@drawable/ic_share_white_24dp</item>
|
<item type="reference" name="icon_share">@drawable/ic_share_white_24dp</item>
|
||||||
|
<item type="reference" name="ic_cloud_download">@drawable/ic_cloud_download_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>
|
||||||
|
|
||||||
|
@ -212,6 +213,7 @@
|
||||||
<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_share">@drawable/ic_share_white_24dp</item>
|
<item type="reference" name="icon_share">@drawable/ic_share_white_24dp</item>
|
||||||
|
<item type="reference" name="ic_cloud_download">@drawable/ic_cloud_download_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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue