• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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