diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 7be16eab0a..526a308532 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Build import android.os.Bundle +import android.view.KeyEvent import android.view.MotionEvent import android.view.WindowManager import androidx.activity.SystemBarStyle @@ -241,7 +242,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { lifecycle.addObserver(defaultDeviceFlipDetector) if (BuildConfig.TESTER_MENU_ENABLED) { - lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver) + lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver) } } @@ -417,10 +418,17 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { override fun dispatchTouchEvent(event: MotionEvent): Boolean { val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler) - return if (result) super.dispatchTouchEvent(event) else false } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + return if (BuildConfig.TESTER_MENU_ENABLED) { + testerMenuLauncher.launchOnKeyEventObserver.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) + } else { + super.dispatchKeyEvent(event) + } + } + private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { val backStack = appRouterConfig.stack ?: emptyList() // TODO move inital navigation to navigation component ([REDACTED_JIRA]) diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt new file mode 100644 index 0000000000..d431c7cd85 --- /dev/null +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt @@ -0,0 +1,13 @@ +package com.tangem.features.tester.api + +import android.view.KeyEvent +import androidx.lifecycle.DefaultLifecycleObserver + +/** + * Interface for observing to key events in a lifecycle-aware manner. + * Implementations should handle key events and return true if the event was consumed. + */ +interface KeyEventObserver : DefaultLifecycleObserver { + + fun dispatchKeyEvent(event: KeyEvent): Boolean +} \ 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 index a6338eaa73..b16f8b366d 100644 --- 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 @@ -1,7 +1,5 @@ package com.tangem.features.tester.api -import androidx.lifecycle.DefaultLifecycleObserver - /** * Interface for launching the tester menu * @@ -9,6 +7,9 @@ import androidx.lifecycle.DefaultLifecycleObserver */ interface TesterMenuLauncher { - /** Observer for detecting shake events and launching the tester menu */ - val launchOnShakeObserver: DefaultLifecycleObserver + /** + * Observer for key events to open the tester menu. + * Implementations should handle key events and return true if the event was consumed. + */ + val launchOnKeyEventObserver: KeyEventObserver } \ 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 index 859469177f..48e8ef8bfd 100644 --- 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 @@ -6,15 +6,15 @@ 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 +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext @Module -@InstallIn(SingletonComponent::class) +@InstallIn(ActivityComponent::class) internal object TesterMenuLauncherModule { @Provides - fun provideTesterMenuLauncher(@ApplicationContext context: Context): TesterMenuLauncher { - return DefaultTesterMenuLauncher(context = context) + fun provideTesterMenuLauncher(@ActivityContext context: Context): TesterMenuLauncher { + return DefaultTesterMenuLauncher(context) } } \ 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 3c28e87b46..8cd7fd94f1 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 @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -92,6 +93,7 @@ internal class TesterActivity : ComposeActivity() { innerTesterRouter.open(route) }, ), + modifier = Modifier.systemBarsPadding(), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt index da2e67b08d..b369752bd1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt @@ -31,11 +31,11 @@ import kotlinx.collections.immutable.toImmutableList */ @OptIn(ExperimentalFoundationApi::class) @Composable -internal fun TesterMenuScreen(state: TesterMenuUM) { +internal fun TesterMenuScreen(state: TesterMenuUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.onBackClick) LazyColumn( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(TangemTheme.colors.background.primary), ) { 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 index fbd8a860ff..1c1fc90ee1 100644 --- 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 @@ -1,53 +1,15 @@ 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. + * Default implementation of [TesterMenuLauncher] that listens for double-press events + * of the volume down button. When a double press is detected, it opens the tester menu. * - * @param context the application context used to access system services - * -[REDACTED_AUTHOR] + * @param context the application context used to launch the tester menu */ 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) - } + override val launchOnKeyEventObserver = VolumeButtonDoublePressObserver(context) } \ No newline at end of file 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 deleted file mode 100644 index 133628c9ac..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt +++ /dev/null @@ -1,51 +0,0 @@ -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 diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt new file mode 100644 index 0000000000..c683121653 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt @@ -0,0 +1,64 @@ + +package com.tangem.feature.tester.presentation.navigation + +import android.content.Context +import android.content.Intent +import android.os.SystemClock +import android.view.KeyEvent +import androidx.lifecycle.LifecycleOwner +import com.tangem.feature.tester.presentation.TesterActivity +import com.tangem.features.tester.api.KeyEventObserver + +/** + * A key event observer that listens for volume down button presses to open the tester menu. + * It requires two consecutive volume down presses within a specified interval to trigger the menu. + */ +internal class VolumeButtonDoublePressObserver(private val context: Context) : KeyEventObserver { + + private var lastVolumeDownTime = 0L + private var volumeDownCount = 0 + private var isReady = false + + override fun onResume(owner: LifecycleOwner) { + isReady = true + } + + override fun onPause(owner: LifecycleOwner) { + isReady = false + } + + /** + * Returns true if the tester menu was opened. + */ + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (!isReady) return false + + if (event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + val now = SystemClock.elapsedRealtime() + volumeDownCount = if (now - lastVolumeDownTime <= DOUBLE_PRESS_INTERVAL_MS) { + volumeDownCount + 1 + } else { + 1 + } + lastVolumeDownTime = now + + if (volumeDownCount == REQUIRED_PRESS_COUNT) { + volumeDownCount = 0 + openTesterMenu(context) + return true + } + } + + return false + } + + private fun openTesterMenu(context: Context) { + val intent = Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + companion object { + private const val DOUBLE_PRESS_INTERVAL_MS = 300L + private const val REQUIRED_PRESS_COUNT = 2 + } +} \ No newline at end of file