Files
arcanechat-android/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
T
2026-04-29 16:48:31 +02:00

436 lines
16 KiB
Java

package org.thoughtcrime.securesms;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.NotificationManagerCompat;
import androidx.multidex.MultiDexApplication;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import com.b44t.messenger.DcAccounts;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcEventChannel;
import com.b44t.messenger.DcEventEmitter;
import com.b44t.messenger.FFITransport;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.TimeUnit;
import org.thoughtcrime.securesms.calls.CallCoordinator;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.connect.FetchWorker;
import org.thoughtcrime.securesms.connect.ForegroundDetector;
import org.thoughtcrime.securesms.connect.KeepAliveService;
import org.thoughtcrime.securesms.connect.NetworkStateReceiver;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
import org.thoughtcrime.securesms.notifications.InChatSounds;
import org.thoughtcrime.securesms.notifications.NotificationCenter;
import org.thoughtcrime.securesms.util.AndroidSignalProtocolLogger;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.SignalProtocolLoggerProvider;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.webxdc.WebxdcGarbageCollectionWorker;
public class ApplicationContext extends MultiDexApplication {
private static final String TAG = "ApplicationContext";
private static final Object initLock = new Object();
private static volatile boolean isInitialized = false;
private static DcAccounts dcAccounts;
private Rpc rpc;
private DcContext dcContext;
private DcEventCenter eventCenter;
private NotificationCenter notificationCenter;
private JobManager jobManager;
private int debugOnAvailableCount;
private int debugOnBlockedStatusChangedCount;
private int debugOnCapabilitiesChangedCount;
private int debugOnLinkPropertiesChangedCount;
public static ApplicationContext getInstance(@NonNull Context context) {
return (ApplicationContext) context.getApplicationContext();
}
private static void ensureInitialized() {
synchronized (initLock) {
while (!isInitialized) {
try {
initLock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for initialization", e);
}
}
}
}
/**
* Get DcAccounts instance, waiting for initialization if necessary. This method is thread-safe
* and will block until initialization is complete.
*/
public static DcAccounts getDcAccounts() {
ensureInitialized();
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() {
ensureInitialized();
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() {
ensureInitialized();
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 dcContext) {
synchronized (initLock) {
this.dcContext = dcContext;
}
}
/**
* Get DcEventCenter instance, waiting for initialization if necessary. This method is thread-safe
* and will block until initialization is complete.
*/
public DcEventCenter getEventCenter() {
ensureInitialized();
return eventCenter;
}
/**
* Get NotificationCenter instance, waiting for initialization if necessary. This method is
* thread-safe and will block until initialization is complete.
*/
public NotificationCenter getNotificationCenter() {
ensureInitialized();
return notificationCenter;
}
@Override
public void onCreate() {
super.onCreate();
Thread.setDefaultUncaughtExceptionHandler(
(thread, throwable) -> {
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter, true));
String errorMsg =
"Android " + Build.VERSION.RELEASE + ":\n" + stringWriter.getBuffer().toString();
errorMsg += "\n" + LogViewFragment.grabLogcat();
String subject =
"ArcaneChat " + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR + " Crash Report";
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
intent.putExtra(android.content.Intent.EXTRA_TEXT, subject + "\n\n" + errorMsg);
intent.putExtra(Intent.EXTRA_EMAIL, "adb@merlinux.eu");
Intent chooser = Intent.createChooser(intent, subject);
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
startActivity(chooser);
try {
ApplicationContext.this.finalize();
} catch (Throwable e) {
}
});
// if (LeakCanary.isInAnalyzerProcess(this)) {
// // This process is dedicated to LeakCanary for heap analysis.
// // You should not init your app in this process.
// return;
// }
// LeakCanary.install(this);
Log.i("DeltaChat", "++++++++++++++++++ ApplicationContext.onCreate() ++++++++++++++++++");
System.loadLibrary("native-utils");
// Initialize DcAccounts in background to avoid ANR during SQL migrations
Util.runOnBackground(
() -> {
synchronized (initLock) {
try {
DcEventChannel eventChannel = new DcEventChannel();
DcEventEmitter emitter = eventChannel.getEventEmitter();
eventCenter = new DcEventCenter(this);
new Thread(
() -> {
Log.i(TAG, "Starting event loop");
while (true) {
DcEvent event = emitter.getNextEvent();
if (event == null) {
break;
}
if (isInitialized) {
eventCenter.handleEvent(event);
} else {
// not fully initialized, only handle logging events,
// ex. account migrations during DcAccounts initialization
eventCenter.handleLogging(event);
}
}
Log.i("DeltaChat", "shutting down event handler");
},
"eventThread")
.start();
dcAccounts =
new DcAccounts(
new File(getFilesDir(), "accounts").getAbsolutePath(), eventChannel);
Log.i(TAG, "DcAccounts created");
rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
Log.i(TAG, "Rpc created");
AccountManager.getInstance().migrateToDcAccounts(this, dcAccounts);
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(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-12-16: The setting was removed.
// Revert it to the default if it was changed in the past.
ac.setConfigInt("webxdc_realtime_enabled", 1);
// 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);
isInitialized = true;
initLock.notifyAll();
Log.i(TAG, "DcAccounts initialization complete");
// set translations before starting I/O to avoid sending untranslated MDNs (issue
// #2288)
DcHelper.setStockTranslations(this);
dcAccounts.startIo();
} catch (Exception e) {
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);
}
}
});
// October-2025 migration: delete deprecated "permanent channel" id
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.deleteNotificationChannel("dc_foreground_notification_ch");
// end October-2025 migration
new ForegroundDetector(ApplicationContext.getInstance(this));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ConnectivityManager connectivityManager =
(ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
connectivityManager.registerDefaultNetworkCallback(
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(@NonNull android.net.Network network) {
Log.i(
"DeltaChat",
"++++++++++++++++++ NetworkCallback.onAvailable() #" + debugOnAvailableCount++);
getDcAccounts().maybeNetwork();
}
@Override
public void onBlockedStatusChanged(
@NonNull android.net.Network network, boolean blocked) {
Log.i(
"DeltaChat",
"++++++++++++++++++ NetworkCallback.onBlockedStatusChanged() #"
+ debugOnBlockedStatusChangedCount++);
}
@Override
public void onCapabilitiesChanged(
@NonNull android.net.Network network, NetworkCapabilities networkCapabilities) {
// usually called after onAvailable(), so a maybeNetwork seems contraproductive
Log.i(
"DeltaChat",
"++++++++++++++++++ NetworkCallback.onCapabilitiesChanged() #"
+ debugOnCapabilitiesChangedCount++);
}
@Override
public void onLinkPropertiesChanged(
@NonNull android.net.Network network, LinkProperties linkProperties) {
Log.i(
"DeltaChat",
"++++++++++++++++++ NetworkCallback.onLinkPropertiesChanged() #"
+ debugOnLinkPropertiesChangedCount++);
}
});
} // no else: use old method for debugging
BroadcastReceiver networkStateReceiver = new NetworkStateReceiver();
registerReceiver(
networkStateReceiver,
new IntentFilter(android.net.ConnectivityManager.CONNECTIVITY_ACTION));
KeepAliveService.maybeStartSelf(this);
initializeLogging();
initializeJobManager();
InChatSounds.getInstance(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
EglUtils.getEglBase();
CallCoordinator.getInstance(this);
}
DynamicTheme.setDefaultDayNightMode(this);
IntentFilter filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Util.localeChanged();
DcHelper.setStockTranslations(context);
}
},
filter);
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
if (Prefs.isPushEnabled(this)) {
FcmReceiveService.register(this);
} else {
Log.i(TAG, "FCM disabled at build time");
// MAYBE TODO: i think the ApplicationContext is also created
// when the app is stated by FetchWorker timeouts.
// in this case, the normal threads shall not be started.
Constraints constraints =
new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
PeriodicWorkRequest fetchWorkRequest =
new PeriodicWorkRequest.Builder(
FetchWorker.class,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, // usually 15 minutes
TimeUnit.MILLISECONDS,
PeriodicWorkRequest
.MIN_PERIODIC_FLEX_MILLIS, // the start may be preferred by up to 5 minutes,
// so we run every 10-15 minutes
TimeUnit.MILLISECONDS)
.setConstraints(constraints)
.build();
WorkManager.getInstance(this)
.enqueueUniquePeriodicWork(
"FetchWorker", ExistingPeriodicWorkPolicy.KEEP, fetchWorkRequest);
}
PeriodicWorkRequest webxdcGarbageCollectionRequest =
new PeriodicWorkRequest.Builder(
WebxdcGarbageCollectionWorker.class,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS,
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS,
TimeUnit.MILLISECONDS)
.build();
WorkManager.getInstance(this)
.enqueueUniquePeriodicWork(
"WebxdcGarbageCollectionWorker",
ExistingPeriodicWorkPolicy.KEEP,
webxdcGarbageCollectionRequest);
Log.i("DeltaChat", "+++++++++++ ApplicationContext.onCreate() finished ++++++++++");
}
@Override
public void onTerminate() {
super.onTerminate();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
EglUtils.release();
}
}
public JobManager getJobManager() {
return jobManager;
}
private void initializeLogging() {
SignalProtocolLoggerProvider.setProvider(new AndroidSignalProtocolLogger());
}
private void initializeJobManager() {
this.jobManager = new JobManager(this, 5);
}
}