/* * 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 Foldable states and postures 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 mTabletopModeRotations = new ArraySet<>(); private final List 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); } }