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