Updated on 2026-08-14
This commit is contained in:
parent
02f6e52f46
commit
d0f1e7c6c4
9 changed files with 105 additions and 106 deletions
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue