1 /* 2 * Copyright (C) 2020 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.car.window; 18 19 import static android.view.WindowInsets.Type.statusBars; 20 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS; 21 22 import android.view.MotionEvent; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.view.ViewStub; 26 import android.view.WindowInsets; 27 28 import androidx.annotation.IdRes; 29 import androidx.annotation.MainThread; 30 31 import com.android.car.ui.FocusArea; 32 import com.android.internal.annotations.VisibleForTesting; 33 34 import java.util.ArrayList; 35 36 /** 37 * Owns a {@link View} that is present in SystemUIOverlayWindow. 38 */ 39 public class OverlayViewController { 40 protected static final int INVALID_INSET_SIDE = -1; 41 protected static final int NO_INSET_SIDE = 0; 42 43 private final int mStubId; 44 private final OverlayViewGlobalStateController mOverlayViewGlobalStateController; 45 46 private View mLayout; 47 48 protected final ArrayList<OverlayViewStateListener> mViewStateListeners = 49 new ArrayList<>(); 50 OverlayViewController(int stubId, OverlayViewGlobalStateController overlayViewGlobalStateController)51 public OverlayViewController(int stubId, 52 OverlayViewGlobalStateController overlayViewGlobalStateController) { 53 mLayout = null; 54 mStubId = stubId; 55 mOverlayViewGlobalStateController = overlayViewGlobalStateController; 56 } 57 58 /** 59 * Shows content of {@link OverlayViewController}. 60 * 61 * Should be used to show view externally and in particular by {@link OverlayViewMediator}. 62 */ 63 @MainThread start()64 public final void start() { 65 mOverlayViewGlobalStateController.showView(/* viewController= */ this, this::show); 66 } 67 68 /** 69 * Hides content of {@link OverlayViewController}. 70 * 71 * Should be used to hide view externally and in particular by {@link OverlayViewMediator}. 72 */ 73 @MainThread stop()74 public final void stop() { 75 mOverlayViewGlobalStateController.hideView(/* viewController= */ this, this::hide); 76 } 77 78 /** 79 * Inflate layout owned by controller. 80 */ 81 @MainThread inflate(ViewGroup baseLayout)82 public final void inflate(ViewGroup baseLayout) { 83 ViewStub viewStub = baseLayout.findViewById(mStubId); 84 mLayout = viewStub.inflate(); 85 onFinishInflate(); 86 } 87 88 /** 89 * Called once inflate finishes. 90 */ 91 @MainThread onFinishInflate()92 protected void onFinishInflate() { 93 // no-op 94 } 95 96 /** 97 * Touches will be passed to ONLY the top most OverlayViewController which have the highest 98 * z-ordering. This method will not be called for controllers that are not at the top. 99 */ 100 @MainThread onTouchEvent(View v, MotionEvent event)101 protected void onTouchEvent(View v, MotionEvent event) { 102 // no-op 103 } 104 105 /** 106 * Returns {@code true} if layout owned by controller has been inflated. 107 */ isInflated()108 public final boolean isInflated() { 109 return mLayout != null; 110 } 111 show()112 private void show() { 113 if (mLayout == null) { 114 // layout must be inflated before show() is called. 115 return; 116 } 117 showInternal(); 118 } 119 120 /** 121 * Subclasses should override this method to implement reveal animations and implement logic 122 * specific to when the layout owned by the controller is shown. 123 * 124 * Should only be overridden by Superclass but not called by any {@link OverlayViewMediator}. 125 */ 126 @MainThread showInternal()127 protected void showInternal() { 128 mLayout.setVisibility(View.VISIBLE); 129 for (OverlayViewStateListener l : mViewStateListeners) { 130 l.onVisibilityChanged(/* isVisible= */ true); 131 } 132 } 133 hide()134 private void hide() { 135 if (mLayout == null) { 136 // layout must be inflated before hide() is called. 137 return; 138 } 139 hideInternal(); 140 } 141 142 /** 143 * Subclasses should override this method to implement conceal animations and implement logic 144 * specific to when the layout owned by the controller is hidden. 145 * 146 * Should only be overridden by Superclass but not called by any {@link OverlayViewMediator}. 147 */ 148 @MainThread hideInternal()149 protected void hideInternal() { 150 mLayout.setVisibility(View.GONE); 151 for (OverlayViewStateListener l : mViewStateListeners) { 152 l.onVisibilityChanged(/* isVisible= */ false); 153 } 154 } 155 156 /** 157 * Provides access to layout owned by controller. 158 */ getLayout()159 protected final View getLayout() { 160 return mLayout; 161 } 162 163 /** Returns the {@link OverlayViewGlobalStateController}. */ getOverlayViewGlobalStateController()164 protected final OverlayViewGlobalStateController getOverlayViewGlobalStateController() { 165 return mOverlayViewGlobalStateController; 166 } 167 168 /** Returns whether the view controlled by this controller is visible. */ isVisible()169 public final boolean isVisible() { 170 return mLayout.getVisibility() == View.VISIBLE; 171 } 172 173 /** 174 * Returns the ID of the focus area that should receive focus when this view is the 175 * topmost view or {@link View#NO_ID} if there is no focus area. 176 */ 177 @IdRes getFocusAreaViewId()178 protected int getFocusAreaViewId() { 179 return View.NO_ID; 180 } 181 182 /** Returns whether the view controlled by this controller has rotary focus. */ hasRotaryFocus()183 protected final boolean hasRotaryFocus() { 184 return !mLayout.isInTouchMode() && mLayout.hasFocus(); 185 } 186 187 /** 188 * Sets whether this view allows rotary focus. This should be set to {@code true} for the 189 * topmost layer in the overlay window and {@code false} for the others. 190 * 191 * @return true if the rotary focus allowed state has changed. 192 */ setAllowRotaryFocus(boolean allowRotaryFocus)193 public boolean setAllowRotaryFocus(boolean allowRotaryFocus) { 194 if (!isInflated() || !(mLayout instanceof ViewGroup)) { 195 return false; 196 } 197 198 ViewGroup viewGroup = (ViewGroup) mLayout; 199 int newFocusability = allowRotaryFocus 200 ? ViewGroup.FOCUS_BEFORE_DESCENDANTS 201 : ViewGroup.FOCUS_BLOCK_DESCENDANTS; 202 if (viewGroup.getDescendantFocusability() == newFocusability) { 203 return false; 204 } 205 viewGroup.setDescendantFocusability(newFocusability); 206 return true; 207 } 208 209 /** 210 * Refreshes the rotary focus in this view if we are in rotary mode. If the view already has 211 * rotary focus, it leaves the focus alone. Returns {@code true} if a new view was focused. 212 */ refreshRotaryFocusIfNeeded()213 public boolean refreshRotaryFocusIfNeeded() { 214 if (mLayout.isInTouchMode()) { 215 return false; 216 } 217 218 if (hasRotaryFocus()) { 219 return false; 220 } 221 222 View view = mLayout.findViewById(getFocusAreaViewId()); 223 if (view == null || !(view instanceof FocusArea)) { 224 return mLayout.requestFocus(); 225 } 226 227 FocusArea focusArea = (FocusArea) view; 228 return focusArea.performAccessibilityAction(ACTION_FOCUS, /* arguments= */ null); 229 } 230 231 /** 232 * Returns {@code true} if heads up notifications should be displayed over this view. 233 */ shouldShowHUN()234 protected boolean shouldShowHUN() { 235 return true; 236 } 237 238 /** 239 * Returns {@code true} if navigation bar insets should be displayed over this view. Has no 240 * effect if {@link #shouldFocusWindow} returns {@code false}. 241 */ shouldShowNavigationBarInsets()242 protected boolean shouldShowNavigationBarInsets() { 243 return false; 244 } 245 246 /** 247 * Returns {@code true} if status bar insets should be displayed over this view. Has no 248 * effect if {@link #shouldFocusWindow} returns {@code false}. 249 */ shouldShowStatusBarInsets()250 protected boolean shouldShowStatusBarInsets() { 251 return false; 252 } 253 254 /** 255 * Returns {@code true} if this view should be hidden during the occluded state. 256 */ shouldShowWhenOccluded()257 protected boolean shouldShowWhenOccluded() { 258 return false; 259 } 260 261 /** 262 * Returns {@code true} if the window should be focued when this view is visible. Note that 263 * returning {@code false} here means that {@link #shouldShowStatusBarInsets} and 264 * {@link #shouldShowNavigationBarInsets} will have no effect. 265 */ shouldFocusWindow()266 protected boolean shouldFocusWindow() { 267 return true; 268 } 269 270 /** 271 * Returns the amount of dimming to apply to the overlay window when initially brought to front. 272 * Range is from 1.0 for completely opaque to 0.0 for no dim. 273 */ getDefaultDimAmount()274 protected float getDefaultDimAmount() { 275 return 0f; 276 } 277 278 /** 279 * Returns {@code true} if the window should use stable insets. Using stable insets means that 280 * even when system bars are temporarily not visible, inset from the system bars will still be 281 * applied. 282 * 283 * NOTE: When system bars are hidden in transient mode, insets from them will not be applied 284 * even when the system bars become visible. Setting the return value to {@true} here can 285 * prevent the OverlayView from overlapping with the system bars when that happens. 286 */ shouldUseStableInsets()287 protected boolean shouldUseStableInsets() { 288 return false; 289 } 290 291 /** 292 * Returns the insets types to fit to the sysui overlay window when this 293 * {@link OverlayViewController} is in the foreground. 294 */ 295 @WindowInsets.Type.InsetsType getInsetTypesToFit()296 protected int getInsetTypesToFit() { 297 return statusBars(); 298 } 299 300 /** 301 * Optionally returns the sides of enabled system bar insets to fit to the sysui overlay window 302 * when this {@link OverlayViewController} is in the foreground. 303 * 304 * For example, if the bottom and left system bars are enabled and this method returns 305 * WindowInsets.Side.LEFT, then the inset from the bottom system bar will be ignored. 306 * 307 * NOTE: By default, this method returns {@link #INVALID_INSET_SIDE}, so insets to fit are 308 * defined by {@link #getInsetTypesToFit()}, and not by this method, unless it is overridden 309 * by subclasses. 310 * 311 * NOTE: {@link #NO_INSET_SIDE} signifies no insets from any system bars will be honored. Each 312 * {@link OverlayViewController} can first take this value and add sides of the system bar 313 * insets to honor to it. 314 * 315 * NOTE: If getInsetSidesToFit is overridden to return {@link WindowInsets.Side}, it always 316 * takes precedence over {@link #getInsetTypesToFit()}. That is, the return value of {@link 317 * #getInsetTypesToFit()} will be ignored. 318 */ 319 @WindowInsets.Side.InsetsSide getInsetSidesToFit()320 protected int getInsetSidesToFit() { 321 return INVALID_INSET_SIDE; 322 } 323 324 /** Interface for listening to the state of the overlay panel view. */ 325 public interface OverlayViewStateListener { 326 327 /** Called when the panel's visibility changes. */ onVisibilityChanged(boolean isVisible)328 void onVisibilityChanged(boolean isVisible); 329 } 330 331 /** 332 * Add a new listener to the state of this overlay panel view. 333 */ registerViewStateListener(OverlayViewStateListener listener)334 public void registerViewStateListener(OverlayViewStateListener listener) { 335 mViewStateListeners.add(listener); 336 } 337 338 /** 339 * Removes listener for state of this overlay panel view. 340 */ removePanelViewStateListener(OverlayViewStateListener listener)341 public void removePanelViewStateListener(OverlayViewStateListener listener) { 342 mViewStateListeners.remove(listener); 343 } 344 345 @VisibleForTesting setLayout(View layout)346 public void setLayout(View layout) { 347 mLayout = layout; 348 } 349 } 350