1/*
2 * Copyright (C) 2014 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;
18
19import android.app.Notification;
20import android.app.PendingIntent;
21import android.app.RemoteInput;
22import android.content.Context;
23import android.graphics.Rect;
24import android.os.Build;
25import android.service.notification.StatusBarNotification;
26import android.util.AttributeSet;
27import android.view.NotificationHeaderView;
28import android.view.View;
29import android.view.ViewGroup;
30import android.view.ViewTreeObserver;
31import android.widget.FrameLayout;
32import android.widget.ImageView;
33
34import com.android.internal.util.NotificationColorUtil;
35import com.android.systemui.R;
36import com.android.systemui.statusbar.notification.HybridNotificationView;
37import com.android.systemui.statusbar.notification.HybridGroupManager;
38import com.android.systemui.statusbar.notification.NotificationCustomViewWrapper;
39import com.android.systemui.statusbar.notification.NotificationUtils;
40import com.android.systemui.statusbar.notification.NotificationViewWrapper;
41import com.android.systemui.statusbar.phone.NotificationGroupManager;
42import com.android.systemui.statusbar.policy.RemoteInputView;
43
44/**
45 * A frame layout containing the actual payload of the notification, including the contracted,
46 * expanded and heads up layout. This class is responsible for clipping the content and and
47 * switching between the expanded, contracted and the heads up view depending on its clipped size.
48 */
49public class NotificationContentView extends FrameLayout {
50
51    private static final int VISIBLE_TYPE_CONTRACTED = 0;
52    private static final int VISIBLE_TYPE_EXPANDED = 1;
53    private static final int VISIBLE_TYPE_HEADSUP = 2;
54    private static final int VISIBLE_TYPE_SINGLELINE = 3;
55    public static final int UNDEFINED = -1;
56
57    private final Rect mClipBounds = new Rect();
58    private final int mMinContractedHeight;
59    private final int mNotificationContentMarginEnd;
60
61    private View mContractedChild;
62    private View mExpandedChild;
63    private View mHeadsUpChild;
64    private HybridNotificationView mSingleLineView;
65
66    private RemoteInputView mExpandedRemoteInput;
67    private RemoteInputView mHeadsUpRemoteInput;
68
69    private NotificationViewWrapper mContractedWrapper;
70    private NotificationViewWrapper mExpandedWrapper;
71    private NotificationViewWrapper mHeadsUpWrapper;
72    private HybridGroupManager mHybridGroupManager;
73    private int mClipTopAmount;
74    private int mContentHeight;
75    private int mVisibleType = VISIBLE_TYPE_CONTRACTED;
76    private boolean mDark;
77    private boolean mAnimate;
78    private boolean mIsHeadsUp;
79    private boolean mShowingLegacyBackground;
80    private boolean mIsChildInGroup;
81    private int mSmallHeight;
82    private int mHeadsUpHeight;
83    private int mNotificationMaxHeight;
84    private StatusBarNotification mStatusBarNotification;
85    private NotificationGroupManager mGroupManager;
86    private RemoteInputController mRemoteInputController;
87
88    private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener
89            = new ViewTreeObserver.OnPreDrawListener() {
90        @Override
91        public boolean onPreDraw() {
92            // We need to post since we don't want the notification to animate on the very first
93            // frame
94            post(new Runnable() {
95                @Override
96                public void run() {
97                    mAnimate = true;
98                }
99            });
100            getViewTreeObserver().removeOnPreDrawListener(this);
101            return true;
102        }
103    };
104
105    private OnClickListener mExpandClickListener;
106    private boolean mBeforeN;
107    private boolean mExpandable;
108    private boolean mClipToActualHeight = true;
109    private ExpandableNotificationRow mContainingNotification;
110    /** The visible type at the start of a touch driven transformation */
111    private int mTransformationStartVisibleType;
112    /** The visible type at the start of an animation driven transformation */
113    private int mAnimationStartVisibleType = UNDEFINED;
114    private boolean mUserExpanding;
115    private int mSingleLineWidthIndention;
116    private boolean mForceSelectNextLayout = true;
117    private PendingIntent mPreviousExpandedRemoteInputIntent;
118    private PendingIntent mPreviousHeadsUpRemoteInputIntent;
119
120    private int mContentHeightAtAnimationStart = UNDEFINED;
121    private boolean mFocusOnVisibilityChange;
122    private boolean mHeadsupDisappearRunning;
123
124
125    public NotificationContentView(Context context, AttributeSet attrs) {
126        super(context, attrs);
127        mHybridGroupManager = new HybridGroupManager(getContext(), this);
128        mMinContractedHeight = getResources().getDimensionPixelSize(
129                R.dimen.min_notification_layout_height);
130        mNotificationContentMarginEnd = getResources().getDimensionPixelSize(
131                com.android.internal.R.dimen.notification_content_margin_end);
132        reset();
133    }
134
135    public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight) {
136        mSmallHeight = smallHeight;
137        mHeadsUpHeight = headsUpMaxHeight;
138        mNotificationMaxHeight = maxHeight;
139    }
140
141    @Override
142    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
143        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
144        boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
145        boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
146        int maxSize = Integer.MAX_VALUE;
147        int width = MeasureSpec.getSize(widthMeasureSpec);
148        if (hasFixedHeight || isHeightLimited) {
149            maxSize = MeasureSpec.getSize(heightMeasureSpec);
150        }
151        int maxChildHeight = 0;
152        if (mExpandedChild != null) {
153            int size = Math.min(maxSize, mNotificationMaxHeight);
154            ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams();
155            if (layoutParams.height >= 0) {
156                // An actual height is set
157                size = Math.min(maxSize, layoutParams.height);
158            }
159            int spec = size == Integer.MAX_VALUE
160                    ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
161                    : MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
162            mExpandedChild.measure(widthMeasureSpec, spec);
163            maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight());
164        }
165        if (mContractedChild != null) {
166            int heightSpec;
167            int size = Math.min(maxSize, mSmallHeight);
168            if (shouldContractedBeFixedSize()) {
169                heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
170            } else {
171                heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
172            }
173            mContractedChild.measure(widthMeasureSpec, heightSpec);
174            int measuredHeight = mContractedChild.getMeasuredHeight();
175            if (measuredHeight < mMinContractedHeight) {
176                heightSpec = MeasureSpec.makeMeasureSpec(mMinContractedHeight, MeasureSpec.EXACTLY);
177                mContractedChild.measure(widthMeasureSpec, heightSpec);
178            }
179            maxChildHeight = Math.max(maxChildHeight, measuredHeight);
180            if (updateContractedHeaderWidth()) {
181                mContractedChild.measure(widthMeasureSpec, heightSpec);
182            }
183            if (mExpandedChild != null
184                    && mContractedChild.getMeasuredHeight() > mExpandedChild.getMeasuredHeight()) {
185                // the Expanded child is smaller then the collapsed. Let's remeasure it.
186                heightSpec = MeasureSpec.makeMeasureSpec(mContractedChild.getMeasuredHeight(),
187                        MeasureSpec.EXACTLY);
188                mExpandedChild.measure(widthMeasureSpec, heightSpec);
189            }
190        }
191        if (mHeadsUpChild != null) {
192            int size = Math.min(maxSize, mHeadsUpHeight);
193            ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams();
194            if (layoutParams.height >= 0) {
195                // An actual height is set
196                size = Math.min(size, layoutParams.height);
197            }
198            mHeadsUpChild.measure(widthMeasureSpec,
199                    MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST));
200            maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight());
201        }
202        if (mSingleLineView != null) {
203            int singleLineWidthSpec = widthMeasureSpec;
204            if (mSingleLineWidthIndention != 0
205                    && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
206                singleLineWidthSpec = MeasureSpec.makeMeasureSpec(
207                        width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(),
208                        MeasureSpec.AT_MOST);
209            }
210            mSingleLineView.measure(singleLineWidthSpec,
211                    MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST));
212            maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight());
213        }
214        int ownHeight = Math.min(maxChildHeight, maxSize);
215        setMeasuredDimension(width, ownHeight);
216    }
217
218    private boolean updateContractedHeaderWidth() {
219        // We need to update the expanded and the collapsed header to have exactly the same with to
220        // have the expand buttons laid out at the same location.
221        NotificationHeaderView contractedHeader = mContractedWrapper.getNotificationHeader();
222        if (contractedHeader != null) {
223            if (mExpandedChild != null
224                    && mExpandedWrapper.getNotificationHeader() != null) {
225                NotificationHeaderView expandedHeader = mExpandedWrapper.getNotificationHeader();
226                int expandedSize = expandedHeader.getMeasuredWidth()
227                        - expandedHeader.getPaddingEnd();
228                int collapsedSize = contractedHeader.getMeasuredWidth()
229                        - expandedHeader.getPaddingEnd();
230                if (expandedSize != collapsedSize) {
231                    int paddingEnd = contractedHeader.getMeasuredWidth() - expandedSize;
232                    contractedHeader.setPadding(
233                            contractedHeader.isLayoutRtl()
234                                    ? paddingEnd
235                                    : contractedHeader.getPaddingLeft(),
236                            contractedHeader.getPaddingTop(),
237                            contractedHeader.isLayoutRtl()
238                                    ? contractedHeader.getPaddingLeft()
239                                    : paddingEnd,
240                            contractedHeader.getPaddingBottom());
241                    contractedHeader.setShowWorkBadgeAtEnd(true);
242                    return true;
243                }
244            } else {
245                int paddingEnd = mNotificationContentMarginEnd;
246                if (contractedHeader.getPaddingEnd() != paddingEnd) {
247                    contractedHeader.setPadding(
248                            contractedHeader.isLayoutRtl()
249                                    ? paddingEnd
250                                    : contractedHeader.getPaddingLeft(),
251                            contractedHeader.getPaddingTop(),
252                            contractedHeader.isLayoutRtl()
253                                    ? contractedHeader.getPaddingLeft()
254                                    : paddingEnd,
255                            contractedHeader.getPaddingBottom());
256                    contractedHeader.setShowWorkBadgeAtEnd(false);
257                    return true;
258                }
259            }
260        }
261        return false;
262    }
263
264    private boolean shouldContractedBeFixedSize() {
265        return mBeforeN && mContractedWrapper instanceof NotificationCustomViewWrapper;
266    }
267
268    @Override
269    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
270        int previousHeight = 0;
271        if (mExpandedChild != null) {
272            previousHeight = mExpandedChild.getHeight();
273        }
274        super.onLayout(changed, left, top, right, bottom);
275        if (previousHeight != 0 && mExpandedChild.getHeight() != previousHeight) {
276            mContentHeightAtAnimationStart = previousHeight;
277        }
278        updateClipping();
279        invalidateOutline();
280        selectLayout(false /* animate */, mForceSelectNextLayout /* force */);
281        mForceSelectNextLayout = false;
282        updateExpandButtons(mExpandable);
283    }
284
285    @Override
286    protected void onAttachedToWindow() {
287        super.onAttachedToWindow();
288        updateVisibility();
289    }
290
291    public void reset() {
292        if (mContractedChild != null) {
293            mContractedChild.animate().cancel();
294            removeView(mContractedChild);
295        }
296        mPreviousExpandedRemoteInputIntent = null;
297        if (mExpandedRemoteInput != null) {
298            mExpandedRemoteInput.onNotificationUpdateOrReset();
299            if (mExpandedRemoteInput.isActive()) {
300                mPreviousExpandedRemoteInputIntent = mExpandedRemoteInput.getPendingIntent();
301            }
302        }
303        if (mExpandedChild != null) {
304            mExpandedChild.animate().cancel();
305            removeView(mExpandedChild);
306            mExpandedRemoteInput = null;
307        }
308        mPreviousHeadsUpRemoteInputIntent = null;
309        if (mHeadsUpRemoteInput != null) {
310            mHeadsUpRemoteInput.onNotificationUpdateOrReset();
311            if (mHeadsUpRemoteInput.isActive()) {
312                mPreviousHeadsUpRemoteInputIntent = mHeadsUpRemoteInput.getPendingIntent();
313            }
314        }
315        if (mHeadsUpChild != null) {
316            mHeadsUpChild.animate().cancel();
317            removeView(mHeadsUpChild);
318            mHeadsUpRemoteInput = null;
319        }
320        mContractedChild = null;
321        mExpandedChild = null;
322        mHeadsUpChild = null;
323    }
324
325    public View getContractedChild() {
326        return mContractedChild;
327    }
328
329    public View getExpandedChild() {
330        return mExpandedChild;
331    }
332
333    public View getHeadsUpChild() {
334        return mHeadsUpChild;
335    }
336
337    public void setContractedChild(View child) {
338        if (mContractedChild != null) {
339            mContractedChild.animate().cancel();
340            removeView(mContractedChild);
341        }
342        addView(child);
343        mContractedChild = child;
344        mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child,
345                mContainingNotification);
346        mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
347    }
348
349    public void setExpandedChild(View child) {
350        if (mExpandedChild != null) {
351            mExpandedChild.animate().cancel();
352            removeView(mExpandedChild);
353        }
354        addView(child);
355        mExpandedChild = child;
356        mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child,
357                mContainingNotification);
358    }
359
360    public void setHeadsUpChild(View child) {
361        if (mHeadsUpChild != null) {
362            mHeadsUpChild.animate().cancel();
363            removeView(mHeadsUpChild);
364        }
365        addView(child);
366        mHeadsUpChild = child;
367        mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child,
368                mContainingNotification);
369    }
370
371    @Override
372    protected void onVisibilityChanged(View changedView, int visibility) {
373        super.onVisibilityChanged(changedView, visibility);
374        updateVisibility();
375    }
376
377    private void updateVisibility() {
378        setVisible(isShown());
379    }
380
381    @Override
382    protected void onDetachedFromWindow() {
383        super.onDetachedFromWindow();
384        getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
385    }
386
387    private void setVisible(final boolean isVisible) {
388        if (isVisible) {
389            // This call can happen multiple times, but removing only removes a single one.
390            // We therefore need to remove the old one.
391            getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
392            // We only animate if we are drawn at least once, otherwise the view might animate when
393            // it's shown the first time
394            getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener);
395        } else {
396            getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
397            mAnimate = false;
398        }
399    }
400
401    private void focusExpandButtonIfNecessary() {
402        if (mFocusOnVisibilityChange) {
403            NotificationHeaderView header = getVisibleNotificationHeader();
404            if (header != null) {
405                ImageView expandButton = header.getExpandButton();
406                if (expandButton != null) {
407                    expandButton.requestAccessibilityFocus();
408                }
409            }
410            mFocusOnVisibilityChange = false;
411        }
412    }
413
414    public void setContentHeight(int contentHeight) {
415        mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());
416        selectLayout(mAnimate /* animate */, false /* force */);
417
418        int minHeightHint = getMinContentHeightHint();
419
420        NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
421        if (wrapper != null) {
422            wrapper.setContentHeight(mContentHeight, minHeightHint);
423        }
424
425        wrapper = getVisibleWrapper(mTransformationStartVisibleType);
426        if (wrapper != null) {
427            wrapper.setContentHeight(mContentHeight, minHeightHint);
428        }
429
430        updateClipping();
431        invalidateOutline();
432    }
433
434    /**
435     * @return the minimum apparent height that the wrapper should allow for the purpose
436     *         of aligning elements at the bottom edge. If this is larger than the content
437     *         height, the notification is clipped instead of being further shrunk.
438     */
439    private int getMinContentHeightHint() {
440        if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) {
441            return mContext.getResources().getDimensionPixelSize(
442                        com.android.internal.R.dimen.notification_action_list_height);
443        }
444
445        // Transition between heads-up & expanded, or pinned.
446        if (mHeadsUpChild != null && mExpandedChild != null) {
447            boolean transitioningBetweenHunAndExpanded =
448                    isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) ||
449                    isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP);
450            boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED)
451                    && (mIsHeadsUp || mHeadsupDisappearRunning);
452            if (transitioningBetweenHunAndExpanded || pinned) {
453                return Math.min(mHeadsUpChild.getHeight(), mExpandedChild.getHeight());
454            }
455        }
456
457        // Size change of the expanded version
458        if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart >= 0
459                && mExpandedChild != null) {
460            return Math.min(mContentHeightAtAnimationStart, mExpandedChild.getHeight());
461        }
462
463        int hint;
464        if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) {
465            hint = mHeadsUpChild.getHeight();
466        } else if (mExpandedChild != null) {
467            hint = mExpandedChild.getHeight();
468        } else {
469            hint = mContractedChild.getHeight() + mContext.getResources().getDimensionPixelSize(
470                    com.android.internal.R.dimen.notification_action_list_height);
471        }
472
473        if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) {
474            hint = Math.min(hint, mExpandedChild.getHeight());
475        }
476        return hint;
477    }
478
479    private boolean isTransitioningFromTo(int from, int to) {
480        return (mTransformationStartVisibleType == from || mAnimationStartVisibleType == from)
481                && mVisibleType == to;
482    }
483
484    private boolean isVisibleOrTransitioning(int type) {
485        return mVisibleType == type || mTransformationStartVisibleType == type
486                || mAnimationStartVisibleType == type;
487    }
488
489    private void updateContentTransformation() {
490        int visibleType = calculateVisibleType();
491        if (visibleType != mVisibleType) {
492            // A new transformation starts
493            mTransformationStartVisibleType = mVisibleType;
494            final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
495            final TransformableView hiddenView = getTransformableViewForVisibleType(
496                    mTransformationStartVisibleType);
497            shownView.transformFrom(hiddenView, 0.0f);
498            getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
499            hiddenView.transformTo(shownView, 0.0f);
500            mVisibleType = visibleType;
501            updateBackgroundColor(true /* animate */);
502        }
503        if (mForceSelectNextLayout) {
504            forceUpdateVisibilities();
505        }
506        if (mTransformationStartVisibleType != UNDEFINED
507                && mVisibleType != mTransformationStartVisibleType
508                && getViewForVisibleType(mTransformationStartVisibleType) != null) {
509            final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType);
510            final TransformableView hiddenView = getTransformableViewForVisibleType(
511                    mTransformationStartVisibleType);
512            float transformationAmount = calculateTransformationAmount();
513            shownView.transformFrom(hiddenView, transformationAmount);
514            hiddenView.transformTo(shownView, transformationAmount);
515            updateBackgroundTransformation(transformationAmount);
516        } else {
517            updateViewVisibilities(visibleType);
518            updateBackgroundColor(false);
519        }
520    }
521
522    private void updateBackgroundTransformation(float transformationAmount) {
523        int endColor = getBackgroundColor(mVisibleType);
524        int startColor = getBackgroundColor(mTransformationStartVisibleType);
525        if (endColor != startColor) {
526            if (startColor == 0) {
527                startColor = mContainingNotification.getBackgroundColorWithoutTint();
528            }
529            if (endColor == 0) {
530                endColor = mContainingNotification.getBackgroundColorWithoutTint();
531            }
532            endColor = NotificationUtils.interpolateColors(startColor, endColor,
533                    transformationAmount);
534        }
535        mContainingNotification.updateBackgroundAlpha(transformationAmount);
536        mContainingNotification.setContentBackground(endColor, false, this);
537    }
538
539    private float calculateTransformationAmount() {
540        int startHeight = getViewForVisibleType(mTransformationStartVisibleType).getHeight();
541        int endHeight = getViewForVisibleType(mVisibleType).getHeight();
542        int progress = Math.abs(mContentHeight - startHeight);
543        int totalDistance = Math.abs(endHeight - startHeight);
544        float amount = (float) progress / (float) totalDistance;
545        return Math.min(1.0f, amount);
546    }
547
548    public int getContentHeight() {
549        return mContentHeight;
550    }
551
552    public int getMaxHeight() {
553        if (mExpandedChild != null) {
554            return mExpandedChild.getHeight();
555        } else if (mIsHeadsUp && mHeadsUpChild != null) {
556            return mHeadsUpChild.getHeight();
557        }
558        return mContractedChild.getHeight();
559    }
560
561    public int getMinHeight() {
562        return getMinHeight(false /* likeGroupExpanded */);
563    }
564
565    public int getMinHeight(boolean likeGroupExpanded) {
566        if (likeGroupExpanded || !mIsChildInGroup || isGroupExpanded()) {
567            return mContractedChild.getHeight();
568        } else {
569            return mSingleLineView.getHeight();
570        }
571    }
572
573    private boolean isGroupExpanded() {
574        return mGroupManager.isGroupExpanded(mStatusBarNotification);
575    }
576
577    public void setClipTopAmount(int clipTopAmount) {
578        mClipTopAmount = clipTopAmount;
579        updateClipping();
580    }
581
582    private void updateClipping() {
583        if (mClipToActualHeight) {
584            mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
585            setClipBounds(mClipBounds);
586        } else {
587            setClipBounds(null);
588        }
589    }
590
591    public void setClipToActualHeight(boolean clipToActualHeight) {
592        mClipToActualHeight = clipToActualHeight;
593        updateClipping();
594    }
595
596    private void selectLayout(boolean animate, boolean force) {
597        if (mContractedChild == null) {
598            return;
599        }
600        if (mUserExpanding) {
601            updateContentTransformation();
602        } else {
603            int visibleType = calculateVisibleType();
604            boolean changedType = visibleType != mVisibleType;
605            if (changedType || force) {
606                View visibleView = getViewForVisibleType(visibleType);
607                if (visibleView != null) {
608                    visibleView.setVisibility(VISIBLE);
609                    transferRemoteInputFocus(visibleType);
610                }
611                NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType);
612                if (visibleWrapper != null) {
613                    visibleWrapper.setContentHeight(mContentHeight, getMinContentHeightHint());
614                }
615
616                if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null)
617                        || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null)
618                        || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null)
619                        || visibleType == VISIBLE_TYPE_CONTRACTED)) {
620                    animateToVisibleType(visibleType);
621                } else {
622                    updateViewVisibilities(visibleType);
623                }
624                mVisibleType = visibleType;
625                if (changedType) {
626                    focusExpandButtonIfNecessary();
627                }
628                updateBackgroundColor(animate);
629            }
630        }
631    }
632
633    private void forceUpdateVisibilities() {
634        boolean contractedVisible = mVisibleType == VISIBLE_TYPE_CONTRACTED
635                || mTransformationStartVisibleType == VISIBLE_TYPE_CONTRACTED;
636        boolean expandedVisible = mVisibleType == VISIBLE_TYPE_EXPANDED
637                || mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED;
638        boolean headsUpVisible = mVisibleType == VISIBLE_TYPE_HEADSUP
639                || mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP;
640        boolean singleLineVisible = mVisibleType == VISIBLE_TYPE_SINGLELINE
641                || mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE;
642        if (!contractedVisible) {
643            mContractedChild.setVisibility(View.INVISIBLE);
644        } else {
645            mContractedWrapper.setVisible(true);
646        }
647        if (mExpandedChild != null) {
648            if (!expandedVisible) {
649                mExpandedChild.setVisibility(View.INVISIBLE);
650            } else {
651                mExpandedWrapper.setVisible(true);
652            }
653        }
654        if (mHeadsUpChild != null) {
655            if (!headsUpVisible) {
656                mHeadsUpChild.setVisibility(View.INVISIBLE);
657            } else {
658                mHeadsUpWrapper.setVisible(true);
659            }
660        }
661        if (mSingleLineView != null) {
662            if (!singleLineVisible) {
663                mSingleLineView.setVisibility(View.INVISIBLE);
664            } else {
665                mSingleLineView.setVisible(true);
666            }
667        }
668    }
669
670    public void updateBackgroundColor(boolean animate) {
671        int customBackgroundColor = getBackgroundColor(mVisibleType);
672        mContainingNotification.resetBackgroundAlpha();
673        mContainingNotification.setContentBackground(customBackgroundColor, animate, this);
674    }
675
676    public int getVisibleType() {
677        return mVisibleType;
678    }
679
680    public int getBackgroundColorForExpansionState() {
681        // When expanding or user locked we want the new type, when collapsing we want
682        // the original type
683        final int visibleType = (mContainingNotification.isGroupExpanded()
684                || mContainingNotification.isUserLocked())
685                        ? calculateVisibleType()
686                        : getVisibleType();
687        return getBackgroundColor(visibleType);
688    }
689
690    public int getBackgroundColor(int visibleType) {
691        NotificationViewWrapper currentVisibleWrapper = getVisibleWrapper(visibleType);
692        int customBackgroundColor = 0;
693        if (currentVisibleWrapper != null) {
694            customBackgroundColor = currentVisibleWrapper.getCustomBackgroundColor();
695        }
696        return customBackgroundColor;
697    }
698
699    private void updateViewVisibilities(int visibleType) {
700        boolean contractedVisible = visibleType == VISIBLE_TYPE_CONTRACTED;
701        mContractedWrapper.setVisible(contractedVisible);
702        if (mExpandedChild != null) {
703            boolean expandedVisible = visibleType == VISIBLE_TYPE_EXPANDED;
704            mExpandedWrapper.setVisible(expandedVisible);
705        }
706        if (mHeadsUpChild != null) {
707            boolean headsUpVisible = visibleType == VISIBLE_TYPE_HEADSUP;
708            mHeadsUpWrapper.setVisible(headsUpVisible);
709        }
710        if (mSingleLineView != null) {
711            boolean singleLineVisible = visibleType == VISIBLE_TYPE_SINGLELINE;
712            mSingleLineView.setVisible(singleLineVisible);
713        }
714    }
715
716    private void animateToVisibleType(int visibleType) {
717        final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
718        final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType);
719        if (shownView == hiddenView || hiddenView == null) {
720            shownView.setVisible(true);
721            return;
722        }
723        mAnimationStartVisibleType = mVisibleType;
724        shownView.transformFrom(hiddenView);
725        getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
726        hiddenView.transformTo(shownView, new Runnable() {
727            @Override
728            public void run() {
729                if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) {
730                    hiddenView.setVisible(false);
731                }
732                mAnimationStartVisibleType = UNDEFINED;
733            }
734        });
735    }
736
737    private void transferRemoteInputFocus(int visibleType) {
738        if (visibleType == VISIBLE_TYPE_HEADSUP
739                && mHeadsUpRemoteInput != null
740                && (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive())) {
741            mHeadsUpRemoteInput.stealFocusFrom(mExpandedRemoteInput);
742        }
743        if (visibleType == VISIBLE_TYPE_EXPANDED
744                && mExpandedRemoteInput != null
745                && (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive())) {
746            mExpandedRemoteInput.stealFocusFrom(mHeadsUpRemoteInput);
747        }
748    }
749
750    /**
751     * @param visibleType one of the static enum types in this view
752     * @return the corresponding transformable view according to the given visible type
753     */
754    private TransformableView getTransformableViewForVisibleType(int visibleType) {
755        switch (visibleType) {
756            case VISIBLE_TYPE_EXPANDED:
757                return mExpandedWrapper;
758            case VISIBLE_TYPE_HEADSUP:
759                return mHeadsUpWrapper;
760            case VISIBLE_TYPE_SINGLELINE:
761                return mSingleLineView;
762            default:
763                return mContractedWrapper;
764        }
765    }
766
767    /**
768     * @param visibleType one of the static enum types in this view
769     * @return the corresponding view according to the given visible type
770     */
771    private View getViewForVisibleType(int visibleType) {
772        switch (visibleType) {
773            case VISIBLE_TYPE_EXPANDED:
774                return mExpandedChild;
775            case VISIBLE_TYPE_HEADSUP:
776                return mHeadsUpChild;
777            case VISIBLE_TYPE_SINGLELINE:
778                return mSingleLineView;
779            default:
780                return mContractedChild;
781        }
782    }
783
784    private NotificationViewWrapper getVisibleWrapper(int visibleType) {
785        switch (visibleType) {
786            case VISIBLE_TYPE_EXPANDED:
787                return mExpandedWrapper;
788            case VISIBLE_TYPE_HEADSUP:
789                return mHeadsUpWrapper;
790            case VISIBLE_TYPE_CONTRACTED:
791                return mContractedWrapper;
792            default:
793                return null;
794        }
795    }
796
797    /**
798     * @return one of the static enum types in this view, calculated form the current state
799     */
800    public int calculateVisibleType() {
801        if (mUserExpanding) {
802            int height = !mIsChildInGroup || isGroupExpanded()
803                    || mContainingNotification.isExpanded(true /* allowOnKeyguard */)
804                    ? mContainingNotification.getMaxContentHeight()
805                    : mContainingNotification.getShowingLayout().getMinHeight();
806            if (height == 0) {
807                height = mContentHeight;
808            }
809            int expandedVisualType = getVisualTypeForHeight(height);
810            int collapsedVisualType = mIsChildInGroup && !isGroupExpanded()
811                    ? VISIBLE_TYPE_SINGLELINE
812                    : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight());
813            return mTransformationStartVisibleType == collapsedVisualType
814                    ? expandedVisualType
815                    : collapsedVisualType;
816        }
817        int intrinsicHeight = mContainingNotification.getIntrinsicHeight();
818        int viewHeight = mContentHeight;
819        if (intrinsicHeight != 0) {
820            // the intrinsicHeight might be 0 because it was just reset.
821            viewHeight = Math.min(mContentHeight, intrinsicHeight);
822        }
823        return getVisualTypeForHeight(viewHeight);
824    }
825
826    private int getVisualTypeForHeight(float viewHeight) {
827        boolean noExpandedChild = mExpandedChild == null;
828        if (!noExpandedChild && viewHeight == mExpandedChild.getHeight()) {
829            return VISIBLE_TYPE_EXPANDED;
830        }
831        if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) {
832            return VISIBLE_TYPE_SINGLELINE;
833        }
834
835        if ((mIsHeadsUp || mHeadsupDisappearRunning) && mHeadsUpChild != null) {
836            if (viewHeight <= mHeadsUpChild.getHeight() || noExpandedChild) {
837                return VISIBLE_TYPE_HEADSUP;
838            } else {
839                return VISIBLE_TYPE_EXPANDED;
840            }
841        } else {
842            if (noExpandedChild || (viewHeight <= mContractedChild.getHeight()
843                    && (!mIsChildInGroup || isGroupExpanded()
844                            || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
845                return VISIBLE_TYPE_CONTRACTED;
846            } else {
847                return VISIBLE_TYPE_EXPANDED;
848            }
849        }
850    }
851
852    public boolean isContentExpandable() {
853        return mExpandedChild != null;
854    }
855
856    public void setDark(boolean dark, boolean fade, long delay) {
857        if (mContractedChild == null) {
858            return;
859        }
860        mDark = dark;
861        if (mVisibleType == VISIBLE_TYPE_CONTRACTED || !dark) {
862            mContractedWrapper.setDark(dark, fade, delay);
863        }
864        if (mVisibleType == VISIBLE_TYPE_EXPANDED || (mExpandedChild != null && !dark)) {
865            mExpandedWrapper.setDark(dark, fade, delay);
866        }
867        if (mVisibleType == VISIBLE_TYPE_HEADSUP || (mHeadsUpChild != null && !dark)) {
868            mHeadsUpWrapper.setDark(dark, fade, delay);
869        }
870        if (mSingleLineView != null && (mVisibleType == VISIBLE_TYPE_SINGLELINE || !dark)) {
871            mSingleLineView.setDark(dark, fade, delay);
872        }
873    }
874
875    public void setHeadsUp(boolean headsUp) {
876        mIsHeadsUp = headsUp;
877        selectLayout(false /* animate */, true /* force */);
878        updateExpandButtons(mExpandable);
879    }
880
881    @Override
882    public boolean hasOverlappingRendering() {
883
884        // This is not really true, but good enough when fading from the contracted to the expanded
885        // layout, and saves us some layers.
886        return false;
887    }
888
889    public void setShowingLegacyBackground(boolean showing) {
890        mShowingLegacyBackground = showing;
891        updateShowingLegacyBackground();
892    }
893
894    private void updateShowingLegacyBackground() {
895        if (mContractedChild != null) {
896            mContractedWrapper.setShowingLegacyBackground(mShowingLegacyBackground);
897        }
898        if (mExpandedChild != null) {
899            mExpandedWrapper.setShowingLegacyBackground(mShowingLegacyBackground);
900        }
901        if (mHeadsUpChild != null) {
902            mHeadsUpWrapper.setShowingLegacyBackground(mShowingLegacyBackground);
903        }
904    }
905
906    public void setIsChildInGroup(boolean isChildInGroup) {
907        mIsChildInGroup = isChildInGroup;
908        updateSingleLineView();
909    }
910
911    public void onNotificationUpdated(NotificationData.Entry entry) {
912        mStatusBarNotification = entry.notification;
913        mBeforeN = entry.targetSdk < Build.VERSION_CODES.N;
914        updateSingleLineView();
915        applyRemoteInput(entry);
916        if (mContractedChild != null) {
917            mContractedWrapper.notifyContentUpdated(entry.notification);
918        }
919        if (mExpandedChild != null) {
920            mExpandedWrapper.notifyContentUpdated(entry.notification);
921        }
922        if (mHeadsUpChild != null) {
923            mHeadsUpWrapper.notifyContentUpdated(entry.notification);
924        }
925        updateShowingLegacyBackground();
926        mForceSelectNextLayout = true;
927        setDark(mDark, false /* animate */, 0 /* delay */);
928        mPreviousExpandedRemoteInputIntent = null;
929        mPreviousHeadsUpRemoteInputIntent = null;
930    }
931
932    private void updateSingleLineView() {
933        if (mIsChildInGroup) {
934            mSingleLineView = mHybridGroupManager.bindFromNotification(
935                    mSingleLineView, mStatusBarNotification.getNotification());
936        } else if (mSingleLineView != null) {
937            removeView(mSingleLineView);
938            mSingleLineView = null;
939        }
940    }
941
942    private void applyRemoteInput(final NotificationData.Entry entry) {
943        if (mRemoteInputController == null) {
944            return;
945        }
946
947        boolean hasRemoteInput = false;
948
949        Notification.Action[] actions = entry.notification.getNotification().actions;
950        if (actions != null) {
951            for (Notification.Action a : actions) {
952                if (a.getRemoteInputs() != null) {
953                    for (RemoteInput ri : a.getRemoteInputs()) {
954                        if (ri.getAllowFreeFormInput()) {
955                            hasRemoteInput = true;
956                            break;
957                        }
958                    }
959                }
960            }
961        }
962
963        View bigContentView = mExpandedChild;
964        if (bigContentView != null) {
965            mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasRemoteInput,
966                    mPreviousExpandedRemoteInputIntent);
967        } else {
968            mExpandedRemoteInput = null;
969        }
970
971        View headsUpContentView = mHeadsUpChild;
972        if (headsUpContentView != null) {
973            mHeadsUpRemoteInput = applyRemoteInput(headsUpContentView, entry, hasRemoteInput,
974                    mPreviousHeadsUpRemoteInputIntent);
975        } else {
976            mHeadsUpRemoteInput = null;
977        }
978    }
979
980    private RemoteInputView applyRemoteInput(View view, NotificationData.Entry entry,
981            boolean hasRemoteInput, PendingIntent existingPendingIntent) {
982        View actionContainerCandidate = view.findViewById(
983                com.android.internal.R.id.actions_container);
984        if (actionContainerCandidate instanceof FrameLayout) {
985            RemoteInputView existing = (RemoteInputView)
986                    view.findViewWithTag(RemoteInputView.VIEW_TAG);
987
988            if (existing != null) {
989                existing.onNotificationUpdateOrReset();
990            }
991
992            if (existing == null && hasRemoteInput) {
993                ViewGroup actionContainer = (FrameLayout) actionContainerCandidate;
994                RemoteInputView riv = RemoteInputView.inflate(
995                        mContext, actionContainer, entry, mRemoteInputController);
996
997                riv.setVisibility(View.INVISIBLE);
998                actionContainer.addView(riv, new LayoutParams(
999                        ViewGroup.LayoutParams.MATCH_PARENT,
1000                        ViewGroup.LayoutParams.MATCH_PARENT)
1001                );
1002                existing = riv;
1003            }
1004            if (hasRemoteInput) {
1005                int color = entry.notification.getNotification().color;
1006                if (color == Notification.COLOR_DEFAULT) {
1007                    color = mContext.getColor(R.color.default_remote_input_background);
1008                }
1009                existing.setBackgroundColor(NotificationColorUtil.ensureTextBackgroundColor(color,
1010                        mContext.getColor(R.color.remote_input_text_enabled),
1011                        mContext.getColor(R.color.remote_input_hint)));
1012
1013                if (existingPendingIntent != null || existing.isActive()) {
1014                    // The current action could be gone, or the pending intent no longer valid.
1015                    // If we find a matching action in the new notification, focus, otherwise close.
1016                    Notification.Action[] actions = entry.notification.getNotification().actions;
1017                    if (existingPendingIntent != null) {
1018                        existing.setPendingIntent(existingPendingIntent);
1019                    }
1020                    if (existing.updatePendingIntentFromActions(actions)) {
1021                        if (!existing.isActive()) {
1022                            existing.focus();
1023                        }
1024                    } else {
1025                        if (existing.isActive()) {
1026                            existing.close();
1027                        }
1028                    }
1029                }
1030            }
1031            return existing;
1032        }
1033        return null;
1034    }
1035
1036    public void closeRemoteInput() {
1037        if (mHeadsUpRemoteInput != null) {
1038            mHeadsUpRemoteInput.close();
1039        }
1040        if (mExpandedRemoteInput != null) {
1041            mExpandedRemoteInput.close();
1042        }
1043    }
1044
1045    public void setGroupManager(NotificationGroupManager groupManager) {
1046        mGroupManager = groupManager;
1047    }
1048
1049    public void setRemoteInputController(RemoteInputController r) {
1050        mRemoteInputController = r;
1051    }
1052
1053    public void setExpandClickListener(OnClickListener expandClickListener) {
1054        mExpandClickListener = expandClickListener;
1055    }
1056
1057    public void updateExpandButtons(boolean expandable) {
1058        mExpandable = expandable;
1059        // if the expanded child has the same height as the collapsed one we hide it.
1060        if (mExpandedChild != null && mExpandedChild.getHeight() != 0) {
1061            if ((!mIsHeadsUp || mHeadsUpChild == null)) {
1062                if (mExpandedChild.getHeight() == mContractedChild.getHeight()) {
1063                    expandable = false;
1064                }
1065            } else if (mExpandedChild.getHeight() == mHeadsUpChild.getHeight()) {
1066                expandable = false;
1067            }
1068        }
1069        if (mExpandedChild != null) {
1070            mExpandedWrapper.updateExpandability(expandable, mExpandClickListener);
1071        }
1072        if (mContractedChild != null) {
1073            mContractedWrapper.updateExpandability(expandable, mExpandClickListener);
1074        }
1075        if (mHeadsUpChild != null) {
1076            mHeadsUpWrapper.updateExpandability(expandable,  mExpandClickListener);
1077        }
1078    }
1079
1080    public NotificationHeaderView getNotificationHeader() {
1081        NotificationHeaderView header = null;
1082        if (mContractedChild != null) {
1083            header = mContractedWrapper.getNotificationHeader();
1084        }
1085        if (header == null && mExpandedChild != null) {
1086            header = mExpandedWrapper.getNotificationHeader();
1087        }
1088        if (header == null && mHeadsUpChild != null) {
1089            header = mHeadsUpWrapper.getNotificationHeader();
1090        }
1091        return header;
1092    }
1093
1094    public NotificationHeaderView getVisibleNotificationHeader() {
1095        NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
1096        return wrapper == null ? null : wrapper.getNotificationHeader();
1097    }
1098
1099    public void setContainingNotification(ExpandableNotificationRow containingNotification) {
1100        mContainingNotification = containingNotification;
1101    }
1102
1103    public void requestSelectLayout(boolean needsAnimation) {
1104        selectLayout(needsAnimation, false);
1105    }
1106
1107    public void reInflateViews() {
1108        if (mIsChildInGroup && mSingleLineView != null) {
1109            removeView(mSingleLineView);
1110            mSingleLineView = null;
1111            updateSingleLineView();
1112        }
1113    }
1114
1115    public void setUserExpanding(boolean userExpanding) {
1116        mUserExpanding = userExpanding;
1117        if (userExpanding) {
1118            mTransformationStartVisibleType = mVisibleType;
1119        } else {
1120            mTransformationStartVisibleType = UNDEFINED;
1121            mVisibleType = calculateVisibleType();
1122            updateViewVisibilities(mVisibleType);
1123            updateBackgroundColor(false);
1124        }
1125    }
1126
1127    /**
1128     * Set by how much the single line view should be indented. Used when a overflow indicator is
1129     * present and only during measuring
1130     */
1131    public void setSingleLineWidthIndention(int singleLineWidthIndention) {
1132        if (singleLineWidthIndention != mSingleLineWidthIndention) {
1133            mSingleLineWidthIndention = singleLineWidthIndention;
1134            mContainingNotification.forceLayout();
1135            forceLayout();
1136        }
1137    }
1138
1139    public HybridNotificationView getSingleLineView() {
1140        return mSingleLineView;
1141    }
1142
1143    public void setRemoved() {
1144        if (mExpandedRemoteInput != null) {
1145            mExpandedRemoteInput.setRemoved();
1146        }
1147        if (mHeadsUpRemoteInput != null) {
1148            mHeadsUpRemoteInput.setRemoved();
1149        }
1150    }
1151
1152    public void setContentHeightAnimating(boolean animating) {
1153        if (!animating) {
1154            mContentHeightAtAnimationStart = UNDEFINED;
1155        }
1156    }
1157
1158    public void setHeadsupDisappearRunning(boolean headsupDisappearRunning) {
1159        mHeadsupDisappearRunning = headsupDisappearRunning;
1160        selectLayout(false /* animate */, true /* force */);
1161    }
1162
1163    public void setFocusOnVisibilityChange() {
1164        mFocusOnVisibilityChange = true;
1165    }
1166}
1167