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