Files
arcanechat-android/src/main/java/org/thoughtcrime/securesms/WelcomeActivity.java
T
2026-01-21 18:59:56 +01:00

402 lines
16 KiB
Java

package org.thoughtcrime.securesms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.util.Linkify;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.BackupTransferActivity;
import org.thoughtcrime.securesms.qr.QrCodeHandler;
import org.thoughtcrime.securesms.qr.RegistrationQrActivity;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.StreamUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.ProgressDialog;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class WelcomeActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate {
public static final String BACKUP_QR_EXTRA = "backup_qr_extra";
public static final int PICK_BACKUP = 20574;
private final static String TAG = WelcomeActivity.class.getSimpleName();
public static final String TMP_BACKUP_FILE = "tmp-backup-file";
private ProgressDialog progressDialog = null;
private boolean imexUserAborted;
DcContext dcContext;
private NotificationController notificationController;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.welcome_activity);
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(findViewById(R.id.content_container));
Button signUpButton = findViewById(R.id.signup_button);
Button signInButton = findViewById(R.id.signin_button);
View view = View.inflate(this, R.layout.login_options_view, null);
AlertDialog signInDialog = new AlertDialog.Builder(this)
.setView(view)
.setTitle(R.string.onboarding_alternative_logins)
.setNegativeButton(R.string.cancel, null)
.create();
view.findViewById(R.id.add_as_second_device_button).setOnClickListener((v) -> {
showSignInDialogWithPermission(signInDialog);
});
view.findViewById(R.id.backup_button).setOnClickListener((v) -> {
startImportBackup();
signInDialog.dismiss();
});
signUpButton.setOnClickListener((v) -> startActivity(new Intent(this, InstantOnboardingActivity.class)));
signInButton.setOnClickListener((v) -> signInDialog.show());
registerForEvents();
initializeActionBar();
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
AccountManager accountManager = AccountManager.getInstance();
if (accountManager.canRollbackAccountCreation(WelcomeActivity.this)) {
accountManager.rollbackAccountCreation(WelcomeActivity.this);
} else {
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
}
}
});
DcHelper.maybeShowMigrationError(this);
}
private void showSignInDialogWithPermission(AlertDialog signInDialog) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& !Prefs.getBooleanPreference(this, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, false)) {
Prefs.setBooleanPreference(this, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, true);
Permissions.with(this)
.request(Manifest.permission.POST_NOTIFICATIONS)
.ifNecessary()
.onAllGranted(() -> {
startAddAsSecondDeviceActivity();
signInDialog.dismiss();
})
.onAnyDenied(() -> {
startAddAsSecondDeviceActivity();
signInDialog.dismiss();
})
.execute();
} else {
startAddAsSecondDeviceActivity();
signInDialog.dismiss();
}
}
protected void initializeActionBar() {
ActionBar supportActionBar = getSupportActionBar();
if (supportActionBar == null) throw new AssertionError();
boolean canGoBack = AccountManager.getInstance().canRollbackAccountCreation(this);
supportActionBar.setDisplayHomeAsUpEnabled(canGoBack);
getSupportActionBar().setTitle(canGoBack? R.string.add_account : R.string.welcome_desktop);
}
private void registerForEvents() {
dcContext = DcHelper.getContext(this);
DcHelper.getEventCenter(this).addObserver(DcContext.DC_EVENT_IMEX_PROGRESS, this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
getOnBackPressedDispatcher().onBackPressed();
return true;
}
return false;
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
}
@Override
public void onStart() {
super.onStart();
String backupQr = getIntent().getStringExtra(BACKUP_QR_EXTRA);
if (backupQr != null) {
getIntent().removeExtra(BACKUP_QR_EXTRA);
startBackupTransfer(backupQr);
}
}
@Override
public void onDestroy() {
super.onDestroy();
DcHelper.getEventCenter(this).removeObservers(this);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void startAddAsSecondDeviceActivity() {
new IntentIntegrator(this).setCaptureActivity(RegistrationQrActivity.class)
.addExtra(RegistrationQrActivity.ADD_AS_SECOND_DEVICE_EXTRA, true)
.initiateScan();
}
@SuppressLint("InlinedApi")
private void startImportBackup() {
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.alwaysGrantOnSdk30()
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied))
.onAllGranted(() -> {
File imexDir = DcHelper.getImexDir();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AttachmentManager.selectMediaType(this, "application/x-tar", null, PICK_BACKUP, StorageUtil.getDownloadUri());
} else {
final String backupFile = dcContext.imexHasBackup(imexDir.getAbsolutePath());
if (backupFile != null) {
new AlertDialog.Builder(this)
.setTitle(R.string.import_backup_title)
.setMessage(String.format(getResources().getString(R.string.import_backup_ask), backupFile))
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> startImport(backupFile, null))
.show();
}
else {
new AlertDialog.Builder(this)
.setTitle(R.string.import_backup_title)
.setMessage(String.format(getResources().getString(R.string.import_backup_no_backup_found), imexDir.getAbsolutePath()))
.setPositiveButton(android.R.string.ok, null)
.show();
}
}
})
.execute();
}
private void startImport(@Nullable final String backupFile, final @Nullable Uri backupFileUri)
{
notificationController = GenericForegroundService.startForegroundTask(this, getString(R.string.import_backup_title));
if( progressDialog!=null ) {
progressDialog.dismiss();
progressDialog = null;
}
imexUserAborted = false;
progressDialog = new ProgressDialog(this);
progressDialog.setMessage(getResources().getString(R.string.one_moment));
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.setCancelable(false);
progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getResources().getString(android.R.string.cancel), (dialog, which) -> {
imexUserAborted = true;
dcContext.stopOngoingProcess();
notificationController.close();
cleanupTempBackupFile();
});
progressDialog.show();
Util.runOnBackground(() -> {
String file = backupFile;
if (backupFile == null) {
try {
file = copyToCacheDir(backupFileUri).getAbsolutePath();
} catch (IOException e) {
e.printStackTrace();
notificationController.close();
cleanupTempBackupFile();
return;
}
}
DcHelper.getEventCenter(this).captureNextError();
dcContext.imex(DcContext.DC_IMEX_IMPORT_BACKUP, file);
});
}
private File copyToCacheDir(Uri uri) throws IOException {
try (InputStream inputStream = getContentResolver().openInputStream(uri)) {
File file = File.createTempFile(TMP_BACKUP_FILE, ".tmp", getCacheDir());
try (OutputStream outputStream = new FileOutputStream(file)) {
StreamUtil.copy(inputStream, outputStream);
}
return file;
}
}
private void startBackupTransfer(String qrCode)
{
if (progressDialog!=null) {
progressDialog.dismiss();
progressDialog = null;
}
Intent intent = new Intent(this, BackupTransferActivity.class);
intent.putExtra(BackupTransferActivity.TRANSFER_MODE, BackupTransferActivity.TransferMode.RECEIVER_SCAN_QR.getInt());
intent.putExtra(BackupTransferActivity.QR_CODE, qrCode);
startActivity(intent);
}
private void progressError(String data2) {
progressDialog.dismiss();
maybeShowConfigurationError(this, data2);
}
private void progressUpdate(int progress) {
int percent = progress / 10;
progressDialog.setMessage(getResources().getString(R.string.one_moment)+String.format(" %d%%", percent));
}
private void progressSuccess() {
DcHelper.getEventCenter(this).endCaptureNextError();
progressDialog.dismiss();
Intent intent = new Intent(getApplicationContext(), ConversationListActivity.class);
intent.putExtra(ConversationListActivity.FROM_WELCOME, true);
startActivity(intent);
finish();
}
public static void maybeShowConfigurationError(Activity activity, String data2) {
if (activity.isFinishing()) return; // avoid android.view.WindowManager$BadTokenException
if (data2 != null && !data2.isEmpty()) {
AlertDialog d = new AlertDialog.Builder(activity)
.setMessage(data2)
.setPositiveButton(android.R.string.ok, null)
.create();
d.show();
try {
//noinspection ConstantConditions
Linkify.addLinks((TextView) d.findViewById(android.R.id.message), Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES);
} catch(NullPointerException e) {
e.printStackTrace();
}
}
}
@Override
public void handleEvent(@NonNull DcEvent event) {
int eventId = event.getId();
if (eventId== DcContext.DC_EVENT_IMEX_PROGRESS ) {
long progress = event.getData1Int();
if (progressDialog == null || notificationController == null) {
// IMEX runs in BackupTransferActivity
if (progress == 1000) {
finish(); // transfer done - remove ourself from the activity stack (finishAffinity is available in API 16, we're targeting API 14)
}
return;
}
if (progress==0/*error/aborted*/) {
if (!imexUserAborted) {
progressError(dcContext.getLastError());
}
notificationController.close();
cleanupTempBackupFile();
}
else if (progress<1000/*progress in permille*/) {
progressUpdate((int)progress);
notificationController.setProgress(1000, progress, String.format(" %d%%", (int) progress / 10));
}
else if (progress==1000/*done*/) {
DcHelper.getAccounts(this).startIo();
progressSuccess();
notificationController.close();
cleanupTempBackupFile();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) {
return;
}
if (requestCode==IntentIntegrator.REQUEST_CODE) {
String qrRaw = data.getStringExtra(RegistrationQrActivity.QRDATA_EXTRA);
if (qrRaw == null) {
IntentResult scanResult = IntentIntegrator.parseActivityResult(resultCode, data);
qrRaw = scanResult.getContents();
}
if (!new QrCodeHandler(this).handleBackupQr(qrRaw)) {
new AlertDialog.Builder(this)
.setMessage(R.string.qraccount_qr_code_cannot_be_used)
.setPositiveButton(R.string.ok, null)
.show();
}
} else if (requestCode == PICK_BACKUP) {
Uri uri = (data != null ? data.getData() : null);
if (uri == null) {
Log.e(TAG, " Can't import null URI");
return;
}
startImport(null, uri);
}
}
private void cleanupTempBackupFile() {
try {
File[] files = getCacheDir().listFiles((dir, name) -> name.startsWith(TMP_BACKUP_FILE));
for (File file : files) {
if (file.getName().endsWith("tmp")) {
Log.i(TAG, "Deleting temp backup file " + file);
file.delete();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}