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

Commit 01015deb authored by Bill Lin's avatar Bill Lin
Browse files

Integrate OneHandedTutorialHandler with OneHandedState

Previously if user do not trigger OHM, target view was attached
to window and setVisbility INVISIBLE.
Refactor to improve memory & perforamcne for systemui, and align
the state changes of one handed controller

1) Bind OneHandedController OneHandedState to handle target view
   attach and remove from window.
2) Do NOT create view and attach to window until STATE_ENTERING,
   remove legacy redundant visibility change flow.
3) Detach view from window when STATE_NONE
4) Refine OneHandedState static field definition
5) Consolidate OneHandedTutorialHandlerTest

Test: atest WMShellUnitTests
Test: atest OneHandedTutorialHandlerTest
Test: adb shell dumpsys activity service com.android.systemui
Test: adb shell settings put secure one_handed_tutorial_show_count 0
Test: manual trigger OHM
Test: Trigger OHM and invoke onConfigurationChanged()
Bug: 185558765
Change-Id: I3e9666e2379bceb23e3f80f0d8617a7884100a72
parent e8b62e4d
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.wm.shell.onehanded;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
import android.view.SurfaceControl;
@@ -203,8 +204,10 @@ public class OneHandedAnimationController {
        }

        OneHandedTransitionAnimator addOneHandedAnimationCallback(
                OneHandedAnimationCallback callback) {
                @Nullable OneHandedAnimationCallback callback) {
            if (callback != null) {
                mOneHandedAnimationCallbacks.add(callback);
            }
            return this;
        }

+9 −7
Original line number Diff line number Diff line
@@ -217,7 +217,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
        OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor);
        OneHandedState transitionState = new OneHandedState();
        OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context,
                displayLayout, windowManager, mainExecutor);
                displayLayout, windowManager, settingsUtil, mainExecutor);
        OneHandedAnimationController animationController =
                new OneHandedAnimationController(context);
        OneHandedTouchHandler touchHandler = new OneHandedTouchHandler(timeoutHandler,
@@ -299,6 +299,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
        mAccessibilityManager = AccessibilityManager.getInstance(context);
        mAccessibilityManager.addAccessibilityStateChangeListener(
                mAccessibilityStateChangeListener);

        mState.addSListeners(mTutorialHandler);
    }

    public OneHanded asOneHanded() {
@@ -627,13 +629,13 @@ public class OneHandedController implements RemoteCallable<OneHandedController>
    }

    private void onConfigChanged(Configuration newConfig) {
        if (mTutorialHandler != null) {
            if (!mIsOneHandedEnabled
                    || newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        if (mTutorialHandler == null) {
            return;
        }
            mTutorialHandler.onConfigurationChanged(newConfig);
        if (!mIsOneHandedEnabled || newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            return;
        }
        mTutorialHandler.onConfigurationChanged();
    }

    private void onUserSwitch(int newUserId) {
+33 −10
Original line number Diff line number Diff line
@@ -62,7 +62,7 @@ public final class OneHandedSettingsUtil {
    public static final int ONE_HANDED_TIMEOUT_LONG_IN_SECONDS = 12;

    /**
     * Register one handed preference settings observer
     * Registers one handed preference settings observer
     *
     * @param key       Setting key to monitor in observer
     * @param resolver  ContentResolver of context
@@ -82,7 +82,7 @@ public final class OneHandedSettingsUtil {
    }

    /**
     * Unregister one handed preference settings observer
     * Unregisters one handed preference settings observer.
     *
     * @param resolver  ContentResolver of context
     * @param observer  preference key change observer
@@ -95,7 +95,7 @@ public final class OneHandedSettingsUtil {
    }

    /**
     * Query one handed enable or disable flag from Settings provider.
     * Queries one handed enable or disable flag from Settings provider.
     *
     * @return enable or disable one handed mode flag.
     */
@@ -105,7 +105,7 @@ public final class OneHandedSettingsUtil {
    }

    /**
     * Query taps app to exit config from Settings provider.
     * Queries taps app to exit config from Settings provider.
     *
     * @return enable or disable taps app exit.
     */
@@ -115,7 +115,7 @@ public final class OneHandedSettingsUtil {
    }

    /**
     * Query timeout value from Settings provider. Default is
     * Queries timeout value from Settings provider. Default is.
     * {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS}
     *
     * @return timeout value in seconds.
@@ -135,10 +135,31 @@ public final class OneHandedSettingsUtil {
                Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 0, userId) == 1;
    }


    /**
     * Queries tutorial shown counts from Settings provider. Default is 0.
     *
     * @return counts tutorial shown counts.
     */
    public int getTutorialShownCounts(ContentResolver resolver, int userId) {
        return Settings.Secure.getIntForUser(resolver,
                Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, 0, userId);
    }

    /**
     * Sets tutorial shown counts.
     *
     * @return true if the value was set, false on database errors.
     */
    public boolean setTutorialShownCounts(ContentResolver resolver, int shownCounts, int userId) {
        return Settings.Secure.putIntForUser(resolver,
                Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, shownCounts, userId);
    }

    /**
     * Sets one handed activated or not to notify state for shortcut
     * Sets one handed activated or not to notify state for shortcut.
     *
     * @return activated or not
     * @return true if one handed mode is activated.
     */
    public boolean getOneHandedModeActivated(ContentResolver resolver, int userId) {
        return Settings.Secure.getIntForUser(resolver,
@@ -146,9 +167,9 @@ public final class OneHandedSettingsUtil {
    }

    /**
     * Sets one handed activated or not to notify state for shortcut
     * Sets one handed activated or not to notify state for shortcut.
     *
     * @return activated or not
     * @return true if the value was set, false on database errors.
     */
    public boolean setOneHandedModeActivated(ContentResolver resolver, int state, int userId) {
        return Settings.Secure.putIntForUser(resolver,
@@ -167,6 +188,8 @@ public final class OneHandedSettingsUtil {
        pw.println(getSettingsTapsAppToExit(resolver, userId));
        pw.print(innerPrefix + "shortcutActivated=");
        pw.println(getOneHandedModeActivated(resolver, userId));
        pw.print(innerPrefix + "tutorialShownCounts=");
        pw.println(getTutorialShownCounts(resolver, userId));
    }

    public OneHandedSettingsUtil() {
+29 −5
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import android.annotation.IntDef;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

/**
 Represents current OHM state by following steps, a generic CUJ is
@@ -28,13 +30,13 @@ import java.lang.annotation.RetentionPolicy;
 */
public class OneHandedState {
    /** DEFAULT STATE after OHM feature initialized. */
    public static final int STATE_NONE = 0x00000000;
    public static final int STATE_NONE = 0;
    /** The state flag set when user trigger OHM. */
    public static final int STATE_ENTERING = 0x00000001;
    public static final int STATE_ENTERING = 1;
    /** The state flag set when transitioning */
    public static final int STATE_ACTIVE = 0x00000002;
    public static final int STATE_ACTIVE = 2;
    /** The state flag set when user stop OHM feature. */
    public static final int STATE_EXITING = 0x00000004;
    public static final int STATE_EXITING = 3;

    @IntDef(prefix = { "STATE_" }, value =  {
            STATE_NONE,
@@ -54,9 +56,18 @@ public class OneHandedState {

    private static final String TAG = OneHandedState.class.getSimpleName();

    private List<OnStateChangedListener> mStateChangeListeners = new ArrayList<>();

    /**
     * Adds listener to be called back when one handed state changed.
     * @param listener the listener to be called back
     */
    public void addSListeners(OnStateChangedListener listener) {
        mStateChangeListeners.add(listener);
    }

    /**
     * Gets current transition state of One handed mode.
     *
     * @return The bitwise flags representing current states.
     */
    public @State int getState() {
@@ -85,6 +96,9 @@ public class OneHandedState {
     */
    public void setState(@State int newState) {
        sCurrentState = newState;
        if (!mStateChangeListeners.isEmpty()) {
            mStateChangeListeners.forEach((listener) -> listener.onStateChanged(newState));
        }
    }

    /** Dumps internal state. */
@@ -93,4 +107,14 @@ public class OneHandedState {
        pw.println(TAG);
        pw.println(innerPrefix + "sCurrentState=" + sCurrentState);
    }

    /**
     * Gets notified when one handed state changed
     *
     * @see OneHandedState
     */
    public interface OnStateChangedListener {
        /** Called when one handed state changed */
        default void onStateChanged(@State int newState) {}
    }
}
+100 −118
Original line number Diff line number Diff line
@@ -16,13 +16,19 @@

package com.android.wm.shell.onehanded;

import static android.os.UserHandle.myUserId;

import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE;
import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING;
import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING;
import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE;

import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.SystemProperties;
import android.provider.Settings;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -32,6 +38,7 @@ import android.widget.FrameLayout;

import androidx.annotation.NonNull;

import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
@@ -39,42 +46,33 @@ import com.android.wm.shell.common.ShellExecutor;
import java.io.PrintWriter;

/**
 * Manages the user tutorial handling for One Handed operations, including animations synchronized
 * with one-handed translation.
 * Refer {@link OneHandedGestureHandler} and {@link OneHandedTouchHandler} to see start and stop
 * one handed gesture
 * Handles tutorial visibility and synchronized transition for One Handed operations,
 * TargetViewContainer only be created and attach to window when
 * shown counts < {@link MAX_TUTORIAL_SHOW_COUNT}, and detach TargetViewContainer from window
 * after exiting one handed mode.
 */
public class OneHandedTutorialHandler implements OneHandedTransitionCallback {
public class OneHandedTutorialHandler implements OneHandedTransitionCallback,
        OneHandedState.OnStateChangedListener {
    private static final String TAG = "OneHandedTutorialHandler";
    private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
            "persist.debug.one_handed_offset_percentage";
    private static final int MAX_TUTORIAL_SHOW_COUNT = 2;
    private final WindowManager mWindowManager;
    private final String mPackageName;

    private final float mTutorialHeightRatio;
    private final WindowManager mWindowManager;
    private final OneHandedSettingsUtil mSettingsUtil;
    private final ShellExecutor mShellExecutor;

    private boolean mCanShow;
    private @OneHandedState.State int mCurrentState;
    private int mShownCounts;
    private int mTutorialAreaHeight;

    private Context mContext;
    private Rect mDisplayBounds;
    private View mTutorialView;
    private ContentResolver mContentResolver;
    private boolean mCanShowTutorial;
    private boolean mIsOneHandedMode;

    private enum ONE_HANDED_TRIGGER_STATE {
        UNSET, ENTERING, EXITING
    }
    /**
     * Current One-Handed trigger state.
     * Note: This is a dynamic state, whenever last state has been confirmed
     * (i.e. onStartFinished() or onStopFinished()), the state should be set "UNSET" at final.
     */
    private ONE_HANDED_TRIGGER_STATE mTriggerState = ONE_HANDED_TRIGGER_STATE.UNSET;

    /**
     * Container of the tutorial panel showing at outside region when one handed starting
     */
    private ViewGroup mTargetViewContainer;
    private int mTutorialAreaHeight;
    private Rect mDisplayBounds;
    private @Nullable View mTutorialView;
    private @Nullable ViewGroup mTargetViewContainer;

    private final OneHandedAnimationCallback mAnimationCallback = new OneHandedAnimationCallback() {
        @Override
@@ -82,63 +80,51 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback {
            if (!canShowTutorial()) {
                return;
            }
            mTargetViewContainer.setVisibility(View.VISIBLE);
            mTargetViewContainer.setTransitionGroup(true);
            mTargetViewContainer.setTranslationY(yPos - mTargetViewContainer.getHeight());
        }

        @Override
        public void onOneHandedAnimationStart(
                OneHandedAnimationController.OneHandedTransitionAnimator animator) {
            final float startValue = (float) animator.getStartValue();
            if (mTriggerState == ONE_HANDED_TRIGGER_STATE.UNSET) {
                mTriggerState = (startValue == 0f)
                        ? ONE_HANDED_TRIGGER_STATE.ENTERING : ONE_HANDED_TRIGGER_STATE.EXITING;
                if (mCanShowTutorial && mTriggerState == ONE_HANDED_TRIGGER_STATE.ENTERING) {
                    attachTurtorialTarget();
                }
            }
        }
    };

    public OneHandedTutorialHandler(Context context, DisplayLayout displayLayout,
            WindowManager windowManager, ShellExecutor mainExecutor) {
            WindowManager windowManager, OneHandedSettingsUtil settingsUtil,
            ShellExecutor mainExecutor) {
        mContext = context;
        mWindowManager = windowManager;
        mPackageName = context.getPackageName();
        mContentResolver = context.getContentResolver();
        mWindowManager = windowManager;
        mSettingsUtil = settingsUtil;
        mShellExecutor = mainExecutor;
        final float offsetPercentageConfig = context.getResources().getFraction(
                R.fraction.config_one_handed_offset, 1, 1);
        final int sysPropPercentageConfig = SystemProperties.getInt(
                ONE_HANDED_MODE_OFFSET_PERCENTAGE, Math.round(offsetPercentageConfig * 100.0f));
        mTutorialHeightRatio = sysPropPercentageConfig / 100.0f;
        onDisplayChanged(displayLayout);
        mCanShowTutorial = (Settings.Secure.getInt(mContentResolver,
                Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, 0) >= MAX_TUTORIAL_SHOW_COUNT)
                ? false : true;
        mIsOneHandedMode = false;

        mainExecutor.execute(() -> {
            recreateTutorialView(mContext);
        });
        mShownCounts = mSettingsUtil.getTutorialShownCounts(mContentResolver, myUserId());
    }

    @Override
    public void onStartFinished(Rect bounds) {
        updateFinished(View.VISIBLE, 0f);
        updateTutorialCount();
        mTriggerState = ONE_HANDED_TRIGGER_STATE.UNSET;
    public void onStateChanged(int newState) {
        mCurrentState = newState;
        if (!canShowTutorial()) {
            return;
        }
        switch (newState) {
            case STATE_ENTERING:
                createViewAndAttachToWindow(mContext);
                break;
            case STATE_ACTIVE:
            case STATE_EXITING:
                // no - op
                break;
            case STATE_NONE:
                removeTutorialFromWindowManager(true /* increment */);
                break;
            default:
                break;
        }

    @Override
    public void onStopFinished(Rect bounds) {
        updateFinished(View.INVISIBLE, -mTargetViewContainer.getHeight());
        removeTutorialFromWindowManager();
        mTriggerState = ONE_HANDED_TRIGGER_STATE.UNSET;
    }

    /**
     * Called when onDisplayAdded() or onDisplayRemoved() callback
     * Called when onDisplayAdded() or onDisplayRemoved() callback.
     * @param displayLayout The latest {@link DisplayLayout} representing current displayId
     */
    public void onDisplayChanged(DisplayLayout displayLayout) {
@@ -151,38 +137,32 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback {
        mTutorialAreaHeight = Math.round(mDisplayBounds.height() * mTutorialHeightRatio);
    }

    private void recreateTutorialView(Context context) {
        mTutorialView = LayoutInflater.from(context).inflate(R.layout.one_handed_tutorial,
                null);
    @VisibleForTesting
    void createViewAndAttachToWindow(Context context) {
        if (!canShowTutorial()) {
            return;
        }
        mTutorialView = LayoutInflater.from(context).inflate(R.layout.one_handed_tutorial, null);
        mTargetViewContainer = new FrameLayout(context);
        mTargetViewContainer.setClipChildren(false);
        mTargetViewContainer.addView(mTutorialView);
        mTargetViewContainer.setVisibility(mIsOneHandedMode ? View.VISIBLE : View.GONE);

        attachTargetToWindow();
    }

    private void updateFinished(int visible, float finalPosition) {
    @VisibleForTesting
    boolean setTutorialShownCountIncrement() {
        if (!canShowTutorial()) {
            return;
        }
        mIsOneHandedMode = (finalPosition == 0f) ? true : false;
        mTargetViewContainer.setVisibility(visible);
        mTargetViewContainer.setTranslationY(finalPosition);
            return false;
        }

    private void updateTutorialCount() {
        int showCount = Settings.Secure.getInt(mContentResolver,
                Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, 0);
        showCount = Math.min(MAX_TUTORIAL_SHOW_COUNT, showCount + 1);
        mCanShowTutorial = showCount < MAX_TUTORIAL_SHOW_COUNT;
        Settings.Secure.putInt(mContentResolver,
                Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, showCount);
        mShownCounts += 1;
        return mSettingsUtil.setTutorialShownCounts(mContentResolver, mShownCounts, myUserId());
    }

    /**
     * Adds the tutorial target view to the WindowManager and update its layout, so it's ready
     * to be animated in.
     * Adds the tutorial target view to the WindowManager and update its layout.
     */
    private void attachTurtorialTarget() {
    private void attachTargetToWindow() {
        if (!mTargetViewContainer.isAttachedToWindow()) {
            try {
                mWindowManager.addView(mTargetViewContainer, getTutorialTargetLayoutParams());
@@ -195,14 +175,18 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback {
        }
    }

    private void removeTutorialFromWindowManager() {
        if (mTargetViewContainer.isAttachedToWindow()) {
    @VisibleForTesting
    void removeTutorialFromWindowManager(boolean increment) {
        if (mTargetViewContainer != null && mTargetViewContainer.isAttachedToWindow()) {
            mWindowManager.removeViewImmediate(mTargetViewContainer);
            if (increment) {
                setTutorialShownCountIncrement();
            }
        }
    }

    OneHandedAnimationCallback getAnimationCallback() {
        return mAnimationCallback;
    @Nullable OneHandedAnimationCallback getAnimationCallback() {
        return canShowTutorial() ? mAnimationCallback : null /* Disabled */;
    }

    /**
@@ -222,38 +206,36 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback {
        return lp;
    }

    void dump(@NonNull PrintWriter pw) {
        final String innerPrefix = "  ";
        pw.println(TAG);
        pw.print(innerPrefix + "mTriggerState=");
        pw.println(mTriggerState);
        pw.print(innerPrefix + "mDisplayBounds=");
        pw.println(mDisplayBounds);
        pw.print(innerPrefix + "mTutorialAreaHeight=");
        pw.println(mTutorialAreaHeight);
    }

    private boolean canShowTutorial() {
        if (!mCanShowTutorial) {
            // Since canSHowTutorial() will be called in onAnimationUpdate() and we still need to
            // hide Tutorial text in the period of continuously onAnimationUpdate() API call,
            // so we have to hide mTargetViewContainer here.
            mTargetViewContainer.setVisibility(View.GONE);
            return false;
        }
        return true;
    @VisibleForTesting
    boolean canShowTutorial() {
        return mCanShow = mShownCounts < MAX_TUTORIAL_SHOW_COUNT;
    }

    /**
     * onConfigurationChanged events for updating tutorial text.
     * @param newConfig
     */
    public void onConfigurationChanged(Configuration newConfig) {
        if (!mCanShowTutorial) {
    public void onConfigurationChanged() {
        if (!canShowTutorial()) {
            return;
        }
        removeTutorialFromWindowManager();
        recreateTutorialView(mContext.createConfigurationContext(newConfig));
        attachTurtorialTarget();
        removeTutorialFromWindowManager(false /* increment */);
        if (mCurrentState == STATE_ENTERING || mCurrentState == STATE_ACTIVE) {
            createViewAndAttachToWindow(mContext);
        }
    }

    void dump(@NonNull PrintWriter pw) {
        final String innerPrefix = "  ";
        pw.println(TAG);
        pw.print(innerPrefix + "mCanShow=");
        pw.println(mCanShow);
        pw.print(innerPrefix + "mCurrentState=");
        pw.println(mCurrentState);
        pw.print(innerPrefix + "mDisplayBounds=");
        pw.println(mDisplayBounds);
        pw.print(innerPrefix + "mShownCounts=");
        pw.println(mShownCounts);
        pw.print(innerPrefix + "mTutorialAreaHeight=");
        pw.println(mTutorialAreaHeight);
    }
}
Loading