Merge "Maintain the position of accessibility floating menu" into sc-dev
diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java
index bf09975..782cd29 100644
--- a/packages/SystemUI/src/com/android/systemui/Prefs.java
+++ b/packages/SystemUI/src/com/android/systemui/Prefs.java
@@ -74,7 +74,8 @@
             Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP,
             Key.HAS_SEEN_REVERSE_BOTTOM_SHEET,
             Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT,
-            Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP
+            Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
+            Key.ACCESSIBILITY_FLOATING_MENU_POSITION
     })
     // TODO: annotate these with their types so {@link PrefsCommandLine} can know how to set them
     public @interface Key {
@@ -125,6 +126,7 @@
         String CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT = "ControlsStructureSwipeTooltipCount";
         String HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP =
                 "HasSeenAccessibilityFloatingMenuDockTooltip";
+        String ACCESSIBILITY_FLOATING_MENU_POSITION = "AccessibilityFloatingMenuPosition";
     }
 
     public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
index ee62768..47f3739 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
@@ -28,12 +28,16 @@
 import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType;
 import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.SizeType;
 
+import android.annotation.FloatRange;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.Prefs;
@@ -44,7 +48,13 @@
 public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
     private static final int DEFAULT_FADE_EFFECT_IS_ENABLED = 1;
     private static final int DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED = 0;
+    @FloatRange(from = 0.0, to = 1.0)
     private static final float DEFAULT_OPACITY_VALUE = 0.55f;
+    @FloatRange(from = 0.0, to = 1.0)
+    private static final float DEFAULT_POSITION_X_PERCENT = 1.0f;
+    @FloatRange(from = 0.0, to = 1.0)
+    private static final float DEFAULT_POSITION_Y_PERCENT = 0.8f;
+
     private final Context mContext;
     private final AccessibilityFloatingMenuView mMenuView;
     private final MigrationTooltipView mMigrationTooltipView;
@@ -85,7 +95,10 @@
             };
 
     public AccessibilityFloatingMenu(Context context) {
-        this(context, new AccessibilityFloatingMenuView(context));
+        mContext = context;
+        mMenuView = new AccessibilityFloatingMenuView(context, getPosition(context));
+        mMigrationTooltipView = new MigrationTooltipView(mContext, mMenuView);
+        mDockTooltipView = new DockTooltipView(mContext, mMenuView);
     }
 
     @VisibleForTesting
@@ -113,7 +126,7 @@
                 getOpacityValue(mContext));
         mMenuView.setSizeType(getSizeType(mContext));
         mMenuView.setShapeType(getShapeType(mContext));
-        mMenuView.setOnDragEndListener(this::showDockTooltipIfNecessary);
+        mMenuView.setOnDragEndListener(this::onDragEnd);
 
         showMigrationTooltipIfNecessary();
 
@@ -127,12 +140,25 @@
         }
 
         mMenuView.hide();
+        mMenuView.setOnDragEndListener(null);
         mMigrationTooltipView.hide();
         mDockTooltipView.hide();
 
         unregisterContentObservers();
     }
 
+    @NonNull
+    private Position getPosition(Context context) {
+        final String absolutePositionString = Prefs.getString(context,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
+
+        if (TextUtils.isEmpty(absolutePositionString)) {
+            return new Position(DEFAULT_POSITION_X_PERCENT, DEFAULT_POSITION_Y_PERCENT);
+        } else {
+            return Position.fromString(absolutePositionString);
+        }
+    }
+
     // Migration tooltip was the android S feature. It's just used on the Android version from R
     // to S. In addition, it only shows once.
     private void showMigrationTooltipIfNecessary() {
@@ -150,18 +176,28 @@
                 DEFAULT_MIGRATION_TOOLTIP_PROMPT_IS_DISABLED) == /* enabled */ 1;
     }
 
+    private void onDragEnd(Position position) {
+        savePosition(mContext, position);
+        showDockTooltipIfNecessary(mContext);
+    }
+
+    private void savePosition(Context context, Position position) {
+        Prefs.putString(context, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
+                position.toString());
+    }
+
     /**
      * Shows tooltip when user drags accessibility floating menu for the first time.
      */
-    private void showDockTooltipIfNecessary() {
-        if (!Prefs.get(mContext).getBoolean(
+    private void showDockTooltipIfNecessary(Context context) {
+        if (!Prefs.get(context).getBoolean(
                 HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, false)) {
             // if the menu is an oval, the user has already dragged it out, so show the tooltip.
             if (mMenuView.isOvalShape()) {
                 mDockTooltipView.show();
             }
 
-            Prefs.putBoolean(mContext, HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, true);
+            Prefs.putBoolean(context, HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, true);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
index 259a9f7..5bb5522 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
@@ -26,6 +26,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.annotation.FloatRange;
 import android.annotation.IntDef;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -85,7 +86,6 @@
     private static final int FADE_EFFECT_DURATION_MS = 3000;
     private static final int SNAP_TO_LOCATION_DURATION_MS = 150;
     private static final int MIN_WINDOW_Y = 0;
-    private static final float LOCATION_Y_PERCENTAGE = 0.8f;
 
     private static final int ANIMATION_START_OFFSET = 600;
     private static final int ANIMATION_DURATION_MS = 600;
@@ -97,7 +97,7 @@
     private boolean mIsDragging = false;
     private boolean mImeVisibility;
     @Alignment
-    private int mAlignment = Alignment.RIGHT;
+    private int mAlignment;
     @SizeType
     private int mSizeType = SizeType.SMALL;
     @VisibleForTesting
@@ -105,7 +105,7 @@
     int mShapeType = ShapeType.OVAL;
     private int mTemporaryShapeType;
     @RadiusType
-    private int mRadiusType = RadiusType.LEFT_HALF_OVAL;
+    private int mRadiusType;
     private int mMargin;
     private int mPadding;
     private int mScreenHeight;
@@ -118,7 +118,7 @@
     private int mRelativeToPointerDownX;
     private int mRelativeToPointerDownY;
     private float mRadius;
-    private float mPercentageY = LOCATION_Y_PERCENTAGE;
+    private final Position mPosition;
     private float mSquareScaledTouchSlop;
     private final Configuration mLastConfiguration;
     private Optional<OnDragEndListener> mOnDragEndListener = Optional.empty();
@@ -182,25 +182,35 @@
     interface OnDragEndListener {
 
         /**
-         * Invoked when the floating menu has dragged end.
+         * Called when a drag is completed.
+         *
+         * @param position Stores information about the position
          */
-        void onDragEnd();
+        void onDragEnd(Position position);
     }
 
-    public AccessibilityFloatingMenuView(Context context) {
-        this(context, new RecyclerView(context));
+    public AccessibilityFloatingMenuView(Context context, @NonNull Position position) {
+        this(context, position, new RecyclerView(context));
     }
 
     @VisibleForTesting
-    AccessibilityFloatingMenuView(Context context,
+    AccessibilityFloatingMenuView(Context context, @NonNull Position position,
             RecyclerView listView) {
         super(context);
 
         mListView = listView;
         mWindowManager = context.getSystemService(WindowManager.class);
-        mCurrentLayoutParams = createDefaultLayoutParams();
         mAdapter = new AccessibilityTargetAdapter(mTargets);
         mUiHandler = createUiHandler();
+        mPosition = position;
+        mAlignment = transformToAlignment(mPosition.getPercentageX());
+        mRadiusType = (mAlignment == Alignment.RIGHT)
+                ? RadiusType.LEFT_HALF_OVAL
+                : RadiusType.RIGHT_HALF_OVAL;
+
+        updateDimensions();
+
+        mCurrentLayoutParams = createDefaultLayoutParams();
 
         mFadeOutAnimator = ValueAnimator.ofFloat(1.0f, mFadeOutValue);
         mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
@@ -213,10 +223,11 @@
         mDragAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                mAlignment = calculateCurrentAlignment();
-                mPercentageY = calculateCurrentPercentageY();
+                mPosition.update(transformCurrentPercentageXToEdge(),
+                        calculateCurrentPercentageY());
+                mAlignment = transformToAlignment(mPosition.getPercentageX());
 
-                updateLocationWith(mAlignment, mPercentageY);
+                updateLocationWith(mPosition);
 
                 updateInsetWith(getResources().getConfiguration().uiMode, mAlignment);
 
@@ -227,13 +238,13 @@
 
                 fadeOut();
 
-                mOnDragEndListener.ifPresent(OnDragEndListener::onDragEnd);
+                mOnDragEndListener.ifPresent(
+                        onDragEndListener -> onDragEndListener.onDragEnd(mPosition));
             }
         });
 
         mLastConfiguration = new Configuration(getResources().getConfiguration());
 
-        updateDimensions();
         initListView();
         updateStrokeWith(getResources().getConfiguration().uiMode, mAlignment);
     }
@@ -423,7 +434,7 @@
         updateRadiusWith(newSizeType, mRadiusType, mTargets.size());
 
         // When the icon sized changed, the menu size and location will be impacted.
-        updateLocationWith(mAlignment, mPercentageY);
+        updateLocationWith(mPosition);
         updateScrollModeWith(hasExceededMaxLayoutHeight());
         updateOffsetWith(mShapeType, mAlignment);
         setSystemGestureExclusion();
@@ -446,14 +457,14 @@
         fadeOut();
     }
 
-    public void setOnDragEndListener(OnDragEndListener onDragListener) {
-        mOnDragEndListener = Optional.ofNullable(onDragListener);
+    public void setOnDragEndListener(OnDragEndListener onDragEndListener) {
+        mOnDragEndListener = Optional.ofNullable(onDragEndListener);
     }
 
     void startTranslateXAnimation() {
         fadeIn();
 
-        final float toXValue = mAlignment == Alignment.RIGHT
+        final float toXValue = (mAlignment == Alignment.RIGHT)
                 ? ANIMATION_TO_X_VALUE
                 : -ANIMATION_TO_X_VALUE;
         final TranslateAnimation animation =
@@ -581,7 +592,7 @@
         final boolean currentImeVisibility = insets.isVisible(ime());
         if (currentImeVisibility != mImeVisibility) {
             mImeVisibility = currentImeVisibility;
-            updateLocationWith(mAlignment, mPercentageY);
+            updateLocationWith(mPosition);
         }
 
         return insets;
@@ -697,8 +708,10 @@
         params.receiveInsetsIgnoringZOrder = true;
         params.windowAnimations = android.R.style.Animation_Translucent;
         params.gravity = Gravity.START | Gravity.TOP;
-        params.x = getMaxWindowX();
-        params.y = (int) (getMaxWindowY() * mPercentageY);
+        params.x = (mAlignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
+//        params.y = (int) (mPosition.getPercentageY() * getMaxWindowY());
+        final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
+        params.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
         updateAccessibilityTitle(params);
         return params;
     }
@@ -716,7 +729,7 @@
         updateItemViewWith(mSizeType);
         updateColor();
         updateStrokeWith(newConfig.uiMode, mAlignment);
-        updateLocationWith(mAlignment, mPercentageY);
+        updateLocationWith(mPosition);
         updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
         updateScrollModeWith(hasExceededMaxLayoutHeight());
         setSystemGestureExclusion();
@@ -765,9 +778,10 @@
     /**
      * Updates the floating menu to be fixed at the side of the screen.
      */
-    private void updateLocationWith(@Alignment int side, float percentageCurrentY) {
-        mCurrentLayoutParams.x = (side == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
-        final int currentLayoutY = (int) (percentageCurrentY * getMaxWindowY());
+    private void updateLocationWith(Position position) {
+        final @Alignment int alignment = transformToAlignment(position.getPercentageX());
+        mCurrentLayoutParams.x = (alignment == Alignment.RIGHT) ? getMaxWindowX() : getMinWindowX();
+        final int currentLayoutY = (int) (position.getPercentageY() * getMaxWindowY());
         mCurrentLayoutParams.y = Math.max(MIN_WINDOW_Y, currentLayoutY - getInterval());
         mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
     }
@@ -861,10 +875,17 @@
     }
 
     @Alignment
-    private int calculateCurrentAlignment() {
-        return mCurrentLayoutParams.x >= ((getMinWindowX() + getMaxWindowX()) / 2)
-                ? Alignment.RIGHT
-                : Alignment.LEFT;
+    private int transformToAlignment(@FloatRange(from = 0.0, to = 1.0) float percentageX) {
+        return (percentageX < 0.5f) ? Alignment.LEFT : Alignment.RIGHT;
+    }
+
+    private float transformCurrentPercentageXToEdge() {
+        final float percentageX = calculateCurrentPercentageX();
+        return (percentageX < 0.5) ? 0.0f : 1.0f;
+    }
+
+    private float calculateCurrentPercentageX() {
+        return mCurrentLayoutParams.x / (float) getMaxWindowX();
     }
 
     private float calculateCurrentPercentageY() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
new file mode 100644
index 0000000..7b7eda8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
+
+import android.annotation.FloatRange;
+import android.text.TextUtils;
+
+/**
+ * Stores information about the position, which includes percentage of X-axis of the screen,
+ * percentage of Y-axis of the screen.
+ */
+public class Position {
+
+    private static final char STRING_SEPARATOR = ',';
+    private static final TextUtils.SimpleStringSplitter sStringCommaSplitter =
+            new TextUtils.SimpleStringSplitter(STRING_SEPARATOR);
+
+    private float mPercentageX;
+    private float mPercentageY;
+
+    /**
+     * Creates a {@link Position} from a encoded string described in {@link #toString()}.
+     *
+     * @param positionString A string conform to the format described in {@link #toString()}
+     * @return A {@link Position} with the given value retrieved from {@code absolutePositionString}
+     * @throws IllegalArgumentException If {@code positionString} does not conform to the format
+     *                                  described in {@link #toString()}
+     */
+    public static Position fromString(String positionString) {
+        sStringCommaSplitter.setString(positionString);
+        if (sStringCommaSplitter.hasNext()) {
+            final float percentageX = Float.parseFloat(sStringCommaSplitter.next());
+            final float percentageY = Float.parseFloat(sStringCommaSplitter.next());
+            return new Position(percentageX, percentageY);
+        }
+
+        throw new IllegalArgumentException(
+                "Invalid Position string: " + positionString);
+    }
+
+    Position(float percentageX, float percentageY) {
+        update(percentageX, percentageY);
+    }
+
+    @Override
+    public String toString() {
+        return mPercentageX + ", " + mPercentageY;
+    }
+
+    /**
+     * Updates the position with {@code percentageX} and {@code percentageY}.
+     *
+     * @param percentageX the new percentage of X-axis of the screen, from 0.0 to 1.0.
+     * @param percentageY the new percentage of Y-axis of the screen, from 0.0 to 1.0.
+     */
+    public void update(@FloatRange(from = 0.0, to = 1.0) float percentageX,
+            @FloatRange(from = 0.0, to = 1.0) float percentageY) {
+        mPercentageX = percentageX;
+        mPercentageY = percentageY;
+    }
+
+    public float getPercentageX() {
+        return mPercentageX;
+    }
+
+    public float getPercentageY() {
+        return mPercentageY;
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
index 337d97e..e027a2b7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuTest.java
@@ -22,7 +22,6 @@
 
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
 
 import android.content.Context;
 import android.testing.AndroidTestingRunner;
@@ -31,15 +30,16 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -50,6 +50,9 @@
 @TestableLooper.RunWithLooper
 public class AccessibilityFloatingMenuTest extends SysuiTestCase {
 
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
     @Mock
     private AccessibilityManager mAccessibilityManager;
 
@@ -58,18 +61,14 @@
 
     @Before
     public void initMenu() {
-        MockitoAnnotations.initMocks(this);
-
-        final List<AccessibilityTarget> mTargets = new ArrayList<>();
-        mTargets.add(mock(AccessibilityTarget.class));
-
         final List<String> assignedTargets = new ArrayList<>();
         mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager);
         assignedTargets.add(MAGNIFICATION_CONTROLLER_NAME);
         doReturn(assignedTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets(
                 anyInt());
 
-        mMenuView = new AccessibilityFloatingMenuView(mContext);
+        final Position position = new Position(0, 0);
+        mMenuView = new AccessibilityFloatingMenuView(mContext, position);
         mMenu = new AccessibilityFloatingMenu(mContext, mMenuView);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
index 448211e..09e6940 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
@@ -39,7 +39,6 @@
 import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.LayerDrawable;
 import android.testing.AndroidTestingRunner;
@@ -63,13 +62,16 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InOrder;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /** Tests for {@link AccessibilityFloatingMenuView}. */
@@ -77,22 +79,26 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
-    private AccessibilityFloatingMenuView mMenuView;
+
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
+    private final List<AccessibilityTarget> mTargets = new ArrayList<>(
+            Collections.singletonList(mock(AccessibilityTarget.class)));
+
+    private final Rect mAvailableBounds = new Rect(100, 200, 300, 400);
+    private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
 
     @Mock
     private WindowManager mWindowManager;
-
     @Mock
     private ViewPropertyAnimator mAnimator;
-
     @Mock
     private WindowMetrics mWindowMetrics;
-
     private MotionEvent mInterceptMotionEvent;
-
-    private RecyclerView mListView;
-
-    private Rect mAvailableBounds = new Rect(100, 200, 300, 400);
+    private AccessibilityFloatingMenuView mMenuView;
+    private RecyclerView mListView = new RecyclerView(mContext);
 
     private int mScreenHeight;
     private int mMenuWindowHeight;
@@ -101,23 +107,21 @@
     private int mScreenHalfWidth;
     private int mScreenHalfHeight;
     private int mMaxWindowX;
-
-    private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
-    private final List<AccessibilityTarget> mTargets = new ArrayList<>();
+    private int mMaxWindowY;
 
     @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
+    public void initMenuView() {
         final WindowManager wm = mContext.getSystemService(WindowManager.class);
         doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
 
-        mTargets.add(mock(AccessibilityTarget.class));
-        mListView = new RecyclerView(mContext);
-        mMenuView = new AccessibilityFloatingMenuView(mContext, mListView);
+        mMenuView = spy(
+                new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
+    }
 
+    @Before
+    public void setUpMatrices() {
         final Resources res = mContext.getResources();
         final int margin =
                 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
@@ -135,6 +139,7 @@
         mScreenHalfHeight = mScreenHeight / 2;
         mMaxWindowX = screenWidth - margin - menuWidth;
         mMenuWindowHeight = menuHeight + margin * 2;
+        mMaxWindowY = mScreenHeight - mMenuWindowHeight;
     }
 
     @Test
@@ -180,42 +185,46 @@
     }
 
     @Test
-    public void updateListViewRadius_singleTarget_matchResult() {
-        final float radius =
-                getContext().getResources().getDimensionPixelSize(
-                        R.dimen.accessibility_floating_menu_small_single_radius);
-        final float[] expectedRadii =
-                new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
+    public void onTargetsChanged_singleTarget_expectedRadii() {
+        final Position alignRightPosition = new Position(1.0f, 0.0f);
+        final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
+                alignRightPosition);
+        setupBasicMenuView(menuView);
 
-        mMenuView.onTargetsChanged(mTargets);
-        final View view = mMenuView.getChildAt(0);
+        menuView.onTargetsChanged(mTargets);
+
+        final View view = menuView.getChildAt(0);
         final LayerDrawable layerDrawable = (LayerDrawable) view.getBackground();
         final GradientDrawable gradientDrawable =
                 (GradientDrawable) layerDrawable.getDrawable(0);
-        final float[] actualRadii = gradientDrawable.getCornerRadii();
-
-        assertThat(actualRadii).isEqualTo(expectedRadii);
+        final float smallRadius =
+                getContext().getResources().getDimensionPixelSize(
+                        R.dimen.accessibility_floating_menu_small_single_radius);
+        final float[] expectedRadii =
+                new float[]{smallRadius, smallRadius, 0.0f, 0.0f, 0.0f, 0.0f, smallRadius,
+                        smallRadius};
+        assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
     }
 
     @Test
-    public void setSizeType_largeSize_matchResult() {
-        final int shapeType = 2;
-        final float radius = getContext().getResources().getDimensionPixelSize(
-                R.dimen.accessibility_floating_menu_large_single_radius);
-        final float[] expectedRadii =
-                new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
-        final Drawable listViewBackground =
-                mContext.getDrawable(R.drawable.accessibility_floating_menu_background);
-        mListView = spy(new RecyclerView(mContext));
-        mListView.setBackground(listViewBackground);
+    public void setSizeType_alignRightAndLargeSize_expectedRadii() {
+        final RecyclerView listView = spy(new RecyclerView(mContext));
+        final Position alignRightPosition = new Position(1.0f, 0.0f);
+        final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
+                alignRightPosition, listView);
+        setupBasicMenuView(menuView);
 
-        mMenuView = new AccessibilityFloatingMenuView(mContext, mListView);
-        mMenuView.setSizeType(shapeType);
+        menuView.setSizeType(/* largeSize */ 1);
+
         final LayerDrawable layerDrawable =
-                (LayerDrawable) mListView.getBackground();
+                (LayerDrawable) listView.getBackground();
         final GradientDrawable gradientDrawable =
                 (GradientDrawable) layerDrawable.getDrawable(0);
-
+        final float largeRadius = getContext().getResources().getDimensionPixelSize(
+                R.dimen.accessibility_floating_menu_large_single_radius);
+        final float[] expectedRadii =
+                new float[] {largeRadius, largeRadius, 0.0f, 0.0f, 0.0f, 0.0f, largeRadius,
+                        largeRadius};
         assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
     }
 
@@ -223,49 +232,43 @@
     public void setShapeType_halfCircle_translationX() {
         final RecyclerView listView = spy(new RecyclerView(mContext));
         final AccessibilityFloatingMenuView menuView =
-                new AccessibilityFloatingMenuView(mContext, listView);
-        final int shapeType = 2;
+                new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, listView);
+        setupBasicMenuView(menuView);
         doReturn(mAnimator).when(listView).animate();
 
-        menuView.setShapeType(shapeType);
+        menuView.setShapeType(/* halfOvalShape */ 1);
 
         verify(mAnimator).translationX(anyFloat());
     }
 
     @Test
     public void onTargetsChanged_fadeInOut() {
-        final AccessibilityFloatingMenuView menuView = spy(mMenuView);
-        final InOrder inOrderMenuView = inOrder(menuView);
+        final InOrder inOrderMenuView = inOrder(mMenuView);
 
-        menuView.onTargetsChanged(mTargets);
+        mMenuView.onTargetsChanged(mTargets);
 
-        inOrderMenuView.verify(menuView).fadeIn();
-        inOrderMenuView.verify(menuView).fadeOut();
+        inOrderMenuView.verify(mMenuView).fadeIn();
+        inOrderMenuView.verify(mMenuView).fadeOut();
     }
 
     @Test
     public void setSizeType_fadeInOut() {
-        final AccessibilityFloatingMenuView menuView = spy(mMenuView);
-        final InOrder inOrderMenuView = inOrder(menuView);
-        final int smallSize = 0;
-        menuView.setSizeType(smallSize);
+        final InOrder inOrderMenuView = inOrder(mMenuView);
 
-        inOrderMenuView.verify(menuView).fadeIn();
-        inOrderMenuView.verify(menuView).fadeOut();
+        mMenuView.setSizeType(/* smallSize */ 0);
+
+        inOrderMenuView.verify(mMenuView).fadeIn();
+        inOrderMenuView.verify(mMenuView).fadeOut();
     }
 
     @Test
     public void tapOnAndDragMenu_interceptUpEvent() {
         final RecyclerView listView = new RecyclerView(mContext);
         final TestAccessibilityFloatingMenu menuView =
-                new TestAccessibilityFloatingMenu(mContext, listView);
-
-        menuView.show();
-        menuView.onTargetsChanged(mTargets);
-        menuView.setSizeType(0);
-        menuView.setShapeType(0);
-        final int currentWindowX = mMenuView.mCurrentLayoutParams.x;
-        final int currentWindowY = mMenuView.mCurrentLayoutParams.y;
+                new TestAccessibilityFloatingMenu(mContext, mPlaceholderPosition, listView);
+        setupBasicMenuView(menuView);
+        final int currentWindowX = menuView.mCurrentLayoutParams.x;
+        final int currentWindowY = menuView.mCurrentLayoutParams.y;
         final MotionEvent downEvent =
                 mMotionEventHelper.obtainMotionEvent(0, 1,
                         MotionEvent.ACTION_DOWN,
@@ -283,6 +286,7 @@
                         /* screenCenterX */ mScreenHalfWidth
                                 - /* offsetXToScreenLeftHalfRegion */ 10,
                         /* screenCenterY */ mScreenHalfHeight);
+
         listView.dispatchTouchEvent(downEvent);
         listView.dispatchTouchEvent(moveEvent);
         listView.dispatchTouchEvent(upEvent);
@@ -292,12 +296,15 @@
 
     @Test
     public void tapOnAndDragMenu_matchLocation() {
-        mMenuView.show();
-        mMenuView.onTargetsChanged(mTargets);
-        mMenuView.setSizeType(0);
-        mMenuView.setShapeType(0);
-        final int currentWindowX = mMenuView.mCurrentLayoutParams.x;
-        final int currentWindowY = mMenuView.mCurrentLayoutParams.y;
+        final float expectedX = 1.0f;
+        final float expectedY = 0.7f;
+        final Position position = new Position(expectedX, expectedY);
+        final RecyclerView listView = new RecyclerView(mContext);
+        final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
+                position, listView);
+        setupBasicMenuView(menuView);
+        final int currentWindowX = menuView.mCurrentLayoutParams.x;
+        final int currentWindowY = menuView.mCurrentLayoutParams.y;
         final MotionEvent downEvent =
                 mMotionEventHelper.obtainMotionEvent(0, 1,
                         MotionEvent.ACTION_DOWN,
@@ -315,25 +322,28 @@
                         /* screenCenterX */ mScreenHalfWidth
                                 + /* offsetXToScreenRightHalfRegion */ 10,
                         /* screenCenterY */ mScreenHalfHeight);
-        mListView.dispatchTouchEvent(downEvent);
-        mListView.dispatchTouchEvent(moveEvent);
-        mListView.dispatchTouchEvent(upEvent);
-        mMenuView.mDragAnimator.end();
 
-        assertThat(mMenuView.mCurrentLayoutParams.x).isEqualTo(mMaxWindowX);
-        assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(
+        listView.dispatchTouchEvent(downEvent);
+        listView.dispatchTouchEvent(moveEvent);
+        listView.dispatchTouchEvent(upEvent);
+        menuView.mDragAnimator.end();
+
+        assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
+        assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
                 /* newWindowY = screenCenterY - offsetY */ mScreenHalfHeight - mMenuHalfHeight);
     }
 
 
     @Test
     public void tapOnAndDragMenuToScreenSide_transformShapeHalfOval() {
-        mMenuView.show();
-        mMenuView.onTargetsChanged(mTargets);
-        mMenuView.setSizeType(0);
-        mMenuView.setShapeType(/* oval */ 0);
-        final int currentWindowX = mMenuView.mCurrentLayoutParams.x;
-        final int currentWindowY = mMenuView.mCurrentLayoutParams.y;
+        final Position alignRightPosition = new Position(1.0f, 0.8f);
+        final RecyclerView listView = new RecyclerView(mContext);
+        final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
+                alignRightPosition, listView);
+        setupBasicMenuView(menuView);
+
+        final int currentWindowX = menuView.mCurrentLayoutParams.x;
+        final int currentWindowY = menuView.mCurrentLayoutParams.y;
         final MotionEvent downEvent =
                 mMotionEventHelper.obtainMotionEvent(0, 1,
                         MotionEvent.ACTION_DOWN,
@@ -351,125 +361,110 @@
                         /* downX */(currentWindowX + mMenuHalfWidth)
                                 + /* offsetXToScreenRightSide */ mMenuHalfWidth,
                         /* downY */ (currentWindowY +  mMenuHalfHeight));
-        mListView.dispatchTouchEvent(downEvent);
-        mListView.dispatchTouchEvent(moveEvent);
-        mListView.dispatchTouchEvent(upEvent);
 
-        assertThat(mMenuView.mShapeType).isEqualTo(/* halfOval */ 1);
+        listView.dispatchTouchEvent(downEvent);
+        listView.dispatchTouchEvent(moveEvent);
+        listView.dispatchTouchEvent(upEvent);
+
+        assertThat(menuView.mShapeType).isEqualTo(/* halfOval */ 1);
     }
 
     @Test
     public void getAccessibilityActionList_matchResult() {
-        final AccessibilityNodeInfo infos = new AccessibilityNodeInfo();
-        mMenuView.onInitializeAccessibilityNodeInfo(infos);
+        final AccessibilityNodeInfo info = new AccessibilityNodeInfo();
 
-        assertThat(infos.getActionList().size()).isEqualTo(5);
+        mMenuView.onInitializeAccessibilityNodeInfo(info);
+
+        assertThat(info.getActionList().size()).isEqualTo(5);
     }
 
     @Test
     public void accessibilityActionMove_halfOval_moveTopLeft_success() {
-        final AccessibilityFloatingMenuView menuView =
-                spy(new AccessibilityFloatingMenuView(mContext));
-        doReturn(mAvailableBounds).when(menuView).getAvailableBounds();
-        menuView.setShapeType(/* halfOvalShape */ 1);
+        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
+        mMenuView.setShapeType(/* halfOvalShape */ 1);
 
-        final boolean isActionPerformed =
-                menuView.performAccessibilityAction(R.id.action_move_top_left, null);
+        final boolean moveTopLeftAction =
+                mMenuView.performAccessibilityAction(R.id.action_move_top_left, null);
 
-        assertThat(isActionPerformed).isTrue();
-        assertThat(menuView.mShapeType).isEqualTo(/* ovalShape */ 0);
-        verify(menuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.top);
+        assertThat(moveTopLeftAction).isTrue();
+        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
+        verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.top);
     }
 
     @Test
     public void accessibilityActionMove_halfOval_moveTopRight_success() {
-        final AccessibilityFloatingMenuView menuView =
-                spy(new AccessibilityFloatingMenuView(mContext));
-        doReturn(mAvailableBounds).when(menuView).getAvailableBounds();
-        menuView.setShapeType(/* halfOvalShape */ 1);
+        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
+        mMenuView.setShapeType(/* halfOvalShape */ 1);
 
-        final boolean isActionPerformed =
-                menuView.performAccessibilityAction(R.id.action_move_top_right, null);
+        final boolean moveTopRightAction =
+                mMenuView.performAccessibilityAction(R.id.action_move_top_right, null);
 
-        assertThat(isActionPerformed).isTrue();
-        assertThat(menuView.mShapeType).isEqualTo(/* ovalShape */ 0);
-        verify(menuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.top);
+        assertThat(moveTopRightAction).isTrue();
+        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
+        verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.top);
     }
 
     @Test
     public void accessibilityActionMove_halfOval_moveBottomLeft_success() {
-        final AccessibilityFloatingMenuView menuView =
-                spy(new AccessibilityFloatingMenuView(mContext));
-        doReturn(mAvailableBounds).when(menuView).getAvailableBounds();
-        menuView.setShapeType(/* halfOvalShape */ 1);
+        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
+        mMenuView.setShapeType(/* halfOvalShape */ 1);
 
-        final boolean isActionPerformed =
-                menuView.performAccessibilityAction(R.id.action_move_bottom_left, null);
+        final boolean moveBottomLeftAction =
+                mMenuView.performAccessibilityAction(R.id.action_move_bottom_left, null);
 
-        assertThat(isActionPerformed).isTrue();
-        assertThat(menuView.mShapeType).isEqualTo(/* ovalShape */ 0);
-        verify(menuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.bottom);
+        assertThat(moveBottomLeftAction).isTrue();
+        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
+        verify(mMenuView).snapToLocation(mAvailableBounds.left, mAvailableBounds.bottom);
     }
 
     @Test
     public void accessibilityActionMove_halfOval_moveBottomRight_success() {
-        final AccessibilityFloatingMenuView menuView =
-                spy(new AccessibilityFloatingMenuView(mContext));
-        doReturn(mAvailableBounds).when(menuView).getAvailableBounds();
-        menuView.setShapeType(/* halfOvalShape */ 1);
+        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
+        mMenuView.setShapeType(/* halfOvalShape */ 1);
 
-        final boolean isActionPerformed =
-                menuView.performAccessibilityAction(R.id.action_move_bottom_right, null);
+        final boolean moveBottomRightAction =
+                mMenuView.performAccessibilityAction(R.id.action_move_bottom_right, null);
 
-        assertThat(isActionPerformed).isTrue();
-        assertThat(menuView.mShapeType).isEqualTo(/* ovalShape */ 0);
-        verify(menuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.bottom);
+        assertThat(moveBottomRightAction).isTrue();
+        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
+        verify(mMenuView).snapToLocation(mAvailableBounds.right, mAvailableBounds.bottom);
     }
 
     @Test
     public void accessibilityActionMove_halfOval_moveOutEdgeAndShow_success() {
-        final AccessibilityFloatingMenuView menuView =
-                spy(new AccessibilityFloatingMenuView(mContext));
-        doReturn(mAvailableBounds).when(menuView).getAvailableBounds();
-        menuView.setShapeType(/* halfOvalShape */ 1);
+        doReturn(mAvailableBounds).when(mMenuView).getAvailableBounds();
+        mMenuView.setShapeType(/* halfOvalShape */ 1);
 
-        final boolean isActionPerformed =
-                menuView.performAccessibilityAction(R.id.action_move_out_edge_and_show, null);
+        final boolean moveOutEdgeAndShowAction =
+                mMenuView.performAccessibilityAction(R.id.action_move_out_edge_and_show, null);
 
-        assertThat(isActionPerformed).isTrue();
-        assertThat(menuView.mShapeType).isEqualTo(/* ovalShape */ 0);
+        assertThat(moveOutEdgeAndShowAction).isTrue();
+        assertThat(mMenuView.mShapeType).isEqualTo(/* ovalShape */ 0);
     }
 
     @Test
     public void setupAccessibilityActions_oval_hasActionMoveToEdgeAndHide() {
-        final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext);
-        menuView.setShapeType(/* ovalShape */ 0);
+        final AccessibilityNodeInfo info = new AccessibilityNodeInfo();
+        mMenuView.setShapeType(/* ovalShape */ 0);
 
-        final AccessibilityNodeInfo infos = new AccessibilityNodeInfo();
-        menuView.onInitializeAccessibilityNodeInfo(infos);
+        mMenuView.onInitializeAccessibilityNodeInfo(info);
 
-        assertThat(infos.getActionList().stream().anyMatch(
+        assertThat(info.getActionList().stream().anyMatch(
                 action -> action.getId() == R.id.action_move_to_edge_and_hide)).isTrue();
     }
 
     @Test
     public void onTargetsChanged_exceedAvailableHeight_overScrollAlways() {
-        final RecyclerView listView = new RecyclerView(mContext);
-        final AccessibilityFloatingMenuView menuView =
-                spy(new AccessibilityFloatingMenuView(mContext, listView));
-        doReturn(true).when(menuView).hasExceededMaxLayoutHeight();
+        doReturn(true).when(mMenuView).hasExceededMaxLayoutHeight();
 
-        menuView.onTargetsChanged(mTargets);
+        mMenuView.onTargetsChanged(mTargets);
 
-        assertThat(listView.getOverScrollMode()).isEqualTo(OVER_SCROLL_ALWAYS);
+        assertThat(mListView.getOverScrollMode()).isEqualTo(OVER_SCROLL_ALWAYS);
     }
 
     @Test
     public void onTargetsChanged_notExceedAvailableHeight_overScrollNever() {
-        final RecyclerView listView = new RecyclerView(mContext);
-        final AccessibilityFloatingMenuView menuView =
-                spy(new AccessibilityFloatingMenuView(mContext, listView));
-        doReturn(false).when(menuView).hasExceededMaxLayoutHeight();
+        doReturn(false).when(mMenuView).hasExceededMaxLayoutHeight();
 
         mMenuView.onTargetsChanged(mTargets);
 
@@ -480,21 +475,24 @@
     public void showMenuView_insetsListener_overlapWithIme_menuViewShifted() {
         final int offset = 200;
 
-        showMenuWithLatestStatus();
-        final WindowInsets imeInset = fakeImeInsetWith(offset);
+        final Position alignRightPosition = new Position(1.0f, 0.8f);
+        final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
+                alignRightPosition);
+        setupBasicMenuView(menuView);
+        final WindowInsets imeInset = fakeImeInsetWith(menuView, offset);
         when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
         when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
-        final int expectedLayoutY = mMenuView.mCurrentLayoutParams.y - offset;
-        mMenuView.dispatchApplyWindowInsets(imeInset);
+        final int expectedLayoutY = menuView.mCurrentLayoutParams.y - offset;
+        menuView.dispatchApplyWindowInsets(imeInset);
 
-        assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY);
+        assertThat(menuView.mCurrentLayoutParams.y).isEqualTo(expectedLayoutY);
     }
 
     @Test
     public void hideIme_onMenuViewShifted_menuViewMovedBack() {
         final int offset = 200;
-        showMenuWithLatestStatus();
-        final WindowInsets imeInset = fakeImeInsetWith(offset);
+        setupBasicMenuView(mMenuView);
+        final WindowInsets imeInset = fakeImeInsetWith(mMenuView, offset);
         when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
         when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
         final int expectedLayoutY = mMenuView.mCurrentLayoutParams.y;
@@ -510,8 +508,8 @@
     public void showMenuAndIme_withHigherIme_alignScreenTopEdge() {
         final int offset = 99999;
 
-        showMenuWithLatestStatus();
-        final WindowInsets imeInset = fakeImeInsetWith(offset);
+        setupBasicMenuView(mMenuView);
+        final WindowInsets imeInset = fakeImeInsetWith(mMenuView, offset);
         when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
         when(mWindowMetrics.getWindowInsets()).thenReturn(imeInset);
         mMenuView.dispatchApplyWindowInsets(imeInset);
@@ -519,31 +517,47 @@
         assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(0);
     }
 
+    @Test
+    public void testConstructor_withPosition_expectedPosition() {
+        final float expectedX = 1.0f;
+        final float expectedY = 0.7f;
+        final Position position = new Position(expectedX, expectedY);
+
+        final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
+                position);
+        setupBasicMenuView(menuView);
+
+        assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
+        assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
+                expectedY * mMaxWindowY);
+    }
+
     @After
     public void tearDown() {
         mInterceptMotionEvent = null;
         mMotionEventHelper.recycleEvents();
+        mListView = null;
     }
 
-    private void showMenuWithLatestStatus() {
-        mMenuView.show();
-        mMenuView.onTargetsChanged(mTargets);
-        mMenuView.setSizeType(0);
-        mMenuView.setShapeType(0);
+    private void setupBasicMenuView(AccessibilityFloatingMenuView menuView) {
+        menuView.show();
+        menuView.onTargetsChanged(mTargets);
+        menuView.setSizeType(0);
+        menuView.setShapeType(0);
     }
 
     /**
      * Based on the current menu status, fake the ime inset component {@link WindowInsets} used
      * for testing.
      *
-     * @param offset is used for the y-axis position of ime higher than the y-axis position of menu.
+     * @param menuView {@link AccessibilityFloatingMenuView} that needs to be changed
+     * @param offset is used for the y-axis position of ime higher than the y-axis position of menu
      * @return the ime inset
      */
-    private WindowInsets fakeImeInsetWith(int offset) {
+    private WindowInsets fakeImeInsetWith(AccessibilityFloatingMenuView menuView, int offset) {
         // Ensure the keyboard has overlapped on the menu view.
         final int fakeImeHeight =
-                mScreenHeight - (mMenuView.mCurrentLayoutParams.y + mMenuWindowHeight) + offset;
-
+                mScreenHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight) + offset;
         return new WindowInsets.Builder()
                 .setVisible(ime() | navigationBars(), true)
                 .setInsets(ime() | navigationBars(), Insets.of(0, 0, 0, fakeImeHeight))
@@ -551,8 +565,8 @@
     }
 
     private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView {
-        TestAccessibilityFloatingMenu(Context context, RecyclerView listView) {
-            super(context, listView);
+        TestAccessibilityFloatingMenu(Context context, Position position, RecyclerView listView) {
+            super(context, position, listView);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
index 6db5761..eb1f15b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
@@ -55,6 +55,7 @@
     private AccessibilityFloatingMenuView mMenuView;
     private BaseTooltipView mToolTipView;
 
+    private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
     private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
 
     @Before
@@ -66,7 +67,7 @@
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
 
-        mMenuView = new AccessibilityFloatingMenuView(mContext);
+        mMenuView = new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition);
         mToolTipView = new BaseTooltipView(mContext, mMenuView);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
index 41b948f..ca4e3e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
@@ -51,6 +51,7 @@
 
     private AccessibilityFloatingMenuView mMenuView;
     private DockTooltipView mDockTooltipView;
+    private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
     private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
 
     @Before
@@ -62,7 +63,7 @@
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
 
-        mMenuView = spy(new AccessibilityFloatingMenuView(mContext));
+        mMenuView = spy(new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition));
         mDockTooltipView = new DockTooltipView(mContext, mMenuView);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipViewTest.java
index c5bd2fe..2fb0a90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MigrationTooltipViewTest.java
@@ -40,10 +40,12 @@
 public class MigrationTooltipViewTest extends SysuiTestCase {
 
     private TextView mTextView;
+    private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
 
     @Before
     public void setUp() {
-        final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext);
+        final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
+                mPlaceholderPosition);
         final MigrationTooltipView toolTipView = new MigrationTooltipView(mContext, menuView);
         mTextView = toolTipView.findViewById(R.id.text);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/PositionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/PositionTest.java
new file mode 100644
index 0000000..05f306b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/PositionTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 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.systemui.accessibility.floatingmenu;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link Position}. */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class PositionTest extends SysuiTestCase {
+
+    @Test
+    public void fromString_correctFormat_expectedValues() {
+        final float expectedX = 0.0f;
+        final float expectedY = 0.7f;
+        final String correctStringFormat = expectedX + ", " + expectedY;
+
+        final Position position = Position.fromString(correctStringFormat);
+
+        assertThat(position.getPercentageX()).isEqualTo(expectedX);
+        assertThat(position.getPercentageY()).isEqualTo(expectedY);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void fromString_incorrectFormat_throwsException() {
+        final String incorrectStringFormat = "0.0: 1.0";
+
+        // expect to throw IllegalArgumentException for the incorrect separator ":"
+        Position.fromString(incorrectStringFormat);
+    }
+
+    @Test
+    public void constructor() {
+        final float expectedX = 0.5f;
+        final float expectedY = 0.9f;
+
+        final Position position = new Position(expectedX, expectedY);
+
+        assertThat(position.getPercentageX()).isEqualTo(expectedX);
+        assertThat(position.getPercentageY()).isEqualTo(expectedY);
+    }
+}