1 /*
2  * Copyright (C) 2024 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.settingslib.widget
18 
19 import android.annotation.SuppressLint
20 import android.os.Handler
21 import android.os.Looper
22 import android.util.TypedValue
23 import androidx.annotation.DrawableRes
24 import androidx.preference.Preference
25 import androidx.preference.PreferenceCategory
26 import androidx.preference.PreferenceGroup
27 import androidx.preference.PreferenceGroupAdapter
28 import androidx.preference.PreferenceViewHolder
29 import com.android.settingslib.widget.theme.R
30 
31 /**
32  * A custom adapter for displaying settings preferences in a list, handling rounded corners for
33  * preference items within a group.
34  */
35 @SuppressLint("RestrictedApi")
36 open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) :
37     PreferenceGroupAdapter(preferenceGroup) {
38 
39     private val mPreferenceGroup = preferenceGroup
40     private var mRoundCornerMappingList: ArrayList<Int> = ArrayList()
41 
42     private var mNormalPaddingStart = 0
43     private var mGroupPaddingStart = 0
44     private var mNormalPaddingEnd = 0
45     private var mGroupPaddingEnd = 0
46     @DrawableRes private var mLegacyBackgroundRes: Int
47 
48     private val mHandler = Handler(Looper.getMainLooper())
49 
<lambda>null50     private val syncRunnable = Runnable { updatePreferences() }
51 
52     init {
53         val context = preferenceGroup.context
54         mNormalPaddingStart =
55             context.resources.getDimensionPixelSize(R.dimen.settingslib_expressive_space_small1)
56         mGroupPaddingStart = mNormalPaddingStart * 2
57         mNormalPaddingEnd =
58             context.resources.getDimensionPixelSize(R.dimen.settingslib_expressive_space_small1)
59         mGroupPaddingEnd = mNormalPaddingEnd * 2
60         val outValue = TypedValue()
61         context.theme.resolveAttribute(
62             android.R.attr.selectableItemBackground,
63             outValue,
64             true, /* resolveRefs */
65         )
66         mLegacyBackgroundRes = outValue.resourceId
67         updatePreferences()
68     }
69 
70     @SuppressLint("RestrictedApi")
onPreferenceHierarchyChangenull71     override fun onPreferenceHierarchyChange(preference: Preference) {
72         super.onPreferenceHierarchyChange(preference)
73 
74         // Post after super class has posted their sync runnable to update preferences.
75         mHandler.removeCallbacks(syncRunnable)
76         mHandler.post(syncRunnable)
77     }
78 
79     @SuppressLint("RestrictedApi")
onBindViewHoldernull80     override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
81         super.onBindViewHolder(holder, position)
82         updateBackground(holder, position)
83     }
84 
updatePreferencesnull85     private fun updatePreferences() {
86         val oldList = ArrayList(mRoundCornerMappingList)
87         mRoundCornerMappingList = ArrayList()
88         mappingPreferenceGroup(mRoundCornerMappingList, mPreferenceGroup)
89         if (mRoundCornerMappingList != oldList) {
90             notifyDataSetChanged()
91         }
92     }
93 
94     @SuppressLint("RestrictedApi")
mappingPreferenceGroupnull95     private fun mappingPreferenceGroup(cornerStyles: MutableList<Int>, group: PreferenceGroup) {
96         cornerStyles.clear()
97         cornerStyles.addAll(MutableList(itemCount) { 0 })
98 
99         // the first item in to group
100         var startIndex = -1
101         // the last item in the group
102         var endIndex = -1
103         var currentParent: PreferenceGroup? = group
104         for (i in 0 until itemCount) {
105             when (val pref = getItem(i)) {
106                 // the preference has round corner background, so we don't need to handle it.
107                 is GroupSectionDividerMixin -> {
108                     cornerStyles[i] = 0
109                     startIndex = -1
110                     endIndex = -1
111                 }
112 
113                 // PreferenceCategory should not have round corner background.
114                 is PreferenceCategory -> {
115                     cornerStyles[i] = 0
116                     startIndex = -1
117                     endIndex = -1
118                     currentParent = pref
119                 }
120 
121                 // ExpandablePreference is PreferenceGroup but it should handle round corner
122                 is Expandable -> {
123                     // When ExpandablePreference is expanded, we treat is as the first item.
124                     if (pref.isExpanded()) {
125                         currentParent = pref as? PreferenceGroup
126                         startIndex = i
127                         cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_TOP or ROUND_CORNER_CENTER
128                         endIndex = -1
129                     }
130                 }
131 
132                 else -> {
133                     val parent = pref?.parent
134 
135                     // item in the group should have round corner background.
136                     cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_CENTER
137                     if (parent === currentParent) {
138                         // find the first item in the group
139                         if (startIndex == -1) {
140                             startIndex = i
141                             cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_TOP
142                         }
143 
144                         // find the last item in the group, if we find the new last item, we should
145                         // remove the old last item round corner.
146                         if (endIndex == -1 || endIndex < i) {
147                             if (endIndex != -1) {
148                                 cornerStyles[endIndex] =
149                                     cornerStyles[endIndex] and ROUND_CORNER_BOTTOM.inv()
150                             }
151                             endIndex = i
152                             cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_BOTTOM
153                         }
154                     } else {
155                         // this item is new group, we should reset the index.
156                         currentParent = parent
157                         startIndex = i
158                         cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_TOP
159                         endIndex = i
160                         cornerStyles[i] = cornerStyles[i] or ROUND_CORNER_BOTTOM
161                     }
162                 }
163             }
164         }
165     }
166 
167     /** handle roundCorner background */
updateBackgroundnull168     private fun updateBackground(holder: PreferenceViewHolder, position: Int) {
169         val context = holder.itemView.context
170         @DrawableRes
171         val backgroundRes =
172             when (SettingsThemeHelper.isExpressiveTheme(context)) {
173                 true -> getRoundCornerDrawableRes(position, isSelected = false)
174                 else -> mLegacyBackgroundRes
175             }
176 
177         val v = holder.itemView
178         // Update padding
179         if (SettingsThemeHelper.isExpressiveTheme(context)) {
180             val paddingStart = if (backgroundRes == 0) mNormalPaddingStart else mGroupPaddingStart
181             val paddingEnd = if (backgroundRes == 0) mNormalPaddingEnd else mGroupPaddingEnd
182             v.setPaddingRelative(paddingStart, v.paddingTop, paddingEnd, v.paddingBottom)
183         }
184         // Update background
185         v.setBackgroundResource(backgroundRes)
186     }
187 
188     @DrawableRes
getRoundCornerDrawableResnull189     protected fun getRoundCornerDrawableRes(position: Int, isSelected: Boolean): Int {
190         return getRoundCornerDrawableRes(position, isSelected, false)
191     }
192 
193     @DrawableRes
getRoundCornerDrawableResnull194     protected fun getRoundCornerDrawableRes(
195         position: Int,
196         isSelected: Boolean,
197         isHighlighted: Boolean,
198     ): Int {
199         val cornerType = mRoundCornerMappingList[position]
200 
201         if ((cornerType and ROUND_CORNER_CENTER) == 0) {
202             return 0
203         }
204 
205         return when {
206             (cornerType and ROUND_CORNER_TOP) != 0 && (cornerType and ROUND_CORNER_BOTTOM) == 0 -> {
207                 // the first
208                 if (isSelected) R.drawable.settingslib_round_background_top_selected
209                 else if (isHighlighted) R.drawable.settingslib_round_background_top_highlighted
210                 else R.drawable.settingslib_round_background_top
211             }
212 
213             (cornerType and ROUND_CORNER_BOTTOM) != 0 && (cornerType and ROUND_CORNER_TOP) == 0 -> {
214                 // the last
215                 if (isSelected) R.drawable.settingslib_round_background_bottom_selected
216                 else if (isHighlighted) R.drawable.settingslib_round_background_bottom_highlighted
217                 else R.drawable.settingslib_round_background_bottom
218             }
219 
220             (cornerType and ROUND_CORNER_TOP) != 0 && (cornerType and ROUND_CORNER_BOTTOM) != 0 -> {
221                 // the only one preference
222                 if (isSelected) R.drawable.settingslib_round_background_selected
223                 else if (isHighlighted) R.drawable.settingslib_round_background_highlighted
224                 else R.drawable.settingslib_round_background
225             }
226 
227             else -> {
228                 // in the center
229                 if (isSelected) R.drawable.settingslib_round_background_center_selected
230                 else if (isHighlighted) R.drawable.settingslib_round_background_center_highlighted
231                 else R.drawable.settingslib_round_background_center
232             }
233         }
234     }
235 
236     companion object {
237         private const val ROUND_CORNER_CENTER: Int = 1
238         private const val ROUND_CORNER_TOP: Int = 1 shl 1
239         private const val ROUND_CORNER_BOTTOM: Int = 1 shl 2
240     }
241 }
242