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