diff --git a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java index 9892e6492..b982e1be8 100644 --- a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java +++ b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java @@ -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 String file = intent.getStringExtra("file"); - if (password == null || file == null) { + final Uri data = intent.getData(); + final Uri uri; + if (data == null) { + final String file = intent.getStringExtra("file"); + 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; } } diff --git a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java index d5de333da..7ceff68f7 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -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); + } } diff --git a/src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png new file mode 100644 index 000000000..4c5d2d049 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png new file mode 100644 index 000000000..0c6978c1f Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png new file mode 100644 index 000000000..d1a0573af Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png new file mode 100644 index 000000000..3392d972f Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png new file mode 100644 index 000000000..b24e573ed Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png differ diff --git a/src/main/res/menu/import_backup.xml b/src/main/res/menu/import_backup.xml new file mode 100644 index 000000000..0d8a14497 --- /dev/null +++ b/src/main/res/menu/import_backup.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index e2f7ddf76..1f78b19a2 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -43,6 +43,9 @@ + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 90866434e..08f9b02e0 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -871,4 +871,7 @@ Share backup files Conversations backup Event + Open backup + The file you selected is not a Conversations backup file + This account has already been setup diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index f4c870603..8866e9931 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -98,6 +98,7 @@ @drawable/ic_lock_open_white_24dp @drawable/ic_settings_black_24dp @drawable/ic_share_white_24dp + @drawable/ic_cloud_download_white_24dp @drawable/ic_qr_code_scan_white_24dp @drawable/ic_scroll_to_end_black @@ -212,6 +213,7 @@ @drawable/ic_lock_open_white_24dp @drawable/ic_settings_white_24dp @drawable/ic_share_white_24dp + @drawable/ic_cloud_download_white_24dp @drawable/ic_qr_code_scan_white_24dp @drawable/ic_scroll_to_end_white