Merge "[4/n] Add User Aspect Ratio Button" into udc-qpr-dev

This commit is contained in:
Massimo Carli
2023-08-15 16:49:21 +00:00
committed by Android (Google) Code Review
20 changed files with 1214 additions and 77 deletions

View File

@@ -247,6 +247,13 @@ public class TaskInfo {
*/
public boolean topActivityEligibleForUserAspectRatioButton;
/**
* Whether the user has forced the activity to be fullscreen through the user aspect ratio
* settings.
* @hide
*/
public boolean isUserFullscreenOverrideEnabled;
/**
* Hint about the letterbox state of the top activity.
* @hide
@@ -543,7 +550,8 @@ public class TaskInfo {
&& isSleeping == that.isSleeping
&& Objects.equals(mTopActivityLocusId, that.mTopActivityLocusId)
&& parentTaskId == that.parentTaskId
&& Objects.equals(topActivity, that.topActivity);
&& Objects.equals(topActivity, that.topActivity)
&& isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled;
}
/**
@@ -574,7 +582,8 @@ public class TaskInfo {
&& (!hasCompatUI() || configuration.getLayoutDirection()
== that.configuration.getLayoutDirection())
&& (!hasCompatUI() || configuration.uiMode == that.configuration.uiMode)
&& (!hasCompatUI() || isVisible == that.isVisible);
&& (!hasCompatUI() || isVisible == that.isVisible)
&& isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled;
}
/**
@@ -630,6 +639,7 @@ public class TaskInfo {
topActivityLetterboxHorizontalPosition = source.readInt();
topActivityLetterboxWidth = source.readInt();
topActivityLetterboxHeight = source.readInt();
isUserFullscreenOverrideEnabled = source.readBoolean();
}
/**
@@ -686,6 +696,7 @@ public class TaskInfo {
dest.writeInt(topActivityLetterboxHorizontalPosition);
dest.writeInt(topActivityLetterboxWidth);
dest.writeInt(topActivityLetterboxHeight);
dest.writeBoolean(isUserFullscreenOverrideEnabled);
}
@Override
@@ -732,6 +743,7 @@ public class TaskInfo {
+ topActivityLetterboxHorizontalPosition
+ " topActivityLetterboxWidth=" + topActivityLetterboxWidth
+ " topActivityLetterboxHeight=" + topActivityLetterboxHeight
+ " isUserFullscreenOverrideEnabled=" + isUserFullscreenOverrideEnabled
+ " locusId=" + mTopActivityLocusId
+ " displayAreaFeatureId=" + displayAreaFeatureId
+ " cameraCompatControlState="

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="@color/compat_controls_background"
android:strokeAlpha="0.8"
android:fillAlpha="0.8"
android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0"/>
<group
android:translateX="12"
android:translateY="12">
<path
android:fillColor="@color/compat_controls_text"
android:pathData="M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z"/>
</group>
</vector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/compat_background_ripple">
<item android:drawable="@drawable/user_aspect_ratio_settings_button"/>
</ripple>

View File

@@ -55,7 +55,7 @@
<include android:id="@+id/size_compat_hint"
android:visibility="gone"
android:layout_width="@dimen/size_compat_hint_width"
android:layout_width="@dimen/compat_hint_width"
android:layout_height="wrap_content"
layout="@layout/compat_mode_hint"/>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<com.android.wm.shell.compatui.UserAspectRatioSettingsLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="bottom|end">
<include android:id="@+id/user_aspect_ratio_settings_hint"
android:visibility="gone"
android:layout_width="@dimen/compat_hint_width"
android:layout_height="wrap_content"
layout="@layout/compat_mode_hint"/>
<ImageButton
android:id="@+id/user_aspect_ratio_settings_button"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/compat_button_margin"
android:layout_marginBottom="@dimen/compat_button_margin"
android:src="@drawable/user_aspect_ratio_settings_button_ripple"
android:background="@android:color/transparent"
android:contentDescription="@string/user_aspect_ratio_settings_button_description"/>
</com.android.wm.shell.compatui.UserAspectRatioSettingsLayout>

View File

@@ -260,8 +260,8 @@
+ compat_button_margin - compat_hint_corner_radius - compat_hint_point_width / 2). -->
<dimen name="compat_hint_padding_end">7dp</dimen>
<!-- The width of the size compat hint. -->
<dimen name="size_compat_hint_width">188dp</dimen>
<!-- The width of the compat hint. -->
<dimen name="compat_hint_width">188dp</dimen>
<!-- The width of the camera compat hint. -->
<dimen name="camera_compat_hint_width">143dp</dimen>

View File

@@ -173,7 +173,13 @@
<string name="accessibility_bubble_dismissed">Bubble dismissed.</string>
<!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] -->
<string name="restart_button_description">Tap to restart this app for a better view.</string>
<string name="restart_button_description">Tap to restart this app for a better view</string>
<!-- Tooltip text of the button for the user aspect ratio settings. [CHAR LIMIT=NONE] -->
<string name="user_aspect_ratio_settings_button_hint">Change this app\'s aspect ratio in Settings</string>
<!-- Content description of the button for the user aspect ratio settings. [CHAR LIMIT=NONE] -->
<string name="user_aspect_ratio_settings_button_description">Change aspect ratio</string>
<!-- Description of the camera compat button for applying stretched issues treatment in the hint for
compatibility control. [CHAR LIMIT=NONE] -->

View File

@@ -16,12 +16,17 @@
package com.android.wm.shell.compatui;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
import android.app.TaskInfo.CameraCompatControlState;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
import android.provider.Settings;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -41,7 +46,6 @@ import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
import com.android.wm.shell.sysui.KeyguardChangeListener;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -103,6 +107,13 @@ public class CompatUIController implements OnDisplaysChangedListener,
*/
private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>();
/**
* The active user aspect ratio settings button layout if there is one (there can be at most
* one active).
*/
@Nullable
private UserAspectRatioSettingsWindowManager mUserAspectRatioSettingsLayout;
/**
* The active Letterbox Education layout if there is one (there can be at most one active).
*
@@ -121,38 +132,51 @@ public class CompatUIController implements OnDisplaysChangedListener,
/** Avoid creating display context frequently for non-default display. */
private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0);
@NonNull
private final Context mContext;
@NonNull
private final ShellController mShellController;
@NonNull
private final DisplayController mDisplayController;
@NonNull
private final DisplayInsetsController mDisplayInsetsController;
@NonNull
private final DisplayImeController mImeController;
@NonNull
private final SyncTransactionQueue mSyncQueue;
@NonNull
private final ShellExecutor mMainExecutor;
@NonNull
private final Lazy<Transitions> mTransitionsLazy;
@NonNull
private final DockStateReader mDockStateReader;
@NonNull
private final CompatUIConfiguration mCompatUIConfiguration;
// Only show each hint once automatically in the process life.
@NonNull
private final CompatUIHintsState mCompatUIHintsState;
@NonNull
private final CompatUIShellCommandHandler mCompatUIShellCommandHandler;
private CompatUICallback mCallback;
@Nullable
private CompatUICallback mCompatUICallback;
// Indicates if the keyguard is currently showing, in which case compat UIs shouldn't
// be shown.
private boolean mKeyguardShowing;
public CompatUIController(Context context,
ShellInit shellInit,
ShellController shellController,
DisplayController displayController,
DisplayInsetsController displayInsetsController,
DisplayImeController imeController,
SyncTransactionQueue syncQueue,
ShellExecutor mainExecutor,
Lazy<Transitions> transitionsLazy,
DockStateReader dockStateReader,
CompatUIConfiguration compatUIConfiguration,
CompatUIShellCommandHandler compatUIShellCommandHandler) {
public CompatUIController(@NonNull Context context,
@NonNull ShellInit shellInit,
@NonNull ShellController shellController,
@NonNull DisplayController displayController,
@NonNull DisplayInsetsController displayInsetsController,
@NonNull DisplayImeController imeController,
@NonNull SyncTransactionQueue syncQueue,
@NonNull ShellExecutor mainExecutor,
@NonNull Lazy<Transitions> transitionsLazy,
@NonNull DockStateReader dockStateReader,
@NonNull CompatUIConfiguration compatUIConfiguration,
@NonNull CompatUIShellCommandHandler compatUIShellCommandHandler) {
mContext = context;
mShellController = shellController;
mDisplayController = displayController;
@@ -175,9 +199,9 @@ public class CompatUIController implements OnDisplaysChangedListener,
mCompatUIShellCommandHandler.onInit();
}
/** Sets the callback for UI interactions. */
public void setCompatUICallback(CompatUICallback callback) {
mCallback = callback;
/** Sets the callback for Compat UI interactions. */
public void setCompatUICallback(@NonNull CompatUICallback compatUiCallback) {
mCompatUICallback = compatUiCallback;
}
/**
@@ -187,7 +211,7 @@ public class CompatUIController implements OnDisplaysChangedListener,
* @param taskInfo {@link TaskInfo} task the activity is in.
* @param taskListener listener to handle the Task Surface placement.
*/
public void onCompatInfoChanged(TaskInfo taskInfo,
public void onCompatInfoChanged(@NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
if (taskInfo != null && !taskInfo.topActivityInSizeCompat) {
mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
@@ -203,6 +227,16 @@ public class CompatUIController implements OnDisplaysChangedListener,
createOrUpdateRestartDialogLayout(taskInfo, taskListener);
if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) {
createOrUpdateReachabilityEduLayout(taskInfo, taskListener);
// The user aspect ratio button should not be handled when a new TaskInfo is
// sent because of a double tap or when in multi-window mode.
if (taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
mUserAspectRatioSettingsLayout.release();
mUserAspectRatioSettingsLayout = null;
return;
}
if (!taskInfo.isFromLetterboxDoubleTap) {
createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener);
}
}
}
@@ -280,8 +314,8 @@ public class CompatUIController implements OnDisplaysChangedListener,
return mDisplaysWithIme.contains(displayId);
}
private void createOrUpdateCompatLayout(TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
private void createOrUpdateCompatLayout(@NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId);
if (layout != null) {
if (layout.needsToBeRecreated(taskInfo, taskListener)) {
@@ -314,7 +348,7 @@ public class CompatUIController implements OnDisplaysChangedListener,
CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
return new CompatUIWindowManager(context,
taskInfo, mSyncQueue, mCallback, taskListener,
taskInfo, mSyncQueue, mCompatUICallback, taskListener,
mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState,
mCompatUIConfiguration, this::onRestartButtonClicked);
}
@@ -328,12 +362,12 @@ public class CompatUIController implements OnDisplaysChangedListener,
mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId);
onCompatInfoChanged(taskInfoState.first, taskInfoState.second);
} else {
mCallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId);
mCompatUICallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId);
}
}
private void createOrUpdateLetterboxEduLayout(TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
private void createOrUpdateLetterboxEduLayout(@NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
if (mActiveLetterboxEduLayout != null) {
if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
mActiveLetterboxEduLayout.release();
@@ -377,8 +411,8 @@ public class CompatUIController implements OnDisplaysChangedListener,
mDockStateReader, mCompatUIConfiguration);
}
private void createOrUpdateRestartDialogLayout(TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
private void createOrUpdateRestartDialogLayout(@NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
RestartDialogWindowManager layout =
mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId);
if (layout != null) {
@@ -423,7 +457,7 @@ public class CompatUIController implements OnDisplaysChangedListener,
private void onRestartDialogCallback(
Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) {
mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId);
mCallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId);
mCompatUICallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId);
}
private void onRestartDialogDismissCallback(
@@ -432,8 +466,8 @@ public class CompatUIController implements OnDisplaysChangedListener,
onCompatInfoChanged(stateInfo.first, stateInfo.second);
}
private void createOrUpdateReachabilityEduLayout(TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
if (mActiveReachabilityEduLayout != null) {
if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
mActiveReachabilityEduLayout.release();
@@ -474,14 +508,67 @@ public class CompatUIController implements OnDisplaysChangedListener,
ShellTaskOrganizer.TaskListener taskListener) {
return new ReachabilityEduWindowManager(context, taskInfo, mSyncQueue,
taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId),
mCompatUIConfiguration, mMainExecutor);
mCompatUIConfiguration, mMainExecutor, this::onInitialReachabilityEduDismissed);
}
private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo,
@NonNull ShellTaskOrganizer.TaskListener taskListener) {
// We need to update the UI otherwise it will not be shown until the user relaunches the app
createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener);
}
private void createOrUpdateUserAspectRatioSettingsLayout(@NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
if (mUserAspectRatioSettingsLayout != null) {
if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener)) {
mUserAspectRatioSettingsLayout.release();
mUserAspectRatioSettingsLayout = null;
} else {
// UI already exists, update the UI layout.
if (!mUserAspectRatioSettingsLayout.updateCompatInfo(taskInfo, taskListener,
showOnDisplay(mUserAspectRatioSettingsLayout.getDisplayId()))) {
mUserAspectRatioSettingsLayout.release();
mUserAspectRatioSettingsLayout = null;
}
return;
}
}
// Create a new UI layout.
final Context context = getOrCreateDisplayContext(taskInfo.displayId);
if (context == null) {
return;
}
final UserAspectRatioSettingsWindowManager newLayout =
createUserAspectRatioSettingsWindowManager(context, taskInfo, taskListener);
if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) {
// The new layout is eligible to be shown, add it the active layouts.
mUserAspectRatioSettingsLayout = newLayout;
}
}
@VisibleForTesting
@NonNull
UserAspectRatioSettingsWindowManager createUserAspectRatioSettingsWindowManager(
@NonNull Context context, @NonNull TaskInfo taskInfo,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
return new UserAspectRatioSettingsWindowManager(context, taskInfo, mSyncQueue,
taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId),
mCompatUIHintsState, this::launchUserAspectRatioSettings, mMainExecutor);
}
private void launchUserAspectRatioSettings(
@NonNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener) {
final Intent intent = new Intent(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
mContext.startActivity(intent);
}
private void removeLayouts(int taskId) {
final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId);
if (layout != null) {
layout.release();
final CompatUIWindowManager compatLayout = mActiveCompatLayouts.get(taskId);
if (compatLayout != null) {
compatLayout.release();
mActiveCompatLayouts.remove(taskId);
}
@@ -502,6 +589,12 @@ public class CompatUIController implements OnDisplaysChangedListener,
mActiveReachabilityEduLayout.release();
mActiveReachabilityEduLayout = null;
}
if (mUserAspectRatioSettingsLayout != null
&& mUserAspectRatioSettingsLayout.getTaskId() == taskId) {
mUserAspectRatioSettingsLayout.release();
mUserAspectRatioSettingsLayout = null;
}
}
private Context getOrCreateDisplayContext(int displayId) {
@@ -557,6 +650,10 @@ public class CompatUIController implements OnDisplaysChangedListener,
if (mActiveReachabilityEduLayout != null && condition.test(mActiveReachabilityEduLayout)) {
callback.accept(mActiveReachabilityEduLayout);
}
if (mUserAspectRatioSettingsLayout != null && condition.test(
mUserAspectRatioSettingsLayout)) {
callback.accept(mUserAspectRatioSettingsLayout);
}
}
/** An implementation of {@link OnInsetsChangedListener} for a given display id. */
@@ -591,4 +688,14 @@ public class CompatUIController implements OnDisplaysChangedListener,
insetsChanged(insetsState);
}
}
/**
* A class holding the state of the compat UI hints, which is shared between all compat UI
* window managers.
*/
static class CompatUIHintsState {
boolean mHasShownSizeCompatHint;
boolean mHasShownCameraCompatHint;
boolean mHasShownUserAspectRatioSettingsButtonHint;
}
}

View File

@@ -38,6 +38,7 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIController.CompatUICallback;
import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
import java.util.function.Consumer;
@@ -235,15 +236,4 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
return mCameraCompatControlState != CAMERA_COMPAT_CONTROL_HIDDEN
&& mCameraCompatControlState != CAMERA_COMPAT_CONTROL_DISMISSED;
}
/**
* A class holding the state of the compat UI hints, which is shared between all compat UI
* window managers.
*/
static class CompatUIHintsState {
@VisibleForTesting
boolean mHasShownSizeCompatHint;
@VisibleForTesting
boolean mHasShownCameraCompatHint;
}
}

View File

@@ -36,6 +36,8 @@ import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import java.util.function.BiConsumer;
/**
* Window manager for the reachability education
*/
@@ -73,6 +75,8 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
// we need to animate them.
private boolean mHasLetterboxSizeChanged;
private final BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnDismissCallback;
@Nullable
@VisibleForTesting
ReachabilityEduLayout mLayout;
@@ -80,7 +84,8 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
ReachabilityEduWindowManager(Context context, TaskInfo taskInfo,
SyncTransactionQueue syncQueue,
ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout,
CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor) {
CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor,
BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> onDismissCallback) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mIsActivityLetterboxed = taskInfo.isLetterboxDoubleTapEnabled;
mLetterboxVerticalPosition = taskInfo.topActivityLetterboxVerticalPosition;
@@ -89,6 +94,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
mTopActivityLetterboxHeight = taskInfo.topActivityLetterboxHeight;
mCompatUIConfiguration = compatUIConfiguration;
mMainExecutor = mainExecutor;
mOnDismissCallback = onDismissCallback;
}
@Override
@@ -217,13 +223,17 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
return;
}
final TaskInfo lastTaskInfo = getLastTaskInfo();
final boolean hasSeenHorizontalReachabilityEdu =
mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(lastTaskInfo);
final boolean hasSeenVerticalReachabilityEdu =
mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(lastTaskInfo);
final boolean eligibleForDisplayHorizontalEducation = mForceUpdate
|| !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(lastTaskInfo)
|| !hasSeenHorizontalReachabilityEdu
|| (mHasUserDoubleTapped
&& (mLetterboxHorizontalPosition == REACHABILITY_LEFT_OR_UP_POSITION
|| mLetterboxHorizontalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION));
final boolean eligibleForDisplayVerticalEducation = mForceUpdate
|| !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(lastTaskInfo)
|| !hasSeenVerticalReachabilityEdu
|| (mHasUserDoubleTapped
&& (mLetterboxVerticalPosition == REACHABILITY_LEFT_OR_UP_POSITION
|| mLetterboxVerticalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION));
@@ -239,6 +249,14 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
if (!mHasLetterboxSizeChanged) {
updateHideTime();
mMainExecutor.executeDelayed(this::hideReachability, DISAPPEAR_DELAY_MS);
// If reachability education has been seen for the first time, trigger callback to
// display aspect ratio settings button once reachability education disappears
if (hasShownHorizontalReachabilityEduFirstTime(hasSeenHorizontalReachabilityEdu)
|| hasShownVerticalReachabilityEduFirstTime(
hasSeenVerticalReachabilityEdu)) {
mMainExecutor.executeDelayed(this::triggerOnDismissCallback,
DISAPPEAR_DELAY_MS);
}
}
mHasUserDoubleTapped = false;
} else {
@@ -246,6 +264,38 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
}
}
/**
* Compares the value of
* {@link CompatUIConfiguration#hasSeenHorizontalReachabilityEducation} before and after the
* layout is shown. Horizontal reachability education is considered seen for the first time if
* prior to viewing the layout,
* {@link CompatUIConfiguration#hasSeenHorizontalReachabilityEducation} is {@code false}
* but becomes {@code true} once the current layout is shown.
*/
private boolean hasShownHorizontalReachabilityEduFirstTime(
boolean previouslyShownHorizontalReachabilityEducation) {
return !previouslyShownHorizontalReachabilityEducation
&& mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(getLastTaskInfo());
}
/**
* Compares the value of
* {@link CompatUIConfiguration#hasSeenVerticalReachabilityEducation} before and after the
* layout is shown. Horizontal reachability education is considered seen for the first time if
* prior to viewing the layout,
* {@link CompatUIConfiguration#hasSeenVerticalReachabilityEducation} is {@code false}
* but becomes {@code true} once the current layout is shown.
*/
private boolean hasShownVerticalReachabilityEduFirstTime(
boolean previouslyShownVerticalReachabilityEducation) {
return !previouslyShownVerticalReachabilityEducation
&& mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(getLastTaskInfo());
}
private void triggerOnDismissCallback() {
mOnDismissCallback.accept(getLastTaskInfo(), getTaskListener());
}
private void hideReachability() {
if (mLayout == null || !shouldHideEducation()) {
return;

View File

@@ -0,0 +1,136 @@
/*
* 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.compatui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.IdRes;
import android.annotation.NonNull;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.wm.shell.R;
/**
* Layout for the user aspect ratio button which opens the app list page in settings
* and allows users to change apps aspect ratio.
*/
public class UserAspectRatioSettingsLayout extends LinearLayout {
private static final float ALPHA_FULL_TRANSPARENT = 0f;
private static final float ALPHA_FULL_OPAQUE = 1f;
private static final long VISIBILITY_ANIMATION_DURATION_MS = 50;
private static final String ALPHA_PROPERTY_NAME = "alpha";
private UserAspectRatioSettingsWindowManager mWindowManager;
public UserAspectRatioSettingsLayout(Context context) {
this(context, null);
}
public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public UserAspectRatioSettingsLayout(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
void inject(@NonNull UserAspectRatioSettingsWindowManager windowManager) {
mWindowManager = windowManager;
}
void setUserAspectRatioSettingsHintVisibility(boolean show) {
setViewVisibility(R.id.user_aspect_ratio_settings_hint, show);
}
void setUserAspectRatioButtonVisibility(boolean show) {
setViewVisibility(R.id.user_aspect_ratio_settings_button, show);
// Hint should never be visible without button.
if (!show) {
setUserAspectRatioSettingsHintVisibility(/* show= */ false);
}
}
private void setViewVisibility(@IdRes int resId, boolean show) {
final View view = findViewById(resId);
int visibility = show ? View.VISIBLE : View.GONE;
if (view.getVisibility() == visibility) {
return;
}
if (show) {
showItem(view);
} else {
view.setVisibility(visibility);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// Need to relayout after changes like hiding / showing a hint since they affect size.
// Doing this directly in setUserAspectRatioButtonVisibility can result in flaky animation.
mWindowManager.relayout();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
final ImageButton userAspectRatioButton =
findViewById(R.id.user_aspect_ratio_settings_button);
userAspectRatioButton.setOnClickListener(
view -> mWindowManager.onUserAspectRatioSettingsButtonClicked());
userAspectRatioButton.setOnLongClickListener(view -> {
mWindowManager.onUserAspectRatioSettingsButtonLongClicked();
return true;
});
final LinearLayout sizeCompatHint = findViewById(R.id.user_aspect_ratio_settings_hint);
((TextView) sizeCompatHint.findViewById(R.id.compat_mode_hint_text))
.setText(R.string.user_aspect_ratio_settings_button_hint);
sizeCompatHint.setOnClickListener(
view -> setUserAspectRatioSettingsHintVisibility(/* show= */ false));
}
private void showItem(@NonNull View view) {
view.setVisibility(View.VISIBLE);
final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, ALPHA_PROPERTY_NAME,
ALPHA_FULL_TRANSPARENT, ALPHA_FULL_OPAQUE);
fadeIn.setDuration(VISIBILITY_ANIMATION_DURATION_MS);
fadeIn.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.VISIBLE);
}
});
fadeIn.start();
}
}

View File

@@ -0,0 +1,211 @@
/*
* 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.compatui;
import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
import android.content.Context;
import android.graphics.Rect;
import android.os.SystemClock;
import android.view.LayoutInflater;
import android.view.View;
import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
import java.util.function.BiConsumer;
/**
* Window manager for the user aspect ratio settings button which allows users to go to
* app settings and change apps aspect ratio.
*/
class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract {
private static final long SHOW_USER_ASPECT_RATIO_BUTTON_DELAY_MS = 500L;
private static final long HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS = 4000L;
private long mNextButtonHideTimeMs = -1L;
private final BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnButtonClicked;
private final ShellExecutor mShellExecutor;
@VisibleForTesting
@NonNull
final CompatUIHintsState mCompatUIHintsState;
@Nullable
private UserAspectRatioSettingsLayout mLayout;
// Remember the last reported states in case visibility changes due to keyguard or IME updates.
@VisibleForTesting
boolean mHasUserAspectRatioSettingsButton;
UserAspectRatioSettingsWindowManager(@NonNull Context context, @NonNull TaskInfo taskInfo,
@NonNull SyncTransactionQueue syncQueue,
@Nullable ShellTaskOrganizer.TaskListener taskListener,
@NonNull DisplayLayout displayLayout, @NonNull CompatUIHintsState compatUIHintsState,
@NonNull BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> onButtonClicked,
@NonNull ShellExecutor shellExecutor) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mShellExecutor = shellExecutor;
mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo);
mCompatUIHintsState = compatUIHintsState;
mOnButtonClicked = onButtonClicked;
}
@Override
protected int getZOrder() {
return TASK_CHILD_LAYER_COMPAT_UI + 1;
}
@Override
protected @Nullable View getLayout() {
return mLayout;
}
@Override
protected void removeLayout() {
mLayout = null;
}
@Override
protected boolean eligibleToShowLayout() {
return mHasUserAspectRatioSettingsButton;
}
@Override
protected View createLayout() {
mLayout = inflateLayout();
mLayout.inject(this);
updateVisibilityOfViews();
return mLayout;
}
@VisibleForTesting
UserAspectRatioSettingsLayout inflateLayout() {
return (UserAspectRatioSettingsLayout) LayoutInflater.from(mContext).inflate(
R.layout.user_aspect_ratio_settings_layout, null);
}
@Override
public boolean updateCompatInfo(@NonNull TaskInfo taskInfo,
@NonNull ShellTaskOrganizer.TaskListener taskListener, boolean canShow) {
final boolean prevHasUserAspectRatioSettingsButton = mHasUserAspectRatioSettingsButton;
mHasUserAspectRatioSettingsButton = getHasUserAspectRatioSettingsButton(taskInfo);
if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) {
return false;
}
if (prevHasUserAspectRatioSettingsButton != mHasUserAspectRatioSettingsButton) {
updateVisibilityOfViews();
}
return true;
}
/** Called when the user aspect ratio settings button is clicked. */
void onUserAspectRatioSettingsButtonClicked() {
mOnButtonClicked.accept(getLastTaskInfo(), getTaskListener());
}
/** Called when the user aspect ratio settings button is long clicked. */
void onUserAspectRatioSettingsButtonLongClicked() {
if (mLayout == null) {
return;
}
mLayout.setUserAspectRatioSettingsHintVisibility(/* show= */ true);
mNextButtonHideTimeMs = updateHideTime(HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
mShellExecutor.executeDelayed(this::hideUserAspectRatioButton,
HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
}
@Override
@VisibleForTesting
public void updateSurfacePosition() {
if (mLayout == null) {
return;
}
// Position of the button in the container coordinate.
final Rect taskBounds = getTaskBounds();
final Rect taskStableBounds = getTaskStableBounds();
final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
? taskStableBounds.left - taskBounds.left
: taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth();
final int positionY = taskStableBounds.bottom - taskBounds.top
- mLayout.getMeasuredHeight();
updateSurfacePosition(positionX, positionY);
}
@VisibleForTesting
void updateVisibilityOfViews() {
if (mHasUserAspectRatioSettingsButton) {
mShellExecutor.executeDelayed(this::showUserAspectRatioButton,
SHOW_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
mNextButtonHideTimeMs = updateHideTime(HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
mShellExecutor.executeDelayed(this::hideUserAspectRatioButton,
HIDE_USER_ASPECT_RATIO_BUTTON_DELAY_MS);
} else {
mShellExecutor.removeCallbacks(this::showUserAspectRatioButton);
mShellExecutor.execute(this::hideUserAspectRatioButton);
}
}
private void showUserAspectRatioButton() {
if (mLayout == null) {
return;
}
mLayout.setUserAspectRatioButtonVisibility(true);
// Only show by default for the first time.
if (!mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint) {
mLayout.setUserAspectRatioSettingsHintVisibility(/* show= */ true);
mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true;
}
}
private void hideUserAspectRatioButton() {
if (mLayout == null || !isHideDelayReached(mNextButtonHideTimeMs)) {
return;
}
mLayout.setUserAspectRatioButtonVisibility(false);
}
private boolean isHideDelayReached(long nextHideTime) {
return SystemClock.uptimeMillis() >= nextHideTime;
}
private long updateHideTime(long hideDelay) {
return SystemClock.uptimeMillis() + hideDelay;
}
private boolean getHasUserAspectRatioSettingsButton(@NonNull TaskInfo taskInfo) {
return taskInfo.topActivityEligibleForUserAspectRatioButton
&& (taskInfo.topActivityBoundsLetterboxed
|| taskInfo.isUserFullscreenOverrideEnabled);
}
}

View File

@@ -58,6 +58,8 @@ import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import dagger.Lazy;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -66,8 +68,6 @@ import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import dagger.Lazy;
/**
* Tests for {@link CompatUIController}.
*
@@ -82,21 +82,36 @@ public class CompatUIControllerTest extends ShellTestCase {
private CompatUIController mController;
private ShellInit mShellInit;
private @Mock ShellController mMockShellController;
private @Mock DisplayController mMockDisplayController;
private @Mock DisplayInsetsController mMockDisplayInsetsController;
private @Mock DisplayLayout mMockDisplayLayout;
private @Mock DisplayImeController mMockImeController;
private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener;
private @Mock SyncTransactionQueue mMockSyncQueue;
private @Mock ShellExecutor mMockExecutor;
private @Mock Lazy<Transitions> mMockTransitionsLazy;
private @Mock CompatUIWindowManager mMockCompatLayout;
private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout;
private @Mock RestartDialogWindowManager mMockRestartDialogLayout;
private @Mock DockStateReader mDockStateReader;
private @Mock CompatUIConfiguration mCompatUIConfiguration;
private @Mock CompatUIShellCommandHandler mCompatUIShellCommandHandler;
@Mock
private ShellController mMockShellController;
@Mock
private DisplayController mMockDisplayController;
@Mock
private DisplayInsetsController mMockDisplayInsetsController;
@Mock
private DisplayLayout mMockDisplayLayout;
@Mock
private DisplayImeController mMockImeController;
@Mock
private ShellTaskOrganizer.TaskListener mMockTaskListener;
@Mock
private SyncTransactionQueue mMockSyncQueue;
@Mock
private ShellExecutor mMockExecutor;
@Mock
private Lazy<Transitions> mMockTransitionsLazy;
@Mock
private CompatUIWindowManager mMockCompatLayout;
@Mock
private LetterboxEduWindowManager mMockLetterboxEduLayout;
@Mock
private RestartDialogWindowManager mMockRestartDialogLayout;
@Mock
private DockStateReader mDockStateReader;
@Mock
private CompatUIConfiguration mCompatUIConfiguration;
@Mock
private CompatUIShellCommandHandler mCompatUIShellCommandHandler;
@Captor
ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor;

View File

@@ -44,7 +44,7 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
import junit.framework.Assert;

View File

@@ -53,7 +53,7 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
import junit.framework.Assert;

View File

@@ -40,6 +40,8 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.function.BiConsumer;
/**
* Tests for {@link ReachabilityEduWindowManager}.
*
@@ -57,6 +59,8 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase {
private CompatUIConfiguration mCompatUIConfiguration;
@Mock
private DisplayLayout mDisplayLayout;
@Mock
private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener> mOnDismissCallback;
private TestShellExecutor mExecutor;
private TaskInfo mTaskInfo;
private ReachabilityEduWindowManager mWindowManager;
@@ -104,6 +108,7 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase {
private ReachabilityEduWindowManager createReachabilityEduWindowManager(TaskInfo taskInfo) {
return new ReachabilityEduWindowManager(mContext, taskInfo, mSyncTransactionQueue,
mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor);
mTaskListener, mDisplayLayout, mCompatUIConfiguration, mExecutor,
mOnDismissCallback);
}
}

View File

@@ -0,0 +1,154 @@
/*
* 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.compatui;
import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
import android.app.TaskInfo;
import android.app.TaskInfo.CameraCompatControlState;
import android.content.ComponentName;
import android.testing.AndroidTestingRunner;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.SurfaceControlViewHost;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import junit.framework.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.function.BiConsumer;
/**
* Tests for {@link UserAspectRatioSettingsLayout}.
*
* Build/Install/Run:
* atest WMShellUnitTests:UserAspectRatioSettingsLayoutTest
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class UserAspectRatioSettingsLayoutTest extends ShellTestCase {
private static final int TASK_ID = 1;
@Mock
private SyncTransactionQueue mSyncTransactionQueue;
@Mock
private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener>
mOnUserAspectRatioSettingsButtonClicked;
@Mock
private ShellTaskOrganizer.TaskListener mTaskListener;
@Mock
private SurfaceControlViewHost mViewHost;
@Captor
private ArgumentCaptor<ShellTaskOrganizer.TaskListener> mUserAspectRatioTaskListenerCaptor;
@Captor
private ArgumentCaptor<TaskInfo> mUserAspectRationTaskInfoCaptor;
private UserAspectRatioSettingsWindowManager mWindowManager;
private UserAspectRatioSettingsLayout mLayout;
private TaskInfo mTaskInfo;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo,
mSyncTransactionQueue, mTaskListener, new DisplayLayout(),
new CompatUIController.CompatUIHintsState(),
mOnUserAspectRatioSettingsButtonClicked, new TestShellExecutor());
mLayout = (UserAspectRatioSettingsLayout) LayoutInflater.from(mContext).inflate(
R.layout.user_aspect_ratio_settings_layout, null);
mLayout.inject(mWindowManager);
spyOn(mWindowManager);
spyOn(mLayout);
doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost();
doReturn(mLayout).when(mWindowManager).inflateLayout();
}
@Test
public void testOnClickForUserAspectRatioSettingsButton() {
final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button);
button.performClick();
verify(mWindowManager).onUserAspectRatioSettingsButtonClicked();
verify(mOnUserAspectRatioSettingsButtonClicked).accept(
mUserAspectRationTaskInfoCaptor.capture(),
mUserAspectRatioTaskListenerCaptor.capture());
final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result =
new Pair<>(mUserAspectRationTaskInfoCaptor.getValue(),
mUserAspectRatioTaskListenerCaptor.getValue());
Assert.assertEquals(mTaskInfo, result.first);
Assert.assertEquals(mTaskListener, result.second);
}
@Test
public void testOnLongClickForUserAspectRatioButton() {
doNothing().when(mWindowManager).onUserAspectRatioSettingsButtonLongClicked();
final ImageButton button = mLayout.findViewById(R.id.user_aspect_ratio_settings_button);
button.performLongClick();
verify(mWindowManager).onUserAspectRatioSettingsButtonLongClicked();
}
@Test
public void testOnClickForUserAspectRatioSettingsHint() {
mWindowManager.mHasUserAspectRatioSettingsButton = true;
mWindowManager.createLayout(/* canShow= */ true);
final LinearLayout sizeCompatHint = mLayout.findViewById(
R.id.user_aspect_ratio_settings_hint);
sizeCompatHint.performClick();
verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ false);
}
private static TaskInfo createTaskInfo(boolean hasSizeCompat,
@CameraCompatControlState int cameraCompatControlState) {
ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = TASK_ID;
taskInfo.topActivityInSizeCompat = hasSizeCompat;
taskInfo.cameraCompatControlState = cameraCompatControlState;
taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity");
return taskInfo;
}
}

View File

@@ -0,0 +1,350 @@
/*
* 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.compatui;
import static android.view.WindowInsets.Type.navigationBars;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.util.Pair;
import android.view.DisplayInfo;
import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
import junit.framework.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiConsumer;
/**
* Tests for {@link UserAspectRatioSettingsWindowManager}.
*
* Build/Install/Run:
* atest WMShellUnitTests:UserAspectRatioSettingsWindowManagerTest
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase {
private static final int TASK_ID = 1;
@Mock private SyncTransactionQueue mSyncTransactionQueue;
@Mock
private BiConsumer<TaskInfo, ShellTaskOrganizer.TaskListener>
mOnUserAspectRatioSettingsButtonClicked;
@Mock private ShellTaskOrganizer.TaskListener mTaskListener;
@Mock private UserAspectRatioSettingsLayout mLayout;
@Mock private SurfaceControlViewHost mViewHost;
@Captor
private ArgumentCaptor<ShellTaskOrganizer.TaskListener> mUserAspectRatioTaskListenerCaptor;
@Captor
private ArgumentCaptor<TaskInfo> mUserAspectRationTaskInfoCaptor;
private final Set<String> mPackageNameCache = new HashSet<>();
private UserAspectRatioSettingsWindowManager mWindowManager;
private TaskInfo mTaskInfo;
private TestShellExecutor mExecutor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mExecutor = new TestShellExecutor();
mTaskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
false, /* topActivityBoundsLetterboxed */ true);
mWindowManager = new UserAspectRatioSettingsWindowManager(mContext, mTaskInfo,
mSyncTransactionQueue, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
mOnUserAspectRatioSettingsButtonClicked, mExecutor);
spyOn(mWindowManager);
doReturn(mLayout).when(mWindowManager).inflateLayout();
doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost();
}
@Test
public void testCreateUserAspectRatioButton() {
// Doesn't create layout if show is false.
mWindowManager.mHasUserAspectRatioSettingsButton = true;
assertTrue(mWindowManager.createLayout(/* canShow= */ false));
verify(mWindowManager, never()).inflateLayout();
// Doesn't create hint popup.
mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true;
assertTrue(mWindowManager.createLayout(/* canShow= */ true));
verify(mWindowManager).inflateLayout();
mExecutor.flushAll();
verify(mLayout).setUserAspectRatioButtonVisibility(/* show= */ true);
verify(mLayout, never()).setUserAspectRatioSettingsHintVisibility(/* show= */ true);
// Creates hint popup.
clearInvocations(mWindowManager);
clearInvocations(mLayout);
mWindowManager.release();
mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = false;
assertTrue(mWindowManager.createLayout(/* canShow= */ true));
verify(mWindowManager).inflateLayout();
assertNotNull(mLayout);
mExecutor.flushAll();
verify(mLayout).setUserAspectRatioButtonVisibility(/* show= */ true);
verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ true);
assertTrue(mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint);
// Returns false and doesn't create layout if mHasUserAspectRatioSettingsButton is false.
clearInvocations(mWindowManager);
mWindowManager.release();
mWindowManager.mHasUserAspectRatioSettingsButton = false;
assertFalse(mWindowManager.createLayout(/* canShow= */ true));
verify(mWindowManager, never()).inflateLayout();
}
@Test
public void testRelease() {
mWindowManager.mHasUserAspectRatioSettingsButton = true;
mWindowManager.createLayout(/* canShow= */ true);
verify(mWindowManager).inflateLayout();
mWindowManager.release();
verify(mViewHost).release();
}
@Test
public void testUpdateCompatInfo() {
mWindowManager.mHasUserAspectRatioSettingsButton = true;
mWindowManager.createLayout(/* canShow= */ true);
// No diff
clearInvocations(mWindowManager);
TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
true, /* topActivityBoundsLetterboxed */ true);
assertTrue(mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true));
verify(mWindowManager, never()).updateSurfacePosition();
verify(mWindowManager, never()).release();
verify(mWindowManager, never()).createLayout(anyBoolean());
// Change task listener, recreate button.
clearInvocations(mWindowManager);
final ShellTaskOrganizer.TaskListener newTaskListener = mock(
ShellTaskOrganizer.TaskListener.class);
assertTrue(mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
verify(mWindowManager).release();
verify(mWindowManager).createLayout(/* canShow= */ true);
// Change has eligibleForUserAspectRatioButton to false, dispose the component
clearInvocations(mWindowManager);
clearInvocations(mLayout);
taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
false, /* topActivityBoundsLetterboxed */ true);
assertFalse(
mWindowManager.updateCompatInfo(taskInfo, newTaskListener, /* canShow= */ true));
verify(mWindowManager).release();
}
@Test
public void testUpdateCompatInfoLayoutNotInflatedYet() {
mWindowManager.mHasUserAspectRatioSettingsButton = true;
mWindowManager.createLayout(/* canShow= */ false);
verify(mWindowManager, never()).inflateLayout();
// Change topActivityInSizeCompat to false and pass canShow true, layout shouldn't be
// inflated
clearInvocations(mWindowManager);
TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
false, /* topActivityBoundsLetterboxed */ true);
mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
verify(mWindowManager, never()).inflateLayout();
// Change topActivityInSizeCompat to true and pass canShow true, layout should be inflated.
clearInvocations(mWindowManager);
taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
true, /* topActivityBoundsLetterboxed */ true);
mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
verify(mWindowManager).inflateLayout();
}
@Test
public void testUpdateDisplayLayout() {
final DisplayInfo displayInfo = new DisplayInfo();
displayInfo.logicalWidth = 1000;
displayInfo.logicalHeight = 2000;
final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo,
mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false);
mWindowManager.updateDisplayLayout(displayLayout1);
verify(mWindowManager).updateSurfacePosition();
// No update if the display bounds is the same.
clearInvocations(mWindowManager);
final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo,
mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false);
mWindowManager.updateDisplayLayout(displayLayout2);
verify(mWindowManager, never()).updateSurfacePosition();
}
@Test
public void testUpdateDisplayLayoutInsets() {
final DisplayInfo displayInfo = new DisplayInfo();
displayInfo.logicalWidth = 1000;
displayInfo.logicalHeight = 2000;
final DisplayLayout displayLayout = new DisplayLayout(displayInfo,
mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false);
mWindowManager.updateDisplayLayout(displayLayout);
verify(mWindowManager).updateSurfacePosition();
// Update if the insets change on the existing display layout
clearInvocations(mWindowManager);
InsetsState insetsState = new InsetsState();
insetsState.setDisplayFrame(new Rect(0, 0, 1000, 2000));
InsetsSource insetsSource = new InsetsSource(
InsetsSource.createId(null, 0, navigationBars()), navigationBars());
insetsSource.setFrame(0, 1800, 1000, 2000);
insetsState.addSource(insetsSource);
displayLayout.setInsets(mContext.getResources(), insetsState);
mWindowManager.updateDisplayLayout(displayLayout);
verify(mWindowManager).updateSurfacePosition();
}
@Test
public void testUpdateVisibility() {
// Create button if it is not created.
mWindowManager.removeLayout();
mWindowManager.mHasUserAspectRatioSettingsButton = true;
mWindowManager.updateVisibility(/* canShow= */ true);
verify(mWindowManager).createLayout(/* canShow= */ true);
// Hide button.
clearInvocations(mWindowManager);
doReturn(View.VISIBLE).when(mLayout).getVisibility();
mWindowManager.updateVisibility(/* canShow= */ false);
verify(mWindowManager, never()).createLayout(anyBoolean());
verify(mLayout).setVisibility(View.GONE);
// Show button.
doReturn(View.GONE).when(mLayout).getVisibility();
mWindowManager.updateVisibility(/* canShow= */ true);
verify(mWindowManager, never()).createLayout(anyBoolean());
verify(mLayout).setVisibility(View.VISIBLE);
}
@Test
public void testAttachToParentSurface() {
final SurfaceControl.Builder b = new SurfaceControl.Builder();
mWindowManager.attachToParentSurface(b);
verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b);
}
@Test
public void testOnUserAspectRatioButtonClicked() {
mWindowManager.onUserAspectRatioSettingsButtonClicked();
verify(mOnUserAspectRatioSettingsButtonClicked).accept(
mUserAspectRationTaskInfoCaptor.capture(),
mUserAspectRatioTaskListenerCaptor.capture());
final Pair<TaskInfo, ShellTaskOrganizer.TaskListener> result =
new Pair<>(mUserAspectRationTaskInfoCaptor.getValue(),
mUserAspectRatioTaskListenerCaptor.getValue());
Assert.assertEquals(mTaskInfo, result.first);
Assert.assertEquals(mTaskListener, result.second);
}
@Test
public void testOnUserAspectRatioButtonLongClicked_showHint() {
// Not create hint popup.
mWindowManager.mHasUserAspectRatioSettingsButton = true;
mWindowManager.mCompatUIHintsState.mHasShownUserAspectRatioSettingsButtonHint = true;
mWindowManager.createLayout(/* canShow= */ true);
verify(mWindowManager).inflateLayout();
verify(mLayout, never()).setUserAspectRatioSettingsHintVisibility(/* show= */ true);
mWindowManager.onUserAspectRatioSettingsButtonLongClicked();
verify(mLayout).setUserAspectRatioSettingsHintVisibility(/* show= */ true);
}
@Test
public void testWhenDockedStateHasChanged_needsToBeRecreated() {
ActivityManager.RunningTaskInfo newTaskInfo = new ActivityManager.RunningTaskInfo();
newTaskInfo.configuration.uiMode |= Configuration.UI_MODE_TYPE_DESK;
Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener));
}
private static TaskInfo createTaskInfo(boolean eligibleForUserAspectRatioButton,
boolean topActivityBoundsLetterboxed) {
ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = TASK_ID;
taskInfo.topActivityEligibleForUserAspectRatioButton = eligibleForUserAspectRatioButton;
taskInfo.topActivityBoundsLetterboxed = topActivityBoundsLetterboxed;
taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK;
taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity");
return taskInfo;
}
}

View File

@@ -969,8 +969,10 @@ final class LetterboxUiController {
final Rect innerFrame = hasInheritedLetterboxBehavior()
? mActivityRecord.getBounds() : w.getFrame();
mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint);
// We need to notify Shell that letterbox position has changed.
mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
if (mDoubleTapEvent) {
// We need to notify Shell that letterbox position has changed.
mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
}
} else if (mLetterbox != null) {
mLetterbox.hide();
}
@@ -1242,6 +1244,7 @@ final class LetterboxUiController {
? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT
: LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
logLetterboxPositionChange(changeToLog);
mDoubleTapEvent = true;
} else if (mLetterbox.getInnerFrame().right < x) {
// Moving to the next stop on the right side of the app window: left > center > right.
mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
@@ -1252,8 +1255,8 @@ final class LetterboxUiController {
? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT
: LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
logLetterboxPositionChange(changeToLog);
mDoubleTapEvent = true;
}
mDoubleTapEvent = true;
// TODO(197549949): Add animation for transition.
mActivityRecord.recomputeConfiguration();
}
@@ -1281,6 +1284,7 @@ final class LetterboxUiController {
? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP
: LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
logLetterboxPositionChange(changeToLog);
mDoubleTapEvent = true;
} else if (mLetterbox.getInnerFrame().bottom < y) {
// Moving to the next stop on the bottom side of the app window: top > center > bottom.
mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
@@ -1291,8 +1295,8 @@ final class LetterboxUiController {
? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM
: LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
logLetterboxPositionChange(changeToLog);
mDoubleTapEvent = true;
}
mDoubleTapEvent = true;
// TODO(197549949): Add animation for transition.
mActivityRecord.recomputeConfiguration();
}

View File

@@ -3459,6 +3459,8 @@ class Task extends TaskFragment {
info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
info.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET;
info.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET;
info.isUserFullscreenOverrideEnabled = top != null
&& top.mLetterboxUiController.shouldApplyUserFullscreenOverride();
info.isFromLetterboxDoubleTap = top != null && top.mLetterboxUiController.isFromDoubleTap();
if (info.isLetterboxDoubleTapEnabled) {
info.topActivityLetterboxWidth = top.getBounds().width();