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