1 /*
<lambda>null2 * 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.systemui.statusbar.notification.icon.ui.viewbinder
17
18 import android.graphics.Color
19 import android.util.Log
20 import android.view.View
21 import android.view.ViewGroup
22 import android.widget.FrameLayout
23 import androidx.annotation.ColorInt
24 import androidx.collection.ArrayMap
25 import androidx.lifecycle.lifecycleScope
26 import com.android.app.tracing.coroutines.launchTraced as launch
27 import com.android.app.tracing.traceSection
28 import com.android.internal.R as RInternal
29 import com.android.internal.statusbar.StatusBarIcon
30 import com.android.internal.util.ContrastColorUtil
31 import com.android.systemui.common.ui.ConfigurationState
32 import com.android.systemui.lifecycle.repeatWhenAttached
33 import com.android.systemui.res.R
34 import com.android.systemui.statusbar.StatusBarIconView
35 import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior
36 import com.android.systemui.statusbar.notification.collection.NotifCollection
37 import com.android.systemui.statusbar.notification.icon.IconPack
38 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
39 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconColors
40 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
41 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
42 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData
43 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData.LimitType
44 import com.android.systemui.statusbar.phone.NotificationIconContainer
45 import com.android.systemui.statusbar.ui.SystemBarUtilsState
46 import com.android.systemui.util.kotlin.mapValuesNotNullTo
47 import com.android.systemui.util.ui.isAnimating
48 import com.android.systemui.util.ui.stopAnimating
49 import com.android.systemui.util.ui.value
50 import kotlinx.coroutines.DisposableHandle
51 import kotlinx.coroutines.Job
52 import kotlinx.coroutines.coroutineScope
53 import kotlinx.coroutines.flow.Flow
54 import kotlinx.coroutines.flow.StateFlow
55 import kotlinx.coroutines.flow.combine
56 import kotlinx.coroutines.flow.stateIn
57
58 /** Binds a view-model to a [NotificationIconContainer]. */
59 object NotificationIconContainerViewBinder {
60
61 suspend fun bind(
62 displayId: Int,
63 view: NotificationIconContainer,
64 viewModel: NotificationIconContainerStatusBarViewModel,
65 configuration: ConfigurationState,
66 systemBarUtilsState: SystemBarUtilsState,
67 failureTracker: StatusBarIconViewBindingFailureTracker,
68 viewStore: IconViewStore,
69 ): Unit = coroutineScope {
70 launch {
71 val contrastColorUtil = ContrastColorUtil.getInstance(view.context)
72 val iconColors: StateFlow<NotificationIconColors> =
73 viewModel.iconColors(displayId).stateIn(this)
74 viewModel.icons.bindIcons(
75 logTag = "statusbar",
76 view = view,
77 configuration = configuration,
78 systemBarUtilsState = systemBarUtilsState,
79 notifyBindingFailures = { failureTracker.statusBarFailures = it },
80 viewStore = viewStore,
81 ) { _, sbiv ->
82 StatusBarIconViewBinder.bindIconColors(sbiv, iconColors, contrastColorUtil)
83 }
84 }
85 if (!StatusBarNoHunBehavior.isEnabled) {
86 launch { viewModel.bindIsolatedIcon(view, viewStore) }
87 }
88 launch { viewModel.animationsEnabled.bindAnimationsEnabled(view) }
89 }
90
91 @JvmStatic
92 fun bindWhileAttached(
93 view: NotificationIconContainer,
94 viewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
95 configuration: ConfigurationState,
96 systemBarUtilsState: SystemBarUtilsState,
97 failureTracker: StatusBarIconViewBindingFailureTracker,
98 viewStore: IconViewStore,
99 ): DisposableHandle {
100 return view.repeatWhenAttached {
101 lifecycleScope.launch {
102 bind(view, viewModel, configuration, systemBarUtilsState, failureTracker, viewStore)
103 }
104 }
105 }
106
107 suspend fun bind(
108 view: NotificationIconContainer,
109 viewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
110 configuration: ConfigurationState,
111 systemBarUtilsState: SystemBarUtilsState,
112 failureTracker: StatusBarIconViewBindingFailureTracker,
113 viewStore: IconViewStore,
114 ): Unit = coroutineScope {
115 view.setUseIncreasedIconScale(true)
116 launch {
117 // Collect state shared across all icon views, so that we are not duplicating collects
118 // for each individual icon.
119 val color: StateFlow<Int> =
120 configuration
121 .getColorAttr(R.attr.wallpaperTextColor, DEFAULT_AOD_ICON_COLOR)
122 .stateIn(this)
123 val tintAlpha = viewModel.tintAlpha.stateIn(this)
124 val animsEnabled = viewModel.areIconAnimationsEnabled.stateIn(this)
125 viewModel.icons.bindIcons(
126 logTag = "aod",
127 view = view,
128 configuration = configuration,
129 systemBarUtilsState = systemBarUtilsState,
130 notifyBindingFailures = { failureTracker.aodFailures = it },
131 viewStore = viewStore,
132 ) { _, sbiv ->
133 coroutineScope {
134 launch { StatusBarIconViewBinder.bindColor(sbiv, color) }
135 launch { StatusBarIconViewBinder.bindTintAlpha(sbiv, tintAlpha) }
136 launch { StatusBarIconViewBinder.bindAnimationsEnabled(sbiv, animsEnabled) }
137 }
138 }
139 }
140 launch { viewModel.areContainerChangesAnimated.bindAnimationsEnabled(view) }
141 }
142
143 /** Binds to [NotificationIconContainer.setAnimationsEnabled] */
144 private suspend fun Flow<Boolean>.bindAnimationsEnabled(view: NotificationIconContainer) {
145 collectTracingEach("NIC#bindAnimationsEnabled", view::setAnimationsEnabled)
146 }
147
148 private suspend fun NotificationIconContainerStatusBarViewModel.bindIsolatedIcon(
149 view: NotificationIconContainer,
150 viewStore: IconViewStore,
151 ) {
152 StatusBarNoHunBehavior.assertInLegacyMode()
153 coroutineScope {
154 launch {
155 isolatedIconLocation.collectTracingEach("NIC#isolatedIconLocation") { location ->
156 view.setIsolatedIconLocation(location, true)
157 }
158 }
159 launch {
160 isolatedIcon.collectTracingEach("NIC#showIconIsolated") { iconInfo ->
161 val iconView = iconInfo.value?.let { viewStore.iconView(it.notifKey) }
162 if (iconInfo.isAnimating) {
163 view.showIconIsolatedAnimated(iconView, iconInfo::stopAnimating)
164 } else {
165 view.showIconIsolated(iconView)
166 }
167 }
168 }
169 }
170 }
171
172 /**
173 * Binds [NotificationIconsViewData] to a [NotificationIconContainer]'s children.
174 *
175 * [bindIcon] will be invoked to bind a child [StatusBarIconView] to an icon associated with the
176 * given `iconKey`. The parent [Job] of this coroutine will be cancelled automatically when the
177 * view is to be unbound.
178 */
179 suspend fun Flow<NotificationIconsViewData>.bindIcons(
180 logTag: String,
181 view: NotificationIconContainer,
182 configuration: ConfigurationState,
183 systemBarUtilsState: SystemBarUtilsState,
184 notifyBindingFailures: (Collection<String>) -> Unit,
185 viewStore: IconViewStore,
186 bindIcon: suspend (iconKey: String, view: StatusBarIconView) -> Unit = { _, _ -> },
187 ): Unit = coroutineScope {
188 val iconSizeFlow: Flow<Int> =
189 configuration.getDimensionPixelSize(RInternal.dimen.status_bar_icon_size_sp)
190 val iconHorizontalPaddingFlow: Flow<Int> =
191 configuration.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin)
192 val layoutParams: StateFlow<FrameLayout.LayoutParams> =
193 combine(iconSizeFlow, iconHorizontalPaddingFlow, systemBarUtilsState.statusBarHeight) {
194 iconSize,
195 iconHPadding,
196 statusBarHeight ->
197 FrameLayout.LayoutParams(iconSize + 2 * iconHPadding, statusBarHeight)
198 }
199 .stateIn(this)
200 try {
201 bindIcons(logTag, view, layoutParams, notifyBindingFailures, viewStore, bindIcon)
202 } finally {
203 // Detach everything so that child SBIVs don't hold onto a reference to the container.
204 view.detachAllIcons()
205 }
206 }
207
208 private suspend fun Flow<NotificationIconsViewData>.bindIcons(
209 logTag: String,
210 view: NotificationIconContainer,
211 layoutParams: StateFlow<FrameLayout.LayoutParams>,
212 notifyBindingFailures: (Collection<String>) -> Unit,
213 viewStore: IconViewStore,
214 bindIcon: suspend (iconKey: String, view: StatusBarIconView) -> Unit,
215 ): Unit = coroutineScope {
216 val failedBindings = mutableSetOf<String>()
217 val boundViewsByNotifKey = ArrayMap<String, Pair<StatusBarIconView, Job>>()
218 var prevIcons = NotificationIconsViewData()
219 collectTracingEach({ "NIC($logTag)#bindIcons" }) { iconsData: NotificationIconsViewData ->
220 val iconsDiff = NotificationIconsViewData.computeDifference(iconsData, prevIcons)
221 prevIcons = iconsData
222
223 // Lookup 1:1 group icon replacements
224 val replacingIcons: ArrayMap<String, StatusBarIcon> =
225 iconsDiff.groupReplacements.mapValuesNotNullTo(ArrayMap()) { (_, notifKey) ->
226 boundViewsByNotifKey[notifKey]?.first?.statusBarIcon
227 }
228 view.withIconReplacements(replacingIcons) {
229 // Remove and unbind.
230 for (notifKey in iconsDiff.removed) {
231 failedBindings.remove(notifKey)
232 val (child, job) = boundViewsByNotifKey.remove(notifKey) ?: continue
233 traceSection("removeIcon") {
234 view.removeView(child)
235 job.cancel()
236 }
237 }
238
239 // Add and bind.
240 val toAdd: Sequence<String> = iconsDiff.added.asSequence() + failedBindings.toList()
241 for (notifKey in toAdd) {
242 // Lookup the StatusBarIconView from the store.
243 val sbiv = viewStore.iconView(notifKey)
244 if (sbiv == null) {
245 failedBindings.add(notifKey)
246 continue
247 }
248 failedBindings.remove(notifKey)
249 traceSection("addIcon") {
250 (sbiv.parent as? ViewGroup)?.run {
251 if (this !== view) {
252 Log.wtf(TAG, "[$logTag] SBIV($notifKey) has an unexpected parent")
253 }
254 // If the container was re-inflated and re-bound, then SBIVs might still
255 // be attached to the prior view.
256 removeView(sbiv)
257 // The view might still be transiently added if it was just removed and
258 // added again.
259 removeTransientView(sbiv)
260 }
261 view.addView(sbiv, layoutParams.value)
262 boundViewsByNotifKey.remove(notifKey)?.second?.cancel()
263 boundViewsByNotifKey[notifKey] =
264 Pair(
265 sbiv,
266 launch {
267 launch {
268 layoutParams.collectTracingEach(
269 tag = { "[$logTag] SBIV#bindLayoutParams" }
270 ) {
271 if (it != sbiv.layoutParams) {
272 sbiv.layoutParams = it
273 }
274 }
275 }
276 bindIcon(notifKey, sbiv)
277 },
278 )
279 }
280 }
281
282 // Set the maximum number of icons to show in the container. Any icons over this
283 // amount will render as an "overflow dot".
284 val maxIconsAmount: Int =
285 when (iconsData.limitType) {
286 LimitType.MaximumIndex -> {
287 iconsData.visibleIcons.asSequence().take(iconsData.iconLimit).count {
288 info ->
289 info.notifKey in boundViewsByNotifKey
290 }
291 }
292 LimitType.MaximumAmount -> {
293 iconsData.iconLimit
294 }
295 }
296 view.setMaxIconsAmount(maxIconsAmount)
297
298 // Track the binding failures so that they appear in dumpsys.
299 notifyBindingFailures(failedBindings)
300
301 // Re-sort notification icons
302 view.changeViewPositions {
303 traceSection("re-sort") {
304 val expectedChildren: List<StatusBarIconView> =
305 iconsData.visibleIcons.mapNotNull {
306 boundViewsByNotifKey[it.notifKey]?.first
307 }
308 val childCount = view.childCount
309 val toRemove = mutableListOf<View>()
310 for (i in 0 until childCount) {
311 val actual = view.getChildAt(i)
312 val expected = expectedChildren.getOrNull(i)
313 if (expected == null) {
314 Log.wtf(TAG, "[$logTag] Unexpected child $actual")
315 toRemove.add(actual)
316 continue
317 }
318 if (actual === expected) {
319 continue
320 }
321 view.removeView(expected)
322 view.addView(expected, i)
323 }
324 for (child in toRemove) {
325 view.removeView(child)
326 }
327 }
328 }
329 }
330 }
331 }
332
333 /**
334 * Track which groups are being replaced with a different icon instance, but with the same
335 * visual icon. This prevents a weird animation where it looks like an icon disappears and
336 * reappears unchanged.
337 */
338 // TODO(b/305739416): Ideally we wouldn't swap out the StatusBarIconView at all, and instead use
339 // a single SBIV instance for the group. Then this whole concept can go away.
340 private inline fun <R> NotificationIconContainer.withIconReplacements(
341 replacements: ArrayMap<String, StatusBarIcon>,
342 block: () -> R,
343 ): R {
344 setReplacingIcons(replacements)
345 return block().also { setReplacingIcons(null) }
346 }
347
348 /**
349 * Any invocations of [NotificationIconContainer.addView] /
350 * [NotificationIconContainer.removeView] inside of [block] will not cause a new add / remove
351 * animation.
352 */
353 private inline fun <R> NotificationIconContainer.changeViewPositions(block: () -> R): R {
354 setChangingViewPositions(true)
355 return block().also { setChangingViewPositions(false) }
356 }
357
358 /** External storage for [StatusBarIconView] instances. */
359 fun interface IconViewStore {
360 fun iconView(key: String): StatusBarIconView?
361 }
362
363 @ColorInt private const val DEFAULT_AOD_ICON_COLOR = Color.WHITE
364 private const val TAG = "NotifIconContainerViewBinder"
365 }
366
367 /**
368 * Convenience builder for [IconViewStore] that uses [block] to extract the relevant
369 * [StatusBarIconView] from an [IconPack] stored inside of the [NotifCollection].
370 */
NotifCollectionnull371 fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) =
372 IconViewStore { key ->
373 getEntry(key)?.icons?.let(block)
374 }
375
collectTracingEachnull376 private suspend inline fun <T> Flow<T>.collectTracingEach(
377 tag: String,
378 crossinline collector: (T) -> Unit,
379 ) = collect { traceSection(tag) { collector(it) } }
380
collectTracingEachnull381 private suspend inline fun <T> Flow<T>.collectTracingEach(
382 noinline tag: () -> String,
383 crossinline collector: (T) -> Unit,
384 ) {
385 val lazyTag = lazy(mode = LazyThreadSafetyMode.PUBLICATION, tag)
386 collect { traceSection({ lazyTag.value }) { collector(it) } }
387 }
388