| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.car.window; |
| |
| import static android.view.WindowInsets.Type.statusBars; |
| import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS; |
| |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewStub; |
| import android.view.WindowInsets; |
| |
| import androidx.annotation.IdRes; |
| import androidx.annotation.MainThread; |
| |
| import com.android.car.ui.FocusArea; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * Owns a {@link View} that is present in SystemUIOverlayWindow. |
| */ |
| public class OverlayViewController { |
| protected static final int INVALID_INSET_SIDE = -1; |
| protected static final int NO_INSET_SIDE = 0; |
| |
| private final int mStubId; |
| private final OverlayViewGlobalStateController mOverlayViewGlobalStateController; |
| |
| private View mLayout; |
| |
| protected final ArrayList<OverlayViewStateListener> mViewStateListeners = |
| new ArrayList<>(); |
| |
| public OverlayViewController(int stubId, |
| OverlayViewGlobalStateController overlayViewGlobalStateController) { |
| mLayout = null; |
| mStubId = stubId; |
| mOverlayViewGlobalStateController = overlayViewGlobalStateController; |
| } |
| |
| /** |
| * Shows content of {@link OverlayViewController}. |
| * |
| * Should be used to show view externally and in particular by {@link OverlayViewMediator}. |
| */ |
| @MainThread |
| public final void start() { |
| mOverlayViewGlobalStateController.showView(/* viewController= */ this, this::show); |
| } |
| |
| /** |
| * Hides content of {@link OverlayViewController}. |
| * |
| * Should be used to hide view externally and in particular by {@link OverlayViewMediator}. |
| */ |
| @MainThread |
| public final void stop() { |
| mOverlayViewGlobalStateController.hideView(/* viewController= */ this, this::hide); |
| } |
| |
| /** |
| * Inflate layout owned by controller. |
| */ |
| @MainThread |
| public final void inflate(ViewGroup baseLayout) { |
| ViewStub viewStub = baseLayout.findViewById(mStubId); |
| mLayout = viewStub.inflate(); |
| onFinishInflate(); |
| } |
| |
| /** |
| * Called once inflate finishes. |
| */ |
| @MainThread |
| protected void onFinishInflate() { |
| // no-op |
| } |
| |
| /** |
| * Returns {@code true} if layout owned by controller has been inflated. |
| */ |
| public final boolean isInflated() { |
| return mLayout != null; |
| } |
| |
| private void show() { |
| if (mLayout == null) { |
| // layout must be inflated before show() is called. |
| return; |
| } |
| showInternal(); |
| } |
| |
| /** |
| * Subclasses should override this method to implement reveal animations and implement logic |
| * specific to when the layout owned by the controller is shown. |
| * |
| * Should only be overridden by Superclass but not called by any {@link OverlayViewMediator}. |
| */ |
| @MainThread |
| protected void showInternal() { |
| mLayout.setVisibility(View.VISIBLE); |
| for (OverlayViewStateListener l : mViewStateListeners) { |
| l.onVisibilityChanged(/* isVisible= */ true); |
| } |
| } |
| |
| private void hide() { |
| if (mLayout == null) { |
| // layout must be inflated before hide() is called. |
| return; |
| } |
| hideInternal(); |
| } |
| |
| /** |
| * Subclasses should override this method to implement conceal animations and implement logic |
| * specific to when the layout owned by the controller is hidden. |
| * |
| * Should only be overridden by Superclass but not called by any {@link OverlayViewMediator}. |
| */ |
| @MainThread |
| protected void hideInternal() { |
| mLayout.setVisibility(View.GONE); |
| for (OverlayViewStateListener l : mViewStateListeners) { |
| l.onVisibilityChanged(/* isVisible= */ false); |
| } |
| } |
| |
| /** |
| * Provides access to layout owned by controller. |
| */ |
| protected final View getLayout() { |
| return mLayout; |
| } |
| |
| /** Returns the {@link OverlayViewGlobalStateController}. */ |
| protected final OverlayViewGlobalStateController getOverlayViewGlobalStateController() { |
| return mOverlayViewGlobalStateController; |
| } |
| |
| /** Returns whether the view controlled by this controller is visible. */ |
| public final boolean isVisible() { |
| return mLayout.getVisibility() == View.VISIBLE; |
| } |
| |
| /** |
| * Returns the ID of the focus area that should receive focus when this view is the |
| * topmost view or {@link View#NO_ID} if there is no focus area. |
| */ |
| @IdRes |
| protected int getFocusAreaViewId() { |
| return View.NO_ID; |
| } |
| |
| /** Returns whether the view controlled by this controller has rotary focus. */ |
| protected final boolean hasRotaryFocus() { |
| return !mLayout.isInTouchMode() && mLayout.hasFocus(); |
| } |
| |
| /** |
| * Sets whether this view allows rotary focus. This should be set to {@code true} for the |
| * topmost layer in the overlay window and {@code false} for the others. |
| */ |
| public void setAllowRotaryFocus(boolean allowRotaryFocus) { |
| if (!isInflated()) { |
| return; |
| } |
| |
| if (!(mLayout instanceof ViewGroup)) { |
| return; |
| } |
| |
| ViewGroup viewGroup = (ViewGroup) mLayout; |
| viewGroup.setDescendantFocusability(allowRotaryFocus |
| ? ViewGroup.FOCUS_BEFORE_DESCENDANTS |
| : ViewGroup.FOCUS_BLOCK_DESCENDANTS); |
| } |
| |
| /** |
| * Refreshes the rotary focus in this view if we are in rotary mode. If the view already has |
| * rotary focus, it leaves the focus alone. Returns {@code true} if a new view was focused. |
| */ |
| public boolean refreshRotaryFocusIfNeeded() { |
| if (mLayout.isInTouchMode()) { |
| return false; |
| } |
| |
| if (hasRotaryFocus()) { |
| return false; |
| } |
| |
| View view = mLayout.findViewById(getFocusAreaViewId()); |
| if (view == null || !(view instanceof FocusArea)) { |
| return mLayout.requestFocus(); |
| } |
| |
| FocusArea focusArea = (FocusArea) view; |
| return focusArea.performAccessibilityAction(ACTION_FOCUS, /* arguments= */ null); |
| } |
| |
| /** |
| * Returns {@code true} if heads up notifications should be displayed over this view. |
| */ |
| protected boolean shouldShowHUN() { |
| return true; |
| } |
| |
| /** |
| * Returns {@code true} if navigation bar insets should be displayed over this view. Has no |
| * effect if {@link #shouldFocusWindow} returns {@code false}. |
| */ |
| protected boolean shouldShowNavigationBarInsets() { |
| return false; |
| } |
| |
| /** |
| * Returns {@code true} if status bar insets should be displayed over this view. Has no |
| * effect if {@link #shouldFocusWindow} returns {@code false}. |
| */ |
| protected boolean shouldShowStatusBarInsets() { |
| return false; |
| } |
| |
| /** |
| * Returns {@code true} if this view should be hidden during the occluded state. |
| */ |
| protected boolean shouldShowWhenOccluded() { |
| return false; |
| } |
| |
| /** |
| * Returns {@code true} if the window should be focued when this view is visible. Note that |
| * returning {@code false} here means that {@link #shouldShowStatusBarInsets} and |
| * {@link #shouldShowNavigationBarInsets} will have no effect. |
| */ |
| protected boolean shouldFocusWindow() { |
| return true; |
| } |
| |
| /** |
| * Returns {@code true} if the window should use stable insets. Using stable insets means that |
| * even when system bars are temporarily not visible, inset from the system bars will still be |
| * applied. |
| * |
| * NOTE: When system bars are hidden in transient mode, insets from them will not be applied |
| * even when the system bars become visible. Setting the return value to {@true} here can |
| * prevent the OverlayView from overlapping with the system bars when that happens. |
| */ |
| protected boolean shouldUseStableInsets() { |
| return false; |
| } |
| |
| /** |
| * Returns the insets types to fit to the sysui overlay window when this |
| * {@link OverlayViewController} is in the foreground. |
| */ |
| @WindowInsets.Type.InsetsType |
| protected int getInsetTypesToFit() { |
| return statusBars(); |
| } |
| |
| /** |
| * Optionally returns the sides of enabled system bar insets to fit to the sysui overlay window |
| * when this {@link OverlayViewController} is in the foreground. |
| * |
| * For example, if the bottom and left system bars are enabled and this method returns |
| * WindowInsets.Side.LEFT, then the inset from the bottom system bar will be ignored. |
| * |
| * NOTE: By default, this method returns {@link #INVALID_INSET_SIDE}, so insets to fit are |
| * defined by {@link #getInsetTypesToFit()}, and not by this method, unless it is overridden |
| * by subclasses. |
| * |
| * NOTE: {@link #NO_INSET_SIDE} signifies no insets from any system bars will be honored. Each |
| * {@link OverlayViewController} can first take this value and add sides of the system bar |
| * insets to honor to it. |
| * |
| * NOTE: If getInsetSidesToFit is overridden to return {@link WindowInsets.Side}, it always |
| * takes precedence over {@link #getInsetTypesToFit()}. That is, the return value of {@link |
| * #getInsetTypesToFit()} will be ignored. |
| */ |
| @WindowInsets.Side.InsetsSide |
| protected int getInsetSidesToFit() { |
| return INVALID_INSET_SIDE; |
| } |
| |
| /** Interface for listening to the state of the overlay panel view. */ |
| public interface OverlayViewStateListener { |
| |
| /** Called when the panel's visibility changes. */ |
| void onVisibilityChanged(boolean isVisible); |
| } |
| |
| /** |
| * Add a new listener to the state of this overlay panel view. |
| */ |
| public void registerViewStateListener(OverlayViewStateListener listener) { |
| mViewStateListeners.add(listener); |
| } |
| |
| /** |
| * Removes listener for state of this overlay panel view. |
| */ |
| public void removePanelViewStateListener(OverlayViewStateListener listener) { |
| mViewStateListeners.remove(listener); |
| } |
| |
| @VisibleForTesting |
| public void setLayout(View layout) { |
| mLayout = layout; |
| } |
| } |