• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      * Callback for the individual view controllers when the window focusable state has changed.
189      * This will only go to the highest z-order window and will be re-called when the window
190      * visibilities change.
191      */
onWindowFocusableChanged(boolean focusable)192     public void onWindowFocusableChanged(boolean focusable) {
193     }
194 
195     /**
196      * Sets whether this view allows rotary focus. This should be set to {@code true} for the
197      * topmost layer in the overlay window and {@code false} for the others.
198      *
199      * @return true if the rotary focus allowed state has changed.
200      */
setAllowRotaryFocus(boolean allowRotaryFocus)201     public boolean setAllowRotaryFocus(boolean allowRotaryFocus) {
202         if (!isInflated() || !(mLayout instanceof ViewGroup)) {
203             return false;
204         }
205 
206         ViewGroup viewGroup = (ViewGroup) mLayout;
207         int newFocusability = allowRotaryFocus
208                 ? ViewGroup.FOCUS_BEFORE_DESCENDANTS
209                 : ViewGroup.FOCUS_BLOCK_DESCENDANTS;
210         if (viewGroup.getDescendantFocusability() == newFocusability) {
211             return false;
212         }
213         viewGroup.setDescendantFocusability(newFocusability);
214         return true;
215     }
216 
217     /**
218      * Refreshes the rotary focus in this view if we are in rotary mode. If the view already has
219      * rotary focus, it leaves the focus alone. Returns {@code true} if a new view was focused.
220      */
refreshRotaryFocusIfNeeded()221     public boolean refreshRotaryFocusIfNeeded() {
222         if (mLayout.isInTouchMode()) {
223             return false;
224         }
225 
226         if (hasRotaryFocus()) {
227             return false;
228         }
229 
230         View view = mLayout.findViewById(getFocusAreaViewId());
231         if (view == null || !(view instanceof FocusArea)) {
232             return mLayout.requestFocus();
233         }
234 
235         FocusArea focusArea = (FocusArea) view;
236         return focusArea.performAccessibilityAction(ACTION_FOCUS, /* arguments= */ null);
237     }
238 
239     /**
240      * Returns {@code true} if heads up notifications should be displayed over this view.
241      */
shouldShowHUN()242     protected boolean shouldShowHUN() {
243         return true;
244     }
245 
246     /**
247      * Returns {@code true} if navigation bar insets should be displayed over this view. Has no
248      * effect if {@link #shouldFocusWindow} returns {@code false}.
249      */
shouldShowNavigationBarInsets()250     protected boolean shouldShowNavigationBarInsets() {
251         return false;
252     }
253 
254     /**
255      * Returns {@code true} if status bar insets should be displayed over this view. Has no
256      * effect if {@link #shouldFocusWindow} returns {@code false}.
257      */
shouldShowStatusBarInsets()258     protected boolean shouldShowStatusBarInsets() {
259         return false;
260     }
261 
262     /**
263      * Returns {@code true} if this view should be hidden during the occluded state.
264      */
shouldShowWhenOccluded()265     protected boolean shouldShowWhenOccluded() {
266         return false;
267     }
268 
269     /**
270      * Returns {@code true} if the window should be focued when this view is visible. Note that
271      * returning {@code false} here means that {@link #shouldShowStatusBarInsets} and
272      * {@link #shouldShowNavigationBarInsets} will have no effect.
273      */
shouldFocusWindow()274     protected boolean shouldFocusWindow() {
275         return true;
276     }
277 
278     /**
279      * Returns the amount of dimming to apply to the overlay window when initially brought to front.
280      * Range is from 1.0 for completely opaque to 0.0 for no dim.
281      */
getDefaultDimAmount()282     protected float getDefaultDimAmount() {
283         return 0f;
284     }
285 
286     /**
287      * Returns {@code true} if the window should use stable insets. Using stable insets means that
288      * even when system bars are temporarily not visible, inset from the system bars will still be
289      * applied.
290      *
291      * NOTE: When system bars are hidden in transient mode, insets from them will not be applied
292      * even when the system bars become visible. Setting the return value to {@true} here can
293      * prevent the OverlayView from overlapping with the system bars when that happens.
294      */
shouldUseStableInsets()295     protected boolean shouldUseStableInsets() {
296         return false;
297     }
298 
299     /**
300      * Returns the insets types to fit to the sysui overlay window when this
301      * {@link OverlayViewController} is in the foreground.
302      */
303     @WindowInsets.Type.InsetsType
getInsetTypesToFit()304     protected int getInsetTypesToFit() {
305         return statusBars();
306     }
307 
308     /**
309      * Optionally returns the sides of enabled system bar insets to fit to the sysui overlay window
310      * when this {@link OverlayViewController} is in the foreground.
311      *
312      * For example, if the bottom and left system bars are enabled and this method returns
313      * WindowInsets.Side.LEFT, then the inset from the bottom system bar will be ignored.
314      *
315      * NOTE: By default, this method returns {@link #INVALID_INSET_SIDE}, so insets to fit are
316      * defined by {@link #getInsetTypesToFit()}, and not by this method, unless it is overridden
317      * by subclasses.
318      *
319      * NOTE: {@link #NO_INSET_SIDE} signifies no insets from any system bars will be honored. Each
320      * {@link OverlayViewController} can first take this value and add sides of the system bar
321      * insets to honor to it.
322      *
323      * NOTE: If getInsetSidesToFit is overridden to return {@link WindowInsets.Side}, it always
324      * takes precedence over {@link #getInsetTypesToFit()}. That is, the return value of {@link
325      * #getInsetTypesToFit()} will be ignored.
326      */
327     @WindowInsets.Side.InsetsSide
getInsetSidesToFit()328     protected int getInsetSidesToFit() {
329         return INVALID_INSET_SIDE;
330     }
331 
332     /** Interface for listening to the state of the overlay panel view. */
333     public interface OverlayViewStateListener {
334 
335         /** Called when the panel's visibility changes. */
onVisibilityChanged(boolean isVisible)336         void onVisibilityChanged(boolean isVisible);
337     }
338 
339     /**
340      * Add a new listener to the state of this overlay panel view.
341      */
registerViewStateListener(OverlayViewStateListener listener)342     public void registerViewStateListener(OverlayViewStateListener listener) {
343         mViewStateListeners.add(listener);
344     }
345 
346     /**
347      * Removes listener for state of this overlay panel view.
348      */
removePanelViewStateListener(OverlayViewStateListener listener)349     public void removePanelViewStateListener(OverlayViewStateListener listener) {
350         mViewStateListeners.remove(listener);
351     }
352 
353     @VisibleForTesting
setLayout(View layout)354     public void setLayout(View layout) {
355         mLayout = layout;
356     }
357 }
358