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 
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.graphics.Point;
27 import android.graphics.Rect;
28 import android.util.AttributeSet;
29 import android.util.EventLog;
30 import android.util.Pair;
31 import android.view.Display;
32 import android.view.DisplayCutout;
33 import android.view.Gravity;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.WindowInsets;
38 import android.view.accessibility.AccessibilityEvent;
39 import android.widget.FrameLayout;
40 import android.widget.LinearLayout;
41 
42 import com.android.systemui.Dependency;
43 import com.android.systemui.EventLogTags;
44 import com.android.systemui.R;
45 import com.android.systemui.statusbar.policy.DarkIconDispatcher;
46 import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
47 
48 import java.util.Objects;
49 
50 public 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 
PhoneStatusBarView(Context context, AttributeSet attrs)82     public PhoneStatusBarView(Context context, AttributeSet attrs) {
83         super(context, attrs);
84 
85         mBarTransitions = new PhoneStatusBarTransitions(this);
86     }
87 
getBarTransitions()88     public BarTransitions getBarTransitions() {
89         return mBarTransitions;
90     }
91 
setBar(StatusBar bar)92     public void setBar(StatusBar bar) {
93         mBar = bar;
94     }
95 
setScrimController(ScrimController scrimController)96     public void setScrimController(ScrimController scrimController) {
97         mScrimController = scrimController;
98     }
99 
100     @Override
onFinishInflate()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
onAttachedToWindow()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
onDetachedFromWindow()120     protected void onDetachedFromWindow() {
121         super.onDetachedFromWindow();
122         Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery);
123         mDisplayCutout = null;
124     }
125 
126     @Override
onConfigurationChanged(Configuration newConfig)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
onApplyWindowInsets(WindowInsets insets)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      */
updateOrientationAndCutout(int newOrientation)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
panelEnabled()169     public boolean panelEnabled() {
170         return mBar.panelsEnabled();
171     }
172 
173     @Override
onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event)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
onPanelPeeked()189     public void onPanelPeeked() {
190         super.onPanelPeeked();
191         mBar.makeExpandedVisible(false);
192     }
193 
194     @Override
onPanelCollapsed()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 
removePendingHideExpandedRunnables()202     public void removePendingHideExpandedRunnables() {
203         removeCallbacks(mHideExpandedRunnable);
204     }
205 
206     @Override
onPanelFullyOpened()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
onTouchEvent(MotionEvent event)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
onTrackingStarted()231     public void onTrackingStarted() {
232         super.onTrackingStarted();
233         mBar.onTrackingStarted();
234         mScrimController.onTrackingStarted();
235         removePendingHideExpandedRunnables();
236     }
237 
238     @Override
onClosingFinished()239     public void onClosingFinished() {
240         super.onClosingFinished();
241         mBar.onClosingFinished();
242     }
243 
244     @Override
onTrackingStopped(boolean expand)245     public void onTrackingStopped(boolean expand) {
246         super.onTrackingStopped(expand);
247         mBar.onTrackingStopped(expand);
248     }
249 
250     @Override
onExpandingFinished()251     public void onExpandingFinished() {
252         super.onExpandingFinished();
253         mScrimController.onExpandingFinished();
254     }
255 
256     @Override
onInterceptTouchEvent(MotionEvent event)257     public boolean onInterceptTouchEvent(MotionEvent event) {
258         return mBar.interceptTouchEvent(event) || super.onInterceptTouchEvent(event);
259     }
260 
261     @Override
panelScrimMinFractionChanged(float minFraction)262     public void panelScrimMinFractionChanged(float minFraction) {
263         if (mMinFraction != minFraction) {
264             mMinFraction = minFraction;
265             updateScrimFraction();
266         }
267     }
268 
269     @Override
panelExpansionChanged(float frac, boolean expanded)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 
updateScrimFraction()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 
updateResources()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 
updateLayoutForCutout()298     private void updateLayoutForCutout() {
299         Pair<Integer, Integer> cornerCutoutMargins = cornerCutoutMargins(mDisplayCutout,
300                 getDisplay());
301         updateCutoutLocation(cornerCutoutMargins);
302         updateSafeInsets(cornerCutoutMargins);
303     }
304 
updateCutoutLocation(Pair<Integer, Integer> cornerCutoutMargins)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 
updateSafeInsets(Pair<Integer, Integer> cornerCutoutMargins)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 
cornerCutoutMargins(DisplayCutout cutout, Display display)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