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.os.Bundle 21 import android.view.View 22 import androidx.core.os.bundleOf 23 import androidx.fragment.app.activityViewModels 24 import androidx.fragment.app.viewModels 25 import androidx.navigation.fragment.findNavController 26 import androidx.preference.Preference 27 import androidx.preference.PreferenceGroup 28 import com.android.healthconnect.controller.HealthFitnessUiStatsLog.* 29 import com.android.healthconnect.controller.R 30 import com.android.healthconnect.controller.dataentries.formatters.DurationFormatter 31 import com.android.healthconnect.controller.migration.MigrationActivity.Companion.maybeShowWhatsNewDialog 32 import com.android.healthconnect.controller.migration.MigrationViewModel 33 import com.android.healthconnect.controller.migration.api.MigrationState 34 import com.android.healthconnect.controller.permissions.shared.Constants 35 import com.android.healthconnect.controller.recentaccess.RecentAccessEntry 36 import com.android.healthconnect.controller.recentaccess.RecentAccessPreference 37 import com.android.healthconnect.controller.recentaccess.RecentAccessViewModel 38 import com.android.healthconnect.controller.recentaccess.RecentAccessViewModel.RecentAccessState 39 import com.android.healthconnect.controller.shared.app.ConnectedAppMetadata 40 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus 41 import com.android.healthconnect.controller.shared.dialog.AlertDialogBuilder 42 import com.android.healthconnect.controller.shared.preference.BannerPreference 43 import com.android.healthconnect.controller.shared.preference.HealthPreference 44 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment 45 import com.android.healthconnect.controller.utils.AttributeResolver 46 import com.android.healthconnect.controller.utils.logging.ErrorPageElement 47 import com.android.healthconnect.controller.utils.logging.HomePageElement 48 import com.android.healthconnect.controller.utils.logging.PageName 49 import dagger.hilt.android.AndroidEntryPoint 50 import java.time.Duration 51 52 /** Home fragment for Health Connect. */ 53 @AndroidEntryPoint(HealthPreferenceFragment::class) 54 class HomeFragment : Hilt_HomeFragment() { 55 56 companion object { 57 private const val DATA_AND_ACCESS_PREFERENCE_KEY = "data_and_access" 58 private const val RECENT_ACCESS_PREFERENCE_KEY = "recent_access" 59 private const val CONNECTED_APPS_PREFERENCE_KEY = "connected_apps" 60 private const val MIGRATION_BANNER_PREFERENCE_KEY = "migration_banner" 61 62 @JvmStatic fun newInstance() = HomeFragment() 63 } 64 65 init { 66 this.setPageName(PageName.HOME_PAGE) 67 } 68 69 private val recentAccessViewModel: RecentAccessViewModel by viewModels() 70 private val homeFragmentViewModel: HomeFragmentViewModel by viewModels() 71 private val migrationViewModel: MigrationViewModel by activityViewModels() 72 73 private val mDataAndAccessPreference: HealthPreference? by lazy { 74 preferenceScreen.findPreference(DATA_AND_ACCESS_PREFERENCE_KEY) 75 } 76 77 private val mRecentAccessPreference: PreferenceGroup? by lazy { 78 preferenceScreen.findPreference(RECENT_ACCESS_PREFERENCE_KEY) 79 } 80 81 private val mConnectedAppsPreference: HealthPreference? by lazy { 82 preferenceScreen.findPreference(CONNECTED_APPS_PREFERENCE_KEY) 83 } 84 85 private lateinit var migrationBannerSummary: String 86 private var migrationBanner: BannerPreference? = null 87 88 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 89 super.onCreatePreferences(savedInstanceState, rootKey) 90 setPreferencesFromResource(R.xml.home_preference_screen, rootKey) 91 mDataAndAccessPreference?.logName = HomePageElement.DATA_AND_ACCESS_BUTTON 92 mDataAndAccessPreference?.setOnPreferenceClickListener { 93 findNavController().navigate(R.id.action_homeFragment_to_healthDataCategoriesFragment) 94 true 95 } 96 mConnectedAppsPreference?.logName = HomePageElement.APP_PERMISSIONS_BUTTON 97 mConnectedAppsPreference?.setOnPreferenceClickListener { 98 findNavController().navigate(R.id.action_homeFragment_to_connectedAppsFragment) 99 true 100 } 101 102 migrationBannerSummary = getString(R.string.resume_migration_banner_description_fallback) 103 migrationBanner = getMigrationBanner() 104 } 105 106 override fun onResume() { 107 super.onResume() 108 recentAccessViewModel.loadRecentAccessApps(maxNumEntries = 3) 109 homeFragmentViewModel.loadConnectedApps() 110 } 111 112 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 113 super.onViewCreated(view, savedInstanceState) 114 115 recentAccessViewModel.loadRecentAccessApps(maxNumEntries = 3) 116 recentAccessViewModel.recentAccessApps.observe(viewLifecycleOwner) { recentAppsState -> 117 when (recentAppsState) { 118 is RecentAccessState.WithData -> { 119 updateRecentApps(recentAppsState.recentAccessEntries) 120 } 121 else -> { 122 updateRecentApps(emptyList()) 123 } 124 } 125 } 126 homeFragmentViewModel.connectedApps.observe(viewLifecycleOwner) { connectedApps -> 127 updateConnectedApps(connectedApps) 128 } 129 migrationViewModel.migrationState.observe(viewLifecycleOwner) { migrationState -> 130 when (migrationState) { 131 is MigrationViewModel.MigrationFragmentState.WithData -> { 132 showMigrationState(migrationState.migrationState) 133 } 134 else -> { 135 // do nothing 136 } 137 } 138 } 139 } 140 141 private fun updateMigrationBannerText(duration: Duration) { 142 val formattedDuration = 143 DurationFormatter.formatDurationDaysOrHours(requireContext(), duration) 144 migrationBannerSummary = 145 getString(R.string.resume_migration_banner_description, formattedDuration) 146 migrationBanner?.summary = migrationBannerSummary 147 } 148 149 private fun showMigrationState(migrationState: MigrationState) { 150 preferenceScreen.removePreferenceRecursively(MIGRATION_BANNER_PREFERENCE_KEY) 151 152 when (migrationState) { 153 MigrationState.ALLOWED_PAUSED, 154 MigrationState.ALLOWED_NOT_STARTED, 155 MigrationState.MODULE_UPGRADE_REQUIRED, 156 MigrationState.APP_UPGRADE_REQUIRED -> { 157 migrationBanner = getMigrationBanner() 158 preferenceScreen.addPreference(migrationBanner) 159 } 160 MigrationState.COMPLETE -> { 161 maybeShowWhatsNewDialog(requireContext()) 162 } 163 MigrationState.ALLOWED_ERROR -> { 164 maybeShowMigrationNotCompleteDialog() 165 } 166 else -> { 167 // show nothing 168 } 169 } 170 } 171 172 private fun maybeShowMigrationNotCompleteDialog() { 173 val sharedPreference = 174 requireActivity().getSharedPreferences("USER_ACTIVITY_TRACKER", Context.MODE_PRIVATE) 175 val dialogSeen = 176 sharedPreference.getBoolean( 177 getString(R.string.migration_not_complete_dialog_seen), false) 178 179 if (!dialogSeen) { 180 AlertDialogBuilder(this) 181 .setLogName(ErrorPageElement.UNKNOWN_ELEMENT) 182 .setTitle(R.string.migration_not_complete_dialog_title) 183 .setMessage(R.string.migration_not_complete_dialog_content) 184 .setCancelable(false) 185 .setNegativeButton( 186 R.string.migration_whats_new_dialog_button, ErrorPageElement.UNKNOWN_ELEMENT) { 187 _, 188 _ -> 189 sharedPreference.edit().apply { 190 putBoolean(getString(R.string.migration_not_complete_dialog_seen), true) 191 apply() 192 } 193 } 194 .create() 195 .show() 196 } 197 } 198 199 private fun getMigrationBanner(): BannerPreference { 200 return BannerPreference(requireContext()).also { 201 it.setButton(resources.getString(R.string.resume_migration_banner_button)) 202 it.title = resources.getString(R.string.resume_migration_banner_title) 203 it.key = MIGRATION_BANNER_PREFERENCE_KEY 204 it.summary = migrationBannerSummary 205 it.setIcon(R.drawable.ic_settings_alert) 206 it.setButtonOnClickListener { 207 findNavController().navigate(R.id.action_homeFragment_to_migrationActivity) 208 } 209 it.order = 1 210 } 211 } 212 213 private fun updateConnectedApps(connectedApps: List<ConnectedAppMetadata>) { 214 val connectedAppsGroup = connectedApps.groupBy { it.status } 215 val numAllowedApps = connectedAppsGroup[ConnectedAppStatus.ALLOWED].orEmpty().size 216 val numNotAllowedApps = connectedAppsGroup[ConnectedAppStatus.DENIED].orEmpty().size 217 val numTotalApps = numAllowedApps + numNotAllowedApps 218 219 if (numTotalApps == 0) { 220 mConnectedAppsPreference?.summary = 221 getString(R.string.connected_apps_button_no_permissions_subtitle) 222 } else if (numAllowedApps == 1 && numAllowedApps == numTotalApps) { 223 mConnectedAppsPreference?.summary = 224 getString( 225 R.string.connected_apps_one_app_connected_subtitle, numAllowedApps.toString()) 226 } else if (numAllowedApps == numTotalApps) { 227 mConnectedAppsPreference?.summary = 228 getString( 229 R.string.connected_apps_all_apps_connected_subtitle, numAllowedApps.toString()) 230 } else { 231 mConnectedAppsPreference?.summary = 232 getString( 233 R.string.connected_apps_button_subtitle, 234 numAllowedApps.toString(), 235 numTotalApps.toString()) 236 } 237 } 238 239 private fun updateRecentApps(recentAppsList: List<RecentAccessEntry>) { 240 mRecentAccessPreference?.removeAll() 241 242 if (recentAppsList.isEmpty()) { 243 mRecentAccessPreference?.addPreference( 244 Preference(requireContext()).also { it.setSummary(R.string.no_recent_access) }) 245 } else { 246 recentAppsList.forEach { recentApp -> 247 val newRecentAccessPreference = 248 RecentAccessPreference(requireContext(), recentApp, false).also { newPreference 249 -> 250 if (!recentApp.isInactive) { 251 newPreference.setOnPreferenceClickListener { 252 findNavController() 253 .navigate( 254 R.id.action_homeFragment_to_connectedAppFragment, 255 bundleOf( 256 Intent.EXTRA_PACKAGE_NAME to 257 recentApp.metadata.packageName, 258 Constants.EXTRA_APP_NAME to recentApp.metadata.appName)) 259 true 260 } 261 } 262 } 263 mRecentAccessPreference?.addPreference(newRecentAccessPreference) 264 } 265 val seeAllPreference = 266 HealthPreference(requireContext()).also { 267 it.setTitle(R.string.show_recent_access_entries_button_title) 268 it.setIcon(AttributeResolver.getResource(requireContext(), R.attr.seeAllIcon)) 269 it.logName = HomePageElement.SEE_ALL_RECENT_ACCESS_BUTTON 270 } 271 seeAllPreference.setOnPreferenceClickListener { 272 findNavController().navigate(R.id.action_homeFragment_to_recentAccessFragment) 273 true 274 } 275 mRecentAccessPreference?.addPreference(seeAllPreference) 276 } 277 } 278 } 279