mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Merge pull request #4129 from deltachat/adb/issue-4116-p2
do accounts migration in background
This commit is contained in:
@@ -55,10 +55,13 @@ import chat.delta.rpc.RpcException;
|
||||
|
||||
public class ApplicationContext extends MultiDexApplication {
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
private static final Object initLock = new Object();
|
||||
private static volatile boolean isInitialized = false;
|
||||
|
||||
private static DcAccounts dcAccounts;
|
||||
private Rpc rpc;
|
||||
private DcContext dcContext;
|
||||
|
||||
public static DcAccounts dcAccounts;
|
||||
public Rpc rpc;
|
||||
public DcContext dcContext;
|
||||
public DcLocationManager dcLocationManager;
|
||||
public DcEventCenter eventCenter;
|
||||
public NotificationCenter notificationCenter;
|
||||
@@ -73,6 +76,57 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
@@ -88,67 +142,88 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
|
||||
System.loadLibrary("native-utils");
|
||||
|
||||
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(this);
|
||||
// Initialize DcAccounts in background to avoid ANR during SQL migrations
|
||||
Util.runOnBackground(() -> {
|
||||
synchronized (initLock) {
|
||||
try {
|
||||
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(this);
|
||||
|
||||
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.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);
|
||||
|
||||
// Mark as initialized before starting threads that depend on it
|
||||
isInitialized = true;
|
||||
initLock.notifyAll();
|
||||
Log.i(TAG, "DcAccounts initialization complete");
|
||||
|
||||
dcLocationManager = new DcLocationManager(this); // depends on dcContext
|
||||
|
||||
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(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
|
||||
|
||||
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.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(() -> {
|
||||
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(this);
|
||||
|
||||
dcAccounts.startIo();
|
||||
|
||||
new ForegroundDetector(ApplicationContext.getInstance(this));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@@ -158,7 +233,7 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
@Override
|
||||
public void onAvailable(@NonNull android.net.Network network) {
|
||||
Log.i("DeltaChat", "++++++++++++++++++ NetworkCallback.onAvailable() #" + debugOnAvailableCount++);
|
||||
dcAccounts.maybeNetwork();
|
||||
getDcAccounts().maybeNetwork();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -187,7 +262,6 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
initializeJobManager();
|
||||
InChatSounds.getInstance(this);
|
||||
|
||||
dcLocationManager = new DcLocationManager(this);
|
||||
DynamicTheme.setDefaultDayNightMode(this);
|
||||
|
||||
IntentFilter filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
|
||||
@@ -236,6 +310,8 @@ public class ApplicationContext extends MultiDexApplication {
|
||||
"WebxdcGarbageCollectionWorker",
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
webxdcGarbageCollectionRequest);
|
||||
|
||||
Log.i("DeltaChat", "+++++++++++ ApplicationContext.onCreate() finished ++++++++++");
|
||||
}
|
||||
|
||||
public JobManager getJobManager() {
|
||||
|
||||
@@ -955,8 +955,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
int accountId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, dcContext.getAccountId());
|
||||
if (accountId != dcContext.getAccountId()) {
|
||||
AccountManager.getInstance().switchAccount(context, accountId);
|
||||
dcContext = context.dcContext;
|
||||
fragment.dcContext = context.dcContext;
|
||||
fragment.dcContext = dcContext = context.getDcContext();
|
||||
initializeBackground();
|
||||
}
|
||||
chatId = getIntent().getIntExtra(CHAT_ID_EXTRA, -1);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -220,7 +220,7 @@ public class DcEventCenter {
|
||||
break;
|
||||
}
|
||||
|
||||
if (accountId != context.dcContext.getAccountId()) {
|
||||
if (accountId != context.getDcContext().getAccountId()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,15 +66,15 @@ public class DcHelper {
|
||||
public static final String CONFIG_STATS_ID = "stats_id";
|
||||
|
||||
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.getInstance(context).getDcAccounts();
|
||||
}
|
||||
|
||||
public static DcEventCenter getEventCenter(@NonNull Context context) {
|
||||
|
||||
@@ -426,7 +426,7 @@ public class NotificationCenter {
|
||||
public void notifyCall(int accId, int callId, String payload) {
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
DcContext dcContext = context.dcAccounts.getAccount(accId);
|
||||
DcContext dcContext = context.getDcAccounts().getAccount(accId);
|
||||
int chatId = dcContext.getMsg(callId).getChatId();
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
String name = dcChat.getName();
|
||||
@@ -478,7 +478,7 @@ public class NotificationCenter {
|
||||
|
||||
public void notifyMessage(int accountId, int chatId, int msgId) {
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
DcContext dcContext = context.dcAccounts.getAccount(accountId);
|
||||
DcContext dcContext = context.getDcAccounts().getAccount(accountId);
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
|
||||
DcMsg dcMsg = dcContext.getMsg(msgId);
|
||||
@@ -508,7 +508,7 @@ public class NotificationCenter {
|
||||
|
||||
public void notifyReaction(int accountId, int contactId, int msgId, String reaction) {
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
DcContext dcContext = context.dcAccounts.getAccount(accountId);
|
||||
DcContext dcContext = context.getDcAccounts().getAccount(accountId);
|
||||
DcMsg dcMsg = dcContext.getMsg(msgId);
|
||||
|
||||
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
|
||||
@@ -530,7 +530,7 @@ public class NotificationCenter {
|
||||
return; // showing "New Message" is wrong, just do nothing.
|
||||
}
|
||||
|
||||
DcContext dcContext = context.dcAccounts.getAccount(accountId);
|
||||
DcContext dcContext = context.getDcAccounts().getAccount(accountId);
|
||||
DcMsg dcMsg = dcContext.getMsg(msgId);
|
||||
DcMsg parentMsg;
|
||||
if(dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) {
|
||||
@@ -554,7 +554,7 @@ public class NotificationCenter {
|
||||
@WorkerThread
|
||||
private void maybeAddNotification(int accountId, DcChat dcChat, int msgId, String shortLine, String tickerLine, boolean playInChatSound, boolean isMention) {
|
||||
|
||||
DcContext dcContext = context.dcAccounts.getAccount(accountId);
|
||||
DcContext dcContext = context.getDcAccounts().getAccount(accountId);
|
||||
int chatId = dcChat.getId();
|
||||
ChatData chatData = new ChatData(accountId, chatId);
|
||||
isMention = isMention && dcContext.isMentionsEnabled();
|
||||
@@ -602,7 +602,7 @@ public class NotificationCenter {
|
||||
}
|
||||
|
||||
String accountTag = dcContext.getConfig(CONFIG_PRIVATE_TAG);
|
||||
if (accountTag.isEmpty() && context.dcAccounts.getAll().length > 1) {
|
||||
if (accountTag.isEmpty() && context.getDcAccounts().getAll().length > 1) {
|
||||
accountTag = dcContext.getName();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user