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;
18 
19 import android.annotation.Nullable;
20 import android.app.Notification;
21 import android.content.Context;
22 import android.os.AsyncTask;
23 import android.os.CancellationSignal;
24 import android.service.notification.StatusBarNotification;
25 import android.util.Log;
26 import android.view.View;
27 import android.widget.RemoteViews;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.systemui.R;
31 import com.android.systemui.statusbar.InflationTask;
32 import com.android.systemui.statusbar.ExpandableNotificationRow;
33 import com.android.systemui.statusbar.NotificationContentView;
34 import com.android.systemui.statusbar.NotificationData;
35 import com.android.systemui.statusbar.phone.StatusBar;
36 import com.android.systemui.util.Assert;
37 
38 import java.util.HashMap;
39 import java.util.concurrent.Executor;
40 import java.util.concurrent.LinkedBlockingQueue;
41 import java.util.concurrent.ThreadFactory;
42 import java.util.concurrent.ThreadPoolExecutor;
43 import java.util.concurrent.TimeUnit;
44 import java.util.concurrent.atomic.AtomicInteger;
45 
46 /**
47  * A utility that inflates the right kind of contentView based on the state
48  */
49 public class NotificationInflater {
50 
51     public static final String TAG = "NotificationInflater";
52     @VisibleForTesting
53     static final int FLAG_REINFLATE_ALL = ~0;
54     private static final int FLAG_REINFLATE_CONTENT_VIEW = 1<<0;
55     @VisibleForTesting
56     static final int FLAG_REINFLATE_EXPANDED_VIEW = 1<<1;
57     private static final int FLAG_REINFLATE_HEADS_UP_VIEW = 1<<2;
58     private static final int FLAG_REINFLATE_PUBLIC_VIEW = 1<<3;
59     private static final int FLAG_REINFLATE_AMBIENT_VIEW = 1<<4;
60     private static final InflationExecutor EXECUTOR = new InflationExecutor();
61 
62     private final ExpandableNotificationRow mRow;
63     private boolean mIsLowPriority;
64     private boolean mUsesIncreasedHeight;
65     private boolean mUsesIncreasedHeadsUpHeight;
66     private RemoteViews.OnClickHandler mRemoteViewClickHandler;
67     private boolean mIsChildInGroup;
68     private InflationCallback mCallback;
69     private boolean mRedactAmbient;
70 
NotificationInflater(ExpandableNotificationRow row)71     public NotificationInflater(ExpandableNotificationRow row) {
72         mRow = row;
73     }
74 
setIsLowPriority(boolean isLowPriority)75     public void setIsLowPriority(boolean isLowPriority) {
76         mIsLowPriority = isLowPriority;
77     }
78 
79     /**
80      * Set whether the notification is a child in a group
81      *
82      * @return whether the view was re-inflated
83      */
setIsChildInGroup(boolean childInGroup)84     public void setIsChildInGroup(boolean childInGroup) {
85         if (childInGroup != mIsChildInGroup) {
86             mIsChildInGroup = childInGroup;
87             if (mIsLowPriority) {
88                 int flags = FLAG_REINFLATE_CONTENT_VIEW | FLAG_REINFLATE_EXPANDED_VIEW;
89                 inflateNotificationViews(flags);
90             }
91         } ;
92     }
93 
setUsesIncreasedHeight(boolean usesIncreasedHeight)94     public void setUsesIncreasedHeight(boolean usesIncreasedHeight) {
95         mUsesIncreasedHeight = usesIncreasedHeight;
96     }
97 
setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight)98     public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) {
99         mUsesIncreasedHeadsUpHeight = usesIncreasedHeight;
100     }
101 
setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler)102     public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) {
103         mRemoteViewClickHandler = remoteViewClickHandler;
104     }
105 
setRedactAmbient(boolean redactAmbient)106     public void setRedactAmbient(boolean redactAmbient) {
107         if (mRedactAmbient != redactAmbient) {
108             mRedactAmbient = redactAmbient;
109             if (mRow.getEntry() == null) {
110                 return;
111             }
112             inflateNotificationViews(FLAG_REINFLATE_AMBIENT_VIEW);
113         }
114     }
115 
116     /**
117      * Inflate all views of this notification on a background thread. This is asynchronous and will
118      * notify the callback once it's finished.
119      */
inflateNotificationViews()120     public void inflateNotificationViews() {
121         inflateNotificationViews(FLAG_REINFLATE_ALL);
122     }
123 
124     /**
125      * Reinflate all views for the specified flags on a background thread. This is asynchronous and
126      * will notify the callback once it's finished.
127      *
128      * @param reInflateFlags flags which views should be reinflated. Use {@link #FLAG_REINFLATE_ALL}
129      *                       to reinflate all of views.
130      */
131     @VisibleForTesting
inflateNotificationViews(int reInflateFlags)132     void inflateNotificationViews(int reInflateFlags) {
133         if (mRow.isRemoved()) {
134             // We don't want to reinflate anything for removed notifications. Otherwise views might
135             // be readded to the stack, leading to leaks. This may happen with low-priority groups
136             // where the removal of already removed children can lead to a reinflation.
137             return;
138         }
139         StatusBarNotification sbn = mRow.getEntry().notification;
140         new AsyncInflationTask(sbn, reInflateFlags, mRow, mIsLowPriority,
141                 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
142                 mCallback, mRemoteViewClickHandler).execute();
143     }
144 
145     @VisibleForTesting
inflateNotificationViews(int reInflateFlags, Notification.Builder builder, Context packageContext)146     InflationProgress inflateNotificationViews(int reInflateFlags,
147             Notification.Builder builder, Context packageContext) {
148         InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority,
149                 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
150                 mRedactAmbient, packageContext);
151         apply(result, reInflateFlags, mRow, mRedactAmbient, mRemoteViewClickHandler, null);
152         return result;
153     }
154 
createRemoteViews(int reInflateFlags, Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, Context packageContext)155     private static InflationProgress createRemoteViews(int reInflateFlags,
156             Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup,
157             boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
158             Context packageContext) {
159         InflationProgress result = new InflationProgress();
160         isLowPriority = isLowPriority && !isChildInGroup;
161         if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
162             result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);
163         }
164 
165         if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
166             result.newExpandedView = createExpandedView(builder, isLowPriority);
167         }
168 
169         if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
170             result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);
171         }
172 
173         if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
174             result.newPublicView = builder.makePublicContentView();
175         }
176 
177         if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
178             result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification()
179                     : builder.makeAmbientNotification();
180         }
181         result.packageContext = packageContext;
182         return result;
183     }
184 
apply(InflationProgress result, int reInflateFlags, ExpandableNotificationRow row, boolean redactAmbient, RemoteViews.OnClickHandler remoteViewClickHandler, @Nullable InflationCallback callback)185     public static CancellationSignal apply(InflationProgress result, int reInflateFlags,
186             ExpandableNotificationRow row, boolean redactAmbient,
187             RemoteViews.OnClickHandler remoteViewClickHandler,
188             @Nullable InflationCallback callback) {
189         NotificationData.Entry entry = row.getEntry();
190         NotificationContentView privateLayout = row.getPrivateLayout();
191         NotificationContentView publicLayout = row.getPublicLayout();
192         final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
193 
194         int flag = FLAG_REINFLATE_CONTENT_VIEW;
195         if ((reInflateFlags & flag) != 0) {
196             boolean isNewView = !compareRemoteViews(result.newContentView, entry.cachedContentView);
197             ApplyCallback applyCallback = new ApplyCallback() {
198                 @Override
199                 public void setResultView(View v) {
200                     result.inflatedContentView = v;
201                 }
202 
203                 @Override
204                 public RemoteViews getRemoteView() {
205                     return result.newContentView;
206                 }
207             };
208             applyRemoteView(result, reInflateFlags, flag, row, redactAmbient,
209                     isNewView, remoteViewClickHandler, callback, entry, privateLayout,
210                     privateLayout.getContractedChild(), privateLayout.getVisibleWrapper(
211                             NotificationContentView.VISIBLE_TYPE_CONTRACTED),
212                     runningInflations, applyCallback);
213         }
214 
215         flag = FLAG_REINFLATE_EXPANDED_VIEW;
216         if ((reInflateFlags & flag) != 0) {
217             if (result.newExpandedView != null) {
218                 boolean isNewView = !compareRemoteViews(result.newExpandedView,
219                         entry.cachedBigContentView);
220                 ApplyCallback applyCallback = new ApplyCallback() {
221                     @Override
222                     public void setResultView(View v) {
223                         result.inflatedExpandedView = v;
224                     }
225 
226                     @Override
227                     public RemoteViews getRemoteView() {
228                         return result.newExpandedView;
229                     }
230                 };
231                 applyRemoteView(result, reInflateFlags, flag, row,
232                         redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
233                         privateLayout, privateLayout.getExpandedChild(),
234                         privateLayout.getVisibleWrapper(
235                                 NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations,
236                         applyCallback);
237             }
238         }
239 
240         flag = FLAG_REINFLATE_HEADS_UP_VIEW;
241         if ((reInflateFlags & flag) != 0) {
242             if (result.newHeadsUpView != null) {
243                 boolean isNewView = !compareRemoteViews(result.newHeadsUpView,
244                         entry.cachedHeadsUpContentView);
245                 ApplyCallback applyCallback = new ApplyCallback() {
246                     @Override
247                     public void setResultView(View v) {
248                         result.inflatedHeadsUpView = v;
249                     }
250 
251                     @Override
252                     public RemoteViews getRemoteView() {
253                         return result.newHeadsUpView;
254                     }
255                 };
256                 applyRemoteView(result, reInflateFlags, flag, row,
257                         redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
258                         privateLayout, privateLayout.getHeadsUpChild(),
259                         privateLayout.getVisibleWrapper(
260                                 NotificationContentView.VISIBLE_TYPE_HEADSUP), runningInflations,
261                         applyCallback);
262             }
263         }
264 
265         flag = FLAG_REINFLATE_PUBLIC_VIEW;
266         if ((reInflateFlags & flag) != 0) {
267             boolean isNewView = !compareRemoteViews(result.newPublicView,
268                     entry.cachedPublicContentView);
269             ApplyCallback applyCallback = new ApplyCallback() {
270                 @Override
271                 public void setResultView(View v) {
272                     result.inflatedPublicView = v;
273                 }
274 
275                 @Override
276                 public RemoteViews getRemoteView() {
277                     return result.newPublicView;
278                 }
279             };
280             applyRemoteView(result, reInflateFlags, flag, row,
281                     redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
282                     publicLayout, publicLayout.getContractedChild(),
283                     publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),
284                     runningInflations, applyCallback);
285         }
286 
287         flag = FLAG_REINFLATE_AMBIENT_VIEW;
288         if ((reInflateFlags & flag) != 0) {
289             NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout;
290             boolean isNewView = !canReapplyAmbient(row, redactAmbient) ||
291                     !compareRemoteViews(result.newAmbientView, entry.cachedAmbientContentView);
292             ApplyCallback applyCallback = new ApplyCallback() {
293                 @Override
294                 public void setResultView(View v) {
295                     result.inflatedAmbientView = v;
296                 }
297 
298                 @Override
299                 public RemoteViews getRemoteView() {
300                     return result.newAmbientView;
301                 }
302             };
303             applyRemoteView(result, reInflateFlags, flag, row,
304                     redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
305                     newParent, newParent.getAmbientChild(), newParent.getVisibleWrapper(
306                             NotificationContentView.VISIBLE_TYPE_AMBIENT), runningInflations,
307                     applyCallback);
308         }
309 
310         // Let's try to finish, maybe nobody is even inflating anything
311         finishIfDone(result, reInflateFlags, runningInflations, callback, row,
312                 redactAmbient);
313         CancellationSignal cancellationSignal = new CancellationSignal();
314         cancellationSignal.setOnCancelListener(
315                 () -> runningInflations.values().forEach(CancellationSignal::cancel));
316         return cancellationSignal;
317     }
318 
319     @VisibleForTesting
applyRemoteView(final InflationProgress result, final int reInflateFlags, int inflationId, final ExpandableNotificationRow row, final boolean redactAmbient, boolean isNewView, RemoteViews.OnClickHandler remoteViewClickHandler, @Nullable final InflationCallback callback, NotificationData.Entry entry, NotificationContentView parentLayout, View existingView, NotificationViewWrapper existingWrapper, final HashMap<Integer, CancellationSignal> runningInflations, ApplyCallback applyCallback)320     static void applyRemoteView(final InflationProgress result,
321             final int reInflateFlags, int inflationId,
322             final ExpandableNotificationRow row,
323             final boolean redactAmbient, boolean isNewView,
324             RemoteViews.OnClickHandler remoteViewClickHandler,
325             @Nullable final InflationCallback callback, NotificationData.Entry entry,
326             NotificationContentView parentLayout, View existingView,
327             NotificationViewWrapper existingWrapper,
328             final HashMap<Integer, CancellationSignal> runningInflations,
329             ApplyCallback applyCallback) {
330         RemoteViews newContentView = applyCallback.getRemoteView();
331         RemoteViews.OnViewAppliedListener listener
332                 = new RemoteViews.OnViewAppliedListener() {
333 
334             @Override
335             public void onViewApplied(View v) {
336                 if (isNewView) {
337                     v.setIsRootNamespace(true);
338                     applyCallback.setResultView(v);
339                 } else if (existingWrapper != null) {
340                     existingWrapper.onReinflated();
341                 }
342                 runningInflations.remove(inflationId);
343                 finishIfDone(result, reInflateFlags, runningInflations, callback, row,
344                         redactAmbient);
345             }
346 
347             @Override
348             public void onError(Exception e) {
349                 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could
350                 // actually also be a system issue, so let's try on the UI thread again to be safe.
351                 try {
352                     View newView = existingView;
353                     if (isNewView) {
354                         newView = newContentView.apply(
355                                 result.packageContext,
356                                 parentLayout,
357                                 remoteViewClickHandler);
358                     } else {
359                         newContentView.reapply(
360                                 result.packageContext,
361                                 existingView,
362                                 remoteViewClickHandler);
363                     }
364                     Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.",
365                             e);
366                     onViewApplied(newView);
367                 } catch (Exception anotherException) {
368                     runningInflations.remove(inflationId);
369                     handleInflationError(runningInflations, e, entry.notification, callback);
370                 }
371             }
372         };
373         CancellationSignal cancellationSignal;
374         if (isNewView) {
375             cancellationSignal = newContentView.applyAsync(
376                     result.packageContext,
377                     parentLayout,
378                     EXECUTOR,
379                     listener,
380                     remoteViewClickHandler);
381         } else {
382             cancellationSignal = newContentView.reapplyAsync(
383                     result.packageContext,
384                     existingView,
385                     EXECUTOR,
386                     listener,
387                     remoteViewClickHandler);
388         }
389         runningInflations.put(inflationId, cancellationSignal);
390     }
391 
handleInflationError(HashMap<Integer, CancellationSignal> runningInflations, Exception e, StatusBarNotification notification, @Nullable InflationCallback callback)392     private static void handleInflationError(HashMap<Integer, CancellationSignal> runningInflations,
393             Exception e, StatusBarNotification notification, @Nullable InflationCallback callback) {
394         Assert.isMainThread();
395         runningInflations.values().forEach(CancellationSignal::cancel);
396         if (callback != null) {
397             callback.handleInflationException(notification, e);
398         }
399     }
400 
401     /**
402      * Finish the inflation of the views
403      *
404      * @return true if the inflation was finished
405      */
finishIfDone(InflationProgress result, int reInflateFlags, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, ExpandableNotificationRow row, boolean redactAmbient)406     private static boolean finishIfDone(InflationProgress result, int reInflateFlags,
407             HashMap<Integer, CancellationSignal> runningInflations,
408             @Nullable InflationCallback endListener, ExpandableNotificationRow row,
409             boolean redactAmbient) {
410         Assert.isMainThread();
411         NotificationData.Entry entry = row.getEntry();
412         NotificationContentView privateLayout = row.getPrivateLayout();
413         NotificationContentView publicLayout = row.getPublicLayout();
414         if (runningInflations.isEmpty()) {
415             if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
416                 if (result.inflatedContentView != null) {
417                     privateLayout.setContractedChild(result.inflatedContentView);
418                 }
419                 entry.cachedContentView = result.newContentView;
420             }
421 
422             if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
423                 if (result.inflatedExpandedView != null) {
424                     privateLayout.setExpandedChild(result.inflatedExpandedView);
425                 } else if (result.newExpandedView == null) {
426                     privateLayout.setExpandedChild(null);
427                 }
428                 entry.cachedBigContentView = result.newExpandedView;
429                 row.setExpandable(result.newExpandedView != null);
430             }
431 
432             if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
433                 if (result.inflatedHeadsUpView != null) {
434                     privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
435                 } else if (result.newHeadsUpView == null) {
436                     privateLayout.setHeadsUpChild(null);
437                 }
438                 entry.cachedHeadsUpContentView = result.newHeadsUpView;
439             }
440 
441             if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
442                 if (result.inflatedPublicView != null) {
443                     publicLayout.setContractedChild(result.inflatedPublicView);
444                 }
445                 entry.cachedPublicContentView = result.newPublicView;
446             }
447 
448             if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
449                 if (result.inflatedAmbientView != null) {
450                     NotificationContentView newParent = redactAmbient
451                             ? publicLayout : privateLayout;
452                     NotificationContentView otherParent = !redactAmbient
453                             ? publicLayout : privateLayout;
454                     newParent.setAmbientChild(result.inflatedAmbientView);
455                     otherParent.setAmbientChild(null);
456                 }
457                 entry.cachedAmbientContentView = result.newAmbientView;
458             }
459             if (endListener != null) {
460                 endListener.onAsyncInflationFinished(row.getEntry());
461             }
462             return true;
463         }
464         return false;
465     }
466 
createExpandedView(Notification.Builder builder, boolean isLowPriority)467     private static RemoteViews createExpandedView(Notification.Builder builder,
468             boolean isLowPriority) {
469         RemoteViews bigContentView = builder.createBigContentView();
470         if (bigContentView != null) {
471             return bigContentView;
472         }
473         if (isLowPriority) {
474             RemoteViews contentView = builder.createContentView();
475             Notification.Builder.makeHeaderExpanded(contentView);
476             return contentView;
477         }
478         return null;
479     }
480 
createContentView(Notification.Builder builder, boolean isLowPriority, boolean useLarge)481     private static RemoteViews createContentView(Notification.Builder builder,
482             boolean isLowPriority, boolean useLarge) {
483         if (isLowPriority) {
484             return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
485         }
486         return builder.createContentView(useLarge);
487     }
488 
489     // Returns true if the RemoteViews are the same.
compareRemoteViews(final RemoteViews a, final RemoteViews b)490     private static boolean compareRemoteViews(final RemoteViews a, final RemoteViews b) {
491         return (a == null && b == null) ||
492                 (a != null && b != null
493                         && b.getPackage() != null
494                         && a.getPackage() != null
495                         && a.getPackage().equals(b.getPackage())
496                         && a.getLayoutId() == b.getLayoutId());
497     }
498 
setInflationCallback(InflationCallback callback)499     public void setInflationCallback(InflationCallback callback) {
500         mCallback = callback;
501     }
502 
503     public interface InflationCallback {
handleInflationException(StatusBarNotification notification, Exception e)504         void handleInflationException(StatusBarNotification notification, Exception e);
onAsyncInflationFinished(NotificationData.Entry entry)505         void onAsyncInflationFinished(NotificationData.Entry entry);
506     }
507 
onDensityOrFontScaleChanged()508     public void onDensityOrFontScaleChanged() {
509         NotificationData.Entry entry = mRow.getEntry();
510         entry.cachedAmbientContentView = null;
511         entry.cachedBigContentView = null;
512         entry.cachedContentView = null;
513         entry.cachedHeadsUpContentView = null;
514         entry.cachedPublicContentView = null;
515         inflateNotificationViews();
516     }
517 
canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient)518     private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) {
519         NotificationContentView ambientView = redactAmbient ? row.getPublicLayout()
520                 : row.getPrivateLayout();            ;
521         return ambientView.getAmbientChild() != null;
522     }
523 
524     public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
525             implements InflationCallback, InflationTask {
526 
527         private final StatusBarNotification mSbn;
528         private final Context mContext;
529         private final boolean mIsLowPriority;
530         private final boolean mIsChildInGroup;
531         private final boolean mUsesIncreasedHeight;
532         private final InflationCallback mCallback;
533         private final boolean mUsesIncreasedHeadsUpHeight;
534         private final boolean mRedactAmbient;
535         private int mReInflateFlags;
536         private ExpandableNotificationRow mRow;
537         private Exception mError;
538         private RemoteViews.OnClickHandler mRemoteViewClickHandler;
539         private CancellationSignal mCancellationSignal;
540 
AsyncInflationTask(StatusBarNotification notification, int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, InflationCallback callback, RemoteViews.OnClickHandler remoteViewClickHandler)541         private AsyncInflationTask(StatusBarNotification notification,
542                 int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority,
543                 boolean isChildInGroup, boolean usesIncreasedHeight,
544                 boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
545                 InflationCallback callback,
546                 RemoteViews.OnClickHandler remoteViewClickHandler) {
547             mRow = row;
548             mSbn = notification;
549             mReInflateFlags = reInflateFlags;
550             mContext = mRow.getContext();
551             mIsLowPriority = isLowPriority;
552             mIsChildInGroup = isChildInGroup;
553             mUsesIncreasedHeight = usesIncreasedHeight;
554             mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
555             mRedactAmbient = redactAmbient;
556             mRemoteViewClickHandler = remoteViewClickHandler;
557             mCallback = callback;
558             NotificationData.Entry entry = row.getEntry();
559             entry.setInflationTask(this);
560         }
561 
562         @VisibleForTesting
getReInflateFlags()563         public int getReInflateFlags() {
564             return mReInflateFlags;
565         }
566 
567         @Override
doInBackground(Void... params)568         protected InflationProgress doInBackground(Void... params) {
569             try {
570                 final Notification.Builder recoveredBuilder
571                         = Notification.Builder.recoverBuilder(mContext,
572                         mSbn.getNotification());
573                 Context packageContext = mSbn.getPackageContext(mContext);
574                 Notification notification = mSbn.getNotification();
575                 if (mIsLowPriority) {
576                     int backgroundColor = mContext.getColor(
577                             R.color.notification_material_background_low_priority_color);
578                     recoveredBuilder.setBackgroundColorHint(backgroundColor);
579                 }
580                 if (notification.isMediaNotification()) {
581                     MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext,
582                             packageContext);
583                     processor.setIsLowPriority(mIsLowPriority);
584                     processor.processNotification(notification, recoveredBuilder);
585                 }
586                 return createRemoteViews(mReInflateFlags,
587                         recoveredBuilder, mIsLowPriority, mIsChildInGroup,
588                         mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
589                         packageContext);
590             } catch (Exception e) {
591                 mError = e;
592                 return null;
593             }
594         }
595 
596         @Override
onPostExecute(InflationProgress result)597         protected void onPostExecute(InflationProgress result) {
598             if (mError == null) {
599                 mCancellationSignal = apply(result, mReInflateFlags, mRow, mRedactAmbient,
600                         mRemoteViewClickHandler, this);
601             } else {
602                 handleError(mError);
603             }
604         }
605 
handleError(Exception e)606         private void handleError(Exception e) {
607             mRow.getEntry().onInflationTaskFinished();
608             StatusBarNotification sbn = mRow.getStatusBarNotification();
609             final String ident = sbn.getPackageName() + "/0x"
610                     + Integer.toHexString(sbn.getId());
611             Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e);
612             mCallback.handleInflationException(sbn,
613                     new InflationException("Couldn't inflate contentViews" + e));
614         }
615 
616         @Override
abort()617         public void abort() {
618             cancel(true /* mayInterruptIfRunning */);
619             if (mCancellationSignal != null) {
620                 mCancellationSignal.cancel();
621             }
622         }
623 
624         @Override
supersedeTask(InflationTask task)625         public void supersedeTask(InflationTask task) {
626             if (task instanceof AsyncInflationTask) {
627                 // We want to inflate all flags of the previous task as well
628                 mReInflateFlags |= ((AsyncInflationTask) task).mReInflateFlags;
629             }
630         }
631 
632         @Override
handleInflationException(StatusBarNotification notification, Exception e)633         public void handleInflationException(StatusBarNotification notification, Exception e) {
634             handleError(e);
635         }
636 
637         @Override
onAsyncInflationFinished(NotificationData.Entry entry)638         public void onAsyncInflationFinished(NotificationData.Entry entry) {
639             mRow.getEntry().onInflationTaskFinished();
640             mRow.onNotificationUpdated();
641             mCallback.onAsyncInflationFinished(mRow.getEntry());
642         }
643     }
644 
645     @VisibleForTesting
646     static class InflationProgress {
647         private RemoteViews newContentView;
648         private RemoteViews newHeadsUpView;
649         private RemoteViews newExpandedView;
650         private RemoteViews newAmbientView;
651         private RemoteViews newPublicView;
652 
653         @VisibleForTesting
654         Context packageContext;
655 
656         private View inflatedContentView;
657         private View inflatedHeadsUpView;
658         private View inflatedExpandedView;
659         private View inflatedAmbientView;
660         private View inflatedPublicView;
661     }
662 
663     @VisibleForTesting
664     abstract static class ApplyCallback {
setResultView(View v)665         public abstract void setResultView(View v);
getRemoteView()666         public abstract RemoteViews getRemoteView();
667     }
668 
669     /**
670      * A custom executor that allows more tasks to be queued. Default values are copied from
671      * AsyncTask
672       */
673     private static class InflationExecutor implements Executor {
674         private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
675         // We want at least 2 threads and at most 4 threads in the core pool,
676         // preferring to have 1 less than the CPU count to avoid saturating
677         // the CPU with background work
678         private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
679         private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
680         private static final int KEEP_ALIVE_SECONDS = 30;
681 
682         private static final ThreadFactory sThreadFactory = new ThreadFactory() {
683             private final AtomicInteger mCount = new AtomicInteger(1);
684 
685             public Thread newThread(Runnable r) {
686                 return new Thread(r, "InflaterThread #" + mCount.getAndIncrement());
687             }
688         };
689 
690         private final ThreadPoolExecutor mExecutor;
691 
InflationExecutor()692         private InflationExecutor() {
693             mExecutor = new ThreadPoolExecutor(
694                     CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
695                     new LinkedBlockingQueue<>(), sThreadFactory);
696             mExecutor.allowCoreThreadTimeOut(true);
697         }
698 
699         @Override
execute(Runnable runnable)700         public void execute(Runnable runnable) {
701             mExecutor.execute(runnable);
702         }
703     }
704 }
705