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