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