diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 3a4577ab02..1fabef9308 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -34,9 +34,7 @@ import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.deeplink.DEEPLINK_KEY import com.tangem.core.deeplink.DeepLinksRegistry 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.ui.UiDependencies import com.tangem.data.balancehiding.DefaultDeviceFlipDetector import com.tangem.data.card.sdk.CardSdkOwner 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.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent 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.google.GoogleServicesHelper 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.WalletConnectLinkIntentHandler import com.tangem.tap.features.main.MainViewModel -import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.configurator.AppRouterConfig @@ -103,9 +101,6 @@ val mainScope = CoroutineScope(mainCoroutineContext) @AndroidEntryPoint class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { - @Inject - lateinit var appStateHolder: AppStateHolder - /** Router for opening tester menu */ @Inject lateinit var cardSdkOwner: CardSdkOwner @@ -143,9 +138,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var userWalletsListManager: UserWalletsListManager - @Inject - lateinit var emailSender: EmailSender - @Inject @RootAppComponentContext internal lateinit var rootComponentContext: AppComponentContext @@ -174,9 +166,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var dispatchers: CoroutineDispatcherProvider - @Inject - internal lateinit var uiDependencies: UiDependencies - @Inject internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector @@ -192,6 +181,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var urlOpener: UrlOpener + @Inject + internal lateinit var testerMenuLauncher: TesterMenuLauncher + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -251,6 +243,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { lifecycle.addObserver(WindowObscurationObserver) lifecycle.addObserver(defaultDeviceFlipDetector) + + if (BuildConfig.TESTER_MENU_ENABLED) { + lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver) + } } private fun setRootContent() { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 825a388842..2e75544715 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -26,7 +26,6 @@ import com.tangem.features.send.v2.api.NFTSendComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.swap.SwapComponent -import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent @@ -86,7 +85,6 @@ internal class ChildFactory @Inject constructor( private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, - private val testerRouter: TesterRouter, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, ) { @@ -132,9 +130,6 @@ internal class ChildFactory @Inject constructor( componentFactory = welcomeComponentFactory, ) } - is AppRoute.TesterMenu -> { - Child.LegacyIntent(testerRouter.getEntryIntent()) - } is AppRoute.WalletSettings -> { createComponentChild( context = context, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 410b01ee6d..9d9c95165f 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -30,10 +30,6 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/welcome"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val INITIAL_INTENT_KEY = "intent" - } } @Serializable @@ -177,9 +173,6 @@ sealed class AppRoute(val path: String) : Route { "/$isInitialReverseOrder", ) - @Serializable - data object TesterMenu : AppRoute(path = "/tester_menu") - @Serializable data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 93eee5750e..d7aad6e31a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -5,10 +5,8 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.details.entity.DetailsItemUM -import com.tangem.features.details.impl.BuildConfig import com.tangem.features.details.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -76,17 +74,6 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { onClick = { router.push(AppRoute.AppSettings) }, ), ).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(), ) diff --git a/features/tester/api/build.gradle.kts b/features/tester/api/build.gradle.kts index 0456459cad..8022f8d874 100644 --- a/features/tester/api/build.gradle.kts +++ b/features/tester/api/build.gradle.kts @@ -6,4 +6,8 @@ plugins { android { namespace = "com.tangem.features.tester.api" +} + +dependencies { + api(deps.lifecycle.common.java8) } \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt new file mode 100644 index 0000000000..a6338eaa73 --- /dev/null +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt @@ -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 +} \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt deleted file mode 100644 index 7e647c8323..0000000000 --- a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt new file mode 100644 index 0000000000..859469177f --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt @@ -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) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt index 433341f7f7..ec8dd8d1cd 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt @@ -1,7 +1,7 @@ package com.tangem.feature.tester.di 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.Module import dagger.hilt.InstallIn @@ -14,5 +14,5 @@ internal interface TesterRouterModule { @Binds @ActivityScoped - fun bindTesterRouter(defaultTesterRouter: DefaultTesterRouter): TesterRouter + fun bindTesterRouter(defaultTesterRouter: DefaultTesterRouter): InnerTesterRouter } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 60ea5abd62..3c28e87b46 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -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.testpush.ui.TestPushScreen import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel -import com.tangem.features.tester.api.TesterRouter import dagger.hilt.android.AndroidEntryPoint import kotlinx.collections.immutable.persistentSetOf import javax.inject.Inject @@ -44,9 +43,8 @@ internal class TesterActivity : ComposeActivity() { @Inject override lateinit var uiDependencies: UiDependencies - /** Router for inner feature navigation */ @Inject - lateinit var testerRouter: TesterRouter + lateinit var innerTesterRouter: InnerTesterRouter @Inject lateinit var appFinisher: AppFinisher @@ -54,11 +52,6 @@ internal class TesterActivity : ComposeActivity() { @Inject lateinit var appRouter: AppRouter - private val innerTesterRouter: InnerTesterRouter - get() = requireNotNull(testerRouter as? InnerTesterRouter) { - "TesterRouter must be InnerTesterRouter for tester feature" - } - @Composable override fun ScreenContent(modifier: Modifier) { val systemBarsColor = TangemTheme.colors.background.secondary diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt new file mode 100644 index 0000000000..fbd8a860ff --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt @@ -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) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt index 0d3c250e91..62bbfefebb 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt @@ -1,31 +1,19 @@ package com.tangem.feature.tester.presentation.navigation -import android.content.Context -import android.content.Intent import androidx.navigation.NavController -import com.tangem.feature.tester.presentation.TesterActivity -import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject /** * Implementation of router for tester feature * - * @property context activity context - * [REDACTED_AUTHOR] */ @ActivityScoped -internal class DefaultTesterRouter @Inject constructor( - @ActivityContext private val context: Context, -) : InnerTesterRouter { +internal class DefaultTesterRouter @Inject constructor() : InnerTesterRouter { 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) { this.navController = navController } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt index 0d5468767f..7135c2e34c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt @@ -1,14 +1,13 @@ package com.tangem.feature.tester.presentation.navigation import androidx.navigation.NavController -import com.tangem.features.tester.api.TesterRouter /** * Inner feature router * [REDACTED_AUTHOR] */ -internal interface InnerTesterRouter : TesterRouter { +internal interface InnerTesterRouter { /** Set up a navigation controller that bound to tester navigation graph */ fun setNavController(navController: NavController) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt new file mode 100644 index 0000000000..133628c9ac --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt @@ -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 + } +} \ No newline at end of file