allow backup to be restored from selected file

Daniel Gultsch created

Change summary

src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java | 89 
src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java      | 89 
src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png                     |  0 
src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png                     |  0 
src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png                    |  0 
src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png                   |  0 
src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png                  |  0 
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(-)

Detailed changes

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;
         }
     }
 

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);
+    }
 }

src/main/res/menu/import_backup.xml 🔗

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

src/main/res/values/attrs.xml 🔗

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

src/main/res/values/strings.xml 🔗

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

src/main/res/values/themes.xml 🔗

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