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>(R.id.app_bar) 61 appBarLayout?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES 62 appBarLayout?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) 63 } 64 onResumenull65 override fun onResume() { 66 super.onResume() 67 logger.setPageId(pageName) 68 logger.logPageImpression() 69 } 70 onCreateViewnull71 override fun onCreateView( 72 inflater: LayoutInflater, 73 container: ViewGroup?, 74 savedInstanceState: Bundle? 75 ): View? { 76 logger.setPageId(pageName) 77 val rootView = 78 inflater.inflate(R.layout.preference_frame, container, /*attachToRoot */ false) 79 loadingView = rootView.findViewById(R.id.progress_indicator) 80 errorView = rootView.findViewById(R.id.error_view) 81 prefView = rootView.findViewById(R.id.pref_container) 82 preferenceContainer = 83 super.onCreateView(inflater, container, savedInstanceState) as ViewGroup 84 setLoading(isLoading, animate = false, force = true) 85 prefView.addView(preferenceContainer) 86 return rootView 87 } 88 onViewCreatednull89 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 90 super.onViewCreated(view, savedInstanceState) 91 setupSharedMenu(viewLifecycleOwner, logger) 92 logger.logImpression(ToolbarElement.TOOLBAR_SETTINGS_BUTTON) 93 } 94 onCreatePreferencesnull95 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 96 preferenceManager.preferenceComparisonCallback = HealthPreferenceComparisonCallback() 97 } 98 onCreateAdapternull99 override fun onCreateAdapter(preferenceScreen: PreferenceScreen?): RecyclerView.Adapter<*> { 100 val adapter = super.onCreateAdapter(preferenceScreen) 101 /* By default, the PreferenceGroupAdapter does setHasStableIds(true). Since each Preference 102 * is internally allocated with an auto-incremented ID, it does not allow us to gracefully 103 * update only changed preferences based on HealthPreferenceComparisonCallback. In order to 104 * allow the list to track the changes, we need to ignore the Preference IDs. */ 105 adapter.setHasStableIds(false) 106 return adapter 107 } 108 setLoadingnull109 protected fun setLoading(isLoading: Boolean, animate: Boolean = true) { 110 setLoading(isLoading, animate, false) 111 } 112 setErrornull113 protected fun setError(hasError: Boolean, @StringRes errorText: Int = R.string.default_error) { 114 if (this.hasError != hasError) { 115 this.hasError = hasError 116 // If there is no created view, there is no reason to animate. 117 val canAnimate = view != null 118 setViewShown(preferenceContainer, !hasError, canAnimate) 119 setViewShown(loadingView, !hasError, canAnimate) 120 setViewShown(errorView, hasError, canAnimate) 121 errorView.setText(errorText) 122 } 123 } 124 setLoadingnull125 private fun setLoading(loading: Boolean, animate: Boolean, force: Boolean) { 126 if (isLoading != loading || force) { 127 isLoading = loading 128 // If there is no created view, there is no reason to animate. 129 val canAnimate = animate && view != null 130 setViewShown(preferenceContainer, !loading, canAnimate) 131 setViewShown(errorView, shown = false, animate = false) 132 setViewShown(loadingView, loading, canAnimate) 133 } 134 } 135 setViewShownnull136 private fun setViewShown(view: View, shown: Boolean, animate: Boolean) { 137 if (animate) { 138 val animation: Animation = 139 loadAnimation( 140 context, if (shown) android.R.anim.fade_in else android.R.anim.fade_out) 141 if (shown) { 142 view.visibility = View.VISIBLE 143 } else { 144 animation.setAnimationListener( 145 object : AnimationListener { 146 override fun onAnimationStart(animation: Animation) {} 147 148 override fun onAnimationRepeat(animation: Animation) {} 149 150 override fun onAnimationEnd(animation: Animation) { 151 view.visibility = View.INVISIBLE 152 } 153 }) 154 } 155 view.startAnimation(animation) 156 } else { 157 view.clearAnimation() 158 view.visibility = if (shown) View.VISIBLE else View.GONE 159 } 160 } 161 setupLoggernull162 private fun setupLogger() { 163 val hiltEntryPoint = 164 EntryPointAccessors.fromApplication( 165 requireContext().applicationContext, HealthConnectLoggerEntryPoint::class.java) 166 logger = hiltEntryPoint.logger() 167 logger.setPageId(pageName) 168 } 169 } 170