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
17package com.android.systemui.statusbar.notification;
18
19import android.annotation.Nullable;
20import android.app.Notification;
21import android.content.Context;
22import android.os.AsyncTask;
23import android.os.CancellationSignal;
24import android.service.notification.StatusBarNotification;
25import android.util.Log;
26import android.view.View;
27import android.widget.RemoteViews;
28
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.systemui.R;
31import com.android.systemui.statusbar.InflationTask;
32import com.android.systemui.statusbar.ExpandableNotificationRow;
33import com.android.systemui.statusbar.NotificationContentView;
34import com.android.systemui.statusbar.NotificationData;
35import com.android.systemui.statusbar.phone.StatusBar;
36import com.android.systemui.util.Assert;
37
38import java.util.HashMap;
39import java.util.concurrent.Executor;
40import java.util.concurrent.LinkedBlockingQueue;
41import java.util.concurrent.ThreadFactory;
42import java.util.concurrent.ThreadPoolExecutor;
43import java.util.concurrent.TimeUnit;
44import java.util.concurrent.atomic.AtomicInteger;
45
46/**
47 * A utility that inflates the right kind of contentView based on the state
48 */
49public 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
71    public NotificationInflater(ExpandableNotificationRow row) {
72        mRow = row;
73    }
74
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     */
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
94    public void setUsesIncreasedHeight(boolean usesIncreasedHeight) {
95        mUsesIncreasedHeight = usesIncreasedHeight;
96    }
97
98    public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) {
99        mUsesIncreasedHeadsUpHeight = usesIncreasedHeight;
100    }
101
102    public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) {
103        mRemoteViewClickHandler = remoteViewClickHandler;
104    }
105
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     */
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
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
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
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
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
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
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     */
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
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
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
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
541    public void setInflationCallback(InflationCallback callback) {
542        mCallback = callback;
543    }
544
545    public interface InflationCallback {
546        void handleInflationException(StatusBarNotification notification, Exception e);
547        void onAsyncInflationFinished(NotificationData.Entry entry);
548
549        /**
550         * Used to disable async-ness for tests. Should only be used for tests.
551         */
552        default boolean doInflateSynchronous() {
553            return false;
554        }
555    }
556
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
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
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
612        public int getReInflateFlags() {
613            return mReInflateFlags;
614        }
615
616        @Override
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
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
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
660        public void abort() {
661            cancel(true /* mayInterruptIfRunning */);
662            if (mCancellationSignal != null) {
663                mCancellationSignal.cancel();
664            }
665        }
666
667        @Override
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
676        public void handleInflationException(StatusBarNotification notification, Exception e) {
677            handleError(e);
678        }
679
680        @Override
681        public void onAsyncInflationFinished(NotificationData.Entry entry) {
682            mRow.getEntry().onInflationTaskFinished();
683            mRow.onNotificationUpdated();
684            mCallback.onAsyncInflationFinished(mRow.getEntry());
685        }
686
687        @Override
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 {
715        public abstract void setResultView(View v);
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
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
750        public void execute(Runnable runnable) {
751            mExecutor.execute(runnable);
752        }
753    }
754}
755