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