Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 39ed2478 authored by Vinit Nayak's avatar Vinit Nayak
Browse files

Update A11y strings for flex split

* Refactor to put A11y and targets we need for a given snap mode
into a new class, SplitTargetProvider
* Ideally this could be sent to DragAndDrop and other external to split
parties who need to know our split statae.
* Over time we could migrate DividerView's use of SpitLayout to another
interface.
* One issue that this does duplicate tracking of mIsLeftRightSplit since
I wanted the class to be isolated. It uses same logic as SplitLayout, but
doesn't keep it as a variable since I didn't want to add more dependencies
on config changes and what not.
* Fix logic bug in DividerSnapAlgorithm highlighted by IDE (yay!)

Test: Updated existing tests, manual
Flag: com.android.wm.shell.enable_flexible_two_app_split
Fixes: 415827083

Change-Id: I0f1a722244ae3bb8f4a2eaf5498e7a4b8e401520
parent 5bb2d23a
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -21,9 +21,11 @@

    <!-- Accessibility actions for the docked stack divider -->
    <item type="id" name="action_move_tl_full" />
    <item type="id" name="action_move_tl_90" />
    <item type="id" name="action_move_tl_70" />
    <item type="id" name="action_move_tl_50" />
    <item type="id" name="action_move_tl_30" />
    <item type="id" name="action_move_tl_10" />
    <item type="id" name="action_move_rb_full" />
    <item type="id" name="action_swap_apps" />

+16 −12
Original line number Diff line number Diff line
@@ -83,29 +83,33 @@
    <string name="divider_title">Split screen divider</string>

    <!-- Accessibility action for moving docked stack divider to make the left screen full screen [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_left_full">Left full screen</string>
    <string name="accessibility_action_divider_left_full">Make left app full screen</string>
    <!-- Accessibility action for moving docked stack divider to make the left screen 70% [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_left_70">Left 70%</string>
    <string formatted="false" name="accessibility_action_divider_left_10">10% left, 90% right</string>
    <string formatted="false" name="accessibility_action_divider_left_30">30% left, 70% right</string>
    <!-- Accessibility action for moving docked stack divider to make the left screen 50% [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_left_50">Left 50%</string>
    <string formatted="false" name="accessibility_action_divider_left_50">50% left, 50% right</string>
    <!-- Accessibility action for moving docked stack divider to make the left screen 30% [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_left_30">Left 30%</string>
    <string formatted="false" name="accessibility_action_divider_left_70">70% left, 30% right</string>
    <string formatted="false" name="accessibility_action_divider_left_90">90% left, 10% right</string>
    <!-- Accessibility action for moving docked stack divider to make the right screen full screen [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_right_full">Right full screen</string>
    <string name="accessibility_action_divider_right_full">Make right app full screen</string>
    <!-- Accessibility action for swapping the apps around the divider (double tap action) [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_swap_vertical">Swap top app with bottom</string>
    <string name="accessibility_action_divider_swap_horizontal">Swap left app with right</string>
    <string name="accessibility_action_divider_swap_vertical">Swap top app with bottom app, keep current split</string>
    <string name="accessibility_action_divider_swap_horizontal">Swap left app with right app, keep current split</string>

    <!-- Accessibility action for moving docked stack divider to make the top screen full screen [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_top_full">Top full screen</string>
    <string name="accessibility_action_divider_top_full">Make top app full screen</string>
    <!-- Accessibility action for moving docked stack divider to make the top screen 70% [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_top_70">Top 70%</string>
    <string formatted="false" name="accessibility_action_divider_top_10">10% top, 90% bottom</string>
    <string formatted="false" name="accessibility_action_divider_top_30">30% top, 70% bottom</string>
    <!-- Accessibility action for moving docked stack divider to make the top screen 50% [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_top_50">Top 50%</string>
    <string formatted="false" name="accessibility_action_divider_top_50">50% top, 50% bottom</string>
    <!-- Accessibility action for moving docked stack divider to make the top screen 30% [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_top_30">Top 30%</string>
    <string formatted="false" name="accessibility_action_divider_top_70">70% top, 30% bottom</string>
    <string formatted="false" name="accessibility_action_divider_top_90">90% top, 10% bottom</string>
    <!-- Accessibility action for moving docked stack divider to make the bottom screen full screen [CHAR LIMIT=NONE] -->
    <string name="accessibility_action_divider_bottom_full">Bottom full screen</string>
    <string name="accessibility_action_divider_bottom_full">Make bottom app full screen</string>

    <!-- Accessibility label for splitting to the left drop zone [CHAR LIMIT=NONE] -->
    <string name="accessibility_split_left">Split left</string>
+23 −23
Original line number Diff line number Diff line
@@ -100,6 +100,7 @@ public class DividerSnapAlgorithm {
    private final boolean mIsLeftRightSplit;
    /** In SNAP_MODE_MINIMIZED, the side of the screen on which an app will "dock" when minimized */
    private final int mDockSide;
    private final SplitTargetProvider mSplitTargetProvider;

    /**
     * The first, and usually only, snap target between the left/top screen edge and center.
@@ -126,14 +127,16 @@ public class DividerSnapAlgorithm {
    private final MotionSpec mMotionSpec;

    public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
            boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide) {
            boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide,
            SplitTargetProvider splitTargetProvider) {
        this(res, displayWidth, displayHeight, dividerSize, isLeftRightSplit, insets,
                pinnedTaskbarInsets, dockSide, false /* minimized */, true /* resizable */);
                pinnedTaskbarInsets, dockSide, true /* resizable */,
                splitTargetProvider);
    }

    public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize,
            boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide,
            boolean isMinimizedMode, boolean isHomeResizable) {
            boolean isHomeResizable, SplitTargetProvider splitTargetProvider) {
        mMinFlingVelocityPxPerSecond =
                MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density;
        mMinDismissVelocityPxPerSecond =
@@ -143,17 +146,10 @@ public class DividerSnapAlgorithm {
        mDisplayHeight = displayHeight;
        mIsLeftRightSplit = isLeftRightSplit;
        mDockSide = dockSide;
        mSplitTargetProvider = splitTargetProvider;
        mInsets.set(insets);
        mPinnedTaskbarInsets.set(pinnedTaskbarInsets);
        if (Flags.enableFlexibleTwoAppSplit()) {
            mSnapMode = SNAP_FLEXIBLE_HYBRID;
        } else {
            // Set SNAP_MODE_MINIMIZED, SNAP_MODE_16_9, or SNAP_FIXED_RATIO depending on config
            mSnapMode = isMinimizedMode
                    ? SNAP_MODE_MINIMIZED
                    : res.getInteger(
                            com.android.internal.R.integer.config_dockedStackDividerSnapMode);
        }
        mSnapMode = mSplitTargetProvider.getSnapMode();
        mFreeSnapMode = res.getBoolean(
                com.android.internal.R.bool.config_dockedStackDividerFreeSnapMode);
        mFixedRatio = res.getFraction(
@@ -358,8 +354,10 @@ public class DividerSnapAlgorithm {
     */
    private void addNonDismissingTargets(List<Integer> positions, int dividerMax) {
        // Get the desired layout for our snap mode.
        List<Integer> targetSpec =
                SplitSpec.getSnapTargetLayout(mSnapMode, areOffscreenRatiosSupported());
        List<Integer> targetSpec = mSplitTargetProvider
                .getTargets(false /*includeDismissal*/)
                .stream().map(SplitTargetProvider.SplitTarget::getSnapPosition)
                .toList();

        if (positions.size() != targetSpec.size()) {
            throw new IllegalStateException("unexpected number of snap positions");
@@ -372,12 +370,18 @@ public class DividerSnapAlgorithm {
            int position = positions.get(i);

            if (!midpointPassed) {
                maybeAddTarget(position, position - getStartInset(), target);
            } else if (target == SNAP_TO_2_50_50) {
                if (target == SNAP_TO_2_50_50) {
                    // midpoint
                    mTargets.add(new SnapTarget(position, target));
                    midpointPassed = true;
                } else {
                maybeAddTarget(position, dividerMax - getEndInset() - (position + mDividerSize),
                    // before midpoint
                    maybeAddTarget(position, position - getStartInset(), target);
                }
            } else {
                // after midpoint
                maybeAddTarget(position,
                        dividerMax - getEndInset() - (position + mDividerSize),
                        target);
            }
        }
@@ -552,10 +556,6 @@ public class DividerSnapAlgorithm {
        return mMotionSpec;
    }

    public int getSnapMode() {
        return mSnapMode;
    }

    /**
     * An object, calculated at boot time, representing a legal position for the split screen
     * divider (i.e. the divider can be dragged to this spot).
+20 −52
Original line number Diff line number Diff line
@@ -68,6 +68,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopState;

import com.google.android.msdl.data.model.MSDLToken;

import java.util.List;
import java.util.Objects;

/**
@@ -118,6 +119,8 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
     */
    private final Rect mDividerBounds = new Rect();
    private final Rect mTempRect = new Rect();
    private SplitTargetProvider mSplitTargetProvider;

    private FrameLayout mDividerBar;

    static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY =
@@ -149,48 +152,20 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
    };

    final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() {
        private List<SplitTargetProvider.SplitTarget> mTargets;

        @Override
        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
            super.onInitializeAccessibilityNodeInfo(host, info);
            final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm;

            mTargets = mSplitTargetProvider.getTargets(true /*includeDismissal*/);
            mTargets.forEach(target ->
                    info.addAction(new AccessibilityAction(target.getA11yActionId(),
                            mContext.getString(target.getA11yActionString()))));
            if (mSplitLayout.isLeftRightSplit()) {
                info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
                        mContext.getString(R.string.accessibility_action_divider_left_full)));
                if (snapAlgorithm.isFirstSplitTargetAvailable()) {
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
                            mContext.getString(R.string.accessibility_action_divider_left_70)));
                }
                if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
                    // Only show the middle target if there are more than 1 split target
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
                            mContext.getString(R.string.accessibility_action_divider_left_50)));
                }
                if (snapAlgorithm.isLastSplitTargetAvailable()) {
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
                            mContext.getString(R.string.accessibility_action_divider_left_30)));
                }
                info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
                        mContext.getString(R.string.accessibility_action_divider_right_full)));
                info.addAction(new AccessibilityAction(R.id.action_swap_apps,
                        mContext.getString(R.string.accessibility_action_divider_swap_horizontal)));
            } else {
                info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
                        mContext.getString(R.string.accessibility_action_divider_top_full)));
                if (snapAlgorithm.isFirstSplitTargetAvailable()) {
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
                            mContext.getString(R.string.accessibility_action_divider_top_70)));
                }
                if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
                    // Only show the middle target if there are more than 1 split target
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
                            mContext.getString(R.string.accessibility_action_divider_top_50)));
                }
                if (snapAlgorithm.isLastSplitTargetAvailable()) {
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
                            mContext.getString(R.string.accessibility_action_divider_top_30)));
                }
                info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
                        mContext.getString(R.string.accessibility_action_divider_bottom_full)));
                info.addAction(new AccessibilityAction(R.id.action_swap_apps,
                        mContext.getString(R.string.accessibility_action_divider_swap_vertical)));
            }
@@ -204,21 +179,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
                return true;
            }

            SnapTarget nextTarget = null;
            DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm;
            if (action == R.id.action_move_tl_full) {
                nextTarget = snapAlgorithm.getDismissEndTarget();
            } else if (action == R.id.action_move_tl_70) {
                nextTarget = snapAlgorithm.getLastSplitTarget();
            } else if (action == R.id.action_move_tl_50) {
                nextTarget = snapAlgorithm.getMiddleTarget();
            } else if (action == R.id.action_move_tl_30) {
                nextTarget = snapAlgorithm.getFirstSplitTarget();
            } else if (action == R.id.action_move_rb_full) {
                nextTarget = snapAlgorithm.getDismissStartTarget();
            }
            if (nextTarget != null) {
                mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), nextTarget);
            SplitTargetProvider.SplitTarget selectedTarget = mTargets.stream()
                    .filter(target -> target.getA11yActionId() == action)
                    .findFirst()
                    .orElse(null);
            if (selectedTarget != null) {
                mSplitLayout.snapToTarget(selectedTarget.getSnapPosition());
                return true;
            }
            return super.performAccessibilityAction(host, action, args);
@@ -246,7 +212,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
    /** Sets up essential dependencies of the divider bar. */
    public void setup(SplitLayout layout, SplitWindowManager splitWindowManager,
            SurfaceControlViewHost viewHost, InsetsState insetsState,
            DesktopState desktopState) {
            DesktopState desktopState, SplitTargetProvider splitTargetProvider) {
        mSplitLayout = layout;
        mSplitWindowManager = splitWindowManager;
        mViewHost = viewHost;
@@ -266,6 +232,9 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
                : desktopState.canEnterDesktopMode()
                        ? R.dimen.desktop_mode_portrait_split_divider_handle_region_height
                        : R.dimen.split_divider_handle_region_height);

        mSplitTargetProvider = splitTargetProvider;
        mHandle.setAccessibilityDelegate(mHandleDelegate);
    }

    void onInsetsChanged(InsetsState insetsState, boolean animate) {
@@ -314,7 +283,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
        mInteractive = true;
        mHideHandle = false;
        setOnTouchListener(this);
        mHandle.setAccessibilityDelegate(mHandleDelegate);
        setWillNotDraw(false);
        mPaint.setColor(getResources().getColor(R.color.split_divider_background, null));
        mPaint.setAntiAlias(true);
+207 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.wm.shell.common.split

import android.content.res.Resources
import com.android.wm.shell.Flags
import com.android.wm.shell.R
import com.android.wm.shell.common.split.SplitTargetProvider.SplitTarget
import com.android.wm.shell.shared.split.SplitScreenConstants

/**
 * For a given snap mode, based on current feature flags and resource config values, generates list
 * of viable snap targets which should be accessed via the [SplitTargetProvider] interface
 */
class SnapToTargetConverter(val res: Resources,
                            isMinimizedMode: Boolean) : SplitTargetProvider {

    private var snapMode: Int = 0

    init {
        snapMode = if (Flags.enableFlexibleTwoAppSplit()) {
            DividerSnapAlgorithm.SNAP_FLEXIBLE_HYBRID
        } else {
            if (isMinimizedMode)
                DividerSnapAlgorithm.SNAP_MODE_MINIMIZED
            else
                res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode)
        }
    }

    /** See [SplitScreenUtils.isLeftRightSplit] */
    private fun isLeftRightSplit() : Boolean {
        val mAllowLeftRightSplitInPortrait = SplitScreenUtils.allowLeftRightSplitInPortrait(res)
        return SplitScreenUtils.isLeftRightSplit(
            mAllowLeftRightSplitInPortrait,
            res.configuration
        )
    }

    /**
     * @param includeDismissal if true then the returned list will include the start and end
     *                         dismiss targets at the start and end of the list, respectively.
     * @return A [List] of [SplitTarget] for the device's current [snapMode], by ordering of left
     *         to right based on the different snap positions
     */
    override fun getTargets(includeDismissal: Boolean): List<SplitTarget> {
        val targets: MutableList<SplitTarget> = ArrayList()
        val areOffscreenRatiosSupported = SplitScreenUtils.allowOffscreenRatios(res)
        val isLeftRightSplit = isLeftRightSplit()
        if (includeDismissal) {
            targets.add(
                SplitTarget(
                    SplitScreenConstants.SNAP_TO_START_AND_DISMISS,
                    if (isLeftRightSplit) R.string.accessibility_action_divider_right_full else
                        R.string.accessibility_action_divider_bottom_full,
                    R.id.action_move_tl_full
                )
            )
        }
        when (snapMode) {
            DividerSnapAlgorithm.SNAP_ONLY_1_1 -> targets.add(
                SplitTarget(
                    SplitScreenConstants.SNAP_TO_2_50_50,
                    if (isLeftRightSplit) R.string.accessibility_action_divider_left_50 else
                        R.string.accessibility_action_divider_top_50,
                    R.id.action_move_tl_50
                )
            )

            DividerSnapAlgorithm.SNAP_MODE_MINIMIZED -> targets.add(
                SplitTarget(
                    SplitScreenConstants.SNAP_TO_MINIMIZE,
                    if (isLeftRightSplit) R.string.accessibility_action_divider_left_full else
                        R.string.accessibility_action_divider_top_full,
                    R.id.action_move_tl_full
                )
            )

            DividerSnapAlgorithm.SNAP_MODE_16_9, DividerSnapAlgorithm.SNAP_FIXED_RATIO ->
                targets.addAll(
                    getOnscreenTargets(isLeftRightSplit)
                )

            DividerSnapAlgorithm.SNAP_FLEXIBLE_SPLIT -> {
                if (areOffscreenRatiosSupported) {
                    targets.add(
                        SplitTarget(
                            SplitScreenConstants.SNAP_TO_2_10_90,
                            if (isLeftRightSplit) R.string.accessibility_action_divider_left_10
                            else R.string.accessibility_action_divider_top_10,
                            R.id.action_move_tl_90
                        )
                    )
                    targets.add(
                        SplitTarget(
                            SplitScreenConstants.SNAP_TO_2_50_50,
                            if (isLeftRightSplit) R.string.accessibility_action_divider_left_50
                            else R.string.accessibility_action_divider_top_50,
                            R.id.action_move_tl_50
                        )
                    )
                    targets.add(
                        SplitTarget(
                            SplitScreenConstants.SNAP_TO_2_90_10,
                            if (isLeftRightSplit) R.string.accessibility_action_divider_left_90
                            else R.string.accessibility_action_divider_top_90,
                            R.id.action_move_tl_10
                        )
                    )
                } else {
                    targets.addAll(getOnscreenTargets(isLeftRightSplit))
                }
            }

            DividerSnapAlgorithm.SNAP_FLEXIBLE_HYBRID -> {
                if (areOffscreenRatiosSupported) {
                    targets.add(
                        SplitTarget(
                            SplitScreenConstants.SNAP_TO_2_10_90,
                            if (isLeftRightSplit) R.string.accessibility_action_divider_left_10
                            else R.string.accessibility_action_divider_top_10,
                            R.id.action_move_tl_90
                        )
                    )
                    targets.addAll(getOnscreenTargets(isLeftRightSplit))
                    targets.add(
                        SplitTarget(
                            SplitScreenConstants.SNAP_TO_2_90_10,
                            if (isLeftRightSplit) R.string.accessibility_action_divider_left_90
                            else R.string.accessibility_action_divider_top_90,
                            R.id.action_move_tl_10
                        )
                    )
                } else {
                    targets.addAll(getOnscreenTargets(isLeftRightSplit))
                }
            }
            else -> throw IllegalStateException("unrecognized snap mode")
        }

        if (includeDismissal) {
            targets.add(
                SplitTarget(
                    SplitScreenConstants.SNAP_TO_END_AND_DISMISS,
                    if (isLeftRightSplit) R.string.accessibility_action_divider_left_full else
                        R.string.accessibility_action_divider_top_full,
                    R.id.action_move_rb_full
                )
            )
        }
        return targets
    }

    /**
     * Current snap mode of the device. You really probably shouldn't be using this. Exposed only
     * for the few use cases that actually need it.
     */
    override fun getSnapMode(): Int {
        return snapMode
    }

    /**
     * @return Default, 3 non-flex split targets. 30/50/70, in that specific order.
     */
    private fun getOnscreenTargets(isLeftRightSplit: Boolean): List<SplitTarget> {
        val targets: MutableList<SplitTarget> = java.util.ArrayList(3)
        targets.add(
            SplitTarget(
                SplitScreenConstants.SNAP_TO_2_33_66,
                if (isLeftRightSplit) R.string.accessibility_action_divider_left_30 else
                    R.string.accessibility_action_divider_top_30,
                R.id.action_move_tl_30
            )
        )
        targets.add(
            SplitTarget(
                SplitScreenConstants.SNAP_TO_2_50_50,
                if (isLeftRightSplit) R.string.accessibility_action_divider_left_50 else
                    R.string.accessibility_action_divider_top_50,
                R.id.action_move_tl_50
            )
        )
        targets.add(
            SplitTarget(
                SplitScreenConstants.SNAP_TO_2_66_33,
                if (isLeftRightSplit) R.string.accessibility_action_divider_left_70 else
                    R.string.accessibility_action_divider_top_70,
                R.id.action_move_tl_70
            )
        )
        return targets
    }
}
 No newline at end of file
Loading