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