NTP-66775 | Home sensor manager (#16436)
This commit is contained in:
@@ -430,6 +430,14 @@ class NaviAnalytics private constructor() {
|
||||
fun topNavRevealed() {
|
||||
NaviTrackEvent.trackEvent("home_page_top_nav_expanded")
|
||||
}
|
||||
|
||||
fun onShakeListenerRegister() {
|
||||
NaviTrackEvent.trackEvent("home_page_shake_listener_registered")
|
||||
}
|
||||
|
||||
fun onShakeListenerUnregister() {
|
||||
NaviTrackEvent.trackEvent("home_page_shake_listener_unregistered")
|
||||
}
|
||||
}
|
||||
|
||||
inner class Notifications {
|
||||
|
||||
@@ -9,6 +9,9 @@ package com.naviapp.home.compose.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
@@ -136,6 +139,7 @@ import com.naviapp.home.model.BottomBarTabType
|
||||
import com.naviapp.home.reducer.HpEffects
|
||||
import com.naviapp.home.reducer.HpEvents
|
||||
import com.naviapp.home.usecase.HomePageRedirectionUseCase
|
||||
import com.naviapp.home.usecase.HomeSensorManager
|
||||
import com.naviapp.home.utils.PaymentSdkResultUtil
|
||||
import com.naviapp.home.viewmodel.HomeViewModel
|
||||
import com.naviapp.home.viewmodel.NotificationVM
|
||||
@@ -190,7 +194,8 @@ class HomePageActivity :
|
||||
ActionHandler.ActionOwner,
|
||||
WidgetCallback,
|
||||
PaymentListener,
|
||||
InAppUpdateBridge {
|
||||
InAppUpdateBridge,
|
||||
SensorEventListener {
|
||||
|
||||
@Inject lateinit var userDataUseCase: UploadUserDataUseCase
|
||||
|
||||
@@ -210,6 +215,8 @@ class HomePageActivity :
|
||||
|
||||
@Inject lateinit var screenOverlayEffectHandler: ScreenOverlayEffectHandler
|
||||
|
||||
@Inject lateinit var homeSensorManager: Lazy<HomeSensorManager>
|
||||
|
||||
private val homeVM by lazy { ViewModelProvider(this)[HomeViewModel::class.java] }
|
||||
private val profileVM by lazy { ViewModelProvider(this)[ProfileVM::class.java] }
|
||||
private val notificationVM by lazy { ViewModelProvider(this)[NotificationVM::class.java] }
|
||||
@@ -317,6 +324,7 @@ class HomePageActivity :
|
||||
initResourceManager()
|
||||
TraceManager.endTrace(TC.TRACE_HP_ON_CREATE)
|
||||
TraceManager.endAllTraces()
|
||||
homeSensorManager.get().initSensorManager(this)
|
||||
}
|
||||
|
||||
private fun handleInitialTouchEventState() {
|
||||
@@ -373,6 +381,11 @@ class HomePageActivity :
|
||||
handleStatusBarColor()
|
||||
homeVM.updateCurrentModule(tabId)
|
||||
bottomNavBarVM.triggerTabClickedEvent(tabId = tabId, screenName = screenName)
|
||||
if (tabId == BottomBarTabType.HOME.name) {
|
||||
homeSensorManager.get().registerListener(this, naviAnalyticsEventTracker)
|
||||
} else {
|
||||
homeSensorManager.get().unregisterListener(this, naviAnalyticsEventTracker)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTab(screen: String, bundle: Bundle? = null, isResetCall: Boolean = false) {
|
||||
@@ -474,6 +487,17 @@ class HomePageActivity :
|
||||
bottomNavBarVM.setBottomNudge(false)
|
||||
}
|
||||
}
|
||||
if (
|
||||
sharedVM.getSelectedTabId() == BottomBarTabType.HOME.name &&
|
||||
!homeVM.state.value.profileDrawerState
|
||||
) {
|
||||
homeSensorManager.get().registerListener(this, naviAnalyticsEventTracker)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
homeSensorManager.get().unregisterListener(this, naviAnalyticsEventTracker)
|
||||
}
|
||||
|
||||
override fun postLocationData() {
|
||||
@@ -490,6 +514,12 @@ class HomePageActivity :
|
||||
homeVM.clearVideoView()
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
homeSensorManager.get().onSensorChanged(event, this)
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
paymentInitListener = null
|
||||
|
||||
@@ -102,7 +102,9 @@ fun HomeContentFrame(
|
||||
drawerState.open()
|
||||
activity.fetchProfileItems(isProfileDrawerOpen = true)
|
||||
}
|
||||
activity.homeSensorManager.get().unregisterListener(activity, naviHomeAnalytics)
|
||||
} else {
|
||||
activity.homeSensorManager.get().registerListener(activity, naviHomeAnalytics)
|
||||
if (drawerState.isOpen) drawerState.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright © 2025 by Navi Technologies Limited
|
||||
* * All rights reserved. Strictly confidential
|
||||
*
|
||||
*/
|
||||
|
||||
package com.naviapp.home.usecase
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorManager
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import androidx.appcompat.app.AppCompatActivity.SENSOR_SERVICE
|
||||
import com.navi.base.model.CtaData
|
||||
import com.navi.base.model.LineItem
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.HOMEPAGE_SHAKE_MIN_RAM_THRESHOLD
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.HOMEPAGE_SHAKE_THRESHOLD_GRAVITY
|
||||
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.REGISTER_SHAKE_LISTENER_ON_HOMEPAGE
|
||||
import com.navi.common.utils.Constants.SCREEN_SOURCE
|
||||
import com.navi.common.utils.Constants.SHAKE_ON_HOME_SCREEN
|
||||
import com.navi.common.utils.getTotalRamMemory
|
||||
import com.navi.pay.common.model.view.NaviPayScreenType
|
||||
import com.navi.pay.utils.NAVI_PAY_CTA_URL_PREFIX
|
||||
import com.naviapp.analytics.utils.NaviAnalytics
|
||||
import com.naviapp.common.navigator.NaviDeepLinkNavigator
|
||||
import com.naviapp.home.compose.activity.HomePageActivity
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Manager class responsible for handling device shake detection using the accelerometer sensor. It
|
||||
* triggers QR scanner navigation when the device is shaken. Key shake detection parameters (e.g.,
|
||||
* sensitivity threshold, shake count) are configurable, with some values potentially sourced from
|
||||
* Firebase Remote Config.
|
||||
*/
|
||||
class HomeSensorManager @Inject constructor(@ApplicationContext private val context: Context) {
|
||||
private lateinit var sensorManager: SensorManager
|
||||
private var sensor: Sensor? = null
|
||||
private lateinit var vibrator: Vibrator
|
||||
|
||||
private var lastShakeTimestamp: Long = 0
|
||||
private var previousAcceleration = AccelerationValues()
|
||||
private var consecutiveShakeCount = 0
|
||||
private var isDeviceUpright = false
|
||||
private val totalRamMemory = getTotalRamMemory(context = context)?.toDoubleOrNull() ?: 100.0
|
||||
|
||||
private val gravityThresholdY = 2f
|
||||
|
||||
private val shouldRegisterShake =
|
||||
FirebaseRemoteConfigHelper.getBoolean(
|
||||
key = REGISTER_SHAKE_LISTENER_ON_HOMEPAGE,
|
||||
defaultValue = false,
|
||||
)
|
||||
|
||||
private val shakeThresholdGravity: Float =
|
||||
FirebaseRemoteConfigHelper.getLong(
|
||||
key = HOMEPAGE_SHAKE_THRESHOLD_GRAVITY,
|
||||
defaultValue = 10L,
|
||||
)
|
||||
.toFloat()
|
||||
|
||||
private val minRamThreshold: Double =
|
||||
FirebaseRemoteConfigHelper.getDouble(
|
||||
key = HOMEPAGE_SHAKE_MIN_RAM_THRESHOLD,
|
||||
defaultValue = 4.0,
|
||||
)
|
||||
|
||||
private data class AccelerationValues(var x: Float = 0f, var y: Float = 0f, var z: Float = 0f)
|
||||
|
||||
companion object {
|
||||
private const val SHAKE_COOLDOWN_MS = 400
|
||||
private const val REQUIRED_SHAKE_COUNT = 2
|
||||
private const val SHAKE_RESET_TIMEOUT_MS = 1500
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the sensor manager and accelerometer. Shows a toast message if the device doesn't
|
||||
* have an accelerometer.
|
||||
*
|
||||
* @param activity The HomePageActivity instance
|
||||
*/
|
||||
fun initSensorManager(activity: HomePageActivity) {
|
||||
sensorManager = activity.getSystemService(SENSOR_SERVICE) as SensorManager
|
||||
vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles sensor change events to detect device shakes. Triggers QR scanner navigation after
|
||||
* detecting required number of consecutive shakes.
|
||||
*
|
||||
* @param event The sensor event containing acceleration data
|
||||
* @param activity The HomePageActivity instance
|
||||
*/
|
||||
fun onSensorChanged(event: SensorEvent, activity: HomePageActivity) {
|
||||
if (event.sensor?.type != Sensor.TYPE_ACCELEROMETER) return
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val timeSinceLastShake = currentTime - lastShakeTimestamp
|
||||
|
||||
// Check if enough time has passed since the last shake
|
||||
if (timeSinceLastShake > SHAKE_COOLDOWN_MS) {
|
||||
val currentAcceleration =
|
||||
AccelerationValues(x = event.values[0], y = event.values[1], z = event.values[2])
|
||||
|
||||
// Check if device is upright using gravity thresholds
|
||||
isDeviceUpright =
|
||||
currentAcceleration.let { acc -> acc.y > gravityThresholdY && acc.z > 0 }
|
||||
|
||||
val accelerationMagnitude = calculateAccelerationMagnitude(currentAcceleration)
|
||||
|
||||
when {
|
||||
accelerationMagnitude > shakeThresholdGravity && isDeviceUpright -> {
|
||||
handleShakeDetected(currentTime, activity)
|
||||
}
|
||||
timeSinceLastShake > SHAKE_RESET_TIMEOUT_MS -> {
|
||||
consecutiveShakeCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
previousAcceleration = currentAcceleration
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateAccelerationMagnitude(current: AccelerationValues): Float {
|
||||
return sqrt(
|
||||
(current.x - previousAcceleration.x).pow(2) +
|
||||
(current.y - previousAcceleration.y).pow(2) +
|
||||
(current.z - previousAcceleration.z).pow(2)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleShakeDetected(currentTime: Long, activity: HomePageActivity) {
|
||||
consecutiveShakeCount++
|
||||
lastShakeTimestamp = currentTime
|
||||
|
||||
if (consecutiveShakeCount >= REQUIRED_SHAKE_COUNT) {
|
||||
// Vibrate for 200ms
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(
|
||||
VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE)
|
||||
)
|
||||
} else {
|
||||
vibrator.vibrate(200)
|
||||
}
|
||||
navigateToNaviPayQRScanner(activity)
|
||||
consecutiveShakeCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the sensor listener when the activity resumes.
|
||||
*
|
||||
* @param activity The HomePageActivity instance
|
||||
*/
|
||||
fun registerListener(activity: HomePageActivity, analyticsEventTracker: NaviAnalytics.Home) {
|
||||
if (shouldRegisterShake && sensor != null && totalRamMemory >= minRamThreshold) {
|
||||
analyticsEventTracker.onShakeListenerRegister()
|
||||
sensorManager.registerListener(activity, sensor, SensorManager.SENSOR_DELAY_UI)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the sensor listener when the activity is paused to conserve battery.
|
||||
*
|
||||
* @param activity The HomePageActivity instance
|
||||
*/
|
||||
fun unregisterListener(activity: HomePageActivity, analyticsEventTracker: NaviAnalytics.Home) {
|
||||
if (shouldRegisterShake && sensor != null && totalRamMemory >= minRamThreshold) {
|
||||
analyticsEventTracker.onShakeListenerUnregister()
|
||||
sensorManager.unregisterListener(activity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the Navi Pay QR scanner screen.
|
||||
*
|
||||
* @param activity The HomePageActivity instance
|
||||
*/
|
||||
private fun navigateToNaviPayQRScanner(activity: HomePageActivity) {
|
||||
val url = "$NAVI_PAY_CTA_URL_PREFIX${NaviPayScreenType.NAVI_PAY_QR_SCANNER_SCREEN.name}"
|
||||
|
||||
val ctaData =
|
||||
CtaData(
|
||||
url = url,
|
||||
parameters = listOf(LineItem(key = SCREEN_SOURCE, value = SHAKE_ON_HOME_SCREEN)),
|
||||
)
|
||||
|
||||
NaviDeepLinkNavigator.navigate(activity = activity, ctaData = ctaData, needsResult = true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user