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         AsyncInflationTask task = new AsyncInflationTask(sbn, reInflateFlags, mRow,
141                 mIsLowPriority,
142                 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
143                 mCallback, mRemoteViewClickHandler);
144         if (mCallback != null && mCallback.doInflateSynchronous()) {
145             task.onPostExecute(task.doInBackground());
146         } else {
147             task.execute();
148         }
149     }
150 
151     @VisibleForTesting
inflateNotificationViews(int reInflateFlags, Notification.Builder builder, Context packageContext)152     InflationProgress inflateNotificationViews(int reInflateFlags,
153             Notification.Builder builder, Context packageContext) {
154         InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority,
155                 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
156                 mRedactAmbient, packageContext);
157         apply(result, reInflateFlags, mRow, mRedactAmbient, mRemoteViewClickHandler, null);
158         return result;
159     }
160 
createRemoteViews(int reInflateFlags, Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, Context packageContext)161     private static InflationProgress createRemoteViews(int reInflateFlags,
162             Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup,
163             boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
164             Context packageContext) {
165         InflationProgress result = new InflationProgress();
166         isLowPriority = isLowPriority && !isChildInGroup;
167         if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
168             result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);
169         }
170 
171         if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
172             result.newExpandedView = createExpandedView(builder, isLowPriority);
173         }
174 
175         if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
176             result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);
177         }
178 
179         if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
180             result.newPublicView = builder.makePublicContentView();
181         }
182 
183         if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
184             result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification()
185                     : builder.makeAmbientNotification();
186         }
187         result.packageContext = packageContext;
188         result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */);
189         result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
190                 true /* showingPublic */);
191         return result;
192     }
193 
apply(InflationProgress result, int reInflateFlags, ExpandableNotificationRow row, boolean redactAmbient, RemoteViews.OnClickHandler remoteViewClickHandler, @Nullable InflationCallback callback)194     public static CancellationSignal apply(InflationProgress result, int reInflateFlags,
195             ExpandableNotificationRow row, boolean redactAmbient,
196             RemoteViews.OnClickHandler remoteViewClickHandler,
197             @Nullable InflationCallback callback) {
198         NotificationData.Entry entry = row.getEntry();
199         NotificationContentView privateLayout = row.getPrivateLayout();
200         NotificationContentView publicLayout = row.getPublicLayout();
201         final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
202 
203         int flag = FLAG_REINFLATE_CONTENT_VIEW;
204         if ((reInflateFlags & flag) != 0) {
205             boolean isNewView = !canReapplyRemoteView(result.newContentView, entry.cachedContentView);
206             ApplyCallback applyCallback = new ApplyCallback() {
207                 @Override
208                 public void setResultView(View v) {
209                     result.inflatedContentView = v;
210                 }
211 
212                 @Override
213                 public RemoteViews getRemoteView() {
214                     return result.newContentView;
215                 }
216             };
217             applyRemoteView(result, reInflateFlags, flag, row, redactAmbient,
218                     isNewView, remoteViewClickHandler, callback, entry, privateLayout,
219                     privateLayout.getContractedChild(), privateLayout.getVisibleWrapper(
220                             NotificationContentView.VISIBLE_TYPE_CONTRACTED),
221                     runningInflations, applyCallback);
222         }
223 
224         flag = FLAG_REINFLATE_EXPANDED_VIEW;
225         if ((reInflateFlags & flag) != 0) {
226             if (result.newExpandedView != null) {
227                 boolean isNewView = !canReapplyRemoteView(result.newExpandedView,
228                         entry.cachedBigContentView);
229                 ApplyCallback applyCallback = new ApplyCallback() {
230                     @Override
231                     public void setResultView(View v) {
232                         result.inflatedExpandedView = v;
233                     }
234 
235                     @Override
236                     public RemoteViews getRemoteView() {
237                         return result.newExpandedView;
238                     }
239                 };
240                 applyRemoteView(result, reInflateFlags, flag, row,
241                         redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
242                         privateLayout, privateLayout.getExpandedChild(),
243                         privateLayout.getVisibleWrapper(
244                                 NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations,
245                         applyCallback);
246             }
247         }
248 
249         flag = FLAG_REINFLATE_HEADS_UP_VIEW;
250         if ((reInflateFlags & flag) != 0) {
251             if (result.newHeadsUpView != null) {
252                 boolean isNewView = !canReapplyRemoteView(result.newHeadsUpView,
253                         entry.cachedHeadsUpContentView);
254                 ApplyCallback applyCallback = new ApplyCallback() {
255                     @Override
256                     public void setResultView(View v) {
257                         result.inflatedHeadsUpView = v;
258                     }
259 
260                     @Override
261                     public RemoteViews getRemoteView() {
262                         return result.newHeadsUpView;
263                     }
264                 };
265                 applyRemoteView(result, reInflateFlags, flag, row,
266                         redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
267                         privateLayout, privateLayout.getHeadsUpChild(),
268                         privateLayout.getVisibleWrapper(
269                                 NotificationContentView.VISIBLE_TYPE_HEADSUP), runningInflations,
270                         applyCallback);
271             }
272         }
273 
274         flag = FLAG_REINFLATE_PUBLIC_VIEW;
275         if ((reInflateFlags & flag) != 0) {
276             boolean isNewView = !canReapplyRemoteView(result.newPublicView,
277                     entry.cachedPublicContentView);
278             ApplyCallback applyCallback = new ApplyCallback() {
279                 @Override
280                 public void setResultView(View v) {
281                     result.inflatedPublicView = v;
282                 }
283 
284                 @Override
285                 public RemoteViews getRemoteView() {
286                     return result.newPublicView;
287                 }
288             };
289             applyRemoteView(result, reInflateFlags, flag, row,
290                     redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
291                     publicLayout, publicLayout.getContractedChild(),
292                     publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),
293                     runningInflations, applyCallback);
294         }
295 
296         flag = FLAG_REINFLATE_AMBIENT_VIEW;
297         if ((reInflateFlags & flag) != 0) {
298             NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout;
299             boolean isNewView = !canReapplyAmbient(row, redactAmbient) ||
300                     !canReapplyRemoteView(result.newAmbientView, entry.cachedAmbientContentView);
301             ApplyCallback applyCallback = new ApplyCallback() {
302                 @Override
303                 public void setResultView(View v) {
304                     result.inflatedAmbientView = v;
305                 }
306 
307                 @Override
308                 public RemoteViews getRemoteView() {
309                     return result.newAmbientView;
310                 }
311             };
312             applyRemoteView(result, reInflateFlags, flag, row,
313                     redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
314                     newParent, newParent.getAmbientChild(), newParent.getVisibleWrapper(
315                             NotificationContentView.VISIBLE_TYPE_AMBIENT), runningInflations,
316                     applyCallback);
317         }
318 
319         // Let's try to finish, maybe nobody is even inflating anything
320         finishIfDone(result, reInflateFlags, runningInflations, callback, row,
321                 redactAmbient);
322         CancellationSignal cancellationSignal = new CancellationSignal();
323         cancellationSignal.setOnCancelListener(
324                 () -> runningInflations.values().forEach(CancellationSignal::cancel));
325         return cancellationSignal;
326     }
327 
328     @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)329     static void applyRemoteView(final InflationProgress result,
330             final int reInflateFlags, int inflationId,
331             final ExpandableNotificationRow row,
332             final boolean redactAmbient, boolean isNewView,
333             RemoteViews.OnClickHandler remoteViewClickHandler,
334             @Nullable final InflationCallback callback, NotificationData.Entry entry,
335             NotificationContentView parentLayout, View existingView,
336             NotificationViewWrapper existingWrapper,
337             final HashMap<Integer, CancellationSignal> runningInflations,
338             ApplyCallback applyCallback) {
339         RemoteViews newContentView = applyCallback.getRemoteView();
340         if (callback != null && callback.doInflateSynchronous()) {
341             try {
342                 if (isNewView) {
343                     View v = newContentView.apply(
344                             result.packageContext,
345                             parentLayout,
346                             remoteViewClickHandler);
347                     v.setIsRootNamespace(true);
348                     applyCallback.setResultView(v);
349                 } else {
350                     newContentView.reapply(
351                             result.packageContext,
352                             existingView,
353                             remoteViewClickHandler);
354                     existingWrapper.onReinflated();
355                 }
356             } catch (Exception e) {
357                 handleInflationError(runningInflations, e, entry.notification, callback);
358                 // Add a running inflation to make sure we don't trigger callbacks.
359                 // Safe to do because only happens in tests.
360                 runningInflations.put(inflationId, new CancellationSignal());
361             }
362             return;
363         }
364         RemoteViews.OnViewAppliedListener listener
365                 = new RemoteViews.OnViewAppliedListener() {
366 
367             @Override
368             public void onViewApplied(View v) {
369                 if (isNewView) {
370                     v.setIsRootNamespace(true);
371                     applyCallback.setResultView(v);
372                 } else if (existingWrapper != null) {
373                     existingWrapper.onReinflated();
374                 }
375                 runningInflations.remove(inflationId);
376                 finishIfDone(result, reInflateFlags, runningInflations, callback, row,
377                         redactAmbient);
378             }
379 
380             @Override
381             public void onError(Exception e) {
382                 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could
383                 // actually also be a system issue, so let's try on the UI thread again to be safe.
384                 try {
385                     View newView = existingView;
386                     if (isNewView) {
387                         newView = newContentView.apply(
388                                 result.packageContext,
389                                 parentLayout,
390                                 remoteViewClickHandler);
391                     } else {
392                         newContentView.reapply(
393                                 result.packageContext,
394                                 existingView,
395                                 remoteViewClickHandler);
396                     }
397                     Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.",
398                             e);
399                     onViewApplied(newView);
400                 } catch (Exception anotherException) {
401                     runningInflations.remove(inflationId);
402                     handleInflationError(runningInflations, e, entry.notification, callback);
403                 }
404             }
405         };
406         CancellationSignal cancellationSignal;
407         if (isNewView) {
408             cancellationSignal = newContentView.applyAsync(
409                     result.packageContext,
410                     parentLayout,
411                     EXECUTOR,
412                     listener,
413                     remoteViewClickHandler);
414         } else {
415             cancellationSignal = newContentView.reapplyAsync(
416                     result.packageContext,
417                     existingView,
418                     EXECUTOR,
419                     listener,
420                     remoteViewClickHandler);
421         }
422         runningInflations.put(inflationId, cancellationSignal);
423     }
424 
handleInflationError(HashMap<Integer, CancellationSignal> runningInflations, Exception e, StatusBarNotification notification, @Nullable InflationCallback callback)425     private static void handleInflationError(HashMap<Integer, CancellationSignal> runningInflations,
426             Exception e, StatusBarNotification notification, @Nullable InflationCallback callback) {
427         Assert.isMainThread();
428         runningInflations.values().forEach(CancellationSignal::cancel);
429         if (callback != null) {
430             callback.handleInflationException(notification, e);
431         }
432     }
433 
434     /**
435      * Finish the inflation of the views
436      *
437      * @return true if the inflation was finished
438      */
finishIfDone(InflationProgress result, int reInflateFlags, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, ExpandableNotificationRow row, boolean redactAmbient)439     private static boolean finishIfDone(InflationProgress result, int reInflateFlags,
440             HashMap<Integer, CancellationSignal> runningInflations,
441             @Nullable InflationCallback endListener, ExpandableNotificationRow row,
442             boolean redactAmbient) {
443         Assert.isMainThread();
444         NotificationData.Entry entry = row.getEntry();
445         NotificationContentView privateLayout = row.getPrivateLayout();
446         NotificationContentView publicLayout = row.getPublicLayout();
447         if (runningInflations.isEmpty()) {
448             if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
449                 if (result.inflatedContentView != null) {
450                     privateLayout.setContractedChild(result.inflatedContentView);
451                 }
452                 entry.cachedContentView = result.newContentView;
453             }
454 
455             if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
456                 if (result.inflatedExpandedView != null) {
457                     privateLayout.setExpandedChild(result.inflatedExpandedView);
458                 } else if (result.newExpandedView == null) {
459                     privateLayout.setExpandedChild(null);
460                 }
461                 entry.cachedBigContentView = result.newExpandedView;
462                 row.setExpandable(result.newExpandedView != null);
463             }
464 
465             if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
466                 if (result.inflatedHeadsUpView != null) {
467                     privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
468                 } else if (result.newHeadsUpView == null) {
469                     privateLayout.setHeadsUpChild(null);
470                 }
471                 entry.cachedHeadsUpContentView = result.newHeadsUpView;
472             }
473 
474             if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
475                 if (result.inflatedPublicView != null) {
476                     publicLayout.setContractedChild(result.inflatedPublicView);
477                 }
478                 entry.cachedPublicContentView = result.newPublicView;
479             }
480 
481             if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
482                 if (result.inflatedAmbientView != null) {
483                     NotificationContentView newParent = redactAmbient
484                             ? publicLayout : privateLayout;
485                     NotificationContentView otherParent = !redactAmbient
486                             ? publicLayout : privateLayout;
487                     newParent.setAmbientChild(result.inflatedAmbientView);
488                     otherParent.setAmbientChild(null);
489                 }
490                 entry.cachedAmbientContentView = result.newAmbientView;
491             }
492             entry.headsUpStatusBarText = result.headsUpStatusBarText;
493             entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic;
494             if (endListener != null) {
495                 endListener.onAsyncInflationFinished(row.getEntry());
496             }
497             return true;
498         }
499         return false;
500     }
501 
createExpandedView(Notification.Builder builder, boolean isLowPriority)502     private static RemoteViews createExpandedView(Notification.Builder builder,
503             boolean isLowPriority) {
504         RemoteViews bigContentView = builder.createBigContentView();
505         if (bigContentView != null) {
506             return bigContentView;
507         }
508         if (isLowPriority) {
509             RemoteViews contentView = builder.createContentView();
510             Notification.Builder.makeHeaderExpanded(contentView);
511             return contentView;
512         }
513         return null;
514     }
515 
createContentView(Notification.Builder builder, boolean isLowPriority, boolean useLarge)516     private static RemoteViews createContentView(Notification.Builder builder,
517             boolean isLowPriority, boolean useLarge) {
518         if (isLowPriority) {
519             return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
520         }
521         return builder.createContentView(useLarge);
522     }
523 
524     /**
525      * @param newView The new view that will be applied
526      * @param oldView The old view that was applied to the existing view before
527      * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply.
528      */
529      @VisibleForTesting
canReapplyRemoteView(final RemoteViews newView, final RemoteViews oldView)530      static boolean canReapplyRemoteView(final RemoteViews newView,
531             final RemoteViews oldView) {
532         return (newView == null && oldView == null) ||
533                 (newView != null && oldView != null
534                         && oldView.getPackage() != null
535                         && newView.getPackage() != null
536                         && newView.getPackage().equals(oldView.getPackage())
537                         && newView.getLayoutId() == oldView.getLayoutId()
538                         && !oldView.isReapplyDisallowed());
539     }
540 
setInflationCallback(InflationCallback callback)541     public void setInflationCallback(InflationCallback callback) {
542         mCallback = callback;
543     }
544 
545     public interface InflationCallback {
handleInflationException(StatusBarNotification notification, Exception e)546         void handleInflationException(StatusBarNotification notification, Exception e);
onAsyncInflationFinished(NotificationData.Entry entry)547         void onAsyncInflationFinished(NotificationData.Entry entry);
548 
549         /**
550          * Used to disable async-ness for tests. Should only be used for tests.
551          */
doInflateSynchronous()552         default boolean doInflateSynchronous() {
553             return false;
554         }
555     }
556 
onDensityOrFontScaleChanged()557     public void onDensityOrFontScaleChanged() {
558         NotificationData.Entry entry = mRow.getEntry();
559         entry.cachedAmbientContentView = null;
560         entry.cachedBigContentView = null;
561         entry.cachedContentView = null;
562         entry.cachedHeadsUpContentView = null;
563         entry.cachedPublicContentView = null;
564         inflateNotificationViews();
565     }
566 
canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient)567     private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) {
568         NotificationContentView ambientView = redactAmbient ? row.getPublicLayout()
569                 : row.getPrivateLayout();            ;
570         return ambientView.getAmbientChild() != null;
571     }
572 
573     public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
574             implements InflationCallback, InflationTask {
575 
576         private final StatusBarNotification mSbn;
577         private final Context mContext;
578         private final boolean mIsLowPriority;
579         private final boolean mIsChildInGroup;
580         private final boolean mUsesIncreasedHeight;
581         private final InflationCallback mCallback;
582         private final boolean mUsesIncreasedHeadsUpHeight;
583         private final boolean mRedactAmbient;
584         private int mReInflateFlags;
585         private ExpandableNotificationRow mRow;
586         private Exception mError;
587         private RemoteViews.OnClickHandler mRemoteViewClickHandler;
588         private CancellationSignal mCancellationSignal;
589 
AsyncInflationTask(StatusBarNotification notification, int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, InflationCallback callback, RemoteViews.OnClickHandler remoteViewClickHandler)590         private AsyncInflationTask(StatusBarNotification notification,
591                 int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority,
592                 boolean isChildInGroup, boolean usesIncreasedHeight,
593                 boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
594                 InflationCallback callback,
595                 RemoteViews.OnClickHandler remoteViewClickHandler) {
596             mRow = row;
597             mSbn = notification;
598             mReInflateFlags = reInflateFlags;
599             mContext = mRow.getContext();
600             mIsLowPriority = isLowPriority;
601             mIsChildInGroup = isChildInGroup;
602             mUsesIncreasedHeight = usesIncreasedHeight;
603             mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
604             mRedactAmbient = redactAmbient;
605             mRemoteViewClickHandler = remoteViewClickHandler;
606             mCallback = callback;
607             NotificationData.Entry entry = row.getEntry();
608             entry.setInflationTask(this);
609         }
610 
611         @VisibleForTesting
getReInflateFlags()612         public int getReInflateFlags() {
613             return mReInflateFlags;
614         }
615 
616         @Override
doInBackground(Void... params)617         protected InflationProgress doInBackground(Void... params) {
618             try {
619                 final Notification.Builder recoveredBuilder
620                         = Notification.Builder.recoverBuilder(mContext,
621                         mSbn.getNotification());
622                 Context packageContext = mSbn.getPackageContext(mContext);
623                 Notification notification = mSbn.getNotification();
624                 if (notification.isMediaNotification()) {
625                     MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext,
626                             packageContext);
627                     processor.processNotification(notification, recoveredBuilder);
628                 }
629                 return createRemoteViews(mReInflateFlags,
630                         recoveredBuilder, mIsLowPriority, mIsChildInGroup,
631                         mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
632                         packageContext);
633             } catch (Exception e) {
634                 mError = e;
635                 return null;
636             }
637         }
638 
639         @Override
onPostExecute(InflationProgress result)640         protected void onPostExecute(InflationProgress result) {
641             if (mError == null) {
642                 mCancellationSignal = apply(result, mReInflateFlags, mRow, mRedactAmbient,
643                         mRemoteViewClickHandler, this);
644             } else {
645                 handleError(mError);
646             }
647         }
648 
handleError(Exception e)649         private void handleError(Exception e) {
650             mRow.getEntry().onInflationTaskFinished();
651             StatusBarNotification sbn = mRow.getStatusBarNotification();
652             final String ident = sbn.getPackageName() + "/0x"
653                     + Integer.toHexString(sbn.getId());
654             Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e);
655             mCallback.handleInflationException(sbn,
656                     new InflationException("Couldn't inflate contentViews" + e));
657         }
658 
659         @Override
abort()660         public void abort() {
661             cancel(true /* mayInterruptIfRunning */);
662             if (mCancellationSignal != null) {
663                 mCancellationSignal.cancel();
664             }
665         }
666 
667         @Override
supersedeTask(InflationTask task)668         public void supersedeTask(InflationTask task) {
669             if (task instanceof AsyncInflationTask) {
670                 // We want to inflate all flags of the previous task as well
671                 mReInflateFlags |= ((AsyncInflationTask) task).mReInflateFlags;
672             }
673         }
674 
675         @Override
handleInflationException(StatusBarNotification notification, Exception e)676         public void handleInflationException(StatusBarNotification notification, Exception e) {
677             handleError(e);
678         }
679 
680         @Override
onAsyncInflationFinished(NotificationData.Entry entry)681         public void onAsyncInflationFinished(NotificationData.Entry entry) {
682             mRow.getEntry().onInflationTaskFinished();
683             mRow.onNotificationUpdated();
684             mCallback.onAsyncInflationFinished(mRow.getEntry());
685         }
686 
687         @Override
doInflateSynchronous()688         public boolean doInflateSynchronous() {
689             return mCallback != null && mCallback.doInflateSynchronous();
690         }
691     }
692 
693     @VisibleForTesting
694     static class InflationProgress {
695         private RemoteViews newContentView;
696         private RemoteViews newHeadsUpView;
697         private RemoteViews newExpandedView;
698         private RemoteViews newAmbientView;
699         private RemoteViews newPublicView;
700 
701         @VisibleForTesting
702         Context packageContext;
703 
704         private View inflatedContentView;
705         private View inflatedHeadsUpView;
706         private View inflatedExpandedView;
707         private View inflatedAmbientView;
708         private View inflatedPublicView;
709         private CharSequence headsUpStatusBarText;
710         private CharSequence headsUpStatusBarTextPublic;
711     }
712 
713     @VisibleForTesting
714     abstract static class ApplyCallback {
setResultView(View v)715         public abstract void setResultView(View v);
getRemoteView()716         public abstract RemoteViews getRemoteView();
717     }
718 
719     /**
720      * A custom executor that allows more tasks to be queued. Default values are copied from
721      * AsyncTask
722       */
723     private static class InflationExecutor implements Executor {
724         private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
725         // We want at least 2 threads and at most 4 threads in the core pool,
726         // preferring to have 1 less than the CPU count to avoid saturating
727         // the CPU with background work
728         private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
729         private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
730         private static final int KEEP_ALIVE_SECONDS = 30;
731 
732         private static final ThreadFactory sThreadFactory = new ThreadFactory() {
733             private final AtomicInteger mCount = new AtomicInteger(1);
734 
735             public Thread newThread(Runnable r) {
736                 return new Thread(r, "InflaterThread #" + mCount.getAndIncrement());
737             }
738         };
739 
740         private final ThreadPoolExecutor mExecutor;
741 
InflationExecutor()742         private InflationExecutor() {
743             mExecutor = new ThreadPoolExecutor(
744                     CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
745                     new LinkedBlockingQueue<>(), sThreadFactory);
746             mExecutor.allowCoreThreadTimeOut(true);
747         }
748 
749         @Override
execute(Runnable runnable)750         public void execute(Runnable runnable) {
751             mExecutor.execute(runnable);
752         }
753     }
754 }
755