1/*
2 * Copyright (C) 2008 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 static android.content.res.Configuration.ORIENTATION_PORTRAIT;
20
21import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection;
22
23import android.annotation.Nullable;
24import android.content.Context;
25import android.content.res.Configuration;
26import android.graphics.Point;
27import android.graphics.Rect;
28import android.util.AttributeSet;
29import android.util.EventLog;
30import android.util.Pair;
31import android.view.Display;
32import android.view.DisplayCutout;
33import android.view.Gravity;
34import android.view.MotionEvent;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.WindowInsets;
38import android.view.accessibility.AccessibilityEvent;
39import android.widget.FrameLayout;
40import android.widget.LinearLayout;
41
42import com.android.systemui.Dependency;
43import com.android.systemui.EventLogTags;
44import com.android.systemui.R;
45import com.android.systemui.statusbar.policy.DarkIconDispatcher;
46import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
47
48import java.util.Objects;
49
50public class PhoneStatusBarView extends PanelBar {
51    private static final String TAG = "PhoneStatusBarView";
52    private static final boolean DEBUG = StatusBar.DEBUG;
53    private static final boolean DEBUG_GESTURES = false;
54    private static final int NO_VALUE = Integer.MIN_VALUE;
55
56    StatusBar mBar;
57
58    boolean mIsFullyOpenedPanel = false;
59    private final PhoneStatusBarTransitions mBarTransitions;
60    private ScrimController mScrimController;
61    private float mMinFraction;
62    private float mPanelFraction;
63    private Runnable mHideExpandedRunnable = new Runnable() {
64        @Override
65        public void run() {
66            if (mPanelFraction == 0.0f) {
67                mBar.makeExpandedInvisible();
68            }
69        }
70    };
71    private DarkReceiver mBattery;
72    private int mLastOrientation;
73    @Nullable
74    private View mCutoutSpace;
75    @Nullable
76    private DisplayCutout mDisplayCutout;
77    /**
78     * Draw this many pixels into the left/right side of the cutout to optimally use the space
79     */
80    private int mCutoutSideNudge = 0;
81
82    public PhoneStatusBarView(Context context, AttributeSet attrs) {
83        super(context, attrs);
84
85        mBarTransitions = new PhoneStatusBarTransitions(this);
86    }
87
88    public BarTransitions getBarTransitions() {
89        return mBarTransitions;
90    }
91
92    public void setBar(StatusBar bar) {
93        mBar = bar;
94    }
95
96    public void setScrimController(ScrimController scrimController) {
97        mScrimController = scrimController;
98    }
99
100    @Override
101    public void onFinishInflate() {
102        mBarTransitions.init();
103        mBattery = findViewById(R.id.battery);
104        mCutoutSpace = findViewById(R.id.cutout_space_view);
105
106        updateResources();
107    }
108
109    @Override
110    protected void onAttachedToWindow() {
111        super.onAttachedToWindow();
112        // Always have Battery meters in the status bar observe the dark/light modes.
113        Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery);
114        if (updateOrientationAndCutout(getResources().getConfiguration().orientation)) {
115            updateLayoutForCutout();
116        }
117    }
118
119    @Override
120    protected void onDetachedFromWindow() {
121        super.onDetachedFromWindow();
122        Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery);
123        mDisplayCutout = null;
124    }
125
126    @Override
127    protected void onConfigurationChanged(Configuration newConfig) {
128        super.onConfigurationChanged(newConfig);
129
130        // May trigger cutout space layout-ing
131        if (updateOrientationAndCutout(newConfig.orientation)) {
132            updateLayoutForCutout();
133            requestLayout();
134        }
135    }
136
137    @Override
138    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
139        if (updateOrientationAndCutout(mLastOrientation)) {
140            updateLayoutForCutout();
141            requestLayout();
142        }
143        return super.onApplyWindowInsets(insets);
144    }
145
146    /**
147     *
148     * @param newOrientation may pass NO_VALUE for no change
149     * @return boolean indicating if we need to update the cutout location / margins
150     */
151    private boolean updateOrientationAndCutout(int newOrientation) {
152        boolean changed = false;
153        if (newOrientation != NO_VALUE) {
154            if (mLastOrientation != newOrientation) {
155                changed = true;
156                mLastOrientation = newOrientation;
157            }
158        }
159
160        if (!Objects.equals(getRootWindowInsets().getDisplayCutout(), mDisplayCutout)) {
161            changed = true;
162            mDisplayCutout = getRootWindowInsets().getDisplayCutout();
163        }
164
165        return changed;
166    }
167
168    @Override
169    public boolean panelEnabled() {
170        return mBar.panelsEnabled();
171    }
172
173    @Override
174    public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
175        if (super.onRequestSendAccessibilityEventInternal(child, event)) {
176            // The status bar is very small so augment the view that the user is touching
177            // with the content of the status bar a whole. This way an accessibility service
178            // may announce the current item as well as the entire content if appropriate.
179            AccessibilityEvent record = AccessibilityEvent.obtain();
180            onInitializeAccessibilityEvent(record);
181            dispatchPopulateAccessibilityEvent(record);
182            event.appendRecord(record);
183            return true;
184        }
185        return false;
186    }
187
188    @Override
189    public void onPanelPeeked() {
190        super.onPanelPeeked();
191        mBar.makeExpandedVisible(false);
192    }
193
194    @Override
195    public void onPanelCollapsed() {
196        super.onPanelCollapsed();
197        // Close the status bar in the next frame so we can show the end of the animation.
198        post(mHideExpandedRunnable);
199        mIsFullyOpenedPanel = false;
200    }
201
202    public void removePendingHideExpandedRunnables() {
203        removeCallbacks(mHideExpandedRunnable);
204    }
205
206    @Override
207    public void onPanelFullyOpened() {
208        super.onPanelFullyOpened();
209        if (!mIsFullyOpenedPanel) {
210            mPanel.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
211        }
212        mIsFullyOpenedPanel = true;
213    }
214
215    @Override
216    public boolean onTouchEvent(MotionEvent event) {
217        boolean barConsumedEvent = mBar.interceptTouchEvent(event);
218
219        if (DEBUG_GESTURES) {
220            if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
221                EventLog.writeEvent(EventLogTags.SYSUI_PANELBAR_TOUCH,
222                        event.getActionMasked(), (int) event.getX(), (int) event.getY(),
223                        barConsumedEvent ? 1 : 0);
224            }
225        }
226
227        return barConsumedEvent || super.onTouchEvent(event);
228    }
229
230    @Override
231    public void onTrackingStarted() {
232        super.onTrackingStarted();
233        mBar.onTrackingStarted();
234        mScrimController.onTrackingStarted();
235        removePendingHideExpandedRunnables();
236    }
237
238    @Override
239    public void onClosingFinished() {
240        super.onClosingFinished();
241        mBar.onClosingFinished();
242    }
243
244    @Override
245    public void onTrackingStopped(boolean expand) {
246        super.onTrackingStopped(expand);
247        mBar.onTrackingStopped(expand);
248    }
249
250    @Override
251    public void onExpandingFinished() {
252        super.onExpandingFinished();
253        mScrimController.onExpandingFinished();
254    }
255
256    @Override
257    public boolean onInterceptTouchEvent(MotionEvent event) {
258        return mBar.interceptTouchEvent(event) || super.onInterceptTouchEvent(event);
259    }
260
261    @Override
262    public void panelScrimMinFractionChanged(float minFraction) {
263        if (mMinFraction != minFraction) {
264            mMinFraction = minFraction;
265            updateScrimFraction();
266        }
267    }
268
269    @Override
270    public void panelExpansionChanged(float frac, boolean expanded) {
271        super.panelExpansionChanged(frac, expanded);
272        mPanelFraction = frac;
273        updateScrimFraction();
274        if ((frac == 0 || frac == 1) && mBar.getNavigationBarView() != null) {
275            mBar.getNavigationBarView().onPanelExpandedChange(expanded);
276        }
277    }
278
279    private void updateScrimFraction() {
280        float scrimFraction = mPanelFraction;
281        if (mMinFraction < 1.0f) {
282            scrimFraction = Math.max((mPanelFraction - mMinFraction) / (1.0f - mMinFraction),
283                    0);
284        }
285        mScrimController.setPanelExpansion(scrimFraction);
286    }
287
288    public void updateResources() {
289        mCutoutSideNudge = getResources().getDimensionPixelSize(
290                R.dimen.display_cutout_margin_consumption);
291
292        ViewGroup.LayoutParams layoutParams = getLayoutParams();
293        layoutParams.height = getResources().getDimensionPixelSize(
294                R.dimen.status_bar_height);
295        setLayoutParams(layoutParams);
296    }
297
298    private void updateLayoutForCutout() {
299        Pair<Integer, Integer> cornerCutoutMargins = cornerCutoutMargins(mDisplayCutout,
300                getDisplay());
301        updateCutoutLocation(cornerCutoutMargins);
302        updateSafeInsets(cornerCutoutMargins);
303    }
304
305    private void updateCutoutLocation(Pair<Integer, Integer> cornerCutoutMargins) {
306        // Not all layouts have a cutout (e.g., Car)
307        if (mCutoutSpace == null) {
308            return;
309        }
310
311        if (mDisplayCutout == null || mDisplayCutout.isEmpty()
312                    || mLastOrientation != ORIENTATION_PORTRAIT || cornerCutoutMargins != null) {
313            mCutoutSpace.setVisibility(View.GONE);
314            return;
315        }
316
317        mCutoutSpace.setVisibility(View.VISIBLE);
318        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mCutoutSpace.getLayoutParams();
319
320        Rect bounds = new Rect();
321        boundsFromDirection(mDisplayCutout, Gravity.TOP, bounds);
322
323        bounds.left = bounds.left + mCutoutSideNudge;
324        bounds.right = bounds.right - mCutoutSideNudge;
325        lp.width = bounds.width();
326        lp.height = bounds.height();
327    }
328
329    private void updateSafeInsets(Pair<Integer, Integer> cornerCutoutMargins) {
330        // Depending on our rotation, we may have to work around a cutout in the middle of the view,
331        // or letterboxing from the right or left sides.
332
333        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
334        if (mDisplayCutout == null) {
335            lp.leftMargin = 0;
336            lp.rightMargin = 0;
337            return;
338        }
339
340        lp.leftMargin = mDisplayCutout.getSafeInsetLeft();
341        lp.rightMargin = mDisplayCutout.getSafeInsetRight();
342
343        if (cornerCutoutMargins != null) {
344            lp.leftMargin = Math.max(lp.leftMargin, cornerCutoutMargins.first);
345            lp.rightMargin = Math.max(lp.rightMargin, cornerCutoutMargins.second);
346
347            // If we're already inset enough (e.g. on the status bar side), we can have 0 margin
348            WindowInsets insets = getRootWindowInsets();
349            int leftInset = insets.getSystemWindowInsetLeft();
350            int rightInset = insets.getSystemWindowInsetRight();
351            if (lp.leftMargin <= leftInset) {
352                lp.leftMargin = 0;
353            }
354            if (lp.rightMargin <= rightInset) {
355                lp.rightMargin = 0;
356            }
357
358        }
359    }
360
361    public static Pair<Integer, Integer> cornerCutoutMargins(DisplayCutout cutout,
362            Display display) {
363        if (cutout == null) {
364            return null;
365        }
366        Point size = new Point();
367        display.getRealSize(size);
368
369        Rect bounds = new Rect();
370        boundsFromDirection(cutout, Gravity.TOP, bounds);
371
372        if (bounds.left <= 0) {
373            return new Pair<>(bounds.right, 0);
374        }
375        if (bounds.right >= size.x) {
376            return new Pair<>(0, size.x - bounds.left);
377        }
378        return null;
379    }
380}
381