• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.row;
18 
19 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_AMBIENT;
20 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED;
21 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
22 
23 import android.annotation.IntDef;
24 import android.annotation.Nullable;
25 import android.app.Notification;
26 import android.content.Context;
27 import android.os.AsyncTask;
28 import android.os.CancellationSignal;
29 import android.service.notification.StatusBarNotification;
30 import android.util.ArrayMap;
31 import android.util.Log;
32 import android.view.View;
33 import android.widget.RemoteViews;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.widget.ImageMessageConsumer;
37 import com.android.systemui.Dependency;
38 import com.android.systemui.statusbar.InflationTask;
39 import com.android.systemui.statusbar.SmartReplyController;
40 import com.android.systemui.statusbar.notification.InflationException;
41 import com.android.systemui.statusbar.notification.MediaNotificationProcessor;
42 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
43 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
44 import com.android.systemui.statusbar.phone.StatusBar;
45 import com.android.systemui.statusbar.policy.HeadsUpManager;
46 import com.android.systemui.statusbar.policy.InflatedSmartReplies;
47 import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions;
48 import com.android.systemui.statusbar.policy.SmartReplyConstants;
49 import com.android.systemui.util.Assert;
50 
51 import java.lang.annotation.Retention;
52 import java.lang.annotation.RetentionPolicy;
53 import java.util.HashMap;
54 import java.util.concurrent.Executor;
55 import java.util.concurrent.LinkedBlockingQueue;
56 import java.util.concurrent.ThreadFactory;
57 import java.util.concurrent.ThreadPoolExecutor;
58 import java.util.concurrent.TimeUnit;
59 import java.util.concurrent.atomic.AtomicInteger;
60 
61 /**
62  * A utility that inflates the right kind of contentView based on the state
63  */
64 public class NotificationContentInflater {
65 
66     public static final String TAG = "NotifContentInflater";
67 
68     @Retention(RetentionPolicy.SOURCE)
69     @IntDef(flag = true,
70             prefix = {"FLAG_CONTENT_VIEW_"},
71             value = {
72                 FLAG_CONTENT_VIEW_CONTRACTED,
73                 FLAG_CONTENT_VIEW_EXPANDED,
74                 FLAG_CONTENT_VIEW_HEADS_UP,
75                 FLAG_CONTENT_VIEW_AMBIENT,
76                 FLAG_CONTENT_VIEW_PUBLIC,
77                 FLAG_CONTENT_VIEW_ALL})
78     public @interface InflationFlag {}
79     /**
80      * The default, contracted view.  Seen when the shade is pulled down and in the lock screen
81      * if there is no worry about content sensitivity.
82      */
83     public static final int FLAG_CONTENT_VIEW_CONTRACTED = 1;
84 
85     /**
86      * The expanded view.  Seen when the user expands a notification.
87      */
88     public static final int FLAG_CONTENT_VIEW_EXPANDED = 1 << 1;
89 
90     /**
91      * The heads up view.  Seen when a high priority notification peeks in from the top.
92      */
93     public static final int FLAG_CONTENT_VIEW_HEADS_UP = 1 << 2;
94 
95     /**
96      * The ambient view.  Seen when a high priority notification is received and the phone
97      * is dozing.
98      */
99     public static final int FLAG_CONTENT_VIEW_AMBIENT = 1 << 3;
100 
101     /**
102      * The public view.  This is a version of the contracted view that hides sensitive
103      * information and is used on the lock screen if we determine that the notification's
104      * content should be hidden.
105      */
106     public static final int FLAG_CONTENT_VIEW_PUBLIC = 1 << 4;
107 
108     public static final int FLAG_CONTENT_VIEW_ALL = ~0;
109 
110     /**
111      * Content views that must be inflated at all times.
112      */
113     @InflationFlag
114     private static final int REQUIRED_INFLATION_FLAGS =
115             FLAG_CONTENT_VIEW_CONTRACTED
116             | FLAG_CONTENT_VIEW_EXPANDED;
117 
118     /**
119      * The set of content views to inflate.
120      */
121     @InflationFlag
122     private int mInflationFlags = REQUIRED_INFLATION_FLAGS;
123 
124     private final ExpandableNotificationRow mRow;
125     private boolean mIsLowPriority;
126     private boolean mUsesIncreasedHeight;
127     private boolean mUsesIncreasedHeadsUpHeight;
128     private RemoteViews.OnClickHandler mRemoteViewClickHandler;
129     private boolean mIsChildInGroup;
130     private InflationCallback mCallback;
131     private boolean mRedactAmbient;
132     private boolean mInflateSynchronously = false;
133     private final ArrayMap<Integer, RemoteViews> mCachedContentViews = new ArrayMap<>();
134 
NotificationContentInflater(ExpandableNotificationRow row)135     public NotificationContentInflater(ExpandableNotificationRow row) {
136         mRow = row;
137     }
138 
setIsLowPriority(boolean isLowPriority)139     public void setIsLowPriority(boolean isLowPriority) {
140         mIsLowPriority = isLowPriority;
141     }
142 
143     /**
144      * Set whether the notification is a child in a group
145      *
146      * @return whether the view was re-inflated
147      */
setIsChildInGroup(boolean childInGroup)148     public void setIsChildInGroup(boolean childInGroup) {
149         if (childInGroup != mIsChildInGroup) {
150             mIsChildInGroup = childInGroup;
151             if (mIsLowPriority) {
152                 int flags = FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED;
153                 inflateNotificationViews(flags);
154             }
155         }
156     }
157 
setUsesIncreasedHeight(boolean usesIncreasedHeight)158     public void setUsesIncreasedHeight(boolean usesIncreasedHeight) {
159         mUsesIncreasedHeight = usesIncreasedHeight;
160     }
161 
setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight)162     public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) {
163         mUsesIncreasedHeadsUpHeight = usesIncreasedHeight;
164     }
165 
setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler)166     public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) {
167         mRemoteViewClickHandler = remoteViewClickHandler;
168     }
169 
170     /**
171      * Update whether or not the notification is redacted on the lock screen.  If the notification
172      * is now redacted, we should inflate the public contracted view and public ambient view to
173      * now show on the lock screen.
174      *
175      * @param needsRedaction true if the notification should now be redacted on the lock screen
176      */
updateNeedsRedaction(boolean needsRedaction)177     public void updateNeedsRedaction(boolean needsRedaction) {
178         mRedactAmbient = needsRedaction;
179         if (mRow.getEntry() == null) {
180             return;
181         }
182         int flags = FLAG_CONTENT_VIEW_AMBIENT;
183         if (needsRedaction) {
184             flags |= FLAG_CONTENT_VIEW_PUBLIC;
185         }
186         inflateNotificationViews(flags);
187     }
188 
189     /**
190      * Set whether or not a particular content view is needed and whether or not it should be
191      * inflated.  These flags will be used when we inflate or reinflate.
192      *
193      * @param flag the {@link InflationFlag} corresponding to the view that should/should not be
194      *             inflated
195      * @param shouldInflate true if the view should be inflated, false otherwise
196      */
updateInflationFlag(@nflationFlag int flag, boolean shouldInflate)197     public void updateInflationFlag(@InflationFlag int flag, boolean shouldInflate) {
198         if (shouldInflate) {
199             mInflationFlags |= flag;
200         } else if ((REQUIRED_INFLATION_FLAGS & flag) == 0) {
201             mInflationFlags &= ~flag;
202         }
203     }
204 
205     /**
206      * Convenience method for setting multiple flags at once.
207      *
208      * @param flags a set of {@link InflationFlag} corresponding to content views that should be
209      *              inflated
210      */
211     @VisibleForTesting
addInflationFlags(@nflationFlag int flags)212     public void addInflationFlags(@InflationFlag int flags) {
213         mInflationFlags |= flags;
214     }
215 
216     /**
217      * Whether or not the view corresponding to the flag is set to be inflated currently.
218      *
219      * @param flag the {@link InflationFlag} corresponding to the view
220      * @return true if the flag is set and view will be inflated, false o/w
221      */
isInflationFlagSet(@nflationFlag int flag)222     public boolean isInflationFlagSet(@InflationFlag int flag) {
223         return ((mInflationFlags & flag) != 0);
224     }
225 
226     /**
227      * Inflate views for set flags on a background thread. This is asynchronous and will
228      * notify the callback once it's finished.
229      */
inflateNotificationViews()230     public void inflateNotificationViews() {
231         inflateNotificationViews(mInflationFlags);
232     }
233 
234     /**
235      * Inflate all views for the specified flags on a background thread.  This is asynchronous and
236      * will notify the callback once it's finished.  If the content view is already inflated, this
237      * will reinflate it.
238      *
239      * @param reInflateFlags flags which views should be inflated. Should be a subset of
240      *                       {@link #mInflationFlags} as only those will be inflated/reinflated.
241      */
inflateNotificationViews(@nflationFlag int reInflateFlags)242     private void inflateNotificationViews(@InflationFlag int reInflateFlags) {
243         if (mRow.isRemoved()) {
244             // We don't want to reinflate anything for removed notifications. Otherwise views might
245             // be readded to the stack, leading to leaks. This may happen with low-priority groups
246             // where the removal of already removed children can lead to a reinflation.
247             return;
248         }
249         // Only inflate the ones that are set.
250         reInflateFlags &= mInflationFlags;
251         StatusBarNotification sbn = mRow.getEntry().notification;
252 
253         // To check if the notification has inline image and preload inline image if necessary.
254         mRow.getImageResolver().preloadImages(sbn.getNotification());
255 
256         AsyncInflationTask task = new AsyncInflationTask(
257                 sbn,
258                 mInflateSynchronously,
259                 reInflateFlags,
260                 mCachedContentViews,
261                 mRow,
262                 mIsLowPriority,
263                 mIsChildInGroup,
264                 mUsesIncreasedHeight,
265                 mUsesIncreasedHeadsUpHeight,
266                 mRedactAmbient,
267                 mCallback,
268                 mRemoteViewClickHandler);
269         if (mInflateSynchronously) {
270             task.onPostExecute(task.doInBackground());
271         } else {
272             task.execute();
273         }
274     }
275 
276     @VisibleForTesting
inflateNotificationViews( boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, Context packageContext)277     InflationProgress inflateNotificationViews(
278             boolean inflateSynchronously,
279             @InflationFlag int reInflateFlags,
280             Notification.Builder builder,
281             Context packageContext) {
282         InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority,
283                 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
284                 mRedactAmbient, packageContext);
285         result = inflateSmartReplyViews(result, reInflateFlags, mRow.getEntry(),
286                 mRow.getContext(), mRow.getHeadsUpManager(),
287                 mRow.getExistingSmartRepliesAndActions());
288         apply(
289                 inflateSynchronously,
290                 result,
291                 reInflateFlags,
292                 mCachedContentViews,
293                 mRow,
294                 mRedactAmbient,
295                 mRemoteViewClickHandler,
296                 null);
297         return result;
298     }
299 
300     /**
301      * Frees the content view associated with the inflation flag.  Will only succeed if the
302      * view is safe to remove.
303      *
304      * @param inflateFlag the flag corresponding to the content view which should be freed
305      */
freeNotificationView(@nflationFlag int inflateFlag)306     public void freeNotificationView(@InflationFlag int inflateFlag) {
307         if ((mInflationFlags & inflateFlag) != 0) {
308             // The view should still be inflated.
309             return;
310         }
311         switch (inflateFlag) {
312             case FLAG_CONTENT_VIEW_HEADS_UP:
313                 if (mRow.getPrivateLayout().isContentViewInactive(VISIBLE_TYPE_HEADSUP)) {
314                     mRow.getPrivateLayout().setHeadsUpChild(null);
315                     mCachedContentViews.remove(FLAG_CONTENT_VIEW_HEADS_UP);
316                     mRow.getPrivateLayout().setHeadsUpInflatedSmartReplies(null);
317                 }
318                 break;
319             case FLAG_CONTENT_VIEW_AMBIENT:
320                 boolean privateSafeToRemove = mRow.getPrivateLayout().isContentViewInactive(
321                         VISIBLE_TYPE_AMBIENT);
322                 boolean publicSafeToRemove = mRow.getPublicLayout().isContentViewInactive(
323                         VISIBLE_TYPE_AMBIENT);
324                 if (privateSafeToRemove) {
325                     mRow.getPrivateLayout().setAmbientChild(null);
326                 }
327                 if (publicSafeToRemove) {
328                     mRow.getPublicLayout().setAmbientChild(null);
329                 }
330                 if (privateSafeToRemove && publicSafeToRemove) {
331                     mCachedContentViews.remove(FLAG_CONTENT_VIEW_AMBIENT);
332                 }
333                 break;
334             case FLAG_CONTENT_VIEW_PUBLIC:
335                 if (mRow.getPublicLayout().isContentViewInactive(VISIBLE_TYPE_CONTRACTED)) {
336                     mRow.getPublicLayout().setContractedChild(null);
337                     mCachedContentViews.remove(FLAG_CONTENT_VIEW_PUBLIC);
338                 }
339                 break;
340             case FLAG_CONTENT_VIEW_CONTRACTED:
341             case FLAG_CONTENT_VIEW_EXPANDED:
342             default:
343                 break;
344         }
345     }
346 
inflateSmartReplyViews(InflationProgress result, @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, HeadsUpManager headsUpManager, SmartRepliesAndActions previousSmartRepliesAndActions)347     private static InflationProgress inflateSmartReplyViews(InflationProgress result,
348             @InflationFlag int reInflateFlags, NotificationEntry entry, Context context,
349             HeadsUpManager headsUpManager, SmartRepliesAndActions previousSmartRepliesAndActions) {
350         SmartReplyConstants smartReplyConstants = Dependency.get(SmartReplyConstants.class);
351         SmartReplyController smartReplyController = Dependency.get(SmartReplyController.class);
352         if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 && result.newExpandedView != null) {
353             result.expandedInflatedSmartReplies =
354                     InflatedSmartReplies.inflate(
355                             context, entry, smartReplyConstants, smartReplyController,
356                             headsUpManager, previousSmartRepliesAndActions);
357         }
358         if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 && result.newHeadsUpView != null) {
359             result.headsUpInflatedSmartReplies =
360                     InflatedSmartReplies.inflate(
361                             context, entry, smartReplyConstants, smartReplyController,
362                             headsUpManager, previousSmartRepliesAndActions);
363         }
364         return result;
365     }
366 
createRemoteViews(@nflationFlag int reInflateFlags, Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, Context packageContext)367     private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags,
368             Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup,
369             boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
370             Context packageContext) {
371         InflationProgress result = new InflationProgress();
372         isLowPriority = isLowPriority && !isChildInGroup;
373 
374         if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
375             result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);
376         }
377 
378         if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
379             result.newExpandedView = createExpandedView(builder, isLowPriority);
380         }
381 
382         if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
383             result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);
384         }
385 
386         if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
387             result.newPublicView = builder.makePublicContentView();
388         }
389 
390         if ((reInflateFlags & FLAG_CONTENT_VIEW_AMBIENT) != 0) {
391             result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification()
392                     : builder.makeAmbientNotification();
393         }
394         result.packageContext = packageContext;
395         result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */);
396         result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
397                 true /* showingPublic */);
398         return result;
399     }
400 
apply( boolean inflateSynchronously, InflationProgress result, @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews, ExpandableNotificationRow row, boolean redactAmbient, RemoteViews.OnClickHandler remoteViewClickHandler, @Nullable InflationCallback callback)401     public static CancellationSignal apply(
402             boolean inflateSynchronously,
403             InflationProgress result,
404             @InflationFlag int reInflateFlags,
405             ArrayMap<Integer, RemoteViews> cachedContentViews,
406             ExpandableNotificationRow row,
407             boolean redactAmbient,
408             RemoteViews.OnClickHandler remoteViewClickHandler,
409             @Nullable InflationCallback callback) {
410         NotificationContentView privateLayout = row.getPrivateLayout();
411         NotificationContentView publicLayout = row.getPublicLayout();
412         final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
413 
414         int flag = FLAG_CONTENT_VIEW_CONTRACTED;
415         if ((reInflateFlags & flag) != 0) {
416             boolean isNewView =
417                     !canReapplyRemoteView(result.newContentView,
418                             cachedContentViews.get(FLAG_CONTENT_VIEW_CONTRACTED));
419             ApplyCallback applyCallback = new ApplyCallback() {
420                 @Override
421                 public void setResultView(View v) {
422                     result.inflatedContentView = v;
423                 }
424 
425                 @Override
426                 public RemoteViews getRemoteView() {
427                     return result.newContentView;
428                 }
429             };
430             applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, cachedContentViews,
431                     row, redactAmbient, isNewView, remoteViewClickHandler, callback, privateLayout,
432                     privateLayout.getContractedChild(), privateLayout.getVisibleWrapper(
433                             NotificationContentView.VISIBLE_TYPE_CONTRACTED),
434                     runningInflations, applyCallback);
435         }
436 
437         flag = FLAG_CONTENT_VIEW_EXPANDED;
438         if ((reInflateFlags & flag) != 0) {
439             if (result.newExpandedView != null) {
440                 boolean isNewView =
441                         !canReapplyRemoteView(result.newExpandedView,
442                                 cachedContentViews.get(FLAG_CONTENT_VIEW_EXPANDED));
443                 ApplyCallback applyCallback = new ApplyCallback() {
444                     @Override
445                     public void setResultView(View v) {
446                         result.inflatedExpandedView = v;
447                     }
448 
449                     @Override
450                     public RemoteViews getRemoteView() {
451                         return result.newExpandedView;
452                     }
453                 };
454                 applyRemoteView(inflateSynchronously, result, reInflateFlags, flag,
455                         cachedContentViews, row, redactAmbient, isNewView, remoteViewClickHandler,
456                         callback, privateLayout, privateLayout.getExpandedChild(),
457                         privateLayout.getVisibleWrapper(
458                                 NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations,
459                         applyCallback);
460             }
461         }
462 
463         flag = FLAG_CONTENT_VIEW_HEADS_UP;
464         if ((reInflateFlags & flag) != 0) {
465             if (result.newHeadsUpView != null) {
466                 boolean isNewView =
467                         !canReapplyRemoteView(result.newHeadsUpView,
468                                 cachedContentViews.get(FLAG_CONTENT_VIEW_HEADS_UP));
469                 ApplyCallback applyCallback = new ApplyCallback() {
470                     @Override
471                     public void setResultView(View v) {
472                         result.inflatedHeadsUpView = v;
473                     }
474 
475                     @Override
476                     public RemoteViews getRemoteView() {
477                         return result.newHeadsUpView;
478                     }
479                 };
480                 applyRemoteView(inflateSynchronously, result, reInflateFlags, flag,
481                         cachedContentViews, row, redactAmbient, isNewView, remoteViewClickHandler,
482                         callback, privateLayout, privateLayout.getHeadsUpChild(),
483                         privateLayout.getVisibleWrapper(
484                                 VISIBLE_TYPE_HEADSUP), runningInflations,
485                         applyCallback);
486             }
487         }
488 
489         flag = FLAG_CONTENT_VIEW_PUBLIC;
490         if ((reInflateFlags & flag) != 0) {
491             boolean isNewView =
492                     !canReapplyRemoteView(result.newPublicView,
493                             cachedContentViews.get(FLAG_CONTENT_VIEW_PUBLIC));
494             ApplyCallback applyCallback = new ApplyCallback() {
495                 @Override
496                 public void setResultView(View v) {
497                     result.inflatedPublicView = v;
498                 }
499 
500                 @Override
501                 public RemoteViews getRemoteView() {
502                     return result.newPublicView;
503                 }
504             };
505             applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, cachedContentViews,
506                     row, redactAmbient, isNewView, remoteViewClickHandler, callback,
507                     publicLayout, publicLayout.getContractedChild(),
508                     publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),
509                     runningInflations, applyCallback);
510         }
511 
512         flag = FLAG_CONTENT_VIEW_AMBIENT;
513         if ((reInflateFlags & flag) != 0) {
514             NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout;
515             boolean isNewView = (!canReapplyAmbient(row, redactAmbient)
516                     || !canReapplyRemoteView(result.newAmbientView,
517                             cachedContentViews.get(FLAG_CONTENT_VIEW_AMBIENT)));
518             ApplyCallback applyCallback = new ApplyCallback() {
519                 @Override
520                 public void setResultView(View v) {
521                     result.inflatedAmbientView = v;
522                 }
523 
524                 @Override
525                 public RemoteViews getRemoteView() {
526                     return result.newAmbientView;
527                 }
528             };
529             applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, cachedContentViews,
530                     row, redactAmbient, isNewView, remoteViewClickHandler, callback,
531                     newParent, newParent.getAmbientChild(), newParent.getVisibleWrapper(
532                             NotificationContentView.VISIBLE_TYPE_AMBIENT), runningInflations,
533                     applyCallback);
534         }
535 
536         // Let's try to finish, maybe nobody is even inflating anything
537         finishIfDone(result, reInflateFlags, cachedContentViews, runningInflations, callback, row,
538                 redactAmbient);
539         CancellationSignal cancellationSignal = new CancellationSignal();
540         cancellationSignal.setOnCancelListener(
541                 () -> runningInflations.values().forEach(CancellationSignal::cancel));
542         return cancellationSignal;
543     }
544 
545     @VisibleForTesting
applyRemoteView( boolean inflateSynchronously, final InflationProgress result, final @InflationFlag int reInflateFlags, @InflationFlag int inflationId, final ArrayMap<Integer, RemoteViews> cachedContentViews, final ExpandableNotificationRow row, final boolean redactAmbient, boolean isNewView, RemoteViews.OnClickHandler remoteViewClickHandler, @Nullable final InflationCallback callback, NotificationContentView parentLayout, View existingView, NotificationViewWrapper existingWrapper, final HashMap<Integer, CancellationSignal> runningInflations, ApplyCallback applyCallback)546     static void applyRemoteView(
547             boolean inflateSynchronously,
548             final InflationProgress result,
549             final @InflationFlag int reInflateFlags,
550             @InflationFlag int inflationId,
551             final ArrayMap<Integer, RemoteViews> cachedContentViews,
552             final ExpandableNotificationRow row,
553             final boolean redactAmbient,
554             boolean isNewView,
555             RemoteViews.OnClickHandler remoteViewClickHandler,
556             @Nullable final InflationCallback callback,
557             NotificationContentView parentLayout,
558             View existingView,
559             NotificationViewWrapper existingWrapper,
560             final HashMap<Integer, CancellationSignal> runningInflations,
561             ApplyCallback applyCallback) {
562         RemoteViews newContentView = applyCallback.getRemoteView();
563         if (inflateSynchronously) {
564             try {
565                 if (isNewView) {
566                     View v = newContentView.apply(
567                             result.packageContext,
568                             parentLayout,
569                             remoteViewClickHandler);
570                     v.setIsRootNamespace(true);
571                     applyCallback.setResultView(v);
572                 } else {
573                     newContentView.reapply(
574                             result.packageContext,
575                             existingView,
576                             remoteViewClickHandler);
577                     existingWrapper.onReinflated();
578                 }
579             } catch (Exception e) {
580                 handleInflationError(runningInflations, e, row.getStatusBarNotification(), callback);
581                 // Add a running inflation to make sure we don't trigger callbacks.
582                 // Safe to do because only happens in tests.
583                 runningInflations.put(inflationId, new CancellationSignal());
584             }
585             return;
586         }
587         RemoteViews.OnViewAppliedListener listener = new RemoteViews.OnViewAppliedListener() {
588 
589             @Override
590             public void onViewInflated(View v) {
591                 if (v instanceof ImageMessageConsumer) {
592                     ((ImageMessageConsumer) v).setImageResolver(row.getImageResolver());
593                 }
594             }
595 
596             @Override
597             public void onViewApplied(View v) {
598                 if (isNewView) {
599                     v.setIsRootNamespace(true);
600                     applyCallback.setResultView(v);
601                 } else if (existingWrapper != null) {
602                     existingWrapper.onReinflated();
603                 }
604                 runningInflations.remove(inflationId);
605                 finishIfDone(result, reInflateFlags, cachedContentViews, runningInflations,
606                         callback, row, redactAmbient);
607             }
608 
609             @Override
610             public void onError(Exception e) {
611                 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could
612                 // actually also be a system issue, so let's try on the UI thread again to be safe.
613                 try {
614                     View newView = existingView;
615                     if (isNewView) {
616                         newView = newContentView.apply(
617                                 result.packageContext,
618                                 parentLayout,
619                                 remoteViewClickHandler);
620                     } else {
621                         newContentView.reapply(
622                                 result.packageContext,
623                                 existingView,
624                                 remoteViewClickHandler);
625                     }
626                     Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.",
627                             e);
628                     onViewApplied(newView);
629                 } catch (Exception anotherException) {
630                     runningInflations.remove(inflationId);
631                     handleInflationError(runningInflations, e, row.getStatusBarNotification(),
632                             callback);
633                 }
634             }
635         };
636         CancellationSignal cancellationSignal;
637         if (isNewView) {
638             cancellationSignal = newContentView.applyAsync(
639                     result.packageContext,
640                     parentLayout,
641                     null,
642                     listener,
643                     remoteViewClickHandler);
644         } else {
645             cancellationSignal = newContentView.reapplyAsync(
646                     result.packageContext,
647                     existingView,
648                     null,
649                     listener,
650                     remoteViewClickHandler);
651         }
652         runningInflations.put(inflationId, cancellationSignal);
653     }
654 
handleInflationError( HashMap<Integer, CancellationSignal> runningInflations, Exception e, StatusBarNotification notification, @Nullable InflationCallback callback)655     private static void handleInflationError(
656             HashMap<Integer, CancellationSignal> runningInflations, Exception e,
657             StatusBarNotification notification, @Nullable InflationCallback callback) {
658         Assert.isMainThread();
659         runningInflations.values().forEach(CancellationSignal::cancel);
660         if (callback != null) {
661             callback.handleInflationException(notification, e);
662         }
663     }
664 
665     /**
666      * Finish the inflation of the views
667      *
668      * @return true if the inflation was finished
669      */
finishIfDone(InflationProgress result, @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, ExpandableNotificationRow row, boolean redactAmbient)670     private static boolean finishIfDone(InflationProgress result,
671             @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews,
672             HashMap<Integer, CancellationSignal> runningInflations,
673             @Nullable InflationCallback endListener, ExpandableNotificationRow row,
674             boolean redactAmbient) {
675         Assert.isMainThread();
676         NotificationEntry entry = row.getEntry();
677         NotificationContentView privateLayout = row.getPrivateLayout();
678         NotificationContentView publicLayout = row.getPublicLayout();
679         if (runningInflations.isEmpty()) {
680             if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
681                 if (result.inflatedContentView != null) {
682                     // New view case
683                     privateLayout.setContractedChild(result.inflatedContentView);
684                     cachedContentViews.put(FLAG_CONTENT_VIEW_CONTRACTED, result.newContentView);
685                 } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_CONTRACTED) != null) {
686                     // Reinflation case. Only update if it's still cached (i.e. view has not been
687                     // freed while inflating).
688                     cachedContentViews.put(FLAG_CONTENT_VIEW_CONTRACTED, result.newContentView);
689                 }
690             }
691 
692             if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
693                 if (result.inflatedExpandedView != null) {
694                     privateLayout.setExpandedChild(result.inflatedExpandedView);
695                     cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, result.newExpandedView);
696                 } else if (result.newExpandedView == null) {
697                     privateLayout.setExpandedChild(null);
698                     cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, null);
699                 } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_EXPANDED) != null) {
700                     cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, result.newExpandedView);
701                 }
702                 if (result.newExpandedView != null) {
703                     privateLayout.setExpandedInflatedSmartReplies(
704                             result.expandedInflatedSmartReplies);
705                 } else {
706                     privateLayout.setExpandedInflatedSmartReplies(null);
707                 }
708                 row.setExpandable(result.newExpandedView != null);
709             }
710 
711             if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
712                 if (result.inflatedHeadsUpView != null) {
713                     privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
714                     cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, result.newHeadsUpView);
715                 } else if (result.newHeadsUpView == null) {
716                     privateLayout.setHeadsUpChild(null);
717                     cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, null);
718                 } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_HEADS_UP) != null) {
719                     cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, result.newHeadsUpView);
720                 }
721                 if (result.newHeadsUpView != null) {
722                     privateLayout.setHeadsUpInflatedSmartReplies(
723                             result.headsUpInflatedSmartReplies);
724                 } else {
725                     privateLayout.setHeadsUpInflatedSmartReplies(null);
726                 }
727             }
728 
729             if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
730                 if (result.inflatedPublicView != null) {
731                     publicLayout.setContractedChild(result.inflatedPublicView);
732                     cachedContentViews.put(FLAG_CONTENT_VIEW_PUBLIC, result.newPublicView);
733                 } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_PUBLIC) != null) {
734                     cachedContentViews.put(FLAG_CONTENT_VIEW_PUBLIC, result.newPublicView);
735                 }
736             }
737 
738             if ((reInflateFlags & FLAG_CONTENT_VIEW_AMBIENT) != 0) {
739                 if (result.inflatedAmbientView != null) {
740                     NotificationContentView newParent = redactAmbient
741                             ? publicLayout : privateLayout;
742                     NotificationContentView otherParent = !redactAmbient
743                             ? publicLayout : privateLayout;
744                     newParent.setAmbientChild(result.inflatedAmbientView);
745                     otherParent.setAmbientChild(null);
746                     cachedContentViews.put(FLAG_CONTENT_VIEW_AMBIENT, result.newAmbientView);
747                 } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_AMBIENT) != null) {
748                     cachedContentViews.put(FLAG_CONTENT_VIEW_AMBIENT, result.newAmbientView);
749                 }
750             }
751             entry.headsUpStatusBarText = result.headsUpStatusBarText;
752             entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic;
753             if (endListener != null) {
754                 endListener.onAsyncInflationFinished(row.getEntry(), reInflateFlags);
755             }
756             return true;
757         }
758         return false;
759     }
760 
createExpandedView(Notification.Builder builder, boolean isLowPriority)761     private static RemoteViews createExpandedView(Notification.Builder builder,
762             boolean isLowPriority) {
763         RemoteViews bigContentView = builder.createBigContentView();
764         if (bigContentView != null) {
765             return bigContentView;
766         }
767         if (isLowPriority) {
768             RemoteViews contentView = builder.createContentView();
769             Notification.Builder.makeHeaderExpanded(contentView);
770             return contentView;
771         }
772         return null;
773     }
774 
createContentView(Notification.Builder builder, boolean isLowPriority, boolean useLarge)775     private static RemoteViews createContentView(Notification.Builder builder,
776             boolean isLowPriority, boolean useLarge) {
777         if (isLowPriority) {
778             return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
779         }
780         return builder.createContentView(useLarge);
781     }
782 
783     /**
784      * @param newView The new view that will be applied
785      * @param oldView The old view that was applied to the existing view before
786      * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply.
787      */
788      @VisibleForTesting
canReapplyRemoteView(final RemoteViews newView, final RemoteViews oldView)789      static boolean canReapplyRemoteView(final RemoteViews newView,
790             final RemoteViews oldView) {
791         return (newView == null && oldView == null) ||
792                 (newView != null && oldView != null
793                         && oldView.getPackage() != null
794                         && newView.getPackage() != null
795                         && newView.getPackage().equals(oldView.getPackage())
796                         && newView.getLayoutId() == oldView.getLayoutId()
797                         && !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED));
798     }
799 
setInflationCallback(InflationCallback callback)800     public void setInflationCallback(InflationCallback callback) {
801         mCallback = callback;
802     }
803 
804     public interface InflationCallback {
handleInflationException(StatusBarNotification notification, Exception e)805         void handleInflationException(StatusBarNotification notification, Exception e);
806 
807         /**
808          * Callback for after the content views finish inflating.
809          *
810          * @param entry the entry with the content views set
811          * @param inflatedFlags the flags associated with the content views that were inflated
812          */
onAsyncInflationFinished(NotificationEntry entry, @InflationFlag int inflatedFlags)813         void onAsyncInflationFinished(NotificationEntry entry, @InflationFlag int inflatedFlags);
814     }
815 
clearCachesAndReInflate()816     public void clearCachesAndReInflate() {
817         mCachedContentViews.clear();
818         inflateNotificationViews();
819     }
820 
821     /**
822      * Sets whether to perform inflation on the same thread as the caller. This method should only
823      * be used in tests, not in production.
824      */
825     @VisibleForTesting
setInflateSynchronously(boolean inflateSynchronously)826     void setInflateSynchronously(boolean inflateSynchronously) {
827         mInflateSynchronously = inflateSynchronously;
828     }
829 
canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient)830     private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) {
831         NotificationContentView ambientView = redactAmbient ? row.getPublicLayout()
832                 : row.getPrivateLayout();
833         return ambientView.getAmbientChild() != null;
834     }
835 
836     public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
837             implements InflationCallback, InflationTask {
838 
839         private final StatusBarNotification mSbn;
840         private final Context mContext;
841         private final boolean mInflateSynchronously;
842         private final boolean mIsLowPriority;
843         private final boolean mIsChildInGroup;
844         private final boolean mUsesIncreasedHeight;
845         private final InflationCallback mCallback;
846         private final boolean mUsesIncreasedHeadsUpHeight;
847         private final boolean mRedactAmbient;
848         private @InflationFlag int mReInflateFlags;
849         private final ArrayMap<Integer, RemoteViews> mCachedContentViews;
850         private ExpandableNotificationRow mRow;
851         private Exception mError;
852         private RemoteViews.OnClickHandler mRemoteViewClickHandler;
853         private CancellationSignal mCancellationSignal;
854 
AsyncInflationTask( StatusBarNotification notification, boolean inflateSynchronously, @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews, ExpandableNotificationRow row, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, InflationCallback callback, RemoteViews.OnClickHandler remoteViewClickHandler)855         private AsyncInflationTask(
856                 StatusBarNotification notification,
857                 boolean inflateSynchronously,
858                 @InflationFlag int reInflateFlags,
859                 ArrayMap<Integer, RemoteViews> cachedContentViews,
860                 ExpandableNotificationRow row,
861                 boolean isLowPriority,
862                 boolean isChildInGroup,
863                 boolean usesIncreasedHeight,
864                 boolean usesIncreasedHeadsUpHeight,
865                 boolean redactAmbient,
866                 InflationCallback callback,
867                 RemoteViews.OnClickHandler remoteViewClickHandler) {
868             mRow = row;
869             mSbn = notification;
870             mInflateSynchronously = inflateSynchronously;
871             mReInflateFlags = reInflateFlags;
872             mCachedContentViews = cachedContentViews;
873             mContext = mRow.getContext();
874             mIsLowPriority = isLowPriority;
875             mIsChildInGroup = isChildInGroup;
876             mUsesIncreasedHeight = usesIncreasedHeight;
877             mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
878             mRedactAmbient = redactAmbient;
879             mRemoteViewClickHandler = remoteViewClickHandler;
880             mCallback = callback;
881             NotificationEntry entry = row.getEntry();
882             entry.setInflationTask(this);
883         }
884 
885         @VisibleForTesting
886         @InflationFlag
getReInflateFlags()887         public int getReInflateFlags() {
888             return mReInflateFlags;
889         }
890 
891         @Override
doInBackground(Void... params)892         protected InflationProgress doInBackground(Void... params) {
893             try {
894                 final Notification.Builder recoveredBuilder
895                         = Notification.Builder.recoverBuilder(mContext,
896                         mSbn.getNotification());
897 
898                 Context packageContext = mSbn.getPackageContext(mContext);
899                 Notification notification = mSbn.getNotification();
900                 if (notification.isMediaNotification()) {
901                     MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext,
902                             packageContext);
903                     processor.processNotification(notification, recoveredBuilder);
904                 }
905                 InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
906                         recoveredBuilder, mIsLowPriority,
907                         mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
908                         mRedactAmbient, packageContext);
909                 return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mRow.getEntry(),
910                         mRow.getContext(), mRow.getHeadsUpManager(),
911                         mRow.getExistingSmartRepliesAndActions());
912             } catch (Exception e) {
913                 mError = e;
914                 return null;
915             }
916         }
917 
918         @Override
onPostExecute(InflationProgress result)919         protected void onPostExecute(InflationProgress result) {
920             if (mError == null) {
921                 mCancellationSignal = apply(mInflateSynchronously, result, mReInflateFlags,
922                         mCachedContentViews, mRow, mRedactAmbient, mRemoteViewClickHandler, this);
923             } else {
924                 handleError(mError);
925             }
926         }
927 
handleError(Exception e)928         private void handleError(Exception e) {
929             mRow.getEntry().onInflationTaskFinished();
930             StatusBarNotification sbn = mRow.getStatusBarNotification();
931             final String ident = sbn.getPackageName() + "/0x"
932                     + Integer.toHexString(sbn.getId());
933             Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e);
934             mCallback.handleInflationException(sbn,
935                     new InflationException("Couldn't inflate contentViews" + e));
936         }
937 
938         @Override
abort()939         public void abort() {
940             cancel(true /* mayInterruptIfRunning */);
941             if (mCancellationSignal != null) {
942                 mCancellationSignal.cancel();
943             }
944         }
945 
946         @Override
supersedeTask(InflationTask task)947         public void supersedeTask(InflationTask task) {
948             if (task instanceof AsyncInflationTask) {
949                 // We want to inflate all flags of the previous task as well
950                 mReInflateFlags |= ((AsyncInflationTask) task).mReInflateFlags;
951             }
952         }
953 
954         @Override
handleInflationException(StatusBarNotification notification, Exception e)955         public void handleInflationException(StatusBarNotification notification, Exception e) {
956             handleError(e);
957         }
958 
959         @Override
onAsyncInflationFinished(NotificationEntry entry, @InflationFlag int inflatedFlags)960         public void onAsyncInflationFinished(NotificationEntry entry,
961                 @InflationFlag int inflatedFlags) {
962             mRow.getEntry().onInflationTaskFinished();
963             mRow.onNotificationUpdated();
964             mCallback.onAsyncInflationFinished(mRow.getEntry(), inflatedFlags);
965 
966             // Notify the resolver that the inflation task has finished,
967             // try to purge unnecessary cached entries.
968             mRow.getImageResolver().purgeCache();
969         }
970     }
971 
972     @VisibleForTesting
973     static class InflationProgress {
974         private RemoteViews newContentView;
975         private RemoteViews newHeadsUpView;
976         private RemoteViews newExpandedView;
977         private RemoteViews newAmbientView;
978         private RemoteViews newPublicView;
979 
980         @VisibleForTesting
981         Context packageContext;
982 
983         private View inflatedContentView;
984         private View inflatedHeadsUpView;
985         private View inflatedExpandedView;
986         private View inflatedAmbientView;
987         private View inflatedPublicView;
988         private CharSequence headsUpStatusBarText;
989         private CharSequence headsUpStatusBarTextPublic;
990 
991         private InflatedSmartReplies expandedInflatedSmartReplies;
992         private InflatedSmartReplies headsUpInflatedSmartReplies;
993     }
994 
995     @VisibleForTesting
996     abstract static class ApplyCallback {
setResultView(View v)997         public abstract void setResultView(View v);
getRemoteView()998         public abstract RemoteViews getRemoteView();
999     }
1000 }
1001