allow backup to be restored from selected file

This commit is contained in:
Daniel Gultsch 2019-07-16 16:49:47 +02:00
parent b68851b719
commit 603e1b35a5
11 changed files with 164 additions and 33 deletions

View file

@ -7,6 +7,7 @@ import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
@ -16,18 +17,20 @@ import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@ -81,14 +84,22 @@ public class ImportBackupService extends Service {
return START_NOT_STICKY;
}
final String password = intent.getStringExtra("password");
final Uri data = intent.getData();
final Uri uri;
if (data == null) {
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;
}
if (running.compareAndSet(false, true)) {
executor.execute(() -> {
startForegroundService();
final boolean success = importBackup(new File(file), password);
final boolean success = importBackup(uri, password);
stopForeground(true);
running.set(false);
if (success) {
@ -122,7 +133,7 @@ public class ImportBackupService extends Service {
try {
final BackupFile backupFile = BackupFile.read(file);
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 {
backupFiles.add(backupFile);
}
@ -145,21 +156,43 @@ public class ImportBackupService extends Service {
startForeground(NOTIFICATION_ID, mBuilder.build());
}
private boolean importBackup(File file, String password) {
Log.d(Config.LOGTAG, "importing backup from file " + file.getAbsolutePath());
private boolean importBackup(Uri uri, String password) {
Log.d(Config.LOGTAG, "importing backup from " + uri);
if (password == null || password.isEmpty()) {
synchronized (mOnBackupProcessedListeners) {
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
l.onBackupDecryptionFailed();
}
}
return false;
}
try {
SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
final FileInputStream fileInputStream = new FileInputStream(file);
final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
final InputStream inputStream;
if ("file".equals(uri.getScheme())) {
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());
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);
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);
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
@ -197,12 +230,7 @@ public class ImportBackupService extends Service {
return true;
} catch (Exception e) {
Throwable throwable = e.getCause();
final boolean reasonWasCrypto;
if (throwable instanceof BadPaddingException) {
reasonWasCrypto = true;
} else {
reasonWasCrypto = false;
}
final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException;
synchronized (mOnBackupProcessedListeners) {
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
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;
}
}
@ -259,14 +287,16 @@ public class ImportBackupService extends Service {
void onBackupDecryptionFailed();
void onBackupRestoreFailed();
void onAccountAlreadySetup();
}
public static class BackupFile {
private final File file;
private final Uri uri;
private final BackupFileHeader header;
private BackupFile(File file, BackupFileHeader header) {
this.file = file;
private BackupFile(Uri uri, BackupFileHeader header) {
this.uri = uri;
this.header = header;
}
@ -275,15 +305,26 @@ public class ImportBackupService extends Service {
final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
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() {
return header;
}
public File getFile() {
return file;
public Uri getUri() {
return uri;
}
}

View file

@ -5,6 +5,8 @@ import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.databinding.DataBindingUtil;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.support.design.widget.Snackbar;
@ -13,8 +15,11 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import java.io.IOException;
import java.util.List;
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.services.ImportBackupService;
import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
import eu.siacs.conversations.utils.ThemeHelper;
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 ImportBackupService service;
private boolean mLoadingState = false;
private int mTheme;
@Override
@ -47,6 +55,14 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
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
public void onStart() {
super.onStart();
@ -87,9 +103,22 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
}
@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);
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()));
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(enterPasswordBinding.getRoot());
@ -97,9 +126,16 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.restore, (dialog, which) -> {
final String password = enterPasswordBinding.accountPassword.getEditableText().toString();
final Uri uri = backupFile.getUri();
Intent intent = new Intent(this, ImportBackupService.class);
intent.setAction(Intent.ACTION_SEND);
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);
ContextCompat.startForegroundService(this, intent);
});
@ -108,10 +144,29 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
}
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);
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
@ -126,17 +181,33 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
@Override
public void onBackupDecryptionFailed() {
runOnUiThread(()-> {
runOnUiThread(() -> {
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
public void onBackupRestoreFailed() {
runOnUiThread(()-> {
runOnUiThread(() -> {
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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

View 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>

View file

@ -43,6 +43,9 @@
<attr name="ic_attach_photo" 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_sent" format="reference"/>
<attr name="message_bubble_received_green" format="reference"/>

View file

@ -871,4 +871,7 @@
<string name="share_backup_files">Share backup files</string>
<string name="conversations_backup">Conversations backup</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>

View file

@ -98,6 +98,7 @@
<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_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_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_settings">@drawable/ic_settings_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_scroll_down">@drawable/ic_scroll_to_end_white</item>