From f9800cdbe308384cc1130e20fcc7fc299d121689 Mon Sep 17 00:00:00 2001 From: Nikita Kulikov Date: Tue, 10 Oct 2023 17:11:46 +0400 Subject: [PATCH] New deeplink for open update/mfkey32 screen (#715) **Background** User can scan QR Code and open app **Changes** * New deep link model + parser + handler * Wait Flipper connection on mfkey **Test plan** Try go to https://flpr.app/o/mfkey32 https://flpr.app/o/update Co-authored: @Programistich --------- Co-authored-by: Dzhos Oleksii --- CHANGELOG.md | 1 + .../core/ktx/jre/IterratorKtx.kt | 9 +- .../flippermockup/ComposableFlipperMockup.kt | 57 ++++++---- .../ComposableFlipperMockupInternal.kt | 9 +- .../ComposableFlipperMockupInternalRaw.kt | 90 ++++++++++++++++ .../flipperdevices/deeplink/model/Deeplink.kt | 8 ++ .../impl/parser/delegates/DeepLinkMfKey.kt | 36 +++++++ .../impl/parser/delegates/DeepLinkUpdate.kt | 36 +++++++ .../info/impl/api/InfoDeeplinkHandler.kt | 1 + .../impl/api/NFCAttackFeatureEntryImpl.kt | 15 ++- .../nfc/mfkey32/api/MfKey32HandleDeeplink.kt | 7 ++ .../nfc/mfkey32/api/MfKey32ScreenEntry.kt | 2 + .../nfc/mfkey32/screen/build.gradle.kts | 2 + .../screen/api/DeepLinkMfKey32Handler.kt | 34 ++++++ .../screen/api/MfKey32ScreenEntryImpl.kt | 20 +++- .../composable/ComposableMfKey32Screen.kt | 1 + .../progressbar/ComposableMfKey32Progress.kt | 7 +- .../ComposableWaitingFlipperConnection.kt | 86 +++++++++++++++ .../nfc/mfkey32/screen/model/MfKey32State.kt | 4 +- .../screen/viewmodel/MfKey32ViewModel.kt | 101 ++++++++++++------ .../screen/src/main/res/values/strings.xml | 1 + 21 files changed, 463 insertions(+), 64 deletions(-) rename components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/{ => internal}/ComposableFlipperMockupInternal.kt (86%) create mode 100644 components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/internal/ComposableFlipperMockupInternalRaw.kt create mode 100644 components/deeplink/impl/src/main/java/com/flipperdevices/deeplink/impl/parser/delegates/DeepLinkMfKey.kt create mode 100644 components/deeplink/impl/src/main/java/com/flipperdevices/deeplink/impl/parser/delegates/DeepLinkUpdate.kt create mode 100644 components/nfc/mfkey32/api/src/main/java/com/flipperdevices/nfc/mfkey32/api/MfKey32HandleDeeplink.kt create mode 100644 components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/api/DeepLinkMfKey32Handler.kt create mode 100644 components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/progressbar/ComposableWaitingFlipperConnection.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index e72590fd1f..5198dc7db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - [Feature] Check Self Update App in Options (only for github) - [Feature] Fap Catalog save sort - [Feature] Add metrics for faphub +- [Feature] Add open mfkey32 deeplink - [Feature] Add dialog about failed BLE HID connection - [Feature] Add countly sessions - [FIX] Use by default dark theme in Wear OS diff --git a/components/core/ktx/src/main/java/com/flipperdevices/core/ktx/jre/IterratorKtx.kt b/components/core/ktx/src/main/java/com/flipperdevices/core/ktx/jre/IterratorKtx.kt index f6e5392718..60757e0af6 100644 --- a/components/core/ktx/src/main/java/com/flipperdevices/core/ktx/jre/IterratorKtx.kt +++ b/components/core/ktx/src/main/java/com/flipperdevices/core/ktx/jre/IterratorKtx.kt @@ -10,12 +10,17 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * Parallel map */ -suspend fun Iterable.pmap(block: suspend (A) -> B): List = coroutineScope { - map { async { block(it) } }.awaitAll() +suspend fun Iterable.pmap( + context: CoroutineContext = EmptyCoroutineContext, + block: suspend (A) -> B +): List = coroutineScope { + map { async(context) { block(it) } }.awaitAll() } fun StateFlow.map( diff --git a/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/ComposableFlipperMockup.kt b/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/ComposableFlipperMockup.kt index 5c37b6bc65..a423d9e146 100644 --- a/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/ComposableFlipperMockup.kt +++ b/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/ComposableFlipperMockup.kt @@ -8,6 +8,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.flipperdevices.core.preference.pb.HardwareColor +import com.flipperdevices.core.ui.flippermockup.internal.ComposableFlipperMockupInternal +import com.flipperdevices.core.ui.flippermockup.internal.ComposableFlipperMockupInternalRaw import com.flipperdevices.core.ui.theme.FlipperThemeInternal @Composable @@ -17,23 +19,7 @@ fun ComposableFlipperMockup( mockupImage: ComposableFlipperMockupImage, modifier: Modifier = Modifier ) { - val templatePicId = when (flipperColor) { - HardwareColor.UNRECOGNIZED, - HardwareColor.WHITE -> when (isActive) { - true -> R.drawable.template_white_flipper_active - false -> R.drawable.template_white_flipper_disabled - } - - HardwareColor.BLACK -> when (isActive) { - true -> R.drawable.template_black_flipper_active - false -> R.drawable.template_black_flipper_disabled - } - - HardwareColor.TRANSPARENT -> when (isActive) { - true -> R.drawable.template_transparent_flipper_active - false -> R.drawable.template_transparent_flipper_disabled - } - } + val templatePicId = getTemplatePicId(flipperColor, isActive) ComposableFlipperMockupInternal( templatePicId = templatePicId, @@ -42,6 +28,40 @@ fun ComposableFlipperMockup( ) } +@Composable +fun ComposableFlipperMockup( + flipperColor: HardwareColor, + isActive: Boolean, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val templatePicId = getTemplatePicId(flipperColor, isActive) + + ComposableFlipperMockupInternalRaw( + templatePicId = templatePicId, + modifier = modifier, + content = content + ) +} + +private fun getTemplatePicId(color: HardwareColor, isActive: Boolean) = when (color) { + HardwareColor.UNRECOGNIZED, + HardwareColor.WHITE -> when (isActive) { + true -> R.drawable.template_white_flipper_active + false -> R.drawable.template_white_flipper_disabled + } + + HardwareColor.BLACK -> when (isActive) { + true -> R.drawable.template_black_flipper_active + false -> R.drawable.template_black_flipper_disabled + } + + HardwareColor.TRANSPARENT -> when (isActive) { + true -> R.drawable.template_transparent_flipper_active + false -> R.drawable.template_transparent_flipper_disabled + } +} + @Preview( showBackground = true, heightDp = 1500 @@ -56,7 +76,8 @@ private fun PreviewComposableFlipperMockup() { flipperColor = color, isActive = isActive, mockupImage = ComposableFlipperMockupImage.DEFAULT, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .padding(top = 16.dp) ) } diff --git a/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/ComposableFlipperMockupInternal.kt b/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/internal/ComposableFlipperMockupInternal.kt similarity index 86% rename from components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/ComposableFlipperMockupInternal.kt rename to components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/internal/ComposableFlipperMockupInternal.kt index 503f0886a3..5c8262bea5 100644 --- a/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/ComposableFlipperMockupInternal.kt +++ b/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/internal/ComposableFlipperMockupInternal.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.core.ui.flippermockup +package com.flipperdevices.core.ui.flippermockup.internal import androidx.annotation.DrawableRes import androidx.compose.foundation.Image @@ -13,11 +13,12 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.flipperdevices.core.ui.flippermockup.R import com.flipperdevices.core.ui.theme.FlipperThemeInternal -private const val FLIPPER_DEFAULT_HEIGHT = 100f -private const val FLIPPER_DEFAULT_WIDTH = 238f -private const val FLIPPER_RATIO = FLIPPER_DEFAULT_WIDTH / FLIPPER_DEFAULT_HEIGHT +internal const val FLIPPER_DEFAULT_HEIGHT = 100f +internal const val FLIPPER_DEFAULT_WIDTH = 238f +internal const val FLIPPER_RATIO = FLIPPER_DEFAULT_WIDTH / FLIPPER_DEFAULT_HEIGHT @Composable internal fun ComposableFlipperMockupInternal( diff --git a/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/internal/ComposableFlipperMockupInternalRaw.kt b/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/internal/ComposableFlipperMockupInternalRaw.kt new file mode 100644 index 0000000000..90a511b490 --- /dev/null +++ b/components/core/ui/flippermockup/src/main/java/com/flipperdevices/core/ui/flippermockup/internal/ComposableFlipperMockupInternalRaw.kt @@ -0,0 +1,90 @@ +package com.flipperdevices.core.ui.flippermockup.internal + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.flipperdevices.core.ui.flippermockup.R +import com.flipperdevices.core.ui.theme.FlipperThemeInternal + +private const val IMAGE_WIDTH_PADDING_PERCENT = 60.56f / FLIPPER_DEFAULT_WIDTH +private const val IMAGE_HEIGHT_PADDING_PERCENT = 10.54f / FLIPPER_DEFAULT_HEIGHT +private const val IMAGE_WIDTH_PERCENT = 85.33f / FLIPPER_DEFAULT_WIDTH +private const val IMAGE_HEIGHT_PERCENT = 46.96f / FLIPPER_DEFAULT_HEIGHT +private const val IMAGE_ROUND_CORNER_PERCENT = 3.4f / FLIPPER_DEFAULT_WIDTH + +@Composable +fun ComposableFlipperMockupInternalRaw( + @DrawableRes templatePicId: Int, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + BoxWithConstraints( + modifier + .aspectRatio( + ratio = FLIPPER_RATIO + ) + ) { + Image( + modifier = Modifier.fillMaxSize(), + painter = painterResource(templatePicId), + contentDescription = stringResource(R.string.flippermockup_template_desc) + ) + Box( + modifier = Modifier + .padding( + start = remember(maxWidth) { maxWidth * IMAGE_WIDTH_PADDING_PERCENT }, + top = remember(maxHeight) { maxHeight * IMAGE_HEIGHT_PADDING_PERCENT } + ) + .size( + width = remember(maxWidth) { maxWidth * IMAGE_WIDTH_PERCENT }, + height = remember(maxHeight) { maxHeight * IMAGE_HEIGHT_PERCENT } + ) + .clip( + RoundedCornerShape( + size = remember(maxWidth) { maxWidth * IMAGE_ROUND_CORNER_PERCENT } + ) + ), + ) { + content() + } + } +} + +@Preview( + showSystemUi = true, + showBackground = true +) +@Composable +private fun ComposableFlipperMockupInternalPreview() { + FlipperThemeInternal { + Column { + ComposableFlipperMockupInternalRaw( + modifier = Modifier.fillMaxWidth(), + templatePicId = R.drawable.template_white_flipper_active + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Red) + ) + } + } + } +} diff --git a/components/deeplink/api/src/main/java/com/flipperdevices/deeplink/model/Deeplink.kt b/components/deeplink/api/src/main/java/com/flipperdevices/deeplink/model/Deeplink.kt index ff4204a339..c949a17659 100644 --- a/components/deeplink/api/src/main/java/com/flipperdevices/deeplink/model/Deeplink.kt +++ b/components/deeplink/api/src/main/java/com/flipperdevices/deeplink/model/Deeplink.kt @@ -45,4 +45,12 @@ sealed class Deeplink : Parcelable { data class Fap( val appId: String, ) : Deeplink() + + @Parcelize + @Serializable + data object OpenMfKey : Deeplink() + + @Parcelize + @Serializable + data object OpenUpdate : Deeplink() } diff --git a/components/deeplink/impl/src/main/java/com/flipperdevices/deeplink/impl/parser/delegates/DeepLinkMfKey.kt b/components/deeplink/impl/src/main/java/com/flipperdevices/deeplink/impl/parser/delegates/DeepLinkMfKey.kt new file mode 100644 index 0000000000..9c60907e86 --- /dev/null +++ b/components/deeplink/impl/src/main/java/com/flipperdevices/deeplink/impl/parser/delegates/DeepLinkMfKey.kt @@ -0,0 +1,36 @@ +package com.flipperdevices.deeplink.impl.parser.delegates + +import android.content.Context +import android.content.Intent +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.deeplink.api.DeepLinkParserDelegate +import com.flipperdevices.deeplink.impl.utils.Constants +import com.flipperdevices.deeplink.model.DeepLinkParserDelegatePriority +import com.flipperdevices.deeplink.model.Deeplink +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject + +private val PATHS = listOf("o", "mfkey32") + +@ContributesMultibinding(AppGraph::class, DeepLinkParserDelegate::class) +class DeepLinkMfKey @Inject constructor() : DeepLinkParserDelegate, LogTagProvider { + override val TAG = "DeepLinkFap" + + override fun getPriority( + context: Context, + intent: Intent + ): DeepLinkParserDelegatePriority? { + val pathSegment = intent.data?.pathSegments + + return when { + intent.data == null -> null + !Constants.SUPPORTED_HOSTS.contains(intent.data?.host) -> null + pathSegment == null -> null + pathSegment == PATHS -> DeepLinkParserDelegatePriority.HIGH + else -> null + } + } + + override suspend fun fromIntent(context: Context, intent: Intent) = Deeplink.OpenMfKey +} diff --git a/components/deeplink/impl/src/main/java/com/flipperdevices/deeplink/impl/parser/delegates/DeepLinkUpdate.kt b/components/deeplink/impl/src/main/java/com/flipperdevices/deeplink/impl/parser/delegates/DeepLinkUpdate.kt new file mode 100644 index 0000000000..6bd1ca677a --- /dev/null +++ b/components/deeplink/impl/src/main/java/com/flipperdevices/deeplink/impl/parser/delegates/DeepLinkUpdate.kt @@ -0,0 +1,36 @@ +package com.flipperdevices.deeplink.impl.parser.delegates + +import android.content.Context +import android.content.Intent +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.deeplink.api.DeepLinkParserDelegate +import com.flipperdevices.deeplink.impl.utils.Constants +import com.flipperdevices.deeplink.model.DeepLinkParserDelegatePriority +import com.flipperdevices.deeplink.model.Deeplink +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject + +private val PATHS = listOf("o", "update") + +@ContributesMultibinding(AppGraph::class, DeepLinkParserDelegate::class) +class DeepLinkUpdate @Inject constructor() : DeepLinkParserDelegate, LogTagProvider { + override val TAG = "DeepLinkFap" + + override fun getPriority( + context: Context, + intent: Intent + ): DeepLinkParserDelegatePriority? { + val pathSegment = intent.data?.pathSegments + + return when { + intent.data == null -> null + !Constants.SUPPORTED_HOSTS.contains(intent.data?.host) -> null + pathSegment == null -> null + pathSegment == PATHS -> DeepLinkParserDelegatePriority.HIGH + else -> null + } + } + + override suspend fun fromIntent(context: Context, intent: Intent) = Deeplink.OpenUpdate +} diff --git a/components/info/impl/src/main/java/com/flipperdevices/info/impl/api/InfoDeeplinkHandler.kt b/components/info/impl/src/main/java/com/flipperdevices/info/impl/api/InfoDeeplinkHandler.kt index 18988d3fb1..ea0ba62f9b 100644 --- a/components/info/impl/src/main/java/com/flipperdevices/info/impl/api/InfoDeeplinkHandler.kt +++ b/components/info/impl/src/main/java/com/flipperdevices/info/impl/api/InfoDeeplinkHandler.kt @@ -20,6 +20,7 @@ class InfoDeeplinkHandler @Inject constructor( override fun isSupportLink(link: Deeplink): DispatcherPriority? { return when (link) { is Deeplink.WebUpdate -> DispatcherPriority.DEFAULT + is Deeplink.OpenUpdate -> DispatcherPriority.DEFAULT else -> null } } diff --git a/components/nfc/attack/impl/src/main/java/com/flipperdevices/nfc/attack/impl/api/NFCAttackFeatureEntryImpl.kt b/components/nfc/attack/impl/src/main/java/com/flipperdevices/nfc/attack/impl/api/NFCAttackFeatureEntryImpl.kt index 812c5b84e0..a84960168f 100644 --- a/components/nfc/attack/impl/src/main/java/com/flipperdevices/nfc/attack/impl/api/NFCAttackFeatureEntryImpl.kt +++ b/components/nfc/attack/impl/src/main/java/com/flipperdevices/nfc/attack/impl/api/NFCAttackFeatureEntryImpl.kt @@ -1,5 +1,6 @@ package com.flipperdevices.nfc.attack.impl.api +import android.content.Intent import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable @@ -8,19 +9,27 @@ import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.ui.navigation.AggregateFeatureEntry import com.flipperdevices.nfc.attack.api.NFCAttackFeatureEntry import com.flipperdevices.nfc.attack.impl.composable.ComposableNfcAttack +import com.flipperdevices.nfc.mfkey32.api.MfKey32HandleDeeplink import com.flipperdevices.nfc.mfkey32.api.MfKey32ScreenEntry import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject +import javax.inject.Singleton +@Singleton @ContributesBinding(AppGraph::class, NFCAttackFeatureEntry::class) +@ContributesBinding(AppGraph::class, MfKey32HandleDeeplink::class) @ContributesMultibinding(AppGraph::class, AggregateFeatureEntry::class) class NFCAttackFeatureEntryImpl @Inject constructor( private val mfKey32ScreenEntry: MfKey32ScreenEntry -) : NFCAttackFeatureEntry { +) : NFCAttackFeatureEntry, MfKey32HandleDeeplink { + + private var rootNavHostController: NavHostController? = null + override fun start() = "@${ROUTE.name}" override fun NavGraphBuilder.navigation(navController: NavHostController) { + rootNavHostController = navController navigation(startDestination = start(), route = ROUTE.name) { composable(start()) { ComposableNfcAttack(onOpenMfKey32 = { @@ -29,4 +38,8 @@ class NFCAttackFeatureEntryImpl @Inject constructor( } } } + + override fun handleDeepLink(intent: Intent) { + rootNavHostController?.handleDeepLink(intent) + } } diff --git a/components/nfc/mfkey32/api/src/main/java/com/flipperdevices/nfc/mfkey32/api/MfKey32HandleDeeplink.kt b/components/nfc/mfkey32/api/src/main/java/com/flipperdevices/nfc/mfkey32/api/MfKey32HandleDeeplink.kt new file mode 100644 index 0000000000..21cafd0f92 --- /dev/null +++ b/components/nfc/mfkey32/api/src/main/java/com/flipperdevices/nfc/mfkey32/api/MfKey32HandleDeeplink.kt @@ -0,0 +1,7 @@ +package com.flipperdevices.nfc.mfkey32.api + +import android.content.Intent + +interface MfKey32HandleDeeplink { + fun handleDeepLink(intent: Intent) +} diff --git a/components/nfc/mfkey32/api/src/main/java/com/flipperdevices/nfc/mfkey32/api/MfKey32ScreenEntry.kt b/components/nfc/mfkey32/api/src/main/java/com/flipperdevices/nfc/mfkey32/api/MfKey32ScreenEntry.kt index bfc3a31410..6f8ce8da28 100644 --- a/components/nfc/mfkey32/api/src/main/java/com/flipperdevices/nfc/mfkey32/api/MfKey32ScreenEntry.kt +++ b/components/nfc/mfkey32/api/src/main/java/com/flipperdevices/nfc/mfkey32/api/MfKey32ScreenEntry.kt @@ -8,4 +8,6 @@ interface MfKey32ScreenEntry : AggregateFeatureEntry { get() = FeatureScreenRootRoute.MFKEY32 fun startDestination(): String + + fun getMfKeyScreenByDeeplink(): String } diff --git a/components/nfc/mfkey32/screen/build.gradle.kts b/components/nfc/mfkey32/screen/build.gradle.kts index a07b74fc8f..b371508ac9 100644 --- a/components/nfc/mfkey32/screen/build.gradle.kts +++ b/components/nfc/mfkey32/screen/build.gradle.kts @@ -28,6 +28,8 @@ dependencies { implementation(projects.components.bridge.pbutils) implementation(projects.components.analytics.metric.api) + implementation(projects.components.deeplink.api) + implementation(projects.components.bottombar.api) // Compose implementation(libs.compose.ui) diff --git a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/api/DeepLinkMfKey32Handler.kt b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/api/DeepLinkMfKey32Handler.kt new file mode 100644 index 0000000000..caa313562f --- /dev/null +++ b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/api/DeepLinkMfKey32Handler.kt @@ -0,0 +1,34 @@ +package com.flipperdevices.nfc.mfkey32.screen.api + +import android.content.Intent +import androidx.core.net.toUri +import androidx.navigation.NavController +import com.flipperdevices.core.di.AppGraph +import com.flipperdevices.deeplink.api.DeepLinkHandler +import com.flipperdevices.deeplink.api.DispatcherPriority +import com.flipperdevices.deeplink.model.Deeplink +import com.flipperdevices.nfc.mfkey32.api.MfKey32ScreenEntry +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject + +@ContributesMultibinding(AppGraph::class, DeepLinkHandler::class) +class DeepLinkMfKey32Handler @Inject constructor( + private val mfKey32ScreenEntry: MfKey32ScreenEntry, +) : DeepLinkHandler { + override fun isSupportLink(link: Deeplink): DispatcherPriority? { + if (link is Deeplink.OpenMfKey) { + return DispatcherPriority.HIGH + } + return null + } + + override fun processLink(navController: NavController, link: Deeplink) { + if (link !is Deeplink.OpenMfKey) return + + val intent = Intent().apply { + data = mfKey32ScreenEntry.getMfKeyScreenByDeeplink().toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + navController.handleDeepLink(intent) + } +} diff --git a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/api/MfKey32ScreenEntryImpl.kt b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/api/MfKey32ScreenEntryImpl.kt index ae5372bdc7..01f3537ccf 100644 --- a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/api/MfKey32ScreenEntryImpl.kt +++ b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/api/MfKey32ScreenEntryImpl.kt @@ -4,24 +4,42 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.compose.navigation +import androidx.navigation.navDeepLink import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.ui.navigation.AggregateFeatureEntry +import com.flipperdevices.deeplink.model.DeeplinkConstants import com.flipperdevices.nfc.mfkey32.api.MfKey32ScreenEntry import com.flipperdevices.nfc.mfkey32.screen.composable.ComposableMfKey32Screen import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject +private const val DEEPLINK_SCHEME = DeeplinkConstants.SCHEMA +private const val DEEPLINK_MF_KEY = "${DEEPLINK_SCHEME}/mfkey32" + @ContributesBinding(AppGraph::class, MfKey32ScreenEntry::class) @ContributesMultibinding(AppGraph::class, AggregateFeatureEntry::class) class MfKey32ScreenEntryImpl @Inject constructor() : MfKey32ScreenEntry { override fun startDestination() = "@${ROUTE.name}" + override fun getMfKeyScreenByDeeplink(): String { + return DEEPLINK_MF_KEY + } + + private val deeplinkArguments = listOf( + navDeepLink { + uriPattern = DEEPLINK_MF_KEY + } + ) + override fun NavGraphBuilder.navigation(navController: NavHostController) { navigation( startDestination = startDestination(), route = ROUTE.name ) { - composable(startDestination()) { + composable( + route = startDestination(), + deepLinks = deeplinkArguments + ) { ComposableMfKey32Screen(navController) } } diff --git a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/ComposableMfKey32Screen.kt b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/ComposableMfKey32Screen.kt index e485c99b61..028ef9e19e 100644 --- a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/ComposableMfKey32Screen.kt +++ b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/ComposableMfKey32Screen.kt @@ -41,6 +41,7 @@ fun ComposableMfKey32Screen( { isDisplayDialog = true } } is MfKey32State.Error, + MfKey32State.WaitingForFlipper, is MfKey32State.Saved -> { { navController.popBackStack() } } diff --git a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/progressbar/ComposableMfKey32Progress.kt b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/progressbar/ComposableMfKey32Progress.kt index 2426f9ec2e..d71775bfee 100644 --- a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/progressbar/ComposableMfKey32Progress.kt +++ b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/progressbar/ComposableMfKey32Progress.kt @@ -30,13 +30,15 @@ import com.flipperdevices.nfc.mfkey32.screen.model.MfKey32State @Composable fun ComposableMfKey32Progress(navController: NavController, state: MfKey32State) { when (state) { + MfKey32State.WaitingForFlipper -> ComposableWaitingFlipperConnection() + is MfKey32State.Calculating -> ComposableMfKey32ProgressInternal( titleId = R.string.mfkey32_calculation_title, descriptionId = R.string.mfkey32_calculation_desc, iconId = R.drawable.pic_key, percent = state.percent, accentColor = LocalPallet.current.calculationMfKey32, - secondColor = LocalPallet.current.calculationMfKey32Background + secondColor = LocalPallet.current.calculationMfKey32Background, ) is MfKey32State.DownloadingRawFile -> ComposableMfKey32ProgressInternal( titleId = R.string.mfkey32_downloading_title, @@ -99,7 +101,8 @@ private fun ComposableMfKey32ProgressInternal( if (percent != null) { val animatedProgress by animateFloatAsState( targetValue = percent, - animationSpec = tween(durationMillis = 500, easing = LinearEasing) + animationSpec = tween(durationMillis = 500, easing = LinearEasing), + label = "Progress" ) FlipperProgressIndicator( diff --git a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/progressbar/ComposableWaitingFlipperConnection.kt b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/progressbar/ComposableWaitingFlipperConnection.kt new file mode 100644 index 0000000000..630b3e93ca --- /dev/null +++ b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/composable/progressbar/ComposableWaitingFlipperConnection.kt @@ -0,0 +1,86 @@ +package com.flipperdevices.nfc.mfkey32.screen.composable.progressbar + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flipperdevices.core.preference.pb.HardwareColor +import com.flipperdevices.core.ui.flippermockup.ComposableFlipperMockup +import com.flipperdevices.core.ui.ktx.placeholderConnecting +import com.flipperdevices.core.ui.theme.FlipperThemeInternal +import com.flipperdevices.core.ui.theme.LocalPallet +import com.flipperdevices.core.ui.theme.LocalTypography +import com.flipperdevices.nfc.mfkey32.screen.R +import com.flipperdevices.nfc.mfkey32.screen.viewmodel.FlipperColorViewModel +import tangle.viewmodel.compose.tangleViewModel + +@Composable +fun ComposableWaitingFlipperConnection( + modifier: Modifier = Modifier +) { + val flipperColorViewModel = tangleViewModel() + val flipperColor by flipperColorViewModel.getFlipperColor().collectAsState() + + ComposableWaitingFlipperConnectionInternal( + flipperColor = flipperColor, + modifier = modifier + ) +} + +@Composable +private fun ComposableWaitingFlipperConnectionInternal( + flipperColor: HardwareColor, + modifier: Modifier = Modifier +) = Column( + modifier = modifier.fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally +) { + Text( + modifier = Modifier.padding(top = 32.dp, bottom = 18.dp), + text = stringResource(R.string.mfkey32_connecting), + style = LocalTypography.current.titleSB18, + color = LocalPallet.current.text100, + textAlign = TextAlign.Center + ) + ComposableFlipperMockup( + modifier = Modifier + .fillMaxWidth() + .padding(start = 14.dp, end = 14.dp, bottom = 32.dp), + flipperColor = flipperColor, + isActive = false, + content = { + Box( + modifier = Modifier + .fillMaxSize() + .placeholderConnecting() + ) + } + ) +} + +@Preview( + showBackground = true, + showSystemUi = true +) +@Composable +private fun ComposableWaitingFlipperConnectionPreview() { + FlipperThemeInternal { + Box { + ComposableWaitingFlipperConnectionInternal( + flipperColor = HardwareColor.TRANSPARENT + ) + } + } +} diff --git a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/model/MfKey32State.kt b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/model/MfKey32State.kt index 6e2dcba290..565073d766 100644 --- a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/model/MfKey32State.kt +++ b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/model/MfKey32State.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Stable import kotlinx.collections.immutable.ImmutableList sealed class MfKey32State { + data object WaitingForFlipper : MfKey32State() + data class DownloadingRawFile( val percent: Float? ) : MfKey32State() @@ -12,7 +14,7 @@ sealed class MfKey32State { val percent: Float ) : MfKey32State() - object Uploading : MfKey32State() + data object Uploading : MfKey32State() data class Saved(val keys: ImmutableList) : MfKey32State() data class Error(val errorType: ErrorType) : MfKey32State() diff --git a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/viewmodel/MfKey32ViewModel.kt b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/viewmodel/MfKey32ViewModel.kt index 84350e395e..fa600637f1 100644 --- a/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/viewmodel/MfKey32ViewModel.kt +++ b/components/nfc/mfkey32/screen/src/main/java/com/flipperdevices/nfc/mfkey32/screen/viewmodel/MfKey32ViewModel.kt @@ -4,11 +4,11 @@ import android.content.Context import androidx.lifecycle.viewModelScope import com.flipperdevices.bridge.api.manager.FlipperRequestApi import com.flipperdevices.bridge.api.manager.ktx.state.ConnectionState -import com.flipperdevices.bridge.api.manager.ktx.state.FlipperSupportedState import com.flipperdevices.bridge.api.model.wrapToRequest import com.flipperdevices.bridge.service.api.FlipperServiceApi import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceConsumer import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider +import com.flipperdevices.core.ktx.jre.pmap import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.error import com.flipperdevices.core.log.info @@ -27,14 +27,17 @@ import com.flipperdevices.protobuf.main import com.flipperdevices.protobuf.storage.deleteRequest import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.async +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import tangle.viewmodel.VMInject import java.io.FileNotFoundException import java.math.BigInteger @@ -55,13 +58,15 @@ class MfKey32ViewModel @VMInject constructor( Runtime.getRuntime().availableProcessors() ).asCoroutineDispatcher() private val mfKey32StateFlow = MutableStateFlow( - MfKey32State.DownloadingRawFile(null) + MfKey32State.Error(ErrorType.FLIPPER_CONNECTION) ) private val existedKeysStorage = ExistedKeysStorage(context) private val fileWithNonce by lazy { FlipperStorageProvider.getTemporaryFile(context) } + private var stateJob: Job? = null + private val mutex = Mutex() init { flipperServiceProvider.provideServiceApi(this, this) @@ -72,45 +77,71 @@ class MfKey32ViewModel @VMInject constructor( existedKeysStorage.getFoundedInformation() override fun onServiceApiReady(serviceApi: FlipperServiceApi) { - viewModelScope.launch(Dispatchers.Default) { - if (!prepare(serviceApi)) { - return@launch + viewModelScope.launch { + mutex.withLock { + val localJob = stateJob + stateJob = viewModelScope.launch(Dispatchers.Default) { + localJob?.cancelAndJoin() + serviceApi + .connectionInformationApi + .getConnectionStateFlow() + .collectLatest { connectionState -> + startCalculation(serviceApi, connectionState) + } + } } + } + } - val nonces = KeyNonceParser.parse(fileWithNonce.readText()) - mfKey32StateFlow.emit(MfKey32State.Calculating(0f)) - nonces.map { nonce -> - async(bruteforceDispatcher) { - val key = nfcToolsApi.bruteforceKey(nonce) - info { "Key for nonce $nonce = $key" } - onFoundKey(nonce, key, nonces.size) - } - }.forEach { - it.await() + private suspend fun startCalculation( + serviceApi: FlipperServiceApi, + connectionState: ConnectionState + ) { + info { "Start calculation on $connectionState" } + when (connectionState) { + ConnectionState.Connecting, + ConnectionState.Disconnecting, + ConnectionState.RetrievingInformation, + ConnectionState.Initializing -> { + mfKey32StateFlow.emit(MfKey32State.WaitingForFlipper) + return } - mfKey32StateFlow.emit(MfKey32State.Uploading) - val addedKeys = try { - existedKeysStorage.upload(serviceApi.requestApi) - } catch (exception: Throwable) { - error(exception) { "When save keys" } - mfKey32StateFlow.emit(MfKey32State.Error(ErrorType.READ_WRITE)) - return@launch + + is ConnectionState.Disconnected -> { + mfKey32StateFlow.emit(MfKey32State.Error(ErrorType.FLIPPER_CONNECTION)) + return } - deleteBruteforceApp(serviceApi.requestApi) - mfKey32StateFlow.emit(MfKey32State.Saved(addedKeys.toImmutableList())) + + is ConnectionState.Ready -> {} } - } - private suspend fun prepare(serviceApi: FlipperServiceApi): Boolean { - val connectionState = serviceApi.connectionInformationApi.getConnectionStateFlow().first() + if (!prepare(serviceApi)) { + info { "Failed prepare" } + return + } - if (connectionState !is ConnectionState.Ready || - connectionState.supportedState != FlipperSupportedState.READY - ) { - error { "Flipper not connected" } - mfKey32StateFlow.emit(MfKey32State.Error(ErrorType.FLIPPER_CONNECTION)) - return false + val nonces = KeyNonceParser.parse(fileWithNonce.readText()) + mfKey32StateFlow.emit(MfKey32State.Calculating(0f)) + nonces.pmap(bruteforceDispatcher) { nonce -> + val key = nfcToolsApi.bruteforceKey(nonce) + info { "Key for nonce $nonce = $key" } + onFoundKey(nonce, key, nonces.size) + } + mfKey32StateFlow.emit(MfKey32State.Uploading) + val addedKeys = try { + existedKeysStorage.upload(serviceApi.requestApi) + } catch (exception: Throwable) { + error(exception) { "When save keys" } + mfKey32StateFlow.emit(MfKey32State.Error(ErrorType.READ_WRITE)) + return } + deleteBruteforceApp(serviceApi.requestApi) + mfKey32StateFlow.emit(MfKey32State.Saved(addedKeys.toImmutableList())) + } + + private suspend fun prepare(serviceApi: FlipperServiceApi): Boolean { + info { "Flipper connected" } + MfKey32State.DownloadingRawFile(null) try { DownloadFileHelper.downloadFile( diff --git a/components/nfc/mfkey32/screen/src/main/res/values/strings.xml b/components/nfc/mfkey32/screen/src/main/res/values/strings.xml index 774b052761..3377ef0f69 100644 --- a/components/nfc/mfkey32/screen/src/main/res/values/strings.xml +++ b/components/nfc/mfkey32/screen/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Calculating… Calculation Completed Syncing with Flipper… + Connecting Flipper… 1 New Key added to User Dict. %1$s New Keys added to User Dict. New Keys Not Found