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.view.View
23 import androidx.core.os.bundleOf
24 import androidx.fragment.app.activityViewModels
25 import androidx.fragment.app.viewModels
26 import androidx.navigation.fragment.findNavController
27 import androidx.preference.Preference
28 import androidx.preference.PreferenceGroup
29 import com.android.healthconnect.controller.HealthFitnessUiStatsLog.*
30 import com.android.healthconnect.controller.R
31 import com.android.healthconnect.controller.exportimport.api.ExportStatusViewModel
32 import com.android.healthconnect.controller.exportimport.api.ScheduledExportUiState
33 import com.android.healthconnect.controller.exportimport.api.ScheduledExportUiStatus
34 import com.android.healthconnect.controller.migration.MigrationActivity.Companion.maybeShowWhatsNewDialog
35 import com.android.healthconnect.controller.migration.MigrationViewModel
36 import com.android.healthconnect.controller.migration.api.MigrationRestoreState
37 import com.android.healthconnect.controller.migration.api.MigrationRestoreState.DataRestoreUiState
38 import com.android.healthconnect.controller.migration.api.MigrationRestoreState.MigrationUiState
39 import com.android.healthconnect.controller.recentaccess.RecentAccessEntry
40 import com.android.healthconnect.controller.recentaccess.RecentAccessPreference
41 import com.android.healthconnect.controller.recentaccess.RecentAccessViewModel
42 import com.android.healthconnect.controller.recentaccess.RecentAccessViewModel.RecentAccessState
43 import com.android.healthconnect.controller.shared.Constants
44 import com.android.healthconnect.controller.shared.Constants.MIGRATION_NOT_COMPLETE_DIALOG_SEEN
45 import com.android.healthconnect.controller.shared.Constants.USER_ACTIVITY_TRACKER
46 import com.android.healthconnect.controller.shared.app.ConnectedAppMetadata
47 import com.android.healthconnect.controller.shared.app.ConnectedAppStatus
48 import com.android.healthconnect.controller.shared.dialog.AlertDialogBuilder
49 import com.android.healthconnect.controller.shared.preference.BannerPreference
50 import com.android.healthconnect.controller.shared.preference.HealthPreference
51 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
52 import com.android.healthconnect.controller.utils.AttributeResolver
53 import com.android.healthconnect.controller.utils.FeatureUtils
54 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
55 import com.android.healthconnect.controller.utils.NavigationUtils
56 import com.android.healthconnect.controller.utils.TimeSource
57 import com.android.healthconnect.controller.utils.logging.DataRestoreElement
58 import com.android.healthconnect.controller.utils.logging.ErrorPageElement
59 import com.android.healthconnect.controller.utils.logging.HomePageElement
60 import com.android.healthconnect.controller.utils.logging.MigrationElement
61 import com.android.healthconnect.controller.utils.logging.PageName
62 import com.android.healthfitness.flags.Flags
63 import dagger.hilt.android.AndroidEntryPoint
64 import java.time.Instant
65 import java.time.temporal.ChronoUnit
66 import javax.inject.Inject
67 
68 /** Home fragment for Health Connect. */
69 @AndroidEntryPoint(HealthPreferenceFragment::class)
70 class HomeFragment : Hilt_HomeFragment() {
71 
72     companion object {
73         private const val DATA_AND_ACCESS_PREFERENCE_KEY = "data_and_access"
74         private const val RECENT_ACCESS_PREFERENCE_KEY = "recent_access"
75         private const val CONNECTED_APPS_PREFERENCE_KEY = "connected_apps"
76         private const val MIGRATION_BANNER_PREFERENCE_KEY = "migration_banner"
77         private const val DATA_RESTORE_BANNER_PREFERENCE_KEY = "data_restore_banner"
78         private const val MANAGE_DATA_PREFERENCE_KEY = "manage_data"
79         private const val EXPORT_FILE_ACCESS_ERROR_BANNER_PREFERENCE_KEY =
80             "export_file_access_error_banner"
81         private const val HOME_FRAGMENT_BANNER_ORDER = 1
82 
83         @JvmStatic fun newInstance() = HomeFragment()
84     }
85 
86     init {
87         this.setPageName(PageName.HOME_PAGE)
88     }
89 
90     @Inject lateinit var featureUtils: FeatureUtils
91     @Inject lateinit var timeSource: TimeSource
92     @Inject lateinit var navigationUtils: NavigationUtils
93 
94     private val recentAccessViewModel: RecentAccessViewModel by viewModels()
95     private val homeFragmentViewModel: HomeFragmentViewModel by viewModels()
96     private val migrationViewModel: MigrationViewModel by activityViewModels()
97     private val exportStatusViewModel: ExportStatusViewModel by activityViewModels()
98 
99     private val mDataAndAccessPreference: HealthPreference? by lazy {
100         preferenceScreen.findPreference(DATA_AND_ACCESS_PREFERENCE_KEY)
101     }
102 
103     private val mRecentAccessPreference: PreferenceGroup? by lazy {
104         preferenceScreen.findPreference(RECENT_ACCESS_PREFERENCE_KEY)
105     }
106 
107     private val mConnectedAppsPreference: HealthPreference? by lazy {
108         preferenceScreen.findPreference(CONNECTED_APPS_PREFERENCE_KEY)
109     }
110 
111     private val mManageDataPreference: HealthPreference? by lazy {
112         preferenceScreen.findPreference(MANAGE_DATA_PREFERENCE_KEY)
113     }
114 
115     private val dateFormatter: LocalDateTimeFormatter by lazy {
116         LocalDateTimeFormatter(requireContext())
117     }
118 
119     private lateinit var migrationBannerSummary: String
120     private var migrationBanner: BannerPreference? = null
121 
122     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
123         super.onCreatePreferences(savedInstanceState, rootKey)
124         setPreferencesFromResource(R.xml.home_preference_screen, rootKey)
125         mDataAndAccessPreference?.logName = HomePageElement.DATA_AND_ACCESS_BUTTON
126         mDataAndAccessPreference?.setOnPreferenceClickListener {
127             findNavController().navigate(R.id.action_homeFragment_to_healthDataCategoriesFragment)
128             true
129         }
130         mConnectedAppsPreference?.logName = HomePageElement.APP_PERMISSIONS_BUTTON
131         mConnectedAppsPreference?.setOnPreferenceClickListener {
132             findNavController().navigate(R.id.action_homeFragment_to_connectedAppsFragment)
133             true
134         }
135 
136         if (featureUtils.isNewAppPriorityEnabled() ||
137             featureUtils.isNewInformationArchitectureEnabled()) {
138             mManageDataPreference?.logName = HomePageElement.MANAGE_DATA_BUTTON
139             mManageDataPreference?.setOnPreferenceClickListener {
140                 findNavController().navigate(R.id.action_homeFragment_to_manageDataFragment)
141                 true
142             }
143         } else {
144             preferenceScreen.removePreferenceRecursively(MANAGE_DATA_PREFERENCE_KEY)
145         }
146 
147         migrationBannerSummary = getString(R.string.resume_migration_banner_description_fallback)
148         migrationBanner = getMigrationBanner()
149     }
150 
151     override fun onResume() {
152         super.onResume()
153         recentAccessViewModel.loadRecentAccessApps(maxNumEntries = 3)
154         homeFragmentViewModel.loadConnectedApps()
155     }
156 
157     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
158         super.onViewCreated(view, savedInstanceState)
159 
160         recentAccessViewModel.loadRecentAccessApps(maxNumEntries = 3)
161         recentAccessViewModel.recentAccessApps.observe(viewLifecycleOwner) { recentAppsState ->
162             when (recentAppsState) {
163                 is RecentAccessState.WithData -> {
164                     updateRecentApps(recentAppsState.recentAccessEntries)
165                 }
166                 else -> {
167                     updateRecentApps(emptyList())
168                 }
169             }
170         }
171         homeFragmentViewModel.connectedApps.observe(viewLifecycleOwner) { connectedApps ->
172             updateConnectedApps(connectedApps)
173         }
174         migrationViewModel.migrationState.observe(viewLifecycleOwner) { migrationState ->
175             when (migrationState) {
176                 is MigrationViewModel.MigrationFragmentState.WithData -> {
177                     showMigrationState(migrationState.migrationRestoreState)
178                 }
179                 else -> {
180                     // do nothing
181                 }
182             }
183         }
184 
185         if (Flags.exportImport()) {
186             exportStatusViewModel.storedScheduledExportStatus.observe(viewLifecycleOwner) {
187                 scheduledExportUiStatus ->
188                 when (scheduledExportUiStatus) {
189                     is ScheduledExportUiStatus.WithData -> {
190                         maybeShowExportErrorBanner(scheduledExportUiStatus.scheduledExportUiState)
191                     }
192                     else -> {
193                         // do nothing
194                     }
195                 }
196             }
197         }
198     }
199 
200     private fun showMigrationState(migrationRestoreState: MigrationRestoreState) {
201         preferenceScreen.removePreferenceRecursively(MIGRATION_BANNER_PREFERENCE_KEY)
202         preferenceScreen.removePreferenceRecursively(DATA_RESTORE_BANNER_PREFERENCE_KEY)
203 
204         val (migrationUiState, dataRestoreUiState, dataErrorState) = migrationRestoreState
205 
206         if (dataRestoreUiState == DataRestoreUiState.PENDING) {
207             // TODO (b/327170886) uncomment when states are correct
208             // preferenceScreen.addPreference(getDataRestorePendingBanner())
209         } else if (migrationUiState in
210             listOf(
211                 MigrationUiState.ALLOWED_PAUSED,
212                 MigrationUiState.ALLOWED_NOT_STARTED,
213                 MigrationUiState.MODULE_UPGRADE_REQUIRED,
214                 MigrationUiState.APP_UPGRADE_REQUIRED)) {
215             migrationBanner = getMigrationBanner()
216             preferenceScreen.addPreference(migrationBanner as BannerPreference)
217         } else if (migrationUiState == MigrationUiState.COMPLETE) {
218             maybeShowWhatsNewDialog(requireContext())
219         } else if (migrationUiState == MigrationUiState.ALLOWED_ERROR) {
220             maybeShowMigrationNotCompleteDialog()
221         }
222     }
223 
224     private fun maybeShowMigrationNotCompleteDialog() {
225         val sharedPreference =
226             requireActivity().getSharedPreferences(USER_ACTIVITY_TRACKER, Context.MODE_PRIVATE)
227         val dialogSeen = sharedPreference.getBoolean(MIGRATION_NOT_COMPLETE_DIALOG_SEEN, false)
228 
229         if (!dialogSeen) {
230             AlertDialogBuilder(this, MigrationElement.MIGRATION_NOT_COMPLETE_DIALOG_CONTAINER)
231                 .setTitle(R.string.migration_not_complete_dialog_title)
232                 .setMessage(R.string.migration_not_complete_dialog_content)
233                 .setCancelable(false)
234                 .setNegativeButton(
235                     R.string.migration_whats_new_dialog_button,
236                     MigrationElement.MIGRATION_NOT_COMPLETE_DIALOG_BUTTON) { _, _ ->
237                         sharedPreference.edit().apply {
238                             putBoolean(MIGRATION_NOT_COMPLETE_DIALOG_SEEN, true)
239                             apply()
240                         }
241                     }
242                 .create()
243                 .show()
244         }
245     }
246 
247     private fun maybeShowExportErrorBanner(scheduledExportUiState: ScheduledExportUiState) {
248         when (scheduledExportUiState.dataExportError) {
249             ScheduledExportUiState.DataExportError.DATA_EXPORT_LOST_FILE_ACCESS -> {
250                 scheduledExportUiState.lastSuccessfulExportTime?.let {
251                     preferenceScreen.addPreference(
252                         getExportFileAccessErrorBanner(it, scheduledExportUiState.periodInDays))
253                 }
254             }
255             else -> {
256                 // Do nothing yet.
257             }
258         }
259     }
260 
261     private fun getExportFileAccessErrorBanner(
262         lastSuccessfulDate: Instant,
263         periodInDays: Int
264     ): BannerPreference {
265         // TODO: b/325917283 - Add proper logging for the export file access error banner.
266         return BannerPreference(requireContext(), ErrorPageElement.UNKNOWN_ELEMENT).also {
267             it.setPrimaryButton(
268                 getString(R.string.export_file_access_error_banner_button),
269                 ErrorPageElement.UNKNOWN_ELEMENT)
270             it.title = getString(R.string.export_file_access_error_banner_title)
271             it.key = EXPORT_FILE_ACCESS_ERROR_BANNER_PREFERENCE_KEY
272             it.summary =
273                 getString(
274                     R.string.export_file_access_error_banner_summary,
275                     dateFormatter.formatLongDate(
276                         lastSuccessfulDate.plus(periodInDays.toLong(), ChronoUnit.DAYS)))
277             it.icon = AttributeResolver.getNullableDrawable(requireContext(), R.attr.warningIcon)
278             it.setPrimaryButtonOnClickListener {
279                 findNavController().navigate(R.id.action_homeFragment_to_exportSetupActivity)
280             }
281             it.order = HOME_FRAGMENT_BANNER_ORDER
282         }
283     }
284 
285     private fun getMigrationBanner(): BannerPreference {
286         return BannerPreference(requireContext(), MigrationElement.MIGRATION_RESUME_BANNER).also {
287             it.setPrimaryButton(
288                 resources.getString(R.string.resume_migration_banner_button),
289                 MigrationElement.MIGRATION_RESUME_BANNER_BUTTON)
290             it.title = resources.getString(R.string.resume_migration_banner_title)
291             it.key = MIGRATION_BANNER_PREFERENCE_KEY
292             it.summary = migrationBannerSummary
293             it.icon =
294                 AttributeResolver.getNullableDrawable(requireContext(), R.attr.settingsAlertIcon)
295             it.setPrimaryButtonOnClickListener {
296                 findNavController().navigate(R.id.action_homeFragment_to_migrationActivity)
297             }
298             it.order = HOME_FRAGMENT_BANNER_ORDER
299         }
300     }
301 
302     private fun getDataRestorePendingBanner(): BannerPreference {
303         return BannerPreference(requireContext(), DataRestoreElement.RESTORE_PENDING_BANNER).also {
304             it.setPrimaryButton(
305                 resources.getString(R.string.data_restore_pending_banner_button),
306                 DataRestoreElement.RESTORE_PENDING_BANNER_UPDATE_BUTTON)
307             it.title = resources.getString(R.string.data_restore_pending_banner_title)
308             it.key = DATA_RESTORE_BANNER_PREFERENCE_KEY
309             it.summary = resources.getString(R.string.data_restore_pending_banner_content)
310             it.icon =
311                 AttributeResolver.getNullableDrawable(requireContext(), R.attr.updateNeededIcon)
312             it.setPrimaryButtonOnClickListener {
313                 findNavController().navigate(R.id.action_homeFragment_to_systemUpdateActivity)
314             }
315             it.order = 1
316         }
317     }
318 
319     private fun updateConnectedApps(connectedApps: List<ConnectedAppMetadata>) {
320         val connectedAppsGroup = connectedApps.groupBy { it.status }
321         val numAllowedApps = connectedAppsGroup[ConnectedAppStatus.ALLOWED].orEmpty().size
322         val numNotAllowedApps = connectedAppsGroup[ConnectedAppStatus.DENIED].orEmpty().size
323         val numTotalApps = numAllowedApps + numNotAllowedApps
324 
325         if (numTotalApps == 0) {
326             mConnectedAppsPreference?.summary =
327                 getString(R.string.connected_apps_button_no_permissions_subtitle)
328         } else if (numAllowedApps == numTotalApps) {
329             mConnectedAppsPreference?.summary =
330                 MessageFormat.format(
331                     getString(R.string.connected_apps_connected_subtitle),
332                     mapOf("count" to numAllowedApps))
333         } else {
334             mConnectedAppsPreference?.summary =
335                 getString(
336                     if (numAllowedApps == 1) R.string.only_one_connected_app_button_subtitle
337                     else R.string.connected_apps_button_subtitle,
338                     numAllowedApps.toString(),
339                     numTotalApps.toString())
340         }
341     }
342 
343     private fun updateRecentApps(recentAppsList: List<RecentAccessEntry>) {
344         mRecentAccessPreference?.removeAll()
345 
346         if (recentAppsList.isEmpty()) {
347             mRecentAccessPreference?.addPreference(
348                 Preference(requireContext())
349                     .also { it.setSummary(R.string.no_recent_access) }
350                     .also { it.isSelectable = false })
351         } else {
352             recentAppsList.forEach { recentApp ->
353                 val newRecentAccessPreference =
354                     RecentAccessPreference(requireContext(), recentApp, timeSource, false).also {
355                         newPreference ->
356                         if (!recentApp.isInactive) {
357                             newPreference.setOnPreferenceClickListener {
358                                 findNavController()
359                                     .navigate(
360                                         R.id.action_homeFragment_to_connectedAppFragment,
361                                         bundleOf(
362                                             Intent.EXTRA_PACKAGE_NAME to
363                                                 recentApp.metadata.packageName,
364                                             Constants.EXTRA_APP_NAME to recentApp.metadata.appName))
365                                 true
366                             }
367                         }
368                     }
369                 mRecentAccessPreference?.addPreference(newRecentAccessPreference)
370             }
371             val seeAllPreference =
372                 HealthPreference(requireContext()).also {
373                     it.setTitle(R.string.show_recent_access_entries_button_title)
374                     it.setIcon(AttributeResolver.getResource(requireContext(), R.attr.seeAllIcon))
375                     it.logName = HomePageElement.SEE_ALL_RECENT_ACCESS_BUTTON
376                 }
377             seeAllPreference.setOnPreferenceClickListener {
378                 findNavController().navigate(R.id.action_homeFragment_to_recentAccessFragment)
379                 true
380             }
381             mRecentAccessPreference?.addPreference(seeAllPreference)
382         }
383     }
384 }
385