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}