blob: 655007cf3cd1a981c40b4e6813d006819d34e87f [file] [log] [blame]
/*
* Copyright (C) 2018 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.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.InsetsState.ITYPE_CAPTION_BAR;
import static android.view.InsetsState.ITYPE_CLIMATE_BAR;
import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
import static android.view.InsetsState.ITYPE_IME;
import static android.view.InsetsState.ITYPE_INVALID;
import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
import static android.view.InsetsState.ITYPE_STATUS_BAR;
import static android.view.WindowInsets.Type.displayCutout;
import static android.view.WindowInsets.Type.mandatorySystemGestures;
import static android.view.WindowInsets.Type.systemGestures;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IME;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WindowConfiguration;
import android.app.WindowConfiguration.WindowingMode;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.InsetsState.InternalInsetsType;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams.WindowType;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.inputmethod.InputMethodManagerInternal;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.function.Consumer;
/**
* Manages global window inset state in the system represented by {@link InsetsState}.
*/
class InsetsStateController {
private final InsetsState mLastState = new InsetsState();
private final InsetsState mState = new InsetsState();
private final DisplayContent mDisplayContent;
private final ArrayMap<Integer, InsetsSourceProvider> mProviders = new ArrayMap<>();
private final ArrayMap<InsetsControlTarget, ArrayList<Integer>> mControlTargetTypeMap =
new ArrayMap<>();
private final SparseArray<InsetsControlTarget> mTypeControlTargetMap = new SparseArray<>();
/** @see #onControlFakeTargetChanged */
private final SparseArray<InsetsControlTarget> mTypeFakeControlTargetMap = new SparseArray<>();
private final ArraySet<InsetsControlTarget> mPendingControlChanged = new ArraySet<>();
private final Consumer<WindowState> mDispatchInsetsChanged = w -> {
if (w.isReadyToDispatchInsetsState()) {
w.notifyInsetsChanged();
}
};
private final InsetsControlTarget mEmptyImeControlTarget = new InsetsControlTarget() {
@Override
public void notifyInsetsControlChanged() {
InsetsSourceControl[] controls = getControlsForDispatch(this);
if (controls == null) {
return;
}
for (InsetsSourceControl control : controls) {
if (control.getType() == ITYPE_IME) {
mDisplayContent.mWmService.mH.post(() ->
InputMethodManagerInternal.get().removeImeSurface());
}
}
}
};
InsetsStateController(DisplayContent displayContent) {
mDisplayContent = displayContent;
}
/**
* Gets the insets state from the perspective of the target. When performing layout of the
* target or dispatching insets to the target, we need to exclude sources which should not be
* visible to the target. e.g., the source which represents the target window itself, and the
* IME source when the target is above IME. We also need to exclude certain types of insets
* source for client within specific windowing modes.
* This is to get the insets for a window layout on the screen. If the window is not there, use
* the {@link #getInsetsForWindowMetrics} to get insets instead.
*
* @param target The window associate with the perspective.
* @return The state stripped of the necessary information.
*/
InsetsState getInsetsForWindow(@NonNull WindowState target) {
final InsetsState rotatedState = target.mToken.getFixedRotationTransformInsetsState();
if (rotatedState != null) {
return rotatedState;
}
final InsetsSourceProvider provider = target.getControllableInsetProvider();
final @InternalInsetsType int type = provider != null
? provider.getSource().getType() : ITYPE_INVALID;
return getInsetsForTarget(type, target.getWindowingMode(), target.isAlwaysOnTop(),
target.getFrozenInsetsState() != null ? target.getFrozenInsetsState() :
(target.mAttrs.receiveInsetsIgnoringZOrder ? mState :
target.mAboveInsetsState));
}
InsetsState getInsetsForWindowMetrics(@NonNull WindowManager.LayoutParams attrs) {
final @InternalInsetsType int type = getInsetsTypeForLayoutParams(attrs);
final WindowToken token = mDisplayContent.getWindowToken(attrs.token);
if (token != null) {
final InsetsState rotatedState = token.getFixedRotationTransformInsetsState();
if (rotatedState != null) {
return rotatedState;
}
}
final @WindowingMode int windowingMode = token != null
? token.getWindowingMode() : WINDOWING_MODE_UNDEFINED;
final boolean alwaysOnTop = token != null && token.isAlwaysOnTop();
return getInsetsForTarget(type, windowingMode, alwaysOnTop, mState);
}
private static @InternalInsetsType
int getInsetsTypeForLayoutParams(WindowManager.LayoutParams attrs) {
@WindowType int type = attrs.type;
switch (type) {
case TYPE_STATUS_BAR:
return ITYPE_STATUS_BAR;
case TYPE_NAVIGATION_BAR:
return ITYPE_NAVIGATION_BAR;
case TYPE_INPUT_METHOD:
return ITYPE_IME;
}
// If not one of the types above, check whether an internal inset mapping is specified.
if (attrs.providesInsetsTypes != null) {
for (@InternalInsetsType int insetsType : attrs.providesInsetsTypes) {
switch (insetsType) {
case ITYPE_STATUS_BAR:
case ITYPE_NAVIGATION_BAR:
case ITYPE_CLIMATE_BAR:
case ITYPE_EXTRA_NAVIGATION_BAR:
return insetsType;
}
}
}
return ITYPE_INVALID;
}
/**
* @see #getInsetsForWindow
* @see #getInsetsForWindowMetrics
*/
private InsetsState getInsetsForTarget(@InternalInsetsType int type,
@WindowingMode int windowingMode, boolean isAlwaysOnTop, InsetsState state) {
boolean stateCopied = false;
if (type != ITYPE_INVALID) {
state = new InsetsState(state);
stateCopied = true;
state.removeSource(type);
// Navigation bar doesn't get influenced by anything else
if (type == ITYPE_NAVIGATION_BAR || type == ITYPE_EXTRA_NAVIGATION_BAR) {
state.removeSource(ITYPE_IME);
state.removeSource(ITYPE_STATUS_BAR);
state.removeSource(ITYPE_CLIMATE_BAR);
state.removeSource(ITYPE_CAPTION_BAR);
state.removeSource(ITYPE_NAVIGATION_BAR);
state.removeSource(ITYPE_EXTRA_NAVIGATION_BAR);
}
// Status bar doesn't get influenced by caption bar
if (type == ITYPE_STATUS_BAR || type == ITYPE_CLIMATE_BAR) {
state.removeSource(ITYPE_CAPTION_BAR);
}
// IME needs different frames for certain cases (e.g. navigation bar in gesture nav).
if (type == ITYPE_IME) {
for (int i = mProviders.size() - 1; i >= 0; i--) {
InsetsSourceProvider otherProvider = mProviders.valueAt(i);
if (otherProvider.overridesImeFrame()) {
InsetsSource override =
new InsetsSource(
state.getSource(otherProvider.getSource().getType()));
override.setFrame(otherProvider.getImeOverrideFrame());
state.addSource(override);
}
}
}
}
if (WindowConfiguration.isFloating(windowingMode)
|| (windowingMode == WINDOWING_MODE_MULTI_WINDOW && isAlwaysOnTop)) {
if (!stateCopied) {
state = new InsetsState(state);
stateCopied = true;
}
state.removeSource(ITYPE_STATUS_BAR);
state.removeSource(ITYPE_NAVIGATION_BAR);
state.removeSource(ITYPE_EXTRA_NAVIGATION_BAR);
if (windowingMode == WINDOWING_MODE_PINNED) {
state.removeSource(ITYPE_IME);
}
}
return state;
}
InsetsState getRawInsetsState() {
return mState;
}
@Nullable InsetsSourceControl[] getControlsForDispatch(InsetsControlTarget target) {
ArrayList<Integer> controlled = mControlTargetTypeMap.get(target);
if (controlled == null) {
return null;
}
final int size = controlled.size();
final InsetsSourceControl[] result = new InsetsSourceControl[size];
for (int i = 0; i < size; i++) {
result[i] = mProviders.get(controlled.get(i)).getControl(target);
}
return result;
}
public void addProvidersToTransition() {
for (int i = mProviders.size() - 1; i >= 0; --i) {
final InsetsSourceProvider p = mProviders.valueAt(i);
if (p == null) continue;
final WindowContainer wc = p.mWin;
if (wc == null) continue;
mDisplayContent.mAtmService.getTransitionController().collect(wc);
}
}
/**
* @return The provider of a specific type.
*/
InsetsSourceProvider getSourceProvider(@InternalInsetsType int type) {
if (type == ITYPE_IME) {
return mProviders.computeIfAbsent(type,
key -> new ImeInsetsSourceProvider(
mState.getSource(key), this, mDisplayContent));
} else {
return mProviders.computeIfAbsent(type,
key -> new InsetsSourceProvider(mState.getSource(key), this, mDisplayContent));
}
}
ImeInsetsSourceProvider getImeSourceProvider() {
return (ImeInsetsSourceProvider) getSourceProvider(ITYPE_IME);
}
/**
* @return The provider of a specific type or null if we don't have it.
*/
@Nullable InsetsSourceProvider peekSourceProvider(@InternalInsetsType int type) {
return mProviders.get(type);
}
/**
* Called when a layout pass has occurred.
*/
void onPostLayout() {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ISC.onPostLayout");
for (int i = mProviders.size() - 1; i >= 0; i--) {
mProviders.valueAt(i).onPostLayout();
}
final ArrayList<WindowState> winInsetsChanged = mDisplayContent.mWinInsetsChanged;
if (!mLastState.equals(mState)) {
mLastState.set(mState, true /* copySources */);
notifyInsetsChanged();
} else {
// The global insets state has not changed but there might be windows whose conditions
// (e.g., z-order) have changed. They can affect the insets states that we dispatch to
// the clients.
for (int i = winInsetsChanged.size() - 1; i >= 0; i--) {
mDispatchInsetsChanged.accept(winInsetsChanged.get(i));
}
}
winInsetsChanged.clear();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
/**
* Updates {@link WindowState#mAboveInsetsState} for all windows in the display while the
* z-order of a window is changed.
*
* @param win The window whose z-order has changed.
* @param notifyInsetsChange {@code true} if the clients should be notified about the change.
*/
void updateAboveInsetsState(WindowState win, boolean notifyInsetsChange) {
if (win == null || win.getDisplayContent() != mDisplayContent) {
return;
}
final boolean[] aboveWin = { true };
final InsetsState aboveInsetsState = new InsetsState();
aboveInsetsState.set(mState,
displayCutout() | systemGestures() | mandatorySystemGestures());
final SparseArray<InsetsSource> winProvidedSources = win.mProvidedInsetsSources;
final ArrayList<WindowState> insetsChangedWindows = new ArrayList<>();
mDisplayContent.forAllWindows(w -> {
if (aboveWin[0]) {
if (w == win) {
aboveWin[0] = false;
if (!win.mAboveInsetsState.equals(aboveInsetsState)) {
win.mAboveInsetsState.set(aboveInsetsState);
insetsChangedWindows.add(win);
}
return winProvidedSources.size() == 0;
} else {
final SparseArray<InsetsSource> providedSources = w.mProvidedInsetsSources;
for (int i = providedSources.size() - 1; i >= 0; i--) {
aboveInsetsState.addSource(providedSources.valueAt(i));
}
if (winProvidedSources.size() == 0) {
return false;
}
boolean changed = false;
for (int i = winProvidedSources.size() - 1; i >= 0; i--) {
changed |= w.mAboveInsetsState.removeSource(winProvidedSources.keyAt(i));
}
if (changed) {
insetsChangedWindows.add(w);
}
}
} else {
for (int i = winProvidedSources.size() - 1; i >= 0; i--) {
w.mAboveInsetsState.addSource(winProvidedSources.valueAt(i));
}
insetsChangedWindows.add(w);
}
return false;
}, true /* traverseTopToBottom */);
if (notifyInsetsChange) {
for (int i = insetsChangedWindows.size() - 1; i >= 0; i--) {
mDispatchInsetsChanged.accept(insetsChangedWindows.get(i));
}
}
}
void onDisplayInfoUpdated(boolean notifyInsetsChange) {
final ArrayList<WindowState> insetsChangedWindows = new ArrayList<>();
mDisplayContent.forAllWindows(w -> {
w.mAboveInsetsState.set(mState, displayCutout());
insetsChangedWindows.add(w);
}, true /* traverseTopToBottom */);
if (notifyInsetsChange) {
for (int i = insetsChangedWindows.size() - 1; i >= 0; i--) {
mDispatchInsetsChanged.accept(insetsChangedWindows.get(i));
}
}
}
void onInsetsModified(InsetsControlTarget caller) {
boolean changed = false;
for (int i = mProviders.size() - 1; i >= 0; i--) {
changed |= mProviders.valueAt(i).updateClientVisibility(caller);
}
if (changed) {
notifyInsetsChanged();
mDisplayContent.updateSystemGestureExclusion();
mDisplayContent.getDisplayPolicy().updateSystemUiVisibilityLw();
}
}
/**
* Computes insets state of the insets provider window in the display frames.
*
* @param win The owner window of insets provider.
* @param displayFrames The display frames to create insets source.
* @param windowFrames The specified frames to represent the owner window.
*/
void computeSimulatedState(WindowState win, DisplayFrames displayFrames,
WindowFrames windowFrames) {
final InsetsState state = displayFrames.mInsetsState;
for (int i = mProviders.size() - 1; i >= 0; i--) {
final InsetsSourceProvider provider = mProviders.valueAt(i);
if (provider.mWin == win) {
state.addSource(provider.createSimulatedSource(displayFrames, windowFrames));
}
}
}
boolean isFakeTarget(@InternalInsetsType int type, InsetsControlTarget target) {
return mTypeFakeControlTargetMap.get(type) == target;
}
void onImeControlTargetChanged(@Nullable InsetsControlTarget imeTarget) {
// Make sure that we always have a control target for the IME, even if the IME target is
// null. Otherwise there is no leash that will hide it and IME becomes "randomly" visible.
InsetsControlTarget target = imeTarget != null ? imeTarget : mEmptyImeControlTarget;
onControlChanged(ITYPE_IME, target);
ProtoLog.d(WM_DEBUG_IME, "onImeControlTargetChanged %s",
target != null ? target.getWindow() : "null");
notifyPendingInsetsControlChanged();
}
/**
* Called when the focused window that is able to control the system bars changes.
*
* @param statusControlling The target that is now able to control the status bar appearance
* and visibility.
* @param navControlling The target that is now able to control the nav bar appearance
* and visibility.
*/
void onBarControlTargetChanged(@Nullable InsetsControlTarget statusControlling,
@Nullable InsetsControlTarget fakeStatusControlling,
@Nullable InsetsControlTarget navControlling,
@Nullable InsetsControlTarget fakeNavControlling) {
onControlChanged(ITYPE_STATUS_BAR, statusControlling);
onControlChanged(ITYPE_NAVIGATION_BAR, navControlling);
onControlChanged(ITYPE_CLIMATE_BAR, statusControlling);
onControlChanged(ITYPE_EXTRA_NAVIGATION_BAR, navControlling);
onControlFakeTargetChanged(ITYPE_STATUS_BAR, fakeStatusControlling);
onControlFakeTargetChanged(ITYPE_NAVIGATION_BAR, fakeNavControlling);
onControlFakeTargetChanged(ITYPE_CLIMATE_BAR, fakeStatusControlling);
onControlFakeTargetChanged(ITYPE_EXTRA_NAVIGATION_BAR, fakeNavControlling);
notifyPendingInsetsControlChanged();
}
void notifyControlRevoked(@NonNull InsetsControlTarget previousControlTarget,
InsetsSourceProvider provider) {
removeFromControlMaps(previousControlTarget, provider.getSource().getType(),
false /* fake */);
}
private void onControlChanged(@InternalInsetsType int type,
@Nullable InsetsControlTarget target) {
final InsetsControlTarget previous = mTypeControlTargetMap.get(type);
if (target == previous) {
return;
}
final InsetsSourceProvider provider = mProviders.get(type);
if (provider == null) {
return;
}
if (!provider.isControllable()) {
return;
}
provider.updateControlForTarget(target, false /* force */);
target = provider.getControlTarget();
if (previous != null) {
removeFromControlMaps(previous, type, false /* fake */);
mPendingControlChanged.add(previous);
}
if (target != null) {
addToControlMaps(target, type, false /* fake */);
mPendingControlChanged.add(target);
}
}
/**
* The fake target saved here will be used to pretend to the app that it's still under control
* of the bars while it's not really, but we still need to find out the apps intentions around
* showing/hiding. For example, when the transient bars are showing, and the fake target
* requests to show system bars, the transient state will be aborted.
*/
void onControlFakeTargetChanged(@InternalInsetsType int type,
@Nullable InsetsControlTarget fakeTarget) {
final InsetsControlTarget previous = mTypeFakeControlTargetMap.get(type);
if (fakeTarget == previous) {
return;
}
final InsetsSourceProvider provider = mProviders.get(type);
if (provider == null) {
return;
}
provider.updateControlForFakeTarget(fakeTarget);
if (previous != null) {
removeFromControlMaps(previous, type, true /* fake */);
mPendingControlChanged.add(previous);
}
if (fakeTarget != null) {
addToControlMaps(fakeTarget, type, true /* fake */);
mPendingControlChanged.add(fakeTarget);
}
}
private void removeFromControlMaps(@NonNull InsetsControlTarget target,
@InternalInsetsType int type, boolean fake) {
final ArrayList<Integer> array = mControlTargetTypeMap.get(target);
if (array == null) {
return;
}
array.remove((Integer) type);
if (array.isEmpty()) {
mControlTargetTypeMap.remove(target);
}
if (fake) {
mTypeFakeControlTargetMap.remove(type);
} else {
mTypeControlTargetMap.remove(type);
}
}
private void addToControlMaps(@NonNull InsetsControlTarget target,
@InternalInsetsType int type, boolean fake) {
final ArrayList<Integer> array = mControlTargetTypeMap.computeIfAbsent(target,
key -> new ArrayList<>());
array.add(type);
if (fake) {
mTypeFakeControlTargetMap.put(type, target);
} else {
mTypeControlTargetMap.put(type, target);
}
}
void notifyControlChanged(InsetsControlTarget target) {
mPendingControlChanged.add(target);
notifyPendingInsetsControlChanged();
}
private void notifyPendingInsetsControlChanged() {
if (mPendingControlChanged.isEmpty()) {
return;
}
mDisplayContent.mWmService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
for (int i = mProviders.size() - 1; i >= 0; i--) {
final InsetsSourceProvider provider = mProviders.valueAt(i);
provider.onSurfaceTransactionApplied();
}
final ArraySet<InsetsControlTarget> newControlTargets = new ArraySet<>();
for (int i = mPendingControlChanged.size() - 1; i >= 0; i--) {
final InsetsControlTarget controlTarget = mPendingControlChanged.valueAt(i);
controlTarget.notifyInsetsControlChanged();
if (mControlTargetTypeMap.containsKey(controlTarget)) {
// We only collect targets who get controls, not lose controls.
newControlTargets.add(controlTarget);
}
}
mPendingControlChanged.clear();
// This updates the insets visibilities AFTER sending current insets state and controls
// to the clients, so that the clients can change the current visibilities to the
// requested visibilities with animations.
for (int i = newControlTargets.size() - 1; i >= 0; i--) {
onInsetsModified(newControlTargets.valueAt(i));
}
newControlTargets.clear();
});
}
void notifyInsetsChanged() {
mDisplayContent.notifyInsetsChanged(mDispatchInsetsChanged);
}
void dump(String prefix, PrintWriter pw) {
pw.println(prefix + "WindowInsetsStateController");
prefix = prefix + " ";
mState.dump(prefix, pw);
pw.println(prefix + "Control map:");
for (int i = mTypeControlTargetMap.size() - 1; i >= 0; i--) {
pw.print(prefix + " ");
pw.println(InsetsState.typeToString(mTypeControlTargetMap.keyAt(i)) + " -> "
+ mTypeControlTargetMap.valueAt(i));
}
pw.println(prefix + "InsetsSourceProviders:");
for (int i = mProviders.size() - 1; i >= 0; i--) {
mProviders.valueAt(i).dump(pw, prefix + " ");
}
}
}