1/*
2 * Copyright (C) 2015 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.stack;
18
19import android.app.Notification;
20import android.content.Context;
21import android.content.res.Configuration;
22import android.graphics.drawable.ColorDrawable;
23import android.service.notification.StatusBarNotification;
24import android.util.AttributeSet;
25import android.view.LayoutInflater;
26import android.view.NotificationHeaderView;
27import android.view.View;
28import android.view.ViewGroup;
29import android.widget.RemoteViews;
30import android.widget.TextView;
31
32import com.android.systemui.R;
33import com.android.systemui.ViewInvertHelper;
34import com.android.systemui.statusbar.CrossFadeHelper;
35import com.android.systemui.statusbar.ExpandableNotificationRow;
36import com.android.systemui.statusbar.NotificationHeaderUtil;
37import com.android.systemui.statusbar.notification.HybridGroupManager;
38import com.android.systemui.statusbar.notification.HybridNotificationView;
39import com.android.systemui.statusbar.notification.NotificationUtils;
40import com.android.systemui.statusbar.notification.NotificationViewWrapper;
41import com.android.systemui.statusbar.phone.NotificationPanelView;
42
43import java.util.ArrayList;
44import java.util.List;
45
46/**
47 * A container containing child notifications
48 */
49public class NotificationChildrenContainer extends ViewGroup {
50
51    private static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2;
52    private static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5;
53    private static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8;
54
55    private final List<View> mDividers = new ArrayList<>();
56    private final List<ExpandableNotificationRow> mChildren = new ArrayList<>();
57    private final HybridGroupManager mHybridGroupManager;
58    private int mChildPadding;
59    private int mDividerHeight;
60    private int mMaxNotificationHeight;
61    private int mNotificationHeaderMargin;
62    private int mNotificatonTopPadding;
63    private float mCollapsedBottompadding;
64    private ViewInvertHelper mOverflowInvertHelper;
65    private boolean mChildrenExpanded;
66    private ExpandableNotificationRow mNotificationParent;
67    private TextView mOverflowNumber;
68    private ViewState mGroupOverFlowState;
69    private int mRealHeight;
70    private boolean mUserLocked;
71    private int mActualHeight;
72    private boolean mNeverAppliedGroupState;
73    private int mHeaderHeight;
74
75    private NotificationHeaderView mNotificationHeader;
76    private NotificationViewWrapper mNotificationHeaderWrapper;
77    private NotificationHeaderUtil mHeaderUtil;
78    private ViewState mHeaderViewState;
79
80    public NotificationChildrenContainer(Context context) {
81        this(context, null);
82    }
83
84    public NotificationChildrenContainer(Context context, AttributeSet attrs) {
85        this(context, attrs, 0);
86    }
87
88    public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) {
89        this(context, attrs, defStyleAttr, 0);
90    }
91
92    public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
93            int defStyleRes) {
94        super(context, attrs, defStyleAttr, defStyleRes);
95        initDimens();
96        mHybridGroupManager = new HybridGroupManager(getContext(), this);
97    }
98
99    private void initDimens() {
100        mChildPadding = getResources().getDimensionPixelSize(
101                R.dimen.notification_children_padding);
102        mDividerHeight = Math.max(1, getResources().getDimensionPixelSize(
103                R.dimen.notification_divider_height));
104        mHeaderHeight = getResources().getDimensionPixelSize(R.dimen.notification_header_height);
105        mMaxNotificationHeight = getResources().getDimensionPixelSize(
106                R.dimen.notification_max_height);
107        mNotificationHeaderMargin = getResources().getDimensionPixelSize(
108                com.android.internal.R.dimen.notification_content_margin_top);
109        mNotificatonTopPadding = getResources().getDimensionPixelSize(
110                R.dimen.notification_children_container_top_padding);
111        mCollapsedBottompadding = getResources().getDimensionPixelSize(
112                com.android.internal.R.dimen.notification_content_margin_bottom);
113    }
114
115    @Override
116    protected void onLayout(boolean changed, int l, int t, int r, int b) {
117        int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
118        for (int i = 0; i < childCount; i++) {
119            View child = mChildren.get(i);
120            // We need to layout all children even the GONE ones, such that the heights are
121            // calculated correctly as they are used to calculate how many we can fit on the screen
122            child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
123            mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
124        }
125        if (mOverflowNumber != null) {
126            mOverflowNumber.layout(getWidth() - mOverflowNumber.getMeasuredWidth(), 0, getWidth(),
127                    mOverflowNumber.getMeasuredHeight());
128        }
129        if (mNotificationHeader != null) {
130            mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(),
131                    mNotificationHeader.getMeasuredHeight());
132        }
133    }
134
135    @Override
136    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
137        int ownMaxHeight = mMaxNotificationHeight;
138        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
139        boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
140        boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
141        int size = MeasureSpec.getSize(heightMeasureSpec);
142        if (hasFixedHeight || isHeightLimited) {
143            ownMaxHeight = Math.min(ownMaxHeight, size);
144        }
145        int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
146        int width = MeasureSpec.getSize(widthMeasureSpec);
147        if (mOverflowNumber != null) {
148            mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
149                    newHeightSpec);
150        }
151        int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
152        int height = mNotificationHeaderMargin + mNotificatonTopPadding;
153        int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
154        int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
155        int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1;
156        for (int i = 0; i < childCount; i++) {
157            ExpandableNotificationRow child = mChildren.get(i);
158            // We need to measure all children even the GONE ones, such that the heights are
159            // calculated correctly as they are used to calculate how many we can fit on the screen.
160            boolean isOverflow = i == overflowIndex;
161            child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null
162                    ? mOverflowNumber.getMeasuredWidth()
163                    : 0);
164            child.measure(widthMeasureSpec, newHeightSpec);
165            // layout the divider
166            View divider = mDividers.get(i);
167            divider.measure(widthMeasureSpec, dividerHeightSpec);
168            if (child.getVisibility() != GONE) {
169                height += child.getMeasuredHeight() + mDividerHeight;
170            }
171        }
172        mRealHeight = height;
173        if (heightMode != MeasureSpec.UNSPECIFIED) {
174            height = Math.min(height, size);
175        }
176
177        if (mNotificationHeader != null) {
178            int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
179            mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec);
180        }
181
182        setMeasuredDimension(width, height);
183    }
184
185    @Override
186    public boolean pointInView(float localX, float localY, float slop) {
187        return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
188                localY < (mRealHeight + slop);
189    }
190
191    /**
192     * Add a child notification to this view.
193     *
194     * @param row the row to add
195     * @param childIndex the index to add it at, if -1 it will be added at the end
196     */
197    public void addNotification(ExpandableNotificationRow row, int childIndex) {
198        int newIndex = childIndex < 0 ? mChildren.size() : childIndex;
199        mChildren.add(newIndex, row);
200        addView(row);
201        row.setUserLocked(mUserLocked);
202
203        View divider = inflateDivider();
204        addView(divider);
205        mDividers.add(newIndex, divider);
206
207        updateGroupOverflow();
208    }
209
210    public void removeNotification(ExpandableNotificationRow row) {
211        int childIndex = mChildren.indexOf(row);
212        mChildren.remove(row);
213        removeView(row);
214
215        final View divider = mDividers.remove(childIndex);
216        removeView(divider);
217        getOverlay().add(divider);
218        CrossFadeHelper.fadeOut(divider, new Runnable() {
219            @Override
220            public void run() {
221                getOverlay().remove(divider);
222            }
223        });
224
225        row.setSystemChildExpanded(false);
226        row.setUserLocked(false);
227        updateGroupOverflow();
228        if (!row.isRemoved()) {
229            mHeaderUtil.restoreNotificationHeader(row);
230        }
231    }
232
233    /**
234     * @return The number of notification children in the container.
235     */
236    public int getNotificationChildCount() {
237        return mChildren.size();
238    }
239
240    public void recreateNotificationHeader(OnClickListener listener, StatusBarNotification notification) {
241        final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(),
242                mNotificationParent.getStatusBarNotification().getNotification());
243        final RemoteViews header = builder.makeNotificationHeader();
244        if (mNotificationHeader == null) {
245            mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
246            final View expandButton = mNotificationHeader.findViewById(
247                    com.android.internal.R.id.expand_button);
248            expandButton.setVisibility(VISIBLE);
249            mNotificationHeader.setOnClickListener(listener);
250            mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
251                    mNotificationHeader, mNotificationParent);
252            addView(mNotificationHeader, 0);
253            invalidate();
254        } else {
255            header.reapply(getContext(), mNotificationHeader);
256            mNotificationHeaderWrapper.notifyContentUpdated(notification);
257        }
258        updateChildrenHeaderAppearance();
259    }
260
261    public void updateChildrenHeaderAppearance() {
262        mHeaderUtil.updateChildrenHeaderAppearance();
263    }
264
265    public void updateGroupOverflow() {
266        int childCount = mChildren.size();
267        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
268        if (childCount > maxAllowedVisibleChildren) {
269            mOverflowNumber = mHybridGroupManager.bindOverflowNumber(
270                    mOverflowNumber, childCount - maxAllowedVisibleChildren);
271            if (mOverflowInvertHelper == null) {
272                mOverflowInvertHelper = new ViewInvertHelper(mOverflowNumber,
273                        NotificationPanelView.DOZE_ANIMATION_DURATION);
274            }
275            if (mGroupOverFlowState == null) {
276                mGroupOverFlowState = new ViewState();
277                mNeverAppliedGroupState = true;
278            }
279        } else if (mOverflowNumber != null) {
280            removeView(mOverflowNumber);
281            if (isShown()) {
282                final View removedOverflowNumber = mOverflowNumber;
283                addTransientView(removedOverflowNumber, getTransientViewCount());
284                CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() {
285                    @Override
286                    public void run() {
287                        removeTransientView(removedOverflowNumber);
288                    }
289                });
290            }
291            mOverflowNumber = null;
292            mOverflowInvertHelper = null;
293            mGroupOverFlowState = null;
294        }
295    }
296
297    @Override
298    protected void onConfigurationChanged(Configuration newConfig) {
299        super.onConfigurationChanged(newConfig);
300        updateGroupOverflow();
301    }
302
303    private View inflateDivider() {
304        return LayoutInflater.from(mContext).inflate(
305                R.layout.notification_children_divider, this, false);
306    }
307
308    public List<ExpandableNotificationRow> getNotificationChildren() {
309        return mChildren;
310    }
311
312    /**
313     * Apply the order given in the list to the children.
314     *
315     * @param childOrder the new list order
316     * @return whether the list order has changed
317     */
318    public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
319        if (childOrder == null) {
320            return false;
321        }
322        boolean result = false;
323        for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) {
324            ExpandableNotificationRow child = mChildren.get(i);
325            ExpandableNotificationRow desiredChild = childOrder.get(i);
326            if (child != desiredChild) {
327                mChildren.remove(desiredChild);
328                mChildren.add(i, desiredChild);
329                result = true;
330            }
331        }
332        updateExpansionStates();
333        return result;
334    }
335
336    private void updateExpansionStates() {
337        if (mChildrenExpanded || mUserLocked) {
338            // we don't modify it the group is expanded or if we are expanding it
339            return;
340        }
341        int size = mChildren.size();
342        for (int i = 0; i < size; i++) {
343            ExpandableNotificationRow child = mChildren.get(i);
344            child.setSystemChildExpanded(i == 0 && size == 1);
345        }
346    }
347
348    /**
349     *
350     * @return the intrinsic size of this children container, i.e the natural fully expanded state
351     */
352    public int getIntrinsicHeight() {
353        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
354        return getIntrinsicHeight(maxAllowedVisibleChildren);
355    }
356
357    /**
358     * @return the intrinsic height with a number of children given
359     *         in @param maxAllowedVisibleChildren
360     */
361    private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
362        int intrinsicHeight = mNotificationHeaderMargin;
363        int visibleChildren = 0;
364        int childCount = mChildren.size();
365        boolean firstChild = true;
366        float expandFactor = 0;
367        if (mUserLocked) {
368            expandFactor = getGroupExpandFraction();
369        }
370        for (int i = 0; i < childCount; i++) {
371            if (visibleChildren >= maxAllowedVisibleChildren) {
372                break;
373            }
374            if (!firstChild) {
375                if (mUserLocked) {
376                    intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
377                            expandFactor);
378                } else {
379                    intrinsicHeight += mChildrenExpanded ? mDividerHeight : mChildPadding;
380                }
381            } else {
382                if (mUserLocked) {
383                    intrinsicHeight += NotificationUtils.interpolate(
384                            0,
385                            mNotificatonTopPadding + mDividerHeight,
386                            expandFactor);
387                } else {
388                    intrinsicHeight += mChildrenExpanded
389                            ? mNotificatonTopPadding + mDividerHeight
390                            : 0;
391                }
392                firstChild = false;
393            }
394            ExpandableNotificationRow child = mChildren.get(i);
395            intrinsicHeight += child.getIntrinsicHeight();
396            visibleChildren++;
397        }
398        if (mUserLocked) {
399            intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottompadding, 0.0f,
400                    expandFactor);
401        } else if (!mChildrenExpanded) {
402            intrinsicHeight += mCollapsedBottompadding;
403        }
404        return intrinsicHeight;
405    }
406
407    /**
408     * Update the state of all its children based on a linear layout algorithm.
409     *
410     * @param resultState the state to update
411     * @param parentState the state of the parent
412     */
413    public void getState(StackScrollState resultState, StackViewState parentState) {
414        int childCount = mChildren.size();
415        int yPosition = mNotificationHeaderMargin;
416        boolean firstChild = true;
417        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
418        int lastVisibleIndex = maxAllowedVisibleChildren - 1;
419        int firstOverflowIndex = lastVisibleIndex + 1;
420        float expandFactor = 0;
421        if (mUserLocked) {
422            expandFactor = getGroupExpandFraction();
423            firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
424        }
425
426        boolean childrenExpanded = !mNotificationParent.isGroupExpansionChanging()
427                && mChildrenExpanded;
428        int parentHeight = parentState.height;
429        for (int i = 0; i < childCount; i++) {
430            ExpandableNotificationRow child = mChildren.get(i);
431            if (!firstChild) {
432                if (mUserLocked) {
433                    yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
434                            expandFactor);
435                } else {
436                    yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding;
437                }
438            } else {
439                if (mUserLocked) {
440                    yPosition += NotificationUtils.interpolate(
441                            0,
442                            mNotificatonTopPadding + mDividerHeight,
443                            expandFactor);
444                } else {
445                    yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0;
446                }
447                firstChild = false;
448            }
449
450            StackViewState childState = resultState.getViewStateForView(child);
451            int intrinsicHeight = child.getIntrinsicHeight();
452            if (childrenExpanded) {
453                // When a group is expanded and moving into bottom stack, the bottom visible child
454                // adjusts its height to move into it. Children after it are hidden.
455                if (updateChildStateForExpandedGroup(child, parentHeight, childState, yPosition)) {
456                    // Clipping might be deactivated if the view is transforming, however, clipping
457                    // the child into the bottom stack should take precedent over this.
458                    childState.isBottomClipped = true;
459                }
460            } else {
461                childState.hidden = false;
462                childState.height = intrinsicHeight;
463                childState.isBottomClipped = false;
464            }
465            childState.yTranslation = yPosition;
466            // When the group is expanded, the children cast the shadows rather than the parent
467            // so use the parent's elevation here.
468            childState.zTranslation = childrenExpanded
469                    ? mNotificationParent.getTranslationZ()
470                    : 0;
471            childState.dimmed = parentState.dimmed;
472            childState.dark = parentState.dark;
473            childState.hideSensitive = parentState.hideSensitive;
474            childState.belowSpeedBump = parentState.belowSpeedBump;
475            childState.clipTopAmount = 0;
476            childState.alpha = 0;
477            if (i < firstOverflowIndex) {
478                childState.alpha = 1;
479            } else if (expandFactor == 1.0f && i <= lastVisibleIndex) {
480                childState.alpha = (mActualHeight - childState.yTranslation) / childState.height;
481                childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha));
482            }
483            childState.location = parentState.location;
484            yPosition += intrinsicHeight;
485        }
486        if (mOverflowNumber != null) {
487            ExpandableNotificationRow overflowView = mChildren.get(Math.min(
488                    getMaxAllowedVisibleChildren(true /* likeCollpased */), childCount) - 1);
489            mGroupOverFlowState.copyFrom(resultState.getViewStateForView(overflowView));
490            if (!mChildrenExpanded) {
491                if (mUserLocked) {
492                    HybridNotificationView singleLineView = overflowView.getSingleLineView();
493                    View mirrorView = singleLineView.getTextView();
494                    if (mirrorView.getVisibility() == GONE) {
495                        mirrorView = singleLineView.getTitleView();
496                    }
497                    if (mirrorView.getVisibility() == GONE) {
498                        mirrorView = singleLineView;
499                    }
500                    mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset(
501                            mirrorView, overflowView);
502                    mGroupOverFlowState.alpha = mirrorView.getAlpha();
503                }
504            } else {
505                mGroupOverFlowState.yTranslation += mNotificationHeaderMargin;
506                mGroupOverFlowState.alpha = 0.0f;
507            }
508        }
509        if (mNotificationHeader != null) {
510            if (mHeaderViewState == null) {
511                mHeaderViewState = new ViewState();
512            }
513            mHeaderViewState.initFrom(mNotificationHeader);
514            mHeaderViewState.zTranslation = childrenExpanded
515                    ? mNotificationParent.getTranslationZ()
516                    : 0;
517        }
518    }
519
520    /**
521     * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
522     * height, children in the group after this are gone.
523     *
524     * @param child the child who's height to adjust.
525     * @param parentHeight the height of the parent.
526     * @param childState the state to update.
527     * @param yPosition the yPosition of the view.
528     * @return true if children after this one should be hidden.
529     */
530    private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
531            int parentHeight, StackViewState childState, int yPosition) {
532        final int top = yPosition + child.getClipTopAmount();
533        final int intrinsicHeight = child.getIntrinsicHeight();
534        final int bottom = top + intrinsicHeight;
535        int newHeight = intrinsicHeight;
536        if (bottom >= parentHeight) {
537            // Child is either clipped or gone
538            newHeight = Math.max((parentHeight - top), 0);
539        }
540        childState.hidden = newHeight == 0;
541        childState.height = newHeight;
542        return childState.height != intrinsicHeight && !childState.hidden;
543    }
544
545    private int getMaxAllowedVisibleChildren() {
546        return getMaxAllowedVisibleChildren(false /* likeCollapsed */);
547    }
548
549    private int getMaxAllowedVisibleChildren(boolean likeCollapsed) {
550        if (!likeCollapsed && (mChildrenExpanded || mNotificationParent.isUserLocked())) {
551            return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
552        }
553        if (!mNotificationParent.isOnKeyguard()
554                && (mNotificationParent.isExpanded() || mNotificationParent.isHeadsUp())) {
555            return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
556        }
557        return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
558    }
559
560    public void applyState(StackScrollState state) {
561        int childCount = mChildren.size();
562        ViewState tmpState = new ViewState();
563        float expandFraction = 0.0f;
564        if (mUserLocked) {
565            expandFraction = getGroupExpandFraction();
566        }
567        final boolean dividersVisible = mUserLocked
568                || mNotificationParent.isGroupExpansionChanging();
569        for (int i = 0; i < childCount; i++) {
570            ExpandableNotificationRow child = mChildren.get(i);
571            StackViewState viewState = state.getViewStateForView(child);
572            state.applyState(child, viewState);
573
574            // layout the divider
575            View divider = mDividers.get(i);
576            tmpState.initFrom(divider);
577            tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
578            float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
579            if (mUserLocked && viewState.alpha != 0) {
580                alpha = NotificationUtils.interpolate(0, 0.5f,
581                        Math.min(viewState.alpha, expandFraction));
582            }
583            tmpState.hidden = !dividersVisible;
584            tmpState.alpha = alpha;
585            state.applyViewState(divider, tmpState);
586            // There is no fake shadow to be drawn on the children
587            child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
588        }
589        if (mOverflowNumber != null) {
590            state.applyViewState(mOverflowNumber, mGroupOverFlowState);
591            mNeverAppliedGroupState = false;
592        }
593        if (mNotificationHeader != null) {
594            state.applyViewState(mNotificationHeader, mHeaderViewState);
595        }
596    }
597
598    /**
599     * This is called when the children expansion has changed and positions the children properly
600     * for an appear animation.
601     *
602     * @param state the new state we animate to
603     */
604    public void prepareExpansionChanged(StackScrollState state) {
605        // TODO: do something that makes sense, like placing the invisible views correctly
606        return;
607    }
608
609    public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator,
610            long baseDelay, long duration) {
611        int childCount = mChildren.size();
612        ViewState tmpState = new ViewState();
613        float expandFraction = getGroupExpandFraction();
614        final boolean dividersVisible = mUserLocked
615                || mNotificationParent.isGroupExpansionChanging();
616        for (int i = childCount - 1; i >= 0; i--) {
617            ExpandableNotificationRow child = mChildren.get(i);
618            StackViewState viewState = state.getViewStateForView(child);
619            stateAnimator.startStackAnimations(child, viewState, state, -1, baseDelay);
620
621            // layout the divider
622            View divider = mDividers.get(i);
623            tmpState.initFrom(divider);
624            tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
625            float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
626            if (mUserLocked && viewState.alpha != 0) {
627                alpha = NotificationUtils.interpolate(0, 0.5f,
628                        Math.min(viewState.alpha, expandFraction));
629            }
630            tmpState.hidden = !dividersVisible;
631            tmpState.alpha = alpha;
632            stateAnimator.startViewAnimations(divider, tmpState, baseDelay, duration);
633            // There is no fake shadow to be drawn on the children
634            child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
635        }
636        if (mOverflowNumber != null) {
637            if (mNeverAppliedGroupState) {
638                float alpha = mGroupOverFlowState.alpha;
639                mGroupOverFlowState.alpha = 0;
640                state.applyViewState(mOverflowNumber, mGroupOverFlowState);
641                mGroupOverFlowState.alpha = alpha;
642                mNeverAppliedGroupState = false;
643            }
644            stateAnimator.startViewAnimations(mOverflowNumber, mGroupOverFlowState,
645                    baseDelay, duration);
646        }
647        if (mNotificationHeader != null) {
648            state.applyViewState(mNotificationHeader, mHeaderViewState);
649        }
650    }
651
652    public ExpandableNotificationRow getViewAtPosition(float y) {
653        // find the view under the pointer, accounting for GONE views
654        final int count = mChildren.size();
655        for (int childIdx = 0; childIdx < count; childIdx++) {
656            ExpandableNotificationRow slidingChild = mChildren.get(childIdx);
657            float childTop = slidingChild.getTranslationY();
658            float top = childTop + slidingChild.getClipTopAmount();
659            float bottom = childTop + slidingChild.getActualHeight();
660            if (y >= top && y <= bottom) {
661                return slidingChild;
662            }
663        }
664        return null;
665    }
666
667    public void setChildrenExpanded(boolean childrenExpanded) {
668        mChildrenExpanded = childrenExpanded;
669        updateExpansionStates();
670        if (mNotificationHeader != null) {
671            mNotificationHeader.setExpanded(childrenExpanded);
672        }
673        final int count = mChildren.size();
674        for (int childIdx = 0; childIdx < count; childIdx++) {
675            ExpandableNotificationRow child = mChildren.get(childIdx);
676            child.setChildrenExpanded(childrenExpanded, false);
677        }
678    }
679
680    public void setNotificationParent(ExpandableNotificationRow parent) {
681        mNotificationParent = parent;
682        mHeaderUtil = new NotificationHeaderUtil(mNotificationParent);
683    }
684
685    public ExpandableNotificationRow getNotificationParent() {
686        return mNotificationParent;
687    }
688
689    public NotificationHeaderView getHeaderView() {
690        return mNotificationHeader;
691    }
692
693    public void updateHeaderVisibility(int visiblity) {
694        if (mNotificationHeader != null) {
695            mNotificationHeader.setVisibility(visiblity);
696        }
697    }
698
699    /**
700     * Called when a groups expansion changes to adjust the background of the header view.
701     *
702     * @param expanded whether the group is expanded.
703     */
704    public void updateHeaderForExpansion(boolean expanded) {
705        if (mNotificationHeader != null) {
706            if (expanded) {
707                ColorDrawable cd = new ColorDrawable();
708                cd.setColor(mNotificationParent.calculateBgColor());
709                mNotificationHeader.setHeaderBackgroundDrawable(cd);
710            } else {
711                mNotificationHeader.setHeaderBackgroundDrawable(null);
712            }
713        }
714    }
715
716    public int getMaxContentHeight() {
717        int maxContentHeight = mNotificationHeaderMargin + mNotificatonTopPadding;
718        int visibleChildren = 0;
719        int childCount = mChildren.size();
720        for (int i = 0; i < childCount; i++) {
721            if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) {
722                break;
723            }
724            ExpandableNotificationRow child = mChildren.get(i);
725            float childHeight = child.isExpanded(true /* allowOnKeyguard */)
726                    ? child.getMaxExpandHeight()
727                    : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
728            maxContentHeight += childHeight;
729            visibleChildren++;
730        }
731        if (visibleChildren > 0) {
732            maxContentHeight += visibleChildren * mDividerHeight;
733        }
734        return maxContentHeight;
735    }
736
737    public void setActualHeight(int actualHeight) {
738        if (!mUserLocked) {
739            return;
740        }
741        mActualHeight = actualHeight;
742        float fraction = getGroupExpandFraction();
743        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
744        int childCount = mChildren.size();
745        for (int i = 0; i < childCount; i++) {
746            ExpandableNotificationRow child = mChildren.get(i);
747            float childHeight = child.isExpanded(true /* allowOnKeyguard */)
748                    ? child.getMaxExpandHeight()
749                    : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
750            if (i < maxAllowedVisibleChildren) {
751                float singleLineHeight = child.getShowingLayout().getMinHeight(
752                        false /* likeGroupExpanded */);
753                child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight,
754                        childHeight, fraction), false);
755            } else {
756                child.setActualHeight((int) childHeight, false);
757            }
758        }
759    }
760
761    public float getGroupExpandFraction() {
762        int visibleChildrenExpandedHeight = getVisibleChildrenExpandHeight();
763        int minExpandHeight = getCollapsedHeight();
764        float factor = (mActualHeight - minExpandHeight)
765                / (float) (visibleChildrenExpandedHeight - minExpandHeight);
766        return Math.max(0.0f, Math.min(1.0f, factor));
767    }
768
769    private int getVisibleChildrenExpandHeight() {
770        int intrinsicHeight = mNotificationHeaderMargin + mNotificatonTopPadding + mDividerHeight;
771        int visibleChildren = 0;
772        int childCount = mChildren.size();
773        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
774        for (int i = 0; i < childCount; i++) {
775            if (visibleChildren >= maxAllowedVisibleChildren) {
776                break;
777            }
778            ExpandableNotificationRow child = mChildren.get(i);
779            float childHeight = child.isExpanded(true /* allowOnKeyguard */)
780                    ? child.getMaxExpandHeight()
781                    : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
782            intrinsicHeight += childHeight;
783            visibleChildren++;
784        }
785        return intrinsicHeight;
786    }
787
788    public int getMinHeight() {
789        return getMinHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED);
790    }
791
792    public int getCollapsedHeight() {
793        return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */));
794    }
795
796    private int getMinHeight(int maxAllowedVisibleChildren) {
797        int minExpandHeight = mNotificationHeaderMargin;
798        int visibleChildren = 0;
799        boolean firstChild = true;
800        int childCount = mChildren.size();
801        for (int i = 0; i < childCount; i++) {
802            if (visibleChildren >= maxAllowedVisibleChildren) {
803                break;
804            }
805            if (!firstChild) {
806                minExpandHeight += mChildPadding;
807            } else {
808                firstChild = false;
809            }
810            ExpandableNotificationRow child = mChildren.get(i);
811            minExpandHeight += child.getSingleLineView().getHeight();
812            visibleChildren++;
813        }
814        minExpandHeight += mCollapsedBottompadding;
815        return minExpandHeight;
816    }
817
818    public void setDark(boolean dark, boolean fade, long delay) {
819        if (mOverflowNumber != null) {
820            mOverflowInvertHelper.setInverted(dark, fade, delay);
821        }
822        mNotificationHeaderWrapper.setDark(dark, fade, delay);
823    }
824
825    public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
826        removeView(mNotificationHeader);
827        mNotificationHeader = null;
828        recreateNotificationHeader(listener, notification);
829        initDimens();
830        for (int i = 0; i < mDividers.size(); i++) {
831            View prevDivider = mDividers.get(i);
832            int index = indexOfChild(prevDivider);
833            removeView(prevDivider);
834            View divider = inflateDivider();
835            addView(divider, index);
836            mDividers.set(i, divider);
837        }
838        removeView(mOverflowNumber);
839        mOverflowNumber = null;
840        mOverflowInvertHelper = null;
841        mGroupOverFlowState = null;
842        updateGroupOverflow();
843    }
844
845    public void setUserLocked(boolean userLocked) {
846        mUserLocked = userLocked;
847        int childCount = mChildren.size();
848        for (int i = 0; i < childCount; i++) {
849            ExpandableNotificationRow child = mChildren.get(i);
850            child.setUserLocked(userLocked);
851        }
852    }
853
854    public void onNotificationUpdated() {
855        mHybridGroupManager.setOverflowNumberColor(mOverflowNumber,
856                mNotificationParent.getNotificationColor());
857    }
858
859    public int getPositionInLinearLayout(View childInGroup) {
860        int position = mNotificationHeaderMargin + mNotificatonTopPadding;
861
862        for (int i = 0; i < mChildren.size(); i++) {
863            ExpandableNotificationRow child = mChildren.get(i);
864            boolean notGone = child.getVisibility() != View.GONE;
865            if (notGone) {
866                position += mDividerHeight;
867            }
868            if (child == childInGroup) {
869                return position;
870            }
871            if (notGone) {
872                position += child.getIntrinsicHeight();
873            }
874        }
875        return 0;
876    }
877}
878