read backup file header from URI on background thread

Daniel Gultsch created

backup files can be on a network drive and thus take a while to read
even if just the header is read

Change summary

src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java | 13 
src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java      | 61 
2 files changed, 55 insertions(+), 19 deletions(-)

Detailed changes

src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java 🔗

@@ -23,6 +23,8 @@ import androidx.core.app.NotificationManagerCompat;
 import com.google.common.base.Charsets;
 import com.google.common.base.Stopwatch;
 import com.google.common.io.CountingInputStream;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
 
@@ -64,6 +66,8 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.WeakHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.regex.Pattern;
 import java.util.zip.GZIPInputStream;
@@ -73,6 +77,9 @@ import javax.crypto.BadPaddingException;
 
 public class ImportBackupService extends Service {
 
+    private static final ExecutorService BACKUP_FILE_READER_EXECUTOR =
+            Executors.newSingleThreadExecutor();
+
     private static final int NOTIFICATION_ID = 21;
     private static final AtomicBoolean running = new AtomicBoolean(false);
     private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
@@ -433,6 +440,10 @@ public class ImportBackupService extends Service {
         }
     }
 
+    public static ListenableFuture<BackupFile> read(final Context context, final Uri uri) {
+        return Futures.submit(() -> BackupFile.read(context, uri), BACKUP_FILE_READER_EXECUTOR);
+    }
+
     @Override
     public IBinder onBind(Intent intent) {
         return this.binder;
@@ -475,7 +486,7 @@ public class ImportBackupService extends Service {
                 throw new FileNotFoundException();
             }
             final DataInputStream dataInputStream = new DataInputStream(inputStream);
-            BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
+            final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
             inputStream.close();
             return new BackupFile(uri, backupFileHeader);
         }

src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java 🔗

@@ -19,6 +19,7 @@ import android.view.View;
 
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 import androidx.core.content.ContextCompat;
 import androidx.databinding.DataBindingUtil;
@@ -27,6 +28,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.snackbar.Snackbar;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -34,6 +37,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.MainThreadExecutor;
 import eu.siacs.conversations.utils.BackupFileHeader;
 
 import java.io.IOException;
@@ -182,21 +186,36 @@ public class ImportBackupActivity extends ActionBarActivity
     }
 
     private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) {
-        try {
-            final ImportBackupService.BackupFile backupFile =
-                    ImportBackupService.BackupFile.read(this, uri);
-            showEnterPasswordDialog(backupFile, finishOnCancel);
-        } catch (final BackupFileHeader.OutdatedBackupFileVersion e) {
+        final var backupFileFuture = ImportBackupService.read(this, uri);
+        Futures.addCallback(
+                backupFileFuture,
+                new FutureCallback<>() {
+                    @Override
+                    public void onSuccess(final ImportBackupService.BackupFile backupFile) {
+                        showEnterPasswordDialog(backupFile, finishOnCancel);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull final Throwable throwable) {
+                        Log.d(Config.LOGTAG, "could not open backup file " + uri, throwable);
+                        showBackupThrowable(throwable);
+                    }
+                },
+                MainThreadExecutor.getInstance());
+    }
+
+    private void showBackupThrowable(final Throwable throwable) {
+        if (throwable instanceof BackupFileHeader.OutdatedBackupFileVersion) {
             Snackbar.make(
                             binding.coordinator,
                             R.string.outdated_backup_file_format,
                             Snackbar.LENGTH_LONG)
                     .show();
-        } catch (final IOException | IllegalArgumentException e) {
-            Log.d(Config.LOGTAG, "unable to open backup file " + uri, e);
+        } else if (throwable instanceof IOException
+                || throwable instanceof IllegalArgumentException) {
             Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG)
                     .show();
-        } catch (final SecurityException e) {
+        } else if (throwable instanceof SecurityException e) {
             Snackbar.make(
                             binding.coordinator,
                             R.string.sharing_application_not_grant_permission,
@@ -243,16 +262,7 @@ public class ImportBackupActivity extends ActionBarActivity
                                                     getString(R.string.please_enter_password));
                                             return;
                                         }
-                                        final Uri uri = backupFile.getUri();
-                                        Intent intent = new Intent(this, ImportBackupService.class);
-                                        intent.setAction(Intent.ACTION_SEND);
-                                        intent.putExtra("password", password);
-                                        if ("file".equals(uri.getScheme())) {
-                                            intent.putExtra("file", uri.getPath());
-                                        } else {
-                                            intent.setData(uri);
-                                            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-                                        }
+                                        final Intent intent = getIntent(backupFile, password);
                                         setLoadingState(true);
                                         ContextCompat.startForegroundService(this, intent);
                                         d.dismiss();
@@ -261,6 +271,21 @@ public class ImportBackupActivity extends ActionBarActivity
         dialog.show();
     }
 
+    @NonNull
+    private Intent getIntent(ImportBackupService.BackupFile backupFile, String password) {
+        final Uri uri = backupFile.getUri();
+        Intent intent = new Intent(this, ImportBackupService.class);
+        intent.setAction(Intent.ACTION_SEND);
+        intent.putExtra("password", password);
+        if ("file".equals(uri.getScheme())) {
+            intent.putExtra("file", uri.getPath());
+        } else {
+            intent.setData(uri);
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        }
+        return intent;
+    }
+
     private void setLoadingState(final boolean loadingState) {
         binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE);
         binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE);