NotificationInflater.java revision 0f66a4cc16ec1a927c90ac559c73c80ddcb5ee71
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.statusbar.Abortable;
31import com.android.systemui.statusbar.ExpandableNotificationRow;
32import com.android.systemui.statusbar.NotificationContentView;
33import com.android.systemui.statusbar.NotificationData;
34import com.android.systemui.statusbar.phone.StatusBar;
35import com.android.systemui.util.Assert;
36
37import java.util.HashMap;
38
39/**
40 * A utility that inflates the right kind of contentView based on the state
41 */
42public class NotificationInflater {
43
44    @VisibleForTesting
45    static final int FLAG_REINFLATE_ALL = ~0;
46    private static final int FLAG_REINFLATE_CONTENT_VIEW = 1<<0;
47    @VisibleForTesting
48    static final int FLAG_REINFLATE_EXPANDED_VIEW = 1<<1;
49    private static final int FLAG_REINFLATE_HEADS_UP_VIEW = 1<<2;
50    private static final int FLAG_REINFLATE_PUBLIC_VIEW = 1<<3;
51    private static final int FLAG_REINFLATE_AMBIENT_VIEW = 1<<4;
52
53    private final ExpandableNotificationRow mRow;
54    private boolean mIsLowPriority;
55    private boolean mUsesIncreasedHeight;
56    private boolean mUsesIncreasedHeadsUpHeight;
57    private RemoteViews.OnClickHandler mRemoteViewClickHandler;
58    private boolean mIsChildInGroup;
59    private InflationCallback mCallback;
60    private boolean mRedactAmbient;
61
62    public NotificationInflater(ExpandableNotificationRow row) {
63        mRow = row;
64    }
65
66    public void setIsLowPriority(boolean isLowPriority) {
67        mIsLowPriority = isLowPriority;
68    }
69
70    /**
71     * Set whether the notification is a child in a group
72     *
73     * @return whether the view was re-inflated
74     */
75    public void setIsChildInGroup(boolean childInGroup) {
76        if (childInGroup != mIsChildInGroup) {
77            mIsChildInGroup = childInGroup;
78            if (mIsLowPriority) {
79                int flags = FLAG_REINFLATE_CONTENT_VIEW | FLAG_REINFLATE_EXPANDED_VIEW;
80                inflateNotificationViews(flags);
81            }
82        } ;
83    }
84
85    public void setUsesIncreasedHeight(boolean usesIncreasedHeight) {
86        mUsesIncreasedHeight = usesIncreasedHeight;
87    }
88
89    public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) {
90        mUsesIncreasedHeadsUpHeight = usesIncreasedHeight;
91    }
92
93    public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) {
94        mRemoteViewClickHandler = remoteViewClickHandler;
95    }
96
97    public void setRedactAmbient(boolean redactAmbient) {
98        if (mRedactAmbient != redactAmbient) {
99            mRedactAmbient = redactAmbient;
100            if (mRow.getEntry() == null) {
101                return;
102            }
103            inflateNotificationViews(FLAG_REINFLATE_AMBIENT_VIEW);
104        }
105    }
106
107    /**
108     * Inflate all views of this notification on a background thread. This is asynchronous and will
109     * notify the callback once it's finished.
110     */
111    public void inflateNotificationViews() {
112        inflateNotificationViews(FLAG_REINFLATE_ALL);
113    }
114
115    /**
116     * Reinflate all views for the specified flags on a background thread. This is asynchronous and
117     * will notify the callback once it's finished.
118     *
119     * @param reInflateFlags flags which views should be reinflated. Use {@link #FLAG_REINFLATE_ALL}
120     *                       to reinflate all of views.
121     */
122    @VisibleForTesting
123    void inflateNotificationViews(int reInflateFlags) {
124        StatusBarNotification sbn = mRow.getEntry().notification;
125        new AsyncInflationTask(sbn, reInflateFlags, mRow, mIsLowPriority,
126                mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
127                mCallback, mRemoteViewClickHandler).execute();
128    }
129
130    @VisibleForTesting
131    InflationProgress inflateNotificationViews(int reInflateFlags,
132            Notification.Builder builder, Context packageContext) {
133        InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority,
134                mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
135                mRedactAmbient, packageContext);
136        apply(result, reInflateFlags, mRow, mRedactAmbient, mRemoteViewClickHandler, null);
137        return result;
138    }
139
140    private static InflationProgress createRemoteViews(int reInflateFlags,
141            Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup,
142            boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
143            Context packageContext) {
144        InflationProgress result = new InflationProgress();
145        isLowPriority = isLowPriority && !isChildInGroup;
146        if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
147            result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);
148        }
149
150        if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
151            result.newExpandedView = createExpandedView(builder, isLowPriority);
152        }
153
154        if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
155            result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);
156        }
157
158        if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
159            result.newPublicView = builder.makePublicContentView();
160        }
161
162        if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
163            result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification()
164                    : builder.makeAmbientNotification();
165        }
166        result.packageContext = packageContext;
167        return result;
168    }
169
170    public static CancellationSignal apply(InflationProgress result, int reInflateFlags,
171            ExpandableNotificationRow row, boolean redactAmbient,
172            RemoteViews.OnClickHandler remoteViewClickHandler,
173            @Nullable InflationCallback callback) {
174        NotificationData.Entry entry = row.getEntry();
175        NotificationContentView privateLayout = row.getPrivateLayout();
176        NotificationContentView publicLayout = row.getPublicLayout();
177        final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
178
179        int flag = FLAG_REINFLATE_CONTENT_VIEW;
180        if ((reInflateFlags & flag) != 0) {
181            boolean isNewView = !compareRemoteViews(result.newContentView, entry.cachedContentView);
182            ApplyCallback applyCallback = new ApplyCallback() {
183                @Override
184                public void setResultView(View v) {
185                    result.inflatedContentView = v;
186                }
187
188                @Override
189                public RemoteViews getRemoteView() {
190                    return result.newContentView;
191                }
192            };
193            applyRemoteView(result, reInflateFlags, flag, row, redactAmbient,
194                    isNewView, remoteViewClickHandler, callback, entry, privateLayout,
195                    privateLayout.getContractedChild(),
196                    runningInflations, applyCallback);
197        }
198
199        flag = FLAG_REINFLATE_EXPANDED_VIEW;
200        if ((reInflateFlags & flag) != 0) {
201            if (result.newExpandedView != null) {
202                boolean isNewView = !compareRemoteViews(result.newExpandedView,
203                        entry.cachedBigContentView);
204                ApplyCallback applyCallback = new ApplyCallback() {
205                    @Override
206                    public void setResultView(View v) {
207                        result.inflatedExpandedView = v;
208                    }
209
210                    @Override
211                    public RemoteViews getRemoteView() {
212                        return result.newExpandedView;
213                    }
214                };
215                applyRemoteView(result, reInflateFlags, flag, row,
216                        redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
217                        privateLayout, privateLayout.getExpandedChild(), runningInflations,
218                        applyCallback);
219            }
220        }
221
222        flag = FLAG_REINFLATE_HEADS_UP_VIEW;
223        if ((reInflateFlags & flag) != 0) {
224            if (result.newHeadsUpView != null) {
225                boolean isNewView = !compareRemoteViews(result.newHeadsUpView,
226                        entry.cachedHeadsUpContentView);
227                ApplyCallback applyCallback = new ApplyCallback() {
228                    @Override
229                    public void setResultView(View v) {
230                        result.inflatedHeadsUpView = v;
231                    }
232
233                    @Override
234                    public RemoteViews getRemoteView() {
235                        return result.newHeadsUpView;
236                    }
237                };
238                applyRemoteView(result, reInflateFlags, flag, row,
239                        redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
240                        privateLayout, privateLayout.getHeadsUpChild(), runningInflations,
241                        applyCallback);
242            }
243        }
244
245        flag = FLAG_REINFLATE_PUBLIC_VIEW;
246        if ((reInflateFlags & flag) != 0) {
247            boolean isNewView = !compareRemoteViews(result.newPublicView,
248                    entry.cachedPublicContentView);
249            ApplyCallback applyCallback = new ApplyCallback() {
250                @Override
251                public void setResultView(View v) {
252                    result.inflatedPublicView = v;
253                }
254
255                @Override
256                public RemoteViews getRemoteView() {
257                    return result.newPublicView;
258                }
259            };
260            applyRemoteView(result, reInflateFlags, flag, row,
261                    redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
262                    publicLayout, publicLayout.getContractedChild(), runningInflations,
263                    applyCallback);
264        }
265
266        flag = FLAG_REINFLATE_AMBIENT_VIEW;
267        if ((reInflateFlags & flag) != 0) {
268            NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout;
269            boolean isNewView = !canReapplyAmbient(row, redactAmbient) ||
270                    !compareRemoteViews(result.newAmbientView, entry.cachedAmbientContentView);
271            ApplyCallback applyCallback = new ApplyCallback() {
272                @Override
273                public void setResultView(View v) {
274                    result.inflatedAmbientView = v;
275                }
276
277                @Override
278                public RemoteViews getRemoteView() {
279                    return result.newAmbientView;
280                }
281            };
282            applyRemoteView(result, reInflateFlags, flag, row,
283                    redactAmbient, isNewView, remoteViewClickHandler, callback, entry,
284                    newParent, newParent.getAmbientChild(), runningInflations,
285                    applyCallback);
286        }
287
288        // Let's try to finish, maybe nobody is even inflating anything
289        finishIfDone(result, reInflateFlags, runningInflations, callback, row,
290                redactAmbient);
291        CancellationSignal cancellationSignal = new CancellationSignal();
292        cancellationSignal.setOnCancelListener(
293                () -> runningInflations.values().forEach(CancellationSignal::cancel));
294        return cancellationSignal;
295    }
296
297    private static void applyRemoteView(final InflationProgress result,
298            final int reInflateFlags, int inflationId,
299            final ExpandableNotificationRow row,
300            final boolean redactAmbient, boolean isNewView,
301            RemoteViews.OnClickHandler remoteViewClickHandler,
302            @Nullable final InflationCallback callback, NotificationData.Entry entry,
303            NotificationContentView parentLayout, View existingView,
304            final HashMap<Integer, CancellationSignal> runningInflations,
305            ApplyCallback applyCallback) {
306        RemoteViews.OnViewAppliedListener listener
307                = new RemoteViews.OnViewAppliedListener() {
308
309            @Override
310            public void onViewApplied(View v) {
311                if (isNewView) {
312                    v.setIsRootNamespace(true);
313                    applyCallback.setResultView(v);
314                }
315                runningInflations.remove(inflationId);
316                finishIfDone(result, reInflateFlags, runningInflations, callback, row,
317                        redactAmbient);
318            }
319
320            @Override
321            public void onError(Exception e) {
322                runningInflations.remove(inflationId);
323                handleInflationError(runningInflations, e, entry.notification, callback);
324            }
325        };
326        CancellationSignal cancellationSignal;
327        RemoteViews newContentView = applyCallback.getRemoteView();
328        if (isNewView) {
329            cancellationSignal = newContentView.applyAsync(
330                    result.packageContext,
331                    parentLayout,
332                    null /* executor */,
333                    listener,
334                    remoteViewClickHandler);
335        } else {
336            cancellationSignal = newContentView.reapplyAsync(
337                    result.packageContext,
338                    existingView,
339                    null /* executor */,
340                    listener,
341                    remoteViewClickHandler);
342        }
343        runningInflations.put(inflationId, cancellationSignal);
344    }
345
346    private static void handleInflationError(HashMap<Integer, CancellationSignal> runningInflations,
347            Exception e, StatusBarNotification notification, @Nullable InflationCallback callback) {
348        Assert.isMainThread();
349        runningInflations.values().forEach(CancellationSignal::cancel);
350        if (callback != null) {
351            callback.handleInflationException(notification, e);
352        }
353    }
354
355    /**
356     * Finish the inflation of the views
357     *
358     * @return true if the inflation was finished
359     */
360    private static boolean finishIfDone(InflationProgress result, int reInflateFlags,
361            HashMap<Integer, CancellationSignal> runningInflations,
362            @Nullable InflationCallback endListener, ExpandableNotificationRow row,
363            boolean redactAmbient) {
364        Assert.isMainThread();
365        NotificationData.Entry entry = row.getEntry();
366        NotificationContentView privateLayout = row.getPrivateLayout();
367        NotificationContentView publicLayout = row.getPublicLayout();
368        if (runningInflations.isEmpty()) {
369            if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {
370                if (result.inflatedContentView != null) {
371                    privateLayout.setContractedChild(result.inflatedContentView);
372                }
373                entry.cachedContentView = result.newContentView;
374            }
375
376            if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {
377                if (result.inflatedExpandedView != null) {
378                    privateLayout.setExpandedChild(result.inflatedExpandedView);
379                } else if (result.newExpandedView == null) {
380                    privateLayout.setExpandedChild(null);
381                }
382                entry.cachedBigContentView = result.newExpandedView;
383                row.setExpandable(result.newExpandedView != null);
384            }
385
386            if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {
387                if (result.inflatedHeadsUpView != null) {
388                    privateLayout.setHeadsUpChild(result.inflatedHeadsUpView);
389                } else if (result.newHeadsUpView == null) {
390                    privateLayout.setHeadsUpChild(null);
391                }
392                entry.cachedHeadsUpContentView = result.newHeadsUpView;
393            }
394
395            if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {
396                if (result.inflatedPublicView != null) {
397                    publicLayout.setContractedChild(result.inflatedPublicView);
398                }
399                entry.cachedPublicContentView = result.newPublicView;
400            }
401
402            if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {
403                if (result.inflatedAmbientView != null) {
404                    NotificationContentView newParent = redactAmbient
405                            ? publicLayout : privateLayout;
406                    NotificationContentView otherParent = !redactAmbient
407                            ? publicLayout : privateLayout;
408                    newParent.setAmbientChild(result.inflatedAmbientView);
409                    otherParent.setAmbientChild(null);
410                }
411                entry.cachedAmbientContentView = result.newAmbientView;
412            }
413            if (endListener != null) {
414                endListener.onAsyncInflationFinished(row.getEntry());
415            }
416            return true;
417        }
418        return false;
419    }
420
421    private static RemoteViews createExpandedView(Notification.Builder builder,
422            boolean isLowPriority) {
423        RemoteViews bigContentView = builder.createBigContentView();
424        if (bigContentView != null) {
425            return bigContentView;
426        }
427        if (isLowPriority) {
428            RemoteViews contentView = builder.createContentView();
429            Notification.Builder.makeHeaderExpanded(contentView);
430            return contentView;
431        }
432        return null;
433    }
434
435    private static RemoteViews createContentView(Notification.Builder builder,
436            boolean isLowPriority, boolean useLarge) {
437        if (isLowPriority) {
438            return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
439        }
440        return builder.createContentView(useLarge);
441    }
442
443    // Returns true if the RemoteViews are the same.
444    private static boolean compareRemoteViews(final RemoteViews a, final RemoteViews b) {
445        return (a == null && b == null) ||
446                (a != null && b != null
447                        && b.getPackage() != null
448                        && a.getPackage() != null
449                        && a.getPackage().equals(b.getPackage())
450                        && a.getLayoutId() == b.getLayoutId());
451    }
452
453    public void setInflationCallback(InflationCallback callback) {
454        mCallback = callback;
455    }
456
457    public interface InflationCallback {
458        void handleInflationException(StatusBarNotification notification, Exception e);
459        void onAsyncInflationFinished(NotificationData.Entry entry);
460    }
461
462    public void onDensityOrFontScaleChanged() {
463        NotificationData.Entry entry = mRow.getEntry();
464        entry.cachedAmbientContentView = null;
465        entry.cachedBigContentView = null;
466        entry.cachedContentView = null;
467        entry.cachedHeadsUpContentView = null;
468        entry.cachedPublicContentView = null;
469        inflateNotificationViews();
470    }
471
472    private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) {
473        NotificationContentView ambientView = redactAmbient ? row.getPublicLayout()
474                : row.getPrivateLayout();            ;
475        return ambientView.getAmbientChild() != null;
476    }
477
478    public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>
479            implements InflationCallback, Abortable {
480
481        private final StatusBarNotification mSbn;
482        private final Context mContext;
483        private final int mReInflateFlags;
484        private final boolean mIsLowPriority;
485        private final boolean mIsChildInGroup;
486        private final boolean mUsesIncreasedHeight;
487        private final InflationCallback mCallback;
488        private final boolean mUsesIncreasedHeadsUpHeight;
489        private final boolean mRedactAmbient;
490        private ExpandableNotificationRow mRow;
491        private Exception mError;
492        private RemoteViews.OnClickHandler mRemoteViewClickHandler;
493        private CancellationSignal mCancellationSignal;
494
495        private AsyncInflationTask(StatusBarNotification notification,
496                int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority,
497                boolean isChildInGroup, boolean usesIncreasedHeight,
498                boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
499                InflationCallback callback,
500                RemoteViews.OnClickHandler remoteViewClickHandler) {
501            mRow = row;
502            NotificationData.Entry entry = row.getEntry();
503            entry.setInflationTask(this);
504            mSbn = notification;
505            mReInflateFlags = reInflateFlags;
506            mContext = mRow.getContext();
507            mIsLowPriority = isLowPriority;
508            mIsChildInGroup = isChildInGroup;
509            mUsesIncreasedHeight = usesIncreasedHeight;
510            mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight;
511            mRedactAmbient = redactAmbient;
512            mRemoteViewClickHandler = remoteViewClickHandler;
513            mCallback = callback;
514        }
515
516        @Override
517        protected InflationProgress doInBackground(Void... params) {
518            try {
519                final Notification.Builder recoveredBuilder
520                        = Notification.Builder.recoverBuilder(mContext,
521                        mSbn.getNotification());
522                Context packageContext = mSbn.getPackageContext(mContext);
523                Notification notification = mSbn.getNotification();
524                if (notification.isMediaNotification()) {
525                    MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext,
526                            packageContext);
527                    processor.setIsLowPriority(mIsLowPriority);
528                    processor.processNotification(notification, recoveredBuilder);
529                }
530                return createRemoteViews(mReInflateFlags,
531                        recoveredBuilder, mIsLowPriority, mIsChildInGroup,
532                        mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
533                        packageContext);
534            } catch (Exception e) {
535                mError = e;
536                return null;
537            }
538        }
539
540        @Override
541        protected void onPostExecute(InflationProgress result) {
542            if (mError == null) {
543                mCancellationSignal = apply(result, mReInflateFlags, mRow, mRedactAmbient,
544                        mRemoteViewClickHandler, this);
545            } else {
546                handleError(mError);
547            }
548        }
549
550        private void handleError(Exception e) {
551            mRow.getEntry().onInflationTaskFinished();
552            StatusBarNotification sbn = mRow.getStatusBarNotification();
553            final String ident = sbn.getPackageName() + "/0x"
554                    + Integer.toHexString(sbn.getId());
555            Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e);
556            mCallback.handleInflationException(sbn,
557                    new InflationException("Couldn't inflate contentViews" + e));
558        }
559
560        @Override
561        public void abort() {
562            cancel(true /* mayInterruptIfRunning */);
563            if (mCancellationSignal != null) {
564                mCancellationSignal.cancel();
565            }
566        }
567
568        @Override
569        public void handleInflationException(StatusBarNotification notification, Exception e) {
570            handleError(e);
571        }
572
573        @Override
574        public void onAsyncInflationFinished(NotificationData.Entry entry) {
575            mRow.getEntry().onInflationTaskFinished();
576            mRow.onNotificationUpdated();
577            mCallback.onAsyncInflationFinished(mRow.getEntry());
578        }
579    }
580
581    private static class InflationProgress {
582        private RemoteViews newContentView;
583        private RemoteViews newHeadsUpView;
584        private RemoteViews newExpandedView;
585        private RemoteViews newAmbientView;
586        private RemoteViews newPublicView;
587
588        private Context packageContext;
589
590        private View inflatedContentView;
591        private View inflatedHeadsUpView;
592        private View inflatedExpandedView;
593        private View inflatedAmbientView;
594        private View inflatedPublicView;
595    }
596
597    private abstract static class ApplyCallback {
598        public abstract void setResultView(View v);
599        public abstract RemoteViews getRemoteView();
600    }
601}
602