From 603e1b35a58c5395623aa6a8a5d3876c272b2d15 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 16 Jul 2019 16:49:47 +0200 Subject: [PATCH] allow backup to be restored from selected file --- .../services/ImportBackupService.java | 89 +++++++++++++----- .../ui/ImportBackupActivity.java | 89 ++++++++++++++++-- .../ic_cloud_download_white_24dp.png | Bin 0 -> 353 bytes .../ic_cloud_download_white_24dp.png | Bin 0 -> 242 bytes .../ic_cloud_download_white_24dp.png | Bin 0 -> 417 bytes .../ic_cloud_download_white_24dp.png | Bin 0 -> 610 bytes .../ic_cloud_download_white_24dp.png | Bin 0 -> 789 bytes src/main/res/menu/import_backup.xml | 11 +++ src/main/res/values/attrs.xml | 3 + src/main/res/values/strings.xml | 3 + src/main/res/values/themes.xml | 2 + 11 files changed, 164 insertions(+), 33 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png create mode 100644 src/main/res/menu/import_backup.xml 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 0000000000000000000000000000000000000000..4c5d2d049e392a237958679a6599519c7f59cfd5 GIT binary patch literal 353 zcmV-n0iOPeP)Ds!(`sk%0Imd!2!g#;^aK|14&oK&0Agip;Ro0XVm!fOA$tM8h?s!JLJdM9 zN*;?T%p^0rZ(OjDJpcaJ4D-sk{}n|xIpu*4pENjUiy{)5;NKM6bQrA54%4GRi`)?E zjw{K!!=Loc?S2@V zrwt=R(By=5CV!w9RFJ<*<}6f!To61GA;Mm62oeh7eegQQ&Vh^_b%xsz-tpxUF6Zk^ z_y9c>VBlG}1vO;|^(7pFk}`yA32UID3~AK_;X*+f(yIzWiFP`b$s`oQpH|-!5hB=? zi1}R!(cpz#WuI%h=<3m;MwJ3$q5K;|e+_*DoPSf;fViWX00000NkvXXu0mjf?unJ- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0c6978c1fd094c1d045389e83aed90b724495b1c GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+iot`d^Ar^vn58C=M1xmDCoX!== z+&Q&VIzc>z%c^|g(Grt*Rn=o{X9IltFJ>${V|Qjop@y7F+@ZfSv)5R%j6|idht{$r%KdvdG=I3 qtrz=sbV9UQ&BW^gfiHaCFdn(ZwdkJK-s?b5FnGH9xvX)D*jn?auJVYUT*KKR0m$E(zB|-j;dX+0$g&eYKP9}0PF7g=t!OJ z&jLCMBlSD9It|z{Fs+!J)4Ey?+=01)y)s~%Mu4F&Ufa}~1Ud$_fbzcD_!lT>7=TC4 zjDe2PLI56*fn62^uqnd+0)d{@@Kbq7WGpGK8&1e4DxRm91(4=A&$sY{+C(y|Vc4f8 zk;nmdsYN6?9fT`J)d@*nMF?-HNF=#+Ls&DYL?pTOv_lBD$R?8fwnP~3w&r^>`DEVo zTa)cMI|+o#-WX}3o~^QP%bL4B_-?39Lk9|7ZBYg^wEqfN0!!d8m2gFXs7S&200000 LNkvXXu0mjfq%yl{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3392d972fe024190e6d77048018cabf9185f42d8 GIT binary patch literal 610 zcmV-o0-gPdP)MSTYjf}^-3lMOyWom`6O;OJ20xG5q;ks?S25v$}QfCy47Za3Yla;8Af=GPOmsZ#y4mu&xlil9sWQOr|A&4 z4kzjJ0UF>Arrl3^R?dF2@Cxn8RmZSg#Knphlde%l_U$UC?G`AOX<09%zsz zBm#P?1M24qBm{yc6P#feeceGda0t@vB`3)L2~>hqKmczj{SL}9mm&z@EYZ)P!;mrv zt`V(*9`yhSF8ly(g`NPx&^Lp5!uRdLCgq3&qlmDwEA}L7&OC)xP@cz@B!z92UmuD^ zW=XS)YoF3Zrb(+?L~B7pp0Sj^PSpJzvzu9xY@W)$4ptEdm?DW1|&)--fBOT!<-Ol0Y&M(CX0zFBHk(aV zRC9rT#)z{>iX3Y!v&cIh)6aSKBh-${$BuG~x8&Ha9J5>_;y*we&snF`ay+BeKY(+@ zanc*kmdI5X_fU~;=w3GK0;5#!s;0nh|12mHYF8~foaDxNe11k6gz6AiJknGzU&<{Qb z04&h9IUr0r5CF(CKfP-WL1%Pov#enM&FaX>w2E+phfQ$bCO%O=H z5;X-tS3m(UQ~<;R3&_%l5WK=!5}|M;P8vW6=D|2Y;;e6k1j}HUAaUMF!5SDPNZb@H z0L&32?wS<*1Y-n=+Y*9#Fhr2J?}y+e7$Hbhv?>?|0|W`G0A1iYL87Xp;4pYhkfuBGtseei%FQQZQe!qZKpwG$-j8$<{S69ugV ziTbjHi+?SC)<}@(D7NwE?g>(w2$G`If7BQ6X4W+jBzo%DY{Ah(P>OA}V(MWX|2IY1 zRscaezwvid?A?CNdYn1@Ta2=u7u^-yCXN4EqNDUvgD^v6@K0HWsoCwZM;(2{$z!?& z1_?Vqw`ri0d%WN)NmlWyRgxr_AW91%^@p#9#{w+C0xZA+EWiRRzyd750u;<&TKF}4 Tehrn800000NkvXXu0mjfJxo+? literal 0 HcmV?d00001 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