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 
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.graphics.Rect;
24 import android.util.AttributeSet;
25 import android.util.Log;
26 import android.util.Pair;
27 import android.view.DisplayCutout;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.WindowInsets;
32 import android.view.accessibility.AccessibilityEvent;
33 import android.widget.FrameLayout;
34 import android.widget.LinearLayout;
35 
36 import com.android.internal.policy.SystemBarUtils;
37 import com.android.systemui.Dependency;
38 import com.android.systemui.R;
39 import com.android.systemui.plugins.DarkIconDispatcher;
40 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
41 import com.android.systemui.util.leak.RotationUtils;
42 
43 import java.util.Objects;
44 
45 public class PhoneStatusBarView extends FrameLayout {
46     private static final String TAG = "PhoneStatusBarView";
47     private final StatusBarContentInsetsProvider mContentInsetsProvider;
48 
49     private DarkReceiver mBattery;
50     private DarkReceiver mClock;
51     private int mRotationOrientation = -1;
52     @Nullable
53     private View mCenterIconSpace;
54     @Nullable
55     private View mCutoutSpace;
56     @Nullable
57     private DisplayCutout mDisplayCutout;
58     private int mStatusBarHeight;
59     @Nullable
60     private TouchEventHandler mTouchEventHandler;
61 
62     /**
63      * Draw this many pixels into the left/right side of the cutout to optimally use the space
64      */
65     private int mCutoutSideNudge = 0;
66 
PhoneStatusBarView(Context context, AttributeSet attrs)67     public PhoneStatusBarView(Context context, AttributeSet attrs) {
68         super(context, attrs);
69         mContentInsetsProvider = Dependency.get(StatusBarContentInsetsProvider.class);
70     }
71 
setTouchEventHandler(TouchEventHandler handler)72     void setTouchEventHandler(TouchEventHandler handler) {
73         mTouchEventHandler = handler;
74     }
75 
76     @Override
onFinishInflate()77     public void onFinishInflate() {
78         mBattery = findViewById(R.id.battery);
79         mClock = findViewById(R.id.clock);
80         mCutoutSpace = findViewById(R.id.cutout_space_view);
81         mCenterIconSpace = findViewById(R.id.centered_icon_area);
82 
83         updateResources();
84     }
85 
86     @Override
onAttachedToWindow()87     protected void onAttachedToWindow() {
88         super.onAttachedToWindow();
89         // Always have Battery meters in the status bar observe the dark/light modes.
90         Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery);
91         Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mClock);
92         if (updateOrientationAndCutout()) {
93             updateLayoutForCutout();
94         }
95     }
96 
97     @Override
onDetachedFromWindow()98     protected void onDetachedFromWindow() {
99         super.onDetachedFromWindow();
100         Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery);
101         Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mClock);
102         mDisplayCutout = null;
103     }
104 
105     @Override
onConfigurationChanged(Configuration newConfig)106     protected void onConfigurationChanged(Configuration newConfig) {
107         super.onConfigurationChanged(newConfig);
108         updateResources();
109 
110         // May trigger cutout space layout-ing
111         if (updateOrientationAndCutout()) {
112             updateLayoutForCutout();
113             requestLayout();
114         }
115     }
116 
117     @Override
onApplyWindowInsets(WindowInsets insets)118     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
119         if (updateOrientationAndCutout()) {
120             updateLayoutForCutout();
121             requestLayout();
122         }
123         return super.onApplyWindowInsets(insets);
124     }
125 
126     /**
127      * @return boolean indicating if we need to update the cutout location / margins
128      */
updateOrientationAndCutout()129     private boolean updateOrientationAndCutout() {
130         boolean changed = false;
131         int newRotation = RotationUtils.getExactRotation(mContext);
132         if (newRotation != mRotationOrientation) {
133             changed = true;
134             mRotationOrientation = newRotation;
135         }
136 
137         if (!Objects.equals(getRootWindowInsets().getDisplayCutout(), mDisplayCutout)) {
138             changed = true;
139             mDisplayCutout = getRootWindowInsets().getDisplayCutout();
140         }
141 
142         return changed;
143     }
144 
145     @Override
onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event)146     public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
147         if (super.onRequestSendAccessibilityEventInternal(child, event)) {
148             // The status bar is very small so augment the view that the user is touching
149             // with the content of the status bar a whole. This way an accessibility service
150             // may announce the current item as well as the entire content if appropriate.
151             AccessibilityEvent record = AccessibilityEvent.obtain();
152             onInitializeAccessibilityEvent(record);
153             dispatchPopulateAccessibilityEvent(record);
154             event.appendRecord(record);
155             return true;
156         }
157         return false;
158     }
159 
160     @Override
onTouchEvent(MotionEvent event)161     public boolean onTouchEvent(MotionEvent event) {
162         if (mTouchEventHandler == null) {
163             Log.w(
164                     TAG,
165                     String.format(
166                             "onTouch: No touch handler provided; eating gesture at (%d,%d)",
167                             (int) event.getX(),
168                             (int) event.getY()
169                     )
170             );
171             return true;
172         }
173         return mTouchEventHandler.handleTouchEvent(event);
174     }
175 
176     @Override
onInterceptTouchEvent(MotionEvent event)177     public boolean onInterceptTouchEvent(MotionEvent event) {
178         mTouchEventHandler.onInterceptTouchEvent(event);
179         return super.onInterceptTouchEvent(event);
180     }
181 
updateResources()182     public void updateResources() {
183         mCutoutSideNudge = getResources().getDimensionPixelSize(
184                 R.dimen.display_cutout_margin_consumption);
185 
186         updateStatusBarHeight();
187     }
188 
updateStatusBarHeight()189     private void updateStatusBarHeight() {
190         final int waterfallTopInset =
191                 mDisplayCutout == null ? 0 : mDisplayCutout.getWaterfallInsets().top;
192         ViewGroup.LayoutParams layoutParams = getLayoutParams();
193         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
194         layoutParams.height = mStatusBarHeight - waterfallTopInset;
195 
196         int statusBarPaddingTop = getResources().getDimensionPixelSize(
197                 R.dimen.status_bar_padding_top);
198         int statusBarPaddingStart = getResources().getDimensionPixelSize(
199                 R.dimen.status_bar_padding_start);
200         int statusBarPaddingEnd = getResources().getDimensionPixelSize(
201                 R.dimen.status_bar_padding_end);
202 
203         View sbContents = findViewById(R.id.status_bar_contents);
204         sbContents.setPaddingRelative(
205                 statusBarPaddingStart,
206                 statusBarPaddingTop,
207                 statusBarPaddingEnd,
208                 0);
209 
210         findViewById(R.id.notification_lights_out)
211                 .setPaddingRelative(0, statusBarPaddingStart, 0, 0);
212 
213         setLayoutParams(layoutParams);
214     }
215 
updateLayoutForCutout()216     private void updateLayoutForCutout() {
217         updateStatusBarHeight();
218         updateCutoutLocation();
219         updateSafeInsets();
220     }
221 
updateCutoutLocation()222     private void updateCutoutLocation() {
223         // Not all layouts have a cutout (e.g., Car)
224         if (mCutoutSpace == null) {
225             return;
226         }
227 
228         boolean hasCornerCutout = mContentInsetsProvider.currentRotationHasCornerCutout();
229         if (mDisplayCutout == null || mDisplayCutout.isEmpty() || hasCornerCutout) {
230             mCenterIconSpace.setVisibility(View.VISIBLE);
231             mCutoutSpace.setVisibility(View.GONE);
232             return;
233         }
234 
235         mCenterIconSpace.setVisibility(View.GONE);
236         mCutoutSpace.setVisibility(View.VISIBLE);
237         LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mCutoutSpace.getLayoutParams();
238 
239         Rect bounds = mDisplayCutout.getBoundingRectTop();
240 
241         bounds.left = bounds.left + mCutoutSideNudge;
242         bounds.right = bounds.right - mCutoutSideNudge;
243         lp.width = bounds.width();
244         lp.height = bounds.height();
245     }
246 
updateSafeInsets()247     private void updateSafeInsets() {
248         Pair<Integer, Integer> insets = mContentInsetsProvider
249                 .getStatusBarContentInsetsForCurrentRotation();
250 
251         setPadding(
252                 insets.first,
253                 getPaddingTop(),
254                 insets.second,
255                 getPaddingBottom());
256     }
257 
258     /**
259      * A handler responsible for all touch event handling on the status bar.
260      *
261      * Touches that occur on the status bar view may have ramifications for the notification
262      * panel (e.g. a touch that pulls down the shade could start on the status bar), so this
263      * interface provides a way to notify the panel controller when these touches occur.
264      *
265      * The handler will be notified each time {@link PhoneStatusBarView#onTouchEvent} and
266      * {@link PhoneStatusBarView#onInterceptTouchEvent} are called.
267      **/
268     public interface TouchEventHandler {
269         /** Called each time {@link PhoneStatusBarView#onInterceptTouchEvent} is called. */
onInterceptTouchEvent(MotionEvent event)270         void onInterceptTouchEvent(MotionEvent event);
271 
272         /**
273          * Called each time {@link PhoneStatusBarView#onTouchEvent} is called.
274          *
275          * Should return true if the touch was handled by this handler and false otherwise. The
276          * return value from the handler will be returned from
277          * {@link PhoneStatusBarView#onTouchEvent}.
278          */
handleTouchEvent(MotionEvent event)279         boolean handleTouchEvent(MotionEvent event);
280     }
281 }
282