Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-09 18:25:59 +04:00
parent e4d637927a
commit 2a021f7d03
14 changed files with 155 additions and 76 deletions

View file

@ -34,9 +34,7 @@ import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.deeplink.DEEPLINK_KEY import com.tangem.core.deeplink.DEEPLINK_KEY
import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.WEBLINK_KEY import com.tangem.core.deeplink.WEBLINK_KEY
import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.UiDependencies
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.model.AppThemeMode
@ -53,6 +51,7 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService import com.tangem.operations.backup.BackupService
@ -69,7 +68,6 @@ import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandle
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.AppRouterConfig
@ -103,9 +101,6 @@ val mainScope = CoroutineScope(mainCoroutineContext)
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
lateinit var appStateHolder: AppStateHolder
/** Router for opening tester menu */ /** Router for opening tester menu */
@Inject @Inject
lateinit var cardSdkOwner: CardSdkOwner lateinit var cardSdkOwner: CardSdkOwner
@ -143,9 +138,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject @Inject
lateinit var userWalletsListManager: UserWalletsListManager lateinit var userWalletsListManager: UserWalletsListManager
@Inject
lateinit var emailSender: EmailSender
@Inject @Inject
@RootAppComponentContext @RootAppComponentContext
internal lateinit var rootComponentContext: AppComponentContext internal lateinit var rootComponentContext: AppComponentContext
@ -174,9 +166,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject @Inject
lateinit var dispatchers: CoroutineDispatcherProvider lateinit var dispatchers: CoroutineDispatcherProvider
@Inject
internal lateinit var uiDependencies: UiDependencies
@Inject @Inject
internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector
@ -192,6 +181,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject @Inject
internal lateinit var urlOpener: UrlOpener internal lateinit var urlOpener: UrlOpener
@Inject
internal lateinit var testerMenuLauncher: TesterMenuLauncher
internal val viewModel: MainViewModel by viewModels() internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode> private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -251,6 +243,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
lifecycle.addObserver(WindowObscurationObserver) lifecycle.addObserver(WindowObscurationObserver)
lifecycle.addObserver(defaultDeviceFlipDetector) lifecycle.addObserver(defaultDeviceFlipDetector)
if (BuildConfig.TESTER_MENU_ENABLED) {
lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver)
}
} }
private fun setRootContent() { private fun setRootContent() {

View file

@ -26,7 +26,6 @@ import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.staking.api.StakingComponent import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent import com.tangem.features.swap.SwapComponent
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
@ -86,7 +85,6 @@ internal class ChildFactory @Inject constructor(
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
private val testerRouter: TesterRouter,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles, private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
) { ) {
@ -132,9 +130,6 @@ internal class ChildFactory @Inject constructor(
componentFactory = welcomeComponentFactory, componentFactory = welcomeComponentFactory,
) )
} }
is AppRoute.TesterMenu -> {
Child.LegacyIntent(testerRouter.getEntryIntent())
}
is AppRoute.WalletSettings -> { is AppRoute.WalletSettings -> {
createComponentChild( createComponentChild(
context = context, context = context,

View file

@ -30,10 +30,6 @@ sealed class AppRoute(val path: String) : Route {
) : AppRoute(path = "/welcome"), RouteBundleParams { ) : AppRoute(path = "/welcome"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer()) override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val INITIAL_INTENT_KEY = "intent"
}
} }
@Serializable @Serializable
@ -177,9 +173,6 @@ sealed class AppRoute(val path: String) : Route {
"/$isInitialReverseOrder", "/$isInitialReverseOrder",
) )
@Serializable
data object TesterMenu : AppRoute(path = "/tester_menu")
@Serializable @Serializable
data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") data object AppCurrencySelector : AppRoute(path = "/app_currency_selector")

View file

@ -5,10 +5,8 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.entity.DetailsItemUM
import com.tangem.features.details.impl.BuildConfig
import com.tangem.features.details.impl.R import com.tangem.features.details.impl.R
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -76,17 +74,6 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
onClick = { router.push(AppRoute.AppSettings) }, onClick = { router.push(AppRoute.AppSettings) },
), ),
).let(::add) ).let(::add)
if (BuildConfig.TESTER_MENU_ENABLED) {
DetailsItemUM.Basic.Item(
id = "tester_menu",
block = BlockUM(
text = stringReference(value = "Tester menu"),
iconRes = R.drawable.ic_alert_24,
onClick = { router.push(AppRoute.TesterMenu) },
),
).let(::add)
}
}.toImmutableList(), }.toImmutableList(),
) )

View file

@ -6,4 +6,8 @@ plugins {
android { android {
namespace = "com.tangem.features.tester.api" namespace = "com.tangem.features.tester.api"
}
dependencies {
api(deps.lifecycle.common.java8)
} }

View file

@ -0,0 +1,14 @@
package com.tangem.features.tester.api
import androidx.lifecycle.DefaultLifecycleObserver
/**
* Interface for launching the tester menu
*
[REDACTED_AUTHOR]
*/
interface TesterMenuLauncher {
/** Observer for detecting shake events and launching the tester menu */
val launchOnShakeObserver: DefaultLifecycleObserver
}

View file

@ -1,14 +0,0 @@
package com.tangem.features.tester.api
import android.content.Intent
/**
* Outer tester feature router
*
[REDACTED_AUTHOR]
*/
interface TesterRouter {
/** Open tester menu */
fun getEntryIntent(): Intent
}

View file

@ -0,0 +1,20 @@
package com.tangem.feature.tester.di
import android.content.Context
import com.tangem.feature.tester.presentation.navigation.DefaultTesterMenuLauncher
import com.tangem.features.tester.api.TesterMenuLauncher
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object TesterMenuLauncherModule {
@Provides
fun provideTesterMenuLauncher(@ApplicationContext context: Context): TesterMenuLauncher {
return DefaultTesterMenuLauncher(context = context)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.feature.tester.di package com.tangem.feature.tester.di
import com.tangem.feature.tester.presentation.navigation.DefaultTesterRouter import com.tangem.feature.tester.presentation.navigation.DefaultTesterRouter
import com.tangem.features.tester.api.TesterRouter import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -14,5 +14,5 @@ internal interface TesterRouterModule {
@Binds @Binds
@ActivityScoped @ActivityScoped
fun bindTesterRouter(defaultTesterRouter: DefaultTesterRouter): TesterRouter fun bindTesterRouter(defaultTesterRouter: DefaultTesterRouter): InnerTesterRouter
} }

View file

@ -32,7 +32,6 @@ import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersSc
import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel
import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen
import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel
import com.tangem.features.tester.api.TesterRouter
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import javax.inject.Inject import javax.inject.Inject
@ -44,9 +43,8 @@ internal class TesterActivity : ComposeActivity() {
@Inject @Inject
override lateinit var uiDependencies: UiDependencies override lateinit var uiDependencies: UiDependencies
/** Router for inner feature navigation */
@Inject @Inject
lateinit var testerRouter: TesterRouter lateinit var innerTesterRouter: InnerTesterRouter
@Inject @Inject
lateinit var appFinisher: AppFinisher lateinit var appFinisher: AppFinisher
@ -54,11 +52,6 @@ internal class TesterActivity : ComposeActivity() {
@Inject @Inject
lateinit var appRouter: AppRouter lateinit var appRouter: AppRouter
private val innerTesterRouter: InnerTesterRouter
get() = requireNotNull(testerRouter as? InnerTesterRouter) {
"TesterRouter must be InnerTesterRouter for tester feature"
}
@Composable @Composable
override fun ScreenContent(modifier: Modifier) { override fun ScreenContent(modifier: Modifier) {
val systemBarsColor = TangemTheme.colors.background.secondary val systemBarsColor = TangemTheme.colors.background.secondary

View file

@ -0,0 +1,53 @@
package com.tangem.feature.tester.presentation.navigation
import android.content.Context
import android.content.Intent
import android.hardware.Sensor
import android.hardware.SensorManager
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.tangem.feature.tester.presentation.TesterActivity
import com.tangem.features.tester.api.TesterMenuLauncher
/**
* Default implementation of [TesterMenuLauncher] that listens for shake events using the device's accelerometer.
* When a shake is detected, it opens the tester menu.
*
* @param context the application context used to access system services
*
[REDACTED_AUTHOR]
*/
internal class DefaultTesterMenuLauncher(private val context: Context) : TesterMenuLauncher {
override val launchOnShakeObserver: DefaultLifecycleObserver by lazy(LazyThreadSafetyMode.NONE) {
createObserver(context)
}
private fun createObserver(context: Context): DefaultLifecycleObserver {
return object : DefaultLifecycleObserver {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
private val shakeEventListener = ShakeEventListener(action = ::openTesterMenu)
override fun onResume(owner: LifecycleOwner) {
accelerometer?.let {
sensorManager.registerListener(
/* listener = */ shakeEventListener,
/* sensor = */ it,
/* samplingPeriodUs = */ SensorManager.SENSOR_DELAY_NORMAL,
)
}
}
override fun onPause(owner: LifecycleOwner) {
sensorManager.unregisterListener(shakeEventListener)
}
}
}
private fun openTesterMenu() {
val intent = Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}

View file

@ -1,31 +1,19 @@
package com.tangem.feature.tester.presentation.navigation package com.tangem.feature.tester.presentation.navigation
import android.content.Context
import android.content.Intent
import androidx.navigation.NavController import androidx.navigation.NavController
import com.tangem.feature.tester.presentation.TesterActivity
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Inject import javax.inject.Inject
/** /**
* Implementation of router for tester feature * Implementation of router for tester feature
* *
* @property context activity context
*
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@ActivityScoped @ActivityScoped
internal class DefaultTesterRouter @Inject constructor( internal class DefaultTesterRouter @Inject constructor() : InnerTesterRouter {
@ActivityContext private val context: Context,
) : InnerTesterRouter {
private var navController: NavController? = null private var navController: NavController? = null
override fun getEntryIntent(): Intent {
return Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
override fun setNavController(navController: NavController) { override fun setNavController(navController: NavController) {
this.navController = navController this.navController = navController
} }

View file

@ -1,14 +1,13 @@
package com.tangem.feature.tester.presentation.navigation package com.tangem.feature.tester.presentation.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
import com.tangem.features.tester.api.TesterRouter
/** /**
* Inner feature router * Inner feature router
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal interface InnerTesterRouter : TesterRouter { internal interface InnerTesterRouter {
/** Set up a navigation controller that bound to tester navigation graph */ /** Set up a navigation controller that bound to tester navigation graph */
fun setNavController(navController: NavController) fun setNavController(navController: NavController)

View file

@ -0,0 +1,51 @@
package com.tangem.feature.tester.presentation.navigation
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import kotlin.math.sqrt
/**
* Listener for device shake events.
*
* This class implements [SensorEventListener] and is used to detect device shaking based on accelerometer data.
* When a shake is detected, the provided action is invoked.
*
* @property action lambda function to be called when a shake is detected
*
[REDACTED_AUTHOR]
*/
internal class ShakeEventListener(private val action: () -> Unit) : SensorEventListener {
private var lastShakeTime = 0L
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type != Sensor.TYPE_ACCELEROMETER) return
val acceleration = calculateAcceleration(event = event)
val currentTime = System.currentTimeMillis()
val currentShakeInterval = currentTime - lastShakeTime
if (acceleration > SHAKE_THRESHOLD && currentShakeInterval > SHAKE_INTERVAL_MS) {
lastShakeTime = currentTime
action()
}
}
private fun calculateAcceleration(event: SensorEvent): Float {
val (x, y, z) = event.toXYZ()
return sqrt(x * x + y * y + z * z) - SensorManager.GRAVITY_EARTH
}
private fun SensorEvent.toXYZ() = Triple(values[0], values[1], values[2])
private companion object {
private const val SHAKE_THRESHOLD: Float = 12f
private const val SHAKE_INTERVAL_MS: Long = 1000
}
}