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