diff --git a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt index 85e8e2d6f..a737bc25b 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt @@ -31,6 +31,7 @@ import dagger.hilt.android.HiltAndroidApp import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.prefs.mesh.MeshPrefs @@ -47,10 +48,12 @@ import kotlin.time.toJavaDuration * user preferences. */ @HiltAndroidApp -class MeshUtilApplication : +open class MeshUtilApplication : Application(), Configuration.Provider { @Inject lateinit var workerFactory: HiltWorkerFactory + + private val applicationScope = CoroutineScope(Dispatchers.Default) override fun onCreate() { super.onCreate() @@ -61,11 +64,19 @@ class MeshUtilApplication : // Initialize DatabaseManager asynchronously with current device address so DAO consumers have an active DB val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java) - CoroutineScope(Dispatchers.Default).launch { + applicationScope.launch { entryPoint.databaseManager().init(entryPoint.meshPrefs().deviceAddress) } } + override fun onTerminate() { + // Shutdown managers (useful for Robolectric tests) + val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java) + entryPoint.databaseManager().close() + applicationScope.cancel() + super.onTerminate() + } + private fun scheduleMeshLogCleanup() { val cleanupRequest = PeriodicWorkRequestBuilder(repeatInterval = 1.hours.toJavaDuration()).build() diff --git a/app/src/test/java/com/geeksville/mesh/MeshTestApplication.kt b/app/src/test/java/com/geeksville/mesh/MeshTestApplication.kt new file mode 100644 index 000000000..c68b2abd5 --- /dev/null +++ b/app/src/test/java/com/geeksville/mesh/MeshTestApplication.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.geeksville.mesh + +import androidx.work.Configuration +import dagger.hilt.android.EntryPointAccessors + +/** + * A lightweight application class for Robolectric tests. + * + * It prevents heavy background initialization (WorkManager, DatabaseManager) by default + * to avoid resource leaks and flaky native SQLite issues on the JVM. + */ +class MeshTestApplication : MeshUtilApplication() { + + override fun onCreate() { + // Only run real onCreate logic if a test explicitly asks for it + if (shouldInitialize) { + super.onCreate() + } + } + + override fun onTerminate() { + if (shouldInitialize) { + val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java) + entryPoint.databaseManager().close() + } + super.onTerminate() + } + + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setMinimumLoggingLevel(android.util.Log.DEBUG) + .build() + + companion object { + /** Set to true in a test @Before block if you need real DB/WorkManager init. */ + var shouldInitialize = false + } +} diff --git a/app/src/test/resources/robolectric.properties b/app/src/test/resources/robolectric.properties new file mode 100644 index 000000000..24613e7b1 --- /dev/null +++ b/app/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +application = com.geeksville.mesh.MeshTestApplication diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt index b04887f59..7754211bb 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -24,6 +24,7 @@ import androidx.room.RoomDatabase import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -209,6 +210,14 @@ class DatabaseManager @Inject constructor(private val app: Application, private } prefs.edit().putBoolean(DatabaseConstants.LEGACY_DB_CLEANED_KEY, true).apply() } + + /** Closes all open databases and cancels background work. */ + fun close() { + managerScope.cancel() + dbCache.values.forEach { it.close() } + dbCache.clear() + _currentDb.value = null + } } object DatabaseConstants {