@@ -17,6 +17,11 @@ import androidx.lifecycle.LifecycleOwner
1717import androidx.lifecycle.compose.collectAsStateWithLifecycle
1818import androidx.lifecycle.lifecycleScope
1919import androidx.lifecycle.viewmodel.compose.viewModel
20+ import com.clhs.score.analytics.AnalyticsEvents
21+ import com.clhs.score.analytics.AnalyticsLogger
22+ import com.clhs.score.analytics.AnalyticsParams
23+ import com.clhs.score.analytics.AnalyticsValues
24+ import com.clhs.score.analytics.FirebaseAnalyticsLogger
2025import com.clhs.score.data.AuthenticatedSession
2126import com.clhs.score.data.BiometricHelper
2227import com.clhs.score.data.GradeCacheStore
@@ -34,7 +39,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
3439import kotlinx.coroutines.launch
3540
3641class MainActivity : androidx.fragment.app.FragmentActivity () {
37- private val checkUpdateChannel = Channel <Unit >(Channel .BUFFERED )
42+ private val checkUpdateChannel = Channel <String >(Channel .BUFFERED )
3843 private val gradeReminderOpenChannel = Channel <Pair <String , String >>(Channel .BUFFERED )
3944 private val pendingScheduleOpen = mutableStateOf(false )
4045 private val isAppLocked = mutableStateOf(false )
@@ -44,10 +49,12 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
4449 private var shouldLockOnInitialReady = false
4550 private var isBiometricPromptShowing = false
4651 private lateinit var sessionStore: SessionStore
52+ private lateinit var analyticsLogger: AnalyticsLogger
4753
4854 override fun onCreate (savedInstanceState : Bundle ? ) {
4955 super .onCreate(savedInstanceState)
5056 sessionStore = SessionStore (applicationContext)
57+ analyticsLogger = FirebaseAnalyticsLogger (applicationContext)
5158 val hasBiometricSession = sessionStore.hasBiometricSession()
5259 isAppLocked.value = savedInstanceState?.getBoolean(KEY_APP_LOCKED , false ) == true &&
5360 hasBiometricSession
@@ -86,8 +93,8 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
8693 val isReady by settingsVm.isReady.collectAsStateWithLifecycle()
8794
8895 LaunchedEffect (Unit ) {
89- checkUpdateChannel.receiveAsFlow().collect {
90- settingsVm.checkUpdate()
96+ checkUpdateChannel.receiveAsFlow().collect { trigger ->
97+ settingsVm.checkUpdate(trigger )
9198 }
9299 }
93100
@@ -156,6 +163,13 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
156163 val session = sessionStore.loadSessionWithPin(pin)
157164 if (session != null ) {
158165 scoreVm.loginWithBiometricSession(session)
166+ analyticsLogger.logEvent(
167+ AnalyticsEvents .BIOMETRIC_UNLOCK_RESULT ,
168+ mapOf (
169+ AnalyticsParams .METHOD to AnalyticsValues .METHOD_PIN ,
170+ AnalyticsParams .RESULT to AnalyticsValues .RESULT_SUCCESS ,
171+ ),
172+ )
159173 isAppLocked.value = false
160174 if (isBiometricInvalidated.value) {
161175 isBiometricInvalidated.value = false
@@ -167,13 +181,20 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
167181 )
168182 }
169183 } else {
184+ analyticsLogger.logEvent(
185+ AnalyticsEvents .BIOMETRIC_UNLOCK_RESULT ,
186+ mapOf (
187+ AnalyticsParams .METHOD to AnalyticsValues .METHOD_PIN ,
188+ AnalyticsParams .RESULT to AnalyticsValues .RESULT_FAILURE ,
189+ ),
190+ )
170191 Toast .makeText(this @MainActivity, " 密碼錯誤" , Toast .LENGTH_SHORT ).show()
171192 }
172193 },
173194 onLogout = {
174195 android.webkit.CookieManager .getInstance().removeAllCookies(null )
175196 android.webkit.CookieManager .getInstance().flush()
176- scoreVm.logout()
197+ scoreVm.logout(AnalyticsValues . SOURCE_LOCK_SCREEN )
177198 clearWidgetScheduleCache()
178199 sessionStore.clearBiometricSession()
179200 settingsVm.setBiometricEnabled(false )
@@ -213,7 +234,7 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
213234 onSetDynamicColor = settingsVm::setDynamicColor,
214235 onSetAmoledBlack = settingsVm::setAmoledBlack,
215236 onSetNotificationsEnabled = settingsVm::setNotificationsEnabled,
216- onCheckUpdate = settingsVm:: checkUpdate,
237+ onCheckUpdate = { settingsVm. checkUpdate() } ,
217238 onDismissUpdateResult = settingsVm::dismissUpdateResult,
218239 onVersionTap = settingsVm::onVersionTap,
219240 onDismissDeveloperToast = settingsVm::dismissDeveloperToast,
@@ -222,6 +243,7 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
222243 onDismissNotificationPrompt = settingsVm::dismissNotificationPrompt,
223244 onExportGrades = { selections -> scoreVm.exportGrades(selections, applicationContext) },
224245 onDismissExportResult = scoreVm::dismissExportResult,
246+ analyticsLogger = analyticsLogger,
225247 onSetBiometricEnabled = { enabled, pin ->
226248 if (enabled && pin != null ) {
227249 val currentSession = scoreVm.getCurrentSession()
@@ -261,14 +283,16 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
261283
262284 private fun handleIntent (intent : Intent ? ) {
263285 if (intent == null ) return
286+ var routeSource: String? = null
264287
265288 val isUpdateTopic = intent.getStringExtra(" from" ) == " /topics/app_updates" ||
266289 intent.getStringExtra(ScoreFirebaseMessagingService .EXTRA_CHECK_UPDATE ) == " true" ||
267290 intent.getStringExtra(" action" ) == " check_update" ||
268291 intent.getBooleanExtra(ScoreFirebaseMessagingService .EXTRA_CHECK_UPDATE , false )
269292
270293 if (isUpdateTopic) {
271- checkUpdateChannel.trySend(Unit )
294+ routeSource = AnalyticsValues .SOURCE_NOTIFICATION_UPDATE
295+ checkUpdateChannel.trySend(AnalyticsValues .TRIGGER_NOTIFICATION )
272296 intent.removeExtra(" from" )
273297 intent.removeExtra(" action" )
274298 intent.removeExtra(ScoreFirebaseMessagingService .EXTRA_CHECK_UPDATE )
@@ -279,6 +303,7 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
279303 false ,
280304 )
281305 if (isGradeReminderOpen) {
306+ routeSource = AnalyticsValues .SOURCE_GRADE_REMINDER
282307 val yearValue = intent.getStringExtra(GradeReminderNotifier .EXTRA_YEAR_VALUE ).orEmpty()
283308 val examValue = intent.getStringExtra(GradeReminderNotifier .EXTRA_EXAM_VALUE ).orEmpty()
284309 if (yearValue.isNotBlank() && examValue.isNotBlank()) {
@@ -291,9 +316,27 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
291316
292317 val data = intent.data
293318 if (data?.scheme == " scoreapp" && data.host == " schedule" ) {
319+ routeSource = AnalyticsValues .SOURCE_WIDGET_SCHEDULE
294320 pendingScheduleOpen.value = true
295321 intent.data = null
296322 }
323+
324+ if (routeSource == null &&
325+ intent.action == Intent .ACTION_MAIN &&
326+ intent.hasCategory(Intent .CATEGORY_LAUNCHER )
327+ ) {
328+ routeSource = AnalyticsValues .SOURCE_LAUNCHER
329+ }
330+
331+ routeSource?.let { source ->
332+ analyticsLogger.logEvent(
333+ AnalyticsEvents .APP_OPEN_ROUTE ,
334+ mapOf (
335+ AnalyticsParams .SOURCE to source,
336+ AnalyticsParams .LOCKED to (isAppLocked.value || sessionStore.hasBiometricSession()),
337+ ),
338+ )
339+ }
297340 }
298341
299342 private fun showBiometricUnlockPrompt (
@@ -312,6 +355,13 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
312355 } catch (e: Exception ) {
313356 if (BiometricHelper .isKeyPermanentlyInvalidated(e)) {
314357 isBiometricInvalidated.value = true
358+ analyticsLogger.logEvent(
359+ AnalyticsEvents .BIOMETRIC_UNLOCK_RESULT ,
360+ mapOf (
361+ AnalyticsParams .METHOD to AnalyticsValues .METHOD_BIOMETRIC ,
362+ AnalyticsParams .RESULT to AnalyticsValues .RESULT_INVALIDATED ,
363+ ),
364+ )
315365 } else {
316366 handleKeyInvalidated(scoreVm, settingsVm, " 初始化安全金鑰失敗,請重新登入" )
317367 }
@@ -323,6 +373,20 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
323373 override fun onAuthenticationError (errorCode : Int , errString : CharSequence ) {
324374 super .onAuthenticationError(errorCode, errString)
325375 isBiometricPromptShowing = false
376+ analyticsLogger.logEvent(
377+ AnalyticsEvents .BIOMETRIC_UNLOCK_RESULT ,
378+ mapOf (
379+ AnalyticsParams .METHOD to AnalyticsValues .METHOD_BIOMETRIC ,
380+ AnalyticsParams .RESULT to if (
381+ errorCode == BiometricPrompt .ERROR_USER_CANCELED ||
382+ errorCode == BiometricPrompt .ERROR_NEGATIVE_BUTTON
383+ ) {
384+ AnalyticsValues .RESULT_CANCELLED
385+ } else {
386+ AnalyticsValues .RESULT_FAILURE
387+ },
388+ ),
389+ )
326390 if (errorCode != BiometricPrompt .ERROR_USER_CANCELED && errorCode != BiometricPrompt .ERROR_NEGATIVE_BUTTON ) {
327391 Toast .makeText(this @MainActivity, " 驗證錯誤: $errString " , Toast .LENGTH_SHORT ).show()
328392 }
@@ -336,8 +400,22 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
336400 val session = sessionStore.loadBiometricSession(decryptCipher)
337401 if (session != null ) {
338402 scoreVm.loginWithBiometricSession(session)
403+ analyticsLogger.logEvent(
404+ AnalyticsEvents .BIOMETRIC_UNLOCK_RESULT ,
405+ mapOf (
406+ AnalyticsParams .METHOD to AnalyticsValues .METHOD_BIOMETRIC ,
407+ AnalyticsParams .RESULT to AnalyticsValues .RESULT_SUCCESS ,
408+ ),
409+ )
339410 isAppLocked.value = false
340411 } else {
412+ analyticsLogger.logEvent(
413+ AnalyticsEvents .BIOMETRIC_UNLOCK_RESULT ,
414+ mapOf (
415+ AnalyticsParams .METHOD to AnalyticsValues .METHOD_BIOMETRIC ,
416+ AnalyticsParams .RESULT to AnalyticsValues .RESULT_FAILURE ,
417+ ),
418+ )
341419 handleKeyInvalidated(scoreVm, settingsVm, " 解析登入資訊失敗,請重新登入" )
342420 }
343421 }
@@ -357,6 +435,13 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
357435 biometricPrompt.authenticate(promptInfo, BiometricPrompt .CryptoObject (cipher))
358436 }.onFailure { error ->
359437 isBiometricPromptShowing = false
438+ analyticsLogger.logEvent(
439+ AnalyticsEvents .BIOMETRIC_UNLOCK_RESULT ,
440+ mapOf (
441+ AnalyticsParams .METHOD to AnalyticsValues .METHOD_BIOMETRIC ,
442+ AnalyticsParams .RESULT to AnalyticsValues .RESULT_FAILURE ,
443+ ),
444+ )
360445 Toast .makeText(this , " 啟動生物識別失敗: ${error.message ? : " 未知錯誤" } " , Toast .LENGTH_SHORT ).show()
361446 }
362447 }
@@ -442,7 +527,7 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
442527 sessionStore.clearBiometricSession()
443528 sessionStore.clearNormalSession()
444529 settingsVm.setBiometricEnabled(false )
445- scoreVm.logout()
530+ scoreVm.logout(AnalyticsValues . SOURCE_LOCK_SCREEN )
446531 clearWidgetScheduleCache()
447532 isAppLocked.value = false
448533 }
0 commit comments