From 7744a42e1c2661e59bb1bc6b5a3796354780dc00 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:40:26 -0600 Subject: [PATCH] refactor(deps): inject CoroutineDispatchers (#4170) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/MeshServiceClient.kt | 16 ++++++----- .../geeksville/mesh/android/ServiceClient.kt | 8 +++--- .../repository/radio/RadioInterfaceService.kt | 8 ++---- .../mesh/repository/radio/TCPInterface.kt | 16 +++++++---- .../mesh/ui/contact/ContactsViewModel.kt | 8 ++---- .../DeviceHardwareLocalDataSource.kt | 18 +++++++----- .../FirmwareReleaseLocalDataSource.kt | 18 +++++++----- .../repository/DeviceHardwareRepository.kt | 8 +++--- .../data/repository/LocationRepository.kt | 8 +++--- core/database/build.gradle.kts | 1 + .../core/database/DatabaseManager.kt | 28 ++++++++++--------- core/network/build.gradle.kts | 3 +- .../network/DeviceHardwareRemoteDataSource.kt | 14 ++++++---- .../FirmwareReleaseRemoteDataSource.kt | 14 ++++++---- .../composeResources/values/strings.xml | 1 - .../org/meshtastic/core/ui/util/FormatAgo.kt | 22 +++++---------- 16 files changed, 102 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt index 94cd1a191..102980251 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.geeksville.mesh import android.content.Context @@ -31,6 +30,7 @@ import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.service.startService import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped +import kotlinx.coroutines.launch import org.meshtastic.core.service.IMeshService import org.meshtastic.core.service.ServiceRepository import javax.inject.Inject @@ -75,10 +75,12 @@ constructor( super.onStart(owner) Logger.d { "Lifecycle: ON_START" } - try { - bindMeshService() - } catch (ex: BindFailedException) { - Logger.e { "Bind of MeshService failed: ${ex.message}" } + owner.lifecycleScope.launch { + try { + bindMeshService() + } catch (ex: BindFailedException) { + Logger.e { "Bind of MeshService failed: ${ex.message}" } + } } } @@ -93,7 +95,7 @@ constructor( // endregion @Suppress("TooGenericExceptionCaught") - private fun bindMeshService() { + private suspend fun bindMeshService() { Logger.d { "Binding to mesh service!" } try { MeshService.startService(context) diff --git a/app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt b/app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt index 3216f7daa..65c7373d4 100644 --- a/app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt +++ b/app/src/main/java/com/geeksville/mesh/android/ServiceClient.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.geeksville.mesh.android import android.content.ComponentName @@ -25,6 +24,7 @@ import android.os.IBinder import android.os.IInterface import co.touchlab.kermit.Logger import com.geeksville.mesh.util.exceptionReporter +import kotlinx.coroutines.delay import java.io.Closeable import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -64,7 +64,7 @@ open class ServiceClient(private val stubFactory: (IBinder) -> T } } - fun connect(c: Context, intent: Intent, flags: Int) { + suspend fun connect(c: Context, intent: Intent, flags: Int) { context = c if (isClosed) { isClosed = false @@ -73,7 +73,7 @@ open class ServiceClient(private val stubFactory: (IBinder) -> T // Try // a short sleep to see if that helps Logger.e { "Needed to use the second bind attempt hack" } - Thread.sleep(500) // was 200ms, but received an autobug from a Galaxy Note4, android 6.0.1 + delay(500) // was 200ms, but received an autobug from a Galaxy Note4, android 6.0.1 if (!c.bindService(intent, connection, flags)) { throw BindFailedException() } diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt index b34756481..2d86d46f7 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.geeksville.mesh.repository.radio import android.app.Application @@ -31,7 +30,6 @@ import com.geeksville.mesh.repository.network.NetworkRepository import com.geeksville.mesh.util.ignoreException import com.geeksville.mesh.util.toRemoteExceptions import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.BufferOverflow @@ -99,7 +97,7 @@ constructor( val mockInterfaceAddress: String by lazy { toInterfaceAddress(InterfaceId.MOCK, "") } /** We recreate this scope each time we stop an interface */ - var serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + var serviceScope = CoroutineScope(dispatchers.io + SupervisorJob()) private var radioIf: IRadioInterface = NopInterface("") @@ -292,7 +290,7 @@ constructor( // cancel any old jobs and get ready for the new ones serviceScope.cancel("stopping interface") - serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + serviceScope = CoroutineScope(dispatchers.io + SupervisorJob()) if (logSends) { sentPacketsLog.close() diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt index 68333275e..3b587705b 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.geeksville.mesh.repository.radio import co.touchlab.kermit.Logger @@ -23,9 +22,9 @@ import com.geeksville.mesh.repository.network.NetworkRepository import com.geeksville.mesh.util.Exceptions import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import org.meshtastic.core.di.CoroutineDispatchers import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.IOException @@ -34,8 +33,13 @@ import java.net.InetAddress import java.net.Socket import java.net.SocketTimeoutException -class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @Assisted private val address: String) : - StreamInterface(service) { +class TCPInterface +@AssistedInject +constructor( + service: RadioInterfaceService, + private val dispatchers: CoroutineDispatchers, + @Assisted private val address: String, +) : StreamInterface(service) { companion object { const val MAX_RETRIES_ALLOWED = Int.MAX_VALUE @@ -143,7 +147,7 @@ class TCPInterface @AssistedInject constructor(service: RadioInterfaceService, @ } // Create a socket to make the connection with the server - private suspend fun startConnect() = withContext(Dispatchers.IO) { + private suspend fun startConnect() = withContext(dispatchers.io) { val attemptStart = System.currentTimeMillis() Logger.i { "[$address] TCP connection attempt starting..." } diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt index 7db9d03db..1806b6dde 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package com.geeksville.mesh.ui.contact import androidx.lifecycle.ViewModel @@ -30,7 +29,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.jetbrains.compose.resources.getString import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.PacketRepository @@ -163,8 +161,8 @@ constructor( longName = longName, lastMessageTime = getShortDate(data.time), lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}", - unreadCount = runBlocking(Dispatchers.IO) { packetRepository.getUnreadCount(contactKey) }, - messageCount = runBlocking(Dispatchers.IO) { packetRepository.getMessageCount(contactKey) }, + unreadCount = packetRepository.getUnreadCount(contactKey), + messageCount = packetRepository.getMessageCount(contactKey), isMuted = settings[contactKey]?.isMuted == true, isUnmessageable = user.isUnmessagable, nodeColors = diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt index 00b36aba0..13d61526a 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,27 +14,31 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.data.datasource import dagger.Lazy -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.meshtastic.core.database.dao.DeviceHardwareDao import org.meshtastic.core.database.entity.DeviceHardwareEntity import org.meshtastic.core.database.entity.asEntity +import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.NetworkDeviceHardware import javax.inject.Inject -class DeviceHardwareLocalDataSource @Inject constructor(private val deviceHardwareDaoLazy: Lazy) { +class DeviceHardwareLocalDataSource +@Inject +constructor( + private val deviceHardwareDaoLazy: Lazy, + private val dispatchers: CoroutineDispatchers, +) { private val deviceHardwareDao by lazy { deviceHardwareDaoLazy.get() } - suspend fun insertAllDeviceHardware(deviceHardware: List) = withContext(Dispatchers.IO) { + suspend fun insertAllDeviceHardware(deviceHardware: List) = withContext(dispatchers.io) { deviceHardware.forEach { deviceHardware -> deviceHardwareDao.insert(deviceHardware.asEntity()) } } - suspend fun deleteAllDeviceHardware() = withContext(Dispatchers.IO) { deviceHardwareDao.deleteAll() } + suspend fun deleteAllDeviceHardware() = withContext(dispatchers.io) { deviceHardwareDao.deleteAll() } suspend fun getByHwModel(hwModel: Int): DeviceHardwareEntity? = - withContext(Dispatchers.IO) { deviceHardwareDao.getByHwModel(hwModel) } + withContext(dispatchers.io) { deviceHardwareDao.getByHwModel(hwModel) } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt index 5fbcca789..dff3b0171 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,36 +14,40 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.data.datasource import dagger.Lazy -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.meshtastic.core.database.dao.FirmwareReleaseDao import org.meshtastic.core.database.entity.FirmwareReleaseEntity import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.database.entity.asEntity +import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.NetworkFirmwareRelease import javax.inject.Inject -class FirmwareReleaseLocalDataSource @Inject constructor(private val firmwareReleaseDaoLazy: Lazy) { +class FirmwareReleaseLocalDataSource +@Inject +constructor( + private val firmwareReleaseDaoLazy: Lazy, + private val dispatchers: CoroutineDispatchers, +) { private val firmwareReleaseDao by lazy { firmwareReleaseDaoLazy.get() } suspend fun insertFirmwareReleases( firmwareReleases: List, releaseType: FirmwareReleaseType, - ) = withContext(Dispatchers.IO) { + ) = withContext(dispatchers.io) { firmwareReleases.forEach { firmwareRelease -> firmwareReleaseDao.insert(firmwareRelease.asEntity(releaseType)) } } - suspend fun deleteAllFirmwareReleases() = withContext(Dispatchers.IO) { firmwareReleaseDao.deleteAll() } + suspend fun deleteAllFirmwareReleases() = withContext(dispatchers.io) { firmwareReleaseDao.deleteAll() } suspend fun getLatestRelease(releaseType: FirmwareReleaseType): FirmwareReleaseEntity? = - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val releases = firmwareReleaseDao.getReleasesByType(releaseType) if (releases.isEmpty()) { return@withContext null diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt index f527ae6c8..30cc4c31a 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,17 +14,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.data.repository import co.touchlab.kermit.Logger -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.meshtastic.core.data.datasource.BootloaderOtaQuirksJsonDataSource import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource import org.meshtastic.core.data.datasource.DeviceHardwareLocalDataSource import org.meshtastic.core.database.entity.DeviceHardwareEntity import org.meshtastic.core.database.entity.asExternalModel +import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.BootloaderOtaQuirk import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.network.DeviceHardwareRemoteDataSource @@ -41,6 +40,7 @@ constructor( private val localDataSource: DeviceHardwareLocalDataSource, private val jsonDataSource: DeviceHardwareJsonDataSource, private val bootloaderOtaQuirksJsonDataSource: BootloaderOtaQuirksJsonDataSource, + private val dispatchers: CoroutineDispatchers, ) { /** @@ -58,7 +58,7 @@ constructor( */ @Suppress("LongMethod") suspend fun getDeviceHardwareByModel(hwModel: Int, forceRefresh: Boolean = false): Result = - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { Logger.d { "DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=$hwModel, forceRefresh=$forceRefresh)" } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt index 35d087bf3..a1b7b8a5a 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/LocationRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.data.repository import android.Manifest.permission.ACCESS_COARSE_LOCATION @@ -28,13 +27,13 @@ import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationRequestCompat import androidx.core.location.altitude.AltitudeConverterCompat import co.touchlab.kermit.Logger -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import org.meshtastic.core.analytics.platform.PlatformAnalytics +import org.meshtastic.core.di.CoroutineDispatchers import javax.inject.Inject import javax.inject.Singleton @@ -45,6 +44,7 @@ constructor( private val context: Application, private val locationManager: dagger.Lazy, private val analytics: PlatformAnalytics, + private val dispatchers: CoroutineDispatchers, ) { /** Status of whether the app is actively subscribed to location changes. */ @@ -97,7 +97,7 @@ constructor( this@requestLocationUpdates, provider, locationRequest, - Dispatchers.IO.asExecutor(), + dispatchers.io.asExecutor(), locationListener, ) } diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index ce58ce582..00c914148 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -49,6 +49,7 @@ configure { } dependencies { + implementation(projects.core.di) implementation(projects.core.model) implementation(projects.core.proto) implementation(projects.core.strings) 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 4c8979467..27f1a26fb 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 @@ -22,7 +22,6 @@ import android.content.SharedPreferences import androidx.room.Room import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -33,6 +32,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import org.meshtastic.core.di.CoroutineDispatchers import java.io.File import java.security.MessageDigest import javax.inject.Inject @@ -40,9 +40,9 @@ import javax.inject.Singleton /** Manages per-device Room database instances for node data, with LRU eviction. */ @Singleton -class DatabaseManager @Inject constructor(private val app: Application) { +class DatabaseManager @Inject constructor(private val app: Application, private val dispatchers: CoroutineDispatchers) { val prefs: SharedPreferences = app.getSharedPreferences("db-manager-prefs", Context.MODE_PRIVATE) - private val managerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val managerScope = CoroutineScope(SupervisorJob() + dispatchers.default) private val mutex = Mutex() @@ -72,25 +72,27 @@ class DatabaseManager @Inject constructor(private val app: Application) { private val dbCache = mutableMapOf() // key = dbName /** Initialize the active database for [address]. */ - suspend fun init(address: String?) = switchActiveDatabase(address) + suspend fun init(address: String?) { + switchActiveDatabase(address) + } /** Switch active database to the one associated with [address]. Serialized via mutex. */ suspend fun switchActiveDatabase(address: String?) = mutex.withLock { val dbName = buildDbName(address) - // Remember the previously active DB name (if any) so we can record its last-used time as well. + // Remember the previously active DB name (any) so we can record its last-used time as well. val previousDbName = _currentDb.value?.openHelper?.databaseName // Fast path: no-op if already on this address if (_currentAddress.value == address && _currentDb.value != null) { markLastUsed(dbName) - return + return@withLock } // Build/open Room DB off the main thread val db = dbCache[dbName] - ?: withContext(Dispatchers.IO) { buildRoomDb(app, dbName) }.also { dbCache[dbName] = it } + ?: withContext(dispatchers.io) { buildRoomDb(app, dbName) }.also { dbCache[dbName] = it } _currentDb.value = db _currentAddress.value = address @@ -100,10 +102,10 @@ class DatabaseManager @Inject constructor(private val app: Application) { previousDbName?.let { markLastUsed(it) } // Defer LRU eviction so switch is not blocked by filesystem work - managerScope.launch(Dispatchers.IO) { enforceCacheLimit(activeDbName = dbName) } + managerScope.launch(dispatchers.io) { enforceCacheLimit(activeDbName = dbName) } // One-time cleanup: remove legacy DB if present and not active - managerScope.launch(Dispatchers.IO) { cleanupLegacyDbIfNeeded(activeDbName = dbName) } + managerScope.launch(dispatchers.io) { cleanupLegacyDbIfNeeded(activeDbName = dbName) } Logger.i { "Switched active DB to ${anonymizeDbName(dbName)} for address ${anonymizeAddress(address)}" } } @@ -144,7 +146,7 @@ class DatabaseManager @Inject constructor(private val app: Application) { anonymizeDbName(it) }}" } - if (deviceDbs.size <= limit) return + if (deviceDbs.size <= limit) return@withLock val usageSnapshot = deviceDbs.associateWith { lastUsed(it) } Logger.d { "LRU lastUsed(ms): ${usageSnapshot.entries.joinToString(", ") { (name, ts) -> @@ -173,16 +175,16 @@ class DatabaseManager @Inject constructor(private val app: Application) { _cacheLimit.value = clamped // Enforce asynchronously with current active DB protected val active = _currentDb.value?.openHelper?.databaseName ?: defaultDbName() - managerScope.launch(Dispatchers.IO) { enforceCacheLimit(activeDbName = active) } + managerScope.launch(dispatchers.io) { enforceCacheLimit(activeDbName = active) } } private suspend fun cleanupLegacyDbIfNeeded(activeDbName: String) = mutex.withLock { - if (prefs.getBoolean(DatabaseConstants.LEGACY_DB_CLEANED_KEY, false)) return + if (prefs.getBoolean(DatabaseConstants.LEGACY_DB_CLEANED_KEY, false)) return@withLock val legacy = DatabaseConstants.LEGACY_DB_NAME if (legacy == activeDbName) { // Never delete the active DB; mark as cleaned to avoid repeated checks prefs.edit().putBoolean(DatabaseConstants.LEGACY_DB_CLEANED_KEY, true).apply() - return + return@withLock } val legacyFile = getDbFile(app, legacy) if (legacyFile != null) { diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index d1c4414ad..54a7fd9fb 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -48,6 +48,7 @@ configure { } dependencies { + implementation(projects.core.di) implementation(projects.core.model) implementation(libs.coil.network.core) diff --git a/core/network/src/main/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt b/core/network/src/main/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt index 1b436fc3f..826de8c12 100644 --- a/core/network/src/main/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt +++ b/core/network/src/main/kotlin/org/meshtastic/core/network/DeviceHardwareRemoteDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,16 +14,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.network -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.NetworkDeviceHardware import org.meshtastic.core.network.service.ApiService import javax.inject.Inject -class DeviceHardwareRemoteDataSource @Inject constructor(private val apiService: ApiService) { +class DeviceHardwareRemoteDataSource +@Inject +constructor( + private val apiService: ApiService, + private val dispatchers: CoroutineDispatchers, +) { suspend fun getAllDeviceHardware(): List = - withContext(Dispatchers.IO) { apiService.getDeviceHardware() } + withContext(dispatchers.io) { apiService.getDeviceHardware() } } diff --git a/core/network/src/main/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt b/core/network/src/main/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt index fa25da247..056cdce43 100644 --- a/core/network/src/main/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt +++ b/core/network/src/main/kotlin/org/meshtastic/core/network/FirmwareReleaseRemoteDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * 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 @@ -14,16 +14,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.network -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.NetworkFirmwareReleases import org.meshtastic.core.network.service.ApiService import javax.inject.Inject -class FirmwareReleaseRemoteDataSource @Inject constructor(private val apiService: ApiService) { +class FirmwareReleaseRemoteDataSource +@Inject +constructor( + private val apiService: ApiService, + private val dispatchers: CoroutineDispatchers, +) { suspend fun getFirmwareReleases(): NetworkFirmwareReleases = - withContext(Dispatchers.IO) { apiService.getFirmwareReleases() } + withContext(dispatchers.io) { apiService.getFirmwareReleases() } } diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index af3f2b548..28d543fca 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -1086,5 +1086,4 @@ Estimated area: \u00b1%1$s (\u00b1%2$s) Estimated area: unknown accuracy Mark as read - now diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt index c76d09bbf..248e0a2e2 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt @@ -17,24 +17,16 @@ package org.meshtastic.core.ui.util import android.text.format.DateUtils -import com.meshtastic.core.strings.getString -import org.meshtastic.core.strings.Res -import org.meshtastic.core.strings.now @Suppress("MagicNumber") fun formatAgo(lastSeenUnix: Int, currentTimeMillis: Long = System.currentTimeMillis()): String { val timeInMillis = lastSeenUnix * 1000L - val diff = currentTimeMillis - timeInMillis - return if (diff < 60_000L) { - getString(Res.string.now) - } else { - DateUtils.getRelativeTimeSpanString( - timeInMillis, - currentTimeMillis, - DateUtils.MINUTE_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE, - ) - .toString() - } + return DateUtils.getRelativeTimeSpanString( + timeInMillis, + currentTimeMillis, + DateUtils.SECOND_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ) + .toString() }