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