ExpandableNotificationRow.java revision 3c76d509d362cf5086a63722ab41e04f5d539182
1/*
2 * Copyright (C) 2013 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.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.animation.ValueAnimator;
24import android.animation.ValueAnimator.AnimatorUpdateListener;
25import android.app.Notification;
26import android.content.Context;
27import android.graphics.drawable.AnimatedVectorDrawable;
28import android.graphics.drawable.AnimationDrawable;
29import android.graphics.drawable.ColorDrawable;
30import android.graphics.drawable.Drawable;
31import android.os.Build;
32import android.service.notification.StatusBarNotification;
33import android.util.AttributeSet;
34import android.view.LayoutInflater;
35import android.view.MotionEvent;
36import android.view.NotificationHeaderView;
37import android.view.View;
38import android.view.ViewStub;
39import android.view.accessibility.AccessibilityEvent;
40import android.widget.Chronometer;
41import android.widget.ImageView;
42import android.widget.RemoteViews;
43
44import com.android.systemui.R;
45import com.android.systemui.classifier.FalsingManager;
46import com.android.systemui.statusbar.notification.NotificationViewWrapper;
47import com.android.systemui.statusbar.phone.NotificationGroupManager;
48import com.android.systemui.statusbar.policy.HeadsUpManager;
49import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
50import com.android.systemui.statusbar.stack.StackScrollState;
51import com.android.systemui.statusbar.stack.StackStateAnimator;
52import com.android.systemui.statusbar.stack.StackViewState;
53
54import java.util.ArrayList;
55import java.util.List;
56
57public class ExpandableNotificationRow extends ActivatableNotificationView {
58
59    private static final int DEFAULT_DIVIDER_ALPHA = 0x29;
60    private static final int COLORED_DIVIDER_ALPHA = 0x7B;
61    private int mNotificationMinHeightLegacy;
62    private int mMaxHeadsUpHeightLegacy;
63    private int mMaxHeadsUpHeight;
64    private int mNotificationMinHeight;
65    private int mNotificationMaxHeight;
66
67    /** Does this row contain layouts that can adapt to row expansion */
68    private boolean mExpandable;
69    /** Has the user actively changed the expansion state of this row */
70    private boolean mHasUserChangedExpansion;
71    /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
72    private boolean mUserExpanded;
73
74    /**
75     * Has this notification been expanded while it was pinned
76     */
77    private boolean mExpandedWhenPinned;
78    /** Is the user touching this row */
79    private boolean mUserLocked;
80    /** Are we showing the "public" version */
81    private boolean mShowingPublic;
82    private boolean mSensitive;
83    private boolean mSensitiveHiddenInGeneral;
84    private boolean mShowingPublicInitialized;
85    private boolean mHideSensitiveForIntrinsicHeight;
86
87    /**
88     * Is this notification expanded by the system. The expansion state can be overridden by the
89     * user expansion.
90     */
91    private boolean mIsSystemExpanded;
92
93    /**
94     * Whether the notification is on the keyguard and the expansion is disabled.
95     */
96    private boolean mOnKeyguard;
97
98    private AnimatorSet mTranslateAnim;
99    private ArrayList<View> mTranslateableViews;
100    private NotificationContentView mPublicLayout;
101    private NotificationContentView mPrivateLayout;
102    private int mMaxExpandHeight;
103    private int mHeadsUpHeight;
104    private View mVetoButton;
105    private boolean mClearable;
106    private ExpansionLogger mLogger;
107    private String mLoggingKey;
108    private boolean mWasReset;
109    private NotificationSettingsIconRow mSettingsIconRow;
110    private NotificationGuts mGuts;
111    private NotificationData.Entry mEntry;
112    private StatusBarNotification mStatusBarNotification;
113    private boolean mIsHeadsUp;
114    private boolean mLastChronometerRunning = true;
115    private NotificationHeaderView mNotificationHeader;
116    private NotificationViewWrapper mNotificationHeaderWrapper;
117    private ViewStub mChildrenContainerStub;
118    private NotificationGroupManager mGroupManager;
119    private boolean mChildrenExpanded;
120    private boolean mIsSummaryWithChildren;
121    private NotificationChildrenContainer mChildrenContainer;
122    private ViewStub mSettingsIconRowStub;
123    private ViewStub mGutsStub;
124    private boolean mIsSystemChildExpanded;
125    private boolean mIsPinned;
126    private FalsingManager mFalsingManager;
127    private HeadsUpManager mHeadsUpManager;
128    private NotificationHeaderUtil mHeaderUtil = new NotificationHeaderUtil(this);
129
130    private boolean mJustClicked;
131    private boolean mIconAnimationRunning;
132    private boolean mShowNoBackground;
133    private ExpandableNotificationRow mNotificationParent;
134    private OnExpandClickListener mOnExpandClickListener;
135    private OnClickListener mExpandClickListener = new OnClickListener() {
136        @Override
137        public void onClick(View v) {
138            if (!mShowingPublic && mGroupManager.isSummaryOfGroup(mStatusBarNotification)) {
139                mGroupManager.toggleGroupExpansion(mStatusBarNotification);
140                mOnExpandClickListener.onExpandClicked(mEntry,
141                        mGroupManager.isGroupExpanded(mStatusBarNotification));
142            } else {
143                boolean nowExpanded;
144                if (isPinned()) {
145                    nowExpanded = !mExpandedWhenPinned;
146                    mExpandedWhenPinned = nowExpanded;
147                } else {
148                    nowExpanded = !isExpanded();
149                    setUserExpanded(nowExpanded);
150                }
151                notifyHeightChanged(true);
152                mOnExpandClickListener.onExpandClicked(mEntry, nowExpanded);
153            }
154        }
155    };
156
157    public NotificationContentView getPrivateLayout() {
158        return mPrivateLayout;
159    }
160
161    public NotificationContentView getPublicLayout() {
162        return mPublicLayout;
163    }
164
165    public void setIconAnimationRunning(boolean running) {
166        setIconAnimationRunning(running, mPublicLayout);
167        setIconAnimationRunning(running, mPrivateLayout);
168        setIconAnimationRunningForChild(running, mNotificationHeader);
169        if (mIsSummaryWithChildren) {
170            List<ExpandableNotificationRow> notificationChildren =
171                    mChildrenContainer.getNotificationChildren();
172            for (int i = 0; i < notificationChildren.size(); i++) {
173                ExpandableNotificationRow child = notificationChildren.get(i);
174                child.setIconAnimationRunning(running);
175            }
176        }
177        mIconAnimationRunning = running;
178    }
179
180    private void setIconAnimationRunning(boolean running, NotificationContentView layout) {
181        if (layout != null) {
182            View contractedChild = layout.getContractedChild();
183            View expandedChild = layout.getExpandedChild();
184            View headsUpChild = layout.getHeadsUpChild();
185            setIconAnimationRunningForChild(running, contractedChild);
186            setIconAnimationRunningForChild(running, expandedChild);
187            setIconAnimationRunningForChild(running, headsUpChild);
188        }
189    }
190
191    private void setIconAnimationRunningForChild(boolean running, View child) {
192        if (child != null) {
193            ImageView icon = (ImageView) child.findViewById(com.android.internal.R.id.icon);
194            setIconRunning(icon, running);
195            ImageView rightIcon = (ImageView) child.findViewById(
196                    com.android.internal.R.id.right_icon);
197            setIconRunning(rightIcon, running);
198        }
199    }
200
201    private void setIconRunning(ImageView imageView, boolean running) {
202        if (imageView != null) {
203            Drawable drawable = imageView.getDrawable();
204            if (drawable instanceof AnimationDrawable) {
205                AnimationDrawable animationDrawable = (AnimationDrawable) drawable;
206                if (running) {
207                    animationDrawable.start();
208                } else {
209                    animationDrawable.stop();
210                }
211            } else if (drawable instanceof AnimatedVectorDrawable) {
212                AnimatedVectorDrawable animationDrawable = (AnimatedVectorDrawable) drawable;
213                if (running) {
214                    animationDrawable.start();
215                } else {
216                    animationDrawable.stop();
217                }
218            }
219        }
220    }
221
222    public void onNotificationUpdated(NotificationData.Entry entry) {
223        mEntry = entry;
224        mStatusBarNotification = entry.notification;
225        mPrivateLayout.onNotificationUpdated(entry);
226        mPublicLayout.onNotificationUpdated(entry);
227        mShowingPublicInitialized = false;
228        updateClearability();
229        if (mIsSummaryWithChildren) {
230            recreateNotificationHeader();
231        }
232        if (mIconAnimationRunning) {
233            setIconAnimationRunning(true);
234        }
235        if (mNotificationParent != null) {
236            mNotificationParent.updateChildrenHeaderAppearance();
237        }
238        onChildrenCountChanged();
239        // The public layouts expand button is always visible
240        mPublicLayout.updateExpandButtons(true);
241        updateLimits();
242    }
243
244    private void updateLimits() {
245        boolean customView = getPrivateLayout().getContractedChild().getId()
246                != com.android.internal.R.id.status_bar_latest_event_content;
247        boolean beforeN = mEntry.targetSdk < Build.VERSION_CODES.N;
248        int minHeight = customView && beforeN && !mIsSummaryWithChildren ?
249                mNotificationMinHeightLegacy : mNotificationMinHeight;
250        boolean headsUpCustom = getPrivateLayout().getHeadsUpChild() != null &&
251                getPrivateLayout().getHeadsUpChild().getId()
252                != com.android.internal.R.id.status_bar_latest_event_content;
253        int headsUpheight = headsUpCustom && beforeN ? mMaxHeadsUpHeightLegacy
254                : mMaxHeadsUpHeight;
255        mPrivateLayout.setHeights(minHeight, headsUpheight, mNotificationMaxHeight);
256        mPublicLayout.setHeights(minHeight, headsUpheight, mNotificationMaxHeight);
257    }
258
259    public StatusBarNotification getStatusBarNotification() {
260        return mStatusBarNotification;
261    }
262
263    public boolean isHeadsUp() {
264        return mIsHeadsUp;
265    }
266
267    public void setHeadsUp(boolean isHeadsUp) {
268        int intrinsicBefore = getIntrinsicHeight();
269        mIsHeadsUp = isHeadsUp;
270        mPrivateLayout.setHeadsUp(isHeadsUp);
271        if (intrinsicBefore != getIntrinsicHeight()) {
272            notifyHeightChanged(false  /* needsAnimation */);
273        }
274    }
275
276    public void setGroupManager(NotificationGroupManager groupManager) {
277        mGroupManager = groupManager;
278        mPrivateLayout.setGroupManager(groupManager);
279    }
280
281    public void setRemoteInputController(RemoteInputController r) {
282        mPrivateLayout.setRemoteInputController(r);
283    }
284
285    public void addChildNotification(ExpandableNotificationRow row) {
286        addChildNotification(row, -1);
287    }
288
289    /**
290     * Add a child notification to this view.
291     *
292     * @param row the row to add
293     * @param childIndex the index to add it at, if -1 it will be added at the end
294     */
295    public void addChildNotification(ExpandableNotificationRow row, int childIndex) {
296        if (mChildrenContainer == null) {
297            mChildrenContainerStub.inflate();
298        }
299        mChildrenContainer.addNotification(row, childIndex);
300        onChildrenCountChanged();
301        row.setIsChildInGroup(true, this);
302    }
303
304    public void removeChildNotification(ExpandableNotificationRow row) {
305        if (mChildrenContainer != null) {
306            mChildrenContainer.removeNotification(row);
307        }
308        mHeaderUtil.restoreNotificationHeader(row);
309        onChildrenCountChanged();
310        row.setIsChildInGroup(false, null);
311    }
312
313    public boolean isChildInGroup() {
314        return mNotificationParent != null;
315    }
316
317    public ExpandableNotificationRow getNotificationParent() {
318        return mNotificationParent;
319    }
320
321    /**
322     * @param isChildInGroup Is this notification now in a group
323     * @param parent the new parent notification
324     */
325    public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) {;
326        boolean childInGroup = BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS && isChildInGroup;
327        mNotificationParent = childInGroup ? parent : null;
328        mPrivateLayout.setIsChildInGroup(childInGroup);
329        updateNoBackgroundState();
330    }
331
332    @Override
333    public boolean onTouchEvent(MotionEvent event) {
334        if (event.getActionMasked() != MotionEvent.ACTION_DOWN
335                || !isChildInGroup() || isGroupExpanded()) {
336            return super.onTouchEvent(event);
337        } else {
338            return false;
339        }
340    }
341
342    @Override
343    protected boolean handleSlideBack() {
344        if (mSettingsIconRow != null && mSettingsIconRow.isVisible()) {
345            animateTranslateNotification(0 /* targetLeft */);
346            return true;
347        }
348        return false;
349    }
350
351    @Override
352    protected boolean shouldHideBackground() {
353        return super.shouldHideBackground() || mShowNoBackground;
354    }
355
356    @Override
357    public boolean isSummaryWithChildren() {
358        return mIsSummaryWithChildren;
359    }
360
361    @Override
362    public boolean areChildrenExpanded() {
363        return mChildrenExpanded;
364    }
365
366    public List<ExpandableNotificationRow> getNotificationChildren() {
367        return mChildrenContainer == null ? null : mChildrenContainer.getNotificationChildren();
368    }
369
370    public int getNumberOfNotificationChildren() {
371        if (mChildrenContainer == null) {
372            return 0;
373        }
374        return mChildrenContainer.getNotificationChildren().size();
375    }
376
377    /**
378     * Apply the order given in the list to the children.
379     *
380     * @param childOrder the new list order
381     * @return whether the list order has changed
382     */
383    public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
384        return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder);
385    }
386
387    public void getChildrenStates(StackScrollState resultState) {
388        if (mIsSummaryWithChildren) {
389            StackViewState parentState = resultState.getViewStateForView(this);
390            mChildrenContainer.getState(resultState, parentState);
391        }
392    }
393
394    public void applyChildrenState(StackScrollState state) {
395        if (mIsSummaryWithChildren) {
396            mChildrenContainer.applyState(state);
397        }
398    }
399
400    public void prepareExpansionChanged(StackScrollState state) {
401        if (mIsSummaryWithChildren) {
402            mChildrenContainer.prepareExpansionChanged(state);
403        }
404    }
405
406    public void startChildAnimation(StackScrollState finalState,
407            StackStateAnimator stateAnimator, long delay, long duration) {
408        if (mIsSummaryWithChildren) {
409            mChildrenContainer.startAnimationToState(finalState, stateAnimator, delay,
410                    duration);
411        }
412    }
413
414    public ExpandableNotificationRow getViewAtPosition(float y) {
415        if (!mIsSummaryWithChildren || !mChildrenExpanded) {
416            return this;
417        } else {
418            ExpandableNotificationRow view = mChildrenContainer.getViewAtPosition(y);
419            return view == null ? this : view;
420        }
421    }
422
423    public NotificationGuts getGuts() {
424        return mGuts;
425    }
426
427    /**
428     * Set this notification to be pinned to the top if {@link #isHeadsUp()} is true. By doing this
429     * the notification will be rendered on top of the screen.
430     *
431     * @param pinned whether it is pinned
432     */
433    public void setPinned(boolean pinned) {
434        mIsPinned = pinned;
435        if (pinned) {
436            setIconAnimationRunning(true);
437            mExpandedWhenPinned = false;
438        } else if (mExpandedWhenPinned) {
439            setUserExpanded(true);
440        }
441        setChronometerRunning(mLastChronometerRunning);
442    }
443
444    public boolean isPinned() {
445        return mIsPinned;
446    }
447
448    /**
449     * @param atLeastMinHeight should the value returned be at least the minimum height.
450     *                         Used to avoid cyclic calls
451     * @return the height of the heads up notification when pinned
452     */
453    public int getPinnedHeadsUpHeight(boolean atLeastMinHeight) {
454        if (mIsSummaryWithChildren) {
455            return mChildrenContainer.getIntrinsicHeight();
456        }
457        if(mExpandedWhenPinned) {
458            return Math.max(getMaxExpandHeight(), mHeadsUpHeight);
459        } else if (atLeastMinHeight) {
460            return Math.max(getMinHeight(), mHeadsUpHeight);
461        } else {
462            return mHeadsUpHeight;
463        }
464    }
465
466    /**
467     * Mark whether this notification was just clicked, i.e. the user has just clicked this
468     * notification in this frame.
469     */
470    public void setJustClicked(boolean justClicked) {
471        mJustClicked = justClicked;
472    }
473
474    /**
475     * @return true if this notification has been clicked in this frame, false otherwise
476     */
477    public boolean wasJustClicked() {
478        return mJustClicked;
479    }
480
481    public void setChronometerRunning(boolean running) {
482        mLastChronometerRunning = running;
483        setChronometerRunning(running, mPrivateLayout);
484        setChronometerRunning(running, mPublicLayout);
485        if (mChildrenContainer != null) {
486            List<ExpandableNotificationRow> notificationChildren =
487                    mChildrenContainer.getNotificationChildren();
488            for (int i = 0; i < notificationChildren.size(); i++) {
489                ExpandableNotificationRow child = notificationChildren.get(i);
490                child.setChronometerRunning(running);
491            }
492        }
493    }
494
495    private void setChronometerRunning(boolean running, NotificationContentView layout) {
496        if (layout != null) {
497            running = running || isPinned();
498            View contractedChild = layout.getContractedChild();
499            View expandedChild = layout.getExpandedChild();
500            View headsUpChild = layout.getHeadsUpChild();
501            setChronometerRunningForChild(running, contractedChild);
502            setChronometerRunningForChild(running, expandedChild);
503            setChronometerRunningForChild(running, headsUpChild);
504        }
505    }
506
507    private void setChronometerRunningForChild(boolean running, View child) {
508        if (child != null) {
509            View chronometer = child.findViewById(com.android.internal.R.id.chronometer);
510            if (chronometer instanceof Chronometer) {
511                ((Chronometer) chronometer).setStarted(running);
512            }
513        }
514    }
515
516    public NotificationHeaderView getNotificationHeader() {
517        if (mNotificationHeader != null) {
518            return mNotificationHeader;
519        }
520        return mPrivateLayout.getNotificationHeader();
521    }
522
523    private NotificationHeaderView getVisibleNotificationHeader() {
524        if (mNotificationHeader != null) {
525            return mNotificationHeader;
526        }
527        return getShowingLayout().getVisibleNotificationHeader();
528    }
529
530    public void setOnExpandClickListener(OnExpandClickListener onExpandClickListener) {
531        mOnExpandClickListener = onExpandClickListener;
532    }
533
534    public void setHeadsUpManager(HeadsUpManager headsUpManager) {
535        mHeadsUpManager = headsUpManager;
536    }
537
538    public void reInflateViews() {
539        initDimens();
540        if (mIsSummaryWithChildren) {
541            removeView(mNotificationHeader);
542            mNotificationHeader = null;
543            recreateNotificationHeader();
544            if (mChildrenContainer != null) {
545                mChildrenContainer.reInflateViews();
546            }
547        }
548        if (mGuts != null) {
549            View oldGuts = mGuts;
550            int index = indexOfChild(oldGuts);
551            removeView(oldGuts);
552            mGuts = (NotificationGuts) LayoutInflater.from(mContext).inflate(
553                    R.layout.notification_guts, this, false);
554            mGuts.setVisibility(oldGuts.getVisibility());
555            addView(mGuts, index);
556        }
557        if (mSettingsIconRow != null) {
558            View oldSettings = mSettingsIconRow;
559            int settingsIndex = indexOfChild(oldSettings);
560            removeView(oldSettings);
561            mSettingsIconRow = (NotificationSettingsIconRow) LayoutInflater.from(mContext).inflate(
562                    R.layout.notification_settings_icon_row, this, false);
563            mSettingsIconRow.setNotificationRowParent(ExpandableNotificationRow.this);
564            mSettingsIconRow.setVisibility(oldSettings.getVisibility());
565            addView(mSettingsIconRow, settingsIndex);
566
567        }
568        mPrivateLayout.reInflateViews();
569        mPublicLayout.reInflateViews();
570    }
571
572    public interface ExpansionLogger {
573        public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
574    }
575
576    public ExpandableNotificationRow(Context context, AttributeSet attrs) {
577        super(context, attrs);
578        mFalsingManager = FalsingManager.getInstance(context);
579        initDimens();
580    }
581
582    private void initDimens() {
583        mNotificationMinHeightLegacy = getFontScaledHeight(R.dimen.notification_min_height_legacy);
584        mNotificationMinHeight = getFontScaledHeight(R.dimen.notification_min_height);
585        mNotificationMaxHeight = getFontScaledHeight(R.dimen.notification_max_height);
586        mMaxHeadsUpHeightLegacy = getFontScaledHeight(
587                R.dimen.notification_max_heads_up_height_legacy);
588        mMaxHeadsUpHeight = getFontScaledHeight(R.dimen.notification_max_heads_up_height);
589    }
590
591    /**
592     * @param dimenId the dimen to look up
593     * @return the font scaled dimen as if it were in sp but doesn't shrink sizes below dp
594     */
595    private int getFontScaledHeight(int dimenId) {
596        int dimensionPixelSize = getResources().getDimensionPixelSize(dimenId);
597        float factor = Math.max(1.0f, getResources().getDisplayMetrics().scaledDensity /
598                getResources().getDisplayMetrics().density);
599        return (int) (dimensionPixelSize * factor);
600    }
601
602    /**
603     * Resets this view so it can be re-used for an updated notification.
604     */
605    @Override
606    public void reset() {
607        super.reset();
608        final boolean wasExpanded = isExpanded();
609        mExpandable = false;
610        mHasUserChangedExpansion = false;
611        mUserLocked = false;
612        mShowingPublic = false;
613        mSensitive = false;
614        mShowingPublicInitialized = false;
615        mIsSystemExpanded = false;
616        mOnKeyguard = false;
617        mPublicLayout.reset(mIsHeadsUp);
618        mPrivateLayout.reset(mIsHeadsUp);
619        resetHeight();
620        resetTranslation();
621        logExpansionEvent(false, wasExpanded);
622    }
623
624    public void resetHeight() {
625        if (mIsHeadsUp) {
626            resetActualHeight();
627        }
628        mMaxExpandHeight = 0;
629        mHeadsUpHeight = 0;
630        mWasReset = true;
631        onHeightReset();
632        requestLayout();
633    }
634
635    @Override
636    protected void onFinishInflate() {
637        super.onFinishInflate();
638        mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic);
639        mPublicLayout.setContainingNotification(this);
640        mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
641        mPrivateLayout.setExpandClickListener(mExpandClickListener);
642        mPrivateLayout.setContainingNotification(this);
643        mPublicLayout.setExpandClickListener(mExpandClickListener);
644        mSettingsIconRowStub = (ViewStub) findViewById(R.id.settings_icon_row_stub);
645        mSettingsIconRowStub.setOnInflateListener(new ViewStub.OnInflateListener() {
646            @Override
647            public void onInflate(ViewStub stub, View inflated) {
648                mSettingsIconRow = (NotificationSettingsIconRow) inflated;
649                mSettingsIconRow.setNotificationRowParent(ExpandableNotificationRow.this);
650            }
651        });
652        mGutsStub = (ViewStub) findViewById(R.id.notification_guts_stub);
653        mGutsStub.setOnInflateListener(new ViewStub.OnInflateListener() {
654            @Override
655            public void onInflate(ViewStub stub, View inflated) {
656                mGuts = (NotificationGuts) inflated;
657                mGuts.setClipTopAmount(getClipTopAmount());
658                mGuts.setActualHeight(getActualHeight());
659                mTranslateableViews.add(mGuts);
660                mGutsStub = null;
661            }
662        });
663        mChildrenContainerStub = (ViewStub) findViewById(R.id.child_container_stub);
664        mChildrenContainerStub.setOnInflateListener(new ViewStub.OnInflateListener() {
665
666            @Override
667            public void onInflate(ViewStub stub, View inflated) {
668                mChildrenContainer = (NotificationChildrenContainer) inflated;
669                mChildrenContainer.setNotificationParent(ExpandableNotificationRow.this);
670                mTranslateableViews.add(mChildrenContainer);
671            }
672        });
673        mVetoButton = findViewById(R.id.veto);
674
675        // Add the views that we translate to reveal the gear
676        mTranslateableViews = new ArrayList<View>();
677        for (int i = 0; i < getChildCount(); i++) {
678            mTranslateableViews.add(getChildAt(i));
679        }
680        // Remove views that don't translate
681        mTranslateableViews.remove(mVetoButton);
682        mTranslateableViews.remove(mSettingsIconRowStub);
683        mTranslateableViews.remove(mChildrenContainerStub);
684        mTranslateableViews.remove(mGutsStub);
685    }
686
687    public void setTranslationForOutline(float translationX) {
688        setOutlineRect(false, translationX, getTop(), getRight() + translationX, getBottom());
689    }
690
691    public void resetTranslation() {
692        if (mTranslateableViews != null) {
693            for (int i = 0; i < mTranslateableViews.size(); i++) {
694                mTranslateableViews.get(i).setTranslationX(0);
695            }
696            setTranslationForOutline(0);
697        }
698        if (mSettingsIconRow != null) {
699            mSettingsIconRow.resetState();
700        }
701    }
702
703    public void animateTranslateNotification(final float leftTarget) {
704        if (mTranslateAnim != null) {
705            mTranslateAnim.cancel();
706        }
707        AnimatorSet set = new AnimatorSet();
708        if (mTranslateableViews != null) {
709            for (int i = 0; i < mTranslateableViews.size(); i++) {
710                final View animView = mTranslateableViews.get(i);
711                final ObjectAnimator translateAnim = ObjectAnimator.ofFloat(
712                        animView, "translationX", leftTarget);
713                if (i == 0) {
714                    translateAnim.addUpdateListener(new AnimatorUpdateListener() {
715                        @Override
716                        public void onAnimationUpdate(ValueAnimator animation) {
717                            setTranslationForOutline((float) animation.getAnimatedValue());
718                        }
719                    });
720                }
721                translateAnim.addListener(new AnimatorListenerAdapter() {
722                    @Override
723                    public void onAnimationEnd(Animator anim) {
724                        if (mSettingsIconRow != null && leftTarget == 0) {
725                            mSettingsIconRow.resetState();
726                        }
727                        mTranslateAnim = null;
728                    }
729                });
730                set.play(translateAnim);
731            }
732        }
733        mTranslateAnim = set;
734        set.start();
735    }
736
737    public float getSpaceForGear() {
738        if (mSettingsIconRow != null) {
739            return mSettingsIconRow.getSpaceForGear();
740        }
741        return 0;
742    }
743
744    public NotificationSettingsIconRow getSettingsRow() {
745        if (mSettingsIconRow == null) {
746            mSettingsIconRowStub.inflate();
747        }
748        return mSettingsIconRow;
749    }
750
751    public ArrayList<View> getContentViews() {
752        return mTranslateableViews;
753    }
754
755    public void inflateGuts() {
756        if (mGuts == null) {
757            mGutsStub.inflate();
758        }
759    }
760
761    private void updateChildrenVisibility() {
762        mPrivateLayout.setVisibility(!mShowingPublic && !mIsSummaryWithChildren ? VISIBLE
763                : INVISIBLE);
764        if (mChildrenContainer != null) {
765            mChildrenContainer.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE
766                    : INVISIBLE);
767        }
768        if (mNotificationHeader != null) {
769            mNotificationHeader.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE
770                    : INVISIBLE);
771        }
772        // The limits might have changed if the view suddenly became a group or vice versa
773        updateLimits();
774    }
775
776    @Override
777    public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
778        if (super.onRequestSendAccessibilityEventInternal(child, event)) {
779            // Add a record for the entire layout since its content is somehow small.
780            // The event comes from a leaf view that is interacted with.
781            AccessibilityEvent record = AccessibilityEvent.obtain();
782            onInitializeAccessibilityEvent(record);
783            dispatchPopulateAccessibilityEvent(record);
784            event.appendRecord(record);
785            return true;
786        }
787        return false;
788    }
789
790    @Override
791    public void setDark(boolean dark, boolean fade, long delay) {
792        super.setDark(dark, fade, delay);
793        final NotificationContentView showing = getShowingLayout();
794        if (showing != null) {
795            showing.setDark(dark, fade, delay);
796        }
797        if (mIsSummaryWithChildren) {
798            mChildrenContainer.setDark(dark, fade, delay);
799            mNotificationHeaderWrapper.setDark(dark, fade, delay);
800        }
801    }
802
803    public boolean isExpandable() {
804        if (mIsSummaryWithChildren && !mShowingPublic) {
805            return !mChildrenExpanded;
806        }
807        return mExpandable;
808    }
809
810    public void setExpandable(boolean expandable) {
811        mExpandable = expandable;
812        mPrivateLayout.updateExpandButtons(isExpandable());
813    }
814
815    @Override
816    public void setClipToActualHeight(boolean clipToActualHeight) {
817        super.setClipToActualHeight(clipToActualHeight || isUserLocked());
818        getShowingLayout().setClipToActualHeight(clipToActualHeight || isUserLocked());
819    }
820
821    /**
822     * @return whether the user has changed the expansion state
823     */
824    public boolean hasUserChangedExpansion() {
825        return mHasUserChangedExpansion;
826    }
827
828    public boolean isUserExpanded() {
829        return mUserExpanded;
830    }
831
832    /**
833     * Set this notification to be expanded by the user
834     *
835     * @param userExpanded whether the user wants this notification to be expanded
836     */
837    public void setUserExpanded(boolean userExpanded) {
838        setUserExpanded(userExpanded, false /* allowChildExpansion */);
839    }
840
841    /**
842     * Set this notification to be expanded by the user
843     *
844     * @param userExpanded whether the user wants this notification to be expanded
845     * @param allowChildExpansion whether a call to this method allows expanding children
846     */
847    public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
848        mFalsingManager.setNotificationExpanded();
849        if (mIsSummaryWithChildren && !mShowingPublic && allowChildExpansion) {
850            mGroupManager.setGroupExpanded(mStatusBarNotification, userExpanded);
851            return;
852        }
853        if (userExpanded && !mExpandable) return;
854        final boolean wasExpanded = isExpanded();
855        mHasUserChangedExpansion = true;
856        mUserExpanded = userExpanded;
857        logExpansionEvent(true, wasExpanded);
858    }
859
860    public void resetUserExpansion() {
861        mHasUserChangedExpansion = false;
862        mUserExpanded = false;
863    }
864
865    public boolean isUserLocked() {
866        return mUserLocked;
867    }
868
869    public void setUserLocked(boolean userLocked) {
870        mUserLocked = userLocked;
871    }
872
873    /**
874     * @return has the system set this notification to be expanded
875     */
876    public boolean isSystemExpanded() {
877        return mIsSystemExpanded;
878    }
879
880    /**
881     * Set this notification to be expanded by the system.
882     *
883     * @param expand whether the system wants this notification to be expanded.
884     */
885    public void setSystemExpanded(boolean expand) {
886        if (expand != mIsSystemExpanded) {
887            final boolean wasExpanded = isExpanded();
888            mIsSystemExpanded = expand;
889            notifyHeightChanged(false /* needsAnimation */);
890            logExpansionEvent(false, wasExpanded);
891            if (mChildrenContainer != null) {
892                mChildrenContainer.updateGroupOverflow();
893            }
894        }
895    }
896
897    /**
898     * @param onKeyguard whether to prevent notification expansion
899     */
900    public void setOnKeyguard(boolean onKeyguard) {
901        if (onKeyguard != mOnKeyguard) {
902            final boolean wasExpanded = isExpanded();
903            mOnKeyguard = onKeyguard;
904            logExpansionEvent(false, wasExpanded);
905            if (wasExpanded != isExpanded()) {
906                if (mIsSummaryWithChildren) {
907                    mChildrenContainer.updateGroupOverflow();
908                }
909                notifyHeightChanged(false /* needsAnimation */);
910            }
911        }
912    }
913
914    /**
915     * @return Can the underlying notification be cleared?
916     */
917    public boolean isClearable() {
918        return mStatusBarNotification != null && mStatusBarNotification.isClearable();
919    }
920
921    /**
922     * Apply an expansion state to the layout.
923     */
924    public void applyExpansionToLayout() {
925        boolean expand = isExpanded();
926        if (expand && mExpandable) {
927            setActualHeight(mMaxExpandHeight);
928        } else {
929            setActualHeight(getMinHeight());
930        }
931    }
932
933    @Override
934    public int getIntrinsicHeight() {
935        if (isUserLocked()) {
936            return getActualHeight();
937        }
938        if (mGuts != null && mGuts.areGutsExposed()) {
939            return mGuts.getHeight();
940        } else if ((isChildInGroup() && !isGroupExpanded())) {
941            return mPrivateLayout.getMinHeight();
942        } else if (mSensitive && mHideSensitiveForIntrinsicHeight) {
943            return getMinHeight();
944        } else if (mIsSummaryWithChildren && !mOnKeyguard) {
945            return mChildrenContainer.getIntrinsicHeight();
946        } else if (mIsHeadsUp) {
947            if (isPinned()) {
948                return getPinnedHeadsUpHeight(true /* atLeastMinHeight */);
949            } else if (isExpanded()) {
950                return Math.max(getMaxExpandHeight(), mHeadsUpHeight);
951            } else {
952                return Math.max(getMinHeight(), mHeadsUpHeight);
953            }
954        } else if (isExpanded()) {
955            return getMaxExpandHeight();
956        } else {
957            return getMinHeight();
958        }
959    }
960
961    private boolean isGroupExpanded() {
962        return mGroupManager.isGroupExpanded(mStatusBarNotification);
963    }
964
965    /**
966     * @return whether this view has a header on the top of the content
967     */
968    private boolean hasNotificationHeader() {
969        return mIsSummaryWithChildren;
970    }
971
972    private void onChildrenCountChanged() {
973        mIsSummaryWithChildren = BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS
974                && mGroupManager.hasGroupChildren(mStatusBarNotification);
975        if (mIsSummaryWithChildren) {
976            if (mChildrenContainer == null) {
977                mChildrenContainerStub.inflate();
978            }
979            if (mNotificationHeader == null) {
980                recreateNotificationHeader();
981            }
982        }
983        mPrivateLayout.updateExpandButtons(isExpandable());
984        updateChildrenHeaderAppearance();
985        updateHeaderChildCount();
986        updateChildrenVisibility();
987    }
988
989    /**
990     * Check whether the view state is currently expanded. This is given by the system in {@link
991     * #setSystemExpanded(boolean)} and can be overridden by user expansion or
992     * collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this
993     * view can differ from this state, if layout params are modified from outside.
994     *
995     * @return whether the view state is currently expanded.
996     */
997    public boolean isExpanded() {
998        return !mOnKeyguard
999                && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded())
1000                || isUserExpanded());
1001    }
1002
1003    private boolean isSystemChildExpanded() {
1004        return mIsSystemChildExpanded;
1005    }
1006
1007    public void setSystemChildExpanded(boolean expanded) {
1008        mIsSystemChildExpanded = expanded;
1009    }
1010
1011    @Override
1012    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1013        super.onLayout(changed, left, top, right, bottom);
1014        boolean updateExpandHeight = mMaxExpandHeight == 0 && !mWasReset;
1015        updateMaxHeights();
1016        if (updateExpandHeight) {
1017            applyExpansionToLayout();
1018        }
1019        mWasReset = false;
1020    }
1021
1022    private void updateMaxHeights() {
1023        int intrinsicBefore = getIntrinsicHeight();
1024        View expandedChild = mPrivateLayout.getExpandedChild();
1025        if (expandedChild == null) {
1026            expandedChild = mPrivateLayout.getContractedChild();
1027        }
1028        mMaxExpandHeight = expandedChild.getHeight();
1029        View headsUpChild = mPrivateLayout.getHeadsUpChild();
1030        if (headsUpChild == null) {
1031            headsUpChild = mPrivateLayout.getContractedChild();
1032        }
1033        mHeadsUpHeight = headsUpChild.getHeight();
1034        if (intrinsicBefore != getIntrinsicHeight()) {
1035            notifyHeightChanged(false  /* needsAnimation */);
1036        }
1037    }
1038
1039    @Override
1040    public void notifyHeightChanged(boolean needsAnimation) {
1041        super.notifyHeightChanged(needsAnimation);
1042        getShowingLayout().requestSelectLayout(needsAnimation || isUserLocked());
1043    }
1044
1045    public void setSensitive(boolean sensitive, boolean hideSensitive) {
1046        mSensitive = sensitive;
1047        mSensitiveHiddenInGeneral = hideSensitive;
1048    }
1049
1050    public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
1051        mHideSensitiveForIntrinsicHeight = hideSensitive;
1052    }
1053
1054    public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
1055            long duration) {
1056        boolean oldShowingPublic = mShowingPublic;
1057        mShowingPublic = mSensitive && hideSensitive;
1058        if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) {
1059            return;
1060        }
1061
1062        // bail out if no public version
1063        if (mPublicLayout.getChildCount() == 0) return;
1064
1065        if (!animated) {
1066            mPublicLayout.animate().cancel();
1067            mPrivateLayout.animate().cancel();
1068            mPublicLayout.setAlpha(1f);
1069            mPrivateLayout.setAlpha(1f);
1070            mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE);
1071            updateChildrenVisibility();
1072        } else {
1073            animateShowingPublic(delay, duration);
1074        }
1075
1076        mPrivateLayout.updateExpandButtons(isExpandable());
1077        updateClearability();
1078        mShowingPublicInitialized = true;
1079    }
1080
1081    private void animateShowingPublic(long delay, long duration) {
1082        View[] privateViews = mIsSummaryWithChildren ?
1083                new View[] {mChildrenContainer, mNotificationHeader}
1084                : new View[] {mPrivateLayout};
1085        View[] publicViews = new View[] {mPublicLayout};
1086        View[] hiddenChildren = mShowingPublic ? privateViews : publicViews;
1087        View[] shownChildren = mShowingPublic ? publicViews : privateViews;
1088        for (final View hiddenView : hiddenChildren) {
1089            hiddenView.setVisibility(View.VISIBLE);
1090            hiddenView.animate().cancel();
1091            hiddenView.animate()
1092                    .alpha(0f)
1093                    .setStartDelay(delay)
1094                    .setDuration(duration)
1095                    .withEndAction(new Runnable() {
1096                        @Override
1097                        public void run() {
1098                            hiddenView.setVisibility(View.INVISIBLE);
1099                        }
1100                    });
1101        }
1102        for (View showView : shownChildren) {
1103            showView.setVisibility(View.VISIBLE);
1104            showView.setAlpha(0f);
1105            showView.animate().cancel();
1106            showView.animate()
1107                    .alpha(1f)
1108                    .setStartDelay(delay)
1109                    .setDuration(duration);
1110        }
1111    }
1112
1113    public boolean mustStayOnScreen() {
1114        return mIsHeadsUp;
1115    }
1116
1117    private void updateClearability() {
1118        // public versions cannot be dismissed
1119        mVetoButton.setVisibility(isClearable() && (!mShowingPublic
1120                || !mSensitiveHiddenInGeneral) ? View.VISIBLE : View.GONE);
1121    }
1122
1123    public void setChildrenExpanded(boolean expanded, boolean animate) {
1124        mChildrenExpanded = expanded;
1125        if (mNotificationHeader != null) {
1126            mNotificationHeader.setExpanded(expanded);
1127        }
1128        if (mChildrenContainer != null) {
1129            mChildrenContainer.setChildrenExpanded(expanded);
1130        }
1131    }
1132
1133    public void updateHeaderChildCount() {
1134        if (mIsSummaryWithChildren) {
1135            mNotificationHeader.setChildCount(
1136                    mChildrenContainer.getNotificationChildren().size());
1137        }
1138    }
1139
1140    public static void applyTint(View v, int color) {
1141        int alpha;
1142        if (color != 0) {
1143            alpha = COLORED_DIVIDER_ALPHA;
1144        } else {
1145            color = 0xff000000;
1146            alpha = DEFAULT_DIVIDER_ALPHA;
1147        }
1148        if (v.getBackground() instanceof ColorDrawable) {
1149            ColorDrawable background = (ColorDrawable) v.getBackground();
1150            background.mutate();
1151            background.setColor(color);
1152            background.setAlpha(alpha);
1153        }
1154    }
1155
1156    public int getMaxExpandHeight() {
1157        return mMaxExpandHeight;
1158    }
1159
1160    @Override
1161    public boolean isContentExpandable() {
1162        NotificationContentView showingLayout = getShowingLayout();
1163        return showingLayout.isContentExpandable();
1164    }
1165
1166    @Override
1167    protected View getContentView() {
1168        return getShowingLayout();
1169    }
1170
1171    @Override
1172    public void setActualHeight(int height, boolean notifyListeners) {
1173        super.setActualHeight(height, notifyListeners);
1174        int contentHeight = Math.max(getMinHeight(), height);
1175        mPrivateLayout.setContentHeight(contentHeight);
1176        mPublicLayout.setContentHeight(contentHeight);
1177        if (mGuts != null) {
1178            mGuts.setActualHeight(height);
1179        }
1180        invalidate();
1181    }
1182
1183    @Override
1184    public int getMaxContentHeight() {
1185        if (mIsSummaryWithChildren && !mShowingPublic) {
1186            return mChildrenContainer.getMaxContentHeight();
1187        }
1188        NotificationContentView showingLayout = getShowingLayout();
1189        return showingLayout.getMaxHeight();
1190    }
1191
1192    @Override
1193    public int getMinHeight() {
1194        if (mIsHeadsUp && mHeadsUpManager.isTrackingHeadsUp()) {
1195                return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
1196        } else if (mIsSummaryWithChildren && !isGroupExpanded() && !mShowingPublic) {
1197            return mChildrenContainer.getMinHeight();
1198        } else if (mIsHeadsUp) {
1199            return mHeadsUpHeight;
1200        }
1201        NotificationContentView showingLayout = getShowingLayout();
1202        return showingLayout.getMinHeight();
1203    }
1204
1205    @Override
1206    public int getMinExpandHeight() {
1207        if (mIsSummaryWithChildren && !mOnKeyguard) {
1208            return mChildrenContainer.getMinExpandHeight();
1209        }
1210        return getMinHeight();
1211    }
1212
1213    @Override
1214    public void setClipTopAmount(int clipTopAmount) {
1215        super.setClipTopAmount(clipTopAmount);
1216        mPrivateLayout.setClipTopAmount(clipTopAmount);
1217        mPublicLayout.setClipTopAmount(clipTopAmount);
1218        if (mGuts != null) {
1219            mGuts.setClipTopAmount(clipTopAmount);
1220        }
1221    }
1222
1223    private void recreateNotificationHeader() {
1224        final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(),
1225                getStatusBarNotification().getNotification());
1226        final RemoteViews header = builder.makeNotificationHeader();
1227        if (mNotificationHeader == null) {
1228            mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
1229            final View expandButton = mNotificationHeader.findViewById(
1230                    com.android.internal.R.id.expand_button);
1231            expandButton.setVisibility(VISIBLE);
1232            mNotificationHeader.setOnClickListener(mExpandClickListener);
1233            mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
1234                    mNotificationHeader);
1235            addView(mNotificationHeader, indexOfChild(mChildrenContainer) + 1);
1236            mTranslateableViews.add(mNotificationHeader);
1237        } else {
1238            header.reapply(getContext(), mNotificationHeader);
1239            mNotificationHeaderWrapper.notifyContentUpdated(mEntry.notification);
1240        }
1241        updateHeaderExpandButton();
1242        updateChildrenHeaderAppearance();
1243        updateHeaderChildCount();
1244    }
1245
1246    private void updateHeaderExpandButton() {
1247        if (mIsSummaryWithChildren) {
1248            mNotificationHeader.setIsGroupHeader(true /* isGroupHeader*/);
1249        }
1250    }
1251
1252    public void updateChildrenHeaderAppearance() {
1253        if (mIsSummaryWithChildren) {
1254            mHeaderUtil.updateChildrenHeaderAppearance();
1255        }
1256    }
1257
1258    public boolean isMaxExpandHeightInitialized() {
1259        return mMaxExpandHeight != 0;
1260    }
1261
1262    private NotificationContentView getShowingLayout() {
1263        return mShowingPublic ? mPublicLayout : mPrivateLayout;
1264    }
1265
1266    @Override
1267    public void setShowingLegacyBackground(boolean showing) {
1268        super.setShowingLegacyBackground(showing);
1269        mPrivateLayout.setShowingLegacyBackground(showing);
1270        mPublicLayout.setShowingLegacyBackground(showing);
1271    }
1272
1273    @Override
1274    protected void updateBackgroundTint() {
1275        super.updateBackgroundTint();
1276        updateNoBackgroundState();
1277        if (mIsSummaryWithChildren) {
1278            List<ExpandableNotificationRow> notificationChildren =
1279                    mChildrenContainer.getNotificationChildren();
1280            for (int i = 0; i < notificationChildren.size(); i++) {
1281                ExpandableNotificationRow child = notificationChildren.get(i);
1282                child.updateNoBackgroundState();
1283            }
1284        }
1285    }
1286
1287    private void updateNoBackgroundState() {
1288        mShowNoBackground = isChildInGroup() && hasSameBgColor(mNotificationParent);
1289        updateBackground();
1290    }
1291
1292    public void setExpansionLogger(ExpansionLogger logger, String key) {
1293        mLogger = logger;
1294        mLoggingKey = key;
1295    }
1296
1297    @Override
1298    public boolean needsIncreasedPadding() {
1299        return mIsSummaryWithChildren && isGroupExpanded();
1300    }
1301
1302    @Override
1303    protected boolean disallowSingleClick(MotionEvent event) {
1304        float x = event.getX();
1305        float y = event.getY();
1306        NotificationHeaderView header = getVisibleNotificationHeader();
1307        if (header != null) {
1308            return header.isInTouchRect(x, y);
1309        }
1310        return super.disallowSingleClick(event);
1311    }
1312
1313    private void logExpansionEvent(boolean userAction, boolean wasExpanded) {
1314        final boolean nowExpanded = isExpanded();
1315        if (wasExpanded != nowExpanded && mLogger != null) {
1316            mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded) ;
1317        }
1318    }
1319
1320    public interface OnExpandClickListener {
1321        void onExpandClicked(NotificationData.Entry clickedEntry, boolean nowExpanded);
1322    }
1323}
1324