request READ_MEDIA_* permission when restoring backup on fdroid version

Daniel Gultsch created

Change summary

src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java |   8 
src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java      | 237 
src/free/AndroidManifest.xml                                                    |   5 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java          |   3 
4 files changed, 182 insertions(+), 71 deletions(-)

Detailed changes

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

@@ -58,6 +58,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -188,12 +189,7 @@ public class ImportBackupService extends Service {
                         }
                     }
                     Collections.sort(
-                            backupFiles,
-                            (a, b) ->
-                                    a.header
-                                            .getJid()
-                                            .toString()
-                                            .compareTo(b.header.getJid().toString()));
+                            backupFiles, Comparator.comparing(a -> a.header.getJid().toString()));
                     onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
                 });
     }

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

@@ -1,11 +1,14 @@
 package eu.siacs.conversations.ui;
 
+import android.Manifest;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.util.Log;
@@ -14,15 +17,16 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.appcompat.app.AlertDialog;
 import androidx.core.content.ContextCompat;
 import androidx.databinding.DataBindingUtil;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.snackbar.Snackbar;
-
-import java.io.IOException;
-import java.util.List;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -30,10 +34,18 @@ 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.SettingsUtils;
 import eu.siacs.conversations.utils.BackupFileHeader;
 
-public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed {
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class ImportBackupActivity extends ActionBarActivity
+        implements ServiceConnection,
+                ImportBackupService.OnBackupFilesLoaded,
+                BackupFileAdapter.OnItemClickedListener,
+                ImportBackupService.OnBackupProcessed {
 
     private ActivityImportBackupBinding binding;
 
@@ -41,8 +53,18 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
     private ImportBackupService service;
 
     private boolean mLoadingState = false;
-
-    private int mTheme;
+    private final ActivityResultLauncher<String[]> requestPermissions =
+            registerForActivityResult(
+                    new ActivityResultContracts.RequestMultiplePermissions(),
+                    results -> {
+                        if (results.containsValue(Boolean.TRUE)) {
+                            final var service = this.service;
+                            if (service == null) {
+                                return;
+                            }
+                            service.loadBackupFiles(this);
+                        }
+                    });
 
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
@@ -50,7 +72,9 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
         binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup);
         Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
         setSupportActionBar(binding.toolbar);
-        setLoadingState(savedInstanceState != null && savedInstanceState.getBoolean("loading_state", false));
+        setLoadingState(
+                savedInstanceState != null
+                        && savedInstanceState.getBoolean("loading_state", false));
         this.backupFileAdapter = new BackupFileAdapter();
         this.binding.list.setAdapter(this.backupFileAdapter);
         this.backupFileAdapter.setOnItemClickedListener(this);
@@ -72,15 +96,55 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
 
     @Override
     public void onStart() {
+
         super.onStart();
         bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
         final Intent intent = getIntent();
-        if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction()) && !this.mLoadingState) {
+        if (intent != null
+                && Intent.ACTION_VIEW.equals(intent.getAction())
+                && !this.mLoadingState) {
             Uri uri = intent.getData();
             if (uri != null) {
                 openBackupFileFromUri(uri, true);
+                return;
             }
         }
+        final List<String> desiredPermission;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            desiredPermission =
+                    ImmutableList.of(
+                            Manifest.permission.READ_MEDIA_IMAGES,
+                            Manifest.permission.READ_MEDIA_VIDEO,
+                            Manifest.permission.READ_MEDIA_AUDIO,
+                            Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED);
+        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
+            desiredPermission =
+                    ImmutableList.of(
+                            Manifest.permission.READ_MEDIA_IMAGES,
+                            Manifest.permission.READ_MEDIA_VIDEO,
+                            Manifest.permission.READ_MEDIA_AUDIO);
+        } else {
+            desiredPermission = ImmutableList.of(Manifest.permission.READ_EXTERNAL_STORAGE);
+        }
+        final Set<String> declaredPermission = getDeclaredPermission();
+        if (declaredPermission.containsAll(desiredPermission)) {
+            requestPermissions.launch(desiredPermission.toArray(new String[0]));
+        } else {
+            Log.d(Config.LOGTAG, "Manifest is lacking some desired permission. not requesting");
+        }
+    }
+
+    private Set<String> getDeclaredPermission() {
+        final String[] permissions;
+        try {
+            permissions =
+                    getPackageManager()
+                            .getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS)
+                            .requestedPermissions;
+        } catch (final PackageManager.NameNotFoundException e) {
+            return Collections.emptySet();
+        }
+        return ImmutableSet.copyOf(permissions);
     }
 
     @Override
@@ -94,7 +158,8 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
 
     @Override
     public void onServiceConnected(ComponentName name, IBinder service) {
-        ImportBackupService.ImportBackupServiceBinder binder = (ImportBackupService.ImportBackupServiceBinder) service;
+        ImportBackupService.ImportBackupServiceBinder binder =
+                (ImportBackupService.ImportBackupServiceBinder) service;
         this.service = binder.getService();
         this.service.addOnBackupProcessedListener(this);
         setLoadingState(this.service.getLoadingState());
@@ -118,55 +183,81 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
 
     private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) {
         try {
-            final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri);
+            final ImportBackupService.BackupFile backupFile =
+                    ImportBackupService.BackupFile.read(this, uri);
             showEnterPasswordDialog(backupFile, finishOnCancel);
         } catch (final BackupFileHeader.OutdatedBackupFileVersion e) {
-            Snackbar.make(binding.coordinator, R.string.outdated_backup_file_format, Snackbar.LENGTH_LONG).show();
+            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);
-            Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show();
+            Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG)
+                    .show();
         } catch (final SecurityException e) {
-            Snackbar.make(binding.coordinator, R.string.sharing_application_not_grant_permission, Snackbar.LENGTH_LONG).show();
+            Snackbar.make(
+                            binding.coordinator,
+                            R.string.sharing_application_not_grant_permission,
+                            Snackbar.LENGTH_LONG)
+                    .show();
         }
     }
 
-    private void showEnterPasswordDialog(final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) {
-        final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false);
+    private void showEnterPasswordDialog(
+            final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) {
+        final DialogEnterPasswordBinding enterPasswordBinding =
+                DataBindingUtil.inflate(
+                        LayoutInflater.from(this), R.layout.dialog_enter_password, null, false);
         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()));
         final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setView(enterPasswordBinding.getRoot());
         builder.setTitle(R.string.enter_password);
-        builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
-            if (finishOnCancel) {
-                finish();
-            }
-        });
+        builder.setNegativeButton(
+                R.string.cancel,
+                (dialog, which) -> {
+                    if (finishOnCancel) {
+                        finish();
+                    }
+                });
         builder.setPositiveButton(R.string.restore, null);
         builder.setCancelable(false);
         final AlertDialog dialog = builder.create();
-        dialog.setOnShowListener((d) -> {
-            dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
-                final String password = enterPasswordBinding.accountPassword.getEditableText().toString();
-                if (password.isEmpty()) {
-                    enterPasswordBinding.accountPasswordLayout.setError(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);
-                }
-                setLoadingState(true);
-                ContextCompat.startForegroundService(this, intent);
-                d.dismiss();
-            });
-        });
+        dialog.setOnShowListener(
+                (d) -> {
+                    dialog.getButton(DialogInterface.BUTTON_POSITIVE)
+                            .setOnClickListener(
+                                    v -> {
+                                        final String password =
+                                                enterPasswordBinding
+                                                        .accountPassword
+                                                        .getEditableText()
+                                                        .toString();
+                                        if (password.isEmpty()) {
+                                            enterPasswordBinding.accountPasswordLayout.setError(
+                                                    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);
+                                        }
+                                        setLoadingState(true);
+                                        ContextCompat.startForegroundService(this, intent);
+                                        d.dismiss();
+                                    });
+                });
         dialog.show();
     }
 
@@ -192,36 +283,55 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
 
     @Override
     public void onAccountAlreadySetup() {
-        runOnUiThread(() -> {
-            setLoadingState(false);
-            Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG).show();
-        });
+        runOnUiThread(
+                () -> {
+                    setLoadingState(false);
+                    Snackbar.make(
+                                    binding.coordinator,
+                                    R.string.account_already_setup,
+                                    Snackbar.LENGTH_LONG)
+                            .show();
+                });
     }
 
     @Override
     public void onBackupRestored() {
-        runOnUiThread(() -> {
-            Intent intent = new Intent(this, ConversationActivity.class);
-            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-            startActivity(intent);
-            finish();
-        });
+        runOnUiThread(
+                () -> {
+                    Intent intent = new Intent(this, ConversationActivity.class);
+                    intent.addFlags(
+                            Intent.FLAG_ACTIVITY_CLEAR_TOP
+                                    | Intent.FLAG_ACTIVITY_NEW_TASK
+                                    | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                    startActivity(intent);
+                    finish();
+                });
     }
 
     @Override
     public void onBackupDecryptionFailed() {
-        runOnUiThread(() -> {
-            setLoadingState(false);
-            Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show();
-        });
+        runOnUiThread(
+                () -> {
+                    setLoadingState(false);
+                    Snackbar.make(
+                                    binding.coordinator,
+                                    R.string.unable_to_decrypt_backup,
+                                    Snackbar.LENGTH_LONG)
+                            .show();
+                });
     }
 
     @Override
     public void onBackupRestoreFailed() {
-        runOnUiThread(() -> {
-            setLoadingState(false);
-            Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show();
-        });
+        runOnUiThread(
+                () -> {
+                    setLoadingState(false);
+                    Snackbar.make(
+                                    binding.coordinator,
+                                    R.string.unable_to_restore_backup,
+                                    Snackbar.LENGTH_LONG)
+                            .show();
+                });
     }
 
     @Override
@@ -238,6 +348,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo
         intent.setType("*/*");
         intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
         intent.addCategory(Intent.CATEGORY_OPENABLE);
-        startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
+        startActivityForResult(
+                Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
     }
 }

src/free/AndroidManifest.xml 🔗

@@ -3,4 +3,9 @@
 
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.READ_PROFILE" />
+
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+    <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
 </manifest>

src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java 🔗

@@ -769,8 +769,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     private void askForContactsPermissions() {
-        if (QuickConversationsService.isContactListIntegration(this)
-                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        if (QuickConversationsService.isContactListIntegration(this)) {
             if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
                     != PackageManager.PERMISSION_GRANTED) {
                 if (mRequestedContactsPermission.compareAndSet(false, true)) {