Skip to content

Commit

Permalink
New deeplink for open update/mfkey32 screen (#715)
Browse files Browse the repository at this point in the history
**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 <[email protected]>
  • Loading branch information
LionZXY and Dzhos Oleksii authored Oct 10, 2023
1 parent 4e472a0 commit f9800cd
Show file tree
Hide file tree
Showing 21 changed files with 463 additions and 64 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <A, B> Iterable<A>.pmap(block: suspend (A) -> B): List<B> = coroutineScope {
map { async { block(it) } }.awaitAll()
suspend fun <A, B> Iterable<A>.pmap(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend (A) -> B
): List<B> = coroutineScope {
map { async(context) { block(it) } }.awaitAll()
}

fun <T, M> StateFlow<T>.map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -56,7 +76,8 @@ private fun PreviewComposableFlipperMockup() {
flipperColor = color,
isActive = isActive,
mockupImage = ComposableFlipperMockupImage.DEFAULT,
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 = {
Expand All @@ -29,4 +38,8 @@ class NFCAttackFeatureEntryImpl @Inject constructor(
}
}
}

override fun handleDeepLink(intent: Intent) {
rootNavHostController?.handleDeepLink(intent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.flipperdevices.nfc.mfkey32.api

import android.content.Intent

interface MfKey32HandleDeeplink {
fun handleDeepLink(intent: Intent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ interface MfKey32ScreenEntry : AggregateFeatureEntry {
get() = FeatureScreenRootRoute.MFKEY32

fun startDestination(): String

fun getMfKeyScreenByDeeplink(): String
}
2 changes: 2 additions & 0 deletions components/nfc/mfkey32/screen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit f9800cd

Please sign in to comment.