Letterbox Reachability: Add a central stop for repositioning.

Also, remove a gap between a device edge and an app window.

Bug: 197549949
Test: atest WmTests:SizeCompatTests and manual
Change-Id: I5fb3e027ccb64e705d13b51ed66219d57a68c061
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index ed80013..243d8db 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4903,16 +4903,15 @@
         device orientation. -->
     <bool name="config_letterboxIsReachabilityEnabled">false</bool>
 
-    <!-- Default horizonal position of a center of the letterboxed app window when reachability is
-        enabled and an app is fullscreen in landscape device orientation.
-        0 corresponds to the left side of the screen and 1 to the right side. If given value < 0.0
-        or > 1, it is ignored and right positionis used (1.0). The position multiplier is changed
-        to a symmetrical value computed as (1 - current multiplier) after each double tap in the
-        letterbox area. -->
-    <item name="config_letterboxDefaultPositionMultiplierForReachability"
-          format="float" type="dimen">
-        0.9
-    </item>
+    <!-- Default horizonal position of the letterboxed app window when reachability is
+        enabled and an app is fullscreen in landscape device orientation. When reachability is
+        enabled, the position can change between left, center and right. This config defines the
+        default one:
+            - Option 0 - Left.
+            - Option 1 - Center.
+            - Option 2 - Right.
+        If given value is outside of this range, the option 1 (center) is assummed. -->
+    <integer name="config_letterboxDefaultPositionForReachability">1</integer>
 
     <!-- If true, hide the display cutout with display area -->
     <bool name="config_hideDisplayCutoutWithDisplayArea">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 0c0db2c..b917912 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4259,7 +4259,7 @@
   <java-symbol type="color" name="config_letterboxBackgroundColor" />
   <java-symbol type="dimen" name="config_letterboxHorizontalPositionMultiplier" />
   <java-symbol type="bool" name="config_letterboxIsReachabilityEnabled" />
-  <java-symbol type="dimen" name="config_letterboxDefaultPositionMultiplierForReachability" />
+  <java-symbol type="integer" name="config_letterboxDefaultPositionForReachability" />
 
   <java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
 
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index 45411a9..4b98013 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -36,6 +36,7 @@
 
 import com.android.server.UiThread;
 
+import java.util.function.IntConsumer;
 import java.util.function.Supplier;
 
 /**
@@ -70,7 +71,7 @@
     private final LetterboxSurface mFullWindowSurface = new LetterboxSurface("fullWindow");
     private final LetterboxSurface[] mSurfaces = { mLeft, mTop, mRight, mBottom };
     // Reachability gestures.
-    private final Runnable mDoubleTapCallback;
+    private final IntConsumer mDoubleTapCallback;
 
     /**
      * Constructs a Letterbox.
@@ -84,7 +85,7 @@
             Supplier<Boolean> hasWallpaperBackgroundSupplier,
             Supplier<Integer> blurRadiusSupplier,
             Supplier<Float> darkScrimAlphaSupplier,
-            Runnable doubleTapCallback) {
+            IntConsumer doubleTapCallback) {
         mSurfaceControlFactory = surfaceControlFactory;
         mTransactionFactory = transactionFactory;
         mAreCornersRounded = areCornersRounded;
@@ -262,7 +263,7 @@
         @Override
         public boolean onDoubleTapEvent(MotionEvent e) {
             if (e.getAction() == MotionEvent.ACTION_UP) {
-                mDoubleTapCallback.run();
+                mDoubleTapCallback.accept((int) e.getX());
                 return true;
             }
             return false;
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 72fbfcc..cbb473c 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -54,6 +54,27 @@
     /** Using wallpaper as a background which can be blurred or dimmed with dark scrim. */
     static final int LETTERBOX_BACKGROUND_WALLPAPER = 3;
 
+    /**
+     * Enum for Letterbox reachability position types.
+     *
+     * <p>Order from left to right is important since it's used in {@link
+     * #movePositionForReachabilityToNextRightStop} and {@link
+     * #movePositionForReachabilityToNextLeftStop}.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({LETTERBOX_REACHABILITY_POSITION_LEFT, LETTERBOX_REACHABILITY_POSITION_CENTER,
+            LETTERBOX_REACHABILITY_POSITION_RIGHT})
+    @interface LetterboxReachabilityPosition {};
+
+    /** Letterboxed app window is aligned to the left side. */
+    static final int LETTERBOX_REACHABILITY_POSITION_LEFT = 0;
+
+    /** Letterboxed app window is positioned in the horizontal center. */
+    static final int LETTERBOX_REACHABILITY_POSITION_CENTER = 1;
+
+    /** Letterboxed app window is aligned to the right side. */
+    static final int LETTERBOX_REACHABILITY_POSITION_RIGHT = 2;
+
     final Context mContext;
 
     // Aspect ratio of letterbox for fixed orientation, values <=
@@ -85,25 +106,25 @@
     // side of the screen and 1.0 to the right side.
     private float mLetterboxHorizontalPositionMultiplier;
 
-    // Default horizontal position of a center of the letterboxed app window when reachability is
-    // enabled and an app is fullscreen in landscape device orientatio. 0 corresponds to the left
-    // side of the screen and 1.0 to the right side.
-    // It is used as a starting point for mLetterboxHorizontalMultiplierForReachability.
-    private float mDefaultPositionMultiplierForReachability;
+    // Default horizontal position the letterboxed app window when reachability is enabled and
+    // an app is fullscreen in landscape device orientatio.
+    // It is used as a starting point for mLetterboxPositionForReachability.
+    @LetterboxReachabilityPosition
+    private int mDefaultPositionForReachability;
 
     // Whether reachability repositioning is allowed for letterboxed fullscreen apps in landscape
     // device orientation.
     private boolean mIsReachabilityEnabled;
 
-    // Horizontal position of a center of the letterboxed app window. 0 corresponds to
-    // the left side of the screen and 1 to the right side. Keep it global to prevent
-    // "jumps" when switching between letterboxed apps. It's updated to reposition the app
-    // window in response to a double tap gesture (see LetterboxUiController#handleDoubleTap).
-    // Used in LetterboxUiController#getHorizontalPositionMultiplier which is called from
+    // Horizontal position of a center of the letterboxed app window which is global to prevent
+    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
+    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
+    // LetterboxUiController#getHorizontalPositionMultiplier which is called from
     // ActivityRecord#updateResolvedBoundsHorizontalPosition.
     // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from
     // Overview after changing position in another app.
-    private volatile float mLetterboxHorizontalMultiplierForReachability;
+    @LetterboxReachabilityPosition
+    private volatile int mLetterboxPositionForReachability;
 
     LetterboxConfiguration(Context systemUiContext) {
         mContext = systemUiContext;
@@ -120,9 +141,8 @@
                 R.dimen.config_letterboxHorizontalPositionMultiplier);
         mIsReachabilityEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsReachabilityEnabled);
-        mDefaultPositionMultiplierForReachability = mContext.getResources().getFloat(
-                R.dimen.config_letterboxDefaultPositionMultiplierForReachability);
-        mLetterboxHorizontalMultiplierForReachability = mDefaultPositionMultiplierForReachability;
+        mDefaultPositionForReachability = readLetterboxReachabilityPositionFromConfig(mContext);
+        mLetterboxPositionForReachability = mDefaultPositionForReachability;
     }
 
     /**
@@ -395,58 +415,90 @@
     }
 
     /*
-     * Gets default horizontal position of a center of the letterboxed app window when reachability
-     * is enabled specified in {@link
-     * R.dimen.config_letterboxDefaultPositionMultiplierForReachability} or via an ADB command.
-     * 0 corresponds to the left side of the screen and 1 to the right side. The returned value is
-     * >= 0.0 and <= 1.0.
+     * Gets default horizontal position of the letterboxed app window when reachability is enabled.
+     * Specified in {@link R.integer.config_letterboxDefaultPositionForReachability} or via an ADB
+     * command.
      */
-    float getDefaultPositionMultiplierForReachability() {
-        return (mDefaultPositionMultiplierForReachability < 0.0f
-                || mDefaultPositionMultiplierForReachability > 1.0f)
-                        // Default to a right position if invalid value is provided.
-                        ? 1.0f : mDefaultPositionMultiplierForReachability;
+    @LetterboxReachabilityPosition
+    int getDefaultPositionForReachability() {
+        return mDefaultPositionForReachability;
     }
 
     /**
-     * Overrides default horizontal position of a center of the letterboxed app window when
-     * reachability is enabled. If given value < 0.0 or > 1.0, then it and a value of {@link
-     * R.dimen.config_letterboxDefaultPositionMultiplierForReachability} are ignored and the right
-     * position (1.0) is used.
+     * Overrides default horizonal position of the letterboxed app window when reachability
+     * is enabled.
      */
-    void setDefaultPositionMultiplierForReachability(float multiplier) {
-        mDefaultPositionMultiplierForReachability = multiplier;
+    void setDefaultPositionForReachability(@LetterboxReachabilityPosition int position) {
+        mDefaultPositionForReachability = position;
     }
 
     /**
-     * Resets default horizontal position of a center of the letterboxed app window when
-     * reachability is enabled to {@link
-     * R.dimen.config_letterboxDefaultPositionMultiplierForReachability}.
+     * Resets default horizontal position of the letterboxed app window when reachability is
+     * enabled to {@link R.integer.config_letterboxDefaultPositionForReachability}.
      */
-    void resetDefaultPositionMultiplierForReachability() {
-        mDefaultPositionMultiplierForReachability = mContext.getResources().getFloat(
-                R.dimen.config_letterboxDefaultPositionMultiplierForReachability);
+    void resetDefaultPositionForReachability() {
+        mDefaultPositionForReachability = readLetterboxReachabilityPositionFromConfig(mContext);
+    }
+
+    @LetterboxReachabilityPosition
+    private static int readLetterboxReachabilityPositionFromConfig(Context context) {
+        int position = context.getResources().getInteger(
+                R.integer.config_letterboxDefaultPositionForReachability);
+        return position == LETTERBOX_REACHABILITY_POSITION_LEFT
+                    || position == LETTERBOX_REACHABILITY_POSITION_CENTER
+                    || position == LETTERBOX_REACHABILITY_POSITION_RIGHT
+                    ? position : LETTERBOX_REACHABILITY_POSITION_CENTER;
     }
 
     /*
      * Gets horizontal position of a center of the letterboxed app window when reachability
      * is enabled specified. 0 corresponds to the left side of the screen and 1 to the right side.
      *
-     * <p>The position multiplier is changed to a symmetrical value computed as (1 - current
-     * multiplier) after each double tap in the letterbox area.
+     * <p>The position multiplier is changed after each double tap in the letterbox area.
      */
     float getHorizontalMultiplierForReachability() {
-        return mLetterboxHorizontalMultiplierForReachability;
+        switch (mLetterboxPositionForReachability) {
+            case LETTERBOX_REACHABILITY_POSITION_LEFT:
+                return 0.0f;
+            case LETTERBOX_REACHABILITY_POSITION_CENTER:
+                return 0.5f;
+            case LETTERBOX_REACHABILITY_POSITION_RIGHT:
+                return 1.0f;
+            default:
+                throw new AssertionError(
+                    "Unexpected letterbox position type: " + mLetterboxPositionForReachability);
+        }
+    }
+
+    /** Returns a string representing the given {@link LetterboxReachabilityPosition}. */
+    static String letterboxReachabilityPositionToString(
+            @LetterboxReachabilityPosition int position) {
+        switch (position) {
+            case LETTERBOX_REACHABILITY_POSITION_LEFT:
+                return "LETTERBOX_REACHABILITY_POSITION_LEFT";
+            case LETTERBOX_REACHABILITY_POSITION_CENTER:
+                return "LETTERBOX_REACHABILITY_POSITION_CENTER";
+            case LETTERBOX_REACHABILITY_POSITION_RIGHT:
+                return "LETTERBOX_REACHABILITY_POSITION_RIGHT";
+            default:
+                throw new AssertionError(
+                    "Unexpected letterbox position type: " + position);
+        }
     }
 
     /**
-     * Changes horizontal position of a center of the letterboxed app window to the opposite
-     * (1 - current multiplier) when reachability is enabled specified. 0 corresponds to the left
-     * side of the screen and 1 to the right side.
+     * Changes letterbox position for reachability to the next available one on the right side.
      */
-    void flipHorizontalMultiplierForReachability() {
-        mLetterboxHorizontalMultiplierForReachability =
-                1.0f - mLetterboxHorizontalMultiplierForReachability;
+    void movePositionForReachabilityToNextRightStop() {
+        mLetterboxPositionForReachability = Math.min(
+                mLetterboxPositionForReachability + 1, LETTERBOX_REACHABILITY_POSITION_RIGHT);
+    }
+
+    /**
+     * Changes letterbox position for reachability to the next available one on the left side.
+     */
+    void movePositionForReachabilityToNextLeftStop() {
+        mLetterboxPositionForReachability = Math.max(mLetterboxPositionForReachability - 1, 0);
     }
 
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index cf2afc9..2549407 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -204,12 +204,23 @@
         return mActivityRecord.mWmService.mContext.getResources();
     }
 
-    private void handleDoubleTap() {
+    private void handleDoubleTap(int x) {
         if (!isReachabilityEnabled() || mActivityRecord.isInTransition()) {
             return;
         }
 
-        mLetterboxConfiguration.flipHorizontalMultiplierForReachability();
+        if (mLetterbox.getInnerFrame().left <= x && mLetterbox.getInnerFrame().right >= x) {
+            // Only react to clicks at the sides of the letterboxed app window.
+            return;
+        }
+
+        if (mLetterbox.getInnerFrame().left > x) {
+            // Moving to the next stop on the left side of the app window: right > center > left.
+            mLetterboxConfiguration.movePositionForReachabilityToNextLeftStop();
+        } else if (mLetterbox.getInnerFrame().right < x) {
+            // Moving to the next stop on the right side of the app window: left > center > right.
+            mLetterboxConfiguration.movePositionForReachabilityToNextRightStop();
+        }
 
         // TODO(197549949): Add animation for transition.
         mActivityRecord.recomputeConfiguration();
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 47d7f03..0f8587c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -23,6 +23,9 @@
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_RIGHT;
 
 import android.content.res.Resources.NotFoundException;
 import android.graphics.Color;
@@ -44,6 +47,7 @@
 import com.android.server.LocalServices;
 import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
+import com.android.server.wm.LetterboxConfiguration.LetterboxReachabilityPosition;
 
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -787,22 +791,33 @@
         return 0;
     }
 
-    private int runSetLetterboxDefaultPositionMultiplierForReachability(PrintWriter pw)
+    private int runSetLetterboxDefaultPositionForReachability(PrintWriter pw)
             throws RemoteException {
-        final float multiplier;
+        @LetterboxReachabilityPosition final int position;
         try {
             String arg = getNextArgRequired();
-            multiplier = Float.parseFloat(arg);
-        } catch (NumberFormatException  e) {
-            getErrPrintWriter().println("Error: bad multiplier format " + e);
-            return -1;
+            switch (arg) {
+                case "left":
+                    position = LETTERBOX_REACHABILITY_POSITION_LEFT;
+                    break;
+                case "center":
+                    position = LETTERBOX_REACHABILITY_POSITION_CENTER;
+                    break;
+                case "right":
+                    position = LETTERBOX_REACHABILITY_POSITION_RIGHT;
+                    break;
+                default:
+                    getErrPrintWriter().println(
+                            "Error: 'left', 'center' or 'right' are expected as an argument");
+                    return -1;
+            }
         } catch (IllegalArgumentException  e) {
             getErrPrintWriter().println(
-                    "Error: multiplier should be provided as an argument " + e);
+                    "Error: 'left', 'center' or 'right' are expected as an argument" + e);
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setDefaultPositionMultiplierForReachability(multiplier);
+            mLetterboxConfiguration.setDefaultPositionForReachability(position);
         }
         return 0;
     }
@@ -841,8 +856,8 @@
                 case "--isReachabilityEnabled":
                     runSetLetterboxIsReachabilityEnabled(pw);
                     break;
-                case "--defaultPositionMultiplierReachability":
-                    runSetLetterboxDefaultPositionMultiplierForReachability(pw);
+                case "--defaultPositionForReachability":
+                    runSetLetterboxDefaultPositionForReachability(pw);
                     break;
                 default:
                     getErrPrintWriter().println(
@@ -885,8 +900,8 @@
                     case "isReachabilityEnabled":
                         mLetterboxConfiguration.getIsReachabilityEnabled();
                         break;
-                    case "defaultPositionMultiplierForReachability":
-                        mLetterboxConfiguration.getDefaultPositionMultiplierForReachability();
+                    case "defaultPositionForReachability":
+                        mLetterboxConfiguration.getDefaultPositionForReachability();
                         break;
                     default:
                         getErrPrintWriter().println(
@@ -982,7 +997,7 @@
             mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha();
             mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier();
             mLetterboxConfiguration.resetIsReachabilityEnabled();
-            mLetterboxConfiguration.resetDefaultPositionMultiplierForReachability();
+            mLetterboxConfiguration.resetDefaultPositionForReachability();
         }
     }
 
@@ -996,8 +1011,9 @@
                     + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
             pw.println("Is reachability enabled: "
                     + mLetterboxConfiguration.getIsReachabilityEnabled());
-            pw.println("Default position multiplier for reachability: "
-                    + mLetterboxConfiguration.getDefaultPositionMultiplierForReachability());
+            pw.println("Default position for reachability: "
+                    + LetterboxConfiguration.letterboxReachabilityPositionToString(
+                            mLetterboxConfiguration.getDefaultPositionForReachability()));
 
             pw.println("Background type: "
                     + LetterboxConfiguration.letterboxBackgroundTypeToString(
@@ -1135,11 +1151,9 @@
         pw.println("      --isReachabilityEnabled [true|1|false|0]");
         pw.println("        Whether reachability repositioning is allowed for letterboxed");
         pw.println("        fullscreen apps in landscape device orientation.");
-        pw.println("      --defaultPositionMultiplierReachability multiplier");
-        pw.println("        Default horizontal position of app window center when reachability is");
-        pw.println("        enabled. If multiplier < 0.0 or > 1, both it and ");
-        pw.println("        R.dimen.config_letterboxDefaultPositionMultiplierForReachability");
-        pw.println("        are ignored and right position (1.0) is used.");
+        pw.println("      --defaultPositionForReachability [left|center|right]");
+        pw.println("        Default horizontal position of app window  when reachability is.");
+        pw.println("        enabled.");
         pw.println("  reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
         pw.println("      |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
         pw.println("      |horizontalPositionMultiplier|isReachabilityEnabled");
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
index 78946fc..1e86522 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
@@ -63,7 +63,7 @@
         mLetterbox = new Letterbox(mSurfaces, StubTransaction::new,
                 () -> mAreCornersRounded, () -> Color.valueOf(mColor),
                 () -> mHasWallpaperBackground, () -> mBlurRadius, () -> mDarkScrimAlpha,
-                /* doubleTapCallback= */ () -> {});
+                /* doubleTapCallback= */ x -> {});
         mTransaction = spy(StubTransaction.class);
     }