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