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