• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.healthconnect.controller.home
17 
18 import android.content.Context
19 import android.content.Intent
20 import android.icu.text.MessageFormat
21 import android.os.Bundle
22 import android.provider.Settings.ACTION_SECURITY_SETTINGS
23 import android.view.View
24 import android.widget.Toast
25 import androidx.core.os.bundleOf
26 import androidx.fragment.app.activityViewModels
27 import androidx.fragment.app.viewModels
28 import androidx.navigation.fragment.findNavController
29 import androidx.preference.Preference
30 import androidx.preference.PreferenceGroup
31 import com.android.healthconnect.controller.R
32 import com.android.healthconnect.controller.data.alldata.AllDataFragment.Companion.IS_BROWSE_MEDICAL_DATA_SCREEN
33 import com.android.healthconnect.controller.exportimport.api.ExportStatusViewModel
34 import com.android.healthconnect.controller.exportimport.api.ScheduledExportUiState
35 import com.android.healthconnect.controller.exportimport.api.ScheduledExportUiStatus
36 import com.android.healthconnect.controller.home.HomeViewModel.LockScreenBannerState
37 import com.android.healthconnect.controller.migration.MigrationActivity.Companion.maybeShowWhatsNewDialog
38 import com.android.healthconnect.controller.migration.MigrationViewModel
39 import com.android.healthconnect.controller.migration.api.MigrationRestoreState
40 import com.android.healthconnect.controller.migration.api.MigrationRestoreState.DataRestoreUiState
41 import com.android.healthconnect.controller.migration.api.MigrationRestoreState.MigrationUiState
42 import com.android.healthconnect.controller.recentaccess.RecentAccessEntry
43 import com.android.healthconnect.controller.recentaccess.RecentAccessPreference
44 import com.android.healthconnect.controller.recentaccess.RecentAccessViewModel
45 import com.android.healthconnect.controller.recentaccess.RecentAccessViewModel.RecentAccessState
46 import com.android.healthconnect.controller.shared.Constants
47 import com.android.healthconnect.controller.shared.Constants.LOCK_SCREEN_BANNER_SEEN_FITNESS
48 import com.android.healthconnect.controller.shared.Constants.LOCK_SCREEN_BANNER_SEEN_MEDICAL
49 import com.android.healthconnect.controller.shared.Constants.MIGRATION_NOT_COMPLETE_DIALOG_SEEN
50 import com.android.healthconnect.controller.shared.Constants.USER_ACTIVITY_TRACKER
51 import com.android.healthconnect.controller.shared.app.AppMetadata
52 import com.android.healthconnect.controller.shared.app.AppPermissionsType
53 import com.android.healthconnect.controller.shared.app.ConnectedAppMetadata
54 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus
55 import com.android.healthconnect.controller.shared.dialog.AlertDialogBuilder
56 import com.android.healthconnect.controller.shared.preference.HealthBannerPreference
57 import com.android.healthconnect.controller.shared.preference.HealthButtonPreference
58 import com.android.healthconnect.controller.shared.preference.HealthPreference
59 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
60 import com.android.healthconnect.controller.utils.AttributeResolver
61 import com.android.healthconnect.controller.utils.DeviceInfoUtils
62 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
63 import com.android.healthconnect.controller.utils.TimeSource
64 import com.android.healthconnect.controller.utils.formatRecentAccessTime
65 import com.android.healthconnect.controller.utils.logging.DataRestoreElement
66 import com.android.healthconnect.controller.utils.logging.HomePageElement
67 import com.android.healthconnect.controller.utils.logging.MigrationElement
68 import com.android.healthconnect.controller.utils.logging.PageName
69 import com.android.healthconnect.controller.utils.logging.RecentAccessElement
70 import com.android.healthconnect.controller.utils.logging.UnknownGenericElement
71 import com.android.healthconnect.controller.utils.pref
72 import com.android.healthfitness.flags.AconfigFlagHelper.isPersonalHealthRecordEnabled
73 import com.android.healthfitness.flags.Flags.onboarding
74 import com.android.healthfitness.flags.Flags.personalHealthRecordLockScreenBanner
75 import com.android.settingslib.widget.BannerMessagePreference
76 import com.android.settingslib.widget.BannerMessagePreferenceGroup
77 import com.android.settingslib.widget.SettingsThemeHelper
78 import com.android.settingslib.widget.ZeroStatePreference
79 import dagger.hilt.android.AndroidEntryPoint
80 import java.time.Instant
81 import javax.inject.Inject
82 
83 /** Home fragment for Health Connect. */
84 @AndroidEntryPoint(HealthPreferenceFragment::class)
85 class HomeFragment : Hilt_HomeFragment() {
86 
87     companion object {
88         private const val BANNER_GROUP = "banner_group"
89         private const val NO_RECENT_ACCESS = "no_recent_access"
90         private const val DATA_AND_ACCESS_PREFERENCE_KEY = "data_and_access"
91         private const val RECENT_ACCESS_PREFERENCE_KEY = "recent_access"
92         private const val CONNECTED_APPS_PREFERENCE_KEY = "connected_apps"
93         private const val MIGRATION_BANNER_PREFERENCE_KEY = "migration_banner"
94         private const val DATA_RESTORE_BANNER_PREFERENCE_KEY = "data_restore_banner"
95         private const val MANAGE_DATA_PREFERENCE_KEY = "manage_data"
96         private const val BROWSE_MEDICAL_DATA_PREFERENCE_KEY = "medical_data"
97         private const val EXPORT_ERROR_BANNER_PREFERENCE_KEY = "export_error_banner"
98         private const val START_USING_HC_BANNER_KEY = "start_using_hc"
99         private const val CONNECT_MORE_APPS_BANNER_KEY = "connect_more_apps"
100         private const val SEE_COMPATIBLE_APPS_BANNER_KEY = "see_compatible_apps"
101         private const val LOCK_SCREEN_BANNER_KEY = "lock_screen_banner"
102         private val securitySettingsIntent = Intent(ACTION_SECURITY_SETTINGS)
103 
104         @JvmStatic fun newInstance() = HomeFragment()
105     }
106 
107     init {
108         this.setPageName(PageName.HOME_PAGE)
109     }
110 
111     @Inject lateinit var timeSource: TimeSource
112     @Inject lateinit var deviceInfoUtils: DeviceInfoUtils
113 
114     private val recentAccessViewModel: RecentAccessViewModel by viewModels()
115     private val homeViewModel: HomeViewModel by viewModels()
116     private val migrationViewModel: MigrationViewModel by activityViewModels()
117     private val exportStatusViewModel: ExportStatusViewModel by activityViewModels()
118 
119     private val noRecentAccessPreference: ZeroStatePreference by pref(NO_RECENT_ACCESS)
120 
121     private val dataAndAccessPreference: HealthPreference by pref(DATA_AND_ACCESS_PREFERENCE_KEY)
122 
123     private val recentAccessPreferenceGroup: PreferenceGroup by pref(RECENT_ACCESS_PREFERENCE_KEY)
124 
125     private val appPermissionsPreference: HealthPreference by pref(CONNECTED_APPS_PREFERENCE_KEY)
126 
127     private val manageDataPreference: HealthPreference by pref(MANAGE_DATA_PREFERENCE_KEY)
128 
129     private val browseMedicalDataPreference: HealthPreference by
130         pref(BROWSE_MEDICAL_DATA_PREFERENCE_KEY)
131 
132     private val dateFormatter: LocalDateTimeFormatter by lazy {
133         LocalDateTimeFormatter(requireContext())
134     }
135 
136     private val isLockScreenBannerAvailable: Boolean by lazy {
137         personalHealthRecordLockScreenBanner() &&
138             deviceInfoUtils.isIntentHandlerAvailable(requireContext(), securitySettingsIntent)
139     }
140 
141     private val bannerGroup: BannerMessagePreferenceGroup by pref(BANNER_GROUP)
142     private lateinit var migrationBannerSummary: String
143 
144     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
145         super.onCreatePreferences(savedInstanceState, rootKey)
146         setPreferencesFromResource(R.xml.home_preference_screen, rootKey)
147         dataAndAccessPreference.logName = HomePageElement.DATA_AND_ACCESS_BUTTON
148         dataAndAccessPreference.summary = getString(R.string.browse_data_subtitle)
149         dataAndAccessPreference.setOnPreferenceClickListener {
150             findNavController().navigate(R.id.action_homeFragment_to_healthDataCategoriesFragment)
151             true
152         }
153         appPermissionsPreference.logName = HomePageElement.APP_PERMISSIONS_BUTTON
154         appPermissionsPreference.setOnPreferenceClickListener {
155             findNavController().navigate(R.id.action_homeFragment_to_connectedAppsFragment)
156             true
157         }
158 
159         manageDataPreference.logName = HomePageElement.MANAGE_DATA_BUTTON
160         manageDataPreference.setOnPreferenceClickListener {
161             findNavController().navigate(R.id.action_homeFragment_to_manageDataFragment)
162             true
163         }
164         manageDataPreference.summary = getString(R.string.manage_data_summary)
165 
166         if (isPersonalHealthRecordEnabled()) {
167             browseMedicalDataPreference.setOnPreferenceClickListener {
168                 findNavController()
169                     .navigate(
170                         R.id.action_homeFragment_to_medicalDataFragment,
171                         bundleOf(IS_BROWSE_MEDICAL_DATA_SCREEN to true),
172                     )
173                 true
174             }
175             browseMedicalDataPreference.isVisible = false
176             browseMedicalDataPreference.logName = HomePageElement.BROWSE_HEALTH_RECORDS_BUTTON
177         } else {
178             preferenceScreen.removePreferenceRecursively(BROWSE_MEDICAL_DATA_PREFERENCE_KEY)
179         }
180 
181         migrationBannerSummary = getString(R.string.resume_migration_banner_description_fallback)
182     }
183 
184     override fun onResume() {
185         super.onResume()
186         recentAccessViewModel.loadRecentAccessApps(maxNumEntries = 3)
187         homeViewModel.loadConnectedApps()
188         exportStatusViewModel.loadScheduledExportStatus()
189         if (isPersonalHealthRecordEnabled()) {
190             homeViewModel.loadHasAnyMedicalData()
191             if (isLockScreenBannerAvailable) {
192                 homeViewModel.loadShouldShowLockScreenBanner(
193                     getSharedPreference(),
194                     requireContext(),
195                 )
196             }
197         }
198     }
199 
200     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
201         super.onViewCreated(view, savedInstanceState)
202 
203         recentAccessViewModel.loadRecentAccessApps(maxNumEntries = 3)
204         recentAccessViewModel.recentAccessApps.observe(viewLifecycleOwner) { recentAppsState ->
205             when (recentAppsState) {
206                 is RecentAccessState.WithData -> {
207                     updateRecentApps(recentAppsState.recentAccessEntries)
208                 }
209                 is RecentAccessState.Error -> {
210                     updateRecentAppsWithError()
211                 }
212                 else -> {
213                     updateRecentApps(emptyList())
214                 }
215             }
216         }
217         homeViewModel.connectedApps.observe(viewLifecycleOwner) { connectedApps ->
218             updateConnectedApps(connectedApps)
219             updateOnboardingBanner(connectedApps)
220         }
221         migrationViewModel.migrationState.observe(viewLifecycleOwner) { migrationState ->
222             when (migrationState) {
223                 is MigrationViewModel.MigrationFragmentState.WithData -> {
224                     showMigrationState(migrationState.migrationRestoreState)
225                 }
226                 else -> {
227                     // do nothing
228                 }
229             }
230         }
231         exportStatusViewModel.storedScheduledExportStatus.observe(viewLifecycleOwner) {
232             scheduledExportUiStatus ->
233             when (scheduledExportUiStatus) {
234                 is ScheduledExportUiStatus.WithData -> {
235                     maybeShowExportErrorBanner(scheduledExportUiStatus.scheduledExportUiState)
236                 }
237                 else -> {
238                     // do nothing
239                 }
240             }
241         }
242 
243         if (isPersonalHealthRecordEnabled()) {
244             homeViewModel.loadHasAnyMedicalData()
245             homeViewModel.hasAnyMedicalData.observe(viewLifecycleOwner) { hasAnyMedicalData ->
246                 browseMedicalDataPreference.isVisible = hasAnyMedicalData ?: false
247             }
248             if (isLockScreenBannerAvailable) {
249                 val sharedPreference = getSharedPreference()
250                 homeViewModel.loadShouldShowLockScreenBanner(sharedPreference, requireContext())
251                 homeViewModel.showLockScreenBanner.observe(viewLifecycleOwner) { bannerState ->
252                     if (bannerState is LockScreenBannerState.ShowBanner) {
253                         addLockScreenBanner(bannerState)
254                     } else {
255                         removeLockScreenBanner()
256                     }
257                 }
258             }
259         }
260     }
261 
262     private fun isLockScreenBannerAlreadyAdded(): Boolean {
263         return bannerGroup.findPreference<HealthBannerPreference>(LOCK_SCREEN_BANNER_KEY) != null
264     }
265 
266     private fun addLockScreenBanner(bannerState: LockScreenBannerState.ShowBanner) {
267         if (!isLockScreenBannerAlreadyAdded()) {
268             bannerGroup.addPreference(getLockScreenBanner(bannerState))
269         }
270     }
271 
272     private fun removeLockScreenBanner() {
273         bannerGroup.removePreferenceRecursively(LOCK_SCREEN_BANNER_KEY)
274     }
275 
276     private fun showMigrationState(migrationRestoreState: MigrationRestoreState) {
277         bannerGroup.removePreferenceRecursively(MIGRATION_BANNER_PREFERENCE_KEY)
278         bannerGroup.removePreferenceRecursively(DATA_RESTORE_BANNER_PREFERENCE_KEY)
279 
280         val (migrationUiState, dataRestoreUiState, dataRestoreError) = migrationRestoreState
281 
282         if (
283             dataRestoreUiState == DataRestoreUiState.PENDING &&
284                 dataRestoreError == MigrationRestoreState.DataRestoreUiError.ERROR_VERSION_DIFF
285         ) {
286             bannerGroup.addPreference(getDataRestorePendingBanner())
287         } else if (
288             migrationUiState in
289                 listOf(
290                     MigrationUiState.ALLOWED_PAUSED,
291                     MigrationUiState.ALLOWED_NOT_STARTED,
292                     MigrationUiState.MODULE_UPGRADE_REQUIRED,
293                     MigrationUiState.APP_UPGRADE_REQUIRED,
294                 )
295         ) {
296             bannerGroup.addPreference(getMigrationBanner())
297         } else if (migrationUiState == MigrationUiState.COMPLETE) {
298             maybeShowWhatsNewDialog(requireContext())
299         } else if (migrationUiState == MigrationUiState.ALLOWED_ERROR) {
300             maybeShowMigrationNotCompleteDialog()
301         }
302     }
303 
304     private fun maybeShowMigrationNotCompleteDialog() {
305         val sharedPreference = getSharedPreference()
306         val dialogSeen = sharedPreference.getBoolean(MIGRATION_NOT_COMPLETE_DIALOG_SEEN, false)
307 
308         if (!dialogSeen) {
309             AlertDialogBuilder(this, MigrationElement.MIGRATION_NOT_COMPLETE_DIALOG_CONTAINER)
310                 .setTitle(R.string.migration_not_complete_dialog_title)
311                 .setMessage(R.string.migration_not_complete_dialog_content)
312                 .setCancelable(false)
313                 .setNegativeButton(
314                     R.string.migration_whats_new_dialog_button,
315                     MigrationElement.MIGRATION_NOT_COMPLETE_DIALOG_BUTTON,
316                 ) { _, _ ->
317                     sharedPreference.edit().apply {
318                         putBoolean(MIGRATION_NOT_COMPLETE_DIALOG_SEEN, true)
319                         apply()
320                     }
321                 }
322                 .create()
323                 .show()
324         }
325     }
326 
327     // region Banners
328     private fun maybeShowExportErrorBanner(scheduledExportUiState: ScheduledExportUiState) {
329         if (bannerGroup.findPreference<Preference>(EXPORT_ERROR_BANNER_PREFERENCE_KEY) != null) {
330             bannerGroup.removePreferenceRecursively(EXPORT_ERROR_BANNER_PREFERENCE_KEY)
331         }
332         if (
333             scheduledExportUiState.dataExportError !=
334                 ScheduledExportUiState.DataExportError.DATA_EXPORT_ERROR_NONE
335         ) {
336             scheduledExportUiState.lastFailedExportTime?.let {
337                 bannerGroup.addPreference(getExportFileAccessErrorBanner(it))
338             }
339         }
340     }
341 
342     private fun getExportFileAccessErrorBanner(
343         lastFailedExportTime: Instant
344     ): HealthBannerPreference {
345         return HealthBannerPreference(requireContext(), HomePageElement.EXPORT_ERROR_BANNER).also {
346             banner ->
347             banner.setPositiveButton(
348                 text = getString(R.string.export_file_access_error_banner_button),
349                 logName = HomePageElement.EXPORT_ERROR_BANNER_BUTTON,
350             ) {
351                 findNavController().navigate(R.id.action_homeFragment_to_exportSetupActivity)
352             }
353 
354             banner.title = getString(R.string.export_file_access_error_banner_title)
355             banner.summary =
356                 getString(
357                     R.string.export_file_access_error_banner_summary,
358                     dateFormatter.formatLongDate(lastFailedExportTime),
359                 )
360             banner.icon =
361                 AttributeResolver.getNullableDrawable(requireContext(), R.attr.warningIcon)
362             banner.key = EXPORT_ERROR_BANNER_PREFERENCE_KEY
363         }
364     }
365 
366     private fun getMigrationBanner(): HealthBannerPreference {
367         return HealthBannerPreference(requireContext(), MigrationElement.MIGRATION_RESUME_BANNER)
368             .also { banner ->
369                 banner.setPositiveButton(
370                     text = getString(R.string.resume_migration_banner_button),
371                     logName = MigrationElement.MIGRATION_RESUME_BANNER_BUTTON,
372                 ) {
373                     findNavController().navigate(R.id.action_homeFragment_to_migrationActivity)
374                 }
375 
376                 banner.icon =
377                     AttributeResolver.getNullableDrawable(
378                         requireContext(),
379                         R.attr.settingsAlertIcon,
380                     )
381                 banner.title = getString(R.string.resume_migration_banner_title)
382                 banner.summary = migrationBannerSummary
383                 banner.key = MIGRATION_BANNER_PREFERENCE_KEY
384             }
385     }
386 
387     private fun getDataRestorePendingBanner(): HealthBannerPreference {
388         return HealthBannerPreference(requireContext(), DataRestoreElement.RESTORE_PENDING_BANNER)
389             .also { banner ->
390                 banner.setPositiveButton(
391                     text = getString(R.string.data_restore_pending_banner_button),
392                     logName = DataRestoreElement.RESTORE_PENDING_BANNER_UPDATE_BUTTON,
393                 ) {
394                     findNavController().navigate(R.id.action_homeFragment_to_systemUpdateActivity)
395                 }
396 
397                 banner.icon =
398                     AttributeResolver.getNullableDrawable(requireContext(), R.attr.updateNeededIcon)
399                 banner.title = getString(R.string.data_restore_pending_banner_title)
400                 banner.summary = getString(R.string.data_restore_pending_banner_content)
401                 banner.key = DATA_RESTORE_BANNER_PREFERENCE_KEY
402             }
403     }
404 
405     private fun getStartUsingHealthConnectBanner(): HealthBannerPreference {
406         return HealthBannerPreference(requireContext(), UnknownGenericElement.UNKNOWN_BANNER)
407             .also { banner ->
408                 banner.title = resources.getString(R.string.start_using_hc_banner_title)
409                 banner.summary = resources.getString(R.string.start_using_hc_banner_content)
410                 banner.icon =
411                     AttributeResolver.getNullableDrawable(
412                         requireContext(),
413                         R.attr.healthConnectIcon,
414                     )
415                 banner.key = START_USING_HC_BANNER_KEY
416 
417                 banner.setPositiveButton(
418                     text = getString(R.string.start_using_hc_set_up_button),
419                     logName = UnknownGenericElement.UNKNOWN_BANNER_BUTTON,
420                 ) {
421                     findNavController().navigate(R.id.action_homeFragment_to_connectedAppsFragment)
422                 }
423 
424                 banner.setDismissButtonVisible(true)
425                 banner.setDismissButton(logName = UnknownGenericElement.UNKNOWN_BANNER_BUTTON) {
426                     setBannerSeen(Constants.START_USING_HC_BANNER_SEEN)
427                     bannerGroup.removePreferenceRecursively(START_USING_HC_BANNER_KEY)
428                 }
429             }
430     }
431 
432     private fun getConnectMoreAppsBanner(appMetadata: AppMetadata): HealthBannerPreference {
433         return HealthBannerPreference(requireContext(), UnknownGenericElement.UNKNOWN_BANNER)
434             .also { banner ->
435                 banner.setAttentionLevel(BannerMessagePreference.AttentionLevel.NORMAL)
436                 banner.title = resources.getString(R.string.connect_more_apps_banner_title)
437                 banner.summary =
438                     resources.getString(
439                         R.string.connect_more_apps_banner_content,
440                         appMetadata.appName,
441                     )
442                 banner.icon =
443                     AttributeResolver.getNullableDrawable(requireContext(), R.attr.syncIcon)
444                 banner.key = CONNECT_MORE_APPS_BANNER_KEY
445 
446                 banner.setPositiveButton(
447                     text = getString(R.string.connect_more_apps_set_up_button),
448                     logName = UnknownGenericElement.UNKNOWN_BANNER_BUTTON,
449                 ) {
450                     findNavController().navigate(R.id.action_homeFragment_to_connectedAppsFragment)
451                 }
452 
453                 banner.setDismissButton(logName = UnknownGenericElement.UNKNOWN_BANNER_BUTTON) {
454                     setBannerSeen(Constants.CONNECT_MORE_APPS_BANNER_SEEN)
455                     bannerGroup.removePreferenceRecursively(CONNECT_MORE_APPS_BANNER_KEY)
456                 }
457             }
458     }
459 
460     private fun getSeeCompatibleAppsBanner(appMetadata: AppMetadata): HealthBannerPreference {
461         return HealthBannerPreference(requireContext(), UnknownGenericElement.UNKNOWN_BANNER)
462             .also { banner ->
463                 banner.title = resources.getString(R.string.see_compatible_apps_banner_title)
464                 banner.summary =
465                     resources.getString(
466                         R.string.see_compatible_apps_banner_content,
467                         appMetadata.appName,
468                     )
469                 banner.icon =
470                     AttributeResolver.getNullableDrawable(
471                         requireContext(),
472                         R.attr.seeAllCompatibleAppsIcon,
473                     )
474                 banner.key = SEE_COMPATIBLE_APPS_BANNER_KEY
475 
476                 banner.setPositiveButtonText(getString(R.string.see_compatible_apps_set_up_button))
477                 banner.setPositiveButtonOnClickListener {
478                     findNavController().navigate(R.id.action_homeFragment_to_playstoreActivity)
479                 }
480 
481                 banner.setDismissButtonVisible(true)
482                 banner.setDismissButtonOnClickListener {
483                     setBannerSeen(Constants.SEE_MORE_COMPATIBLE_APPS_BANNER_SEEN)
484                     bannerGroup.removePreferenceRecursively(SEE_COMPATIBLE_APPS_BANNER_KEY)
485                 }
486             }
487     }
488 
489     private fun getLockScreenBanner(
490         bannerState: LockScreenBannerState.ShowBanner
491     ): HealthBannerPreference {
492         return HealthBannerPreference(requireContext(), HomePageElement.LOCK_SCREEN_BANNER).also {
493             banner ->
494             banner.title = resources.getString(R.string.lock_screen_banner_title)
495             banner.summary = resources.getString(R.string.lock_screen_banner_content)
496             banner.icon = AttributeResolver.getNullableDrawable(requireContext(), R.attr.lockIcon)
497             banner.key = LOCK_SCREEN_BANNER_KEY
498 
499             banner.setPositiveButton(
500                 text = getString(R.string.lock_screen_banner_button),
501                 logName = HomePageElement.LOCK_SCREEN_BANNER_BUTTON,
502             ) {
503                 updateLockScreenBannerSeen(bannerState)
504                 navigateToSecuritySettings()
505             }
506 
507             banner.setDismissButtonVisible(true)
508             banner.setDismissButton(logName = HomePageElement.LOCK_SCREEN_BANNER_DISMISS_BUTTON) {
509                 updateLockScreenBannerSeen(bannerState)
510                 bannerGroup.removePreferenceRecursively(LOCK_SCREEN_BANNER_KEY)
511             }
512         }
513     }
514 
515     private fun updateLockScreenBannerSeen(bannerState: LockScreenBannerState.ShowBanner) {
516         val sharedPreference = getSharedPreference()
517         sharedPreference.edit().apply {
518             val anyFitnessData = bannerState.hasAnyFitnessData
519             val anyMedicalData = bannerState.hasAnyMedicalData
520 
521             if (!(anyFitnessData || anyMedicalData)) {
522                 // This should not happen.
523                 putBoolean(LOCK_SCREEN_BANNER_SEEN_FITNESS, true)
524                 putBoolean(LOCK_SCREEN_BANNER_SEEN_MEDICAL, true)
525             }
526             if (anyFitnessData) {
527                 putBoolean(LOCK_SCREEN_BANNER_SEEN_FITNESS, true)
528             }
529             if (anyMedicalData) {
530                 putBoolean(LOCK_SCREEN_BANNER_SEEN_MEDICAL, true)
531             }
532             apply()
533         }
534     }
535 
536     // endregion
537 
538     private fun navigateToSecuritySettings() {
539         startActivity(securitySettingsIntent)
540     }
541 
542     private fun updateConnectedApps(connectedApps: List<ConnectedAppMetadata>) {
543         val connectedAppsGroup = connectedApps.groupBy { it.status }
544         val numAllowedApps = connectedAppsGroup[ConnectedAppStatus.ALLOWED].orEmpty().size
545         val numNotAllowedApps = connectedAppsGroup[ConnectedAppStatus.DENIED].orEmpty().size
546         val numTotalApps = numAllowedApps + numNotAllowedApps
547 
548         if (numTotalApps == 0) {
549             appPermissionsPreference.summary =
550                 getString(R.string.connected_apps_button_no_permissions_subtitle)
551         } else if (numAllowedApps == numTotalApps) {
552             appPermissionsPreference.summary =
553                 MessageFormat.format(
554                     getString(R.string.connected_apps_connected_subtitle),
555                     mapOf("count" to numAllowedApps),
556                 )
557         } else {
558             appPermissionsPreference.summary =
559                 getString(
560                     if (numAllowedApps == 1) R.string.only_one_connected_app_button_subtitle
561                     else R.string.connected_apps_button_subtitle,
562                     numAllowedApps.toString(),
563                     numTotalApps.toString(),
564                 )
565         }
566     }
567 
568     private fun updateOnboardingBanner(connectedApps: List<ConnectedAppMetadata>) {
569         removeAllOnboardingBanners()
570 
571         if (!onboarding()) {
572             return
573         }
574 
575         val connectedAppsGroup = connectedApps.groupBy { it.status }
576         val numAllowedApps = connectedAppsGroup[ConnectedAppStatus.ALLOWED].orEmpty().size
577         val numNotAllowedApps = connectedAppsGroup[ConnectedAppStatus.DENIED].orEmpty().size
578         val numTotalApps = numAllowedApps + numNotAllowedApps
579 
580         val sharedPreference = getSharedPreference()
581 
582         if (numTotalApps > 0 && numAllowedApps == 0) {
583             // No apps connected, one available
584             // Show if not dismissed
585             val bannerSeen =
586                 sharedPreference.getBoolean(Constants.START_USING_HC_BANNER_SEEN, false)
587             if (!bannerSeen) {
588                 val banner = getStartUsingHealthConnectBanner()
589                 bannerGroup.addPreference(banner)
590             }
591         } else if (numAllowedApps == 1 && numNotAllowedApps > 0) {
592             // 1 app connected, at least one available to connect
593             val bannerSeen =
594                 sharedPreference.getBoolean(Constants.CONNECT_MORE_APPS_BANNER_SEEN, false)
595             if (!bannerSeen) {
596                 val banner =
597                     getConnectMoreAppsBanner(
598                         connectedAppsGroup[ConnectedAppStatus.ALLOWED]!![0].appMetadata
599                     )
600                 bannerGroup.addPreference(banner)
601             }
602         } else if (numAllowedApps == 1 && numTotalApps == 1) {
603             // 1 app connected, no more available to connect
604             if (deviceInfoUtils.isPlayStoreAvailable(requireContext())) {
605                 val bannerSeen =
606                     sharedPreference.getBoolean(
607                         Constants.SEE_MORE_COMPATIBLE_APPS_BANNER_SEEN,
608                         false,
609                     )
610                 if (!bannerSeen) {
611                     val banner =
612                         getSeeCompatibleAppsBanner(
613                             connectedAppsGroup[ConnectedAppStatus.ALLOWED]!![0].appMetadata
614                         )
615                     bannerGroup.addPreference(banner)
616                 }
617             }
618         }
619     }
620 
621     private fun removeAllOnboardingBanners() {
622         bannerGroup.removePreferenceRecursively(START_USING_HC_BANNER_KEY)
623         bannerGroup.removePreferenceRecursively(CONNECTED_APPS_PREFERENCE_KEY)
624         bannerGroup.removePreferenceRecursively(SEE_COMPATIBLE_APPS_BANNER_KEY)
625     }
626 
627     private fun updateRecentApps(recentAppsList: List<RecentAccessEntry>) {
628         if (SettingsThemeHelper.isExpressiveTheme(requireContext()) && recentAppsList.isEmpty()) {
629             noRecentAccessPreference.isVisible = true
630             recentAccessPreferenceGroup.isVisible = false
631             return
632         }
633 
634         noRecentAccessPreference.isVisible = false
635         recentAccessPreferenceGroup.isVisible = true
636         recentAccessPreferenceGroup.removeAll()
637 
638         if (recentAppsList.isEmpty()) {
639             recentAccessPreferenceGroup.addPreference(
640                 Preference(requireContext())
641                     .also { it.setSummary(R.string.no_recent_access) }
642                     .also { it.isSelectable = false }
643             )
644         } else {
645             recentAppsList.forEach { recentApp ->
646                 val newRecentAccessPreference = getRecentAccessPreference(recentApp)
647                 recentAccessPreferenceGroup.addPreference(newRecentAccessPreference)
648             }
649             recentAccessPreferenceGroup.addPreference(getSeeAllPreference())
650         }
651     }
652 
653     private fun updateRecentAppsWithError() {
654         noRecentAccessPreference.isVisible = false
655         recentAccessPreferenceGroup.isVisible = true
656         recentAccessPreferenceGroup.removeAll()
657         recentAccessPreferenceGroup.addPreference(
658             HealthPreference(requireContext()).also {
659                 it.title = getString(R.string.recent_access_error)
660                 it.isSelectable = false
661                 it.setIcon(AttributeResolver.getResource(requireContext(), R.attr.warningIcon))
662             }
663         )
664     }
665 
666     private fun getSeeAllPreference(): Preference {
667         val seeAllPreference =
668             if (SettingsThemeHelper.isExpressiveTheme(requireContext())) {
669                 HealthButtonPreference(requireContext()).also {
670                     it.setTitle(R.string.recent_access_view_all_button)
671                     it.setIcon(AttributeResolver.getResource(requireContext(), R.attr.optionsIcon))
672                     it.logName = HomePageElement.SEE_ALL_RECENT_ACCESS_BUTTON
673                     it.setOnClickListener {
674                         findNavController()
675                             .navigate(R.id.action_homeFragment_to_recentAccessFragment)
676                     }
677                 }
678             } else {
679                 HealthPreference(requireContext()).also {
680                     it.setTitle(R.string.show_recent_access_entries_button_title)
681                     it.setIcon(AttributeResolver.getResource(requireContext(), R.attr.seeAllIcon))
682                     it.logName = HomePageElement.SEE_ALL_RECENT_ACCESS_BUTTON
683                     it.setOnPreferenceClickListener {
684                         findNavController()
685                             .navigate(R.id.action_homeFragment_to_recentAccessFragment)
686                         true
687                     }
688                 }
689             }
690 
691         return seeAllPreference
692     }
693 
694     private fun getRecentAccessPreference(recentApp: RecentAccessEntry): HealthPreference {
695         val preference =
696             if (SettingsThemeHelper.isExpressiveTheme(requireContext())) {
697                 HealthPreference(requireContext()).also { newPreference ->
698                     newPreference.logName = RecentAccessElement.RECENT_ACCESS_ENTRY_BUTTON
699                     newPreference.title = recentApp.metadata.appName
700                     newPreference.icon = recentApp.metadata.icon
701                     newPreference.summary =
702                         formatRecentAccessTime(recentApp.instantTime, timeSource, requireContext())
703                 }
704             } else {
705                 RecentAccessPreference(requireContext(), recentApp, timeSource, false)
706             }
707 
708         preference.setOnPreferenceClickListener {
709             if (recentApp.isInactive) {
710                 Toast.makeText(
711                         requireContext(),
712                         getString(R.string.recent_access_inactive_app),
713                         Toast.LENGTH_LONG,
714                     )
715                     .show()
716             } else {
717                 navigateToAppInfoScreen(recentApp)
718             }
719             true
720         }
721 
722         return preference
723     }
724 
725     private fun navigateToAppInfoScreen(recentApp: RecentAccessEntry) {
726         val appPermissionsType = recentApp.appPermissionsType
727         val navigationId =
728             when (appPermissionsType) {
729                 AppPermissionsType.FITNESS_PERMISSIONS_ONLY ->
730                     R.id.action_homeFragment_to_fitnessAppFragment
731                 AppPermissionsType.MEDICAL_PERMISSIONS_ONLY ->
732                     R.id.action_homeFragment_to_medicalAppFragment
733                 AppPermissionsType.COMBINED_PERMISSIONS ->
734                     R.id.action_homeFragment_to_combinedPermissionsFragment
735             }
736         findNavController()
737             .navigate(
738                 navigationId,
739                 bundleOf(
740                     Intent.EXTRA_PACKAGE_NAME to recentApp.metadata.packageName,
741                     Constants.EXTRA_APP_NAME to recentApp.metadata.appName,
742                 ),
743             )
744     }
745 
746     private fun getSharedPreference() =
747         requireActivity().getSharedPreferences(USER_ACTIVITY_TRACKER, Context.MODE_PRIVATE)
748 
749     private fun setBannerSeen(sharedPrefKey: String, seen: Boolean = true) {
750         val sharedPreference = getSharedPreference()
751         sharedPreference.edit().apply {
752             putBoolean(sharedPrefKey, seen)
753             apply()
754         }
755     }
756 }
757