• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.systemui.statusbar.notification.stack
2 
3 import androidx.core.view.children
4 import androidx.core.view.isVisible
5 import com.android.systemui.dagger.SysUISingleton
6 import com.android.systemui.statusbar.NotificationShelf
7 import com.android.systemui.statusbar.notification.Roundable
8 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
9 import com.android.systemui.statusbar.notification.row.ExpandableView
10 import javax.inject.Inject
11 
12 /**
13  * Utility class that helps us find the targets of an animation, often used to find the notification
14  * ([Roundable]) above and below the current one (see [findRoundableTargets]).
15  */
16 @SysUISingleton
17 class NotificationTargetsHelper @Inject constructor() {
18 
19     /**
20      * This method looks for views that can be rounded (and implement [Roundable]) during a
21      * notification swipe.
22      *
23      * @return The [Roundable] targets above/below the [viewSwiped] (if available). The
24      *   [RoundableTargets.before] and [RoundableTargets.after] parameters can be `null` if there is
25      *   no above/below notification or the notification is not part of the same section.
26      */
findRoundableTargetsnull27     fun findRoundableTargets(
28         viewSwiped: ExpandableNotificationRow,
29         stackScrollLayout: NotificationStackScrollLayout,
30         sectionsManager: NotificationSectionsManager,
31     ): RoundableTargets {
32         val viewBefore: Roundable?
33         val viewAfter: Roundable?
34 
35         val notificationParent = viewSwiped.notificationParent
36         val childrenContainer = notificationParent?.childrenContainer
37         val visibleStackChildren =
38             stackScrollLayout.children
39                 .filterIsInstance<ExpandableView>()
40                 .filter { it.isVisible }
41                 .toList()
42         if (notificationParent != null && childrenContainer != null) {
43             // We are inside a notification group
44 
45             val visibleGroupChildren = childrenContainer.attachedChildren.filter { it.isVisible }
46             val indexOfParentSwipedView = visibleGroupChildren.indexOf(viewSwiped)
47 
48             viewBefore =
49                 visibleGroupChildren.getOrNull(indexOfParentSwipedView - 1)
50                     ?: childrenContainer.notificationHeaderWrapper
51 
52             viewAfter =
53                 visibleGroupChildren.getOrNull(indexOfParentSwipedView + 1)
54                     ?: visibleStackChildren.indexOf(notificationParent).let {
55                         visibleStackChildren.getOrNull(it + 1)
56                     }
57         } else {
58             // Assumption: we are inside the NotificationStackScrollLayout
59 
60             val indexOfSwipedView = visibleStackChildren.indexOf(viewSwiped)
61 
62             viewBefore =
63                 visibleStackChildren.getOrNull(indexOfSwipedView - 1)?.takeIf {
64                     !sectionsManager.beginsSection(viewSwiped, it)
65                 }
66 
67             viewAfter =
68                 visibleStackChildren.getOrNull(indexOfSwipedView + 1)?.takeIf {
69                     !sectionsManager.beginsSection(it, viewSwiped)
70                 }
71         }
72 
73         return RoundableTargets(before = viewBefore, swiped = viewSwiped, after = viewAfter)
74     }
75 
76     /**
77      * This method looks for [ExpandableNotificationRow]s that can magnetically attach to a swiped
78      * [ExpandableNotificationRow] and returns their [MagneticRowListener]s in a list.
79      *
80      * The list contains the swiped row's listener at the center of the list. From the center
81      * towards the left, the list contains the closest notification neighbors above the swiped row.
82      * From the center towards the right, the list contains the closest neighbors below the row.
83      *
84      * The list is filled from the center outwards, stopping at the first neighbor that is not an
85      * [ExpandableNotificationRow]. In addition, the list does not cross the boundaries of a
86      * notification group. Positions where the list halted are filled with null.
87      *
88      * @param[viewSwiped] The [ExpandableNotificationRow] that is swiped.
89      * @param[stackScrollLayout] [NotificationStackScrollLayout] container.
90      * @param[sectionsManager] The [NotificationSectionsManager]
91      * @param[totalMagneticTargets] The total number of magnetic listeners in the resulting list.
92      *   This includes the listener of the view swiped.
93      * @return The list of [MagneticRowListener]s above and below the swiped
94      *   [ExpandableNotificationRow]
95      */
findMagneticTargetsnull96     fun findMagneticTargets(
97         viewSwiped: ExpandableNotificationRow,
98         stackScrollLayout: NotificationStackScrollLayout,
99         sectionsManager: NotificationSectionsManager,
100         totalMagneticTargets: Int,
101     ): List<MagneticRowListener?> {
102         val notificationParent = viewSwiped.notificationParent
103         val childrenContainer = notificationParent?.childrenContainer
104         val visibleStackChildren =
105             stackScrollLayout.children
106                 .filterIsInstance<ExpandableView>()
107                 .filter { it.isVisible }
108                 .toList()
109 
110         val container: List<ExpandableView> =
111             if (notificationParent != null && childrenContainer != null) {
112                 // We are inside a notification group
113                 childrenContainer.attachedChildren.filter { it.isVisible }
114             } else {
115                 visibleStackChildren
116             }
117 
118         // Construct the list of targets
119         val magneticTargets = MutableList<MagneticRowListener?>(totalMagneticTargets) { null }
120         magneticTargets[totalMagneticTargets / 2] = viewSwiped.magneticRowListener
121 
122         // Fill the list outwards from the center
123         val centerIndex = container.indexOf(viewSwiped)
124         var leftIndex = magneticTargets.size / 2 - 1
125         var rightIndex = magneticTargets.size / 2 + 1
126         var canMoveLeft = true
127         var canMoveRight = true
128         for (distance in 1..totalMagneticTargets / 2) {
129             if (canMoveLeft) {
130                 val leftElement =
131                     container.getOrNull(index = centerIndex - distance)?.takeIf {
132                         it.isValidMagneticBoundary() ||
133                             !sectionsManager.beginsSection(view = viewSwiped, previous = it)
134                     }
135                 if (leftElement is ExpandableNotificationRow) {
136                     magneticTargets[leftIndex] = leftElement.magneticRowListener
137                     leftIndex--
138                 } else {
139                     if (leftElement.isValidMagneticBoundary()) {
140                         // Add the boundary and then stop iterating
141                         magneticTargets[leftIndex] = leftElement?.magneticRowListener
142                     }
143                     canMoveLeft = false
144                 }
145             }
146             if (canMoveRight) {
147                 val rightElement =
148                     container.getOrNull(index = centerIndex + distance)?.takeIf {
149                         it.isValidMagneticBoundary() ||
150                             !sectionsManager.beginsSection(view = it, previous = viewSwiped)
151                     }
152                 if (rightElement is ExpandableNotificationRow) {
153                     magneticTargets[rightIndex] = rightElement.magneticRowListener
154                     rightIndex++
155                 } else {
156                     if (rightElement.isValidMagneticBoundary()) {
157                         // Add the boundary and then stop iterating
158                         magneticTargets[rightIndex] = rightElement?.magneticRowListener
159                     }
160                     canMoveRight = false
161                 }
162             }
163         }
164         return magneticTargets
165     }
166 
ExpandableViewnull167     private fun ExpandableView?.isValidMagneticBoundary(): Boolean =
168         when (this) {
169             is NotificationShelf,
170             is SectionHeaderView -> true
171             else -> false
172         }
173 }
174 
175 /**
176  * This object contains targets above/below the [swiped] (if available). The [before] and [after]
177  * parameters can be `null` if there is no above/below notification or the notification is not part
178  * of the same section.
179  */
180 data class RoundableTargets(
181     val before: Roundable?,
182     val swiped: ExpandableNotificationRow?,
183     val after: Roundable?,
184 )
185