blob: ac6e4c2a65211febe8ce51f6a759026b6539e54f [file] [log] [blame]
/*
* Copyright (C) 2023 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.wm.shell.common;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED;
import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FOLDABLE;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.res.Configuration;
import android.os.SystemProperties;
import android.util.ArraySet;
import android.view.Surface;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellInit;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Wrapper class to track the tabletop (aka. flex) mode change on Fold-ables.
* See also <a
* href="https://developer.android.com/guide/topics/large-screens/learn-about-foldables
* #foldable_postures">Foldable states and postures</a> for reference.
*
* Use the {@link DevicePostureController} for more detailed posture changes.
*/
public class TabletopModeController implements
DevicePostureController.OnDevicePostureChangedListener,
DisplayController.OnDisplaysChangedListener {
/**
* When {@code true}, floating windows like PiP would auto move to the position
* specified by {@link #PREFER_TOP_HALF_IN_TABLETOP} when in tabletop mode.
*/
private static final boolean ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP =
SystemProperties.getBoolean(
"persist.wm.debug.enable_move_floating_window_in_tabletop", true);
/**
* Prefer the {@link #PREFERRED_TABLETOP_HALF_TOP} if this flag is enabled,
* {@link #PREFERRED_TABLETOP_HALF_BOTTOM} otherwise.
* See also {@link #getPreferredHalfInTabletopMode()}.
*/
private static final boolean PREFER_TOP_HALF_IN_TABLETOP =
SystemProperties.getBoolean("persist.wm.debug.prefer_top_half_in_tabletop", true);
private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000;
@IntDef(prefix = {"PREFERRED_TABLETOP_HALF_"}, value = {
PREFERRED_TABLETOP_HALF_TOP,
PREFERRED_TABLETOP_HALF_BOTTOM
})
@Retention(RetentionPolicy.SOURCE)
public @interface PreferredTabletopHalf {}
public static final int PREFERRED_TABLETOP_HALF_TOP = 0;
public static final int PREFERRED_TABLETOP_HALF_BOTTOM = 1;
private final Context mContext;
private final DevicePostureController mDevicePostureController;
private final DisplayController mDisplayController;
private final ShellExecutor mMainExecutor;
private final Set<Integer> mTabletopModeRotations = new ArraySet<>();
private final List<OnTabletopModeChangedListener> mListeners = new ArrayList<>();
@VisibleForTesting
final Runnable mOnEnterTabletopModeCallback = () -> {
if (isInTabletopMode()) {
// We are still in tabletop mode, go ahead.
mayBroadcastOnTabletopModeChange(true /* isInTabletopMode */);
}
};
@DevicePostureController.DevicePostureInt
private int mDevicePosture = DEVICE_POSTURE_UNKNOWN;
@Surface.Rotation
private int mDisplayRotation = WindowConfiguration.ROTATION_UNDEFINED;
/**
* Track the last callback value for {@link OnTabletopModeChangedListener}.
* This is to avoid duplicated {@code false} callback to {@link #mListeners}.
*/
private Boolean mLastIsInTabletopModeForCallback;
public TabletopModeController(Context context,
ShellInit shellInit,
DevicePostureController postureController,
DisplayController displayController,
@ShellMainThread ShellExecutor mainExecutor) {
mContext = context;
mDevicePostureController = postureController;
mDisplayController = displayController;
mMainExecutor = mainExecutor;
shellInit.addInitCallback(this::onInit, this);
}
@VisibleForTesting
void onInit() {
mDevicePostureController.registerOnDevicePostureChangedListener(this);
mDisplayController.addDisplayWindowListener(this);
// Aligns with what's in {@link com.android.server.wm.DisplayRotation}.
final int[] deviceTabletopRotations = mContext.getResources().getIntArray(
com.android.internal.R.array.config_deviceTabletopRotations);
if (deviceTabletopRotations == null || deviceTabletopRotations.length == 0) {
ProtoLog.e(WM_SHELL_FOLDABLE,
"No valid config_deviceTabletopRotations, can not tell"
+ " tabletop mode in WMShell");
return;
}
for (int angle : deviceTabletopRotations) {
switch (angle) {
case 0:
mTabletopModeRotations.add(Surface.ROTATION_0);
break;
case 90:
mTabletopModeRotations.add(Surface.ROTATION_90);
break;
case 180:
mTabletopModeRotations.add(Surface.ROTATION_180);
break;
case 270:
mTabletopModeRotations.add(Surface.ROTATION_270);
break;
default:
ProtoLog.e(WM_SHELL_FOLDABLE,
"Invalid surface rotation angle in "
+ "config_deviceTabletopRotations: %d",
angle);
break;
}
}
}
/**
* @return {@code true} if floating windows like PiP would auto move to the position
* specified by {@link #getPreferredHalfInTabletopMode()} when in tabletop mode.
*/
public boolean enableMoveFloatingWindowInTabletop() {
return ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP;
}
/** @return Preferred half for floating windows like PiP when in tabletop mode. */
@PreferredTabletopHalf
public int getPreferredHalfInTabletopMode() {
return PREFER_TOP_HALF_IN_TABLETOP
? PREFERRED_TABLETOP_HALF_TOP
: PREFERRED_TABLETOP_HALF_BOTTOM;
}
/** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */
public void registerOnTabletopModeChangedListener(
@NonNull OnTabletopModeChangedListener listener) {
if (listener == null || mListeners.contains(listener)) return;
mListeners.add(listener);
listener.onTabletopModeChanged(isInTabletopMode());
}
/** Unregister {@link OnTabletopModeChangedListener} for tabletop mode change. */
public void unregisterOnTabletopModeChangedListener(
@NonNull OnTabletopModeChangedListener listener) {
mListeners.remove(listener);
}
@Override
public void onDevicePostureChanged(@DevicePostureController.DevicePostureInt int posture) {
if (mDevicePosture != posture) {
onDevicePostureOrDisplayRotationChanged(posture, mDisplayRotation);
}
}
@Override
public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
final int newDisplayRotation = newConfig.windowConfiguration.getDisplayRotation();
if (displayId == DEFAULT_DISPLAY && newDisplayRotation != mDisplayRotation) {
onDevicePostureOrDisplayRotationChanged(mDevicePosture, newDisplayRotation);
}
}
private void onDevicePostureOrDisplayRotationChanged(
@DevicePostureController.DevicePostureInt int newPosture,
@Surface.Rotation int newDisplayRotation) {
final boolean wasInTabletopMode = isInTabletopMode();
mDevicePosture = newPosture;
mDisplayRotation = newDisplayRotation;
final boolean couldBeInTabletopMode = isInTabletopMode();
mMainExecutor.removeCallbacks(mOnEnterTabletopModeCallback);
if (!wasInTabletopMode && couldBeInTabletopMode) {
// May enter tabletop mode, but we need to wait for additional time since this
// could be an intermediate state.
mMainExecutor.executeDelayed(mOnEnterTabletopModeCallback, TABLETOP_MODE_DELAY_MILLIS);
} else {
// Cancel entering tabletop mode if any condition's changed.
mayBroadcastOnTabletopModeChange(false /* isInTabletopMode */);
}
}
private boolean isHalfOpened(@DevicePostureController.DevicePostureInt int posture) {
return posture == DEVICE_POSTURE_HALF_OPENED;
}
private boolean isInTabletopMode() {
return isHalfOpened(mDevicePosture) && mTabletopModeRotations.contains(mDisplayRotation);
}
private void mayBroadcastOnTabletopModeChange(boolean isInTabletopMode) {
if (mLastIsInTabletopModeForCallback == null
|| mLastIsInTabletopModeForCallback != isInTabletopMode) {
mListeners.forEach(l -> l.onTabletopModeChanged(isInTabletopMode));
mLastIsInTabletopModeForCallback = isInTabletopMode;
}
}
/**
* Listener interface for tabletop mode change.
*/
public interface OnTabletopModeChangedListener {
/**
* Callback when tabletop mode changes. Expect duplicated callbacks with {@code false}.
* @param isInTabletopMode {@code true} if enters tabletop mode, {@code false} otherwise.
*/
void onTabletopModeChanged(boolean isInTabletopMode);
}
}