NTP-66775 | Home sensor manager (#16436)

This commit is contained in:
Hitesh Kumar
2025-06-14 20:42:29 +05:30
committed by GitHub
parent 6de53ae439
commit 52da53fa1e
9 changed files with 366 additions and 3 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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)
}
}