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 
17 package com.android.permissioncontroller.safetycenter.ui
18 
19 import android.content.Context
20 import android.util.AttributeSet
21 import android.view.View
22 import android.view.ViewGroup
23 import android.view.ViewTreeObserver
24 import androidx.preference.Preference
25 import androidx.preference.PreferenceScreen
26 import androidx.preference.PreferenceViewHolder
27 import com.android.permissioncontroller.R
28 import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
29 import com.android.settingslib.widget.FooterPreference
30 import kotlin.math.max
31 
32 /**
33  * A preference that adds an empty space to the bottom of a Safety Center subpage.
34  *
35  * Due to the logic of [CollapsingToolbarBaseActivity], its content won't be scrollable if it fits
36  * the single page. This logic conflicts with the UX of collapsible and expandable items of Safety
37  * Center, and with some other use cases (i.e. opening the page from Search might scroll to bottom
38  * while the scroll is disabled). In such cases user won't be able to expand the collapsed toolbar
39  * by scrolling the screen content.
40  *
41  * [SpacerPreference] makes the page to be slightly bigger than the screen size to unlock the scroll
42  * regardless of the content length and to mitigate this UX problem.
43  *
44  * If a [FooterPreference] is added to the same [PreferenceScreen], its order should be decreased to
45  * keep it with the last visible content above the [SpacerPreference].
46  */
47 internal class SpacerPreference(context: Context, attrs: AttributeSet) :
48     Preference(context, attrs) {
49 
50     init {
51         setLayoutResource(R.layout.preference_spacer)
52         isVisible = SafetyCenterUiFlags.getShowSubpages()
53         // spacer should be the last item on screen
54         setOrder(Int.MAX_VALUE - 1)
55     }
56 
57     private var maxKnownToolbarHeight = 0
58 
onBindViewHoldernull59     override fun onBindViewHolder(holder: PreferenceViewHolder) {
60         super.onBindViewHolder(holder)
61         val spacer = holder.itemView
62 
63         // we should ensure we won't add multiple listeners to the same view,
64         // and Preferences API does not allow to do cleanups when onViewRecycled,
65         // so we are keeping a track of the added listener attaching it as a tag to the View
66         val listener: View.OnLayoutChangeListener =
67             spacer.tag as? View.OnLayoutChangeListener
68                 ?: object : View.OnLayoutChangeListener {
69                         override fun onLayoutChange(
70                             v: View?,
71                             left: Int,
72                             top: Int,
73                             right: Int,
74                             bottom: Int,
75                             oldLeft: Int,
76                             oldTop: Int,
77                             oldRight: Int,
78                             oldBottom: Int,
79                         ) {
80                             adjustHeight(spacer)
81                         }
82                     }
83                     .also { spacer.tag = it }
84 
85         spacer.removeOnLayoutChangeListener(listener)
86         spacer.addOnLayoutChangeListener(listener)
87     }
88 
adjustHeightnull89     private fun adjustHeight(spacer: View) {
90         val root = spacer.rootView as? ViewGroup
91         if (root == null) {
92             return
93         }
94 
95         val contentParent =
96             root.findViewById<ViewGroup>(
97                 com.android.settingslib.collapsingtoolbar.R.id.content_parent
98             )
99         if (contentParent == null) {
100             return
101         }
102         // when opening the Subpage from Search the layout pass may be triggered
103         // differently due to the auto-scroll to highlight a specific item,
104         // and in this case we need to wait the content parent to be measured
105         if (contentParent.height == 0) {
106             val globalLayoutObserver =
107                 object : ViewTreeObserver.OnGlobalLayoutListener {
108                     override fun onGlobalLayout() {
109                         contentParent.viewTreeObserver.removeOnGlobalLayoutListener(this)
110                         adjustHeight(spacer)
111                     }
112                 }
113             contentParent.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutObserver)
114             return
115         }
116 
117         val collapsingToolbar =
118             root.findViewById<View>(
119                 com.android.settingslib.collapsingtoolbar.R.id.collapsing_toolbar
120             )
121         maxKnownToolbarHeight = max(maxKnownToolbarHeight, collapsingToolbar!!.height)
122 
123         val contentHeight = spacer.top + maxKnownToolbarHeight
124         val desiredSpacerHeight =
125             if (contentHeight > contentParent.height) {
126                 // making it 0 height will remove if from recyclerview
127                 1
128             } else {
129                 // to unlock the scrolling we need spacer to go slightly beyond the screen
130                 contentParent.height - contentHeight + 1
131             }
132 
133         val layoutParams = spacer.layoutParams
134         if (layoutParams.height != desiredSpacerHeight) {
135             layoutParams.height = desiredSpacerHeight
136             spacer.layoutParams = layoutParams
137             // need to let RecyclerView to update scroll position
138             spacer.post(::notifyChanged)
139         }
140     }
141 }
142