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.PreferenceFragmentCompat 29 import androidx.preference.PreferenceScreen 30 import androidx.recyclerview.widget.RecyclerView 31 import com.android.healthconnect.controller.R 32 import com.android.healthconnect.controller.shared.HealthPreferenceComparisonCallback 33 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger 34 import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint 35 import com.android.healthconnect.controller.utils.logging.PageName 36 import com.android.healthconnect.controller.utils.logging.ToolbarElement 37 import com.android.healthconnect.controller.utils.setupSharedMenu 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 : PreferenceFragmentCompat() { 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 = requireActivity().findViewById<AppBarLayout>( 61 com.android.settingslib.collapsingtoolbar.R.id.app_bar) 62 appBarLayout?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES 63 appBarLayout?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) 64 } 65 onResumenull66 override fun onResume() { 67 super.onResume() 68 logger.setPageId(pageName) 69 logger.logPageImpression() 70 } 71 onCreateViewnull72 override fun onCreateView( 73 inflater: LayoutInflater, 74 container: ViewGroup?, 75 savedInstanceState: Bundle? 76 ): View { 77 logger.setPageId(pageName) 78 val rootView = 79 inflater.inflate(R.layout.preference_frame, container, /*attachToRoot */ false) 80 loadingView = rootView.findViewById(R.id.progress_indicator) 81 errorView = rootView.findViewById(R.id.error_view) 82 prefView = rootView.findViewById(R.id.pref_container) 83 preferenceContainer = 84 super.onCreateView(inflater, container, savedInstanceState) as ViewGroup 85 setLoading(isLoading, animate = false, force = true) 86 prefView.addView(preferenceContainer) 87 return rootView 88 } 89 onViewCreatednull90 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 91 super.onViewCreated(view, savedInstanceState) 92 setupSharedMenu(viewLifecycleOwner, logger) 93 logger.logImpression(ToolbarElement.TOOLBAR_SETTINGS_BUTTON) 94 } 95 onCreatePreferencesnull96 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 97 preferenceManager.preferenceComparisonCallback = HealthPreferenceComparisonCallback() 98 } 99 onCreateAdapternull100 override fun onCreateAdapter(preferenceScreen: PreferenceScreen): RecyclerView.Adapter<*> { 101 val adapter = super.onCreateAdapter(preferenceScreen) 102 /* By default, the PreferenceGroupAdapter does setHasStableIds(true). Since each Preference 103 * is internally allocated with an auto-incremented ID, it does not allow us to gracefully 104 * update only changed preferences based on HealthPreferenceComparisonCallback. In order to 105 * allow the list to track the changes, we need to ignore the Preference IDs. */ 106 adapter.setHasStableIds(false) 107 return adapter 108 } 109 setLoadingnull110 protected fun setLoading(isLoading: Boolean, animate: Boolean = true) { 111 setLoading(isLoading, animate, false) 112 } 113 setErrornull114 protected fun setError(hasError: Boolean, @StringRes errorText: Int = R.string.default_error) { 115 if (this.hasError != hasError) { 116 this.hasError = hasError 117 // If there is no created view, there is no reason to animate. 118 val canAnimate = view != null 119 setViewShown(preferenceContainer, !hasError, canAnimate) 120 setViewShown(loadingView, !hasError, canAnimate) 121 setViewShown(errorView, hasError, canAnimate) 122 errorView.setText(errorText) 123 } 124 } 125 setLoadingnull126 private fun setLoading(loading: Boolean, animate: Boolean, force: Boolean) { 127 if (isLoading != loading || force) { 128 isLoading = loading 129 // If there is no created view, there is no reason to animate. 130 val canAnimate = animate && view != null 131 setViewShown(preferenceContainer, !loading, canAnimate) 132 setViewShown(errorView, shown = false, animate = false) 133 setViewShown(loadingView, loading, canAnimate) 134 } 135 } 136 setViewShownnull137 private fun setViewShown(view: View, shown: Boolean, animate: Boolean) { 138 if (animate) { 139 val animation: Animation = 140 loadAnimation( 141 context, if (shown) android.R.anim.fade_in else android.R.anim.fade_out) 142 if (shown) { 143 view.visibility = View.VISIBLE 144 } else { 145 animation.setAnimationListener( 146 object : AnimationListener { 147 override fun onAnimationStart(animation: Animation) {} 148 149 override fun onAnimationRepeat(animation: Animation) {} 150 151 override fun onAnimationEnd(animation: Animation) { 152 view.visibility = View.INVISIBLE 153 } 154 }) 155 } 156 view.startAnimation(animation) 157 } else { 158 view.clearAnimation() 159 view.visibility = if (shown) View.VISIBLE else View.GONE 160 } 161 } 162 setupLoggernull163 private fun setupLogger() { 164 val hiltEntryPoint = 165 EntryPointAccessors.fromApplication( 166 requireContext().applicationContext, HealthConnectLoggerEntryPoint::class.java) 167 logger = hiltEntryPoint.logger() 168 logger.setPageId(pageName) 169 } 170 } 171