ImportBackupActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.Manifest;
  4import android.content.ComponentName;
  5import android.content.Context;
  6import android.content.DialogInterface;
  7import android.content.Intent;
  8import android.content.ServiceConnection;
  9import android.content.pm.PackageManager;
 10import android.net.Uri;
 11import android.os.Build;
 12import android.os.Bundle;
 13import android.os.IBinder;
 14import android.util.Log;
 15import android.view.LayoutInflater;
 16import android.view.Menu;
 17import android.view.MenuItem;
 18import android.view.View;
 19
 20import androidx.activity.result.ActivityResultLauncher;
 21import androidx.activity.result.contract.ActivityResultContracts;
 22import androidx.annotation.NonNull;
 23import androidx.appcompat.app.AlertDialog;
 24import androidx.core.content.ContextCompat;
 25import androidx.databinding.DataBindingUtil;
 26
 27import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 28import com.google.android.material.snackbar.Snackbar;
 29import com.google.common.collect.ImmutableList;
 30import com.google.common.collect.ImmutableSet;
 31import com.google.common.util.concurrent.FutureCallback;
 32import com.google.common.util.concurrent.Futures;
 33
 34import eu.siacs.conversations.Config;
 35import eu.siacs.conversations.R;
 36import eu.siacs.conversations.databinding.ActivityImportBackupBinding;
 37import eu.siacs.conversations.databinding.DialogEnterPasswordBinding;
 38import eu.siacs.conversations.services.ImportBackupService;
 39import eu.siacs.conversations.ui.adapter.BackupFileAdapter;
 40import eu.siacs.conversations.ui.util.MainThreadExecutor;
 41import eu.siacs.conversations.utils.BackupFileHeader;
 42
 43import java.io.IOException;
 44import java.util.Collections;
 45import java.util.List;
 46import java.util.Set;
 47
 48public class ImportBackupActivity extends ActionBarActivity
 49        implements ServiceConnection,
 50                ImportBackupService.OnBackupFilesLoaded,
 51                BackupFileAdapter.OnItemClickedListener,
 52                ImportBackupService.OnBackupProcessed {
 53
 54    private ActivityImportBackupBinding binding;
 55
 56    private BackupFileAdapter backupFileAdapter;
 57    private ImportBackupService service;
 58
 59    private boolean mLoadingState = false;
 60    private final ActivityResultLauncher<String[]> requestPermissions =
 61            registerForActivityResult(
 62                    new ActivityResultContracts.RequestMultiplePermissions(),
 63                    results -> {
 64                        if (results.containsValue(Boolean.TRUE)) {
 65                            final var service = this.service;
 66                            if (service == null) {
 67                                return;
 68                            }
 69                            service.loadBackupFiles(this);
 70                        }
 71                    });
 72
 73    @Override
 74    protected void onCreate(final Bundle savedInstanceState) {
 75        super.onCreate(savedInstanceState);
 76        binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup);
 77        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 78        setSupportActionBar(binding.toolbar);
 79        setLoadingState(
 80                savedInstanceState != null
 81                        && savedInstanceState.getBoolean("loading_state", false));
 82        this.backupFileAdapter = new BackupFileAdapter();
 83        this.binding.list.setAdapter(this.backupFileAdapter);
 84        this.backupFileAdapter.setOnItemClickedListener(this);
 85    }
 86
 87    @Override
 88    public boolean onCreateOptionsMenu(final Menu menu) {
 89        getMenuInflater().inflate(R.menu.import_backup, menu);
 90        final MenuItem openBackup = menu.findItem(R.id.action_open_backup_file);
 91        openBackup.setVisible(!this.mLoadingState);
 92        return true;
 93    }
 94
 95    @Override
 96    public void onSaveInstanceState(Bundle bundle) {
 97        bundle.putBoolean("loading_state", this.mLoadingState);
 98        super.onSaveInstanceState(bundle);
 99    }
100
101    @Override
102    public void onStart() {
103
104        super.onStart();
105        bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE);
106        final Intent intent = getIntent();
107        if (intent != null
108                && Intent.ACTION_VIEW.equals(intent.getAction())
109                && !this.mLoadingState) {
110            Uri uri = intent.getData();
111            if (uri != null) {
112                openBackupFileFromUri(uri, true);
113                return;
114            }
115        }
116        final List<String> desiredPermission;
117        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
118            desiredPermission =
119                    ImmutableList.of(
120                            Manifest.permission.READ_MEDIA_IMAGES,
121                            Manifest.permission.READ_MEDIA_VIDEO,
122                            Manifest.permission.READ_MEDIA_AUDIO,
123                            Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED);
124        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
125            desiredPermission =
126                    ImmutableList.of(
127                            Manifest.permission.READ_MEDIA_IMAGES,
128                            Manifest.permission.READ_MEDIA_VIDEO,
129                            Manifest.permission.READ_MEDIA_AUDIO);
130        } else {
131            desiredPermission = ImmutableList.of(Manifest.permission.READ_EXTERNAL_STORAGE);
132        }
133        final Set<String> declaredPermission = getDeclaredPermission();
134        if (declaredPermission.containsAll(desiredPermission)) {
135            requestPermissions.launch(desiredPermission.toArray(new String[0]));
136        } else {
137            Log.d(Config.LOGTAG, "Manifest is lacking some desired permission. not requesting");
138        }
139    }
140
141    private Set<String> getDeclaredPermission() {
142        final String[] permissions;
143        try {
144            permissions =
145                    getPackageManager()
146                            .getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS)
147                            .requestedPermissions;
148        } catch (final PackageManager.NameNotFoundException e) {
149            return Collections.emptySet();
150        }
151        return ImmutableSet.copyOf(permissions);
152    }
153
154    @Override
155    public void onStop() {
156        super.onStop();
157        if (this.service != null) {
158            this.service.removeOnBackupProcessedListener(this);
159        }
160        unbindService(this);
161    }
162
163    @Override
164    public void onServiceConnected(ComponentName name, IBinder service) {
165        ImportBackupService.ImportBackupServiceBinder binder =
166                (ImportBackupService.ImportBackupServiceBinder) service;
167        this.service = binder.getService();
168        this.service.addOnBackupProcessedListener(this);
169        setLoadingState(this.service.getLoadingState());
170        this.service.loadBackupFiles(this);
171    }
172
173    @Override
174    public void onServiceDisconnected(ComponentName name) {
175        this.service = null;
176    }
177
178    @Override
179    public void onBackupFilesLoaded(final List<ImportBackupService.BackupFile> files) {
180        runOnUiThread(() -> backupFileAdapter.setFiles(files));
181    }
182
183    @Override
184    public void onClick(final ImportBackupService.BackupFile backupFile) {
185        showEnterPasswordDialog(backupFile, false);
186    }
187
188    private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) {
189        final var backupFileFuture = ImportBackupService.read(this, uri);
190        Futures.addCallback(
191                backupFileFuture,
192                new FutureCallback<>() {
193                    @Override
194                    public void onSuccess(final ImportBackupService.BackupFile backupFile) {
195                        showEnterPasswordDialog(backupFile, finishOnCancel);
196                    }
197
198                    @Override
199                    public void onFailure(@NonNull final Throwable throwable) {
200                        Log.d(Config.LOGTAG, "could not open backup file " + uri, throwable);
201                        showBackupThrowable(throwable);
202                    }
203                },
204                MainThreadExecutor.getInstance());
205    }
206
207    private void showBackupThrowable(final Throwable throwable) {
208        if (throwable instanceof BackupFileHeader.OutdatedBackupFileVersion) {
209            Snackbar.make(
210                            binding.coordinator,
211                            R.string.outdated_backup_file_format,
212                            Snackbar.LENGTH_LONG)
213                    .show();
214        } else if (throwable instanceof IOException
215                || throwable instanceof IllegalArgumentException) {
216            Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG)
217                    .show();
218        } else if (throwable instanceof SecurityException e) {
219            Snackbar.make(
220                            binding.coordinator,
221                            R.string.sharing_application_not_grant_permission,
222                            Snackbar.LENGTH_LONG)
223                    .show();
224        }
225    }
226
227    private void showEnterPasswordDialog(
228            final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) {
229        final DialogEnterPasswordBinding enterPasswordBinding =
230                DataBindingUtil.inflate(
231                        LayoutInflater.from(this), R.layout.dialog_enter_password, null, false);
232        Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri());
233        enterPasswordBinding.explain.setText(
234                getString(
235                        R.string.enter_password_to_restore,
236                        backupFile.getHeader().getJid().toString()));
237        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
238        builder.setView(enterPasswordBinding.getRoot());
239        builder.setTitle(R.string.enter_password);
240        builder.setNegativeButton(
241                R.string.cancel,
242                (dialog, which) -> {
243                    if (finishOnCancel) {
244                        finish();
245                    }
246                });
247        builder.setPositiveButton(R.string.restore, null);
248        builder.setCancelable(false);
249        final AlertDialog dialog = builder.create();
250        dialog.setOnShowListener(
251                (d) -> {
252                    dialog.getButton(DialogInterface.BUTTON_POSITIVE)
253                            .setOnClickListener(
254                                    v -> {
255                                        final String password =
256                                                enterPasswordBinding
257                                                        .accountPassword
258                                                        .getEditableText()
259                                                        .toString();
260                                        if (password.isEmpty()) {
261                                            enterPasswordBinding.accountPasswordLayout.setError(
262                                                    getString(R.string.please_enter_password));
263                                            return;
264                                        }
265                                        final Intent intent = getIntent(backupFile, password);
266                                        setLoadingState(true);
267                                        ContextCompat.startForegroundService(this, intent);
268                                        d.dismiss();
269                                    });
270                });
271        dialog.show();
272    }
273
274    @NonNull
275    private Intent getIntent(ImportBackupService.BackupFile backupFile, String password) {
276        final Uri uri = backupFile.getUri();
277        Intent intent = new Intent(this, ImportBackupService.class);
278        intent.setAction(Intent.ACTION_SEND);
279        intent.putExtra("password", password);
280        if ("file".equals(uri.getScheme())) {
281            intent.putExtra("file", uri.getPath());
282        } else {
283            intent.setData(uri);
284            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
285        }
286        return intent;
287    }
288
289    private void setLoadingState(final boolean loadingState) {
290        binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE);
291        binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE);
292        setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup);
293        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
294        configureActionBar(getSupportActionBar(), !loadingState);
295        this.mLoadingState = loadingState;
296        invalidateOptionsMenu();
297    }
298
299    @Override
300    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
301        super.onActivityResult(requestCode, resultCode, intent);
302        if (resultCode == RESULT_OK) {
303            if (requestCode == 0xbac) {
304                openBackupFileFromUri(intent.getData(), false);
305            }
306        }
307    }
308
309    @Override
310    public void onAccountAlreadySetup() {
311        runOnUiThread(
312                () -> {
313                    setLoadingState(false);
314                    Snackbar.make(
315                                    binding.coordinator,
316                                    R.string.account_already_setup,
317                                    Snackbar.LENGTH_LONG)
318                            .show();
319                });
320    }
321
322    @Override
323    public void onBackupRestored() {
324        runOnUiThread(
325                () -> {
326                    Intent intent = new Intent(this, ConversationActivity.class);
327                    intent.addFlags(
328                            Intent.FLAG_ACTIVITY_CLEAR_TOP
329                                    | Intent.FLAG_ACTIVITY_NEW_TASK
330                                    | Intent.FLAG_ACTIVITY_CLEAR_TASK);
331                    startActivity(intent);
332                    finish();
333                });
334    }
335
336    @Override
337    public void onBackupDecryptionFailed() {
338        runOnUiThread(
339                () -> {
340                    setLoadingState(false);
341                    Snackbar.make(
342                                    binding.coordinator,
343                                    R.string.unable_to_decrypt_backup,
344                                    Snackbar.LENGTH_LONG)
345                            .show();
346                });
347    }
348
349    @Override
350    public void onBackupRestoreFailed() {
351        runOnUiThread(
352                () -> {
353                    setLoadingState(false);
354                    Snackbar.make(
355                                    binding.coordinator,
356                                    R.string.unable_to_restore_backup,
357                                    Snackbar.LENGTH_LONG)
358                            .show();
359                });
360    }
361
362    @Override
363    public boolean onOptionsItemSelected(MenuItem item) {
364        if (item.getItemId() == R.id.action_open_backup_file) {
365            openBackupFile();
366            return true;
367        }
368        return super.onOptionsItemSelected(item);
369    }
370
371    private void openBackupFile() {
372        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
373        intent.setType("*/*");
374        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
375        intent.addCategory(Intent.CATEGORY_OPENABLE);
376        startActivityForResult(
377                Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac);
378    }
379}