Letterbox Reacheability Multiplier Persistence

Persist values for horizontal and vertical position multipliers.

Test: atest LetterboxConfigurationTest LetterboxConfigurationPersisterTest
Fixes: 199434061

Change-Id: I194828be402ff1fd234e371055522962647a4d32
diff --git a/proto/src/task_snapshot.proto b/proto/src/task_snapshot.proto
deleted file mode 100644
index 1cbc17e..0000000
--- a/proto/src/task_snapshot.proto
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2017 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.
- */
-
- syntax = "proto3";
-
- package com.android.server.wm;
-
- option java_package = "com.android.server.wm";
- option java_outer_classname = "WindowManagerProtos";
-
- message TaskSnapshotProto {
-     int32 orientation = 1;
-     int32 inset_left = 2;
-     int32 inset_top = 3;
-     int32 inset_right = 4;
-     int32 inset_bottom = 5;
-     bool is_real_snapshot = 6;
-     int32 windowing_mode = 7;
-     int32 system_ui_visibility = 8 [deprecated=true];
-     bool is_translucent = 9;
-     string top_activity_component = 10;
-     // deprecated because original width and height are stored now instead of the scale.
-     float legacy_scale = 11 [deprecated=true];
-     int64 id = 12;
-     int32 rotation = 13;
-     // The task width when the snapshot was taken
-     int32 task_width = 14;
-     // The task height when the snapshot was taken
-     int32 task_height = 15;
-     int32 appearance = 16;
-     int32 letterbox_inset_left = 17;
-     int32 letterbox_inset_top = 18;
-     int32 letterbox_inset_right = 19;
-     int32 letterbox_inset_bottom = 20;
- }
diff --git a/proto/src/windowmanager.proto b/proto/src/windowmanager.proto
new file mode 100644
index 0000000..f26404c6
--- /dev/null
+++ b/proto/src/windowmanager.proto
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto3";
+
+package com.android.server.wm;
+
+option java_package = "com.android.server.wm";
+option java_outer_classname = "WindowManagerProtos";
+
+message TaskSnapshotProto {
+  int32 orientation = 1;
+  int32 inset_left = 2;
+  int32 inset_top = 3;
+  int32 inset_right = 4;
+  int32 inset_bottom = 5;
+  bool is_real_snapshot = 6;
+  int32 windowing_mode = 7;
+  int32 system_ui_visibility = 8 [deprecated=true];
+  bool is_translucent = 9;
+  string top_activity_component = 10;
+  // deprecated because original width and height are stored now instead of the scale.
+  float legacy_scale = 11 [deprecated=true];
+  int64 id = 12;
+  int32 rotation = 13;
+  // The task width when the snapshot was taken
+  int32 task_width = 14;
+  // The task height when the snapshot was taken
+  int32 task_height = 15;
+  int32 appearance = 16;
+  int32 letterbox_inset_left = 17;
+  int32 letterbox_inset_top = 18;
+  int32 letterbox_inset_right = 19;
+  int32 letterbox_inset_bottom = 20;
+}
+
+// Persistent letterboxing configurations
+message LetterboxProto {
+
+  // Possible values for the letterbox horizontal reachability
+  enum LetterboxHorizontalReachability {
+    LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT = 0;
+    LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER = 1;
+    LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT = 2;
+  }
+
+  // Possible values for the letterbox vertical reachability
+  enum LetterboxVerticalReachability {
+    LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP = 0;
+    LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER = 1;
+    LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM = 2;
+  }
+
+  // Represents the current horizontal position for the letterboxed activity
+  LetterboxHorizontalReachability letterbox_position_for_horizontal_reachability = 1;
+  // Represents the current vertical position for the letterboxed activity
+  LetterboxVerticalReachability letterbox_position_for_vertical_reachability = 2;
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index a469c6b..c19353c 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -17,14 +17,17 @@
 package com.android.server.wm;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Color;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.function.Function;
 
 /** Reads letterbox configs from resources and controls their overrides at runtime. */
 final class LetterboxConfiguration {
@@ -156,34 +159,25 @@
     // portrait device orientation.
     private boolean mIsVerticalReachabilityEnabled;
 
-
-    // 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#updateResolvedBoundsPosition.
-    // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from
-    // Overview after changing position in another app.
-    @LetterboxHorizontalReachabilityPosition
-    private volatile int mLetterboxPositionForHorizontalReachability;
-
-    // Vertical 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#getVerticalPositionMultiplier which is called from
-    // ActivityRecord#updateResolvedBoundsPosition.
-    // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from
-    // Overview after changing position in another app.
-    @LetterboxVerticalReachabilityPosition
-    private volatile int mLetterboxPositionForVerticalReachability;
-
     // Whether education is allowed for letterboxed fullscreen apps.
     private boolean mIsEducationEnabled;
 
     // Whether using split screen aspect ratio as a default aspect ratio for unresizable apps.
     private boolean mIsSplitScreenAspectRatioForUnresizableAppsEnabled;
 
+    // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier
+    @NonNull
+    private final LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+
     LetterboxConfiguration(Context systemUiContext) {
+        this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
+                () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext),
+                () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext)));
+    }
+
+    @VisibleForTesting
+    LetterboxConfiguration(Context systemUiContext,
+            LetterboxConfigurationPersister letterboxConfigurationPersister) {
         mContext = systemUiContext;
         mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat(
                 R.dimen.config_fixedOrientationLetterboxAspectRatio);
@@ -206,14 +200,14 @@
                 readLetterboxHorizontalReachabilityPositionFromConfig(mContext);
         mDefaultPositionForVerticalReachability =
                 readLetterboxVerticalReachabilityPositionFromConfig(mContext);
-        mLetterboxPositionForHorizontalReachability = mDefaultPositionForHorizontalReachability;
-        mLetterboxPositionForVerticalReachability = mDefaultPositionForVerticalReachability;
         mIsEducationEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsEducationEnabled);
         setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat(
                 R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps));
         mIsSplitScreenAspectRatioForUnresizableAppsEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
+        mLetterboxConfigurationPersister = letterboxConfigurationPersister;
+        mLetterboxConfigurationPersister.start();
     }
 
     /**
@@ -653,7 +647,9 @@
      * <p>The position multiplier is changed after each double tap in the letterbox area.
      */
     float getHorizontalMultiplierForReachability() {
-        switch (mLetterboxPositionForHorizontalReachability) {
+        final int letterboxPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        switch (letterboxPositionForHorizontalReachability) {
             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
                 return 0.0f;
             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER:
@@ -662,10 +658,11 @@
                 return 1.0f;
             default:
                 throw new AssertionError(
-                    "Unexpected letterbox position type: "
-                            + mLetterboxPositionForHorizontalReachability);
+                        "Unexpected letterbox position type: "
+                                + letterboxPositionForHorizontalReachability);
         }
     }
+
     /*
      * Gets vertical position of a center of the letterboxed app window when reachability
      * is enabled specified. 0 corresponds to the top side of the screen and 1 to the bottom side.
@@ -673,7 +670,9 @@
      * <p>The position multiplier is changed after each double tap in the letterbox area.
      */
     float getVerticalMultiplierForReachability() {
-        switch (mLetterboxPositionForVerticalReachability) {
+        final int letterboxPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        switch (letterboxPositionForVerticalReachability) {
             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
                 return 0.0f;
             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER:
@@ -683,7 +682,7 @@
             default:
                 throw new AssertionError(
                         "Unexpected letterbox position type: "
-                                + mLetterboxPositionForVerticalReachability);
+                                + letterboxPositionForVerticalReachability);
         }
     }
 
@@ -693,7 +692,7 @@
      */
     @LetterboxHorizontalReachabilityPosition
     int getLetterboxPositionForHorizontalReachability() {
-        return mLetterboxPositionForHorizontalReachability;
+        return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
     }
 
     /*
@@ -702,7 +701,7 @@
      */
     @LetterboxVerticalReachabilityPosition
     int getLetterboxPositionForVerticalReachability() {
-        return mLetterboxPositionForVerticalReachability;
+        return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
     }
 
     /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */
@@ -742,9 +741,8 @@
      * right side.
      */
     void movePositionForHorizontalReachabilityToNextRightStop() {
-        mLetterboxPositionForHorizontalReachability = Math.min(
-                mLetterboxPositionForHorizontalReachability + 1,
-                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT);
+        updatePositionForHorizontalReachability(prev -> Math.min(
+                prev + 1, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT));
     }
 
     /**
@@ -752,8 +750,7 @@
      * side.
      */
     void movePositionForHorizontalReachabilityToNextLeftStop() {
-        mLetterboxPositionForHorizontalReachability =
-                Math.max(mLetterboxPositionForHorizontalReachability - 1, 0);
+        updatePositionForHorizontalReachability(prev -> Math.max(prev - 1, 0));
     }
 
     /**
@@ -761,9 +758,8 @@
      * side.
      */
     void movePositionForVerticalReachabilityToNextBottomStop() {
-        mLetterboxPositionForVerticalReachability = Math.min(
-                mLetterboxPositionForVerticalReachability + 1,
-                LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM);
+        updatePositionForVerticalReachability(prev -> Math.min(
+                prev + 1, LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM));
     }
 
     /**
@@ -771,8 +767,7 @@
      * side.
      */
     void movePositionForVerticalReachabilityToNextTopStop() {
-        mLetterboxPositionForVerticalReachability =
-                Math.max(mLetterboxPositionForVerticalReachability - 1, 0);
+        updatePositionForVerticalReachability(prev -> Math.max(prev - 1, 0));
     }
 
     /**
@@ -822,4 +817,26 @@
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
     }
 
+    /** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */
+    private void updatePositionForHorizontalReachability(
+            Function<Integer, Integer> newHorizonalPositionFun) {
+        final int letterboxPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int nextHorizontalPosition = newHorizonalPositionFun.apply(
+                letterboxPositionForHorizontalReachability);
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+                nextHorizontalPosition);
+    }
+
+    /** Calculates a new letterboxPositionForVerticalReachability value and updates the store */
+    private void updatePositionForVerticalReachability(
+            Function<Integer, Integer> newVerticalPositionFun) {
+        final int letterboxPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        final int nextVerticalPosition = newVerticalPositionFun.apply(
+                letterboxPositionForVerticalReachability);
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+                nextVerticalPosition);
+    }
+
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
new file mode 100644
index 0000000..70639b1
--- /dev/null
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 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.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Environment;
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition;
+import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilityPosition;
+import com.android.server.wm.nano.WindowManagerProtos;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Persists the values of letterboxPositionForHorizontalReachability and
+ * letterboxPositionForVerticalReachability for {@link LetterboxConfiguration}.
+ */
+class LetterboxConfigurationPersister {
+
+    private static final String TAG =
+            TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM;
+
+    @VisibleForTesting
+    static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config";
+
+    private final Context mContext;
+    private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier;
+    private final Supplier<Integer> mDefaultVerticalReachabilitySupplier;
+
+    // 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#updateResolvedBoundsPosition.
+    @LetterboxHorizontalReachabilityPosition
+    private volatile int mLetterboxPositionForHorizontalReachability;
+
+    // Vertical 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#getVerticalPositionMultiplier which is called from
+    // ActivityRecord#updateResolvedBoundsPosition.
+    @LetterboxVerticalReachabilityPosition
+    private volatile int mLetterboxPositionForVerticalReachability;
+
+    @NonNull
+    private final AtomicFile mConfigurationFile;
+
+    @Nullable
+    private final Consumer<String> mCompletionCallback;
+
+    @NonNull
+    private final PersisterQueue mPersisterQueue;
+
+    LetterboxConfigurationPersister(Context systemUiContext,
+            Supplier<Integer> defaultHorizontalReachabilitySupplier,
+            Supplier<Integer> defaultVerticalReachabilitySupplier) {
+        this(systemUiContext, defaultHorizontalReachabilitySupplier,
+                defaultVerticalReachabilitySupplier,
+                Environment.getDataSystemDirectory(), new PersisterQueue(),
+                /* completionCallback */ null);
+    }
+
+    @VisibleForTesting
+    LetterboxConfigurationPersister(Context systemUiContext,
+            Supplier<Integer> defaultHorizontalReachabilitySupplier,
+            Supplier<Integer> defaultVerticalReachabilitySupplier, File configFolder,
+            PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) {
+        mContext = systemUiContext.createDeviceProtectedStorageContext();
+        mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier;
+        mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier;
+        mCompletionCallback = completionCallback;
+        final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME);
+        mConfigurationFile = new AtomicFile(prefFiles);
+        mPersisterQueue = persisterQueue;
+        readCurrentConfiguration();
+    }
+
+    /**
+     * Startes the persistence queue
+     */
+    void start() {
+        mPersisterQueue.startPersisting();
+    }
+
+    /*
+     * Gets the horizontal position of the letterboxed app window when horizontal reachability is
+     * enabled.
+     */
+    @LetterboxHorizontalReachabilityPosition
+    int getLetterboxPositionForHorizontalReachability() {
+        return mLetterboxPositionForHorizontalReachability;
+    }
+
+    /*
+     * Gets the vertical position of the letterboxed app window when vertical reachability is
+     * enabled.
+     */
+    @LetterboxVerticalReachabilityPosition
+    int getLetterboxPositionForVerticalReachability() {
+        return mLetterboxPositionForVerticalReachability;
+    }
+
+    /**
+     * Updates letterboxPositionForVerticalReachability if different from the current value
+     */
+    void setLetterboxPositionForHorizontalReachability(
+            int letterboxPositionForHorizontalReachability) {
+        if (mLetterboxPositionForHorizontalReachability
+                != letterboxPositionForHorizontalReachability) {
+            mLetterboxPositionForHorizontalReachability =
+                    letterboxPositionForHorizontalReachability;
+            updateConfiguration();
+        }
+    }
+
+    /**
+     * Updates letterboxPositionForVerticalReachability if different from the current value
+     */
+    void setLetterboxPositionForVerticalReachability(
+            int letterboxPositionForVerticalReachability) {
+        if (mLetterboxPositionForVerticalReachability != letterboxPositionForVerticalReachability) {
+            mLetterboxPositionForVerticalReachability = letterboxPositionForVerticalReachability;
+            updateConfiguration();
+        }
+    }
+
+    @VisibleForTesting
+    void useDefaultValue() {
+        mLetterboxPositionForHorizontalReachability = mDefaultHorizontalReachabilitySupplier.get();
+        mLetterboxPositionForVerticalReachability = mDefaultVerticalReachabilitySupplier.get();
+    }
+
+    private void readCurrentConfiguration() {
+        FileInputStream fis = null;
+        try {
+            fis = mConfigurationFile.openRead();
+            byte[] protoData = readInputStream(fis);
+            final WindowManagerProtos.LetterboxProto letterboxData =
+                    WindowManagerProtos.LetterboxProto.parseFrom(protoData);
+            mLetterboxPositionForHorizontalReachability =
+                    letterboxData.letterboxPositionForHorizontalReachability;
+            mLetterboxPositionForVerticalReachability =
+                    letterboxData.letterboxPositionForVerticalReachability;
+        } catch (IOException ioe) {
+            Slog.e(TAG,
+                    "Error reading from LetterboxConfigurationPersister. "
+                            + "Using default values!", ioe);
+            useDefaultValue();
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException e) {
+                    useDefaultValue();
+                    Slog.e(TAG, "Error reading from LetterboxConfigurationPersister ", e);
+                }
+            }
+        }
+    }
+
+    private void updateConfiguration() {
+        mPersisterQueue.addItem(new UpdateValuesCommand(mConfigurationFile,
+                mLetterboxPositionForHorizontalReachability,
+                mLetterboxPositionForVerticalReachability,
+                mCompletionCallback), /* flush */ true);
+    }
+
+    private static byte[] readInputStream(InputStream in) throws IOException {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            byte[] buffer = new byte[1024];
+            int size = in.read(buffer);
+            while (size > 0) {
+                outputStream.write(buffer, 0, size);
+                size = in.read(buffer);
+            }
+            return outputStream.toByteArray();
+        } finally {
+            outputStream.close();
+        }
+    }
+
+    private static class UpdateValuesCommand implements
+            PersisterQueue.WriteQueueItem<UpdateValuesCommand> {
+
+        @NonNull
+        private final AtomicFile mFileToUpdate;
+        @Nullable
+        private final Consumer<String> mOnComplete;
+
+
+        private final int mHorizontalReachability;
+        private final int mVerticalReachability;
+
+        UpdateValuesCommand(@NonNull AtomicFile fileToUpdate,
+                int horizontalReachability, int verticalReachability,
+                @Nullable Consumer<String> onComplete) {
+            mFileToUpdate = fileToUpdate;
+            mHorizontalReachability = horizontalReachability;
+            mVerticalReachability = verticalReachability;
+            mOnComplete = onComplete;
+        }
+
+        @Override
+        public void process() {
+            final WindowManagerProtos.LetterboxProto letterboxData =
+                    new WindowManagerProtos.LetterboxProto();
+            letterboxData.letterboxPositionForHorizontalReachability = mHorizontalReachability;
+            letterboxData.letterboxPositionForVerticalReachability = mVerticalReachability;
+            final byte[] bytes = WindowManagerProtos.LetterboxProto.toByteArray(letterboxData);
+
+            FileOutputStream fos = null;
+            try {
+                fos = mFileToUpdate.startWrite();
+                fos.write(bytes);
+                mFileToUpdate.finishWrite(fos);
+            } catch (IOException ioe) {
+                mFileToUpdate.failWrite(fos);
+                Slog.e(TAG,
+                        "Error writing to LetterboxConfigurationPersister. "
+                                + "Using default values!", ioe);
+            } finally {
+                if (mOnComplete != null) {
+                    mOnComplete.accept("UpdateValuesCommand");
+                }
+            }
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
new file mode 100644
index 0000000..1246d1e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2022 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.server.wm;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import static com.android.server.wm.LetterboxConfigurationPersister.LETTERBOX_CONFIGURATION_FILENAME;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.util.AtomicFile;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+@SmallTest
+@Presubmit
+public class LetterboxConfigurationPersisterTest {
+
+    private static final long TIMEOUT = 2000L; // 2 secs
+
+    private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+    private Context mContext;
+    private PersisterQueue mPersisterQueue;
+    private QueueState mQueueState;
+    private PersisterQueue.Listener mQueueListener;
+    private File mConfigFolder;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = getInstrumentation().getTargetContext();
+        mConfigFolder = mContext.getFilesDir();
+        mPersisterQueue = new PersisterQueue();
+        mQueueState = new QueueState();
+        mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(mContext,
+                () -> mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForHorizontalReachability),
+                () -> mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForVerticalReachability),
+                mConfigFolder, mPersisterQueue, mQueueState);
+        mQueueListener = queueEmpty -> mQueueState.onItemAdded();
+        mPersisterQueue.addListener(mQueueListener);
+        mLetterboxConfigurationPersister.start();
+    }
+
+    public void tearDown() throws InterruptedException {
+        deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
+        waitForCompletion(mPersisterQueue);
+        mPersisterQueue.removeListener(mQueueListener);
+        stopPersisterSafe(mPersisterQueue);
+    }
+
+    @Test
+    public void test_whenStoreIsCreated_valuesAreDefaults() {
+        final int positionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int defaultPositionForHorizontalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForHorizontalReachability);
+        Assert.assertEquals(defaultPositionForHorizontalReachability,
+                positionForHorizontalReachability);
+        final int positionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        final int defaultPositionForVerticalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForVerticalReachability);
+        Assert.assertEquals(defaultPositionForVerticalReachability,
+                positionForVerticalReachability);
+    }
+
+    @Test
+    public void test_whenUpdatedWithNewValues_valuesAreWritten() {
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
+        waitForCompletion(mPersisterQueue);
+        final int newPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int newPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                newPositionForHorizontalReachability);
+        Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                newPositionForVerticalReachability);
+    }
+
+    @Test
+    public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() {
+        final PersisterQueue firstPersisterQueue = new PersisterQueue();
+        final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
+                mContext, () -> -1, () -> -1, mContext.getFilesDir(), firstPersisterQueue,
+                mQueueState);
+        firstPersister.start();
+        firstPersister.setLetterboxPositionForHorizontalReachability(
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
+        firstPersister.setLetterboxPositionForVerticalReachability(
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
+        waitForCompletion(firstPersisterQueue);
+        stopPersisterSafe(firstPersisterQueue);
+        final PersisterQueue secondPersisterQueue = new PersisterQueue();
+        final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
+                mContext, () -> -1, () -> -1, mContext.getFilesDir(), secondPersisterQueue,
+                mQueueState);
+        secondPersister.start();
+        final int newPositionForHorizontalReachability =
+                secondPersister.getLetterboxPositionForHorizontalReachability();
+        final int newPositionForVerticalReachability =
+                secondPersister.getLetterboxPositionForVerticalReachability();
+        Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                newPositionForHorizontalReachability);
+        Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                newPositionForVerticalReachability);
+        deleteConfiguration(secondPersister, secondPersisterQueue);
+        waitForCompletion(secondPersisterQueue);
+        stopPersisterSafe(secondPersisterQueue);
+    }
+
+    @Test
+    public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() {
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
+        waitForCompletion(mPersisterQueue);
+        final int newPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int newPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                newPositionForHorizontalReachability);
+        Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                newPositionForVerticalReachability);
+        deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
+        waitForCompletion(mPersisterQueue);
+        final int positionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int defaultPositionForHorizontalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForHorizontalReachability);
+        Assert.assertEquals(defaultPositionForHorizontalReachability,
+                positionForHorizontalReachability);
+        final int positionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        final int defaultPositionForVerticalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForVerticalReachability);
+        Assert.assertEquals(defaultPositionForVerticalReachability,
+                positionForVerticalReachability);
+    }
+
+    private void stopPersisterSafe(PersisterQueue persisterQueue) {
+        try {
+            persisterQueue.stopPersisting();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void waitForCompletion(PersisterQueue persisterQueue) {
+        final long endTime = System.currentTimeMillis() + TIMEOUT;
+        // The queue could be empty but the last item still processing and not completed. For this
+        // reason the completion happens when there are not more items to process and the last one
+        // has completed.
+        while (System.currentTimeMillis() < endTime && (!isQueueEmpty(persisterQueue)
+                || !hasLastItemCompleted())) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException ie) { /* Nope */}
+        }
+    }
+
+    private boolean isQueueEmpty(PersisterQueue persisterQueue) {
+        return persisterQueue.findLastItem(
+                writeQueueItem -> true, PersisterQueue.WriteQueueItem.class) != null;
+    }
+
+    private boolean hasLastItemCompleted() {
+        return mQueueState.isEmpty();
+    }
+
+    private void deleteConfiguration(LetterboxConfigurationPersister persister,
+            PersisterQueue persisterQueue) {
+        final AtomicFile fileToDelete = new AtomicFile(
+                new File(mConfigFolder, LETTERBOX_CONFIGURATION_FILENAME));
+        persisterQueue.addItem(
+                new DeleteFileCommand(fileToDelete, mQueueState.andThen(
+                        s -> persister.useDefaultValue())), true);
+    }
+
+    private static class DeleteFileCommand implements
+            PersisterQueue.WriteQueueItem<DeleteFileCommand> {
+
+        @NonNull
+        private final AtomicFile mFileToDelete;
+        @Nullable
+        private final Consumer<String> mOnComplete;
+
+        DeleteFileCommand(@NonNull AtomicFile fileToDelete, Consumer<String> onComplete) {
+            mFileToDelete = fileToDelete;
+            mOnComplete = onComplete;
+        }
+
+        @Override
+        public void process() {
+            mFileToDelete.delete();
+            if (mOnComplete != null) {
+                mOnComplete.accept("DeleteFileCommand");
+            }
+        }
+    }
+
+    // Contains the current length of the persister queue
+    private static class QueueState implements Consumer<String> {
+
+        // The current number of commands in the queue
+        @VisibleForTesting
+        private final AtomicInteger mCounter = new AtomicInteger(0);
+
+        @Override
+        public void accept(String s) {
+            mCounter.decrementAndGet();
+        }
+
+        void onItemAdded() {
+            mCounter.incrementAndGet();
+        }
+
+        boolean isEmpty() {
+            return mCounter.get() == 0;
+        }
+
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
new file mode 100644
index 0000000..c927f9e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2022 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.server.wm;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.function.Consumer;
+
+@SmallTest
+@Presubmit
+public class LetterboxConfigurationTest {
+
+    private LetterboxConfiguration mLetterboxConfiguration;
+    private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = getInstrumentation().getTargetContext();
+        mLetterboxConfigurationPersister = mock(LetterboxConfigurationPersister.class);
+        mLetterboxConfiguration = new LetterboxConfiguration(context,
+                mLetterboxConfigurationPersister);
+    }
+
+    @Test
+    public void test_whenReadingValues_storeIsInvoked() {
+        mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability();
+        verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability();
+        mLetterboxConfiguration.getLetterboxPositionForVerticalReachability();
+        verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability();
+    }
+
+    @Test
+    public void test_whenSettingValues_updateConfigurationIsInvoked() {
+        mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
+        verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+                anyInt());
+        mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
+        verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+                anyInt());
+    }
+
+    @Test
+    public void test_whenMovedHorizontally_updatePositionAccordingly() {
+        // Starting from center
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        // Starting from left
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        // Starting from right
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+    }
+
+    @Test
+    public void test_whenMovedVertically_updatePositionAccordingly() {
+        // Starting from center
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        // Starting from top
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        // Starting from bottom
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+    }
+
+    private void assertForHorizontalMove(int from, int expected, int expectedTime,
+            Consumer<LetterboxConfiguration> move) {
+        // We are in the current position
+        when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability())
+                .thenReturn(from);
+        move.accept(mLetterboxConfiguration);
+        verify(mLetterboxConfigurationPersister,
+                times(expectedTime)).setLetterboxPositionForHorizontalReachability(
+                expected);
+    }
+
+    private void assertForVerticalMove(int from, int expected, int expectedTime,
+            Consumer<LetterboxConfiguration> move) {
+        // We are in the current position
+        when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability())
+                .thenReturn(from);
+        move.accept(mLetterboxConfiguration);
+        verify(mLetterboxConfigurationPersister,
+                times(expectedTime)).setLetterboxPositionForVerticalReachability(
+                expected);
+    }
+}