1 /* 2 * Copyright (C) 2023 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.shared.preference 17 18 import android.os.Bundle 19 import android.view.LayoutInflater 20 import android.view.View 21 import android.view.ViewGroup 22 import android.view.accessibility.AccessibilityEvent 23 import android.view.animation.Animation 24 import android.view.animation.Animation.AnimationListener 25 import android.view.animation.AnimationUtils.loadAnimation 26 import android.widget.TextView 27 import androidx.annotation.StringRes 28 import androidx.preference.PreferenceScreen 29 import androidx.recyclerview.widget.RecyclerView 30 import com.android.healthconnect.controller.R 31 import com.android.healthconnect.controller.shared.HealthPreferenceComparisonCallback 32 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger 33 import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint 34 import com.android.healthconnect.controller.utils.logging.PageName 35 import com.android.healthconnect.controller.utils.logging.ToolbarElement 36 import com.android.healthconnect.controller.utils.setupSharedMenu 37 import com.android.settingslib.widget.SettingsBasePreferenceFragment 38 import com.google.android.material.appbar.AppBarLayout 39 import dagger.hilt.android.EntryPointAccessors 40 41 /** A base fragment that represents a page in Health Connect. */ 42 abstract class HealthPreferenceFragment : SettingsBasePreferenceFragment() { 43 44 private lateinit var logger: HealthConnectLogger 45 private lateinit var loadingView: View 46 private lateinit var errorView: TextView 47 private lateinit var preferenceContainer: ViewGroup 48 private lateinit var prefView: ViewGroup 49 private var pageName: PageName = PageName.UNKNOWN_PAGE 50 private var isLoading: Boolean = false 51 private var hasError: Boolean = false 52 setPageNamenull53 fun setPageName(pageName: PageName) { 54 this.pageName = pageName 55 } 56 onCreatenull57 override fun onCreate(savedInstanceState: Bundle?) { 58 setupLogger() 59 super.onCreate(savedInstanceState) 60 val appBarLayout = 61 requireActivity() 62 .findViewById<AppBarLayout>(com.android.settingslib.collapsingtoolbar.R.id.app_bar) 63 appBarLayout?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES 64 appBarLayout?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) 65 } 66 onResumenull67 override fun onResume() { 68 super.onResume() 69 logger.setPageId(pageName) 70 logger.logPageImpression() 71 } 72 onCreateViewnull73 override fun onCreateView( 74 inflater: LayoutInflater, 75 container: ViewGroup?, 76 savedInstanceState: Bundle?, 77 ): View { 78 logger.setPageId(pageName) 79 val rootView = 80 inflater.inflate(R.layout.preference_frame, container, /*attachToRoot */ false) 81 loadingView = rootView.findViewById(R.id.progress_indicator) 82 errorView = rootView.findViewById(R.id.error_view) 83 prefView = rootView.findViewById(R.id.pref_container) 84 preferenceContainer = 85 super.onCreateView(inflater, container, savedInstanceState) as ViewGroup 86 setLoading(isLoading, animate = false, force = true) 87 prefView.addView(preferenceContainer) 88 return rootView 89 } 90 onViewCreatednull91 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 92 super.onViewCreated(view, savedInstanceState) 93 setupSharedMenu(viewLifecycleOwner, logger) 94 logger.logImpression(ToolbarElement.TOOLBAR_SETTINGS_BUTTON) 95 } 96 onCreatePreferencesnull97 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 98 preferenceManager.preferenceComparisonCallback = HealthPreferenceComparisonCallback() 99 } 100 onCreateAdapternull101 override fun onCreateAdapter(preferenceScreen: PreferenceScreen): RecyclerView.Adapter<*> { 102 val adapter = super.onCreateAdapter(preferenceScreen) 103 /* By default, the PreferenceGroupAdapter does setHasStableIds(true). Since each Preference 104 * is internally allocated with an auto-incremented ID, it does not allow us to gracefully 105 * update only changed preferences based on HealthPreferenceComparisonCallback. In order to 106 * allow the list to track the changes, we need to ignore the Preference IDs. */ 107 adapter.setHasStableIds(false) 108 return adapter 109 } 110 setLoadingnull111 protected fun setLoading(isLoading: Boolean, animate: Boolean = true) { 112 setLoading(isLoading, animate, false) 113 } 114 setErrornull115 protected fun setError(hasError: Boolean, @StringRes errorText: Int = R.string.default_error) { 116 if (this.hasError != hasError) { 117 this.hasError = hasError 118 // If there is no created view, there is no reason to animate. 119 val canAnimate = view != null 120 setViewShown(preferenceContainer, !hasError, canAnimate) 121 setViewShown(loadingView, !hasError, canAnimate) 122 setViewShown(errorView, hasError, canAnimate) 123 errorView.setText(errorText) 124 } 125 } 126 setLoadingnull127 private fun setLoading(loading: Boolean, animate: Boolean, force: Boolean) { 128 if (isLoading != loading || force) { 129 isLoading = loading 130 // If there is no created view, there is no reason to animate. 131 val canAnimate = animate && view != null 132 setViewShown(preferenceContainer, !loading, canAnimate) 133 setViewShown(errorView, shown = false, animate = false) 134 setViewShown(loadingView, loading, canAnimate) 135 } 136 } 137 setViewShownnull138 private fun setViewShown(view: View, shown: Boolean, animate: Boolean) { 139 if (animate) { 140 val animation: Animation = 141 loadAnimation( 142 context, 143 if (shown) android.R.anim.fade_in else android.R.anim.fade_out, 144 ) 145 if (shown) { 146 view.visibility = View.VISIBLE 147 } else { 148 animation.setAnimationListener( 149 object : AnimationListener { 150 override fun onAnimationStart(animation: Animation) {} 151 152 override fun onAnimationRepeat(animation: Animation) {} 153 154 override fun onAnimationEnd(animation: Animation) { 155 view.visibility = View.INVISIBLE 156 } 157 } 158 ) 159 } 160 view.startAnimation(animation) 161 } else { 162 view.clearAnimation() 163 view.visibility = if (shown) View.VISIBLE else View.GONE 164 } 165 } 166 setupLoggernull167 private fun setupLogger() { 168 val hiltEntryPoint = 169 EntryPointAccessors.fromApplication( 170 requireContext().applicationContext, 171 HealthConnectLoggerEntryPoint::class.java, 172 ) 173 logger = hiltEntryPoint.logger() 174 logger.setPageId(pageName) 175 } 176 } 177