From 2cb16bf48e1a57d03ac793e9ac74aab74fb90e25 Mon Sep 17 00:00:00 2001 From: Massimo Carli Date: Tue, 7 Mar 2023 16:27:21 +0000 Subject: [2/n] Adds reachability education for double-tap Double-tap education which explains to the users how double-tap works for repositioning an app in letterbox or size compat mode. The education appears the very first time an app is launched by a user and every time the user double-tap to reposition the app. Fixes: 255943773 Test: atest WMShellUnitTests:ReachabilityEduLayoutTest atest WMShellUnitTests:LetterboxEduWindowManagerTest atest WMShellUnitTests:ReachabilityEduWindowManagerTest Change-Id: I583a2ca8e552885b3578eaa4e344d89a67143b57 --- core/java/android/app/TaskInfo.java | 71 ++- .../reachability_education_ic_left_hand.xml | 699 +++++++++++++++++++++ .../reachability_education_ic_right_hand.xml | 699 +++++++++++++++++++++ .../Shell/res/layout/reachability_ui_layout.xml | 70 +++ libs/WindowManager/Shell/res/values/attrs.xml | 2 +- libs/WindowManager/Shell/res/values/colors.xml | 3 + libs/WindowManager/Shell/res/values/dimen.xml | 9 + libs/WindowManager/Shell/res/values/strings.xml | 11 + libs/WindowManager/Shell/res/values/styles.xml | 16 + .../wm/shell/compatui/CompatUIConfiguration.java | 92 ++- .../wm/shell/compatui/CompatUIController.java | 68 +- .../compatui/CompatUIWindowManagerAbstract.java | 9 +- .../shell/compatui/LetterboxEduWindowManager.java | 66 +- .../shell/compatui/ReachabilityEduHandLayout.java | 66 ++ .../wm/shell/compatui/ReachabilityEduLayout.java | 278 ++++++++ .../compatui/ReachabilityEduWindowManager.java | 282 +++++++++ .../shell/compatui/RestartDialogWindowManager.java | 2 +- .../compatui/LetterboxEduWindowManagerTest.java | 95 ++- .../shell/compatui/ReachabilityEduLayoutTest.java | 81 +++ .../compatui/ReachabilityEduWindowManagerTest.java | 137 ++++ .../android/server/wm/LetterboxUiController.java | 20 + services/core/java/com/android/server/wm/Task.java | 19 + 22 files changed, 2681 insertions(+), 114 deletions(-) create mode 100644 libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml create mode 100644 libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml create mode 100644 libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduHandLayout.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduLayout.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index aba4b2c0d591..23f2bdb7fa54 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -48,6 +48,12 @@ import java.util.Objects; public class TaskInfo { private static final String TAG = "TaskInfo"; + /** + * The value to use when the property has not a specific value. + * @hide + */ + public static final int PROPERTY_VALUE_UNSET = -1; + /** * The id of the user the task was running as if this is a leaf task. The id of the current * running user of the system otherwise. @@ -229,6 +235,40 @@ public class TaskInfo { */ public boolean topActivityEligibleForLetterboxEducation; + /** + * Whether the double tap is enabled + * @hide + */ + public boolean isLetterboxDoubleTapEnabled; + + /** + * If {@link isLetterboxDoubleTapEnabled} it contains the current letterbox vertical position or + * {@link TaskInfo.PROPERTY_VALUE_UNSET} otherwise. + * @hide + */ + public int topActivityLetterboxVerticalPosition; + + /** + * If {@link isLetterboxDoubleTapEnabled} it contains the current letterbox vertical position or + * {@link TaskInfo.PROPERTY_VALUE_UNSET} otherwise. + * @hide + */ + public int topActivityLetterboxHorizontalPosition; + + /** + * If {@link isLetterboxDoubleTapEnabled} it contains the current width of the letterboxed + * activity or {@link TaskInfo.PROPERTY_VALUE_UNSET} otherwise + * @hide + */ + public int topActivityLetterboxWidth; + + /** + * If {@link isLetterboxDoubleTapEnabled} it contains the current height of the letterboxed + * activity or {@link TaskInfo.PROPERTY_VALUE_UNSET} otherwise + * @hide + */ + public int topActivityLetterboxHeight; + /** * Whether this task is resizable. Unlike {@link #resizeMode} (which is what the top activity * supports), this is what the system actually uses for resizability based on other policy and @@ -407,7 +447,8 @@ public class TaskInfo { /** @hide */ public boolean hasCompatUI() { return hasCameraCompatControl() || topActivityInSizeCompat - || topActivityEligibleForLetterboxEducation; + || topActivityEligibleForLetterboxEducation + || isLetterboxDoubleTapEnabled; } /** @@ -447,6 +488,12 @@ public class TaskInfo { && isResizeable == that.isResizeable && supportsMultiWindow == that.supportsMultiWindow && displayAreaFeatureId == that.displayAreaFeatureId + && isLetterboxDoubleTapEnabled == that.isLetterboxDoubleTapEnabled + && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition + && topActivityLetterboxWidth == that.topActivityLetterboxWidth + && topActivityLetterboxHeight == that.topActivityLetterboxHeight + && topActivityLetterboxHorizontalPosition + == that.topActivityLetterboxHorizontalPosition && Objects.equals(positionInParent, that.positionInParent) && Objects.equals(pictureInPictureParams, that.pictureInPictureParams) && Objects.equals(shouldDockBigOverlays, that.shouldDockBigOverlays) @@ -475,6 +522,12 @@ public class TaskInfo { && topActivityInSizeCompat == that.topActivityInSizeCompat && topActivityEligibleForLetterboxEducation == that.topActivityEligibleForLetterboxEducation + && isLetterboxDoubleTapEnabled == that.isLetterboxDoubleTapEnabled + && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition + && topActivityLetterboxHorizontalPosition + == that.topActivityLetterboxHorizontalPosition + && topActivityLetterboxWidth == that.topActivityLetterboxWidth + && topActivityLetterboxHeight == that.topActivityLetterboxHeight && cameraCompatControlState == that.cameraCompatControlState // Bounds are important if top activity has compat controls. && (!hasCompatUI() || configuration.windowConfiguration.getBounds() @@ -529,6 +582,11 @@ public class TaskInfo { mTopActivityLocusId = source.readTypedObject(LocusId.CREATOR); displayAreaFeatureId = source.readInt(); cameraCompatControlState = source.readInt(); + isLetterboxDoubleTapEnabled = source.readBoolean(); + topActivityLetterboxVerticalPosition = source.readInt(); + topActivityLetterboxHorizontalPosition = source.readInt(); + topActivityLetterboxWidth = source.readInt(); + topActivityLetterboxHeight = source.readInt(); } /** @@ -576,6 +634,11 @@ public class TaskInfo { dest.writeTypedObject(mTopActivityLocusId, flags); dest.writeInt(displayAreaFeatureId); dest.writeInt(cameraCompatControlState); + dest.writeBoolean(isLetterboxDoubleTapEnabled); + dest.writeInt(topActivityLetterboxVerticalPosition); + dest.writeInt(topActivityLetterboxHorizontalPosition); + dest.writeInt(topActivityLetterboxWidth); + dest.writeInt(topActivityLetterboxHeight); } @Override @@ -611,6 +674,12 @@ public class TaskInfo { + " topActivityInSizeCompat=" + topActivityInSizeCompat + " topActivityEligibleForLetterboxEducation= " + topActivityEligibleForLetterboxEducation + + " topActivityLetterboxed= " + isLetterboxDoubleTapEnabled + + " topActivityLetterboxVerticalPosition= " + topActivityLetterboxVerticalPosition + + " topActivityLetterboxHorizontalPosition= " + + topActivityLetterboxHorizontalPosition + + " topActivityLetterboxWidth=" + topActivityLetterboxWidth + + " topActivityLetterboxHeight=" + topActivityLetterboxHeight + " locusId=" + mTopActivityLocusId + " displayAreaFeatureId=" + displayAreaFeatureId + " cameraCompatControlState=" diff --git a/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml new file mode 100644 index 000000000000..c400dc676325 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_left_hand.xml @@ -0,0 +1,699 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml new file mode 100644 index 000000000000..a807a770aa22 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/reachability_education_ic_right_hand.xml @@ -0,0 +1,699 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml new file mode 100644 index 000000000000..1e36fb62f8da --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/reachability_ui_layout.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + diff --git a/libs/WindowManager/Shell/res/values/attrs.xml b/libs/WindowManager/Shell/res/values/attrs.xml index 2aad4c1c1805..fbb5caa508de 100644 --- a/libs/WindowManager/Shell/res/values/attrs.xml +++ b/libs/WindowManager/Shell/res/values/attrs.xml @@ -1,5 +1,5 @@ @android:color/system_neutral1_900 + + #BFC8CC + #E8EAED #5F6368 diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 680ad5101366..04b53f266a09 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -327,6 +327,15 @@ 8dp + + 16dp + + + 118dp + + + 24dp + 200dp diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 6399232919d2..523657b80317 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -214,6 +214,17 @@ Don\u2019t show again + + Double-tap to move this app + Maximize diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index bc2e71d1c013..d0782ad9b37e 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -144,4 +144,20 @@ @*android:string/config_bodyFontFamily + + + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java index 06f0a70d3d0f..9a76dcfb33cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java @@ -46,15 +46,34 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION = false; /** - * The name of the {@link SharedPreferences} that holds which user has seen the Restart - * confirmation dialog. + * The name of the {@link SharedPreferences} that holds information about compat ui. */ - private static final String DONT_SHOW_RESTART_DIALOG_PREF_NAME = "dont_show_restart_dialog"; + private static final String COMPAT_UI_SHARED_PREFERENCES = "dont_show_restart_dialog"; /** - * The {@link SharedPreferences} instance for {@link #DONT_SHOW_RESTART_DIALOG_PREF_NAME}. + * The name of the {@link SharedPreferences} that holds which user has seen the Letterbox + * Education dialog. */ - private final SharedPreferences mSharedPreferences; + private static final String HAS_SEEN_LETTERBOX_EDUCATION_SHARED_PREFERENCES = + "has_seen_letterbox_education"; + + /** + * Key prefix for the {@link SharedPreferences} entries related to the reachability + * education. + */ + private static final String HAS_SEEN_REACHABILITY_EDUCATION_KEY_PREFIX = + "has_seen_reachability_education"; + + /** + * The {@link SharedPreferences} instance for the restart dialog and the reachability + * education. + */ + private final SharedPreferences mCompatUISharedPreferences; + + /** + * The {@link SharedPreferences} instance for the letterbox education dialog. + */ + private final SharedPreferences mLetterboxEduSharedPreferences; // Whether the extended restart dialog is enabled private boolean mIsRestartDialogEnabled; @@ -88,8 +107,10 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi DEFAULT_VALUE_ENABLE_LETTERBOX_REACHABILITY_EDUCATION); DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_COMPAT, mainExecutor, this); - mSharedPreferences = context.getSharedPreferences(DONT_SHOW_RESTART_DIALOG_PREF_NAME, + mCompatUISharedPreferences = context.getSharedPreferences(getCompatUISharedPreferenceName(), Context.MODE_PRIVATE); + mLetterboxEduSharedPreferences = context.getSharedPreferences( + getHasSeenLetterboxEducationSharedPreferencedName(), Context.MODE_PRIVATE); } /** @@ -122,20 +143,53 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi mIsReachabilityEducationOverrideEnabled = enabled; } - boolean getDontShowRestartDialogAgain(TaskInfo taskInfo) { - final int userId = taskInfo.userId; - final String packageName = taskInfo.topActivity.getPackageName(); - return mSharedPreferences.getBoolean( - getDontShowAgainRestartKey(userId, packageName), /* default= */ false); + void setDontShowRestartDialogAgain(TaskInfo taskInfo) { + mCompatUISharedPreferences.edit().putBoolean( + getDontShowAgainRestartKey(taskInfo.userId, taskInfo.topActivity.getPackageName()), + true).apply(); } - void setDontShowRestartDialogAgain(TaskInfo taskInfo) { - final int userId = taskInfo.userId; - final String packageName = taskInfo.topActivity.getPackageName(); - mSharedPreferences.edit().putBoolean(getDontShowAgainRestartKey(userId, packageName), + boolean shouldShowRestartDialogAgain(TaskInfo taskInfo) { + return !mCompatUISharedPreferences.getBoolean(getDontShowAgainRestartKey(taskInfo.userId, + taskInfo.topActivity.getPackageName()), /* default= */ false); + } + + void setDontShowReachabilityEducationAgain(TaskInfo taskInfo) { + mCompatUISharedPreferences.edit().putBoolean( + getDontShowAgainReachabilityEduKey(taskInfo.userId, + taskInfo.topActivity.getPackageName()), true).apply(); + } + + boolean shouldShowReachabilityEducation(@NonNull TaskInfo taskInfo) { + return getHasSeenLetterboxEducation(taskInfo.userId) + && !mCompatUISharedPreferences.getBoolean( + getDontShowAgainReachabilityEduKey(taskInfo.userId, + taskInfo.topActivity.getPackageName()), /* default= */false); + } + + boolean getHasSeenLetterboxEducation(int userId) { + return mLetterboxEduSharedPreferences + .getBoolean(getDontShowLetterboxEduKey(userId), /* default= */ false); + } + + void setSeenLetterboxEducation(int userId) { + mLetterboxEduSharedPreferences.edit().putBoolean(getDontShowLetterboxEduKey(userId), true).apply(); } + protected String getCompatUISharedPreferenceName() { + return COMPAT_UI_SHARED_PREFERENCES; + } + + protected String getHasSeenLetterboxEducationSharedPreferencedName() { + return HAS_SEEN_LETTERBOX_EDUCATION_SHARED_PREFERENCES; + } + + /** + * Updates the {@link DeviceConfig} state for the CompatUI + * @param properties Contains the complete collection of properties which have changed for a + * single namespace. This includes only those which were added, updated, + */ @Override public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) { if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_RESTART_DIALOG)) { @@ -152,6 +206,14 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi } } + private static String getDontShowAgainReachabilityEduKey(int userId, String packageName) { + return HAS_SEEN_REACHABILITY_EDUCATION_KEY_PREFIX + "_" + packageName + "@" + userId; + } + + private static String getDontShowLetterboxEduKey(int userId) { + return String.valueOf(userId); + } + private String getDontShowAgainRestartKey(int userId, String packageName) { return packageName + "@" + userId; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 6950f24512b1..4d83247e5c03 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -112,6 +112,12 @@ public class CompatUIController implements OnDisplaysChangedListener, @Nullable private LetterboxEduWindowManager mActiveLetterboxEduLayout; + /** + * The active Reachability UI layout. + */ + @Nullable + private ReachabilityEduWindowManager mActiveReachabilityEduLayout; + /** Avoid creating display context frequently for non-default display. */ private final SparseArray> mDisplayContextCache = new SparseArray<>(0); @@ -195,6 +201,7 @@ public class CompatUIController implements OnDisplaysChangedListener, createOrUpdateCompatLayout(taskInfo, taskListener); createOrUpdateLetterboxEduLayout(taskInfo, taskListener); createOrUpdateRestartDialogLayout(taskInfo, taskListener); + createOrUpdateReachabilityEduLayout(taskInfo, taskListener, false); } @Override @@ -308,7 +315,7 @@ public class CompatUIController implements OnDisplaysChangedListener, private void onRestartButtonClicked( Pair taskInfoState) { if (mCompatUIConfiguration.isRestartDialogEnabled() - && !mCompatUIConfiguration.getDontShowRestartDialogAgain( + && mCompatUIConfiguration.shouldShowRestartDialogAgain( taskInfoState.first)) { // We need to show the dialog mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId); @@ -355,13 +362,15 @@ public class CompatUIController implements OnDisplaysChangedListener, ShellTaskOrganizer.TaskListener taskListener) { return new LetterboxEduWindowManager(context, taskInfo, mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), - mTransitionsLazy.get(), - this::onLetterboxEduDismissed, - mDockStateReader); + mTransitionsLazy.get(), this::onLetterboxEduDismissed, mDockStateReader, + mCompatUIConfiguration); } - private void onLetterboxEduDismissed() { + private void onLetterboxEduDismissed( + Pair stateInfo) { mActiveLetterboxEduLayout = null; + // We need to update the UI + createOrUpdateReachabilityEduLayout(stateInfo.first, stateInfo.second, true); } private void createOrUpdateRestartDialogLayout(TaskInfo taskInfo, @@ -419,6 +428,47 @@ public class CompatUIController implements OnDisplaysChangedListener, onCompatInfoChanged(stateInfo.first, stateInfo.second); } + private void createOrUpdateReachabilityEduLayout(TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener, boolean forceUpdate) { + if (mActiveReachabilityEduLayout != null) { + mActiveReachabilityEduLayout.forceUpdate(forceUpdate); + // UI already exists, update the UI layout. + if (!mActiveReachabilityEduLayout.updateCompatInfo(taskInfo, taskListener, + showOnDisplay(mActiveReachabilityEduLayout.getDisplayId()))) { + // The layout is no longer eligible to be shown, remove from active layouts. + mActiveReachabilityEduLayout = null; + } + return; + } + // Create a new UI layout. + final Context context = getOrCreateDisplayContext(taskInfo.displayId); + if (context == null) { + return; + } + ReachabilityEduWindowManager newLayout = createReachabilityEduWindowManager(context, + taskInfo, taskListener); + if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { + // The new layout is eligible to be shown, make it the active layout. + if (mActiveReachabilityEduLayout != null) { + // Release the previous layout since at most one can be active. + // Since letterbox reachability education is only shown once to the user, + // releasing the previous layout is only a precaution. + mActiveReachabilityEduLayout.release(); + } + mActiveReachabilityEduLayout = newLayout; + } + } + + @VisibleForTesting + ReachabilityEduWindowManager createReachabilityEduWindowManager(Context context, + TaskInfo taskInfo, + ShellTaskOrganizer.TaskListener taskListener) { + return new ReachabilityEduWindowManager(context, taskInfo, mSyncQueue, mCallback, + taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), + mCompatUIConfiguration, mMainExecutor); + } + + private void removeLayouts(int taskId) { final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId); if (layout != null) { @@ -438,6 +488,11 @@ public class CompatUIController implements OnDisplaysChangedListener, mTaskIdToRestartDialogWindowManagerMap.remove(taskId); mSetOfTaskIdsShowingRestartDialog.remove(taskId); } + if (mActiveReachabilityEduLayout != null + && mActiveReachabilityEduLayout.getTaskId() == taskId) { + mActiveReachabilityEduLayout.release(); + mActiveReachabilityEduLayout = null; + } } private Context getOrCreateDisplayContext(int displayId) { @@ -490,6 +545,9 @@ public class CompatUIController implements OnDisplaysChangedListener, callback.accept(layout); } } + if (mActiveReachabilityEduLayout != null && condition.test(mActiveReachabilityEduLayout)) { + callback.accept(mActiveReachabilityEduLayout); + } } /** An implementation of {@link OnInsetsChangedListener} for a given display id. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java index cfb2accbcecd..346cd940e678 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java @@ -384,7 +384,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana // Cannot be wrap_content as this determines the actual window size width, height, TYPE_APPLICATION_OVERLAY, - FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL, + getWindowManagerLayoutParamsFlags(), PixelFormat.TRANSLUCENT); winParams.token = new Binder(); winParams.setTitle(getClass().getSimpleName() + mTaskId); @@ -392,6 +392,13 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana return winParams; } + /** + * @return Flags to use for the {@link WindowManager} layout + */ + protected int getWindowManagerLayoutParamsFlags() { + return FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL; + } + protected final String getTag() { return getClass().getSimpleName(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java index bfdbfe3d6ea0..0c21c8ccd686 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java @@ -18,12 +18,13 @@ package com.android.wm.shell.compatui; import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; import android.content.Context; -import android.content.SharedPreferences; import android.graphics.Rect; import android.provider.Settings; +import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup.MarginLayoutParams; @@ -38,10 +39,12 @@ import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.transition.Transitions; +import java.util.function.Consumer; + /** * Window manager for the Letterbox Education. */ -public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { +class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { /** * The Letterbox Education should be the topmost child of the Task in case there can be more @@ -49,19 +52,6 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { */ public static final int Z_ORDER = Integer.MAX_VALUE; - /** - * The name of the {@link SharedPreferences} that holds which user has seen the Letterbox - * Education dialog. - */ - @VisibleForTesting - static final String HAS_SEEN_LETTERBOX_EDUCATION_PREF_NAME = - "has_seen_letterbox_education"; - - /** - * The {@link SharedPreferences} instance for {@link #HAS_SEEN_LETTERBOX_EDUCATION_PREF_NAME}. - */ - private final SharedPreferences mSharedPreferences; - private final DialogAnimationController mAnimationController; private final Transitions mTransitions; @@ -73,6 +63,10 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { */ private final int mUserId; + private final Consumer> mOnDismissCallback; + + private final CompatUIConfiguration mCompatUIConfiguration; + // Remember the last reported state in case visibility changes due to keyguard or IME updates. private boolean mEligibleForLetterboxEducation; @@ -80,7 +74,8 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @VisibleForTesting LetterboxEduDialogLayout mLayout; - private final Runnable mOnDismissCallback; + @NonNull + private TaskInfo mTaskInfo; /** * The vertical margin between the dialog container and the task stable bounds (excluding @@ -90,33 +85,35 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { private final DockStateReader mDockStateReader; - public LetterboxEduWindowManager(Context context, TaskInfo taskInfo, + LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, - Runnable onDismissCallback, DockStateReader dockStateReader) { + Consumer> onDismissCallback, + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions, onDismissCallback, new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"), - dockStateReader); + dockStateReader, compatUIConfiguration); } @VisibleForTesting LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, - DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback, + DisplayLayout displayLayout, Transitions transitions, + Consumer> onDismissCallback, DialogAnimationController animationController, - DockStateReader dockStateReader) { + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { super(context, taskInfo, syncQueue, taskListener, displayLayout); + mTaskInfo = taskInfo; mTransitions = transitions; mOnDismissCallback = onDismissCallback; mAnimationController = animationController; mUserId = taskInfo.userId; - mEligibleForLetterboxEducation = taskInfo.topActivityEligibleForLetterboxEducation; - mSharedPreferences = mContext.getSharedPreferences(HAS_SEEN_LETTERBOX_EDUCATION_PREF_NAME, - Context.MODE_PRIVATE); mDialogVerticalMargin = (int) mContext.getResources().getDimension( R.dimen.letterbox_education_dialog_margin); mDockStateReader = dockStateReader; + mCompatUIConfiguration = compatUIConfiguration; + mEligibleForLetterboxEducation = taskInfo.topActivityEligibleForLetterboxEducation; } @Override @@ -142,8 +139,8 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { // the controller will create a new instance of this class since this one isn't eligible). // - If the layout isn't null then it was previously showing, and we shouldn't check if the // user has seen the letterbox education before. - return mEligibleForLetterboxEducation && !isTaskbarEduShowing() - && (mLayout != null || !getHasSeenLetterboxEducation()) + return mEligibleForLetterboxEducation && !isTaskbarEduShowing() && (mLayout != null + || !mCompatUIConfiguration.getHasSeenLetterboxEducation(mUserId)) && !mDockStateReader.isDocked(); } @@ -192,7 +189,6 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { // Dialog has already been released. return; } - setSeenLetterboxEducation(); mLayout.setDismissOnClickListener(this::onDismiss); // Focus on the dialog title for accessibility. mLayout.getDialogTitle().sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); @@ -202,10 +198,11 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { if (mLayout == null) { return; } + mCompatUIConfiguration.setSeenLetterboxEducation(mUserId); mLayout.setDismissOnClickListener(null); mAnimationController.startExitAnimation(mLayout, () -> { release(); - mOnDismissCallback.run(); + mOnDismissCallback.accept(Pair.create(mTaskInfo, getTaskListener())); }); } @@ -218,6 +215,7 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @Override public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { + mTaskInfo = taskInfo; mEligibleForLetterboxEducation = taskInfo.topActivityEligibleForLetterboxEducation; return super.updateCompatInfo(taskInfo, taskListener, canShow); @@ -248,18 +246,6 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { taskBounds.height()); } - private boolean getHasSeenLetterboxEducation() { - return mSharedPreferences.getBoolean(getPrefKey(), /* default= */ false); - } - - private void setSeenLetterboxEducation() { - mSharedPreferences.edit().putBoolean(getPrefKey(), true).apply(); - } - - private String getPrefKey() { - return String.valueOf(mUserId); - } - @VisibleForTesting boolean isTaskbarEduShowing() { return Settings.Secure.getInt(mContext.getContentResolver(), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduHandLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduHandLayout.java new file mode 100644 index 000000000000..6081ef1ca307 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduHandLayout.java @@ -0,0 +1,66 @@ +/* + * 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.content.Context; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +import androidx.appcompat.widget.AppCompatTextView; + +/** + * Custom layout for Reachability Education hand. + */ +public class ReachabilityEduHandLayout extends AppCompatTextView { + + private Drawable mHandDrawable; + + public ReachabilityEduHandLayout(Context context) { + this(context, null); + } + + public ReachabilityEduHandLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ReachabilityEduHandLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mHandDrawable = getCompoundDrawables()[/* top */ 1]; + } + + void hide() { + stopAnimation(); + setAlpha(0); + setVisibility(View.INVISIBLE); + } + + void startAnimation() { + if (mHandDrawable instanceof Animatable) { + final Animatable animatedBg = (Animatable) mHandDrawable; + animatedBg.start(); + } + } + + void stopAnimation() { + if (mHandDrawable instanceof Animatable) { + final Animatable animatedBg = (Animatable) mHandDrawable; + animatedBg.stop(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduLayout.java new file mode 100644 index 000000000000..6a72d28521b8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduLayout.java @@ -0,0 +1,278 @@ +/* + * 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.ValueAnimator; +import android.app.TaskInfo; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.FrameLayout; + +import com.android.wm.shell.R; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Container for reachability education which handles all the show/hide animations. + */ +public class ReachabilityEduLayout extends FrameLayout { + + private static final float ALPHA_FULL_TRANSPARENT = 0f; + + private static final float ALPHA_FULL_OPAQUE = 1f; + + private static final long VISIBILITY_SHOW_ANIMATION_DURATION_MS = 167; + + private static final long VISIBILITY_SHOW_ANIMATION_DELAY_MS = 250; + + private static final long VISIBILITY_SHOW_DOUBLE_TAP_ANIMATION_DELAY_MS = 80; + + private static final long MARGINS_ANIMATION_DURATION_MS = 250; + + private ReachabilityEduWindowManager mWindowManager; + + private ReachabilityEduHandLayout mMoveLeftButton; + private ReachabilityEduHandLayout mMoveRightButton; + private ReachabilityEduHandLayout mMoveUpButton; + private ReachabilityEduHandLayout mMoveDownButton; + + private int mLastLeftMargin = TaskInfo.PROPERTY_VALUE_UNSET; + private int mLastRightMargin = TaskInfo.PROPERTY_VALUE_UNSET; + private int mLastTopMargin = TaskInfo.PROPERTY_VALUE_UNSET; + private int mLastBottomMargin = TaskInfo.PROPERTY_VALUE_UNSET; + + private boolean mIsLayoutActive; + + public ReachabilityEduLayout(Context context) { + this(context, null); + } + + public ReachabilityEduLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ReachabilityEduLayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ReachabilityEduLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + void inject(ReachabilityEduWindowManager windowManager) { + mWindowManager = windowManager; + } + + void handleVisibility(boolean isActivityLetterboxed, int letterboxVerticalPosition, + int letterboxHorizontalPosition, int availableWidth, int availableHeight, + boolean isDoubleTap) { + // If the app is not letterboxed we hide all the buttons. + if (!mIsLayoutActive || !isActivityLetterboxed || ( + letterboxHorizontalPosition == TaskInfo.PROPERTY_VALUE_UNSET + && letterboxVerticalPosition == TaskInfo.PROPERTY_VALUE_UNSET)) { + hideAllImmediately(); + } else if (letterboxHorizontalPosition != TaskInfo.PROPERTY_VALUE_UNSET) { + handleLetterboxHorizontalPosition(availableWidth, letterboxHorizontalPosition, + isDoubleTap); + } else { + handleLetterboxVerticalPosition(availableHeight, letterboxVerticalPosition, + isDoubleTap); + } + } + + void hideAllImmediately() { + mMoveLeftButton.hide(); + mMoveRightButton.hide(); + mMoveUpButton.hide(); + mMoveDownButton.hide(); + mLastLeftMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastRightMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastTopMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastBottomMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + + void setIsLayoutActive(boolean isLayoutActive) { + this.mIsLayoutActive = isLayoutActive; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mMoveLeftButton = findViewById(R.id.reachability_move_left_button); + mMoveRightButton = findViewById(R.id.reachability_move_right_button); + mMoveUpButton = findViewById(R.id.reachability_move_up_button); + mMoveDownButton = findViewById(R.id.reachability_move_down_button); + mMoveLeftButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mMoveRightButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mMoveUpButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mMoveDownButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + } + + private Animator marginAnimator(View view, Function marginSupplier, + BiConsumer marginConsumer, int from, int to) { + final LayoutParams layoutParams = ((LayoutParams) view.getLayoutParams()); + ValueAnimator animator = ValueAnimator.ofInt(marginSupplier.apply(layoutParams), from, to); + animator.addUpdateListener(valueAnimator -> { + marginConsumer.accept(layoutParams, (Integer) valueAnimator.getAnimatedValue()); + view.requestLayout(); + }); + animator.setDuration(MARGINS_ANIMATION_DURATION_MS); + return animator; + } + + private void handleLetterboxHorizontalPosition(int availableWidth, + int letterboxHorizontalPosition, boolean isDoubleTap) { + mMoveUpButton.hide(); + mMoveDownButton.hide(); + mLastTopMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastBottomMargin = TaskInfo.PROPERTY_VALUE_UNSET; + // We calculate the available space on the left and right + final int horizontalGap = availableWidth / 2; + final int leftAvailableSpace = letterboxHorizontalPosition * horizontalGap; + final int rightAvailableSpace = availableWidth - leftAvailableSpace; + // We show the button if we have enough space + if (leftAvailableSpace >= mMoveLeftButton.getMeasuredWidth()) { + int newLeftMargin = (horizontalGap - mMoveLeftButton.getMeasuredWidth()) / 2; + if (mLastLeftMargin == TaskInfo.PROPERTY_VALUE_UNSET) { + mLastLeftMargin = newLeftMargin; + } + if (mLastLeftMargin != newLeftMargin) { + marginAnimator(mMoveLeftButton, layoutParams -> layoutParams.leftMargin, + (layoutParams, margin) -> layoutParams.leftMargin = margin, + mLastLeftMargin, newLeftMargin).start(); + } else { + final LayoutParams leftParams = ((LayoutParams) mMoveLeftButton.getLayoutParams()); + leftParams.leftMargin = mLastLeftMargin; + mMoveLeftButton.setLayoutParams(leftParams); + } + showItem(mMoveLeftButton, isDoubleTap); + } else { + mMoveLeftButton.hide(); + mLastLeftMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + if (rightAvailableSpace >= mMoveRightButton.getMeasuredWidth()) { + int newRightMargin = (horizontalGap - mMoveRightButton.getMeasuredWidth()) / 2; + if (mLastRightMargin == TaskInfo.PROPERTY_VALUE_UNSET) { + mLastRightMargin = newRightMargin; + } + if (mLastRightMargin != newRightMargin) { + marginAnimator(mMoveRightButton, layoutParams -> layoutParams.rightMargin, + (layoutParams, margin) -> layoutParams.rightMargin = margin, + mLastRightMargin, newRightMargin).start(); + } else { + final LayoutParams rightParams = + ((LayoutParams) mMoveRightButton.getLayoutParams()); + rightParams.rightMargin = mLastRightMargin; + mMoveRightButton.setLayoutParams(rightParams); + } + showItem(mMoveRightButton, isDoubleTap); + } else { + mMoveRightButton.hide(); + mLastRightMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + } + + private void handleLetterboxVerticalPosition(int availableHeight, + int letterboxVerticalPosition, boolean isDoubleTap) { + mMoveLeftButton.hide(); + mMoveRightButton.hide(); + mLastLeftMargin = TaskInfo.PROPERTY_VALUE_UNSET; + mLastRightMargin = TaskInfo.PROPERTY_VALUE_UNSET; + // We calculate the available space on the left and right + final int verticalGap = availableHeight / 2; + final int topAvailableSpace = letterboxVerticalPosition * verticalGap; + final int bottomAvailableSpace = availableHeight - topAvailableSpace; + if (topAvailableSpace >= mMoveUpButton.getMeasuredHeight()) { + int newTopMargin = (verticalGap - mMoveUpButton.getMeasuredHeight()) / 2; + if (mLastTopMargin == TaskInfo.PROPERTY_VALUE_UNSET) { + mLastTopMargin = newTopMargin; + } + if (mLastTopMargin != newTopMargin) { + marginAnimator(mMoveUpButton, layoutParams -> layoutParams.topMargin, + (layoutParams, margin) -> layoutParams.topMargin = margin, + mLastTopMargin, newTopMargin).start(); + } else { + final LayoutParams topParams = ((LayoutParams) mMoveUpButton.getLayoutParams()); + topParams.topMargin = mLastTopMargin; + mMoveUpButton.setLayoutParams(topParams); + } + showItem(mMoveUpButton, isDoubleTap); + } else { + mMoveUpButton.hide(); + mLastTopMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + if (bottomAvailableSpace >= mMoveDownButton.getMeasuredHeight()) { + int newBottomMargin = (verticalGap - mMoveDownButton.getMeasuredHeight()) / 2; + if (mLastBottomMargin == TaskInfo.PROPERTY_VALUE_UNSET) { + mLastBottomMargin = newBottomMargin; + } + if (mLastBottomMargin != newBottomMargin) { + marginAnimator(mMoveDownButton, layoutParams -> layoutParams.bottomMargin, + (layoutParams, margin) -> layoutParams.bottomMargin = margin, + mLastBottomMargin, newBottomMargin).start(); + } else { + final LayoutParams bottomParams = + ((LayoutParams) mMoveDownButton.getLayoutParams()); + bottomParams.bottomMargin = mLastBottomMargin; + mMoveDownButton.setLayoutParams(bottomParams); + } + showItem(mMoveDownButton, isDoubleTap); + } else { + mMoveDownButton.hide(); + mLastBottomMargin = TaskInfo.PROPERTY_VALUE_UNSET; + } + } + + private void showItem(ReachabilityEduHandLayout view, boolean fromDoubleTap) { + if (view.getVisibility() == View.VISIBLE) { + // Already visible we just start animation + view.startAnimation(); + return; + } + view.setVisibility(View.VISIBLE); + final long delay = fromDoubleTap ? VISIBILITY_SHOW_DOUBLE_TAP_ANIMATION_DELAY_MS + : VISIBILITY_SHOW_ANIMATION_DELAY_MS; + AlphaAnimation alphaAnimation = new AlphaAnimation(ALPHA_FULL_TRANSPARENT, + ALPHA_FULL_OPAQUE); + alphaAnimation.setDuration(VISIBILITY_SHOW_ANIMATION_DURATION_MS); + alphaAnimation.setStartOffset(delay); + alphaAnimation.setFillAfter(true); + alphaAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + // We trigger the hand animation + view.setAlpha(ALPHA_FULL_OPAQUE); + view.startAnimation(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + view.startAnimation(alphaAnimation); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java new file mode 100644 index 000000000000..6223efa831b1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java @@ -0,0 +1,282 @@ +/* + * 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.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + +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 android.view.WindowManager; + +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.CompatUICallback; + +/** + * Window manager for the reachability education + */ +class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { + + /** + * The Compat UI should be below the Letterbox Education. + */ + private static final int Z_ORDER = LetterboxEduWindowManager.Z_ORDER - 1; + + // The time to wait before hiding the education + private static final long DISAPPEAR_DELAY_MS = 4000L; + + private final CompatUICallback mCallback; + + private final CompatUIConfiguration mCompatUIConfiguration; + + private final ShellExecutor mMainExecutor; + + @NonNull + private TaskInfo mTaskInfo; + + private boolean mIsActivityLetterboxed; + + private int mLetterboxVerticalPosition; + + private int mLetterboxHorizontalPosition; + + private int mTopActivityLetterboxWidth; + + private int mTopActivityLetterboxHeight; + + private long mNextHideTime = -1L; + + private boolean mForceUpdate = false; + + // We decided to force the visualization of the double-tap animated icons every time the user + // double-taps. We detect a double-tap checking the previous and current state of + // mLetterboxVerticalPosition and mLetterboxHorizontalPosition saving the result in this + // variable. + private boolean mHasUserDoubleTapped; + + // When the size of the letterboxed app changes and the icons are visible + // we need to animate them. + private boolean mHasLetterboxSizeChanged; + + @Nullable + @VisibleForTesting + ReachabilityEduLayout mLayout; + + ReachabilityEduWindowManager(Context context, TaskInfo taskInfo, + SyncTransactionQueue syncQueue, CompatUICallback callback, + ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, + CompatUIConfiguration compatUIConfiguration, ShellExecutor mainExecutor) { + super(context, taskInfo, syncQueue, taskListener, displayLayout); + mCallback = callback; + mTaskInfo = taskInfo; + mIsActivityLetterboxed = taskInfo.isLetterboxDoubleTapEnabled; + mLetterboxVerticalPosition = taskInfo.topActivityLetterboxVerticalPosition; + mLetterboxHorizontalPosition = taskInfo.topActivityLetterboxHorizontalPosition; + mTopActivityLetterboxWidth = taskInfo.topActivityLetterboxWidth; + mTopActivityLetterboxHeight = taskInfo.topActivityLetterboxHeight; + mCompatUIConfiguration = compatUIConfiguration; + mMainExecutor = mainExecutor; + } + + @Override + protected int getZOrder() { + return Z_ORDER; + } + + @Override + protected @Nullable View getLayout() { + return mLayout; + } + + @Override + protected void removeLayout() { + mLayout = null; + } + + @Override + protected boolean eligibleToShowLayout() { + return mCompatUIConfiguration.isReachabilityEducationEnabled() + && mIsActivityLetterboxed + && (mLetterboxVerticalPosition != -1 || mLetterboxHorizontalPosition != -1); + } + + @Override + protected View createLayout() { + mLayout = inflateLayout(); + mLayout.inject(this); + + updateVisibilityOfViews(); + + return mLayout; + } + + @VisibleForTesting + ReachabilityEduLayout inflateLayout() { + return (ReachabilityEduLayout) LayoutInflater.from(mContext).inflate( + R.layout.reachability_ui_layout, null); + } + + @Override + public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, + boolean canShow) { + mTaskInfo = taskInfo; + final boolean prevIsActivityLetterboxed = mIsActivityLetterboxed; + final int prevLetterboxVerticalPosition = mLetterboxVerticalPosition; + final int prevLetterboxHorizontalPosition = mLetterboxHorizontalPosition; + final int prevTopActivityLetterboxWidth = mTopActivityLetterboxWidth; + final int prevTopActivityLetterboxHeight = mTopActivityLetterboxHeight; + mIsActivityLetterboxed = taskInfo.isLetterboxDoubleTapEnabled; + mLetterboxVerticalPosition = taskInfo.topActivityLetterboxVerticalPosition; + mLetterboxHorizontalPosition = taskInfo.topActivityLetterboxHorizontalPosition; + mTopActivityLetterboxWidth = taskInfo.topActivityLetterboxWidth; + mTopActivityLetterboxHeight = taskInfo.topActivityLetterboxHeight; + + mHasUserDoubleTapped = + mLetterboxVerticalPosition != prevLetterboxVerticalPosition + || prevLetterboxHorizontalPosition != mLetterboxHorizontalPosition; + if (mHasUserDoubleTapped) { + // In this case we disable the reachability for the following launch of + // the current application. Anyway because a double tap event happened, + // the reachability education is displayed + mCompatUIConfiguration.setDontShowReachabilityEducationAgain(taskInfo); + } + if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { + return false; + } + + mHasLetterboxSizeChanged = prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth + || prevTopActivityLetterboxHeight != mTopActivityLetterboxHeight; + + if (mForceUpdate || prevIsActivityLetterboxed != mIsActivityLetterboxed + || prevLetterboxVerticalPosition != mLetterboxVerticalPosition + || prevLetterboxHorizontalPosition != mLetterboxHorizontalPosition + || prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth + || prevTopActivityLetterboxHeight != mTopActivityLetterboxHeight) { + updateVisibilityOfViews(); + mForceUpdate = false; + } + + return true; + } + + void forceUpdate(boolean forceUpdate) { + mForceUpdate = forceUpdate; + } + + @Override + protected void onParentBoundsChanged() { + if (mLayout == null) { + return; + } + // Both the layout dimensions and dialog margins depend on the parent bounds. + WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams(); + mLayout.setLayoutParams(windowLayoutParams); + relayout(windowLayoutParams); + } + + /** Gets the layout params. */ + protected WindowManager.LayoutParams getWindowLayoutParams() { + View layout = getLayout(); + if (layout == null) { + return new WindowManager.LayoutParams(); + } + // Measure how big the hint is since its size depends on the text size. + final Rect taskBounds = getTaskBounds(); + layout.measure(View.MeasureSpec.makeMeasureSpec(taskBounds.width(), + View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(taskBounds.height(), + View.MeasureSpec.EXACTLY)); + return getWindowLayoutParams(layout.getMeasuredWidth(), layout.getMeasuredHeight()); + } + + /** + * @return Flags to use for the WindowManager layout + */ + @Override + protected int getWindowManagerLayoutParamsFlags() { + return FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE; + } + + @Override + @VisibleForTesting + public void updateSurfacePosition() { + if (mLayout == null) { + return; + } + updateSurfacePosition(0, 0); + } + + void updateHideTime() { + mNextHideTime = SystemClock.uptimeMillis() + DISAPPEAR_DELAY_MS; + } + + private void updateVisibilityOfViews() { + if (mLayout == null) { + return; + } + if (shouldUpdateEducation()) { + if (!mHasLetterboxSizeChanged) { + mLayout.setIsLayoutActive(true); + } + int availableWidth = getTaskBounds().width() - mTopActivityLetterboxWidth; + int availableHeight = getTaskBounds().height() - mTopActivityLetterboxHeight; + mLayout.handleVisibility(mIsActivityLetterboxed, mLetterboxVerticalPosition, + mLetterboxHorizontalPosition, availableWidth, availableHeight, + mHasUserDoubleTapped); + if (!mHasLetterboxSizeChanged) { + updateHideTime(); + mMainExecutor.executeDelayed(this::hideReachability, DISAPPEAR_DELAY_MS); + } + mHasUserDoubleTapped = false; + } else { + hideReachability(); + } + } + + private void hideReachability() { + if (mLayout != null) { + mLayout.setIsLayoutActive(false); + } + if (mLayout == null || !shouldHideEducation()) { + return; + } + mLayout.hideAllImmediately(); + // We need this in case the icons disappear after the timeout without an explicit + // double tap of the user. + mCompatUIConfiguration.setDontShowReachabilityEducationAgain(mTaskInfo); + } + + private boolean shouldUpdateEducation() { + return mForceUpdate || mHasUserDoubleTapped || mHasLetterboxSizeChanged + || mCompatUIConfiguration.shouldShowReachabilityEducation(mTaskInfo); + } + + private boolean shouldHideEducation() { + return SystemClock.uptimeMillis() >= mNextHideTime; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java index 2440838844c4..aab123a843ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java @@ -130,7 +130,7 @@ class RestartDialogWindowManager extends CompatUIWindowManagerAbstract { protected boolean eligibleToShowLayout() { // We don't show this dialog if the user has explicitly selected so clicking on a checkbox. return mRequestRestartDialog && !isTaskbarEduShowing() && (mLayout != null - || !mCompatUIConfiguration.getDontShowRestartDialogAgain(mTaskInfo)); + || mCompatUIConfiguration.shouldShowRestartDialogAgain(mTaskInfo)); } @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java index 47c9e06e8681..3f79df6a82c8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java @@ -31,14 +31,12 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import android.annotation.Nullable; import android.app.ActivityManager; import android.app.TaskInfo; -import android.content.Context; -import android.content.SharedPreferences; import android.graphics.Insets; import android.graphics.Rect; import android.testing.AndroidTestingRunner; +import android.util.Pair; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.SurfaceControlViewHost; @@ -53,6 +51,7 @@ 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.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; @@ -67,6 +66,8 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.function.Consumer; + /** * Tests for {@link LetterboxEduWindowManager}. * @@ -80,8 +81,10 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { private static final int USER_ID_1 = 1; private static final int USER_ID_2 = 2; - private static final String PREF_KEY_1 = String.valueOf(USER_ID_1); - private static final String PREF_KEY_2 = String.valueOf(USER_ID_2); + private static final String TEST_COMPAT_UI_SHARED_PREFERENCES = "test_compat_ui_configuration"; + + private static final String TEST_HAS_SEEN_LETTERBOX_SHARED_PREFERENCES = + "test_has_seen_letterbox"; private static final int TASK_ID = 1; @@ -103,46 +106,34 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { @Mock private ShellTaskOrganizer.TaskListener mTaskListener; @Mock private SurfaceControlViewHost mViewHost; @Mock private Transitions mTransitions; - @Mock private Runnable mOnDismissCallback; + @Mock private Consumer> mOnDismissCallback; @Mock private DockStateReader mDockStateReader; - private SharedPreferences mSharedPreferences; - @Nullable - private Boolean mInitialPrefValue1 = null; - @Nullable - private Boolean mInitialPrefValue2 = null; + private CompatUIConfiguration mCompatUIConfiguration; + private TestShellExecutor mExecutor; @Before public void setUp() { MockitoAnnotations.initMocks(this); - - mSharedPreferences = mContext.getSharedPreferences( - LetterboxEduWindowManager.HAS_SEEN_LETTERBOX_EDUCATION_PREF_NAME, - Context.MODE_PRIVATE); - if (mSharedPreferences.contains(PREF_KEY_1)) { - mInitialPrefValue1 = mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false); - mSharedPreferences.edit().remove(PREF_KEY_1).apply(); - } - if (mSharedPreferences.contains(PREF_KEY_2)) { - mInitialPrefValue2 = mSharedPreferences.getBoolean(PREF_KEY_2, /* default= */ false); - mSharedPreferences.edit().remove(PREF_KEY_2).apply(); - } + mExecutor = new TestShellExecutor(); + mCompatUIConfiguration = new CompatUIConfiguration(mContext, mExecutor) { + + @Override + protected String getCompatUISharedPreferenceName() { + return TEST_COMPAT_UI_SHARED_PREFERENCES; + } + + @Override + protected String getHasSeenLetterboxEducationSharedPreferencedName() { + return TEST_HAS_SEEN_LETTERBOX_SHARED_PREFERENCES; + } + }; } @After public void tearDown() { - SharedPreferences.Editor editor = mSharedPreferences.edit(); - if (mInitialPrefValue1 == null) { - editor.remove(PREF_KEY_1); - } else { - editor.putBoolean(PREF_KEY_1, mInitialPrefValue1); - } - if (mInitialPrefValue2 == null) { - editor.remove(PREF_KEY_2); - } else { - editor.putBoolean(PREF_KEY_2, mInitialPrefValue2); - } - editor.apply(); + mContext.deleteSharedPreferences(TEST_COMPAT_UI_SHARED_PREFERENCES); + mContext.deleteSharedPreferences(TEST_HAS_SEEN_LETTERBOX_SHARED_PREFERENCES); } @Test @@ -166,8 +157,8 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { @Test public void testCreateLayout_taskBarEducationIsShowing_doesNotCreateLayout() { - LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ - true, USER_ID_1, /* isTaskbarEduShowing= */ true); + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, + USER_ID_1, /* isTaskbarEduShowing= */ true); assertFalse(windowManager.createLayout(/* canShow= */ true)); @@ -180,7 +171,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertTrue(windowManager.createLayout(/* canShow= */ false)); - assertFalse(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertFalse(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); assertNull(windowManager.mLayout); } @@ -201,7 +192,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { spyOn(dialogTitle); // The education shouldn't be marked as seen until enter animation is done. - assertFalse(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertFalse(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); // Clicking the layout does nothing until enter animation is done. layout.performClick(); verify(mAnimationController, never()).startExitAnimation(any(), any()); @@ -210,7 +201,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { verifyAndFinishEnterAnimation(layout); - assertTrue(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertFalse(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); verify(dialogTitle).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); // Exit animation should start following a click on the layout. layout.performClick(); @@ -218,13 +209,16 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { // Window manager isn't released until exit animation is done. verify(windowManager, never()).release(); + // After dismissed the user has seen the dialog + assertTrue(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); + // Verify multiple clicks are ignored. layout.performClick(); verifyAndFinishExitAnimation(layout); verify(windowManager).release(); - verify(mOnDismissCallback).run(); + verify(mOnDismissCallback).accept(any()); } @Test @@ -236,7 +230,10 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertNotNull(windowManager.mLayout); verifyAndFinishEnterAnimation(windowManager.mLayout); - assertTrue(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + + // We dismiss + windowManager.mLayout.findViewById(R.id.letterbox_education_dialog_dismiss_button) + .performClick(); windowManager.release(); windowManager = createWindowManager(/* eligible= */ true, @@ -254,7 +251,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertNotNull(windowManager.mLayout); verifyAndFinishEnterAnimation(windowManager.mLayout); - assertTrue(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertTrue(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); } @Test @@ -271,7 +268,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { mRunOnIdleCaptor.getValue().run(); verify(mAnimationController, never()).startEnterAnimation(any(), any()); - assertFalse(mSharedPreferences.getBoolean(PREF_KEY_1, /* default= */ false)); + assertFalse(mCompatUIConfiguration.getHasSeenLetterboxEducation(USER_ID_1)); } @Test @@ -297,7 +294,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { mTaskListener, /* canShow= */ true)); verify(windowManager).release(); - verify(mOnDismissCallback, never()).run(); + verify(mOnDismissCallback, never()).accept(any()); verify(mAnimationController, never()).startExitAnimation(any(), any()); assertNull(windowManager.mLayout); } @@ -395,8 +392,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { } private LetterboxEduWindowManager createWindowManager(boolean eligible, boolean isDocked) { - return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */ - false, isDocked); + return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */ false, isDocked); } private LetterboxEduWindowManager createWindowManager(boolean eligible, int userId, @@ -410,9 +406,8 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { LetterboxEduWindowManager windowManager = new LetterboxEduWindowManager(mContext, createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener, - createDisplayLayout(), mTransitions, mOnDismissCallback, - mAnimationController, mDockStateReader); - + createDisplayLayout(), mTransitions, mOnDismissCallback, mAnimationController, + mDockStateReader, mCompatUIConfiguration); spyOn(windowManager); doReturn(mViewHost).when(windowManager).createSurfaceViewHost(); doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java new file mode 100644 index 000000000000..0be08ba74d86 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java @@ -0,0 +1,81 @@ +/* + * 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 junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; + +import android.testing.AndroidTestingRunner; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link LetterboxEduDialogLayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityEduLayoutTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class ReachabilityEduLayoutTest extends ShellTestCase { + + private ReachabilityEduLayout mLayout; + private View mMoveUpButton; + private View mMoveDownButton; + private View mMoveLeftButton; + private View mMoveRightButton; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLayout = (ReachabilityEduLayout) LayoutInflater.from(mContext) + .inflate(R.layout.reachability_ui_layout, null); + mMoveLeftButton = mLayout.findViewById(R.id.reachability_move_left_button); + mMoveRightButton = mLayout.findViewById(R.id.reachability_move_right_button); + mMoveUpButton = mLayout.findViewById(R.id.reachability_move_up_button); + mMoveDownButton = mLayout.findViewById(R.id.reachability_move_down_button); + } + + @Test + public void testOnFinishInflate() { + assertNotNull(mMoveUpButton); + assertNotNull(mMoveDownButton); + assertNotNull(mMoveLeftButton); + assertNotNull(mMoveRightButton); + } + + @Test + public void handleVisibility_activityNotLetterboxed_buttonsAreHidden() { + mLayout.handleVisibility(/* isActivityLetterboxed */ false, + /* letterboxVerticalPosition */ -1, /* letterboxHorizontalPosition */ -1, + /* availableWidth */ 0, /* availableHeight */ 0, /* fromDoubleTap */ false); + assertEquals(View.INVISIBLE, mMoveUpButton.getVisibility()); + assertEquals(View.INVISIBLE, mMoveDownButton.getVisibility()); + assertEquals(View.INVISIBLE, mMoveLeftButton.getVisibility()); + assertEquals(View.INVISIBLE, mMoveRightButton.getVisibility()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java new file mode 100644 index 000000000000..91e1e199719c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java @@ -0,0 +1,137 @@ +/* + * 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 org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.testing.AndroidTestingRunner; + +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 org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link ReachabilityEduWindowManager}. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityEduWindowManagerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class ReachabilityEduWindowManagerTest extends ShellTestCase { + + private static final int USER_ID = 1; + private static final int TASK_ID = 1; + + @Mock + private SyncTransactionQueue mSyncTransactionQueue; + @Mock + private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock + private CompatUIController.CompatUICallback mCallback; + @Mock + private CompatUIConfiguration mCompatUIConfiguration; + @Mock + private DisplayLayout mDisplayLayout; + + private TestShellExecutor mExecutor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mExecutor = new TestShellExecutor(); + } + + @After + public void tearDown() { + } + + @Test + public void testCreateLayout_notEligible_doesNotCreateLayout() { + final ReachabilityEduWindowManager windowManager = createReachabilityEduWindowManager( + createTaskInfo(/* userId= */ USER_ID, /*isLetterboxDoubleTapEnabled */ false)); + + assertFalse(windowManager.createLayout(/* canShow= */ true)); + + assertNull(windowManager.mLayout); + } + + @Test + public void testCreateLayout_letterboxPositionChanged_doubleTapIsDetected() { + // Initial left position + final TaskInfo initialTaskInfo = createTaskInfoForHorizontalTapping(USER_ID, 0, 1000); + final ReachabilityEduWindowManager windowManager = + createReachabilityEduWindowManager(initialTaskInfo); + // Move to the right + final TaskInfo newPositionTaskInfo = createTaskInfoForHorizontalTapping(USER_ID, 1, 1000); + windowManager.updateCompatInfo(newPositionTaskInfo, mTaskListener, /* canShow */ true); + + verify(mCompatUIConfiguration).setDontShowReachabilityEducationAgain(newPositionTaskInfo); + } + + + private ReachabilityEduWindowManager createReachabilityEduWindowManager(TaskInfo taskInfo) { + return new ReachabilityEduWindowManager(mContext, taskInfo, + mSyncTransactionQueue, mCallback, mTaskListener, mDisplayLayout, + mCompatUIConfiguration, mExecutor); + } + + private static TaskInfo createTaskInfo(int userId, boolean isLetterboxDoubleTapEnabled) { + return createTaskInfo(userId, /* isLetterboxDoubleTapEnabled */ isLetterboxDoubleTapEnabled, + /* topActivityLetterboxVerticalPosition */ -1, + /* topActivityLetterboxHorizontalPosition */ -1, + /* topActivityLetterboxWidth */ -1, + /* topActivityLetterboxHeight */ -1); + } + + private static TaskInfo createTaskInfoForHorizontalTapping(int userId, + int topActivityLetterboxHorizontalPosition, int topActivityLetterboxWidth) { + return createTaskInfo(userId, /* isLetterboxDoubleTapEnabled */ true, + /* topActivityLetterboxVerticalPosition */ -1, + topActivityLetterboxHorizontalPosition, topActivityLetterboxWidth, + /* topActivityLetterboxHeight */ -1); + } + + private static TaskInfo createTaskInfo(int userId, boolean isLetterboxDoubleTapEnabled, + int topActivityLetterboxVerticalPosition, int topActivityLetterboxHorizontalPosition, + int topActivityLetterboxWidth, int topActivityLetterboxHeight) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.userId = userId; + taskInfo.taskId = TASK_ID; + taskInfo.isLetterboxDoubleTapEnabled = isLetterboxDoubleTapEnabled; + taskInfo.topActivityLetterboxVerticalPosition = topActivityLetterboxVerticalPosition; + taskInfo.topActivityLetterboxHorizontalPosition = topActivityLetterboxHorizontalPosition; + taskInfo.topActivityLetterboxWidth = topActivityLetterboxWidth; + taskInfo.topActivityLetterboxHeight = topActivityLetterboxHeight; + return taskInfo; + } +} diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 54137eb41c5f..b1462888f3e5 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -753,6 +753,8 @@ 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 */); } else if (mLetterbox != null) { mLetterbox.hide(); } @@ -885,6 +887,20 @@ final class LetterboxUiController { return mActivityRecord.mWmService.mContext.getResources(); } + @LetterboxConfiguration.LetterboxVerticalReachabilityPosition + int getLetterboxPositionForVerticalReachability() { + final boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge(); + return mLetterboxConfiguration.getLetterboxPositionForVerticalReachability( + isInFullScreenTabletopMode); + } + + @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition + int getLetterboxPositionForHorizontalReachability() { + final boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge(); + return mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability( + isInFullScreenBookMode); + } + @VisibleForTesting void handleHorizontalDoubleTap(int x) { if (!isHorizontalReachabilityEnabled() || mActivityRecord.isInTransition()) { @@ -992,6 +1008,10 @@ final class LetterboxUiController { return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); } + boolean isLetterboxDoubleTapEducationEnabled() { + return isHorizontalReachabilityEnabled() || isVerticalReachabilityEnabled(); + } + /** * Whether vertical reachability is enabled for an activity in the current configuration. * diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 18d6dea927c1..aed876d4a5b3 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3479,6 +3479,25 @@ class Task extends TaskFragment { info.isFocused = isFocused(); info.isVisible = hasVisibleChildren(); info.isSleeping = shouldSleepActivities(); + info.isLetterboxDoubleTapEnabled = top != null + && top.mLetterboxUiController.isLetterboxDoubleTapEducationEnabled(); + info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET; + info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; + info.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET; + info.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET; + if (info.isLetterboxDoubleTapEnabled) { + info.topActivityLetterboxWidth = top.getBounds().width(); + info.topActivityLetterboxHeight = top.getBounds().height(); + if (info.topActivityLetterboxWidth < info.topActivityLetterboxHeight) { + // Pillarboxed + info.topActivityLetterboxHorizontalPosition = + top.mLetterboxUiController.getLetterboxPositionForHorizontalReachability(); + } else { + // Letterboxed + info.topActivityLetterboxVerticalPosition = + top.mLetterboxUiController.getLetterboxPositionForVerticalReachability(); + } + } } /** -- cgit v1.2.3-59-g8ed1b