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