1 /* 2 * Copyright (C) 2019 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.systemui.statusbar.notification.stack; 18 19 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; 20 21 import android.annotation.Nullable; 22 import android.content.Intent; 23 import android.provider.Settings; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.systemui.R; 29 import com.android.systemui.plugins.ActivityStarter; 30 import com.android.systemui.plugins.statusbar.StatusBarStateController; 31 import com.android.systemui.statusbar.StatusBarState; 32 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; 33 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 34 import com.android.systemui.statusbar.policy.ConfigurationController; 35 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; 36 37 /** 38 * Manages the boundaries of the two notification sections (high priority and low priority). Also 39 * shows/hides the headers for those sections where appropriate. 40 * 41 * TODO: Move remaining sections logic from NSSL into this class. 42 */ 43 class NotificationSectionsManager implements StackScrollAlgorithm.SectionProvider { 44 private final NotificationStackScrollLayout mParent; 45 private final ActivityStarter mActivityStarter; 46 private final StatusBarStateController mStatusBarStateController; 47 private final ConfigurationController mConfigurationController; 48 private final boolean mUseMultipleSections; 49 50 private boolean mInitialized = false; 51 private SectionHeaderView mGentleHeader; 52 private boolean mGentleHeaderVisible = false; 53 @Nullable private ExpandableNotificationRow mFirstGentleNotif; 54 @Nullable private View.OnClickListener mOnClearGentleNotifsClickListener; 55 NotificationSectionsManager( NotificationStackScrollLayout parent, ActivityStarter activityStarter, StatusBarStateController statusBarStateController, ConfigurationController configurationController, boolean useMultipleSections)56 NotificationSectionsManager( 57 NotificationStackScrollLayout parent, 58 ActivityStarter activityStarter, 59 StatusBarStateController statusBarStateController, 60 ConfigurationController configurationController, 61 boolean useMultipleSections) { 62 mParent = parent; 63 mActivityStarter = activityStarter; 64 mStatusBarStateController = statusBarStateController; 65 mConfigurationController = configurationController; 66 mUseMultipleSections = useMultipleSections; 67 } 68 69 /** Must be called before use. */ initialize(LayoutInflater layoutInflater)70 void initialize(LayoutInflater layoutInflater) { 71 if (mInitialized) { 72 throw new IllegalStateException("NotificationSectionsManager already initialized"); 73 } 74 mInitialized = true; 75 reinflateViews(layoutInflater); 76 mConfigurationController.addCallback(mConfigurationListener); 77 } 78 79 /** 80 * Reinflates the entire notification header, including all decoration views. 81 */ reinflateViews(LayoutInflater layoutInflater)82 void reinflateViews(LayoutInflater layoutInflater) { 83 int oldPos = -1; 84 if (mGentleHeader != null) { 85 if (mGentleHeader.getTransientContainer() != null) { 86 mGentleHeader.getTransientContainer().removeView(mGentleHeader); 87 } else if (mGentleHeader.getParent() != null) { 88 oldPos = mParent.indexOfChild(mGentleHeader); 89 mParent.removeView(mGentleHeader); 90 } 91 } 92 93 mGentleHeader = (SectionHeaderView) layoutInflater.inflate( 94 R.layout.status_bar_notification_section_header, mParent, false); 95 mGentleHeader.setOnHeaderClickListener(this::onGentleHeaderClick); 96 mGentleHeader.setOnClearAllClickListener(this::onClearGentleNotifsClick); 97 98 if (oldPos != -1) { 99 mParent.addView(mGentleHeader, oldPos); 100 } 101 } 102 103 /** Listener for when the "clear all" buttton is clciked on the gentle notification header. */ setOnClearGentleNotifsClickListener(View.OnClickListener listener)104 void setOnClearGentleNotifsClickListener(View.OnClickListener listener) { 105 mOnClearGentleNotifsClickListener = listener; 106 } 107 108 /** Must be called whenever the UI mode changes (i.e. when we enter night mode). */ onUiModeChanged()109 void onUiModeChanged() { 110 mGentleHeader.onUiModeChanged(); 111 } 112 113 @Override beginsSection(View view)114 public boolean beginsSection(View view) { 115 return view == getFirstLowPriorityChild(); 116 } 117 118 /** 119 * Should be called whenever notifs are added, removed, or updated. Updates section boundary 120 * bookkeeping and adds/moves/removes section headers if appropriate. 121 */ updateSectionBoundaries()122 void updateSectionBoundaries() { 123 if (!mUseMultipleSections) { 124 return; 125 } 126 127 mFirstGentleNotif = null; 128 int firstGentleNotifIndex = -1; 129 130 final int n = mParent.getChildCount(); 131 for (int i = 0; i < n; i++) { 132 View child = mParent.getChildAt(i); 133 if (child instanceof ExpandableNotificationRow 134 && child.getVisibility() != View.GONE) { 135 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 136 if (!row.getEntry().isTopBucket()) { 137 firstGentleNotifIndex = i; 138 mFirstGentleNotif = row; 139 break; 140 } 141 } 142 } 143 144 adjustGentleHeaderVisibilityAndPosition(firstGentleNotifIndex); 145 146 mGentleHeader.setAreThereDismissableGentleNotifs( 147 mParent.hasActiveClearableNotifications(ROWS_GENTLE)); 148 } 149 adjustGentleHeaderVisibilityAndPosition(int firstGentleNotifIndex)150 private void adjustGentleHeaderVisibilityAndPosition(int firstGentleNotifIndex) { 151 final boolean showGentleHeader = 152 firstGentleNotifIndex != -1 153 && mStatusBarStateController.getState() != StatusBarState.KEYGUARD; 154 final int currentHeaderIndex = mParent.indexOfChild(mGentleHeader); 155 156 if (!showGentleHeader) { 157 if (mGentleHeaderVisible) { 158 mGentleHeaderVisible = false; 159 mParent.removeView(mGentleHeader); 160 } 161 } else { 162 if (!mGentleHeaderVisible) { 163 mGentleHeaderVisible = true; 164 // If the header is animating away, it will still have a parent, so detach it first 165 // TODO: We should really cancel the active animations here. This will happen 166 // automatically when the view's intro animation starts, but it's a fragile link. 167 if (mGentleHeader.getTransientContainer() != null) { 168 mGentleHeader.getTransientContainer().removeTransientView(mGentleHeader); 169 mGentleHeader.setTransientContainer(null); 170 } 171 mParent.addView(mGentleHeader, firstGentleNotifIndex); 172 } else if (currentHeaderIndex != firstGentleNotifIndex - 1) { 173 // Relocate the header to be immediately before the first child in the section 174 int targetIndex = firstGentleNotifIndex; 175 if (currentHeaderIndex < firstGentleNotifIndex) { 176 // Adjust the target index to account for the header itself being temporarily 177 // removed during the position change. 178 targetIndex--; 179 } 180 181 mParent.changeViewPosition(mGentleHeader, targetIndex); 182 } 183 } 184 } 185 186 /** 187 * Updates the boundaries (as tracked by their first and last views) of the high and low 188 * priority sections. 189 * 190 * @return {@code true} If the last view in the top section changed (so we need to animate). 191 */ updateFirstAndLastViewsInSections( final NotificationSection highPrioritySection, final NotificationSection lowPrioritySection, ActivatableNotificationView firstChild, ActivatableNotificationView lastChild)192 boolean updateFirstAndLastViewsInSections( 193 final NotificationSection highPrioritySection, 194 final NotificationSection lowPrioritySection, 195 ActivatableNotificationView firstChild, 196 ActivatableNotificationView lastChild) { 197 if (mUseMultipleSections) { 198 ActivatableNotificationView previousLastHighPriorityChild = 199 highPrioritySection.getLastVisibleChild(); 200 ActivatableNotificationView previousFirstLowPriorityChild = 201 lowPrioritySection.getFirstVisibleChild(); 202 ActivatableNotificationView lastHighPriorityChild = getLastHighPriorityChild(); 203 ActivatableNotificationView firstLowPriorityChild = getFirstLowPriorityChild(); 204 if (lastHighPriorityChild != null && firstLowPriorityChild != null) { 205 highPrioritySection.setFirstVisibleChild(firstChild); 206 highPrioritySection.setLastVisibleChild(lastHighPriorityChild); 207 lowPrioritySection.setFirstVisibleChild(firstLowPriorityChild); 208 lowPrioritySection.setLastVisibleChild(lastChild); 209 } else if (lastHighPriorityChild != null) { 210 highPrioritySection.setFirstVisibleChild(firstChild); 211 highPrioritySection.setLastVisibleChild(lastChild); 212 lowPrioritySection.setFirstVisibleChild(null); 213 lowPrioritySection.setLastVisibleChild(null); 214 } else { 215 highPrioritySection.setFirstVisibleChild(null); 216 highPrioritySection.setLastVisibleChild(null); 217 lowPrioritySection.setFirstVisibleChild(firstChild); 218 lowPrioritySection.setLastVisibleChild(lastChild); 219 } 220 return lastHighPriorityChild != previousLastHighPriorityChild 221 || firstLowPriorityChild != previousFirstLowPriorityChild; 222 } else { 223 highPrioritySection.setFirstVisibleChild(firstChild); 224 highPrioritySection.setLastVisibleChild(lastChild); 225 return false; 226 } 227 } 228 229 @VisibleForTesting getGentleHeaderView()230 SectionHeaderView getGentleHeaderView() { 231 return mGentleHeader; 232 } 233 234 @Nullable getFirstLowPriorityChild()235 private ActivatableNotificationView getFirstLowPriorityChild() { 236 if (mGentleHeaderVisible) { 237 return mGentleHeader; 238 } else { 239 return mFirstGentleNotif; 240 } 241 } 242 243 @Nullable getLastHighPriorityChild()244 private ActivatableNotificationView getLastHighPriorityChild() { 245 ActivatableNotificationView lastChildBeforeGap = null; 246 int childCount = mParent.getChildCount(); 247 for (int i = 0; i < childCount; i++) { 248 View child = mParent.getChildAt(i); 249 if (child.getVisibility() != View.GONE && child instanceof ExpandableNotificationRow) { 250 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 251 if (!row.getEntry().isTopBucket()) { 252 break; 253 } else { 254 lastChildBeforeGap = row; 255 } 256 } 257 } 258 return lastChildBeforeGap; 259 } 260 261 private final ConfigurationListener mConfigurationListener = new ConfigurationListener() { 262 @Override 263 public void onLocaleListChanged() { 264 mGentleHeader.reinflateContents(); 265 } 266 }; 267 onGentleHeaderClick(View v)268 private void onGentleHeaderClick(View v) { 269 Intent intent = new Intent(Settings.ACTION_NOTIFICATION_SETTINGS); 270 mActivityStarter.startActivity( 271 intent, 272 true, 273 true, 274 Intent.FLAG_ACTIVITY_SINGLE_TOP); 275 } 276 onClearGentleNotifsClick(View v)277 private void onClearGentleNotifsClick(View v) { 278 if (mOnClearGentleNotifsClickListener != null) { 279 mOnClearGentleNotifsClickListener.onClick(v); 280 } 281 } 282 } 283