• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.collection.coordinator;
18 
19 import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
20 import static com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
21 
22 import static java.util.Objects.requireNonNull;
23 
24 import android.annotation.IntDef;
25 import android.os.RemoteException;
26 import android.os.Trace;
27 import android.service.notification.StatusBarNotification;
28 import android.util.ArrayMap;
29 import android.util.ArraySet;
30 import android.util.Log;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.statusbar.IStatusBarService;
37 import com.android.systemui.statusbar.notification.collection.BundleEntry;
38 import com.android.systemui.statusbar.notification.collection.GroupEntry;
39 import com.android.systemui.statusbar.notification.collection.ListEntry;
40 import com.android.systemui.statusbar.notification.collection.PipelineEntry;
41 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
42 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
43 import com.android.systemui.statusbar.notification.collection.ShadeListBuilder;
44 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
45 import com.android.systemui.statusbar.notification.collection.inflation.BindEventManagerImpl;
46 import com.android.systemui.statusbar.notification.collection.inflation.NotifInflater;
47 import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustment;
48 import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustmentProvider;
49 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
50 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
51 import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn;
52 import com.android.systemui.statusbar.notification.collection.render.NotifViewController;
53 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
54 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager.NotifInflationErrorListener;
55 import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
56 import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
57 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
58 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
59 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
60 
61 import java.lang.annotation.Retention;
62 import java.lang.annotation.RetentionPolicy;
63 import java.util.Collection;
64 import java.util.HashSet;
65 import java.util.List;
66 import java.util.Map;
67 import java.util.Set;
68 
69 import javax.inject.Inject;
70 
71 /**
72  * Kicks off core notification inflation and view rebinding when a notification is added or updated.
73  * Aborts inflation when a notification is removed.
74  *
75  * If a notification was uninflated, this coordinator will filter the notification out from the
76  * {@link ShadeListBuilder} until it is inflated.
77  */
78 @CoordinatorScope
79 public class PreparationCoordinator implements Coordinator {
80     private static final String TAG = "PreparationCoordinator";
81 
82     private final PreparationCoordinatorLogger mLogger;
83     private final NotifInflater mNotifInflater;
84     private final NotifInflationErrorManager mNotifErrorManager;
85     private final NotifViewBarn mViewBarn;
86     private final NotifUiAdjustmentProvider mAdjustmentProvider;
87     private final ArrayMap<NotificationEntry, Integer> mInflationStates = new ArrayMap<>();
88 
89     /**
90      * The map of notifications to the NotifUiAdjustment (i.e. parameters) that were calculated
91      * when the inflation started.  If an update of any kind results in the adjustment changing,
92      * then the row must be reinflated.  If the row is being inflated, then the inflation must be
93      * aborted and restarted.
94      */
95     private final ArrayMap<NotificationEntry, NotifUiAdjustment> mInflationAdjustments =
96             new ArrayMap<>();
97 
98     /**
99      * The set of notifications that are currently inflating something. Note that this is
100      * separate from inflation state as a view could either be uninflated or inflated and still be
101      * inflating something.
102      */
103     private final ArraySet<NotificationEntry> mInflatingNotifs = new ArraySet<>();
104 
105     private final IStatusBarService mStatusBarService;
106 
107     /**
108      * The number of children in a group we actually keep inflated since we don't actually show
109      * all the children and don't need every child inflated at all times.
110      */
111     private final int mChildBindCutoff;
112 
113     /** How long we can delay a group while waiting for all children to inflate */
114     private final long mMaxGroupInflationDelay;
115     private final BindEventManagerImpl mBindEventManager;
116     private final AppIconProvider mAppIconProvider;
117     private final NotificationIconStyleProvider mNotificationIconStyleProvider;
118 
119     @Inject
PreparationCoordinator( PreparationCoordinatorLogger logger, NotifInflater notifInflater, NotifInflationErrorManager errorManager, NotifViewBarn viewBarn, NotifUiAdjustmentProvider adjustmentProvider, IStatusBarService service, BindEventManagerImpl bindEventManager, AppIconProvider appIconProvider, NotificationIconStyleProvider notificationIconStyleProvider)120     public PreparationCoordinator(
121             PreparationCoordinatorLogger logger,
122             NotifInflater notifInflater,
123             NotifInflationErrorManager errorManager,
124             NotifViewBarn viewBarn,
125             NotifUiAdjustmentProvider adjustmentProvider,
126             IStatusBarService service,
127             BindEventManagerImpl bindEventManager,
128             AppIconProvider appIconProvider,
129             NotificationIconStyleProvider notificationIconStyleProvider) {
130         this(
131                 logger,
132                 notifInflater,
133                 errorManager,
134                 viewBarn,
135                 adjustmentProvider,
136                 service,
137                 bindEventManager,
138                 appIconProvider,
139                 notificationIconStyleProvider,
140                 CHILD_BIND_CUTOFF,
141                 MAX_GROUP_INFLATION_DELAY);
142     }
143 
144     @VisibleForTesting
PreparationCoordinator( PreparationCoordinatorLogger logger, NotifInflater notifInflater, NotifInflationErrorManager errorManager, NotifViewBarn viewBarn, NotifUiAdjustmentProvider adjustmentProvider, IStatusBarService service, BindEventManagerImpl bindEventManager, AppIconProvider appIconProvider, NotificationIconStyleProvider notificationIconStyleProvider, int childBindCutoff, long maxGroupInflationDelay)145     PreparationCoordinator(
146             PreparationCoordinatorLogger logger,
147             NotifInflater notifInflater,
148             NotifInflationErrorManager errorManager,
149             NotifViewBarn viewBarn,
150             NotifUiAdjustmentProvider adjustmentProvider,
151             IStatusBarService service,
152             BindEventManagerImpl bindEventManager,
153             AppIconProvider appIconProvider,
154             NotificationIconStyleProvider notificationIconStyleProvider,
155             int childBindCutoff,
156             long maxGroupInflationDelay) {
157         mLogger = logger;
158         mNotifInflater = notifInflater;
159         mNotifErrorManager = errorManager;
160         mViewBarn = viewBarn;
161         mAdjustmentProvider = adjustmentProvider;
162         mStatusBarService = service;
163         mChildBindCutoff = childBindCutoff;
164         mMaxGroupInflationDelay = maxGroupInflationDelay;
165         mBindEventManager = bindEventManager;
166         mAppIconProvider = appIconProvider;
167         mNotificationIconStyleProvider = notificationIconStyleProvider;
168     }
169 
170     @Override
attach(NotifPipeline pipeline)171     public void attach(NotifPipeline pipeline) {
172         mNotifErrorManager.addInflationErrorListener(mInflationErrorListener);
173         mAdjustmentProvider.addDirtyListener(
174                 () -> mNotifInflatingFilter.invalidateList("adjustmentProviderChanged"));
175 
176         pipeline.addCollectionListener(mNotifCollectionListener);
177         if (android.app.Flags.notificationsRedesignAppIcons()) {
178             pipeline.addOnBeforeTransformGroupsListener(this::purgeCaches);
179         }
180         // Inflate after grouping/sorting since that affects what views to inflate.
181         pipeline.addOnBeforeFinalizeFilterListener(this::inflateAllRequiredViews);
182         pipeline.addFinalizeFilter(mNotifInflationErrorFilter);
183         pipeline.addFinalizeFilter(mNotifInflatingFilter);
184     }
185 
186     private final NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() {
187 
188         @Override
189         public void onEntryInit(NotificationEntry entry) {
190             mInflationStates.put(entry, STATE_UNINFLATED);
191         }
192 
193         @Override
194         public void onEntryUpdated(NotificationEntry entry) {
195             abortInflation(entry, "entryUpdated");
196             @InflationState int state = getInflationState(entry);
197             if (state == STATE_INFLATED) {
198                 mInflationStates.put(entry, STATE_INFLATED_INVALID);
199             } else if (state == STATE_ERROR) {
200                 // Updated so maybe it won't error out now.
201                 mInflationStates.put(entry, STATE_UNINFLATED);
202             }
203         }
204 
205         @Override
206         public void onEntryRemoved(NotificationEntry entry, int reason) {
207             abortInflation(entry, "entryRemoved reason=" + reason);
208         }
209 
210         @Override
211         public void onEntryCleanUp(NotificationEntry entry) {
212             mInflationStates.remove(entry);
213             mViewBarn.removeViewForEntry(entry);
214             mInflationAdjustments.remove(entry);
215         }
216     };
217 
218     private final NotifFilter mNotifInflationErrorFilter = new NotifFilter(
219             TAG + "InflationError") {
220         /**
221          * Filters out notifications that threw an error when attempting to inflate.
222          */
223         @Override
224         public boolean shouldFilterOut(NotificationEntry entry, long now) {
225             return getInflationState(entry) == STATE_ERROR;
226         }
227     };
228 
229     private final NotifFilter mNotifInflatingFilter = new NotifFilter(TAG + "Inflating") {
230         private final Map<GroupEntry, Boolean> mIsDelayedGroupCache = new ArrayMap<>();
231 
232         /**
233          * Filters out notifications that either (a) aren't inflated or (b) are part of a group
234          * that isn't completely inflated yet
235          */
236         @Override
237         public boolean shouldFilterOut(NotificationEntry entry, long now) {
238             final PipelineEntry pipelineEntryParent = requireNonNull(entry.getParent());
239             Boolean isMemberOfDelayedGroup = mIsDelayedGroupCache.get(pipelineEntryParent);
240             if (isMemberOfDelayedGroup == null && pipelineEntryParent instanceof GroupEntry) {
241                 GroupEntry parent = (GroupEntry) pipelineEntryParent;
242                 isMemberOfDelayedGroup = shouldWaitForGroupToInflate(parent, now);
243                 mIsDelayedGroupCache.put(parent, isMemberOfDelayedGroup);
244             }
245             return !isInflated(entry) || (isMemberOfDelayedGroup != null && isMemberOfDelayedGroup);
246         }
247 
248         @Override
249         public void onCleanup() {
250             mIsDelayedGroupCache.clear();
251         }
252     };
253 
254     private final NotifInflationErrorListener mInflationErrorListener =
255             new NotifInflationErrorListener() {
256         @Override
257         public void onNotifInflationError(NotificationEntry entry, Exception e) {
258             mViewBarn.removeViewForEntry(entry);
259             mInflationStates.put(entry, STATE_ERROR);
260             try {
261                 final StatusBarNotification sbn = entry.getSbn();
262                 // report notification inflation errors back up
263                 // to notification delegates
264                 mStatusBarService.onNotificationError(
265                         sbn.getPackageName(),
266                         sbn.getTag(),
267                         sbn.getId(),
268                         sbn.getUid(),
269                         sbn.getInitialPid(),
270                         e.getMessage(),
271                         sbn.getUser().getIdentifier());
272             } catch (RemoteException ex) {
273                 // System server is dead, nothing to do about that
274             }
275             mNotifInflationErrorFilter.invalidateList("onNotifInflationError for " + logKey(entry));
276         }
277 
278         @Override
279         public void onNotifInflationErrorCleared(NotificationEntry entry) {
280             mNotifInflationErrorFilter.invalidateList(
281                     "onNotifInflationErrorCleared for " + logKey(entry));
282         }
283     };
284 
purgeCaches(Collection<PipelineEntry> entries)285     private void purgeCaches(Collection<PipelineEntry> entries) {
286         Set<String> wantedPackages = getPackages(entries);
287         mAppIconProvider.purgeCache(wantedPackages);
288         mNotificationIconStyleProvider.purgeCache(wantedPackages);
289     }
290 
291     /**
292      * Get all app packages present in {@param entries}.
293      */
getPackages(Collection<PipelineEntry> entries)294     private static @NonNull Set<String> getPackages(Collection<PipelineEntry> entries) {
295         Set<String> packages = new HashSet<>();
296         for (PipelineEntry entry : entries) {
297             NotificationEntry notificationEntry = entry.getRepresentativeEntry();
298             if (notificationEntry == null) {
299                 Log.wtf(TAG, "notification entry " + entry.getKey()
300                         + " has no representative entry");
301                 continue;
302             }
303             packages.add(notificationEntry.getSbn().getPackageName());
304         }
305         return packages;
306     }
307 
inflateAllRequiredViews(List<PipelineEntry> entries)308     private void inflateAllRequiredViews(List<PipelineEntry> entries) {
309         for (int i = 0, size = entries.size(); i < size; i++) {
310             PipelineEntry entry = entries.get(i);
311             if (NotificationBundleUi.isEnabled() && entry instanceof BundleEntry bundleEntry) {
312                 for (ListEntry listEntry : bundleEntry.getChildren()) {
313                     if (listEntry instanceof GroupEntry groupEntry) {
314                         inflateRequiredGroupViews(groupEntry);
315                     } else {
316                         NotificationEntry notifEntry = (NotificationEntry) listEntry;
317                         inflateRequiredNotifViews(notifEntry);
318                     }
319                 }
320             } else if (entry instanceof GroupEntry) {
321                 GroupEntry groupEntry = (GroupEntry) entry;
322                 inflateRequiredGroupViews(groupEntry);
323             } else {
324                 NotificationEntry notifEntry = (NotificationEntry) entry;
325                 inflateRequiredNotifViews(notifEntry);
326             }
327         }
328     }
329 
inflateRequiredGroupViews(GroupEntry groupEntry)330     private void inflateRequiredGroupViews(GroupEntry groupEntry) {
331         NotificationEntry summary = groupEntry.getSummary();
332         if (summary != null && AsyncGroupHeaderViewInflation.isEnabled()) {
333             summary.markAsGroupSummary();
334         }
335         List<NotificationEntry> children = groupEntry.getChildren();
336         inflateRequiredNotifViews(summary);
337         for (int j = 0; j < children.size(); j++) {
338             NotificationEntry child = children.get(j);
339             if (AsyncHybridViewInflation.isEnabled()) child.markAsGroupChild();
340             boolean childShouldBeBound = j < mChildBindCutoff;
341             if (childShouldBeBound) {
342                 inflateRequiredNotifViews(child);
343             } else {
344                 if (mInflatingNotifs.contains(child)) {
345                     abortInflation(child, "Past last visible group child");
346                 }
347                 if (isInflated(child)) {
348                     // TODO: May want to put an animation hint here so view manager knows to treat
349                     //  this differently from a regular removal animation
350                     freeNotifViews(child, "Past last visible group child");
351                 }
352             }
353         }
354     }
355 
356     private void inflateRequiredNotifViews(NotificationEntry entry) {
357         NotifUiAdjustment newAdjustment = mAdjustmentProvider.calculateAdjustment(entry);
358         if (mInflatingNotifs.contains(entry)) {
359             // Already inflating this entry
360             String errorIfNoOldAdjustment = "Inflating notification has no adjustments";
361             if (needToReinflate(entry, newAdjustment, errorIfNoOldAdjustment)) {
362                 inflateEntry(entry, newAdjustment, "adjustment changed while inflating");
363             }
364             return;
365         }
366         @InflationState int state = mInflationStates.get(entry);
367         switch (state) {
368             case STATE_UNINFLATED:
369                 inflateEntry(entry, newAdjustment, "entryAdded");
370                 break;
371             case STATE_INFLATED_INVALID:
372                 rebind(entry, newAdjustment, "entryUpdated");
373                 break;
374             case STATE_INFLATED:
375                 String errorIfNoOldAdjustment = "Fully inflated notification has no adjustments";
376                 if (needToReinflate(entry, newAdjustment, errorIfNoOldAdjustment)) {
377                     rebind(entry, newAdjustment, "adjustment changed after inflated");
378                 }
379                 break;
380             case STATE_ERROR:
381                 if (needToReinflate(entry, newAdjustment, null)) {
382                     inflateEntry(entry, newAdjustment, "adjustment changed after error");
383                 }
384                 break;
385             default:
386                 // Nothing to do.
387         }
388     }
389 
390     private boolean needToReinflate(@NonNull NotificationEntry entry,
391             @NonNull NotifUiAdjustment newAdjustment, @Nullable String oldAdjustmentMissingError) {
392         NotifUiAdjustment oldAdjustment = mInflationAdjustments.get(entry);
393         if (oldAdjustment == null) {
394             if (oldAdjustmentMissingError == null) {
395                 return true;
396             } else {
397                 throw new IllegalStateException(oldAdjustmentMissingError);
398             }
399         }
400         return NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment);
401     }
402 
403     private void inflateEntry(NotificationEntry entry,
404             NotifUiAdjustment newAdjustment,
405             String reason) {
406         Trace.beginSection("PrepCoord.inflateEntry");
407         abortInflation(entry, reason);
408         mInflationAdjustments.put(entry, newAdjustment);
409         mInflatingNotifs.add(entry);
410         NotifInflater.Params params = getInflaterParams(newAdjustment, reason);
411         mNotifInflater.inflateViews(entry, params, this::onInflationFinished);
412         Trace.endSection();
413     }
414 
415     private void rebind(NotificationEntry entry,
416             NotifUiAdjustment newAdjustment,
417             String reason) {
418         mInflationAdjustments.put(entry, newAdjustment);
419         mInflatingNotifs.add(entry);
420         NotifInflater.Params params = getInflaterParams(newAdjustment, reason);
421         mNotifInflater.rebindViews(entry, params, this::onInflationFinished);
422     }
423 
424     NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) {
425         return new NotifInflater.Params(
426                 /* isMinimized = */ adjustment.isMinimized(),
427                 /* reason = */ reason,
428                 /* showSnooze = */ adjustment.isSnoozeEnabled(),
429                 /* isChildInGroup = */ adjustment.isChildInGroup(),
430                 /* isGroupSummary = */ adjustment.isGroupSummary(),
431                 /* needsRedaction = */ adjustment.getRedactionType()
432         );
433     }
434 
435     private void abortInflation(NotificationEntry entry, String reason) {
436         final boolean taskAborted = mNotifInflater.abortInflation(entry);
437         final boolean wasInflating = mInflatingNotifs.remove(entry);
438         if (taskAborted || wasInflating) {
439             mLogger.logInflationAborted(entry, reason);
440         }
441     }
442 
443     private void onInflationFinished(NotificationEntry entry, NotifViewController controller) {
444         mLogger.logNotifInflated(entry);
445         mInflatingNotifs.remove(entry);
446         mViewBarn.registerViewForEntry(entry, controller);
447         mInflationStates.put(entry, STATE_INFLATED);
448         mBindEventManager.notifyViewBound(entry);
449         mNotifInflatingFilter.invalidateList("onInflationFinished for " + logKey(entry));
450     }
451 
452     private void freeNotifViews(NotificationEntry entry, String reason) {
453         mLogger.logFreeNotifViews(entry, reason);
454         mViewBarn.removeViewForEntry(entry);
455         mNotifInflater.releaseViews(entry);
456         // TODO: clear the entry's row here, or even better, stop setting the row on the entry!
457         mInflationStates.put(entry, STATE_UNINFLATED);
458     }
459 
460     private boolean isInflated(NotificationEntry entry) {
461         @InflationState int state = getInflationState(entry);
462         return (state == STATE_INFLATED) || (state == STATE_INFLATED_INVALID);
463     }
464 
465     private @InflationState int getInflationState(NotificationEntry entry) {
466         Integer stateObj = mInflationStates.get(entry);
467         requireNonNull(stateObj,
468                 "Asking state of a notification preparation coordinator doesn't know about");
469         return stateObj;
470     }
471 
472     private boolean shouldWaitForGroupToInflate(GroupEntry group, long now) {
473         if (group == GroupEntry.ROOT_ENTRY || group.wasAttachedInPreviousPass()) {
474             return false;
475         }
476         if (isBeyondGroupInitializationWindow(group, now)) {
477             mLogger.logGroupInflationTookTooLong(group);
478             return false;
479         }
480         // Only delay release if the summary is not inflated.
481         // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been
482         //  done by this point, we can revert back to checking for mInflatingNotifs.contains(...)
483         if (group.getSummary() != null && !isInflated(group.getSummary())) {
484             mLogger.logDelayingGroupRelease(group, group.getSummary());
485             return true;
486         }
487         for (NotificationEntry child : group.getChildren()) {
488             if (mInflatingNotifs.contains(child) && !child.wasAttachedInPreviousPass()) {
489                 mLogger.logDelayingGroupRelease(group, child);
490                 return true;
491             }
492         }
493         mLogger.logDoneWaitingForGroupInflation(group);
494         return false;
495     }
496 
497     private boolean isBeyondGroupInitializationWindow(GroupEntry entry, long now) {
498         return now - entry.getCreationTime() > mMaxGroupInflationDelay;
499     }
500 
501     @Retention(RetentionPolicy.SOURCE)
502     @IntDef(prefix = {"STATE_"},
503             value = {STATE_UNINFLATED, STATE_INFLATED_INVALID, STATE_INFLATED, STATE_ERROR})
504     @interface InflationState {}
505 
506     /** The notification has no views attached. */
507     private static final int STATE_UNINFLATED = 0;
508 
509     /** The notification is inflated. */
510     private static final int STATE_INFLATED = 1;
511 
512     /**
513      * The notification is inflated, but its content may be out-of-date since the notification has
514      * been updated.
515      */
516     private static final int STATE_INFLATED_INVALID = 2;
517 
518     /** The notification errored out while inflating */
519     private static final int STATE_ERROR = -1;
520 
521     /**
522      * How big the buffer of extra views we keep around to be ready to show when we do need to
523      * dynamically inflate a row.
524      */
525     private static final int EXTRA_VIEW_BUFFER_COUNT = 1;
526 
527     private static final long MAX_GROUP_INFLATION_DELAY = 500;
528 
529     private static final int CHILD_BIND_CUTOFF =
530             NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED + EXTRA_VIEW_BUFFER_COUNT;
531 }
532