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