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