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.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) {
@ -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;
} }
} }

View file

@ -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);
}); });
@ -112,6 +148,25 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
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
@ -139,4 +194,20 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
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_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"/>

View file

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

View file

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