• 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.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
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_EXPANDED;
22 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.app.Notification;
27 import android.content.Context;
28 import android.content.ContextWrapper;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.PackageManager;
31 import android.content.res.Resources;
32 import android.os.AsyncTask;
33 import android.os.Build;
34 import android.os.CancellationSignal;
35 import android.os.Trace;
36 import android.os.UserHandle;
37 import android.service.notification.StatusBarNotification;
38 import android.util.Log;
39 import android.view.View;
40 import android.widget.RemoteViews;
41 
42 import com.android.internal.annotations.VisibleForTesting;
43 import com.android.internal.widget.ImageMessageConsumer;
44 import com.android.systemui.R;
45 import com.android.systemui.dagger.SysUISingleton;
46 import com.android.systemui.dagger.qualifiers.Background;
47 import com.android.systemui.media.controls.util.MediaFeatureFlag;
48 import com.android.systemui.statusbar.InflationTask;
49 import com.android.systemui.statusbar.NotificationRemoteInputManager;
50 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
51 import com.android.systemui.statusbar.notification.InflationException;
52 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
53 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
54 import com.android.systemui.statusbar.phone.CentralSurfaces;
55 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
56 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
57 import com.android.systemui.statusbar.policy.SmartReplyStateInflater;
58 import com.android.systemui.util.Assert;
59 
60 import java.util.HashMap;
61 import java.util.concurrent.Executor;
62 
63 import javax.inject.Inject;
64 
65 /**
66  * {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by
67  * asynchronously building the content's {@link RemoteViews} and applying it to the row.
68  */
69 @SysUISingleton
70 @VisibleForTesting(visibility = PACKAGE)
71 public class NotificationContentInflater implements NotificationRowContentBinder {
72 
73     public static final String TAG = "NotifContentInflater";
74 
75     private boolean mInflateSynchronously = false;
76     private final boolean mIsMediaInQS;
77     private final NotificationRemoteInputManager mRemoteInputManager;
78     private final NotifRemoteViewCache mRemoteViewCache;
79     private final ConversationNotificationProcessor mConversationProcessor;
80     private final Executor mBgExecutor;
81     private final SmartReplyStateInflater mSmartReplyStateInflater;
82 
83     @Inject
NotificationContentInflater( NotifRemoteViewCache remoteViewCache, NotificationRemoteInputManager remoteInputManager, ConversationNotificationProcessor conversationProcessor, MediaFeatureFlag mediaFeatureFlag, @Background Executor bgExecutor, SmartReplyStateInflater smartRepliesInflater)84     NotificationContentInflater(
85             NotifRemoteViewCache remoteViewCache,
86             NotificationRemoteInputManager remoteInputManager,
87             ConversationNotificationProcessor conversationProcessor,
88             MediaFeatureFlag mediaFeatureFlag,
89             @Background Executor bgExecutor,
90             SmartReplyStateInflater smartRepliesInflater) {
91         mRemoteViewCache = remoteViewCache;
92         mRemoteInputManager = remoteInputManager;
93         mConversationProcessor = conversationProcessor;
94         mIsMediaInQS = mediaFeatureFlag.getEnabled();
95         mBgExecutor = bgExecutor;
96         mSmartReplyStateInflater = smartRepliesInflater;
97     }
98 
99     @Override
bindContent( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int contentToBind, BindParams bindParams, boolean forceInflate, @Nullable InflationCallback callback)100     public void bindContent(
101             NotificationEntry entry,
102             ExpandableNotificationRow row,
103             @InflationFlag int contentToBind,
104             BindParams bindParams,
105             boolean forceInflate,
106             @Nullable InflationCallback callback) {
107         if (row.isRemoved()) {
108             // We don't want to reinflate anything for removed notifications. Otherwise views might
109             // be readded to the stack, leading to leaks. This may happen with low-priority groups
110             // where the removal of already removed children can lead to a reinflation.
111             return;
112         }
113 
114         StatusBarNotification sbn = entry.getSbn();
115 
116         // To check if the notification has inline image and preload inline image if necessary.
117         row.getImageResolver().preloadImages(sbn.getNotification());
118 
119         if (forceInflate) {
120             mRemoteViewCache.clearCache(entry);
121         }
122 
123         // Cancel any pending frees on any view we're trying to bind since we should be bound after.
124         cancelContentViewFrees(row, contentToBind);
125 
126         AsyncInflationTask task = new AsyncInflationTask(
127                 mBgExecutor,
128                 mInflateSynchronously,
129                 contentToBind,
130                 mRemoteViewCache,
131                 entry,
132                 mConversationProcessor,
133                 row,
134                 bindParams.isLowPriority,
135                 bindParams.usesIncreasedHeight,
136                 bindParams.usesIncreasedHeadsUpHeight,
137                 callback,
138                 mRemoteInputManager.getRemoteViewsOnClickHandler(),
139                 mIsMediaInQS,
140                 mSmartReplyStateInflater);
141         if (mInflateSynchronously) {
142             task.onPostExecute(task.doInBackground());
143         } else {
144             task.executeOnExecutor(mBgExecutor);
145         }
146     }
147 
148     @VisibleForTesting
inflateNotificationViews( NotificationEntry entry, ExpandableNotificationRow row, BindParams bindParams, boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, Context packageContext, SmartReplyStateInflater smartRepliesInflater)149     InflationProgress inflateNotificationViews(
150             NotificationEntry entry,
151             ExpandableNotificationRow row,
152             BindParams bindParams,
153             boolean inflateSynchronously,
154             @InflationFlag int reInflateFlags,
155             Notification.Builder builder,
156             Context packageContext,
157             SmartReplyStateInflater smartRepliesInflater) {
158         InflationProgress result = createRemoteViews(reInflateFlags,
159                 builder,
160                 bindParams.isLowPriority,
161                 bindParams.usesIncreasedHeight,
162                 bindParams.usesIncreasedHeadsUpHeight,
163                 packageContext);
164 
165         result = inflateSmartReplyViews(result, reInflateFlags, entry,
166                 row.getContext(), packageContext,
167                 row.getExistingSmartReplyState(),
168                 smartRepliesInflater);
169 
170         apply(
171                 mBgExecutor,
172                 inflateSynchronously,
173                 result,
174                 reInflateFlags,
175                 mRemoteViewCache,
176                 entry,
177                 row,
178                 mRemoteInputManager.getRemoteViewsOnClickHandler(),
179                 null);
180         return result;
181     }
182 
183     @Override
cancelBind( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row)184     public void cancelBind(
185             @NonNull NotificationEntry entry,
186             @NonNull ExpandableNotificationRow row) {
187         entry.abortTask();
188     }
189 
190     @Override
unbindContent( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row, @InflationFlag int contentToUnbind)191     public void unbindContent(
192             @NonNull NotificationEntry entry,
193             @NonNull ExpandableNotificationRow row,
194             @InflationFlag int contentToUnbind) {
195         int curFlag = 1;
196         while (contentToUnbind != 0) {
197             if ((contentToUnbind & curFlag) != 0) {
198                 freeNotificationView(entry, row, curFlag);
199             }
200             contentToUnbind &= ~curFlag;
201             curFlag = curFlag << 1;
202         }
203     }
204 
205     /**
206      * Frees the content view associated with the inflation flag as soon as the view is not showing.
207      *
208      * @param inflateFlag the flag corresponding to the content view which should be freed
209      */
freeNotificationView( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int inflateFlag)210     private void freeNotificationView(
211             NotificationEntry entry,
212             ExpandableNotificationRow row,
213             @InflationFlag int inflateFlag) {
214         switch (inflateFlag) {
215             case FLAG_CONTENT_VIEW_CONTRACTED:
216                 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> {
217                     row.getPrivateLayout().setContractedChild(null);
218                     mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED);
219                 });
220                 break;
221             case FLAG_CONTENT_VIEW_EXPANDED:
222                 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_EXPANDED, () -> {
223                     row.getPrivateLayout().setExpandedChild(null);
224                     mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED);
225                 });
226                 break;
227             case FLAG_CONTENT_VIEW_HEADS_UP:
228                 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_HEADSUP, () -> {
229                     row.getPrivateLayout().setHeadsUpChild(null);
230                     mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP);
231                     row.getPrivateLayout().setHeadsUpInflatedSmartReplies(null);
232                 });
233                 break;
234             case FLAG_CONTENT_VIEW_PUBLIC:
235                 row.getPublicLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> {
236                     row.getPublicLayout().setContractedChild(null);
237                     mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC);
238                 });
239                 break;
240             default:
241                 break;
242         }
243     }
244 
245     /**
246      * Cancel any pending content view frees from {@link #freeNotificationView} for the provided
247      * content views.
248      *
249      * @param row top level notification row containing the content views
250      * @param contentViews content views to cancel pending frees on
251      */
cancelContentViewFrees( ExpandableNotificationRow row, @InflationFlag int contentViews)252     private void cancelContentViewFrees(
253             ExpandableNotificationRow row,
254             @InflationFlag int contentViews) {
255         if ((contentViews & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
256             row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED);
257         }
258         if ((contentViews & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
259             row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_EXPANDED);
260         }
261         if ((contentViews & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
262             row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_HEADSUP);
263         }
264         if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
265             row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED);
266         }
267     }
268 
inflateSmartReplyViews( InflationProgress result, @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, Context packageContext, InflatedSmartReplyState previousSmartReplyState, SmartReplyStateInflater inflater)269     private static InflationProgress inflateSmartReplyViews(
270             InflationProgress result,
271             @InflationFlag int reInflateFlags,
272             NotificationEntry entry,
273             Context context,
274             Context packageContext,
275             InflatedSmartReplyState previousSmartReplyState,
276             SmartReplyStateInflater inflater) {
277         boolean inflateContracted = (reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0
278                 && result.newContentView != null;
279         boolean inflateExpanded = (reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0
280                 && result.newExpandedView != null;
281         boolean inflateHeadsUp = (reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0
282                 && result.newHeadsUpView != null;
283         if (inflateContracted || inflateExpanded || inflateHeadsUp) {
284             result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry);
285         }
286         if (inflateExpanded) {
287             result.expandedInflatedSmartReplies = inflater.inflateSmartReplyViewHolder(
288                     context, packageContext, entry, previousSmartReplyState,
289                     result.inflatedSmartReplyState);
290         }
291         if (inflateHeadsUp) {
292             result.headsUpInflatedSmartReplies = inflater.inflateSmartReplyViewHolder(
293                     context, packageContext, entry, previousSmartReplyState,
294                     result.inflatedSmartReplyState);
295         }
296         return result;
297     }
298 
createRemoteViews(@nflationFlag int reInflateFlags, Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, Context packageContext)299     private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags,
300             Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight,
301             boolean usesIncreasedHeadsUpHeight, Context packageContext) {
302         InflationProgress result = new InflationProgress();
303 
304         if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
305             result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);
306         }
307 
308         if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
309             result.newExpandedView = createExpandedView(builder, isLowPriority);
310         }
311 
312         if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
313             result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);
314         }
315 
316         if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
317             result.newPublicView = builder.makePublicContentView(isLowPriority);
318         }
319 
320         result.packageContext = packageContext;
321         result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */);
322         result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
323                 true /* showingPublic */);
324         return result;
325     }
326 
apply( Executor bgExecutor, boolean inflateSynchronously, InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, NotificationEntry entry, ExpandableNotificationRow row, RemoteViews.InteractionHandler remoteViewClickHandler, @Nullable InflationCallback callback)327     private static CancellationSignal apply(
328             Executor bgExecutor,
329             boolean inflateSynchronously,
330             InflationProgress result,
331             @InflationFlag int reInflateFlags,
332             NotifRemoteViewCache remoteViewCache,
333             NotificationEntry entry,
334             ExpandableNotificationRow row,
335             RemoteViews.InteractionHandler remoteViewClickHandler,
336             @Nullable InflationCallback callback) {
337         NotificationContentView privateLayout = row.getPrivateLayout();
338         NotificationContentView publicLayout = row.getPublicLayout();
339         final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
340 
341         int flag = FLAG_CONTENT_VIEW_CONTRACTED;
342         if ((reInflateFlags & flag) != 0) {
343             boolean isNewView =
344                     !canReapplyRemoteView(result.newContentView,
345                             remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED));
346             ApplyCallback applyCallback = new ApplyCallback() {
347                 @Override
348                 public void setResultView(View v) {
349                     result.inflatedContentView = v;
350                 }
351 
352                 @Override
353                 public RemoteViews getRemoteView() {
354                     return result.newContentView;
355                 }
356             };
357             applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag,
358                     remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
359                     privateLayout,  privateLayout.getContractedChild(),
360                     privateLayout.getVisibleWrapper(
361                             NotificationContentView.VISIBLE_TYPE_CONTRACTED),
362                     runningInflations, applyCallback);
363         }
364 
365         flag = FLAG_CONTENT_VIEW_EXPANDED;
366         if ((reInflateFlags & flag) != 0) {
367             if (result.newExpandedView != null) {
368                 boolean isNewView =
369                         !canReapplyRemoteView(result.newExpandedView,
370                                 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED));
371                 ApplyCallback applyCallback = new ApplyCallback() {
372                     @Override
373                     public void setResultView(View v) {
374                         result.inflatedExpandedView = v;
375                     }
376 
377                     @Override
378                     public RemoteViews getRemoteView() {
379                         return result.newExpandedView;
380                     }
381                 };
382                 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag,
383                         remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
384                         callback, privateLayout, privateLayout.getExpandedChild(),
385                         privateLayout.getVisibleWrapper(
386                                 NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations,
387                         applyCallback);
388             }
389         }
390 
391         flag = FLAG_CONTENT_VIEW_HEADS_UP;
392         if ((reInflateFlags & flag) != 0) {
393             if (result.newHeadsUpView != null) {
394                 boolean isNewView =
395                         !canReapplyRemoteView(result.newHeadsUpView,
396                                 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP));
397                 ApplyCallback applyCallback = new ApplyCallback() {
398                     @Override
399                     public void setResultView(View v) {
400                         result.inflatedHeadsUpView = v;
401                     }
402 
403                     @Override
404                     public RemoteViews getRemoteView() {
405                         return result.newHeadsUpView;
406                     }
407                 };
408                 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag,
409                         remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
410                         callback, privateLayout, privateLayout.getHeadsUpChild(),
411                         privateLayout.getVisibleWrapper(
412                                 VISIBLE_TYPE_HEADSUP), runningInflations,
413                         applyCallback);
414             }
415         }
416 
417         flag = FLAG_CONTENT_VIEW_PUBLIC;
418         if ((reInflateFlags & flag) != 0) {
419             boolean isNewView =
420                     !canReapplyRemoteView(result.newPublicView,
421                             remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC));
422             ApplyCallback applyCallback = new ApplyCallback() {
423                 @Override
424                 public void setResultView(View v) {
425                     result.inflatedPublicView = v;
426                 }
427 
428                 @Override
429                 public RemoteViews getRemoteView() {
430                     return result.newPublicView;
431                 }
432             };
433             applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag,
434                     remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
435                     publicLayout, publicLayout.getContractedChild(),
436                     publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),
437                     runningInflations, applyCallback);
438         }
439 
440         // Let's try to finish, maybe nobody is even inflating anything
441         finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, callback, entry,
442                 row);
443         CancellationSignal cancellationSignal = new CancellationSignal();
444         cancellationSignal.setOnCancelListener(
445                 () -> runningInflations.values().forEach(CancellationSignal::cancel));
446 
447         return cancellationSignal;
448     }
449 
450     @VisibleForTesting
applyRemoteView( Executor bgExecutor, boolean inflateSynchronously, final InflationProgress result, final @InflationFlag int reInflateFlags, @InflationFlag int inflationId, final NotifRemoteViewCache remoteViewCache, final NotificationEntry entry, final ExpandableNotificationRow row, boolean isNewView, RemoteViews.InteractionHandler remoteViewClickHandler, @Nullable final InflationCallback callback, NotificationContentView parentLayout, View existingView, NotificationViewWrapper existingWrapper, final HashMap<Integer, CancellationSignal> runningInflations, ApplyCallback applyCallback)451     static void applyRemoteView(
452             Executor bgExecutor,
453             boolean inflateSynchronously,
454             final InflationProgress result,
455             final @InflationFlag int reInflateFlags,
456             @InflationFlag int inflationId,
457             final NotifRemoteViewCache remoteViewCache,
458             final NotificationEntry entry,
459             final ExpandableNotificationRow row,
460             boolean isNewView,
461             RemoteViews.InteractionHandler remoteViewClickHandler,
462             @Nullable final InflationCallback callback,
463             NotificationContentView parentLayout,
464             View existingView,
465             NotificationViewWrapper existingWrapper,
466             final HashMap<Integer, CancellationSignal> runningInflations,
467             ApplyCallback applyCallback) {
468         RemoteViews newContentView = applyCallback.getRemoteView();
469         if (inflateSynchronously) {
470             try {
471                 if (isNewView) {
472                     View v = newContentView.apply(
473                             result.packageContext,
474                             parentLayout,
475                             remoteViewClickHandler);
476                     validateView(v, entry, row.getResources());
477                     v.setIsRootNamespace(true);
478                     applyCallback.setResultView(v);
479                 } else {
480                     newContentView.reapply(
481                             result.packageContext,
482                             existingView,
483                             remoteViewClickHandler);
484                     validateView(existingView, entry, row.getResources());
485                     existingWrapper.onReinflated();
486                 }
487             } catch (Exception e) {
488                 handleInflationError(runningInflations, e, row.getEntry(), callback);
489                 // Add a running inflation to make sure we don't trigger callbacks.
490                 // Safe to do because only happens in tests.
491                 runningInflations.put(inflationId, new CancellationSignal());
492             }
493             return;
494         }
495         RemoteViews.OnViewAppliedListener listener = new RemoteViews.OnViewAppliedListener() {
496 
497             @Override
498             public void onViewInflated(View v) {
499                 if (v instanceof ImageMessageConsumer) {
500                     ((ImageMessageConsumer) v).setImageResolver(row.getImageResolver());
501                 }
502             }
503 
504             @Override
505             public void onViewApplied(View v) {
506                 String invalidReason = isValidView(v, entry, row.getResources());
507                 if (invalidReason != null) {
508                     handleInflationError(runningInflations, new InflationException(invalidReason),
509                             row.getEntry(), callback);
510                     runningInflations.remove(inflationId);
511                     return;
512                 }
513                 if (isNewView) {
514                     v.setIsRootNamespace(true);
515                     applyCallback.setResultView(v);
516                 } else if (existingWrapper != null) {
517                     existingWrapper.onReinflated();
518                 }
519                 runningInflations.remove(inflationId);
520                 finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations,
521                         callback, entry, row);
522             }
523 
524             @Override
525             public void onError(Exception e) {
526                 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could
527                 // actually also be a system issue, so let's try on the UI thread again to be safe.
528                 try {
529                     View newView = existingView;
530                     if (isNewView) {
531                         newView = newContentView.apply(
532                                 result.packageContext,
533                                 parentLayout,
534                                 remoteViewClickHandler);
535                     } else {
536                         newContentView.reapply(
537                                 result.packageContext,
538                                 existingView,
539                                 remoteViewClickHandler);
540                     }
541                     Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.",
542                             e);
543                     onViewApplied(newView);
544                 } catch (Exception anotherException) {
545                     runningInflations.remove(inflationId);
546                     handleInflationError(runningInflations, e, row.getEntry(),
547                             callback);
548                 }
549             }
550         };
551         CancellationSignal cancellationSignal;
552         if (isNewView) {
553             cancellationSignal = newContentView.applyAsync(
554                     result.packageContext,
555                     parentLayout,
556                     bgExecutor,
557                     listener,
558                     remoteViewClickHandler);
559         } else {
560             cancellationSignal = newContentView.reapplyAsync(
561                     result.packageContext,
562                     existingView,
563                     bgExecutor,
564                     listener,
565                     remoteViewClickHandler);
566         }
567         runningInflations.put(inflationId, cancellationSignal);
568     }
569 
570     /**
571      * Checks if the given View is a valid notification View.
572      *
573      * @return null == valid, non-null == invalid, String represents reason for rejection.
574      */
575     @VisibleForTesting
576     @Nullable
isValidView(View view, NotificationEntry entry, Resources resources)577     static String isValidView(View view,
578             NotificationEntry entry,
579             Resources resources) {
580         if (!satisfiesMinHeightRequirement(view, entry, resources)) {
581             return "inflated notification does not meet minimum height requirement";
582         }
583         return null;
584     }
585 
satisfiesMinHeightRequirement(View view, NotificationEntry entry, Resources resources)586     private static boolean satisfiesMinHeightRequirement(View view,
587             NotificationEntry entry,
588             Resources resources) {
589         if (!requiresHeightCheck(entry)) {
590             return true;
591         }
592         Trace.beginSection("NotificationContentInflater#satisfiesMinHeightRequirement");
593         int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
594         int referenceWidth = resources.getDimensionPixelSize(
595                 R.dimen.notification_validation_reference_width);
596         int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth, View.MeasureSpec.EXACTLY);
597         view.measure(widthSpec, heightSpec);
598         int minHeight = resources.getDimensionPixelSize(
599                 R.dimen.notification_validation_minimum_allowed_height);
600         boolean result = view.getMeasuredHeight() >= minHeight;
601         Trace.endSection();
602         return result;
603     }
604 
requiresHeightCheck(NotificationEntry entry)605     private static boolean requiresHeightCheck(NotificationEntry entry) {
606         // Undecorated custom views are disallowed from S onwards
607         if (entry.targetSdk >= Build.VERSION_CODES.S) {
608             return false;
609         }
610         // No need to check if the app isn't using any custom views
611         Notification notification = entry.getSbn().getNotification();
612         if (notification.contentView == null
613                 && notification.bigContentView == null
614                 && notification.headsUpContentView == null) {
615             return false;
616         }
617         return true;
618     }
619 
validateView(View view, NotificationEntry entry, Resources resources)620     private static void validateView(View view,
621             NotificationEntry entry,
622             Resources resources) throws InflationException {
623         String invalidReason = isValidView(view, entry, resources);
624         if (invalidReason != null) {
625             throw new InflationException(invalidReason);
626         }
627     }
628 
handleInflationError( HashMap<Integer, CancellationSignal> runningInflations, Exception e, NotificationEntry notification, @Nullable InflationCallback callback)629     private static void handleInflationError(
630             HashMap<Integer, CancellationSignal> runningInflations, Exception e,
631             NotificationEntry notification, @Nullable InflationCallback callback) {
632         Assert.isMainThread();
633         runningInflations.values().forEach(CancellationSignal::cancel);
634         if (callback != null) {
635             callback.handleInflationException(notification, e);
636         }
637     }
638 
639     /**
640      * Finish the inflation of the views
641      *
642      * @return true if the inflation was finished
643      */
finishIfDone(InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, NotificationEntry entry, ExpandableNotificationRow row)644     private static boolean finishIfDone(InflationProgress result,
645             @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache,
646             HashMap<Integer, CancellationSignal> runningInflations,
647             @Nullable InflationCallback endListener, NotificationEntry entry,
648             ExpandableNotificationRow row) {
649         Assert.isMainThread();
650         NotificationContentView privateLayout = row.getPrivateLayout();
651         NotificationContentView publicLayout = row.getPublicLayout();
652         if (runningInflations.isEmpty()) {
653             boolean setRepliesAndActions = true;
654             if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
655                 if (result.inflatedContentView != null) {
656                     // New view case
657                     privateLayout.setContractedChild(result.inflatedContentView);
658                     remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED,
659                             result.newContentView);
660                 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) {
661                     // Reinflation case. Only update if it's still cached (i.e. view has not been
662                     // freed while inflating).
663                     remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED,
664                             result.newContentView);
665                 }
666                 setRepliesAndActions = true;
667             }
668 
669             if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
670                 if (result.inflatedExpandedView != null) {
671                     privateLayout.setExpandedChild(result.inflatedExpandedView);
672                     remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED,
673                             result.newExpandedView);
674                 } else if (result.newExpandedView == null) {
675                     privateLayout.setExpandedChild(null);
676                     remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED);
677                 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) {
678                     remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED,
679                             result.newExpandedView);
680                 }
681                 if (result.newExpandedView != null) {
682                     privateLayout.setExpandedInflatedSmartReplies(
683                             result.expandedInflatedSmartReplies);
684                 } else {
685                     privateLayout.setExpandedInflatedSmartReplies(null);
686                 }
687                 row.setExpandable(result.newExpandedView != null);
688                 setRepliesAndActions = true;
689             }
690 
691             if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
692                 if (result.inflatedHeadsUpView != null) {
693                     privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
694                     remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP,
695                             result.newHeadsUpView);
696                 } else if (result.newHeadsUpView == null) {
697                     privateLayout.setHeadsUpChild(null);
698                     remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP);
699                 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) {
700                     remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP,
701                             result.newHeadsUpView);
702                 }
703                 if (result.newHeadsUpView != null) {
704                     privateLayout.setHeadsUpInflatedSmartReplies(
705                             result.headsUpInflatedSmartReplies);
706                 } else {
707                     privateLayout.setHeadsUpInflatedSmartReplies(null);
708                 }
709                 setRepliesAndActions = true;
710             }
711             if (setRepliesAndActions) {
712                 privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState);
713             }
714 
715             if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
716                 if (result.inflatedPublicView != null) {
717                     publicLayout.setContractedChild(result.inflatedPublicView);
718                     remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC,
719                             result.newPublicView);
720                 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) {
721                     remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC,
722                             result.newPublicView);
723                 }
724             }
725 
726             entry.headsUpStatusBarText = result.headsUpStatusBarText;
727             entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic;
728             if (endListener != null) {
729                 endListener.onAsyncInflationFinished(entry);
730             }
731             return true;
732         }
733         return false;
734     }
735 
createExpandedView(Notification.Builder builder, boolean isLowPriority)736     private static RemoteViews createExpandedView(Notification.Builder builder,
737             boolean isLowPriority) {
738         RemoteViews bigContentView = builder.createBigContentView();
739         if (bigContentView != null) {
740             return bigContentView;
741         }
742         if (isLowPriority) {
743             RemoteViews contentView = builder.createContentView();
744             Notification.Builder.makeHeaderExpanded(contentView);
745             return contentView;
746         }
747         return null;
748     }
749 
createContentView(Notification.Builder builder, boolean isLowPriority, boolean useLarge)750     private static RemoteViews createContentView(Notification.Builder builder,
751             boolean isLowPriority, boolean useLarge) {
752         if (isLowPriority) {
753             return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
754         }
755         return builder.createContentView(useLarge);
756     }
757 
758     /**
759      * @param newView The new view that will be applied
760      * @param oldView The old view that was applied to the existing view before
761      * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply.
762      */
763      @VisibleForTesting
canReapplyRemoteView(final RemoteViews newView, final RemoteViews oldView)764      static boolean canReapplyRemoteView(final RemoteViews newView,
765             final RemoteViews oldView) {
766         return (newView == null && oldView == null) ||
767                 (newView != null && oldView != null
768                         && oldView.getPackage() != null
769                         && newView.getPackage() != null
770                         && newView.getPackage().equals(oldView.getPackage())
771                         && newView.getLayoutId() == oldView.getLayoutId()
772                         && !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED));
773     }
774 
775     /**
776      * Sets whether to perform inflation on the same thread as the caller. This method should only
777      * be used in tests, not in production.
778      */
779     @VisibleForTesting
setInflateSynchronously(boolean inflateSynchronously)780     public void setInflateSynchronously(boolean inflateSynchronously) {
781         mInflateSynchronously = inflateSynchronously;
782     }
783 
784     public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
785             implements InflationCallback, InflationTask {
786 
787         private static final long IMG_PRELOAD_TIMEOUT_MS = 1000L;
788         private final NotificationEntry mEntry;
789         private final Context mContext;
790         private final boolean mInflateSynchronously;
791         private final boolean mIsLowPriority;
792         private final boolean mUsesIncreasedHeight;
793         private final InflationCallback mCallback;
794         private final boolean mUsesIncreasedHeadsUpHeight;
795         private final @InflationFlag int mReInflateFlags;
796         private final NotifRemoteViewCache mRemoteViewCache;
797         private final Executor mBgExecutor;
798         private ExpandableNotificationRow mRow;
799         private Exception mError;
800         private RemoteViews.InteractionHandler mRemoteViewClickHandler;
801         private CancellationSignal mCancellationSignal;
802         private final ConversationNotificationProcessor mConversationProcessor;
803         private final boolean mIsMediaInQS;
804         private final SmartReplyStateInflater mSmartRepliesInflater;
805 
AsyncInflationTask( Executor bgExecutor, boolean inflateSynchronously, @InflationFlag int reInflateFlags, NotifRemoteViewCache cache, NotificationEntry entry, ConversationNotificationProcessor conversationProcessor, ExpandableNotificationRow row, boolean isLowPriority, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, InflationCallback callback, RemoteViews.InteractionHandler remoteViewClickHandler, boolean isMediaFlagEnabled, SmartReplyStateInflater smartRepliesInflater)806         private AsyncInflationTask(
807                 Executor bgExecutor,
808                 boolean inflateSynchronously,
809                 @InflationFlag int reInflateFlags,
810                 NotifRemoteViewCache cache,
811                 NotificationEntry entry,
812                 ConversationNotificationProcessor conversationProcessor,
813                 ExpandableNotificationRow row,
814                 boolean isLowPriority,
815                 boolean usesIncreasedHeight,
816                 boolean usesIncreasedHeadsUpHeight,
817                 InflationCallback callback,
818                 RemoteViews.InteractionHandler remoteViewClickHandler,
819                 boolean isMediaFlagEnabled,
820                 SmartReplyStateInflater smartRepliesInflater) {
821             mEntry = entry;
822             mRow = row;
823             mBgExecutor = bgExecutor;
824             mInflateSynchronously = inflateSynchronously;
825             mReInflateFlags = reInflateFlags;
826             mRemoteViewCache = cache;
827             mSmartRepliesInflater = smartRepliesInflater;
828             mContext = mRow.getContext();
829             mIsLowPriority = isLowPriority;
830             mUsesIncreasedHeight = usesIncreasedHeight;
831             mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
832             mRemoteViewClickHandler = remoteViewClickHandler;
833             mCallback = callback;
834             mConversationProcessor = conversationProcessor;
835             mIsMediaInQS = isMediaFlagEnabled;
836             entry.setInflationTask(this);
837         }
838 
839         @VisibleForTesting
840         @InflationFlag
getReInflateFlags()841         public int getReInflateFlags() {
842             return mReInflateFlags;
843         }
844 
updateApplicationInfo(StatusBarNotification sbn)845         void updateApplicationInfo(StatusBarNotification sbn) {
846             String packageName = sbn.getPackageName();
847             int userId = UserHandle.getUserId(sbn.getUid());
848             final ApplicationInfo appInfo;
849             try {
850                 // This method has an internal cache, so we don't need to add our own caching here.
851                 appInfo = mContext.getPackageManager().getApplicationInfoAsUser(packageName,
852                         PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
853             } catch (PackageManager.NameNotFoundException e) {
854                 return;
855             }
856             Notification.addFieldsFromContext(appInfo, sbn.getNotification());
857         }
858 
859         @Override
doInBackground(Void... params)860         protected InflationProgress doInBackground(Void... params) {
861             try {
862                 final StatusBarNotification sbn = mEntry.getSbn();
863                 // Ensure the ApplicationInfo is updated before a builder is recovered.
864                 updateApplicationInfo(sbn);
865                 final Notification.Builder recoveredBuilder
866                         = Notification.Builder.recoverBuilder(mContext,
867                         sbn.getNotification());
868 
869                 Context packageContext = sbn.getPackageContext(mContext);
870                 if (recoveredBuilder.usesTemplate()) {
871                     // For all of our templates, we want it to be RTL
872                     packageContext = new RtlEnabledContext(packageContext);
873                 }
874                 if (mEntry.getRanking().isConversation()) {
875                     mConversationProcessor.processNotification(mEntry, recoveredBuilder);
876                 }
877                 InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
878                         recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
879                         mUsesIncreasedHeadsUpHeight, packageContext);
880                 InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState();
881                 InflationProgress result = inflateSmartReplyViews(
882                         inflationProgress,
883                         mReInflateFlags,
884                         mEntry,
885                         mContext,
886                         packageContext,
887                         previousSmartReplyState,
888                         mSmartRepliesInflater);
889 
890                 // wait for image resolver to finish preloading
891                 mRow.getImageResolver().waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS);
892 
893                 return result;
894             } catch (Exception e) {
895                 mError = e;
896                 return null;
897             }
898         }
899 
900         @Override
onPostExecute(InflationProgress result)901         protected void onPostExecute(InflationProgress result) {
902             if (mError == null) {
903                 mCancellationSignal = apply(
904                         mBgExecutor,
905                         mInflateSynchronously,
906                         result,
907                         mReInflateFlags,
908                         mRemoteViewCache,
909                         mEntry,
910                         mRow,
911                         mRemoteViewClickHandler,
912                         this);
913             } else {
914                 handleError(mError);
915             }
916         }
917 
handleError(Exception e)918         private void handleError(Exception e) {
919             mEntry.onInflationTaskFinished();
920             StatusBarNotification sbn = mEntry.getSbn();
921             final String ident = sbn.getPackageName() + "/0x"
922                     + Integer.toHexString(sbn.getId());
923             Log.e(CentralSurfaces.TAG, "couldn't inflate view for notification " + ident, e);
924             if (mCallback != null) {
925                 mCallback.handleInflationException(mRow.getEntry(),
926                         new InflationException("Couldn't inflate contentViews" + e));
927             }
928 
929             // Cancel any image loading tasks, not useful any more
930             mRow.getImageResolver().cancelRunningTasks();
931         }
932 
933         @Override
abort()934         public void abort() {
935             cancel(true /* mayInterruptIfRunning */);
936             if (mCancellationSignal != null) {
937                 mCancellationSignal.cancel();
938             }
939         }
940 
941         @Override
handleInflationException(NotificationEntry entry, Exception e)942         public void handleInflationException(NotificationEntry entry, Exception e) {
943             handleError(e);
944         }
945 
946         @Override
onAsyncInflationFinished(NotificationEntry entry)947         public void onAsyncInflationFinished(NotificationEntry entry) {
948             mEntry.onInflationTaskFinished();
949             mRow.onNotificationUpdated();
950             if (mCallback != null) {
951                 mCallback.onAsyncInflationFinished(mEntry);
952             }
953 
954             // Notify the resolver that the inflation task has finished,
955             // try to purge unnecessary cached entries.
956             mRow.getImageResolver().purgeCache();
957 
958             // Cancel any image loading tasks that have not completed at this point
959             mRow.getImageResolver().cancelRunningTasks();
960         }
961 
962         private static class RtlEnabledContext extends ContextWrapper {
RtlEnabledContext(Context packageContext)963             private RtlEnabledContext(Context packageContext) {
964                 super(packageContext);
965             }
966 
967             @Override
getApplicationInfo()968             public ApplicationInfo getApplicationInfo() {
969                 ApplicationInfo applicationInfo = super.getApplicationInfo();
970                 applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_RTL;
971                 return applicationInfo;
972             }
973         }
974     }
975 
976     @VisibleForTesting
977     static class InflationProgress {
978         private RemoteViews newContentView;
979         private RemoteViews newHeadsUpView;
980         private RemoteViews newExpandedView;
981         private RemoteViews newPublicView;
982 
983         @VisibleForTesting
984         Context packageContext;
985 
986         private View inflatedContentView;
987         private View inflatedHeadsUpView;
988         private View inflatedExpandedView;
989         private View inflatedPublicView;
990         private CharSequence headsUpStatusBarText;
991         private CharSequence headsUpStatusBarTextPublic;
992 
993         private InflatedSmartReplyState inflatedSmartReplyState;
994         private InflatedSmartReplyViewHolder expandedInflatedSmartReplies;
995         private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies;
996     }
997 
998     @VisibleForTesting
999     abstract static class ApplyCallback {
setResultView(View v)1000         public abstract void setResultView(View v);
getRemoteView()1001         public abstract RemoteViews getRemoteView();
1002     }
1003 }
1004