Compare commits

...

18 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] be7351d683 Add clarifying comments based on code review feedback
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2025-12-15 11:12:02 +00:00
copilot-swe-agent[bot] 79bdc73444 Fix ANR by moving DcAccounts initialization to background thread with synchronized access
Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2025-12-15 11:08:42 +00:00
copilot-swe-agent[bot] e919a8614e Initial plan 2025-12-15 11:02:33 +00:00
adbenitez e289432a09 Merge remote-tracking branch 'upstream/main' 2025-12-13 11:51:12 +01:00
adbenitez 8509049791 update intro image 2025-12-13 11:50:35 +01:00
adbenitez d1b490a02d update spanish strings 2025-12-13 11:09:22 +01:00
adb 64d33bebbd Merge pull request #4119 from deltachat/adb/issue-4118
avoid NPE in InstantOnboardingActivity.onCreate
2025-12-12 17:14:38 +01:00
adbenitez 0a44d00451 add comment 2025-12-12 17:04:36 +01:00
adb 353b2c0488 Merge pull request #4121 from deltachat/adb/issue-4114
Allow to add relay from clipboard or image
2025-12-12 16:40:33 +01:00
B. Petersen 85c68d0485 remove dead account-creation-from-qr code 2025-12-12 16:39:27 +01:00
adbenitez 12c2237e00 remove leftover 2025-12-12 16:30:58 +01:00
adbenitez 68f9533392 Allow to add relay from clipboard or image if camera permission is not granted 2025-12-12 16:29:01 +01:00
adb e93efa318a Merge pull request #4117 from deltachat/adb/issue-4116
add more logging
2025-12-12 15:47:15 +01:00
adbenitez fbc01ff0a2 switch to new profile if no URI is available 2025-12-12 15:05:04 +01:00
adbenitez b0ca48740a avoid NPE in InstantOnboardingActivity.onCreate 2025-12-12 15:01:41 +01:00
adbenitez fa795dd149 add more logging 2025-12-12 14:28:17 +01:00
B. Petersen 0a4f1ded54 tune down 'account' wording 2025-12-11 18:40:00 +01:00
adbenitez b2e88d50fd fix typo 2025-12-10 17:24:01 +01:00
12 changed files with 181 additions and 96 deletions
+4
View File
@@ -1,5 +1,9 @@
# Delta Chat Android Changelog
## Unreleased
* Allow to add relay from clipboard or image if camera permission is not granted
## v2.33.1
2025-12
@@ -58,9 +58,12 @@ import chat.delta.rpc.RpcException;
public class ApplicationContext extends MultiDexApplication {
private static final String TAG = ApplicationContext.class.getSimpleName();
public static DcAccounts dcAccounts;
public Rpc rpc;
public DcContext dcContext;
private static final Object initLock = new Object();
private static volatile boolean isInitialized = false;
private static DcAccounts dcAccounts;
private Rpc rpc;
private DcContext dcContext;
public DcLocationManager dcLocationManager;
public DcEventCenter eventCenter;
public NotificationCenter notificationCenter;
@@ -75,6 +78,71 @@ public class ApplicationContext extends MultiDexApplication {
return (ApplicationContext)context.getApplicationContext();
}
/**
* Get DcAccounts instance, waiting for initialization if necessary.
* This method is thread-safe and will block until initialization is complete.
*/
public static DcAccounts getDcAccounts() {
synchronized (initLock) {
while (!isInitialized) {
try {
initLock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for DcAccounts initialization", e);
}
}
return dcAccounts;
}
}
/**
* Get Rpc instance, waiting for initialization if necessary.
* This method is thread-safe and will block until initialization is complete.
*/
public Rpc getRpc() {
synchronized (initLock) {
while (!isInitialized) {
try {
initLock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for Rpc initialization", e);
}
}
return rpc;
}
}
/**
* Get DcContext instance, waiting for initialization if necessary.
* This method is thread-safe and will block until initialization is complete.
*/
public DcContext getDcContext() {
synchronized (initLock) {
while (!isInitialized) {
try {
initLock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for DcContext initialization", e);
}
}
return dcContext;
}
}
/**
* Set DcContext instance. This should only be called by AccountManager when switching accounts,
* which only happens after initial initialization is complete.
* This method is thread-safe but does NOT trigger initialization or notify waiting threads.
*/
public void setDcContext(DcContext context) {
synchronized (initLock) {
this.dcContext = context;
}
}
@Override
public void onCreate() {
super.onCreate();
@@ -110,61 +178,87 @@ public class ApplicationContext extends MultiDexApplication {
System.loadLibrary("native-utils");
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath());
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
AccountManager.getInstance().migrateToDcAccounts(this);
// October-2025 migration: delete deprecated "permanent channel" id
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.deleteNotificationChannel("dc_foreground_notification_ch");
// end October-2025 migration
int[] allAccounts = dcAccounts.getAll();
for (int accountId : allAccounts) {
DcContext ac = dcAccounts.getAccount(accountId);
if (!ac.isOpen()) {
// Initialize DcAccounts in background to avoid ANR during SQL migrations
Util.runOnBackground(() -> {
synchronized (initLock) {
try {
DatabaseSecret secret = DatabaseSecretProvider.getOrCreateDatabaseSecret(this, accountId);
boolean res = ac.open(secret.asString());
if (res) Log.i(TAG, "Successfully opened account " + accountId + ", path: " + ac.getBlobdir());
else Log.e(TAG, "Error opening account " + accountId + ", path: " + ac.getBlobdir());
Log.i(TAG, "Starting DcAccounts initialization in background");
dcAccounts = new DcAccounts(new File(getFilesDir(), "accounts").getAbsolutePath());
Log.i(TAG, "DcAccounts created");
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
Log.i(TAG, "Rpc created");
AccountManager.getInstance().migrateToDcAccounts(ApplicationContext.this);
// October-2025 migration: delete deprecated "permanent channel" id
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(ApplicationContext.this);
notificationManager.deleteNotificationChannel("dc_foreground_notification_ch");
// end October-2025 migration
int[] allAccounts = dcAccounts.getAll();
Log.i(TAG, "Number of profiles: " + allAccounts.length);
for (int accountId : allAccounts) {
DcContext ac = dcAccounts.getAccount(accountId);
if (!ac.isOpen()) {
try {
DatabaseSecret secret = DatabaseSecretProvider.getOrCreateDatabaseSecret(ApplicationContext.this, accountId);
boolean res = ac.open(secret.asString());
if (res) Log.i(TAG, "Successfully opened account " + accountId + ", path: " + ac.getBlobdir());
else Log.e(TAG, "Error opening account " + accountId + ", path: " + ac.getBlobdir());
} catch (Exception e) {
Log.e(TAG, "Failed to open account " + accountId + ", path: " + ac.getBlobdir() + ": " + e);
e.printStackTrace();
}
}
// 2025.11.12: this is needed until core starts ignoring "delete_server_after" for chatmail
if (ac.isChatmail()) {
ac.setConfig("delete_server_after", null); // reset
}
}
if (allAccounts.length == 0) {
try {
rpc.addAccount();
} catch (RpcException e) {
e.printStackTrace();
}
}
dcContext = dcAccounts.getSelectedAccount();
notificationCenter = new NotificationCenter(ApplicationContext.this);
eventCenter = new DcEventCenter(ApplicationContext.this);
// Mark as initialized before starting threads that depend on it
isInitialized = true;
initLock.notifyAll();
Log.i(TAG, "DcAccounts initialization complete");
// Start event loop thread
new Thread(() -> {
Log.i(TAG, "Starting event loop");
DcEventEmitter emitter = dcAccounts.getEventEmitter();
Log.i(TAG, "DcEventEmitter obtained");
while (true) {
DcEvent event = emitter.getNextEvent();
if (event==null) {
break;
}
eventCenter.handleEvent(event);
}
Log.i("DeltaChat", "shutting down event handler");
}, "eventThread").start();
// set translations before starting I/O to avoid sending untranslated MDNs (issue #2288)
DcHelper.setStockTranslations(ApplicationContext.this);
dcAccounts.startIo();
} catch (Exception e) {
Log.e(TAG, "Failed to open account " + accountId + ", path: " + ac.getBlobdir() + ": " + e);
e.printStackTrace();
Log.e(TAG, "Fatal error during DcAccounts initialization", e);
// Mark as initialized even on error to avoid deadlock
isInitialized = true;
initLock.notifyAll();
throw new RuntimeException("Failed to initialize DcAccounts", e);
}
}
// 2025.11.12: this is needed until core starts ignoring "delete_server_after" for chatmail
if (ac.isChatmail()) {
ac.setConfig("delete_server_after", null); // reset
}
}
if (allAccounts.length == 0) {
try {
rpc.addAccount();
} catch (RpcException e) {
e.printStackTrace();
}
}
dcContext = dcAccounts.getSelectedAccount();
notificationCenter = new NotificationCenter(this);
eventCenter = new DcEventCenter(this);
new Thread(() -> {
DcEventEmitter emitter = dcAccounts.getEventEmitter();
while (true) {
DcEvent event = emitter.getNextEvent();
if (event==null) {
break;
}
eventCenter.handleEvent(event);
}
Log.i("DeltaChat", "shutting down event handler");
}, "eventThread").start();
// set translations before starting I/O to avoid sending untranslated MDNs (issue #2288)
DcHelper.setStockTranslations(this);
dcAccounts.startIo();
});
new ForegroundDetector(ApplicationContext.getInstance(this));
@@ -175,7 +269,9 @@ public class ApplicationContext extends MultiDexApplication {
@Override
public void onAvailable(@NonNull android.net.Network network) {
Log.i("DeltaChat", "++++++++++++++++++ NetworkCallback.onAvailable() #" + debugOnAvailableCount++);
dcAccounts.maybeNetwork();
// getDcAccounts() will wait for initialization to complete before calling maybeNetwork()
// This is intentional - early network events should wait for proper initialization
getDcAccounts().maybeNetwork();
}
@Override
@@ -76,7 +76,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
private static final String INSTANCES_URL = "https://chatmail.at/relays";
private static final String DEFAULT_CHATMAIL_HOST = "arcanechat.me";
public static final String QR_ACCOUNT_EXTRA = "qr_account_extra";
public static final String FROM_WELCOME = "from_welcome";
private static final int REQUEST_CODE_AVATAR = 1;
@@ -113,12 +112,16 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
if (DcHelper.getContext(this).isConfigured() == 1) {
// if account is configured it means we didn't come from Welcome screen nor from QR scanner,
// instead, user clicked a dcaccount:// URI directly, so we need to just offer to add a new relay
Intent intent = new Intent(this, RelayListActivity.class);
String qrData = getIntent().getData().toString();
intent.putExtra(RelayListActivity.EXTRA_QR_DATA, qrData);
startActivity(intent);
finish();
return;
Uri uri = getIntent().getData();
if (uri != null) {
Intent intent = new Intent(this, RelayListActivity.class);
intent.putExtra(RelayListActivity.EXTRA_QR_DATA, uri.toString());
startActivity(intent);
finish();
return;
}
// if URI is unexpectedly null, then fallback to new profile creation
AccountManager.getInstance().beginAccountCreation(this);
}
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(!fromWelcome) {
@@ -241,16 +244,6 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onStart() {
super.onStart();
String accountQr = getIntent().getStringExtra(QR_ACCOUNT_EXTRA);
if (accountQr != null) {
getIntent().removeExtra(QR_ACCOUNT_EXTRA);
setProviderFromQr(accountQr);
}
}
@Override
protected void onPause() {
super.onPause();
@@ -110,7 +110,7 @@ public class DozeReminder {
}
private static boolean isPushAvailableAndSufficient() {
return ApplicationContext.dcAccounts.isAllChatmail()
return ApplicationContext.getDcAccounts().isAllChatmail()
&& FcmReceiveService.getToken() != null;
}
@@ -31,7 +31,7 @@ public class AccountManager {
private void resetDcContext(Context context) {
ApplicationContext appContext = (ApplicationContext)context.getApplicationContext();
appContext.dcContext = ApplicationContext.dcAccounts.getSelectedAccount();
appContext.setDcContext(ApplicationContext.getDcAccounts().getSelectedAccount());
DcHelper.setStockTranslations(context);
DirectShareUtil.resetAllShortcuts(appContext);
}
@@ -55,7 +55,7 @@ public class AccountManager {
for (File file : files) {
// old accounts have the pattern "messenger*.db"
if (!file.isDirectory() && file.getName().startsWith("messenger") && file.getName().endsWith(".db")) {
int accountId = ApplicationContext.dcAccounts.migrateAccount(file.getAbsolutePath());
int accountId = ApplicationContext.getDcAccounts().migrateAccount(file.getAbsolutePath());
if (accountId != 0) {
String selName = PreferenceManager.getDefaultSharedPreferences(context)
.getString("curr_account_db_name", "messenger.db");
@@ -70,7 +70,7 @@ public class AccountManager {
}
if (selectAccountId != 0) {
ApplicationContext.dcAccounts.selectAccount(selectAccountId);
ApplicationContext.getDcAccounts().selectAccount(selectAccountId);
}
} catch (Exception e) {
Log.e(TAG, "Error in migrateToDcAccounts()", e);
@@ -152,14 +152,6 @@ public class AccountManager {
dialog.show(((FragmentActivity) activity).getSupportFragmentManager(), null);
}
public void addAccountFromQr(Activity activity, String qr) {
beginAccountCreation(activity);
activity.finishAffinity();
Intent intent = new Intent(activity, InstantOnboardingActivity.class);
intent.putExtra(InstantOnboardingActivity.QR_ACCOUNT_EXTRA, qr);
activity.startActivity(intent);
}
public void addAccountFromSecondDevice(Activity activity, String backupQr) {
switchAccountAndStartActivity(activity, 0, backupQr);
}
@@ -64,15 +64,15 @@ public class DcHelper {
public static final String CONFIG_PRIVATE_TAG = "private_tag";
public static DcContext getContext(@NonNull Context context) {
return ApplicationContext.getInstance(context).dcContext;
return ApplicationContext.getInstance(context).getDcContext();
}
public static Rpc getRpc(@NonNull Context context) {
return ApplicationContext.getInstance(context).rpc;
return ApplicationContext.getInstance(context).getRpc();
}
public static DcAccounts getAccounts(@NonNull Context context) {
return ApplicationContext.getInstance(context).dcAccounts;
return ApplicationContext.getDcAccounts();
}
public static DcEventCenter getEventCenter(@NonNull Context context) {
@@ -110,7 +110,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
.onAllGranted(() -> ((QrScanFragment) adapter.getItem(TAB_SCAN)).handleQrScanWithPermissions(QrActivity.this))
.onAnyDenied(() -> {
if (scanRelay) {
finish();
Toast.makeText(this, getString(R.string.chat_camera_unavailable), Toast.LENGTH_LONG).show();
} else {
viewPager.setCurrentItem(TAB_SHOW);
}
@@ -162,7 +162,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
&& Manifest.permission.CAMERA.equals(permissions[0])
&& grantResults[0] == PackageManager.PERMISSION_DENIED) {
if (scanRelay) {
finish();
Toast.makeText(this, getString(R.string.chat_camera_unavailable), Toast.LENGTH_LONG).show();
} else {
viewPager.setCurrentItem(TAB_SHOW);
}
@@ -45,7 +45,7 @@ public final class FetchForegroundService extends Service {
// we need to handle the message within 20s, and the time window may be even shorter than 20s,
// so, use 10s to be safe.
fetchingSynchronously = true;
if (ApplicationContext.dcAccounts.backgroundFetch(10)) {
if (ApplicationContext.getDcAccounts().backgroundFetch(10)) {
// The background fetch was successful, but we need to wait until all events were processed.
// After all events were processed, we will get DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE,
// and stop() will be called.
@@ -92,7 +92,7 @@ public final class FetchForegroundService extends Service {
Util.runOnAnyBackgroundThread(() -> {
Log.i(TAG, "Starting fetch");
if (!ApplicationContext.dcAccounts.backgroundFetch(300)) { // as startForeground() was called, there is time
if (!ApplicationContext.getDcAccounts().backgroundFetch(300)) { // as startForeground() was called, there is time
FetchForegroundService.stop(this);
} // else we stop FetchForegroundService on DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE
});
@@ -111,7 +111,7 @@ public final class FetchForegroundService extends Service {
@Override
public void onTimeout(int startId, int fgsType) {
ApplicationContext.dcAccounts.stopBackgroundFetch();
ApplicationContext.getDcAccounts().stopBackgroundFetch();
stopSelf();
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 430 KiB

@@ -8,7 +8,7 @@
<string name="share_location_for_12_hours">Por 12 horas</string>
<!-- device messages for updates -->
<string name="device_welcome_message">🔮 Bienvenido a ArcaneChat✨\n\nPara ponerte en contacto con amigos:\n\n🤳🏻 Toca \"Código QR\" en la pantalla principal de ambos dispositivos. Elije \"Escanear código QR\" en un dispositivo y apúnta la cámara hacia el otro\n\n🔗 Si no estás en la misma habitación, comparte tu enlace de invitación usando \"Invitar amigos\" en el menú\n\n📢 Únete al [Canal Oficial de ArcaneChat](%1$s) para noticias sobre el desarrollo de la aplicación.</string>
<string name="device_welcome_message">🔮 Bienvenido a ArcaneChat✨\n\nPara ponerte en contacto con amigos:\n\n🤳🏻 Toca \"Código QR\" en la pantalla principal de ambos dispositivos. Elije \"Escanear código QR\" en un dispositivo y apunta la cámara hacia el otro\n\n🔗 Si no estás en la misma habitación, comparte tu enlace de invitación usando \"Invitar amigos\" en el menú\n\n📢 Únete al [Canal Oficial de ArcaneChat](%1$s) (en inglés) para noticias sobre el desarrollo de la aplicación.</string>
<string name="update_2_33">🔮 ¿Qué hay de nuevo?\n\n★ Soporte para Android 16\n\n★ Mejorado el diseño para hacer mejor uso del espacio en pantalla\n\n★ Añadido color a los enlaces en las burbujas del chat\n\n★ Más protección de metadatos de los mensajes\n\n★ Arreglado problema con fallos inesperados de la aplicación\n\n★ Varios otros arreglos y pequeñas mejoras\n\n\n💜 Dona para ayudarnos a mantener nuestra independencia y seguir mejorando: %1$s</string>
</resources>
+2 -2
View File
@@ -611,7 +611,7 @@
<string name="onboarding_create_instant_account">Create New Profile</string>
<!-- Secondary button on the welcome screen, allows to "Add as Second Device", "Restore from Backup" -->
<string name="onboarding_alternative_logins">I Already Have a Profile</string>
<!-- This is a button and a title, allowing to log in to existing, classic email accounts, setting ports, passwords and so on -->
<!-- This is a button and a title, allowing to use existing, classic email, setting ports, passwords and so on -->
<string name="manual_account_setup_option">Use Classic Email as Relay</string>
<!-- Instant onboarding title (there is not more to do than to set name and avatar) -->
<string name="instant_onboarding_title">Your Profile</string>
@@ -636,7 +636,7 @@
<string name="welcome_chat_over_email">Secure Decentralized Chat</string>
<string name="scan_invitation_code">Scan Invitation Code</string>
<string name="login_title">Log In</string>
<string name="login_advanced_hint">This login is for advanced users:\n\n• Do not use an account you\'re using in another app.\n\n• Classic email server allow chats without end-to-end encryption marked by a mail icon.</string>
<string name="login_advanced_hint">This login is for advanced users:\n\n• Do not use an address you\'re using in another app.\n\n• Classic email server allow chats without end-to-end encryption marked by a mail icon.</string>
<string name="login_inbox">Inbox</string>
<string name="login_imap_login">IMAP Login Name</string>
<string name="login_imap_server">IMAP Server</string>