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