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 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_SINGLELINE;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.app.Notification;
28 import android.content.Context;
29 import android.content.ContextWrapper;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageManager;
32 import android.content.res.Resources;
33 import android.os.AsyncTask;
34 import android.os.Build;
35 import android.os.CancellationSignal;
36 import android.os.Trace;
37 import android.os.UserHandle;
38 import android.service.notification.StatusBarNotification;
39 import android.util.Log;
40 import android.view.NotificationHeaderView;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.widget.RemoteViews;
44 
45 import com.android.app.tracing.TraceUtils;
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.widget.ImageMessageConsumer;
48 import com.android.systemui.dagger.SysUISingleton;
49 import com.android.systemui.dagger.qualifiers.NotifInflation;
50 import com.android.systemui.media.controls.util.MediaFeatureFlag;
51 import com.android.systemui.res.R;
52 import com.android.systemui.statusbar.InflationTask;
53 import com.android.systemui.statusbar.NotificationRemoteInputManager;
54 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
55 import com.android.systemui.statusbar.notification.InflationException;
56 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
57 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
58 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
59 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor;
60 import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder;
61 import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder;
62 import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel;
63 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
64 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
65 import com.android.systemui.statusbar.phone.CentralSurfaces;
66 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
67 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
68 import com.android.systemui.statusbar.policy.SmartReplyStateInflater;
69 import com.android.systemui.util.Assert;
70 
71 import java.util.HashMap;
72 import java.util.concurrent.Executor;
73 
74 import javax.inject.Inject;
75 
76 /**
77  * {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by
78  * asynchronously building the content's {@link RemoteViews} and applying it to the row.
79  */
80 @SysUISingleton
81 @VisibleForTesting(visibility = PACKAGE)
82 public class NotificationContentInflater implements NotificationRowContentBinder {
83 
84     public static final String TAG = "NotifContentInflater";
85 
86     private boolean mInflateSynchronously = false;
87     private final boolean mIsMediaInQS;
88     private final NotificationRemoteInputManager mRemoteInputManager;
89     private final NotifRemoteViewCache mRemoteViewCache;
90     private final ConversationNotificationProcessor mConversationProcessor;
91     private final Executor mInflationExecutor;
92     private final SmartReplyStateInflater mSmartReplyStateInflater;
93     private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider;
94     private final HeadsUpStyleProvider mHeadsUpStyleProvider;
95 
96     private final NotificationRowContentBinderLogger mLogger;
97 
98     @Inject
NotificationContentInflater( NotifRemoteViewCache remoteViewCache, NotificationRemoteInputManager remoteInputManager, ConversationNotificationProcessor conversationProcessor, MediaFeatureFlag mediaFeatureFlag, @NotifInflation Executor inflationExecutor, SmartReplyStateInflater smartRepliesInflater, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, HeadsUpStyleProvider headsUpStyleProvider, NotificationRowContentBinderLogger logger)99     NotificationContentInflater(
100             NotifRemoteViewCache remoteViewCache,
101             NotificationRemoteInputManager remoteInputManager,
102             ConversationNotificationProcessor conversationProcessor,
103             MediaFeatureFlag mediaFeatureFlag,
104             @NotifInflation Executor inflationExecutor,
105             SmartReplyStateInflater smartRepliesInflater,
106             NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
107             HeadsUpStyleProvider headsUpStyleProvider,
108             NotificationRowContentBinderLogger logger) {
109         NotificationRowContentBinderRefactor.assertInLegacyMode();
110         mRemoteViewCache = remoteViewCache;
111         mRemoteInputManager = remoteInputManager;
112         mConversationProcessor = conversationProcessor;
113         mIsMediaInQS = mediaFeatureFlag.getEnabled();
114         mInflationExecutor = inflationExecutor;
115         mSmartReplyStateInflater = smartRepliesInflater;
116         mNotifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider;
117         mHeadsUpStyleProvider = headsUpStyleProvider;
118         mLogger = logger;
119     }
120 
121     @Override
bindContent( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int contentToBind, BindParams bindParams, boolean forceInflate, @Nullable InflationCallback callback)122     public void bindContent(
123             NotificationEntry entry,
124             ExpandableNotificationRow row,
125             @InflationFlag int contentToBind,
126             BindParams bindParams,
127             boolean forceInflate,
128             @Nullable InflationCallback callback) {
129         if (row.isRemoved()) {
130             // We don't want to reinflate anything for removed notifications. Otherwise views might
131             // be readded to the stack, leading to leaks. This may happen with low-priority groups
132             // where the removal of already removed children can lead to a reinflation.
133             mLogger.logNotBindingRowWasRemoved(entry);
134             return;
135         }
136 
137         mLogger.logBinding(entry, contentToBind);
138 
139         StatusBarNotification sbn = entry.getSbn();
140 
141         // To check if the notification has inline image and preload inline image if necessary.
142         row.getImageResolver().preloadImages(sbn.getNotification());
143 
144         if (forceInflate) {
145             mRemoteViewCache.clearCache(entry);
146         }
147 
148         // Cancel any pending frees on any view we're trying to bind since we should be bound after.
149         cancelContentViewFrees(row, contentToBind);
150 
151         AsyncInflationTask task = new AsyncInflationTask(
152                 mInflationExecutor,
153                 mInflateSynchronously,
154                 /* reInflateFlags = */ contentToBind,
155                 mRemoteViewCache,
156                 entry,
157                 mConversationProcessor,
158                 row,
159                 bindParams.isMinimized,
160                 bindParams.usesIncreasedHeight,
161                 bindParams.usesIncreasedHeadsUpHeight,
162                 callback,
163                 mRemoteInputManager.getRemoteViewsOnClickHandler(),
164                 /* isMediaFlagEnabled = */ mIsMediaInQS,
165                 mSmartReplyStateInflater,
166                 mNotifLayoutInflaterFactoryProvider,
167                 mHeadsUpStyleProvider,
168                 mLogger);
169         if (mInflateSynchronously) {
170             task.onPostExecute(task.doInBackground());
171         } else {
172             task.executeOnExecutor(mInflationExecutor);
173         }
174     }
175 
176     @VisibleForTesting
inflateNotificationViews( NotificationEntry entry, ExpandableNotificationRow row, BindParams bindParams, boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, Context packageContext, SmartReplyStateInflater smartRepliesInflater)177     InflationProgress inflateNotificationViews(
178             NotificationEntry entry,
179             ExpandableNotificationRow row,
180             BindParams bindParams,
181             boolean inflateSynchronously,
182             @InflationFlag int reInflateFlags,
183             Notification.Builder builder,
184             Context packageContext,
185             SmartReplyStateInflater smartRepliesInflater) {
186         InflationProgress result = createRemoteViews(reInflateFlags,
187                 builder,
188                 bindParams.isMinimized,
189                 bindParams.usesIncreasedHeight,
190                 bindParams.usesIncreasedHeadsUpHeight,
191                 packageContext,
192                 row,
193                 mNotifLayoutInflaterFactoryProvider,
194                 mHeadsUpStyleProvider,
195                 mLogger);
196 
197         result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(),
198                 packageContext, row.getExistingSmartReplyState(), smartRepliesInflater, mLogger);
199         if (AsyncHybridViewInflation.isEnabled()) {
200             boolean isConversation = entry.getRanking().isConversation();
201             Notification.MessagingStyle messagingStyle = null;
202             if (isConversation) {
203                 messagingStyle = mConversationProcessor
204                         .processNotification(entry, builder, mLogger);
205             }
206             result.mInflatedSingleLineViewModel = SingleLineViewInflater
207                     .inflateSingleLineViewModel(
208                             entry.getSbn().getNotification(),
209                             messagingStyle,
210                             builder,
211                             row.getContext()
212                     );
213             result.mInflatedSingleLineViewHolder =
214                     SingleLineViewInflater.inflateSingleLineViewHolder(
215                             isConversation,
216                             reInflateFlags,
217                             entry,
218                             row.getContext(),
219                             mLogger
220                     );
221         }
222 
223         apply(
224                 mInflationExecutor,
225                 inflateSynchronously,
226                 bindParams.isMinimized,
227                 result,
228                 reInflateFlags,
229                 mRemoteViewCache,
230                 entry,
231                 row,
232                 mRemoteInputManager.getRemoteViewsOnClickHandler(),
233                 null /* callback */,
234                 mLogger);
235         return result;
236     }
237 
238     @Override
cancelBind( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row)239     public boolean cancelBind(
240             @NonNull NotificationEntry entry,
241             @NonNull ExpandableNotificationRow row) {
242         final boolean abortedTask = entry.abortTask();
243         if (abortedTask) {
244             mLogger.logCancelBindAbortedTask(entry);
245         }
246         return abortedTask;
247     }
248 
249     @Override
unbindContent( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row, @InflationFlag int contentToUnbind)250     public void unbindContent(
251             @NonNull NotificationEntry entry,
252             @NonNull ExpandableNotificationRow row,
253             @InflationFlag int contentToUnbind) {
254         mLogger.logUnbinding(entry, contentToUnbind);
255         int curFlag = 1;
256         while (contentToUnbind != 0) {
257             if ((contentToUnbind & curFlag) != 0) {
258                 freeNotificationView(entry, row, curFlag);
259             }
260             contentToUnbind &= ~curFlag;
261             curFlag = curFlag << 1;
262         }
263     }
264 
265     /**
266      * Frees the content view associated with the inflation flag as soon as the view is not showing.
267      *
268      * @param inflateFlag the flag corresponding to the content view which should be freed
269      */
freeNotificationView( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int inflateFlag)270     private void freeNotificationView(
271             NotificationEntry entry,
272             ExpandableNotificationRow row,
273             @InflationFlag int inflateFlag) {
274         switch (inflateFlag) {
275             case FLAG_CONTENT_VIEW_CONTRACTED:
276                 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> {
277                     row.getPrivateLayout().setContractedChild(null);
278                     mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED);
279                 });
280                 break;
281             case FLAG_CONTENT_VIEW_EXPANDED:
282                 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_EXPANDED, () -> {
283                     row.getPrivateLayout().setExpandedChild(null);
284                     mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED);
285                 });
286                 break;
287             case FLAG_CONTENT_VIEW_HEADS_UP:
288                 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_HEADSUP, () -> {
289                     row.getPrivateLayout().setHeadsUpChild(null);
290                     mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP);
291                     row.getPrivateLayout().setHeadsUpInflatedSmartReplies(null);
292                 });
293                 break;
294             case FLAG_CONTENT_VIEW_PUBLIC:
295                 row.getPublicLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> {
296                     row.getPublicLayout().setContractedChild(null);
297                     mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC);
298                 });
299                 break;
300             case FLAG_CONTENT_VIEW_SINGLE_LINE: {
301                 if (AsyncHybridViewInflation.isEnabled()) {
302                     row.getPrivateLayout().performWhenContentInactive(
303                             VISIBLE_TYPE_SINGLELINE,
304                             () -> row.getPrivateLayout().setSingleLineView(null)
305                     );
306                 }
307                 break;
308             }
309             default:
310                 break;
311         }
312     }
313 
314     /**
315      * Cancel any pending content view frees from {@link #freeNotificationView} for the provided
316      * content views.
317      *
318      * @param row top level notification row containing the content views
319      * @param contentViews content views to cancel pending frees on
320      */
cancelContentViewFrees( ExpandableNotificationRow row, @InflationFlag int contentViews)321     private void cancelContentViewFrees(
322             ExpandableNotificationRow row,
323             @InflationFlag int contentViews) {
324         if ((contentViews & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
325             row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED);
326         }
327         if ((contentViews & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
328             row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_EXPANDED);
329         }
330         if ((contentViews & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
331             row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_HEADSUP);
332         }
333         if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
334             row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED);
335         }
336         if (AsyncHybridViewInflation.isEnabled()
337                 && (contentViews & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) {
338             row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE);
339         }
340     }
341 
inflateSmartReplyViews( InflationProgress result, @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, Context packageContext, InflatedSmartReplyState previousSmartReplyState, SmartReplyStateInflater inflater, NotificationRowContentBinderLogger logger)342     private static InflationProgress inflateSmartReplyViews(
343             InflationProgress result,
344             @InflationFlag int reInflateFlags,
345             NotificationEntry entry,
346             Context context,
347             Context packageContext,
348             InflatedSmartReplyState previousSmartReplyState,
349             SmartReplyStateInflater inflater,
350             NotificationRowContentBinderLogger logger) {
351         boolean inflateContracted = (reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0
352                 && result.newContentView != null;
353         boolean inflateExpanded = (reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0
354                 && result.newExpandedView != null;
355         boolean inflateHeadsUp = (reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0
356                 && result.newHeadsUpView != null;
357         if (inflateContracted || inflateExpanded || inflateHeadsUp) {
358             logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state");
359             result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry);
360         }
361         if (inflateExpanded) {
362             logger.logAsyncTaskProgress(entry, "inflating expanded smart reply state");
363             result.expandedInflatedSmartReplies = inflater.inflateSmartReplyViewHolder(
364                     context, packageContext, entry, previousSmartReplyState,
365                     result.inflatedSmartReplyState);
366         }
367         if (inflateHeadsUp) {
368             logger.logAsyncTaskProgress(entry, "inflating heads up smart reply state");
369             result.headsUpInflatedSmartReplies = inflater.inflateSmartReplyViewHolder(
370                     context, packageContext, entry, previousSmartReplyState,
371                     result.inflatedSmartReplyState);
372         }
373         return result;
374     }
375 
createRemoteViews(@nflationFlag int reInflateFlags, Notification.Builder builder, boolean isMinimized, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, Context packageContext, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, HeadsUpStyleProvider headsUpStyleProvider, NotificationRowContentBinderLogger logger)376     private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags,
377             Notification.Builder builder, boolean isMinimized, boolean usesIncreasedHeight,
378             boolean usesIncreasedHeadsUpHeight, Context packageContext,
379             ExpandableNotificationRow row,
380             NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
381             HeadsUpStyleProvider headsUpStyleProvider,
382             NotificationRowContentBinderLogger logger) {
383         return TraceUtils.trace("NotificationContentInflater.createRemoteViews", () -> {
384             InflationProgress result = new InflationProgress();
385             final NotificationEntry entryForLogging = row.getEntry();
386 
387             if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
388                 logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view");
389                 result.newContentView = createContentView(builder, isMinimized,
390                         usesIncreasedHeight);
391             }
392 
393             if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
394                 logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view");
395                 result.newExpandedView = createExpandedView(builder, isMinimized);
396             }
397 
398             if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
399                 logger.logAsyncTaskProgress(entryForLogging, "creating heads up remote view");
400                 final boolean isHeadsUpCompact = headsUpStyleProvider.shouldApplyCompactStyle();
401                 if (isHeadsUpCompact) {
402                     result.newHeadsUpView = builder.createCompactHeadsUpContentView();
403                 } else {
404                     result.newHeadsUpView = builder.createHeadsUpContentView(
405                             usesIncreasedHeadsUpHeight);
406                 }
407             }
408 
409             if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
410                 logger.logAsyncTaskProgress(entryForLogging, "creating public remote view");
411                 result.newPublicView = builder.makePublicContentView(isMinimized);
412             }
413 
414             if (AsyncGroupHeaderViewInflation.isEnabled()) {
415                 if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
416                     logger.logAsyncTaskProgress(entryForLogging,
417                             "creating group summary remote view");
418                     result.mNewGroupHeaderView = builder.makeNotificationGroupHeader();
419                 }
420 
421                 if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
422                     logger.logAsyncTaskProgress(entryForLogging,
423                             "creating low-priority group summary remote view");
424                     result.mNewMinimizedGroupHeaderView =
425                             builder.makeLowPriorityContentView(true /* useRegularSubtext */);
426                 }
427             }
428             setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider);
429             result.packageContext = packageContext;
430             result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(
431                     false /* showingPublic */);
432             result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
433                     true /* showingPublic */);
434 
435             return result;
436         });
437     }
438 
setNotifsViewsInflaterFactory(InflationProgress result, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider)439     private static void setNotifsViewsInflaterFactory(InflationProgress result,
440             ExpandableNotificationRow row,
441             NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider) {
442         setRemoteViewsInflaterFactory(result.newContentView,
443                 notifLayoutInflaterFactoryProvider.provide(row, FLAG_CONTENT_VIEW_CONTRACTED));
444         setRemoteViewsInflaterFactory(result.newExpandedView,
445                 notifLayoutInflaterFactoryProvider.provide(row, FLAG_CONTENT_VIEW_EXPANDED));
446         setRemoteViewsInflaterFactory(result.newHeadsUpView,
447                 notifLayoutInflaterFactoryProvider.provide(row, FLAG_CONTENT_VIEW_HEADS_UP));
448         setRemoteViewsInflaterFactory(result.newPublicView,
449                 notifLayoutInflaterFactoryProvider.provide(row, FLAG_CONTENT_VIEW_PUBLIC));
450     }
451 
setRemoteViewsInflaterFactory(RemoteViews remoteViews, NotifLayoutInflaterFactory notifLayoutInflaterFactory)452     private static void setRemoteViewsInflaterFactory(RemoteViews remoteViews,
453             NotifLayoutInflaterFactory notifLayoutInflaterFactory) {
454         if (remoteViews != null) {
455             remoteViews.setLayoutInflaterFactory(notifLayoutInflaterFactory);
456         }
457     }
458 
apply( Executor inflationExecutor, boolean inflateSynchronously, boolean isMinimized, InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, NotificationEntry entry, ExpandableNotificationRow row, RemoteViews.InteractionHandler remoteViewClickHandler, @Nullable InflationCallback callback, NotificationRowContentBinderLogger logger)459     private static CancellationSignal apply(
460             Executor inflationExecutor,
461             boolean inflateSynchronously,
462             boolean isMinimized,
463             InflationProgress result,
464             @InflationFlag int reInflateFlags,
465             NotifRemoteViewCache remoteViewCache,
466             NotificationEntry entry,
467             ExpandableNotificationRow row,
468             RemoteViews.InteractionHandler remoteViewClickHandler,
469             @Nullable InflationCallback callback,
470             NotificationRowContentBinderLogger logger) {
471         Trace.beginAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
472 
473         NotificationContentView privateLayout = row.getPrivateLayout();
474         NotificationContentView publicLayout = row.getPublicLayout();
475         final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
476 
477         int flag = FLAG_CONTENT_VIEW_CONTRACTED;
478         if ((reInflateFlags & flag) != 0) {
479             boolean isNewView =
480                     !canReapplyRemoteView(result.newContentView,
481                             remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED));
482             ApplyCallback applyCallback = new ApplyCallback() {
483                 @Override
484                 public void setResultView(View v) {
485                     logger.logAsyncTaskProgress(entry, "contracted view applied");
486                     result.inflatedContentView = v;
487                 }
488                 @Override
489                 public RemoteViews getRemoteView() {
490                     return result.newContentView;
491                 }
492             };
493             logger.logAsyncTaskProgress(entry, "applying contracted view");
494             applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
495                     reInflateFlags, flag,
496                     remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
497                     privateLayout, privateLayout.getContractedChild(),
498                     privateLayout.getVisibleWrapper(
499                             NotificationContentView.VISIBLE_TYPE_CONTRACTED),
500                     runningInflations, applyCallback, logger);
501         }
502 
503         flag = FLAG_CONTENT_VIEW_EXPANDED;
504         if ((reInflateFlags & flag) != 0) {
505             if (result.newExpandedView != null) {
506                 boolean isNewView =
507                         !canReapplyRemoteView(result.newExpandedView,
508                                 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED));
509                 ApplyCallback applyCallback = new ApplyCallback() {
510                     @Override
511                     public void setResultView(View v) {
512                         logger.logAsyncTaskProgress(entry, "expanded view applied");
513                         result.inflatedExpandedView = v;
514                     }
515 
516                     @Override
517                     public RemoteViews getRemoteView() {
518                         return result.newExpandedView;
519                     }
520                 };
521                 logger.logAsyncTaskProgress(entry, "applying expanded view");
522                 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
523                         reInflateFlags,
524                         flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
525                         callback, privateLayout, privateLayout.getExpandedChild(),
526                         privateLayout.getVisibleWrapper(VISIBLE_TYPE_EXPANDED), runningInflations,
527                         applyCallback, logger);
528             }
529         }
530 
531         flag = FLAG_CONTENT_VIEW_HEADS_UP;
532         if ((reInflateFlags & flag) != 0) {
533             if (result.newHeadsUpView != null) {
534                 boolean isNewView =
535                         !canReapplyRemoteView(result.newHeadsUpView,
536                                 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP));
537                 ApplyCallback applyCallback = new ApplyCallback() {
538                     @Override
539                     public void setResultView(View v) {
540                         logger.logAsyncTaskProgress(entry, "heads up view applied");
541                         result.inflatedHeadsUpView = v;
542                     }
543 
544                     @Override
545                     public RemoteViews getRemoteView() {
546                         return result.newHeadsUpView;
547                     }
548                 };
549                 logger.logAsyncTaskProgress(entry, "applying heads up view");
550                 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
551                         result, reInflateFlags,
552                         flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
553                         callback, privateLayout, privateLayout.getHeadsUpChild(),
554                         privateLayout.getVisibleWrapper(VISIBLE_TYPE_HEADSUP), runningInflations,
555                         applyCallback, logger);
556             }
557         }
558 
559         flag = FLAG_CONTENT_VIEW_PUBLIC;
560         if ((reInflateFlags & flag) != 0) {
561             boolean isNewView =
562                     !canReapplyRemoteView(result.newPublicView,
563                             remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC));
564             ApplyCallback applyCallback = new ApplyCallback() {
565                 @Override
566                 public void setResultView(View v) {
567                     logger.logAsyncTaskProgress(entry, "public view applied");
568                     result.inflatedPublicView = v;
569                 }
570 
571                 @Override
572                 public RemoteViews getRemoteView() {
573                     return result.newPublicView;
574                 }
575             };
576             logger.logAsyncTaskProgress(entry, "applying public view");
577             applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
578                     result, reInflateFlags, flag,
579                     remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
580                     publicLayout, publicLayout.getContractedChild(),
581                     publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),
582                     runningInflations, applyCallback, logger);
583         }
584 
585         if (AsyncGroupHeaderViewInflation.isEnabled()) {
586             NotificationChildrenContainer childrenContainer = row.getChildrenContainerNonNull();
587             if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
588                 boolean isNewView =
589                         !canReapplyRemoteView(
590                                 /* newView = */ result.mNewGroupHeaderView,
591                                 /* oldView = */ remoteViewCache
592                                         .getCachedView(entry, FLAG_GROUP_SUMMARY_HEADER));
593                 ApplyCallback applyCallback = new ApplyCallback() {
594                     @Override
595                     public void setResultView(View v) {
596                         logger.logAsyncTaskProgress(entry, "group header view applied");
597                         result.mInflatedGroupHeaderView = (NotificationHeaderView) v;
598                     }
599 
600                     @Override
601                     public RemoteViews getRemoteView() {
602                         return result.mNewGroupHeaderView;
603                     }
604                 };
605                 logger.logAsyncTaskProgress(entry, "applying group header view");
606                 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
607                         result, reInflateFlags,
608                         /* inflationId = */ FLAG_GROUP_SUMMARY_HEADER,
609                         remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
610                         /* parentLayout = */ childrenContainer,
611                         /* existingView = */ childrenContainer.getGroupHeader(),
612                         /* existingWrapper = */ childrenContainer.getNotificationHeaderWrapper(),
613                         runningInflations, applyCallback, logger);
614             }
615 
616             if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
617                 boolean isNewView =
618                         !canReapplyRemoteView(
619                                 /* newView = */ result.mNewMinimizedGroupHeaderView,
620                                 /* oldView = */ remoteViewCache.getCachedView(
621                                         entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER));
622                 ApplyCallback applyCallback = new ApplyCallback() {
623                     @Override
624                     public void setResultView(View v) {
625                         logger.logAsyncTaskProgress(entry,
626                                 "low-priority group header view applied");
627                         result.mInflatedMinimizedGroupHeaderView = (NotificationHeaderView) v;
628                     }
629 
630                     @Override
631                     public RemoteViews getRemoteView() {
632                         return result.mNewMinimizedGroupHeaderView;
633                     }
634                 };
635                 logger.logAsyncTaskProgress(entry, "applying low priority group header view");
636                 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
637                         result, reInflateFlags,
638                         /* inflationId = */ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
639                         remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
640                         /* parentLayout = */ childrenContainer,
641                         /* existingView = */ childrenContainer.getMinimizedNotificationHeader(),
642                         /* existingWrapper = */ childrenContainer
643                                 .getMinimizedGroupHeaderWrapper(),
644                         runningInflations, applyCallback, logger);
645             }
646         }
647 
648         // Let's try to finish, maybe nobody is even inflating anything
649         finishIfDone(result, isMinimized, reInflateFlags, remoteViewCache, runningInflations,
650                 callback, entry, row, logger);
651         CancellationSignal cancellationSignal = new CancellationSignal();
652         cancellationSignal.setOnCancelListener(
653                 () -> {
654                     logger.logAsyncTaskProgress(entry, "apply cancelled");
655                     Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
656                     runningInflations.values().forEach(CancellationSignal::cancel);
657                 });
658 
659         return cancellationSignal;
660     }
661 
662     @VisibleForTesting
applyRemoteView( Executor inflationExecutor, boolean inflateSynchronously, boolean isMinimized, 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, ViewGroup parentLayout, View existingView, NotificationViewWrapper existingWrapper, final HashMap<Integer, CancellationSignal> runningInflations, ApplyCallback applyCallback, NotificationRowContentBinderLogger logger)663     static void applyRemoteView(
664             Executor inflationExecutor,
665             boolean inflateSynchronously,
666             boolean isMinimized,
667             final InflationProgress result,
668             final @InflationFlag int reInflateFlags,
669             @InflationFlag int inflationId,
670             final NotifRemoteViewCache remoteViewCache,
671             final NotificationEntry entry,
672             final ExpandableNotificationRow row,
673             boolean isNewView,
674             RemoteViews.InteractionHandler remoteViewClickHandler,
675             @Nullable final InflationCallback callback,
676             ViewGroup parentLayout,
677             View existingView,
678             NotificationViewWrapper existingWrapper,
679             final HashMap<Integer, CancellationSignal> runningInflations,
680             ApplyCallback applyCallback,
681             NotificationRowContentBinderLogger logger) {
682         RemoteViews newContentView = applyCallback.getRemoteView();
683         if (inflateSynchronously) {
684             try {
685                 if (isNewView) {
686                     View v = newContentView.apply(
687                             result.packageContext,
688                             parentLayout,
689                             remoteViewClickHandler);
690                     validateView(v, entry, row.getResources());
691                     applyCallback.setResultView(v);
692                 } else {
693                     newContentView.reapply(
694                             result.packageContext,
695                             existingView,
696                             remoteViewClickHandler);
697                     validateView(existingView, entry, row.getResources());
698                     existingWrapper.onReinflated();
699                 }
700             } catch (Exception e) {
701                 handleInflationError(runningInflations, e, row.getEntry(), callback, logger,
702                         "applying view synchronously");
703                 // Add a running inflation to make sure we don't trigger callbacks.
704                 // Safe to do because only happens in tests.
705                 runningInflations.put(inflationId, new CancellationSignal());
706             }
707             return;
708         }
709         RemoteViews.OnViewAppliedListener listener = new RemoteViews.OnViewAppliedListener() {
710 
711             @Override
712             public void onViewInflated(View v) {
713                 if (v instanceof ImageMessageConsumer) {
714                     ((ImageMessageConsumer) v).setImageResolver(row.getImageResolver());
715                 }
716             }
717 
718             @Override
719             public void onViewApplied(View v) {
720                 String invalidReason = isValidView(v, entry, row.getResources());
721                 if (invalidReason != null) {
722                     handleInflationError(runningInflations, new InflationException(invalidReason),
723                             row.getEntry(), callback, logger, "applied invalid view");
724                     runningInflations.remove(inflationId);
725                     return;
726                 }
727                 if (isNewView) {
728                     applyCallback.setResultView(v);
729                 } else if (existingWrapper != null) {
730                     existingWrapper.onReinflated();
731                 }
732                 runningInflations.remove(inflationId);
733                 finishIfDone(result, isMinimized,
734                         reInflateFlags, remoteViewCache, runningInflations,
735                         callback, entry, row, logger);
736             }
737 
738             @Override
739             public void onError(Exception e) {
740                 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could
741                 // actually also be a system issue, so let's try on the UI thread again to be safe.
742                 try {
743                     View newView = existingView;
744                     if (isNewView) {
745                         newView = newContentView.apply(
746                                 result.packageContext,
747                                 parentLayout,
748                                 remoteViewClickHandler);
749                     } else {
750                         newContentView.reapply(
751                                 result.packageContext,
752                                 existingView,
753                                 remoteViewClickHandler);
754                     }
755                     Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.",
756                             e);
757                     onViewApplied(newView);
758                 } catch (Exception anotherException) {
759                     runningInflations.remove(inflationId);
760                     handleInflationError(runningInflations, e, row.getEntry(),
761                             callback, logger, "applying view");
762                 }
763             }
764         };
765         CancellationSignal cancellationSignal;
766         if (isNewView) {
767             cancellationSignal = newContentView.applyAsync(
768                     result.packageContext,
769                     parentLayout,
770                     inflationExecutor,
771                     listener,
772                     remoteViewClickHandler);
773         } else {
774             cancellationSignal = newContentView.reapplyAsync(
775                     result.packageContext,
776                     existingView,
777                     inflationExecutor,
778                     listener,
779                     remoteViewClickHandler);
780         }
781         runningInflations.put(inflationId, cancellationSignal);
782     }
783 
784     /**
785      * Checks if the given View is a valid notification View.
786      *
787      * @return null == valid, non-null == invalid, String represents reason for rejection.
788      */
789     @VisibleForTesting
790     @Nullable
isValidView(View view, NotificationEntry entry, Resources resources)791     static String isValidView(View view,
792             NotificationEntry entry,
793             Resources resources) {
794         if (!satisfiesMinHeightRequirement(view, entry, resources)) {
795             return "inflated notification does not meet minimum height requirement";
796         }
797         return null;
798     }
799 
satisfiesMinHeightRequirement(View view, NotificationEntry entry, Resources resources)800     private static boolean satisfiesMinHeightRequirement(View view,
801             NotificationEntry entry,
802             Resources resources) {
803         if (!requiresHeightCheck(entry)) {
804             return true;
805         }
806         return TraceUtils.trace("NotificationContentInflater#satisfiesMinHeightRequirement", () -> {
807             int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
808             int referenceWidth = resources.getDimensionPixelSize(
809                     R.dimen.notification_validation_reference_width);
810             int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth,
811                     View.MeasureSpec.EXACTLY);
812             view.measure(widthSpec, heightSpec);
813             int minHeight = resources.getDimensionPixelSize(
814                     R.dimen.notification_validation_minimum_allowed_height);
815             return view.getMeasuredHeight() >= minHeight;
816         });
817     }
818 
819     /**
820      * Notifications with undecorated custom views need to satisfy a minimum height to avoid visual
821      * issues.
822      */
823     private static boolean requiresHeightCheck(NotificationEntry entry) {
824         // Undecorated custom views are disallowed from S onwards
825         if (entry.targetSdk >= Build.VERSION_CODES.S) {
826             return false;
827         }
828         // No need to check if the app isn't using any custom views
829         Notification notification = entry.getSbn().getNotification();
830         if (notification.contentView == null
831                 && notification.bigContentView == null
832                 && notification.headsUpContentView == null) {
833             return false;
834         }
835         return true;
836     }
837 
838     private static void validateView(View view,
839             NotificationEntry entry,
840             Resources resources) throws InflationException {
841         String invalidReason = isValidView(view, entry, resources);
842         if (invalidReason != null) {
843             throw new InflationException(invalidReason);
844         }
845     }
846 
847     private static void handleInflationError(
848             HashMap<Integer, CancellationSignal> runningInflations, Exception e,
849             NotificationEntry notification, @Nullable InflationCallback callback,
850             NotificationRowContentBinderLogger logger, String logContext) {
851         Assert.isMainThread();
852         logger.logAsyncTaskException(notification, logContext, e);
853         runningInflations.values().forEach(CancellationSignal::cancel);
854         if (callback != null) {
855             callback.handleInflationException(notification, e);
856         }
857     }
858 
859     /**
860      * Finish the inflation of the views
861      *
862      * @return true if the inflation was finished
863      */
864     private static boolean finishIfDone(InflationProgress result,
865             boolean isMinimized,
866             @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache,
867             HashMap<Integer, CancellationSignal> runningInflations,
868             @Nullable InflationCallback endListener, NotificationEntry entry,
869             ExpandableNotificationRow row, NotificationRowContentBinderLogger logger) {
870         Assert.isMainThread();
871         if (!runningInflations.isEmpty()) {
872             return false;
873         }
874         NotificationContentView privateLayout = row.getPrivateLayout();
875         NotificationContentView publicLayout = row.getPublicLayout();
876         logger.logAsyncTaskProgress(entry, "finishing");
877         boolean setRepliesAndActions = true;
878         if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
879             if (result.inflatedContentView != null) {
880                 // New view case
881                 privateLayout.setContractedChild(result.inflatedContentView);
882                 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED,
883                         result.newContentView);
884             } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) {
885                 // Reinflation case. Only update if it's still cached (i.e. view has not been
886                 // freed while inflating).
887                 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED,
888                         result.newContentView);
889             }
890             setRepliesAndActions = true;
891         }
892 
893         if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
894             if (result.inflatedExpandedView != null) {
895                 privateLayout.setExpandedChild(result.inflatedExpandedView);
896                 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED,
897                         result.newExpandedView);
898             } else if (result.newExpandedView == null) {
899                 privateLayout.setExpandedChild(null);
900                 remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED);
901             } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) {
902                 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED,
903                         result.newExpandedView);
904             }
905             if (result.newExpandedView != null) {
906                 privateLayout.setExpandedInflatedSmartReplies(
907                         result.expandedInflatedSmartReplies);
908             } else {
909                 privateLayout.setExpandedInflatedSmartReplies(null);
910             }
911             row.setExpandable(result.newExpandedView != null);
912             setRepliesAndActions = true;
913         }
914 
915         if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
916             if (result.inflatedHeadsUpView != null) {
917                 privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
918                 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP,
919                         result.newHeadsUpView);
920             } else if (result.newHeadsUpView == null) {
921                 privateLayout.setHeadsUpChild(null);
922                 remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP);
923             } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) {
924                 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP,
925                         result.newHeadsUpView);
926             }
927             if (result.newHeadsUpView != null) {
928                 privateLayout.setHeadsUpInflatedSmartReplies(
929                         result.headsUpInflatedSmartReplies);
930             } else {
931                 privateLayout.setHeadsUpInflatedSmartReplies(null);
932             }
933             setRepliesAndActions = true;
934         }
935 
936         if (AsyncHybridViewInflation.isEnabled()
937                 && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) {
938             HybridNotificationView viewHolder = result.mInflatedSingleLineViewHolder;
939             SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel;
940             if (viewHolder != null && viewModel != null) {
941                 if (viewModel.isConversation()) {
942                     SingleLineConversationViewBinder.bind(
943                             result.mInflatedSingleLineViewModel,
944                             result.mInflatedSingleLineViewHolder
945                     );
946                 } else {
947                     SingleLineViewBinder.bind(result.mInflatedSingleLineViewModel,
948                             result.mInflatedSingleLineViewHolder);
949                 }
950                 privateLayout.setSingleLineView(result.mInflatedSingleLineViewHolder);
951             }
952         }
953 
954         if (setRepliesAndActions) {
955             privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState);
956         }
957 
958         if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
959             if (result.inflatedPublicView != null) {
960                 publicLayout.setContractedChild(result.inflatedPublicView);
961                 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC,
962                         result.newPublicView);
963             } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) {
964                 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC,
965                         result.newPublicView);
966             }
967         }
968 
969         if (AsyncGroupHeaderViewInflation.isEnabled()) {
970             if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
971                 if (result.mInflatedGroupHeaderView != null) {
972                     // We need to set if the row is minimized before setting the group header to
973                     // make sure the setting of header view works correctly
974                     row.setIsMinimized(isMinimized);
975                     row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView);
976                     remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER,
977                             result.mNewGroupHeaderView);
978                 } else if (remoteViewCache.hasCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)) {
979                     // Re-inflation case. Only update if it's still cached (i.e. view has not
980                     // been freed while inflating).
981                     remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER,
982                             result.mNewGroupHeaderView);
983                 }
984             }
985 
986             if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
987                 if (result.mInflatedMinimizedGroupHeaderView != null) {
988                     // We need to set if the row is minimized before setting the group header to
989                     // make sure the setting of header view works correctly
990                     row.setIsMinimized(isMinimized);
991                     row.setMinimizedGroupHeader(
992                             /* headerView= */ result.mInflatedMinimizedGroupHeaderView);
993                     remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
994                             result.mNewMinimizedGroupHeaderView);
995                 } else if (remoteViewCache.hasCachedView(entry,
996                         FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) {
997                     // Re-inflation case. Only update if it's still cached (i.e. view has not
998                     // been freed while inflating).
999                     remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
1000                             result.mNewGroupHeaderView);
1001                 }
1002             }
1003         }
1004 
1005         entry.setHeadsUpStatusBarText(result.headsUpStatusBarText);
1006         entry.setHeadsUpStatusBarTextPublic(result.headsUpStatusBarTextPublic);
1007         Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
1008         if (endListener != null) {
1009             endListener.onAsyncInflationFinished(entry);
1010         }
1011         return true;
1012     }
1013 
1014     private static RemoteViews createExpandedView(Notification.Builder builder,
1015             boolean isMinimized) {
1016         RemoteViews bigContentView = builder.createBigContentView();
1017         if (bigContentView != null) {
1018             return bigContentView;
1019         }
1020         if (isMinimized) {
1021             RemoteViews contentView = builder.createContentView();
1022             Notification.Builder.makeHeaderExpanded(contentView);
1023             return contentView;
1024         }
1025         return null;
1026     }
1027 
1028     private static RemoteViews createContentView(Notification.Builder builder,
1029             boolean isMinimized, boolean useLarge) {
1030         if (isMinimized) {
1031             return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
1032         }
1033         return builder.createContentView(useLarge);
1034     }
1035 
1036     /**
1037      * @param newView The new view that will be applied
1038      * @param oldView The old view that was applied to the existing view before
1039      * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply.
1040      */
1041     @VisibleForTesting
1042     static boolean canReapplyRemoteView(final RemoteViews newView,
1043             final RemoteViews oldView) {
1044         return (newView == null && oldView == null) ||
1045                 (newView != null && oldView != null
1046                         && oldView.getPackage() != null
1047                         && newView.getPackage() != null
1048                         && newView.getPackage().equals(oldView.getPackage())
1049                         && newView.getLayoutId() == oldView.getLayoutId()
1050                         && !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED));
1051     }
1052 
1053     /**
1054      * Sets whether to perform inflation on the same thread as the caller. This method should only
1055      * be used in tests, not in production.
1056      */
1057     @VisibleForTesting
1058     public void setInflateSynchronously(boolean inflateSynchronously) {
1059         mInflateSynchronously = inflateSynchronously;
1060     }
1061 
1062     public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
1063             implements InflationCallback, InflationTask {
1064 
1065         private static final long IMG_PRELOAD_TIMEOUT_MS = 1000L;
1066         private final NotificationEntry mEntry;
1067         private final Context mContext;
1068         private final boolean mInflateSynchronously;
1069         private final boolean mIsMinimized;
1070         private final boolean mUsesIncreasedHeight;
1071         private final InflationCallback mCallback;
1072         private final boolean mUsesIncreasedHeadsUpHeight;
1073         private final @InflationFlag int mReInflateFlags;
1074         private final NotifRemoteViewCache mRemoteViewCache;
1075         private final Executor mInflationExecutor;
1076         private ExpandableNotificationRow mRow;
1077         private Exception mError;
1078         private RemoteViews.InteractionHandler mRemoteViewClickHandler;
1079         private CancellationSignal mCancellationSignal;
1080         private final ConversationNotificationProcessor mConversationProcessor;
1081         private final boolean mIsMediaInQS;
1082         private final SmartReplyStateInflater mSmartRepliesInflater;
1083         private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider;
1084         private final HeadsUpStyleProvider mHeadsUpStyleProvider;
1085         private final NotificationRowContentBinderLogger mLogger;
1086 
1087         private AsyncInflationTask(
1088                 Executor inflationExecutor,
1089                 boolean inflateSynchronously,
1090                 @InflationFlag int reInflateFlags,
1091                 NotifRemoteViewCache cache,
1092                 NotificationEntry entry,
1093                 ConversationNotificationProcessor conversationProcessor,
1094                 ExpandableNotificationRow row,
1095                 boolean isMinimized,
1096                 boolean usesIncreasedHeight,
1097                 boolean usesIncreasedHeadsUpHeight,
1098                 InflationCallback callback,
1099                 RemoteViews.InteractionHandler remoteViewClickHandler,
1100                 boolean isMediaFlagEnabled,
1101                 SmartReplyStateInflater smartRepliesInflater,
1102                 NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
1103                 HeadsUpStyleProvider headsUpStyleProvider,
1104                 NotificationRowContentBinderLogger logger) {
1105             mEntry = entry;
1106             mRow = row;
1107             mInflationExecutor = inflationExecutor;
1108             mInflateSynchronously = inflateSynchronously;
1109             mReInflateFlags = reInflateFlags;
1110             mRemoteViewCache = cache;
1111             mSmartRepliesInflater = smartRepliesInflater;
1112             mContext = mRow.getContext();
1113             mIsMinimized = isMinimized;
1114             mUsesIncreasedHeight = usesIncreasedHeight;
1115             mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
1116             mRemoteViewClickHandler = remoteViewClickHandler;
1117             mCallback = callback;
1118             mConversationProcessor = conversationProcessor;
1119             mIsMediaInQS = isMediaFlagEnabled;
1120             mNotifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider;
1121             mHeadsUpStyleProvider = headsUpStyleProvider;
1122             mLogger = logger;
1123             entry.setInflationTask(this);
1124         }
1125 
1126         @VisibleForTesting
1127         @InflationFlag
1128         public int getReInflateFlags() {
1129             return mReInflateFlags;
1130         }
1131 
1132         void updateApplicationInfo(StatusBarNotification sbn) {
1133             String packageName = sbn.getPackageName();
1134             int userId = UserHandle.getUserId(sbn.getUid());
1135             final ApplicationInfo appInfo;
1136             try {
1137                 // This method has an internal cache, so we don't need to add our own caching here.
1138                 appInfo = mContext.getPackageManager().getApplicationInfoAsUser(packageName,
1139                         PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
1140             } catch (PackageManager.NameNotFoundException e) {
1141                 return;
1142             }
1143             Notification.addFieldsFromContext(appInfo, sbn.getNotification());
1144         }
1145 
1146         @Override
1147         protected void onPreExecute() {
1148             Trace.beginAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this));
1149         }
1150 
1151         @Override
1152         protected InflationProgress doInBackground(Void... params) {
1153             return TraceUtils.trace("NotificationContentInflater.AsyncInflationTask#doInBackground",
1154                     () -> {
1155                         try {
1156                             return doInBackgroundInternal();
1157                         } catch (Exception e) {
1158                             mError = e;
1159                             mLogger.logAsyncTaskException(mEntry, "inflating", e);
1160                             return null;
1161                         }
1162                     });
1163         }
1164 
1165         private InflationProgress doInBackgroundInternal() {
1166             final StatusBarNotification sbn = mEntry.getSbn();
1167             // Ensure the ApplicationInfo is updated before a builder is recovered.
1168             updateApplicationInfo(sbn);
1169             final Notification.Builder recoveredBuilder = Notification.Builder.recoverBuilder(
1170                     mContext, sbn.getNotification());
1171 
1172             Context packageContext = sbn.getPackageContext(mContext);
1173             if (recoveredBuilder.usesTemplate()) {
1174                 // For all of our templates, we want it to be RTL
1175                 packageContext = new RtlEnabledContext(packageContext);
1176             }
1177             boolean isConversation = mEntry.getRanking().isConversation();
1178             Notification.MessagingStyle messagingStyle = null;
1179             if (isConversation) {
1180                 messagingStyle = mConversationProcessor.processNotification(
1181                         mEntry, recoveredBuilder, mLogger);
1182             }
1183             InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
1184                     recoveredBuilder, mIsMinimized, mUsesIncreasedHeight,
1185                     mUsesIncreasedHeadsUpHeight, packageContext, mRow,
1186                     mNotifLayoutInflaterFactoryProvider, mHeadsUpStyleProvider, mLogger);
1187 
1188             mLogger.logAsyncTaskProgress(mEntry,
1189                     "getting existing smart reply state (on wrong thread!)");
1190             InflatedSmartReplyState previousSmartReplyState =
1191                     mRow.getExistingSmartReplyState();
1192             mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views");
1193             InflationProgress result = inflateSmartReplyViews(
1194                     /* result = */ inflationProgress,
1195                     mReInflateFlags,
1196                     mEntry,
1197                     mContext,
1198                     packageContext,
1199                     previousSmartReplyState,
1200                     mSmartRepliesInflater,
1201                     mLogger);
1202 
1203             if (AsyncHybridViewInflation.isEnabled()) {
1204                 // Inflate the single-line content view's ViewModel and ViewHolder from the
1205                 // background thread, the ViewHolder needs to be bind with ViewModel later from
1206                 // the main thread.
1207                 result.mInflatedSingleLineViewModel = SingleLineViewInflater
1208                         .inflateSingleLineViewModel(
1209                                 mEntry.getSbn().getNotification(),
1210                                 messagingStyle,
1211                                 recoveredBuilder,
1212                                 mContext
1213                         );
1214                 result.mInflatedSingleLineViewHolder =
1215                         SingleLineViewInflater.inflateSingleLineViewHolder(
1216                                 isConversation,
1217                                 mReInflateFlags,
1218                                 mEntry,
1219                                 mContext,
1220                                 mLogger
1221                         );
1222             }
1223 
1224             mLogger.logAsyncTaskProgress(mEntry,
1225                     "getting row image resolver (on wrong thread!)");
1226             final NotificationInlineImageResolver imageResolver = mRow.getImageResolver();
1227             // wait for image resolver to finish preloading
1228             mLogger.logAsyncTaskProgress(mEntry, "waiting for preloaded images");
1229             imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS);
1230 
1231             return result;
1232         }
1233 
1234         @Override
1235         protected void onPostExecute(InflationProgress result) {
1236             Trace.endAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this));
1237 
1238             if (mError == null) {
1239                 // Logged in detail in apply.
1240                 mCancellationSignal = apply(
1241                         mInflationExecutor,
1242                         mInflateSynchronously,
1243                         mIsMinimized,
1244                         result,
1245                         mReInflateFlags,
1246                         mRemoteViewCache,
1247                         mEntry,
1248                         mRow,
1249                         mRemoteViewClickHandler,
1250                         this /* callback */,
1251                         mLogger);
1252             } else {
1253                 handleError(mError);
1254             }
1255         }
1256 
1257         @Override
1258         protected void onCancelled(InflationProgress result) {
1259             Trace.endAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this));
1260         }
1261 
1262         private void handleError(Exception e) {
1263             mEntry.onInflationTaskFinished();
1264             StatusBarNotification sbn = mEntry.getSbn();
1265             final String ident = sbn.getPackageName() + "/0x"
1266                     + Integer.toHexString(sbn.getId());
1267             Log.e(CentralSurfaces.TAG, "couldn't inflate view for notification " + ident, e);
1268             if (mCallback != null) {
1269                 mCallback.handleInflationException(mRow.getEntry(),
1270                         new InflationException("Couldn't inflate contentViews" + e));
1271             }
1272 
1273             // Cancel any image loading tasks, not useful any more
1274             mRow.getImageResolver().cancelRunningTasks();
1275         }
1276 
1277         @Override
1278         public void abort() {
1279             mLogger.logAsyncTaskProgress(mEntry, "cancelling inflate");
1280             cancel(true /* mayInterruptIfRunning */);
1281             if (mCancellationSignal != null) {
1282                 mLogger.logAsyncTaskProgress(mEntry, "cancelling apply");
1283                 mCancellationSignal.cancel();
1284             }
1285             mLogger.logAsyncTaskProgress(mEntry, "aborted");
1286         }
1287 
1288         @Override
1289         public void handleInflationException(NotificationEntry entry, Exception e) {
1290             handleError(e);
1291         }
1292 
1293         @Override
1294         public void onAsyncInflationFinished(NotificationEntry entry) {
1295             mEntry.onInflationTaskFinished();
1296             mRow.onNotificationUpdated();
1297             if (mCallback != null) {
1298                 mCallback.onAsyncInflationFinished(mEntry);
1299             }
1300 
1301             // Notify the resolver that the inflation task has finished,
1302             // try to purge unnecessary cached entries.
1303             mRow.getImageResolver().purgeCache();
1304 
1305             // Cancel any image loading tasks that have not completed at this point
1306             mRow.getImageResolver().cancelRunningTasks();
1307         }
1308 
1309         private static class RtlEnabledContext extends ContextWrapper {
1310             private RtlEnabledContext(Context packageContext) {
1311                 super(packageContext);
1312             }
1313 
1314             @Override
1315             public ApplicationInfo getApplicationInfo() {
1316                 ApplicationInfo applicationInfo = new ApplicationInfo(super.getApplicationInfo());
1317                 applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_RTL;
1318                 return applicationInfo;
1319             }
1320         }
1321     }
1322 
1323     @VisibleForTesting
1324     static class InflationProgress {
1325         private RemoteViews newContentView;
1326         private RemoteViews newHeadsUpView;
1327         private RemoteViews newExpandedView;
1328         private RemoteViews newPublicView;
1329         private RemoteViews mNewGroupHeaderView;
1330         private RemoteViews mNewMinimizedGroupHeaderView;
1331 
1332         @VisibleForTesting
1333         Context packageContext;
1334 
1335         private View inflatedContentView;
1336         private View inflatedHeadsUpView;
1337         private View inflatedExpandedView;
1338         private View inflatedPublicView;
1339         private NotificationHeaderView mInflatedGroupHeaderView;
1340         private NotificationHeaderView mInflatedMinimizedGroupHeaderView;
1341         private CharSequence headsUpStatusBarText;
1342         private CharSequence headsUpStatusBarTextPublic;
1343 
1344         private InflatedSmartReplyState inflatedSmartReplyState;
1345         private InflatedSmartReplyViewHolder expandedInflatedSmartReplies;
1346         private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies;
1347 
1348         // ViewModel for SingleLineView, holds the UI State
1349         SingleLineViewModel mInflatedSingleLineViewModel;
1350         // Inflated SingleLineViewHolder, SingleLineView that lacks the UI State
1351         HybridNotificationView mInflatedSingleLineViewHolder;
1352     }
1353 
1354     @VisibleForTesting
1355     abstract static class ApplyCallback {
1356         public abstract void setResultView(View v);
1357         public abstract RemoteViews getRemoteView();
1358     }
1359 
1360     private static final String ASYNC_TASK_TRACE_METHOD =
1361             "NotificationContentInflater.AsyncInflationTask";
1362     private static final String APPLY_TRACE_METHOD = "NotificationContentInflater#apply";
1363 }
1364