summaryrefslogtreecommitdiff
path: root/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SpacerPreference.kt
blob: bb09783bee5a28582c91b6e6ea4c6c2df6fc4b36 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.permissioncontroller.safetycenter.ui

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import androidx.preference.PreferenceViewHolder
import com.android.permissioncontroller.R
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
import com.android.settingslib.widget.FooterPreference
import kotlin.math.max

/**
 * A preference that adds an empty space to the bottom of a Safety Center subpage.
 *
 * Due to the logic of [CollapsingToolbarBaseActivity], its content won't be scrollable if it fits
 * the single page. This logic conflicts with the UX of collapsible and expandable items of Safety
 * Center, and with some other use cases (i.e. opening the page from Search might scroll to bottom
 * while the scroll is disabled). In such cases user won't be able to expand the collapsed toolbar
 * by scrolling the screen content.
 *
 * [SpacerPreference] makes the page to be slightly bigger than the screen size to unlock the scroll
 * regardless of the content length and to mitigate this UX problem.
 *
 * If a [FooterPreference] is added to the same [PreferenceScreen], its order should be decreased to
 * keep it with the last visible content above the [SpacerPreference].
 */
internal class SpacerPreference(context: Context, attrs: AttributeSet) :
    Preference(context, attrs) {

    init {
        setLayoutResource(R.layout.preference_spacer)
        isVisible = SafetyCenterUiFlags.getShowSubpages()
        // spacer should be the last item on screen
        setOrder(Int.MAX_VALUE - 1)
    }

    private var maxKnownToolbarHeight = 0
    override fun onBindViewHolder(holder: PreferenceViewHolder) {
        super.onBindViewHolder(holder)
        val spacer = holder.itemView

        // we should ensure we won't add multiple listeners to the same view,
        // and Preferences API does not allow to do cleanups when onViewRecycled,
        // so we are keeping a track of the added listener attaching it as a tag to the View
        val listener: View.OnLayoutChangeListener = spacer.tag as? View.OnLayoutChangeListener
            ?: object : View.OnLayoutChangeListener {
                    override fun onLayoutChange(
                        v: View?,
                        left: Int,
                        top: Int,
                        right: Int,
                        bottom: Int,
                        oldLeft: Int,
                        oldTop: Int,
                        oldRight: Int,
                        oldBottom: Int
                    ) {
                        adjustHeight(spacer)
                    }}.also { spacer.tag = it }

        spacer.removeOnLayoutChangeListener(listener)
        spacer.addOnLayoutChangeListener(listener)
    }

    private fun adjustHeight(spacer: View) {
        val root = spacer.rootView as? ViewGroup
        if (root == null) {
            return
        }

        val contentParent = root.findViewById<ViewGroup>(R.id.content_parent)
        if (contentParent == null) {
            return
        }
        // when opening the Subpage from Search the layout pass may be triggered
        // differently due to the auto-scroll to highlight a specific item,
        // and in this case we need to wait the content parent to be measured
        if (contentParent.height == 0) {
            val globalLayoutObserver = object : ViewTreeObserver.OnGlobalLayoutListener {
                override fun onGlobalLayout() {
                    contentParent.viewTreeObserver.removeOnGlobalLayoutListener(this)
                    adjustHeight(spacer)
                }
            }
            contentParent.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutObserver)
            return
        }

        val collapsingToolbar = root.findViewById<View>(R.id.collapsing_toolbar)
        maxKnownToolbarHeight = max(maxKnownToolbarHeight, collapsingToolbar.height)

        val contentHeight = spacer.top + maxKnownToolbarHeight
        val desiredSpacerHeight = if (contentHeight > contentParent.height) {
            // making it 0 height will remove if from recyclerview
            1
        } else {
            // to unlock the scrolling we need spacer to go slightly beyond the screen
            contentParent.height - contentHeight + 1
        }

        val layoutParams = spacer.layoutParams
        if (layoutParams.height != desiredSpacerHeight) {
            layoutParams.height = desiredSpacerHeight
            spacer.layoutParams = layoutParams
            // need to let RecyclerView to update scroll position
            spacer.post(::notifyChanged)
        }
    }
}