1/*
2 * Copyright (C) 2018 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.phone;
18
19import android.graphics.Point;
20import android.graphics.Rect;
21import android.view.View;
22import android.view.WindowInsets;
23
24import com.android.internal.annotations.VisibleForTesting;
25import com.android.systemui.Dependency;
26import com.android.systemui.R;
27import com.android.systemui.statusbar.CrossFadeHelper;
28import com.android.systemui.statusbar.ExpandableNotificationRow;
29import com.android.systemui.statusbar.HeadsUpStatusBarView;
30import com.android.systemui.statusbar.NotificationData;
31import com.android.systemui.statusbar.policy.DarkIconDispatcher;
32import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
33import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
34
35import java.util.function.BiConsumer;
36import java.util.function.Consumer;
37
38/**
39 * Controls the appearance of heads up notifications in the icon area and the header itself.
40 */
41public class HeadsUpAppearanceController implements OnHeadsUpChangedListener,
42        DarkIconDispatcher.DarkReceiver {
43    public static final int CONTENT_FADE_DURATION = 110;
44    public static final int CONTENT_FADE_DELAY = 100;
45    private final NotificationIconAreaController mNotificationIconAreaController;
46    private final HeadsUpManagerPhone mHeadsUpManager;
47    private final NotificationStackScrollLayout mStackScroller;
48    private final HeadsUpStatusBarView mHeadsUpStatusBarView;
49    private final View mClockView;
50    private final DarkIconDispatcher mDarkIconDispatcher;
51    private final NotificationPanelView mPanelView;
52    private final Consumer<ExpandableNotificationRow>
53            mSetTrackingHeadsUp = this::setTrackingHeadsUp;
54    private final Runnable mUpdatePanelTranslation = this::updatePanelTranslation;
55    private final BiConsumer<Float, Float> mSetExpandedHeight = this::setExpandedHeight;
56    private float mExpandedHeight;
57    private boolean mIsExpanded;
58    private float mExpandFraction;
59    private ExpandableNotificationRow mTrackedChild;
60    private boolean mShown;
61    private final View.OnLayoutChangeListener mStackScrollLayoutChangeListener =
62            (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
63                    -> updatePanelTranslation();
64    Point mPoint;
65
66    public HeadsUpAppearanceController(
67            NotificationIconAreaController notificationIconAreaController,
68            HeadsUpManagerPhone headsUpManager,
69            View statusbarView) {
70        this(notificationIconAreaController, headsUpManager,
71                statusbarView.findViewById(R.id.heads_up_status_bar_view),
72                statusbarView.findViewById(R.id.notification_stack_scroller),
73                statusbarView.findViewById(R.id.notification_panel),
74                statusbarView.findViewById(R.id.clock));
75    }
76
77    @VisibleForTesting
78    public HeadsUpAppearanceController(
79            NotificationIconAreaController notificationIconAreaController,
80            HeadsUpManagerPhone headsUpManager,
81            HeadsUpStatusBarView headsUpStatusBarView,
82            NotificationStackScrollLayout stackScroller,
83            NotificationPanelView panelView,
84            View clockView) {
85        mNotificationIconAreaController = notificationIconAreaController;
86        mHeadsUpManager = headsUpManager;
87        mHeadsUpManager.addListener(this);
88        mHeadsUpStatusBarView = headsUpStatusBarView;
89        headsUpStatusBarView.setOnDrawingRectChangedListener(
90                () -> updateIsolatedIconLocation(true /* requireUpdate */));
91        mStackScroller = stackScroller;
92        mPanelView = panelView;
93        panelView.addTrackingHeadsUpListener(mSetTrackingHeadsUp);
94        panelView.addVerticalTranslationListener(mUpdatePanelTranslation);
95        panelView.setHeadsUpAppearanceController(this);
96        mStackScroller.addOnExpandedHeightListener(mSetExpandedHeight);
97        mStackScroller.addOnLayoutChangeListener(mStackScrollLayoutChangeListener);
98        mStackScroller.setHeadsUpAppearanceController(this);
99        mClockView = clockView;
100        mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class);
101        mDarkIconDispatcher.addDarkReceiver(this);
102    }
103
104
105    public void destroy() {
106        mHeadsUpManager.removeListener(this);
107        mHeadsUpStatusBarView.setOnDrawingRectChangedListener(null);
108        mPanelView.removeTrackingHeadsUpListener(mSetTrackingHeadsUp);
109        mPanelView.removeVerticalTranslationListener(mUpdatePanelTranslation);
110        mPanelView.setHeadsUpAppearanceController(null);
111        mStackScroller.removeOnExpandedHeightListener(mSetExpandedHeight);
112        mStackScroller.removeOnLayoutChangeListener(mStackScrollLayoutChangeListener);
113        mDarkIconDispatcher.removeDarkReceiver(this);
114    }
115
116    private void updateIsolatedIconLocation(boolean requireStateUpdate) {
117        mNotificationIconAreaController.setIsolatedIconLocation(
118                mHeadsUpStatusBarView.getIconDrawingRect(), requireStateUpdate);
119    }
120
121    @Override
122    public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
123        updateTopEntry();
124        updateHeader(headsUp.getEntry());
125    }
126
127    /** To count the distance from the window right boundary to scroller right boundary. The
128     * distance formula is the following:
129     *     Y = screenSize - (SystemWindow's width + Scroller.getRight())
130     * There are four modes MUST to be considered in Cut Out of RTL.
131     * No Cut Out:
132     *   Scroller + NB
133     *   NB + Scroller
134     *     => SystemWindow = NavigationBar's width
135     *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
136     * Corner Cut Out or Tall Cut Out:
137     *   cut out + Scroller + NB
138     *   NB + Scroller + cut out
139     *     => SystemWindow = NavigationBar's width
140     *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
141     * Double Cut Out:
142     *   cut out left + Scroller + (NB + cut out right)
143     *     SystemWindow = NavigationBar's width + cut out right width
144     *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
145     *   (cut out left + NB) + Scroller + cut out right
146     *     SystemWindow = NavigationBar's width + cut out left width
147     *     => Y = screenSize - (SystemWindow's width + Scroller.getRight())
148     * @return the translation X value for RTL. In theory, it should be negative. i.e. -Y
149     */
150    private int getRtlTranslation() {
151        if (mPoint == null) {
152            mPoint = new Point();
153        }
154
155        int realDisplaySize = 0;
156        if (mStackScroller.getDisplay() != null) {
157            mStackScroller.getDisplay().getRealSize(mPoint);
158            realDisplaySize = mPoint.x;
159        }
160
161        WindowInsets windowInset = mStackScroller.getRootWindowInsets();
162        return windowInset.getSystemWindowInsetLeft() + mStackScroller.getRight()
163                + windowInset.getSystemWindowInsetRight() - realDisplaySize;
164    }
165
166    public void updatePanelTranslation() {
167        float newTranslation;
168        if (mStackScroller.isLayoutRtl()) {
169            newTranslation = getRtlTranslation();
170        } else {
171            newTranslation = mStackScroller.getLeft();
172        }
173        newTranslation += mStackScroller.getTranslationX();
174        mHeadsUpStatusBarView.setPanelTranslation(newTranslation);
175    }
176
177    private void updateTopEntry() {
178        NotificationData.Entry newEntry = null;
179        if (!mIsExpanded && mHeadsUpManager.hasPinnedHeadsUp()) {
180            newEntry = mHeadsUpManager.getTopEntry();
181        }
182        NotificationData.Entry previousEntry = mHeadsUpStatusBarView.getShowingEntry();
183        mHeadsUpStatusBarView.setEntry(newEntry);
184        if (newEntry != previousEntry) {
185            boolean animateIsolation = false;
186            if (newEntry == null) {
187                // no heads up anymore, lets start the disappear animation
188
189                setShown(false);
190                animateIsolation = !mIsExpanded;
191            } else if (previousEntry == null) {
192                // We now have a headsUp and didn't have one before. Let's start the disappear
193                // animation
194                setShown(true);
195                animateIsolation = !mIsExpanded;
196            }
197            updateIsolatedIconLocation(false /* requireUpdate */);
198            mNotificationIconAreaController.showIconIsolated(newEntry == null ? null
199                    : newEntry.icon, animateIsolation);
200        }
201    }
202
203    private void setShown(boolean isShown) {
204        if (mShown != isShown) {
205            mShown = isShown;
206            if (isShown) {
207                mHeadsUpStatusBarView.setVisibility(View.VISIBLE);
208                CrossFadeHelper.fadeIn(mHeadsUpStatusBarView, CONTENT_FADE_DURATION /* duration */,
209                        CONTENT_FADE_DELAY /* delay */);
210                CrossFadeHelper.fadeOut(mClockView, CONTENT_FADE_DURATION/* duration */,
211                        0 /* delay */, () -> mClockView.setVisibility(View.INVISIBLE));
212            } else {
213                CrossFadeHelper.fadeIn(mClockView, CONTENT_FADE_DURATION /* duration */,
214                        CONTENT_FADE_DELAY /* delay */);
215                CrossFadeHelper.fadeOut(mHeadsUpStatusBarView, CONTENT_FADE_DURATION/* duration */,
216                        0 /* delay */, () -> mHeadsUpStatusBarView.setVisibility(View.GONE));
217
218            }
219        }
220    }
221
222    @VisibleForTesting
223    public boolean isShown() {
224        return mShown;
225    }
226
227    /**
228     * Should the headsup status bar view be visible right now? This may be different from isShown,
229     * since the headsUp manager might not have notified us yet of the state change.
230     *
231     * @return if the heads up status bar view should be shown
232     */
233    public boolean shouldBeVisible() {
234        return !mIsExpanded && mHeadsUpManager.hasPinnedHeadsUp();
235    }
236
237    @Override
238    public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
239        updateTopEntry();
240        updateHeader(headsUp.getEntry());
241    }
242
243    public void setExpandedHeight(float expandedHeight, float appearFraction) {
244        boolean changedHeight = expandedHeight != mExpandedHeight;
245        mExpandedHeight = expandedHeight;
246        mExpandFraction = appearFraction;
247        boolean isExpanded = expandedHeight > 0;
248        if (changedHeight) {
249            updateHeadsUpHeaders();
250        }
251        if (isExpanded != mIsExpanded) {
252            mIsExpanded = isExpanded;
253            updateTopEntry();
254        }
255    }
256
257    /**
258     * Set a headsUp to be tracked, meaning that it is currently being pulled down after being
259     * in a pinned state on the top. The expand animation is different in that case and we need
260     * to update the header constantly afterwards.
261     *
262     * @param trackedChild the tracked headsUp or null if it's not tracking anymore.
263     */
264    public void setTrackingHeadsUp(ExpandableNotificationRow trackedChild) {
265        ExpandableNotificationRow previousTracked = mTrackedChild;
266        mTrackedChild = trackedChild;
267        if (previousTracked != null) {
268            updateHeader(previousTracked.getEntry());
269        }
270    }
271
272    private void updateHeadsUpHeaders() {
273        mHeadsUpManager.getAllEntries().forEach(entry -> {
274            updateHeader(entry);
275        });
276    }
277
278    public void updateHeader(NotificationData.Entry entry) {
279        ExpandableNotificationRow row = entry.row;
280        float headerVisibleAmount = 1.0f;
281        if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild) {
282            headerVisibleAmount = mExpandFraction;
283        }
284        row.setHeaderVisibleAmount(headerVisibleAmount);
285    }
286
287    @Override
288    public void onDarkChanged(Rect area, float darkIntensity, int tint) {
289        mHeadsUpStatusBarView.onDarkChanged(area, darkIntensity, tint);
290    }
291
292    public void setPublicMode(boolean publicMode) {
293        mHeadsUpStatusBarView.setPublicMode(publicMode);
294        updateTopEntry();
295    }
296}
297