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