Refact devices apps (#135)

* refact: [CORE] device and apps

* refact: [CORE] device and apps

* refact: [CORE] device and apps

* refact: [CORE] device and apps

* refact: [CORE] device and apps

* added db files

---------

Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
Florent CHAMPIGNY 2025-08-21 22:52:15 +02:00 committed by GitHub
parent b0c8c2948b
commit 262ccfd19f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 6840 additions and 345 deletions

View file

@ -25,6 +25,7 @@ object Protocol {
object Method {
const val RegisterDevice = "registerDevice"
const val AppIcon = "appIcon"
}
}
@ -117,6 +118,14 @@ object Protocol {
}
}
object Device {
const val Plugin = "device"
object Method {
const val GetAppIcon = "getAppIcon"
}
}
object Files {
const val Plugin = "files"

View file

@ -1,26 +1,19 @@
package io.github.openflocon.domain.analytics.usecase
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceAppUseCase
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdUseCase
import io.github.openflocon.domain.analytics.repository.AnalyticsRepository
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase
class ResetCurrentDeviceSelectedAnalyticsUseCase(
private val analyticsRepository: AnalyticsRepository,
private val getCurrentDeviceIdUseCase: GetCurrentDeviceIdUseCase,
private val getCurrentPackageNameUseCase: GetCurrentDeviceAppUseCase,
private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase,
private val getCurrentDeviceSelectedAnalyticsUseCase: GetCurrentDeviceSelectedAnalyticsUseCase,
) {
suspend operator fun invoke() {
val deviceId = getCurrentDeviceIdUseCase() ?: return
val packageName = getCurrentPackageNameUseCase()?.packageName ?: return
val deviceAndApp = getCurrentDeviceIdAndPackageNameUseCase() ?: return
val analyticsId = getCurrentDeviceSelectedAnalyticsUseCase() ?: return
analyticsRepository.deleteAnalytics(
deviceIdAndPackageName = DeviceIdAndPackageNameDomainModel(
deviceId = deviceId,
packageName = packageName,
),
deviceIdAndPackageName = deviceAndApp,
analyticsId = analyticsId,
)
}

View file

@ -8,7 +8,7 @@ class SelectCurrentDeviceDatabaseUseCase(
private val databaseRepository: DatabaseRepository,
private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase,
) {
operator fun invoke(databaseId: DeviceDataBaseId) {
suspend operator fun invoke(databaseId: DeviceDataBaseId) {
val current = getCurrentDeviceIdAndPackageNameUseCase() ?: return
databaseRepository.selectDeviceDatabase(

View file

@ -1,33 +1,29 @@
package io.github.openflocon.domain.device
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceAppUseCase
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdUseCase
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceUseCase
import io.github.openflocon.domain.device.usecase.HandleDeviceUseCase
import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceAppUseCase
import io.github.openflocon.domain.device.usecase.HandleDeviceAndAppUseCase
import io.github.openflocon.domain.device.usecase.HandleNewAppUseCase
import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceAppsUseCase
import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceIdAndPackageNameUseCase
import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceIdUseCase
import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceUseCase
import io.github.openflocon.domain.device.usecase.ObserveDevicesUseCase
import io.github.openflocon.domain.device.usecase.SelectDeviceAppUseCase
import io.github.openflocon.domain.device.usecase.SelectDeviceUseCase
import io.github.openflocon.domain.messages.usecase.HandleNewDeviceUseCase
import io.github.openflocon.domain.device.usecase.HandleNewDeviceUseCase
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
internal val deviceModule = module {
factoryOf(::GetCurrentDeviceAppUseCase)
factoryOf(::GetCurrentDeviceIdAndPackageNameUseCase)
factoryOf(::GetCurrentDeviceIdUseCase)
factoryOf(::GetCurrentDeviceUseCase)
factoryOf(::HandleDeviceUseCase)
factoryOf(::HandleDeviceAndAppUseCase)
factoryOf(::HandleNewDeviceUseCase)
factoryOf(::ObserveCurrentDeviceIdUseCase)
factoryOf(::ObserveCurrentDeviceAppUseCase)
factoryOf(::ObserveCurrentDeviceAppsUseCase)
factoryOf(::ObserveCurrentDeviceIdAndPackageNameUseCase)
factoryOf(::ObserveCurrentDeviceUseCase)
factoryOf(::ObserveDevicesUseCase)
factoryOf(::SelectDeviceAppUseCase)
factoryOf(::SelectDeviceUseCase)
factoryOf(::HandleNewAppUseCase)
}

View file

@ -3,4 +3,5 @@ package io.github.openflocon.domain.device.models
data class DeviceAppDomainModel(
val name: String,
val packageName: String,
val iconEncoded: String?,
)

View file

@ -3,5 +3,4 @@ package io.github.openflocon.domain.device.models
data class DeviceDomainModel(
val deviceId: DeviceId,
val deviceName: String,
val apps: List<DeviceAppDomainModel>,
)

View file

@ -0,0 +1,6 @@
package io.github.openflocon.domain.device.models
data class DeviceWithAppDomainModel(
val device: DeviceDomainModel,
val app: DeviceAppDomainModel,
)

View file

@ -0,0 +1,6 @@
package io.github.openflocon.domain.device.models
data class DeviceWithAppsDomainModel(
val device: DeviceDomainModel,
val apps: List<DeviceAppDomainModel>,
)

View file

@ -2,5 +2,7 @@ package io.github.openflocon.domain.device.models
data class HandleDeviceResultDomainModel(
val deviceId: String,
val justConnectedForThisSession: Boolean,
val isNewDevice: Boolean,
val isNewApp: Boolean,
)

View file

@ -0,0 +1,6 @@
package io.github.openflocon.domain.device.models
data class RegisterDeviceWithAppDomainModel(
val device: DeviceDomainModel,
val app: DeviceAppDomainModel,
)

View file

@ -2,27 +2,38 @@ package io.github.openflocon.domain.device.repository
import io.github.openflocon.domain.device.models.DeviceAppDomainModel
import io.github.openflocon.domain.device.models.DeviceDomainModel
import io.github.openflocon.domain.device.models.DeviceId
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.device.models.DeviceWithAppsDomainModel
import io.github.openflocon.domain.device.models.DeviceWithAppDomainModel
import io.github.openflocon.domain.device.models.HandleDeviceResultDomainModel
import io.github.openflocon.domain.device.models.RegisterDeviceWithAppDomainModel
import kotlinx.coroutines.flow.Flow
interface DevicesRepository {
val devices: Flow<List<DeviceDomainModel>>
val currentDeviceId: Flow<DeviceId?>
suspend fun getCurrentDeviceId(): DeviceId?
// returns new if new device
suspend fun register(device: DeviceDomainModel) : Boolean
suspend fun register(registerDeviceWithApp: RegisterDeviceWithAppDomainModel) : HandleDeviceResultDomainModel
suspend fun getCurrentDevice(): DeviceDomainModel?
suspend fun selectDevice(deviceId: DeviceId)
suspend fun unregister(device: DeviceDomainModel)
// region apps
fun observeDeviceApps(deviceId: DeviceId): Flow<List<DeviceAppDomainModel>>
fun observeDeviceSelectedApp(deviceId: DeviceId): Flow<DeviceAppDomainModel?>
suspend fun getDeviceSelectedApp(deviceId: DeviceId): DeviceAppDomainModel?
suspend fun getDeviceAppByPackage(deviceId: DeviceId, appPackageName: String) : DeviceAppDomainModel?
suspend fun selectApp(deviceId: DeviceId, app: DeviceAppDomainModel)
// endregion
// region app icon
suspend fun saveAppIcon(deviceId: DeviceId, appPackageName: String, iconEncoded: String)
suspend fun hasAppIcon(deviceId: DeviceId, appPackageName: String) : Boolean
suspend fun askForDeviceAppIcon(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel)
// endregion
suspend fun clear()
val currentDevice: Flow<DeviceDomainModel?>
val currentDeviceApp: Flow<DeviceAppDomainModel?>
fun getCurrentDevice(): DeviceDomainModel?
fun getCurrentDeviceApp(): DeviceAppDomainModel?
suspend fun selectDevice(device: DeviceDomainModel)
suspend fun selectApp(app: DeviceAppDomainModel)
}

View file

@ -1,10 +0,0 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.repository.DevicesRepository
import io.github.openflocon.domain.device.models.DeviceAppDomainModel
class GetCurrentDeviceAppUseCase(
private val devicesRepository: DevicesRepository,
) {
operator fun invoke(): DeviceAppDomainModel? = devicesRepository.getCurrentDeviceApp()
}

View file

@ -1,14 +1,15 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.device.repository.DevicesRepository
class GetCurrentDeviceIdAndPackageNameUseCase(
private val getCurrentDeviceUseCase: GetCurrentDeviceUseCase,
private val getCurrentDeviceAppUseCase: GetCurrentDeviceAppUseCase,
private val getCurrentDeviceIdUseCase: GetCurrentDeviceIdUseCase,
private val devicesRepository: DevicesRepository,
) {
operator fun invoke(): DeviceIdAndPackageNameDomainModel? {
val deviceId = getCurrentDeviceUseCase()?.deviceId ?: return null
val packageName = getCurrentDeviceAppUseCase()?.packageName ?: return null
suspend operator fun invoke(): DeviceIdAndPackageNameDomainModel? {
val deviceId = getCurrentDeviceIdUseCase() ?: return null
val packageName = devicesRepository.getDeviceSelectedApp(deviceId)?.packageName ?: return null
return DeviceIdAndPackageNameDomainModel(
deviceId = deviceId,

View file

@ -1,9 +1,10 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.models.DeviceId
import io.github.openflocon.domain.device.repository.DevicesRepository
class GetCurrentDeviceIdUseCase(
private val getCurrentDeviceUseCase: GetCurrentDeviceUseCase,
private val devicesRepository: DevicesRepository,
) {
operator fun invoke(): DeviceId? = getCurrentDeviceUseCase()?.deviceId
suspend operator fun invoke(): DeviceId? = devicesRepository.getCurrentDeviceId()
}

View file

@ -1,10 +0,0 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.repository.DevicesRepository
import io.github.openflocon.domain.device.models.DeviceDomainModel
class GetCurrentDeviceUseCase(
private val devicesRepository: DevicesRepository,
) {
operator fun invoke(): DeviceDomainModel? = devicesRepository.getCurrentDevice()
}

View file

@ -0,0 +1,14 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.repository.DevicesRepository
import io.github.openflocon.domain.device.models.HandleDeviceResultDomainModel
import io.github.openflocon.domain.device.models.RegisterDeviceWithAppDomainModel
class HandleDeviceAndAppUseCase(
private val devicesRepository: DevicesRepository,
) {
suspend operator fun invoke(device: RegisterDeviceWithAppDomainModel): HandleDeviceResultDomainModel {
return devicesRepository.register(device)
}
}

View file

@ -1,18 +0,0 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.repository.DevicesRepository
import io.github.openflocon.domain.device.models.DeviceDomainModel
import io.github.openflocon.domain.device.models.HandleDeviceResultDomainModel
class HandleDeviceUseCase(
private val devicesRepository: DevicesRepository,
) {
suspend operator fun invoke(device: DeviceDomainModel): HandleDeviceResultDomainModel {
val isNewDevice = devicesRepository.register(device)
return HandleDeviceResultDomainModel(
deviceId = device.deviceId,
isNewDevice = isNewDevice,
)
}
}

View file

@ -0,0 +1,17 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.device.repository.DevicesRepository
class HandleNewAppUseCase(
private val devicesRepository: DevicesRepository,
) {
suspend operator fun invoke(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel) {
if(!devicesRepository.hasAppIcon(
deviceId = deviceIdAndPackageName.deviceId,
appPackageName = deviceIdAndPackageName.packageName
)) {
devicesRepository.askForDeviceAppIcon(deviceIdAndPackageName)
}
}
}

View file

@ -1,4 +1,4 @@
package io.github.openflocon.domain.messages.usecase
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.adb.repository.AdbRepository
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel

View file

@ -3,9 +3,18 @@ package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.repository.DevicesRepository
import io.github.openflocon.domain.device.models.DeviceAppDomainModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
class ObserveCurrentDeviceAppUseCase(
class ObserveCurrentDeviceAppsUseCase(
private val devicesRepository: DevicesRepository,
private val observeCurrentDeviceIdUseCase: ObserveCurrentDeviceIdUseCase,
) {
operator fun invoke(): Flow<DeviceAppDomainModel?> = devicesRepository.currentDeviceApp
operator fun invoke(): Flow<List<DeviceAppDomainModel>> = observeCurrentDeviceIdUseCase().flatMapLatest {
if (it == null) {
flowOf(emptyList())
} else {
devicesRepository.observeDeviceApps(it)
}
}
}

View file

@ -1,23 +1,31 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.repository.DevicesRepository
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.device.repository.DevicesRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
class ObserveCurrentDeviceIdAndPackageNameUseCase(
private val observeCurrentDeviceIdUseCase: ObserveCurrentDeviceIdUseCase,
private val devicesRepository: DevicesRepository,
) {
operator fun invoke(): Flow<DeviceIdAndPackageNameDomainModel?> = combine(
devicesRepository.currentDevice,
devicesRepository.currentDeviceApp,
) { device, app ->
if (device != null && app != null) {
DeviceIdAndPackageNameDomainModel(deviceId = device.deviceId, packageName = app.packageName)
} else {
null
}
}
.distinctUntilChanged()
operator fun invoke(): Flow<DeviceIdAndPackageNameDomainModel?> =
observeCurrentDeviceIdUseCase().flatMapLatest { deviceId ->
if (deviceId == null) {
flowOf(null)
} else {
devicesRepository.observeDeviceSelectedApp(deviceId)
.map { app ->
app?.let {
DeviceIdAndPackageNameDomainModel(
deviceId = deviceId,
packageName = app.packageName,
)
}
}
}
}.distinctUntilChanged()
}

View file

@ -9,5 +9,5 @@ import kotlinx.coroutines.flow.map
class ObserveCurrentDeviceIdUseCase(
private val devicesRepository: DevicesRepository,
) {
operator fun invoke(): Flow<DeviceId?> = devicesRepository.currentDevice.map { it?.deviceId }.distinctUntilChanged()
operator fun invoke(): Flow<DeviceId?> = devicesRepository.currentDeviceId
}

View file

@ -1,11 +0,0 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.repository.DevicesRepository
import io.github.openflocon.domain.device.models.DeviceDomainModel
import kotlinx.coroutines.flow.Flow
class ObserveCurrentDeviceUseCase(
private val devicesRepository: DevicesRepository,
) {
operator fun invoke(): Flow<DeviceDomainModel?> = devicesRepository.currentDevice
}

View file

@ -1,9 +1,11 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.models.DeviceDomainModel
import io.github.openflocon.domain.device.repository.DevicesRepository
import kotlinx.coroutines.flow.Flow
class ObserveDevicesUseCase(
private val devicesRepository: DevicesRepository,
) {
operator fun invoke() = devicesRepository.devices
operator fun invoke(): Flow<List<DeviceDomainModel>> = devicesRepository.devices
}

View file

@ -5,14 +5,13 @@ import kotlinx.coroutines.flow.firstOrNull
class SelectDeviceAppUseCase(
private val devicesRepository: DevicesRepository,
private val getCurrentDeviceIdUseCase: GetCurrentDeviceIdUseCase,
) {
suspend operator fun invoke(packageName: String) {
val app = devicesRepository.currentDevice
.firstOrNull()
?.apps
?.find { it.packageName == packageName }
?: return
val deviceId = getCurrentDeviceIdUseCase() ?: return
devicesRepository.selectApp(app)
val app = devicesRepository.getDeviceAppByPackage(deviceId = deviceId, appPackageName = packageName) ?: return
devicesRepository.selectApp(deviceId = deviceId, app = app)
}
}

View file

@ -7,11 +7,6 @@ class SelectDeviceUseCase(
private val devicesRepository: DevicesRepository,
) {
suspend operator fun invoke(deviceId: String) {
val device = devicesRepository.devices
.firstOrNull()
?.find { it.deviceId == deviceId }
?: return
devicesRepository.selectDevice(device)
devicesRepository.selectDevice(deviceId)
}
}

View file

@ -1,6 +1,7 @@
package io.github.openflocon.domain.messages
import io.github.openflocon.domain.messages.usecase.HandleIncomingMessagesUseCase
import io.github.openflocon.domain.device.usecase.HandleNewAppUseCase
import io.github.openflocon.domain.messages.usecase.StartServerUseCase
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
@ -10,8 +11,9 @@ internal val messagesModule = module {
HandleIncomingMessagesUseCase(
messagesRepository = get(),
plugins = getAll(),
handleDeviceUseCase = get(),
handleDeviceAndAppUseCase = get(),
handleNewDeviceUseCase = get(),
handleNewAppUseCase = get(),
)
}
factoryOf(::StartServerUseCase)

View file

@ -3,7 +3,10 @@ package io.github.openflocon.domain.messages.usecase
import io.github.openflocon.domain.device.models.DeviceAppDomainModel
import io.github.openflocon.domain.device.models.DeviceDomainModel
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.device.usecase.HandleDeviceUseCase
import io.github.openflocon.domain.device.models.RegisterDeviceWithAppDomainModel
import io.github.openflocon.domain.device.usecase.HandleDeviceAndAppUseCase
import io.github.openflocon.domain.device.usecase.HandleNewAppUseCase
import io.github.openflocon.domain.device.usecase.HandleNewDeviceUseCase
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
import io.github.openflocon.domain.messages.repository.MessagesReceiverRepository
import io.github.openflocon.domain.messages.repository.MessagesRepository
@ -14,14 +17,15 @@ import kotlinx.coroutines.flow.onEach
class HandleIncomingMessagesUseCase(
private val messagesRepository: MessagesRepository,
private val plugins: List<MessagesReceiverRepository>,
private val handleDeviceUseCase: HandleDeviceUseCase,
private val handleDeviceAndAppUseCase: HandleDeviceAndAppUseCase,
private val handleNewDeviceUseCase: HandleNewDeviceUseCase,
private val handleNewAppUseCase: HandleNewAppUseCase,
) {
operator fun invoke(): Flow<Unit> = messagesRepository
.listenMessages()
.onEach {
val handleDeviceResult = handleDeviceUseCase(device = getDevice(it))
val handleDeviceResult = handleDeviceAndAppUseCase(device = getDeviceAndApp(it))
if (handleDeviceResult.isNewDevice) {
handleNewDeviceUseCase(
deviceIdAndPackageName = DeviceIdAndPackageNameDomainModel(
@ -30,14 +34,22 @@ class HandleIncomingMessagesUseCase(
)
)
}
if (handleDeviceResult.isNewApp) {
handleNewAppUseCase(
deviceIdAndPackageName = DeviceIdAndPackageNameDomainModel(
deviceId = handleDeviceResult.deviceId,
packageName = it.appPackageName,
)
)
}
plugins.forEach { plugin ->
if (handleDeviceResult.isNewDevice) {
if (handleDeviceResult.justConnectedForThisSession) {
plugin.onDeviceConnected(
deviceIdAndPackageName = DeviceIdAndPackageNameDomainModel(
deviceId = handleDeviceResult.deviceId,
packageName = it.appPackageName,
),
isNewDevice = true, // TODO on a next MR, for now handleDeviceResult.isNewDevice is always true the first time
isNewDevice = handleDeviceResult.isNewDevice,
)
}
if (plugin.pluginName.contains(it.plugin)) {
@ -47,14 +59,16 @@ class HandleIncomingMessagesUseCase(
}
.map { }
private fun getDevice(message: FloconIncomingMessageDomainModel): DeviceDomainModel = DeviceDomainModel(
deviceName = message.deviceName,
deviceId = message.deviceId,
apps = listOf(
DeviceAppDomainModel(
private fun getDeviceAndApp(message: FloconIncomingMessageDomainModel) =
RegisterDeviceWithAppDomainModel(
device = DeviceDomainModel(
deviceId = message.deviceId,
message.deviceName,
),
app = DeviceAppDomainModel(
name = message.appName,
packageName = message.appPackageName,
iconEncoded = null,
),
),
)
)
}

View file

@ -8,7 +8,7 @@ class SelectCurrentDeviceSharedPreferenceUseCase(
private val sharedPreferenceRepository: SharedPreferencesRepository,
private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase,
) {
operator fun invoke(sharedPreferenceId: DeviceSharedPreferenceId) {
suspend operator fun invoke(sharedPreferenceId: DeviceSharedPreferenceId) {
val current = getCurrentDeviceIdAndPackageNameUseCase() ?: return
sharedPreferenceRepository.selectDeviceSharedPreference(