Skip to content

Commit

Permalink
Decompose Key Emulate (#616)
Browse files Browse the repository at this point in the history
**Background**

Step for integrate infared emulate

**Changes**

* Start/Stop/Error/OpenApp Helpers

**Test plan**

* Green pipeline
* Try emulate keys
  • Loading branch information
Programistich committed Jun 15, 2023
1 parent 8d94246 commit a2a4433
Show file tree
Hide file tree
Showing 9 changed files with 464 additions and 315 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- [Feature] FapHub manifest support offline mode and add target support for buttons
- [Refactor] Migrate all feature components modules from KSP to Anvil
- [Refactor] Migrate to Ktorfit
- [Refactor] Migrate key emulate to new module
- [Refactor] Migrate key emulate to new module, decompose Emulate Helper
- [Refactor] Key Screen state in API and KeyCard with state

# 1.5.1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.flipperdevices.keyemulate.helpers

import com.flipperdevices.bridge.api.manager.FlipperRequestApi
import com.flipperdevices.bridge.api.model.FlipperRequestPriority
import com.flipperdevices.bridge.api.model.wrapToRequest
import com.flipperdevices.bridge.api.utils.Constants
import com.flipperdevices.bridge.dao.api.model.FlipperKeyType
import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.core.log.LogTagProvider
import com.flipperdevices.core.log.error
import com.flipperdevices.core.log.info
import com.flipperdevices.keyemulate.exception.AlreadyOpenedAppException
import com.flipperdevices.protobuf.Flipper
import com.flipperdevices.protobuf.app.Application
import com.flipperdevices.protobuf.app.startRequest
import com.flipperdevices.protobuf.main
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withTimeoutOrNull
import javax.inject.Inject

private const val APP_STARTED_TIMEOUT_MS = 3 * 1000L // 3 seconds

interface AppEmulateHelper {
@Throws(AlreadyOpenedAppException::class)
suspend fun tryOpenApp(
scope: CoroutineScope,
requestApi: FlipperRequestApi,
keyType: FlipperKeyType
): Boolean
}

@ContributesBinding(AppGraph::class, AppEmulateHelper::class)
class AppEmulateHelperImpl @Inject constructor() : AppEmulateHelper, LogTagProvider {

override val TAG: String = "AppEmulateHelper"

@Throws(AlreadyOpenedAppException::class)
override suspend fun tryOpenApp(
scope: CoroutineScope,
requestApi: FlipperRequestApi,
keyType: FlipperKeyType
): Boolean {
val stateAppFlow = MutableStateFlow(Application.AppState.UNRECOGNIZED)
val pendingStateJob = requestApi
.notificationFlow()
.filter { it.hasAppStateResponse() }
.onEach {
info { "Receive app state $it" }
stateAppFlow.emit(it.appStateResponse.state)
}.launchIn(scope)
try {
val appStartResponse = requestApi.request(
flowOf(
main {
appStartRequest = startRequest {
name = keyType.flipperAppName
args = Constants.RPC_START_REQUEST_ARG
}
}.wrapToRequest(FlipperRequestPriority.FOREGROUND)
)
)
return processOpenAppResult(appStartResponse) {
stateAppFlow.filter { it == Application.AppState.APP_STARTED }.first()
}
} finally {
pendingStateJob.cancelAndJoin()
}
}

@Throws(AlreadyOpenedAppException::class)
private suspend fun processOpenAppResult(
appStartResponse: Flipper.Main,
onAppTimeout: suspend () -> Unit,
): Boolean {
if (appStartResponse.commandStatus == Flipper.CommandStatus.ERROR_APP_SYSTEM_LOCKED) {
error { "Handle already opened app" }
throw AlreadyOpenedAppException()
}

if (appStartResponse.commandStatus != Flipper.CommandStatus.OK) {
error { "Failed start rpc app with error $appStartResponse" }
return false
}

info { "Start waiting for stateAppFlow" }
val appState = withTimeoutOrNull(APP_STARTED_TIMEOUT_MS) {
onAppTimeout()
}
if (appState != null) {
info { "Receive that app state started" }
return true
}
info { "Failed wait for app state started" }
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.flipperdevices.keyemulate.helpers

import com.flipperdevices.bridge.api.manager.FlipperRequestApi
import com.flipperdevices.bridge.dao.api.model.FlipperFilePath
import com.flipperdevices.bridge.dao.api.model.FlipperKeyType
import com.flipperdevices.bridge.service.api.FlipperServiceApi
import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.core.ktx.jre.TimeHelper
import com.flipperdevices.core.ktx.jre.launchWithLock
import com.flipperdevices.core.ktx.jre.withLock
import com.flipperdevices.core.ktx.jre.withLockResult
import com.flipperdevices.core.log.LogTagProvider
import com.flipperdevices.core.log.error
import com.flipperdevices.core.log.info
import com.flipperdevices.keyemulate.api.EmulateHelper
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.max

/**
* It is very important for us not to call startEmulate if the application
* is already running - the flipper is very sensitive to the order of execution.
*/
@Singleton
@ContributesBinding(AppGraph::class, EmulateHelper::class)
class EmulateHelperImpl @Inject constructor(
private val startEmulateHelper: StartEmulateHelper,
private val stopEmulateHelper: StopEmulateHelper
) : EmulateHelper, LogTagProvider {
override val TAG = "EmulateHelper"

private var currentKeyEmulating = MutableStateFlow<FlipperFilePath?>(null)

@Volatile
private var stopEmulateTimeAllowedMs: Long = 0
private var stopJob: Job? = null
private val mutex = Mutex()

override fun getCurrentEmulatingKey(): StateFlow<FlipperFilePath?> = currentKeyEmulating

override suspend fun startEmulate(
scope: CoroutineScope,
serviceApi: FlipperServiceApi,
keyType: FlipperKeyType,
keyPath: FlipperFilePath,
minEmulateTime: Long
) = withLockResult(mutex, "start") {
val requestApi = serviceApi.requestApi
if (currentKeyEmulating.value != null) {
info { "Emulate already running, start stop" }
stopEmulateInternal(requestApi)
}
currentKeyEmulating.emit(keyPath)
try {
return@withLockResult startEmulateHelper.onStart(
scope,
serviceApi,
keyType,
keyPath,
minEmulateTime,
onStop = { stopEmulateInternal(requestApi) },
onResultTime = { time -> stopEmulateTimeAllowedMs = time }
)
} catch (throwable: Throwable) {
error(throwable) { "Failed start $keyPath" }
currentKeyEmulating.emit(null)
throw throwable
}
}

override suspend fun stopEmulate(
scope: CoroutineScope,
requestApi: FlipperRequestApi
) = withLock(mutex, "schedule_stop") {
if (stopJob != null) {
info { "Return from #stopEmulate because stop already in progress" }
return@withLock
}
if (TimeHelper.getNow() > stopEmulateTimeAllowedMs) {
info {
"Already passed delay, stop immediately " +
"(current: ${TimeHelper.getNow()}/$stopEmulateTimeAllowedMs)"
}
stopEmulateInternal(requestApi)
return@withLock
}
stopJob = scope.launch(Dispatchers.Default) {
try {
while (TimeHelper.getNow() < stopEmulateTimeAllowedMs) {
val delayMs = max(0, stopEmulateTimeAllowedMs - TimeHelper.getNow())
info { "Can't stop right now, wait $delayMs ms" }
delay(delayMs)
}
launchWithLock(mutex, scope, "stop") {
stopEmulateInternal(requestApi)
}
} finally {
stopJob = null
}
}
}

override suspend fun stopEmulateForce(
requestApi: FlipperRequestApi
) = withLock(mutex, "force_stop") {
if (stopJob != null) {
stopJob?.cancelAndJoin()
stopJob = null
}
stopEmulateInternal(requestApi)
}

private suspend fun stopEmulateInternal(requestApi: FlipperRequestApi) {
stopEmulateHelper.onStop(requestApi)
currentKeyEmulating.emit(null)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.flipperdevices.keyemulate.viewmodel.helpers
package com.flipperdevices.keyemulate.helpers

import com.flipperdevices.bridge.api.model.FlipperRequestPriority
import com.flipperdevices.bridge.api.model.wrapToRequest
Expand Down
Loading

0 comments on commit a2a4433

Please sign in to comment.