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