Migrate OneHanded to WM shell (4/n)
Move OneHanded related codes, res and tests to WM shell.
Some tests add ignored currently due to permission issue.
Add another bug to trace it.
Also fix animation issue caused by wrong logic.
Bug: 161980408
Fix: 168074363
Test: manual check one handed function
Test: atest WMShellUnitTests
Test: atest SystemUITests
Test: make ArcSystemUI; make ArcSystemUITests
Test: make CarSystemUI; make CarSystemUITests
Change-Id: Ib25661d010278202ffe15fd9c4962e76e6698c21
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 16b87c4..1591b06 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -105,6 +105,7 @@
static_libs: [
"protolog-lib",
"WindowManager-Shell-proto",
+ "androidx.appcompat_appcompat",
],
manifest: "AndroidManifest.xml",
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.png b/libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.png
new file mode 100644
index 0000000..6c1f1cf
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable-hdpi/one_handed_tutorial.png
Binary files differ
diff --git a/libs/WindowManager/Shell/res/layout/one_handed_tutorial.xml b/libs/WindowManager/Shell/res/layout/one_handed_tutorial.xml
new file mode 100644
index 0000000..dc54caf
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/one_handed_tutorial.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2020 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
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/one_handed_tutorial_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center_horizontal | center_vertical"
+ android:background="@android:color/transparent">
+
+ <ImageView
+ android:id="@+id/one_handed_tutorial_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="0dp"
+ android:gravity="center_horizontal"
+ android:src="@drawable/one_handed_tutorial"
+ android:scaleType="centerInside" />
+
+ <TextView
+ android:id="@+id/one_handed_tutorial_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="0dp"
+ android:gravity="center_horizontal"
+ android:textAlignment="center"
+ android:fontFamily="google-sans-medium"
+ android:text="@string/one_handed_tutorial_title"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ android:textColor="@android:color/white"/>
+
+ <TextView
+ android:id="@+id/one_handed_tutorial_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:layout_marginBottom="0dp"
+ android:layout_marginStart="86dp"
+ android:layout_marginEnd="86dp"
+ android:gravity="center_horizontal"
+ android:fontFamily="roboto-regular"
+ android:text="@string/one_handed_tutorial_description"
+ android:textAlignment="center"
+ android:textSize="14sp"
+ android:textStyle="normal"
+ android:alpha="0.7"
+ android:textColor="@android:color/white"/>
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 39efd07..63b0f6f 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -29,4 +29,7 @@
<!-- Animation duration when using long press on recents to dock -->
<integer name="long_press_dock_anim_duration">250</integer>
+
+ <!-- Allow one handed to enable round corner -->
+ <bool name="config_one_handed_enable_round_corner">true</bool>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index ce69028..7fb641a 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -62,4 +62,8 @@
<dimen name="docked_divider_handle_width">16dp</dimen>
<dimen name="docked_divider_handle_height">2dp</dimen>
+
+ <!-- One-Handed Mode -->
+ <!-- Threshold for dragging distance to enable one-handed mode -->
+ <dimen name="gestures_onehanded_drag_threshold">20dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index cad9247..b6668fb 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -88,4 +88,9 @@
<string name="accessibility_action_divider_top_30">Top 30%</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>
+
+ <!-- One-Handed Tutorial title [CHAR LIMIT=60] -->
+ <string name="one_handed_tutorial_title">Using one-handed mode</string>
+ <!-- One-Handed Tutorial description [CHAR LIMIT=NONE] -->
+ <string name="one_handed_tutorial_description">To exit, swipe up from the bottom of the screen or tap anywhere above the app</string>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
new file mode 100644
index 0000000..9c78fc5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback;
+
+import java.io.PrintWriter;
+
+/**
+ * Interface to engage one handed feature.
+ */
+public interface OneHanded {
+ /**
+ * Return whether the device has one handed feature or not.
+ */
+ boolean hasOneHandedFeature();
+
+ /**
+ * Return one handed settings enabled or not.
+ */
+ boolean isOneHandedEnabled();
+
+ /**
+ * Return swipe to notification settings enabled or not.
+ */
+ boolean isSwipeToNotificationEnabled();
+
+ /**
+ * Enters one handed mode.
+ */
+ void startOneHanded();
+
+ /**
+ * Exits one handed mode.
+ */
+ void stopOneHanded();
+
+ /**
+ * Exits one handed mode with {@link OneHandedEvents}.
+ */
+ void stopOneHanded(int event);
+
+ /**
+ * Set navigation 3 button mode enabled or disabled by users.
+ */
+ void setThreeButtonModeEnabled(boolean enabled);
+
+ /**
+ * Register callback to be notified after {@link OneHandedDisplayAreaOrganizer}
+ * transition start or finish
+ */
+ void registerTransitionCallback(OneHandedTransitionCallback callback);
+
+ /**
+ * Register callback for one handed gesture, this gesture callbcak will be activated on
+ * 3 button navigation mode only
+ */
+ void registerGestureCallback(OneHandedGestureEventCallback callback);
+
+ /**
+ * Dump one handed status.
+ */
+ void dump(@NonNull PrintWriter pw);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java
new file mode 100644
index 0000000..6749f7e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationCallback.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import android.view.SurfaceControl;
+
+/**
+ * Additional callback interface for OneHanded animation
+ */
+public interface OneHandedAnimationCallback {
+ /**
+ * Called when OneHanded animation is started.
+ */
+ default void onOneHandedAnimationStart(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ }
+
+ /**
+ * Called when OneHanded animation is ended.
+ */
+ default void onOneHandedAnimationEnd(SurfaceControl.Transaction tx,
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ }
+
+ /**
+ * Called when OneHanded animation is cancelled.
+ */
+ default void onOneHandedAnimationCancel(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ }
+
+ /**
+ * Called when OneHanded animator is updating offset
+ */
+ default void onTutorialAnimationUpdate(int offset) {}
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java
new file mode 100644
index 0000000..9639096
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedAnimationController.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Controller class of OneHanded animations (both from and to OneHanded mode).
+ */
+public class OneHandedAnimationController {
+ private static final float FRACTION_START = 0f;
+ private static final float FRACTION_END = 1f;
+
+ public static final int TRANSITION_DIRECTION_NONE = 0;
+ public static final int TRANSITION_DIRECTION_TRIGGER = 1;
+ public static final int TRANSITION_DIRECTION_EXIT = 2;
+
+ @IntDef(prefix = {"TRANSITION_DIRECTION_"}, value = {
+ TRANSITION_DIRECTION_NONE,
+ TRANSITION_DIRECTION_TRIGGER,
+ TRANSITION_DIRECTION_EXIT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TransitionDirection {
+ }
+
+ private final Interpolator mOvershootInterpolator;
+ private final OneHandedSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private final HashMap<SurfaceControl, OneHandedTransitionAnimator> mAnimatorMap =
+ new HashMap<>();
+
+ /**
+ * Constructor of OneHandedAnimationController
+ */
+ public OneHandedAnimationController(Context context) {
+ mSurfaceTransactionHelper = new OneHandedSurfaceTransactionHelper(context);
+ mOvershootInterpolator = new OvershootInterpolator();
+ }
+
+ @SuppressWarnings("unchecked")
+ OneHandedTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds,
+ Rect endBounds) {
+ final OneHandedTransitionAnimator animator = mAnimatorMap.get(leash);
+ if (animator == null) {
+ mAnimatorMap.put(leash, setupOneHandedTransitionAnimator(
+ OneHandedTransitionAnimator.ofBounds(leash, startBounds, endBounds)));
+ } else if (animator.isRunning()) {
+ animator.updateEndValue(endBounds);
+ } else {
+ animator.cancel();
+ mAnimatorMap.put(leash, setupOneHandedTransitionAnimator(
+ OneHandedTransitionAnimator.ofBounds(leash, startBounds, endBounds)));
+ }
+ return mAnimatorMap.get(leash);
+ }
+
+ HashMap<SurfaceControl, OneHandedTransitionAnimator> getAnimatorMap() {
+ return mAnimatorMap;
+ }
+
+ boolean isAnimatorsConsumed() {
+ return mAnimatorMap.isEmpty();
+ }
+
+ void removeAnimator(SurfaceControl key) {
+ final OneHandedTransitionAnimator animator = mAnimatorMap.remove(key);
+ if (animator != null && animator.isRunning()) {
+ animator.cancel();
+ }
+ }
+
+ OneHandedTransitionAnimator setupOneHandedTransitionAnimator(
+ OneHandedTransitionAnimator animator) {
+ animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper);
+ animator.setInterpolator(mOvershootInterpolator);
+ animator.setFloatValues(FRACTION_START, FRACTION_END);
+ return animator;
+ }
+
+ /**
+ * Animator for OneHanded transition animation which supports both alpha and bounds animation.
+ *
+ * @param <T> Type of property to animate, either offset (float)
+ */
+ public abstract static class OneHandedTransitionAnimator<T> extends ValueAnimator implements
+ ValueAnimator.AnimatorUpdateListener,
+ ValueAnimator.AnimatorListener {
+
+ private final SurfaceControl mLeash;
+ private T mStartValue;
+ private T mEndValue;
+ private T mCurrentValue;
+
+ private final List<OneHandedAnimationCallback> mOneHandedAnimationCallbacks =
+ new ArrayList<>();
+ private OneHandedSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+
+ private @TransitionDirection int mTransitionDirection;
+ private int mTransitionOffset;
+
+ private OneHandedTransitionAnimator(SurfaceControl leash, T startValue, T endValue) {
+ mLeash = leash;
+ mStartValue = startValue;
+ mEndValue = endValue;
+ addListener(this);
+ addUpdateListener(this);
+ mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+ mTransitionDirection = TRANSITION_DIRECTION_NONE;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mCurrentValue = mStartValue;
+ mOneHandedAnimationCallbacks.forEach(
+ (callback) -> {
+ callback.onOneHandedAnimationStart(this);
+ }
+ );
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentValue = mEndValue;
+ final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
+ onEndTransaction(mLeash, tx);
+ mOneHandedAnimationCallbacks.forEach(
+ (callback) -> {
+ callback.onOneHandedAnimationEnd(tx, this);
+ }
+ );
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCurrentValue = mEndValue;
+ mOneHandedAnimationCallbacks.forEach(
+ (callback) -> {
+ callback.onOneHandedAnimationCancel(this);
+ }
+ );
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(),
+ animation.getAnimatedFraction());
+ mOneHandedAnimationCallbacks.forEach(
+ (callback) -> {
+ callback.onTutorialAnimationUpdate(((Rect) mCurrentValue).top);
+ }
+ );
+ }
+
+ void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
+ }
+
+ void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
+ }
+
+ abstract void applySurfaceControlTransaction(SurfaceControl leash,
+ SurfaceControl.Transaction tx, float fraction);
+
+ OneHandedSurfaceTransactionHelper getSurfaceTransactionHelper() {
+ return mSurfaceTransactionHelper;
+ }
+
+ void setSurfaceTransactionHelper(OneHandedSurfaceTransactionHelper helper) {
+ mSurfaceTransactionHelper = helper;
+ }
+
+ OneHandedTransitionAnimator<T> setOneHandedAnimationCallbacks(
+ OneHandedAnimationCallback callback) {
+ mOneHandedAnimationCallbacks.add(callback);
+ return this;
+ }
+
+ SurfaceControl getLeash() {
+ return mLeash;
+ }
+
+ Rect getDestinationBounds() {
+ return (Rect) mEndValue;
+ }
+
+ int getDestinationOffset() {
+ return ((Rect) mEndValue).top - ((Rect) mStartValue).top;
+ }
+
+ @TransitionDirection
+ int getTransitionDirection() {
+ return mTransitionDirection;
+ }
+
+ OneHandedTransitionAnimator<T> setTransitionDirection(int direction) {
+ mTransitionDirection = direction;
+ return this;
+ }
+
+ OneHandedTransitionAnimator<T> setTransitionOffset(int offset) {
+ mTransitionOffset = offset;
+ return this;
+ }
+
+ T getStartValue() {
+ return mStartValue;
+ }
+
+ T getEndValue() {
+ return mEndValue;
+ }
+
+ void setCurrentValue(T value) {
+ mCurrentValue = value;
+ }
+
+ /**
+ * Updates the {@link #mEndValue}.
+ */
+ void updateEndValue(T endValue) {
+ mEndValue = endValue;
+ }
+
+ SurfaceControl.Transaction newSurfaceControlTransaction() {
+ return mSurfaceControlTransactionFactory.getTransaction();
+ }
+
+ @VisibleForTesting
+ static OneHandedTransitionAnimator<Rect> ofBounds(SurfaceControl leash,
+ Rect startValue, Rect endValue) {
+
+ return new OneHandedTransitionAnimator<Rect>(leash, new Rect(startValue),
+ new Rect(endValue)) {
+
+ private final Rect mTmpRect = new Rect();
+
+ private int getCastedFractionValue(float start, float end, float fraction) {
+ return (int) (start * (1 - fraction) + end * fraction + .5f);
+ }
+
+ @Override
+ void applySurfaceControlTransaction(SurfaceControl leash,
+ SurfaceControl.Transaction tx, float fraction) {
+ final Rect start = getStartValue();
+ final Rect end = getEndValue();
+ mTmpRect.set(
+ getCastedFractionValue(start.left, end.left, fraction),
+ getCastedFractionValue(start.top, end.top, fraction),
+ getCastedFractionValue(start.right, end.right, fraction),
+ getCastedFractionValue(start.bottom, end.bottom, fraction));
+ setCurrentValue(mTmpRect);
+ getSurfaceTransactionHelper().crop(tx, leash, mTmpRect)
+ .round(tx, leash);
+ tx.apply();
+ }
+
+ @Override
+ void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
+ getSurfaceTransactionHelper()
+ .alpha(tx, leash, 1f)
+ .translate(tx, leash, getEndValue().top - getStartValue().top)
+ .round(tx, leash);
+ tx.apply();
+ }
+ };
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
new file mode 100644
index 0000000..c84b478
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static android.os.UserHandle.USER_CURRENT;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.content.Context;
+import android.content.om.IOverlayManager;
+import android.content.om.OverlayInfo;
+import android.database.ContentObserver;
+import android.graphics.Point;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.common.DisplayChangeController;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages and manipulates the one handed states, transitions, and gesture for phones.
+ */
+public class OneHandedController implements OneHanded {
+ private static final String TAG = "OneHandedController";
+
+ private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
+ "persist.debug.one_handed_offset_percentage";
+ private static final String ONE_HANDED_MODE_GESTURAL_OVERLAY =
+ "com.android.internal.systemui.onehanded.gestural";
+
+ static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
+
+ private final boolean mHasOneHandedFeature;
+ private boolean mIsOneHandedEnabled;
+ private boolean mIsSwipeToNotificationEnabled;
+ private boolean mTaskChangeToExit;
+ private float mOffSetFraction;
+
+ private final Context mContext;
+ private final DisplayController mDisplayController;
+ private final OneHandedGestureHandler mGestureHandler;
+ private final OneHandedTimeoutHandler mTimeoutHandler;
+ private final OneHandedTouchHandler mTouchHandler;
+ private final OneHandedTutorialHandler mTutorialHandler;
+ private final IOverlayManager mOverlayManager;
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+ private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer;
+
+ /**
+ * Handle rotation based on OnDisplayChangingListener callback
+ */
+ private final DisplayChangeController.OnDisplayChangingListener mRotationController =
+ (display, fromRotation, toRotation, wct) -> {
+ if (mDisplayAreaOrganizer != null) {
+ mDisplayAreaOrganizer.onRotateDisplay(fromRotation, toRotation);
+ }
+ };
+
+ private final ContentObserver mEnabledObserver = new ContentObserver(mMainHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final boolean enabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ mContext.getContentResolver());
+ OneHandedEvents.writeEvent(enabled
+ ? OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON
+ : OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF);
+
+ setOneHandedEnabled(enabled);
+
+ // Also checks swipe to notification settings since they all need gesture overlay.
+ setEnabledGesturalOverlay(
+ enabled || OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ mContext.getContentResolver()));
+ }
+ };
+
+ private final ContentObserver mTimeoutObserver = new ContentObserver(mMainHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final int newTimeout = OneHandedSettingsUtil.getSettingsOneHandedModeTimeout(
+ mContext.getContentResolver());
+ int metricsId = OneHandedEvents.OneHandedSettingsTogglesEvent.INVALID.getId();
+ switch (newTimeout) {
+ case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER:
+ metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER;
+ break;
+ case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS:
+ metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4;
+ break;
+ case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS:
+ metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8;
+ break;
+ case OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS:
+ metricsId = OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12;
+ break;
+ default:
+ // do nothing
+ break;
+ }
+ OneHandedEvents.writeEvent(metricsId);
+
+ if (mTimeoutHandler != null) {
+ mTimeoutHandler.setTimeout(newTimeout);
+ }
+ }
+ };
+
+ private final ContentObserver mTaskChangeExitObserver = new ContentObserver(mMainHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final boolean enabled = OneHandedSettingsUtil.getSettingsTapsAppToExit(
+ mContext.getContentResolver());
+ OneHandedEvents.writeEvent(enabled
+ ? OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON
+ : OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF);
+
+ setTaskChangeToExit(enabled);
+ }
+ };
+
+ private final ContentObserver mSwipeToNotificationEnabledObserver =
+ new ContentObserver(mMainHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ final boolean enabled =
+ OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ mContext.getContentResolver());
+ setSwipeToNotificationEnabled(enabled);
+
+ // Also checks one handed mode settings since they all need gesture overlay.
+ setEnabledGesturalOverlay(
+ enabled || OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ mContext.getContentResolver()));
+ }
+ };
+
+ /**
+ * The static constructor method to create OneHnadedController.
+ */
+ public static OneHandedController create(
+ Context context, DisplayController displayController) {
+ OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context);
+ OneHandedAnimationController animationController =
+ new OneHandedAnimationController(context);
+ OneHandedTouchHandler touchHandler = new OneHandedTouchHandler();
+ OneHandedGestureHandler gestureHandler = new OneHandedGestureHandler(
+ context, displayController);
+ OneHandedDisplayAreaOrganizer organizer = new OneHandedDisplayAreaOrganizer(
+ context, displayController, animationController, tutorialHandler);
+ return new OneHandedController(context, displayController, organizer, touchHandler,
+ tutorialHandler, gestureHandler);
+ }
+
+ @VisibleForTesting
+ OneHandedController(Context context,
+ DisplayController displayController,
+ OneHandedDisplayAreaOrganizer displayAreaOrganizer,
+ OneHandedTouchHandler touchHandler,
+ OneHandedTutorialHandler tutorialHandler,
+ OneHandedGestureHandler gestureHandler) {
+ mHasOneHandedFeature = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
+ if (!mHasOneHandedFeature) {
+ Log.i(TAG, "Device config SUPPORT_ONE_HANDED_MODE off");
+ mContext = null;
+ mDisplayAreaOrganizer = null;
+ mDisplayController = null;
+ mTouchHandler = null;
+ mTutorialHandler = null;
+ mGestureHandler = null;
+ mTimeoutHandler = null;
+ mOverlayManager = null;
+ return;
+ }
+
+ mContext = context;
+ mDisplayAreaOrganizer = displayAreaOrganizer;
+ mDisplayController = displayController;
+ mTouchHandler = touchHandler;
+ mTutorialHandler = tutorialHandler;
+ mGestureHandler = gestureHandler;
+
+ mOverlayManager = IOverlayManager.Stub.asInterface(
+ ServiceManager.getService(Context.OVERLAY_SERVICE));
+ mOffSetFraction = SystemProperties.getInt(ONE_HANDED_MODE_OFFSET_PERCENTAGE, 50) / 100.0f;
+ mIsOneHandedEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ context.getContentResolver());
+ mIsSwipeToNotificationEnabled = OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ context.getContentResolver());
+ mTimeoutHandler = OneHandedTimeoutHandler.get();
+
+ mDisplayController.addDisplayChangingController(mRotationController);
+
+ setupCallback();
+ setupSettingObservers();
+ setupTimeoutListener();
+ setupGesturalOverlay();
+ updateSettings();
+ }
+
+ /**
+ * Set one handed enabled or disabled when user update settings
+ */
+ void setOneHandedEnabled(boolean enabled) {
+ mIsOneHandedEnabled = enabled;
+ updateOneHandedEnabled();
+ }
+
+ /**
+ * Set one handed enabled or disabled by when user update settings
+ */
+ void setTaskChangeToExit(boolean enabled) {
+ mTaskChangeToExit = enabled;
+ }
+
+ /**
+ * Sets whether to enable swipe bottom to notification gesture when user update settings.
+ */
+ void setSwipeToNotificationEnabled(boolean enabled) {
+ mIsSwipeToNotificationEnabled = enabled;
+ updateOneHandedEnabled();
+ }
+
+ @Override
+ public boolean hasOneHandedFeature() {
+ return mHasOneHandedFeature;
+ }
+
+ @Override
+ public boolean isOneHandedEnabled() {
+ return mIsOneHandedEnabled;
+ }
+
+ @Override
+ public boolean isSwipeToNotificationEnabled() {
+ return mIsSwipeToNotificationEnabled;
+ }
+
+ @Override
+ public void startOneHanded() {
+ if (!mDisplayAreaOrganizer.isInOneHanded()) {
+ final int yOffSet = Math.round(getDisplaySize().y * mOffSetFraction);
+ mDisplayAreaOrganizer.scheduleOffset(0, yOffSet);
+ mTimeoutHandler.resetTimer();
+
+ OneHandedEvents.writeEvent(OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN);
+ }
+ }
+
+ @Override
+ public void stopOneHanded() {
+ if (mDisplayAreaOrganizer.isInOneHanded()) {
+ mDisplayAreaOrganizer.scheduleOffset(0, 0);
+ mTimeoutHandler.removeTimer();
+ }
+ }
+
+ @Override
+ public void stopOneHanded(int event) {
+ if (!mTaskChangeToExit && event == OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT) {
+ //Task change exit not enable, do nothing and return here.
+ return;
+ }
+
+ if (mDisplayAreaOrganizer.isInOneHanded()) {
+ OneHandedEvents.writeEvent(event);
+ }
+
+ stopOneHanded();
+ }
+
+ @Override
+ public void setThreeButtonModeEnabled(boolean enabled) {
+ mGestureHandler.onThreeButtonModeEnabled(enabled);
+ }
+
+ @Override
+ public void registerTransitionCallback(OneHandedTransitionCallback callback) {
+ mDisplayAreaOrganizer.registerTransitionCallback(callback);
+ }
+
+ @Override
+ public void registerGestureCallback(OneHandedGestureEventCallback callback) {
+ mGestureHandler.setGestureEventListener(callback);
+ }
+
+ private void setupCallback() {
+ mTouchHandler.registerTouchEventListener(() ->
+ stopOneHanded(OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT));
+ mDisplayAreaOrganizer.registerTransitionCallback(mTouchHandler);
+ mDisplayAreaOrganizer.registerTransitionCallback(mGestureHandler);
+ mDisplayAreaOrganizer.registerTransitionCallback(mTutorialHandler);
+ }
+
+ private void setupSettingObservers() {
+ OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_ENABLED,
+ mContext.getContentResolver(), mEnabledObserver);
+ OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_TIMEOUT,
+ mContext.getContentResolver(), mTimeoutObserver);
+ OneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.TAPS_APP_TO_EXIT,
+ mContext.getContentResolver(), mTaskChangeExitObserver);
+ OneHandedSettingsUtil.registerSettingsKeyObserver(
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED,
+ mContext.getContentResolver(), mSwipeToNotificationEnabledObserver);
+ }
+
+ private void updateSettings() {
+ setOneHandedEnabled(OneHandedSettingsUtil
+ .getSettingsOneHandedModeEnabled(mContext.getContentResolver()));
+ mTimeoutHandler.setTimeout(OneHandedSettingsUtil
+ .getSettingsOneHandedModeTimeout(mContext.getContentResolver()));
+ setTaskChangeToExit(OneHandedSettingsUtil
+ .getSettingsTapsAppToExit(mContext.getContentResolver()));
+ setSwipeToNotificationEnabled(OneHandedSettingsUtil
+ .getSettingsSwipeToNotificationEnabled(mContext.getContentResolver()));
+ }
+
+ private void setupTimeoutListener() {
+ mTimeoutHandler.registerTimeoutListener(timeoutTime -> {
+ stopOneHanded(OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT);
+ });
+ }
+
+ /**
+ * Query the current display real size from {@link DisplayController}
+ *
+ * @return {@link DisplayController#getDisplay(int)#getDisplaySize()}
+ */
+ private Point getDisplaySize() {
+ Point displaySize = new Point();
+ if (mDisplayController != null && mDisplayController.getDisplay(DEFAULT_DISPLAY) != null) {
+ mDisplayController.getDisplay(DEFAULT_DISPLAY).getRealSize(displaySize);
+ }
+ return displaySize;
+ }
+
+ private void updateOneHandedEnabled() {
+ if (mDisplayAreaOrganizer.isInOneHanded()) {
+ stopOneHanded();
+ }
+ // TODO Be aware to unregisterOrganizer() after animation finished
+ mDisplayAreaOrganizer.unregisterOrganizer();
+ if (mIsOneHandedEnabled) {
+ mDisplayAreaOrganizer.registerOrganizer(
+ OneHandedDisplayAreaOrganizer.FEATURE_ONE_HANDED);
+ }
+ mTouchHandler.onOneHandedEnabled(mIsOneHandedEnabled);
+ mGestureHandler.onOneHandedEnabled(mIsOneHandedEnabled || mIsSwipeToNotificationEnabled);
+ }
+
+ private void setupGesturalOverlay() {
+ if (!OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(mContext.getContentResolver())) {
+ return;
+ }
+
+ OverlayInfo info = null;
+ try {
+ // TODO(b/157958539) migrate new RRO config file after S+
+ mOverlayManager.setHighestPriority(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT);
+ info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY, USER_CURRENT);
+ } catch (RemoteException e) { /* Do nothing */ }
+
+ if (info != null && !info.isEnabled()) {
+ // Enable the default gestural one handed overlay.
+ setEnabledGesturalOverlay(true);
+ }
+ }
+
+ @VisibleForTesting
+ private void setEnabledGesturalOverlay(boolean enabled) {
+ try {
+ mOverlayManager.setEnabled(ONE_HANDED_MODE_GESTURAL_OVERLAY, enabled, USER_CURRENT);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "mOffSetFraction=");
+ pw.println(mOffSetFraction);
+
+ if (mDisplayAreaOrganizer != null) {
+ mDisplayAreaOrganizer.dump(pw);
+ }
+
+ if (mTouchHandler != null) {
+ mTouchHandler.dump(pw);
+ }
+
+ if (mTimeoutHandler != null) {
+ mTimeoutHandler.dump(pw);
+ }
+
+ if (mTutorialHandler != null) {
+ mTutorialHandler.dump(pw);
+ }
+
+ OneHandedSettingsUtil.dump(pw, innerPrefix, mContext.getContentResolver());
+
+ if (mOverlayManager != null) {
+ OverlayInfo info = null;
+ try {
+ info = mOverlayManager.getOverlayInfo(ONE_HANDED_MODE_GESTURAL_OVERLAY,
+ USER_CURRENT);
+ } catch (RemoteException e) { /* Do nothing */ }
+
+ if (info != null && !info.isEnabled()) {
+ pw.print(innerPrefix + "OverlayInfo=");
+ pw.println(info);
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
new file mode 100644
index 0000000..9954618
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_EXIT;
+import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_TRIGGER;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.window.DisplayAreaInfo;
+import android.window.DisplayAreaOrganizer;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.os.SomeArgs;
+import com.android.wm.shell.common.DisplayController;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Manages OneHanded display areas such as offset.
+ *
+ * This class listens on {@link DisplayAreaOrganizer} callbacks for windowing mode change
+ * both to and from OneHanded and issues corresponding animation if applicable.
+ * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running
+ * and files a final {@link WindowContainerTransaction} at the end of the transition.
+ *
+ * This class is also responsible for translating one handed operations within SysUI component
+ */
+public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer {
+ private static final String TAG = "OneHandedDisplayAreaOrganizer";
+ private static final String ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION =
+ "persist.debug.one_handed_translate_animation_duration";
+
+ @VisibleForTesting
+ static final int MSG_RESET_IMMEDIATE = 1;
+ @VisibleForTesting
+ static final int MSG_OFFSET_ANIMATE = 2;
+ @VisibleForTesting
+ static final int MSG_OFFSET_FINISH = 3;
+
+ private final Rect mLastVisualDisplayBounds = new Rect();
+ private final Rect mDefaultDisplayBounds = new Rect();
+
+ private Handler mUpdateHandler;
+ private boolean mIsInOneHanded;
+ private int mEnterExitAnimationDurationMs;
+
+ @VisibleForTesting
+ HashMap<DisplayAreaInfo, SurfaceControl> mDisplayAreaMap = new HashMap();
+ private DisplayController mDisplayController;
+ private OneHandedAnimationController mAnimationController;
+ private OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+ private OneHandedTutorialHandler mTutorialHandler;
+ private List<OneHandedTransitionCallback> mTransitionCallbacks = new ArrayList<>();
+
+ @VisibleForTesting
+ OneHandedAnimationCallback mOneHandedAnimationCallback =
+ new OneHandedAnimationCallback() {
+ @Override
+ public void onOneHandedAnimationStart(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ }
+
+ @Override
+ public void onOneHandedAnimationEnd(SurfaceControl.Transaction tx,
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ mAnimationController.removeAnimator(animator.getLeash());
+ if (mAnimationController.isAnimatorsConsumed()) {
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_FINISH,
+ obtainArgsFromAnimator(animator)));
+ }
+ }
+
+ @Override
+ public void onOneHandedAnimationCancel(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ mAnimationController.removeAnimator(animator.getLeash());
+ if (mAnimationController.isAnimatorsConsumed()) {
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_FINISH,
+ obtainArgsFromAnimator(animator)));
+ }
+ }
+ };
+
+ @SuppressWarnings("unchecked")
+ private Handler.Callback mUpdateCallback = (msg) -> {
+ SomeArgs args = (SomeArgs) msg.obj;
+ final Rect currentBounds = args.arg1 != null ? (Rect) args.arg1 : mDefaultDisplayBounds;
+ final int yOffset = args.argi2;
+ final int direction = args.argi3;
+
+ switch (msg.what) {
+ case MSG_RESET_IMMEDIATE:
+ resetWindowsOffset();
+ mDefaultDisplayBounds.set(currentBounds);
+ mLastVisualDisplayBounds.set(currentBounds);
+ finishOffset(0, TRANSITION_DIRECTION_EXIT);
+ break;
+ case MSG_OFFSET_ANIMATE:
+ final Rect toBounds = new Rect(mDefaultDisplayBounds.left,
+ mDefaultDisplayBounds.top + yOffset,
+ mDefaultDisplayBounds.right,
+ mDefaultDisplayBounds.bottom + yOffset);
+ offsetWindows(currentBounds, toBounds, direction, mEnterExitAnimationDurationMs);
+ break;
+ case MSG_OFFSET_FINISH:
+ finishOffset(yOffset, direction);
+ break;
+ }
+ args.recycle();
+ return true;
+ };
+
+ /**
+ * Constructor of OneHandedDisplayAreaOrganizer
+ */
+ public OneHandedDisplayAreaOrganizer(Context context,
+ DisplayController displayController,
+ OneHandedAnimationController animationController,
+ OneHandedTutorialHandler tutorialHandler) {
+ mUpdateHandler = new Handler(OneHandedThread.get().getLooper(), mUpdateCallback);
+ mAnimationController = animationController;
+ mDisplayController = displayController;
+ mDefaultDisplayBounds.set(getDisplayBounds());
+ mLastVisualDisplayBounds.set(getDisplayBounds());
+ mEnterExitAnimationDurationMs =
+ SystemProperties.getInt(ONE_HANDED_MODE_TRANSLATE_ANIMATION_DURATION, 300);
+ mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+ mTutorialHandler = tutorialHandler;
+ }
+
+ @Override
+ public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
+ @NonNull SurfaceControl leash) {
+ Objects.requireNonNull(displayAreaInfo, "displayAreaInfo must not be null");
+ Objects.requireNonNull(leash, "leash must not be null");
+
+ if (displayAreaInfo.featureId != FEATURE_ONE_HANDED) {
+ Log.w(TAG, "Bypass onDisplayAreaAppeared()! displayAreaInfo=" + displayAreaInfo);
+ return;
+ }
+ // mDefaultDisplayBounds may out of date after removeDisplayChangingController()
+ mDefaultDisplayBounds.set(getDisplayBounds());
+ mDisplayAreaMap.put(displayAreaInfo, leash);
+ }
+
+ @Override
+ public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) {
+ Objects.requireNonNull(displayAreaInfo,
+ "Requires valid displayArea, and displayArea must not be null");
+
+ if (!mDisplayAreaMap.containsKey(displayAreaInfo)) {
+ Log.w(TAG, "Unrecognized token: " + displayAreaInfo.token);
+ return;
+ }
+ mDisplayAreaMap.remove(displayAreaInfo);
+ }
+
+ @Override
+ public void unregisterOrganizer() {
+ super.unregisterOrganizer();
+ resetWindowsOffset();
+
+ // Ensure all cached instance are cleared after resetWindowsOffset
+ mUpdateHandler.post(() -> {
+ if (mDisplayAreaMap != null && !mDisplayAreaMap.isEmpty()) {
+ mDisplayAreaMap.clear();
+ }
+ });
+ }
+
+ /**
+ * Handler for display rotation changes by below policy which
+ * handles 90 degree display rotation changes {@link Surface.Rotation}
+ *
+ */
+ public void onRotateDisplay(int fromRotation, int toRotation) {
+ // Stop one handed without animation and reset cropped size immediately
+ final Rect newBounds = new Rect(mDefaultDisplayBounds);
+ final boolean isOrientationDiff = Math.abs(fromRotation - toRotation) % 2 == 1;
+
+ if (isOrientationDiff) {
+ newBounds.set(newBounds.left, newBounds.top, newBounds.bottom, newBounds.right);
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = newBounds;
+ args.argi1 = 0 /* xOffset */;
+ args.argi2 = 0 /* yOffset */;
+ args.argi3 = TRANSITION_DIRECTION_EXIT;
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESET_IMMEDIATE, args));
+ }
+ }
+
+ /**
+ * Offset the windows by a given offset on Y-axis, triggered also from screen rotation.
+ * Directly perform manipulation/offset on the leash.
+ */
+ public void scheduleOffset(int xOffset, int yOffset) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = getLastVisualDisplayBounds();
+ args.argi1 = xOffset;
+ args.argi2 = yOffset;
+ args.argi3 = yOffset > 0 ? TRANSITION_DIRECTION_TRIGGER : TRANSITION_DIRECTION_EXIT;
+ mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_ANIMATE, args));
+ }
+
+ private void offsetWindows(Rect fromBounds, Rect toBounds, int direction, int durationMs) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException("Callers should call scheduleOffset() instead of this "
+ + "directly");
+ }
+ mDisplayAreaMap.forEach(
+ (key, leash) -> animateWindows(leash, fromBounds, toBounds, direction,
+ durationMs));
+ }
+
+ private void resetWindowsOffset() {
+ mUpdateHandler.post(() -> {
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
+ mDisplayAreaMap.forEach(
+ (key, leash) -> {
+ final OneHandedAnimationController.OneHandedTransitionAnimator animator =
+ mAnimationController.getAnimatorMap().remove(leash);
+ if (animator != null && animator.isRunning()) {
+ animator.cancel();
+ }
+ tx.setPosition(leash, 0, 0)
+ .setWindowCrop(leash, -1/* reset */, -1/* reset */);
+ });
+ tx.apply();
+ });
+ }
+
+ private void animateWindows(SurfaceControl leash, Rect fromBounds, Rect toBounds,
+ @OneHandedAnimationController.TransitionDirection int direction, int durationMs) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException("Callers should call scheduleOffset() instead of "
+ + "this directly");
+ }
+ mUpdateHandler.post(() -> {
+ final OneHandedAnimationController.OneHandedTransitionAnimator animator =
+ mAnimationController.getAnimator(leash, fromBounds, toBounds);
+ if (animator != null) {
+ animator.setTransitionDirection(direction)
+ .setOneHandedAnimationCallbacks(mOneHandedAnimationCallback)
+ .setOneHandedAnimationCallbacks(mTutorialHandler.getAnimationCallback())
+ .setDuration(durationMs)
+ .start();
+ }
+ });
+ }
+
+ private void finishOffset(int offset,
+ @OneHandedAnimationController.TransitionDirection int direction) {
+ if (Looper.myLooper() != mUpdateHandler.getLooper()) {
+ throw new RuntimeException(
+ "Callers should call scheduleOffset() instead of this directly.");
+ }
+ // Only finishOffset() can update mIsInOneHanded to ensure the state is handle in sequence,
+ // the flag *MUST* be updated before dispatch mTransitionCallbacks
+ mIsInOneHanded = (offset > 0 || direction == TRANSITION_DIRECTION_TRIGGER);
+ mLastVisualDisplayBounds.offsetTo(0,
+ direction == TRANSITION_DIRECTION_TRIGGER ? offset : 0);
+ for (int i = mTransitionCallbacks.size() - 1; i >= 0; i--) {
+ final OneHandedTransitionCallback callback = mTransitionCallbacks.get(i);
+ if (direction == TRANSITION_DIRECTION_TRIGGER) {
+ callback.onStartFinished(getLastVisualDisplayBounds());
+ } else {
+ callback.onStopFinished(getLastVisualDisplayBounds());
+ }
+ }
+ }
+
+ /**
+ * The latest state of one handed mode
+ *
+ * @return true Currently is in one handed mode, otherwise is not in one handed mode
+ */
+ public boolean isInOneHanded() {
+ return mIsInOneHanded;
+ }
+
+ /**
+ * The latest visual bounds of displayArea translated
+ *
+ * @return Rect latest finish_offset
+ */
+ public Rect getLastVisualDisplayBounds() {
+ return mLastVisualDisplayBounds;
+ }
+
+ @Nullable
+ private Rect getDisplayBounds() {
+ Point realSize = new Point(0, 0);
+ if (mDisplayController != null && mDisplayController.getDisplay(DEFAULT_DISPLAY) != null) {
+ mDisplayController.getDisplay(DEFAULT_DISPLAY).getRealSize(realSize);
+ }
+ return new Rect(0, 0, realSize.x, realSize.y);
+ }
+
+ @VisibleForTesting
+ Handler getUpdateHandler() {
+ return mUpdateHandler;
+ }
+
+ /**
+ * Register transition callback
+ */
+ public void registerTransitionCallback(OneHandedTransitionCallback callback) {
+ mTransitionCallbacks.add(callback);
+ }
+
+ private SomeArgs obtainArgsFromAnimator(
+ OneHandedAnimationController.OneHandedTransitionAnimator animator) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = animator.getDestinationBounds();
+ args.argi1 = 0 /* xOffset */;
+ args.argi2 = animator.getDestinationOffset();
+ args.argi3 = animator.getTransitionDirection();
+ return args;
+ }
+
+ void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "mIsInOneHanded=");
+ pw.println(mIsInOneHanded);
+ pw.print(innerPrefix + "mDisplayAreaMap=");
+ pw.println(mDisplayAreaMap);
+ pw.print(innerPrefix + "mDefaultDisplayBounds=");
+ pw.println(mDefaultDisplayBounds);
+ pw.print(innerPrefix + "mLastVisualDisplayBounds=");
+ pw.println(mLastVisualDisplayBounds);
+ pw.print(innerPrefix + "getDisplayBounds()=");
+ pw.println(getDisplayBounds());
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java
new file mode 100644
index 0000000..79ddd2b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedEvents.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.UiEventLoggerImpl;
+
+/**
+ * Interesting events related to the One-Handed.
+ */
+public class OneHandedEvents {
+ private static final String TAG = "OneHandedEvents";
+
+ public static Callback sCallback;
+ @VisibleForTesting
+ static UiEventLogger sUiEventLogger = new UiEventLoggerImpl();
+
+ /**
+ * One-Handed event types
+ */
+ // Triggers
+ public static final int EVENT_ONE_HANDED_TRIGGER_GESTURE_IN = 0;
+ public static final int EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT = 1;
+ public static final int EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT = 2;
+ public static final int EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT = 3;
+ public static final int EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT = 4;
+ public static final int EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT = 5;
+ public static final int EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT = 6;
+ public static final int EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT = 7;
+ // Settings toggles
+ public static final int EVENT_ONE_HANDED_SETTINGS_ENABLED_ON = 8;
+ public static final int EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF = 9;
+ public static final int EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON = 10;
+ public static final int EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF = 11;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON = 12;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF = 13;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER = 14;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4 = 15;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8 = 16;
+ public static final int EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12 = 17;
+
+ private static final String[] EVENT_TAGS = {
+ "one_handed_trigger_gesture_in",
+ "one_handed_trigger_gesture_out",
+ "one_handed_trigger_overspace_out",
+ "one_handed_trigger_pop_ime_out",
+ "one_handed_trigger_rotation_out",
+ "one_handed_trigger_app_taps_out",
+ "one_handed_trigger_timeout_out",
+ "one_handed_trigger_screen_off_out",
+ "one_handed_settings_enabled_on",
+ "one_handed_settings_enabled_off",
+ "one_handed_settings_app_taps_exit_on",
+ "one_handed_settings_app_taps_exit_off",
+ "one_handed_settings_timeout_exit_on",
+ "one_handed_settings_timeout_exit_off",
+ "one_handed_settings_timeout_seconds_never",
+ "one_handed_settings_timeout_seconds_4",
+ "one_handed_settings_timeout_seconds_8",
+ "one_handed_settings_timeout_seconds_12"
+ };
+
+ /**
+ * Events definition that related to One-Handed gestures.
+ */
+ @VisibleForTesting
+ public enum OneHandedTriggerEvent implements UiEventLogger.UiEventEnum {
+ INVALID(0),
+ @UiEvent(doc = "One-Handed trigger in via NavigationBar area")
+ ONE_HANDED_TRIGGER_GESTURE_IN(366),
+
+ @UiEvent(doc = "One-Handed trigger out via NavigationBar area")
+ ONE_HANDED_TRIGGER_GESTURE_OUT(367),
+
+ @UiEvent(doc = "One-Handed trigger out via Overspace area")
+ ONE_HANDED_TRIGGER_OVERSPACE_OUT(368),
+
+ @UiEvent(doc = "One-Handed trigger out while IME pop up")
+ ONE_HANDED_TRIGGER_POP_IME_OUT(369),
+
+ @UiEvent(doc = "One-Handed trigger out while device rotation to landscape")
+ ONE_HANDED_TRIGGER_ROTATION_OUT(370),
+
+ @UiEvent(doc = "One-Handed trigger out when an Activity is launching")
+ ONE_HANDED_TRIGGER_APP_TAPS_OUT(371),
+
+ @UiEvent(doc = "One-Handed trigger out when one-handed mode times up")
+ ONE_HANDED_TRIGGER_TIMEOUT_OUT(372),
+
+ @UiEvent(doc = "One-Handed trigger out when screen off")
+ ONE_HANDED_TRIGGER_SCREEN_OFF_OUT(449);
+
+ private final int mId;
+
+ OneHandedTriggerEvent(int id) {
+ mId = id;
+ }
+
+ public int getId() {
+ return mId;
+ }
+ }
+
+ /**
+ * Events definition that related to Settings toggles.
+ */
+ @VisibleForTesting
+ public enum OneHandedSettingsTogglesEvent implements UiEventLogger.UiEventEnum {
+ INVALID(0),
+ @UiEvent(doc = "One-Handed mode enabled toggle on")
+ ONE_HANDED_SETTINGS_TOGGLES_ENABLED_ON(356),
+
+ @UiEvent(doc = "One-Handed mode enabled toggle off")
+ ONE_HANDED_SETTINGS_TOGGLES_ENABLED_OFF(357),
+
+ @UiEvent(doc = "One-Handed mode app-taps-exit toggle on")
+ ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_ON(358),
+
+ @UiEvent(doc = "One-Handed mode app-taps-exit toggle off")
+ ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_OFF(359),
+
+ @UiEvent(doc = "One-Handed mode timeout-exit toggle on")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_ON(360),
+
+ @UiEvent(doc = "One-Handed mode timeout-exit toggle off")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_OFF(361),
+
+ @UiEvent(doc = "One-Handed mode timeout value changed to never timeout")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_NEVER(362),
+
+ @UiEvent(doc = "One-Handed mode timeout value changed to 4 seconds")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_4(363),
+
+ @UiEvent(doc = "One-Handed mode timeout value changed to 8 seconds")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_8(364),
+
+ @UiEvent(doc = "One-Handed mode timeout value changed to 12 seconds")
+ ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_12(365);
+
+ private final int mId;
+
+ OneHandedSettingsTogglesEvent(int id) {
+ mId = id;
+ }
+
+ public int getId() {
+ return mId;
+ }
+ }
+
+
+ /**
+ * Logs an event to the system log, to sCallback if present, and to the logEvent destinations.
+ * @param tag One of the EVENT_* codes above.
+ */
+ public static void writeEvent(int tag) {
+ final long time = System.currentTimeMillis();
+ logEvent(tag);
+ if (sCallback != null) {
+ sCallback.writeEvent(time, tag);
+ }
+ }
+
+ /**
+ * Logs an event to the UiEvent (statsd) logging.
+ * @param event One of the EVENT_* codes above.
+ * @return String a readable description of the event. Begins "writeEvent <tag_description>"
+ * if the tag is valid.
+ */
+ public static String logEvent(int event) {
+ if (event >= EVENT_TAGS.length) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder("writeEvent ").append(EVENT_TAGS[event]);
+ switch (event) {
+ // Triggers
+ case EVENT_ONE_HANDED_TRIGGER_GESTURE_IN:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_GESTURE_IN);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_GESTURE_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_OVERSPACE_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_POP_IME_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_ROTATION_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_APP_TAPS_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_TIMEOUT_OUT);
+ break;
+ case EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT:
+ sUiEventLogger.log(OneHandedTriggerEvent.ONE_HANDED_TRIGGER_SCREEN_OFF_OUT);
+ break;
+ // Settings
+ case EVENT_ONE_HANDED_SETTINGS_ENABLED_ON:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_ENABLED_ON);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_ENABLED_OFF);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_ON);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_APP_TAPS_EXIT_OFF);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_ON);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_EXIT_OFF);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_NEVER);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_4);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_8);
+ break;
+ case EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12:
+ sUiEventLogger.log(OneHandedSettingsTogglesEvent
+ .ONE_HANDED_SETTINGS_TOGGLES_TIMEOUT_SECONDS_12);
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+ return sb.toString();
+ }
+
+ /**
+ * An interface for logging an event to the system log, if Callback present.
+ */
+ public interface Callback {
+ /**
+ *
+ * @param time System current time.
+ * @param tag Event tag.
+ */
+ void writeEvent(long time, int tag);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java
new file mode 100644
index 0000000..3b1e6cb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedGestureHandler.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.hardware.input.InputManager;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Display;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.ViewConfiguration;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.DisplayChangeController;
+import com.android.wm.shell.common.DisplayController;
+
+/**
+ * The class manage swipe up and down gesture for 3-Button mode navigation,
+ * others(e.g, 2-button, full gesture mode) are handled by Launcher quick steps.
+ */
+public class OneHandedGestureHandler implements OneHandedTransitionCallback,
+ DisplayChangeController.OnDisplayChangingListener {
+ private static final String TAG = "OneHandedGestureHandler";
+ private static final boolean DEBUG_GESTURE = false;
+
+ private static final int ANGLE_MAX = 150;
+ private static final int ANGLE_MIN = 30;
+ private final float mDragDistThreshold;
+ private final float mSquaredSlop;
+ private final PointF mDownPos = new PointF();
+ private final PointF mLastPos = new PointF();
+ private final PointF mStartDragPos = new PointF();
+ private boolean mPassedSlop;
+
+ private boolean mAllowGesture;
+ private boolean mIsEnabled;
+ private int mNavGestureHeight;
+ private boolean mIsThreeButtonModeEnabled;
+ private int mRotation = Surface.ROTATION_0;
+
+ @VisibleForTesting
+ InputMonitor mInputMonitor;
+ @VisibleForTesting
+ InputEventReceiver mInputEventReceiver;
+ private DisplayController mDisplayController;
+ @VisibleForTesting
+ @Nullable
+ OneHandedGestureEventCallback mGestureEventCallback;
+ private Rect mGestureRegion = new Rect();
+
+ /**
+ * Constructor of OneHandedGestureHandler, we only handle the gesture of
+ * {@link Display#DEFAULT_DISPLAY}
+ *
+ * @param context {@link Context}
+ * @param displayController {@link DisplayController}
+ */
+ public OneHandedGestureHandler(Context context, DisplayController displayController) {
+ mDisplayController = displayController;
+ displayController.addDisplayChangingController(this);
+ mNavGestureHeight = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.navigation_bar_gesture_height);
+ mDragDistThreshold = context.getResources().getDimensionPixelSize(
+ R.dimen.gestures_onehanded_drag_threshold);
+ final float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mSquaredSlop = slop * slop;
+ updateIsEnabled();
+ }
+
+ /**
+ * Notified by {@link OneHandedController}, when user update settings of Enabled or Disabled
+ *
+ * @param isEnabled is one handed settings enabled or not
+ */
+ public void onOneHandedEnabled(boolean isEnabled) {
+ if (DEBUG_GESTURE) {
+ Log.d(TAG, "onOneHandedEnabled, isEnabled = " + isEnabled);
+ }
+ mIsEnabled = isEnabled;
+ updateIsEnabled();
+ }
+
+ void onThreeButtonModeEnabled(boolean isEnabled) {
+ mIsThreeButtonModeEnabled = isEnabled;
+ updateIsEnabled();
+ }
+
+ /**
+ * Register {@link OneHandedGestureEventCallback} to receive onStart(), onStop() callback
+ */
+ public void setGestureEventListener(OneHandedGestureEventCallback callback) {
+ mGestureEventCallback = callback;
+ }
+
+ private void onMotionEvent(MotionEvent ev) {
+ int action = ev.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mAllowGesture = isWithinTouchRegion(ev.getX(), ev.getY())
+ && mRotation == Surface.ROTATION_0;
+ if (mAllowGesture) {
+ mDownPos.set(ev.getX(), ev.getY());
+ mLastPos.set(mDownPos);
+ }
+ if (DEBUG_GESTURE) {
+ Log.d(TAG, "ACTION_DOWN, mDownPos=" + mDownPos + ", mAllowGesture="
+ + mAllowGesture);
+ }
+ } else if (mAllowGesture) {
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ mLastPos.set(ev.getX(), ev.getY());
+ if (!mPassedSlop) {
+ if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
+ > mSquaredSlop) {
+ mStartDragPos.set(mLastPos.x, mLastPos.y);
+ if (isValidStartAngle(
+ mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)
+ || isValidExitAngle(
+ mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) {
+ mPassedSlop = true;
+ mInputMonitor.pilferPointers();
+ }
+ }
+ } else {
+ float distance = (float) Math.hypot(mLastPos.x - mDownPos.x,
+ mLastPos.y - mDownPos.y);
+ if (distance > mDragDistThreshold) {
+ mGestureEventCallback.onStop();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mLastPos.y >= mDownPos.y && mPassedSlop) {
+ mGestureEventCallback.onStart();
+ }
+ mPassedSlop = false;
+ mAllowGesture = false;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mPassedSlop = false;
+ mAllowGesture = false;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private void disposeInputChannel() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ mInputMonitor = null;
+ }
+ }
+
+ private boolean isWithinTouchRegion(float x, float y) {
+ if (DEBUG_GESTURE) {
+ Log.d(TAG, "isWithinTouchRegion(), mGestureRegion=" + mGestureRegion + ", downX=" + x
+ + ", downY=" + y);
+ }
+ return mGestureRegion.contains(Math.round(x), Math.round(y));
+ }
+
+ private void updateIsEnabled() {
+ disposeInputChannel();
+
+ if (mIsEnabled && mIsThreeButtonModeEnabled) {
+ final Point displaySize = new Point();
+ if (mDisplayController != null) {
+ final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY);
+ if (display != null) {
+ display.getRealSize(displaySize);
+ }
+ }
+ // Register input event receiver to monitor the touch region of NavBar gesture height
+ mGestureRegion.set(0, displaySize.y - mNavGestureHeight, displaySize.x,
+ displaySize.y);
+ mInputMonitor = InputManager.getInstance().monitorGestureInput(
+ "onehanded-gesture-offset", DEFAULT_DISPLAY);
+ mInputEventReceiver = new EventReceiver(
+ mInputMonitor.getInputChannel(), Looper.getMainLooper());
+ }
+ }
+
+ private void onInputEvent(InputEvent ev) {
+ if (ev instanceof MotionEvent) {
+ onMotionEvent((MotionEvent) ev);
+ }
+ }
+
+ @Override
+ public void onRotateDisplay(int displayId, int fromRotation, int toRotation,
+ WindowContainerTransaction t) {
+ mRotation = toRotation;
+ }
+
+ private class EventReceiver extends InputEventReceiver {
+ EventReceiver(InputChannel channel, Looper looper) {
+ super(channel, looper);
+ }
+
+ public void onInputEvent(InputEvent event) {
+ OneHandedGestureHandler.this.onInputEvent(event);
+ finishInputEvent(event, true);
+ }
+ }
+
+ private boolean isValidStartAngle(float deltaX, float deltaY) {
+ final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+ return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN);
+ }
+
+ private boolean isValidExitAngle(float deltaX, float deltaY) {
+ final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+ return angle > ANGLE_MIN && angle < ANGLE_MAX;
+ }
+
+ private float squaredHypot(float x, float y) {
+ return x * x + y * y;
+ }
+
+ /**
+ * The touch(gesture) events to notify {@link OneHandedController} start or stop one handed
+ */
+ public interface OneHandedGestureEventCallback {
+ /**
+ * Handles the start gesture.
+ */
+ void onStart();
+
+ /**
+ * Handles the exit gesture.
+ */
+ void onStop();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java
new file mode 100644
index 0000000..4d66f29
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import android.annotation.IntDef;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * APIs for querying or updating one handed settings .
+ */
+public final class OneHandedSettingsUtil {
+ private static final String TAG = "OneHandedSettingsUtil";
+
+ @IntDef(prefix = {"ONE_HANDED_TIMEOUT_"}, value = {
+ ONE_HANDED_TIMEOUT_NEVER,
+ ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS,
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS,
+ ONE_HANDED_TIMEOUT_LONG_IN_SECONDS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OneHandedTimeout {
+ }
+
+ /**
+ * Never stop one handed automatically
+ */
+ public static final int ONE_HANDED_TIMEOUT_NEVER = 0;
+ /**
+ * Auto stop one handed in {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS}
+ */
+ public static final int ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS = 4;
+ /**
+ * Auto stop one handed in {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS}
+ */
+ public static final int ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS = 8;
+ /**
+ * Auto stop one handed in {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_LONG_IN_SECONDS}
+ */
+ public static final int ONE_HANDED_TIMEOUT_LONG_IN_SECONDS = 12;
+
+ /**
+ * Register one handed preference settings observer
+ *
+ * @param key Setting key to monitor in observer
+ * @param resolver ContentResolver of context
+ * @param observer Observer from caller
+ * @return uri key for observing
+ */
+ public static Uri registerSettingsKeyObserver(String key, ContentResolver resolver,
+ ContentObserver observer) {
+ Uri uriKey = null;
+ uriKey = Settings.Secure.getUriFor(key);
+ if (resolver != null && uriKey != null) {
+ resolver.registerContentObserver(uriKey, false, observer);
+ }
+ return uriKey;
+ }
+
+ /**
+ * Unregister one handed preference settings observer
+ *
+ * @param resolver ContentResolver of context
+ * @param observer preference key change observer
+ */
+ public static void unregisterSettingsKeyObserver(ContentResolver resolver,
+ ContentObserver observer) {
+ if (resolver != null) {
+ resolver.unregisterContentObserver(observer);
+ }
+ }
+
+ /**
+ * Query one handed enable or disable flag from Settings provider.
+ *
+ * @return enable or disable one handed mode flag.
+ */
+ public static boolean getSettingsOneHandedModeEnabled(ContentResolver resolver) {
+ return Settings.Secure.getInt(resolver,
+ Settings.Secure.ONE_HANDED_MODE_ENABLED, 0 /* Disabled */) == 1;
+ }
+
+ /**
+ * Query taps app to exit config from Settings provider.
+ *
+ * @return enable or disable taps app exit.
+ */
+ public static boolean getSettingsTapsAppToExit(ContentResolver resolver) {
+ return Settings.Secure.getInt(resolver,
+ Settings.Secure.TAPS_APP_TO_EXIT, 0) == 1;
+ }
+
+ /**
+ * Query timeout value from Settings provider.
+ * Default is {@link OneHandedSettingsUtil#ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS}
+ *
+ * @return timeout value in seconds.
+ */
+ public static @OneHandedTimeout int getSettingsOneHandedModeTimeout(ContentResolver resolver) {
+ return Settings.Secure.getInt(resolver,
+ Settings.Secure.ONE_HANDED_MODE_TIMEOUT, ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ }
+
+ /**
+ * Returns whether swipe bottom to notification gesture enabled or not.
+ */
+ public static boolean getSettingsSwipeToNotificationEnabled(ContentResolver resolver) {
+ return Settings.Secure.getInt(resolver,
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 0) == 1;
+ }
+
+ protected static void dump(PrintWriter pw, String prefix, ContentResolver resolver) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.print(innerPrefix + "isOneHandedModeEnable=");
+ pw.println(getSettingsOneHandedModeEnabled(resolver));
+ pw.print(innerPrefix + "oneHandedTimeOut=");
+ pw.println(getSettingsOneHandedModeTimeout(resolver));
+ pw.print(innerPrefix + "tapsAppToExit=");
+ pw.println(getSettingsTapsAppToExit(resolver));
+ }
+
+ private OneHandedSettingsUtil() {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java
new file mode 100644
index 0000000..e7010db
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSurfaceTransactionHelper.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.R;
+
+/**
+ * Abstracts the common operations on {@link SurfaceControl.Transaction} for OneHanded transition.
+ */
+public class OneHandedSurfaceTransactionHelper {
+ private final boolean mEnableCornerRadius;
+ private final float mCornerRadius;
+
+ public OneHandedSurfaceTransactionHelper(Context context) {
+ final Resources res = context.getResources();
+ mCornerRadius = res.getDimension(com.android.internal.R.dimen.rounded_corner_radius);
+ mEnableCornerRadius = res.getBoolean(R.bool.config_one_handed_enable_round_corner);
+ }
+
+ /**
+ * Operates the translation (setPosition) on a given transaction and leash
+ *
+ * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining
+ */
+ OneHandedSurfaceTransactionHelper translate(SurfaceControl.Transaction tx, SurfaceControl leash,
+ float offset) {
+ tx.setPosition(leash, 0, offset);
+ return this;
+ }
+
+ /**
+ * Operates the alpha on a given transaction and leash
+ *
+ * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining
+ */
+ OneHandedSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash,
+ float alpha) {
+ tx.setAlpha(leash, alpha);
+ return this;
+ }
+
+ /**
+ * Operates the crop (setMatrix) on a given transaction and leash
+ *
+ * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining
+ */
+ OneHandedSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect destinationBounds) {
+ tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height())
+ .setPosition(leash, destinationBounds.left, destinationBounds.top);
+ return this;
+ }
+
+ /**
+ * Operates the round corner radius on a given transaction and leash
+ *
+ * @return same {@link OneHandedSurfaceTransactionHelper} instance for method chaining
+ */
+ OneHandedSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash) {
+ if (mEnableCornerRadius) {
+ tx.setCornerRadius(leash, mCornerRadius);
+ }
+ return this;
+ }
+
+ interface SurfaceControlTransactionFactory {
+ SurfaceControl.Transaction getTransaction();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java
new file mode 100644
index 0000000..24d33ed
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedThread.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+/**
+ * Similar to {@link com.android.internal.os.BackgroundThread}, this is a shared singleton
+ * foreground thread for each process for updating one handed.
+ */
+public class OneHandedThread extends HandlerThread {
+ private static OneHandedThread sInstance;
+ private static Handler sHandler;
+
+ private OneHandedThread() {
+ super("OneHanded");
+ }
+
+ private static void ensureThreadLocked() {
+ if (sInstance == null) {
+ sInstance = new OneHandedThread();
+ sInstance.start();
+ sHandler = new Handler(sInstance.getLooper());
+ }
+ }
+
+ /**
+ * @return the static update thread instance
+ */
+ public static OneHandedThread get() {
+ synchronized (OneHandedThread.class) {
+ ensureThreadLocked();
+ return sInstance;
+ }
+ }
+
+ /**
+ * @return the static update thread handler instance
+ */
+ public static Handler getHandler() {
+ synchronized (OneHandedThread.class) {
+ ensureThreadLocked();
+ return sHandler;
+ }
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java
new file mode 100644
index 0000000..9c97cd7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandler.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Timeout handler for stop one handed mode operations.
+ */
+public class OneHandedTimeoutHandler {
+ private static final String TAG = "OneHandedTimeoutHandler";
+ private static boolean sIsDragging = false;
+ // Default timeout is ONE_HANDED_TIMEOUT_MEDIUM
+ private static @OneHandedSettingsUtil.OneHandedTimeout int sTimeout =
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+ private static long sTimeoutMs = TimeUnit.SECONDS.toMillis(sTimeout);
+ private static OneHandedTimeoutHandler sInstance;
+ private static List<TimeoutListener> sListeners = new ArrayList<>();
+
+ @VisibleForTesting
+ static final int ONE_HANDED_TIMEOUT_STOP_MSG = 1;
+ @VisibleForTesting
+ static Handler sHandler;
+
+ /**
+ * Get the current config of timeout
+ *
+ * @return timeout of current config
+ */
+ public @OneHandedSettingsUtil.OneHandedTimeout int getTimeout() {
+ return sTimeout;
+ }
+
+ /**
+ * Listens for notify timeout events
+ */
+ public interface TimeoutListener {
+ /**
+ * Called whenever the config time out
+ *
+ * @param timeoutTime The time in seconds to trigger timeout
+ */
+ void onTimeout(int timeoutTime);
+ }
+
+ /**
+ * Set the specific timeout of {@link OneHandedSettingsUtil.OneHandedTimeout}
+ */
+ public static void setTimeout(@OneHandedSettingsUtil.OneHandedTimeout int timeout) {
+ sTimeout = timeout;
+ sTimeoutMs = TimeUnit.SECONDS.toMillis(sTimeout);
+ resetTimer();
+ }
+
+ /**
+ * Reset the timer when one handed trigger or user is operating in some conditions
+ */
+ public static void removeTimer() {
+ sHandler.removeMessages(ONE_HANDED_TIMEOUT_STOP_MSG);
+ }
+
+ /**
+ * Reset the timer when one handed trigger or user is operating in some conditions
+ */
+ public static void resetTimer() {
+ removeTimer();
+ if (sTimeout == OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) {
+ return;
+ }
+ if (sTimeout != OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) {
+ sHandler.sendEmptyMessageDelayed(ONE_HANDED_TIMEOUT_STOP_MSG, sTimeoutMs);
+ }
+ }
+
+ /**
+ * Register timeout listener to receive time out events
+ *
+ * @param listener the listener be sent events when times up
+ */
+ public static void registerTimeoutListener(TimeoutListener listener) {
+ sListeners.add(listener);
+ }
+
+ /**
+ * Private constructor due to Singleton pattern
+ */
+ private OneHandedTimeoutHandler() {
+ }
+
+ /**
+ * Singleton pattern to get {@link OneHandedTimeoutHandler} instance
+ *
+ * @return the static update thread instance
+ */
+ public static OneHandedTimeoutHandler get() {
+ synchronized (OneHandedTimeoutHandler.class) {
+ if (sInstance == null) {
+ sInstance = new OneHandedTimeoutHandler();
+ }
+ if (sHandler == null) {
+ sHandler = new Handler(Looper.myLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == ONE_HANDED_TIMEOUT_STOP_MSG) {
+ onStop();
+ }
+ }
+ };
+ if (sTimeout != OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER) {
+ sHandler.sendEmptyMessageDelayed(ONE_HANDED_TIMEOUT_STOP_MSG, sTimeoutMs);
+ }
+ }
+ return sInstance;
+ }
+ }
+
+ private static void onStop() {
+ for (int i = sListeners.size() - 1; i >= 0; i--) {
+ final TimeoutListener listener = sListeners.get(i);
+ listener.onTimeout(sTimeout);
+ }
+ }
+
+ void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "sTimeout=");
+ pw.println(sTimeout);
+ pw.print(innerPrefix + "sListeners=");
+ pw.println(sListeners);
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java
new file mode 100644
index 0000000..721382d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTouchHandler.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.graphics.Rect;
+import android.hardware.input.InputManager;
+import android.os.Looper;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages all the touch handling for One Handed on the Phone, including user tap outside region
+ * to exit, reset timer when user is in one-handed mode.
+ * Refer {@link OneHandedGestureHandler} to see start and stop one handed gesture
+ */
+public class OneHandedTouchHandler implements OneHandedTransitionCallback {
+ private static final String TAG = "OneHandedTouchHandler";
+ private final Rect mLastUpdatedBounds = new Rect();
+
+ private OneHandedTimeoutHandler mTimeoutHandler;
+
+ @VisibleForTesting
+ InputMonitor mInputMonitor;
+ @VisibleForTesting
+ InputEventReceiver mInputEventReceiver;
+ @VisibleForTesting
+ OneHandedTouchEventCallback mTouchEventCallback;
+
+ private boolean mIsEnabled;
+ private boolean mIsOnStopTransitioning;
+ private boolean mIsInOutsideRegion;
+
+ public OneHandedTouchHandler() {
+ mTimeoutHandler = OneHandedTimeoutHandler.get();
+ updateIsEnabled();
+ }
+
+ /**
+ * Notified by {@link OneHandedController}, when user update settings of Enabled or Disabled
+ *
+ * @param isEnabled is one handed settings enabled or not
+ */
+ public void onOneHandedEnabled(boolean isEnabled) {
+ mIsEnabled = isEnabled;
+ updateIsEnabled();
+ }
+
+ /**
+ * Register {@link OneHandedTouchEventCallback} to receive onEnter(), onExit() callback
+ */
+ public void registerTouchEventListener(OneHandedTouchEventCallback callback) {
+ mTouchEventCallback = callback;
+ }
+
+ private boolean onMotionEvent(MotionEvent ev) {
+ mIsInOutsideRegion = isWithinTouchOutsideRegion(ev.getX(), ev.getY());
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE: {
+ if (!mIsInOutsideRegion) {
+ mTimeoutHandler.resetTimer();
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ mTimeoutHandler.resetTimer();
+ if (mIsInOutsideRegion && !mIsOnStopTransitioning) {
+ mTouchEventCallback.onStop();
+ mIsOnStopTransitioning = true;
+ }
+ // Reset flag for next operation
+ mIsInOutsideRegion = false;
+ break;
+ }
+ }
+ return true;
+ }
+
+ private void disposeInputChannel() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ mInputMonitor = null;
+ }
+ }
+
+ private boolean isWithinTouchOutsideRegion(float x, float y) {
+ return Math.round(y) < mLastUpdatedBounds.top;
+ }
+
+ private void onInputEvent(InputEvent ev) {
+ if (ev instanceof MotionEvent) {
+ onMotionEvent((MotionEvent) ev);
+ }
+ }
+
+ private void updateIsEnabled() {
+ disposeInputChannel();
+ if (mIsEnabled) {
+ mInputMonitor = InputManager.getInstance().monitorGestureInput(
+ "onehanded-touch", DEFAULT_DISPLAY);
+ mInputEventReceiver = new EventReceiver(
+ mInputMonitor.getInputChannel(), Looper.getMainLooper());
+ }
+ }
+
+ @Override
+ public void onStartFinished(Rect bounds) {
+ mLastUpdatedBounds.set(bounds);
+ }
+
+ @Override
+ public void onStopFinished(Rect bounds) {
+ mLastUpdatedBounds.set(bounds);
+ mIsOnStopTransitioning = false;
+ }
+
+ void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "mLastUpdatedBounds=");
+ pw.println(mLastUpdatedBounds);
+ }
+
+ private class EventReceiver extends InputEventReceiver {
+ EventReceiver(InputChannel channel, Looper looper) {
+ super(channel, looper);
+ }
+
+ public void onInputEvent(InputEvent event) {
+ OneHandedTouchHandler.this.onInputEvent(event);
+ finishInputEvent(event, true);
+ }
+ }
+
+ /**
+ * The touch(gesture) events to notify {@link OneHandedController} start or stop one handed
+ */
+ public interface OneHandedTouchEventCallback {
+ /**
+ * Handle the exit event.
+ */
+ void onStop();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java
new file mode 100644
index 0000000..3af7c4b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTransitionCallback.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import android.graphics.Rect;
+
+/**
+ * The start or stop one handed transition callback for gesture to get latest timing to handle
+ * touch region.(e.g: one handed activated, user tap out regions of displayArea to stop one handed)
+ */
+public interface OneHandedTransitionCallback {
+ /**
+ * Called when start one handed transition finished
+ */
+ default void onStartFinished(Rect bounds) {
+ }
+
+ /**
+ * Called when stop one handed transition finished
+ */
+ default void onStopFinished(Rect bounds) {
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java
new file mode 100644
index 0000000..b15b515
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.R;
+
+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
+ */
+public class OneHandedTutorialHandler implements OneHandedTransitionCallback {
+ 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 Rect mLastUpdatedBounds = new Rect();
+ private final WindowManager mWindowManager;
+
+ private View mTutorialView;
+ private Point mDisplaySize = new Point();
+ private Handler mUpdateHandler;
+ private ContentResolver mContentResolver;
+ private boolean mCanShowTutorial;
+
+ /**
+ * Container of the tutorial panel showing at outside region when one handed starting
+ */
+ private ViewGroup mTargetViewContainer;
+ private int mTutorialAreaHeight;
+
+ private final OneHandedAnimationCallback mAnimationCallback = new OneHandedAnimationCallback() {
+ @Override
+ public void onTutorialAnimationUpdate(int offset) {
+ mUpdateHandler.post(() -> onAnimationUpdate(offset));
+ }
+ };
+
+ public OneHandedTutorialHandler(Context context) {
+ context.getDisplay().getRealSize(mDisplaySize);
+ mContentResolver = context.getContentResolver();
+ mUpdateHandler = new Handler();
+ mWindowManager = context.getSystemService(WindowManager.class);
+ mTargetViewContainer = new FrameLayout(context);
+ mTargetViewContainer.setClipChildren(false);
+ mTutorialAreaHeight = Math.round(mDisplaySize.y
+ * (SystemProperties.getInt(ONE_HANDED_MODE_OFFSET_PERCENTAGE, 50) / 100.0f));
+ mTutorialView = LayoutInflater.from(context).inflate(R.layout.one_handed_tutorial, null);
+ mTargetViewContainer.addView(mTutorialView);
+ mCanShowTutorial = (Settings.Secure.getInt(mContentResolver,
+ Settings.Secure.ONE_HANDED_TUTORIAL_SHOW_COUNT, 0) >= MAX_TUTORIAL_SHOW_COUNT)
+ ? false : true;
+ if (mCanShowTutorial) {
+ createOrUpdateTutorialTarget();
+ }
+ }
+
+ @Override
+ public void onStartFinished(Rect bounds) {
+ mUpdateHandler.post(() -> {
+ updateFinished(View.VISIBLE, 0f);
+ updateTutorialCount();
+ });
+ }
+
+ @Override
+ public void onStopFinished(Rect bounds) {
+ mUpdateHandler.post(() -> updateFinished(
+ View.INVISIBLE, -mTargetViewContainer.getHeight()));
+ }
+
+ private void updateFinished(int visible, float finalPosition) {
+ if (!canShowTutorial()) {
+ return;
+ }
+
+ mTargetViewContainer.setVisibility(visible);
+ mTargetViewContainer.setTranslationY(finalPosition);
+ }
+
+ 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);
+ }
+
+ /**
+ * Adds the tutorial target view to the WindowManager and update its layout, so it's ready
+ * to be animated in.
+ */
+ private void createOrUpdateTutorialTarget() {
+ mUpdateHandler.post(() -> {
+ if (!mTargetViewContainer.isAttachedToWindow()) {
+ mTargetViewContainer.setVisibility(View.INVISIBLE);
+
+ try {
+ mWindowManager.addView(mTargetViewContainer, getTutorialTargetLayoutParams());
+ } catch (IllegalStateException e) {
+ // This shouldn't happen, but if the target is already added, just update its
+ // layout params.
+ mWindowManager.updateViewLayout(
+ mTargetViewContainer, getTutorialTargetLayoutParams());
+ }
+ } else {
+ mWindowManager.updateViewLayout(mTargetViewContainer,
+ getTutorialTargetLayoutParams());
+ }
+ });
+ }
+
+ OneHandedAnimationCallback getAnimationCallback() {
+ return mAnimationCallback;
+ }
+
+ /**
+ * Returns layout params for the dismiss target, using the latest display metrics.
+ */
+ private WindowManager.LayoutParams getTutorialTargetLayoutParams() {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ mDisplaySize.x, mTutorialAreaHeight, 0, 0,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+ lp.gravity = Gravity.TOP | Gravity.LEFT;
+ lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ lp.setFitInsetsTypes(0 /* types */);
+ lp.setTitle("one-handed-tutorial-overlay");
+
+ return lp;
+ }
+
+ void dump(@NonNull PrintWriter pw) {
+ final String innerPrefix = " ";
+ pw.println(TAG + "states: ");
+ pw.print(innerPrefix + "mLastUpdatedBounds=");
+ pw.println(mLastUpdatedBounds);
+ }
+
+ private boolean canShowTutorial() {
+ if (!mCanShowTutorial) {
+ mTargetViewContainer.setVisibility(View.GONE);
+ return false;
+ }
+
+ return true;
+ }
+
+ private void onAnimationUpdate(float value) {
+ if (!canShowTutorial()) {
+ return;
+ }
+ mTargetViewContainer.setVisibility(View.VISIBLE);
+ mTargetViewContainer.setTransitionGroup(true);
+ mTargetViewContainer.setTranslationY(value - mTargetViewContainer.getHeight());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index 692e2fa..937b00b 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -25,6 +25,7 @@
"androidx.test.ext.junit",
"mockito-target-extended-minus-junit4",
"truth-prebuilt",
+ "testables",
],
libs: [
"android.test.mock",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java
new file mode 100644
index 0000000..a8a3a9f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedAnimationControllerTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.SurfaceControl;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests against {@link OneHandedAnimationController} to ensure that it sends the right
+ * callbacks
+ * depending on the various interactions.
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class OneHandedAnimationControllerTest extends OneHandedTestCase {
+ private static final int TEST_BOUNDS_WIDTH = 1000;
+ private static final int TEST_BOUNDS_HEIGHT = 1000;
+
+ OneHandedAnimationController mOneHandedAnimationController;
+ OneHandedTutorialHandler mTutorialHandler;
+
+ @Mock
+ private SurfaceControl mMockLeash;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mTutorialHandler = new OneHandedTutorialHandler(mContext);
+ mOneHandedAnimationController = new OneHandedAnimationController(mContext);
+ }
+
+ @Test
+ public void testGetAnimator_withSameBounds_returnAnimator() {
+ final Rect originalBounds = new Rect(0, 0, TEST_BOUNDS_WIDTH, TEST_BOUNDS_HEIGHT);
+ final Rect destinationBounds = originalBounds;
+ destinationBounds.offset(0, 300);
+ final OneHandedAnimationController.OneHandedTransitionAnimator animator =
+ mOneHandedAnimationController
+ .getAnimator(mMockLeash, originalBounds, destinationBounds);
+
+ assertNotNull(animator);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
new file mode 100644
index 0000000..1ce8b54
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.DisplayController;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedControllerTest extends OneHandedTestCase {
+ Display mDisplay;
+ OneHandedController mOneHandedController;
+ OneHandedTimeoutHandler mTimeoutHandler;
+
+ @Mock
+ DisplayController mMockDisplayController;
+ @Mock
+ OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer;
+ @Mock
+ OneHandedTouchHandler mMockTouchHandler;
+ @Mock
+ OneHandedTutorialHandler mMockTutorialHandler;
+ @Mock
+ OneHandedGestureHandler mMockGestureHandler;
+ @Mock
+ OneHandedTimeoutHandler mMockTimeoutHandler;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mDisplay = mContext.getDisplay();
+ OneHandedController oneHandedController = new OneHandedController(
+ mContext,
+ mMockDisplayController,
+ mMockDisplayAreaOrganizer,
+ mMockTouchHandler,
+ mMockTutorialHandler,
+ mMockGestureHandler);
+ mOneHandedController = Mockito.spy(oneHandedController);
+ mTimeoutHandler = Mockito.spy(OneHandedTimeoutHandler.get());
+
+ when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay);
+ when(mMockDisplayAreaOrganizer.isInOneHanded()).thenReturn(false);
+ }
+
+ @Test
+ public void testDefaultShouldNotInOneHanded() {
+ final OneHandedAnimationController animationController = new OneHandedAnimationController(
+ mContext);
+ OneHandedDisplayAreaOrganizer displayAreaOrganizer = new OneHandedDisplayAreaOrganizer(
+ mContext, mMockDisplayController, animationController, mMockTutorialHandler);
+
+ assertThat(displayAreaOrganizer.isInOneHanded()).isFalse();
+ }
+
+ @Test
+ public void testRegisterOrganizer() {
+ verify(mMockDisplayAreaOrganizer, atLeastOnce()).registerOrganizer(anyInt());
+ }
+
+ @Test
+ public void testStartOneHanded() {
+ mOneHandedController.startOneHanded();
+
+ verify(mMockDisplayAreaOrganizer).scheduleOffset(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testStopOneHanded() {
+ when(mMockDisplayAreaOrganizer.isInOneHanded()).thenReturn(false);
+ mOneHandedController.stopOneHanded();
+
+ verify(mMockDisplayAreaOrganizer, never()).scheduleOffset(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testRegisterTransitionCallbackAfterInit() {
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mMockTouchHandler);
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mMockGestureHandler);
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mMockTutorialHandler);
+ }
+
+ @Test
+ public void testRegisterTransitionCallback() {
+ OneHandedTransitionCallback callback = new OneHandedTransitionCallback() {};
+ mOneHandedController.registerTransitionCallback(callback);
+
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(callback);
+ }
+
+
+ @Test
+ public void testStopOneHanded_shouldRemoveTimer() {
+ mOneHandedController.stopOneHanded();
+
+ verify(mTimeoutHandler).removeTimer();
+ }
+
+ @Test
+ public void testUpdateIsEnabled() {
+ final boolean enabled = true;
+ mOneHandedController.setOneHandedEnabled(enabled);
+
+ verify(mMockTouchHandler, atLeastOnce()).onOneHandedEnabled(enabled);
+ }
+
+ @Test
+ public void testUpdateSwipeToNotificationIsEnabled() {
+ final boolean enabled = true;
+ mOneHandedController.setSwipeToNotificationEnabled(enabled);
+
+ verify(mMockTouchHandler, atLeastOnce()).onOneHandedEnabled(enabled);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void tesSettingsObserver_updateTapAppToExit() {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.TAPS_APP_TO_EXIT, 1);
+
+ verify(mOneHandedController).setTaskChangeToExit(true);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void tesSettingsObserver_updateEnabled() {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_ENABLED, 1);
+
+ verify(mOneHandedController).setOneHandedEnabled(true);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void tesSettingsObserver_updateTimeout() {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_TIMEOUT,
+ OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+
+ verify(mMockTimeoutHandler).setTimeout(
+ OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void tesSettingsObserver_updateSwipeToNotification() {
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1);
+
+ verify(mOneHandedController).setSwipeToNotificationEnabled(true);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java
new file mode 100644
index 0000000..5ff94b6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizerTest.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.window.DisplayAreaOrganizer.FEATURE_ONE_HANDED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Configuration;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Display;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.DisplayAreaInfo;
+import android.window.IWindowContainerToken;
+import android.window.WindowContainerToken;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.DisplayController;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedDisplayAreaOrganizerTest extends OneHandedTestCase {
+ static final int DISPLAY_WIDTH = 1000;
+ static final int DISPLAY_HEIGHT = 1000;
+
+ DisplayAreaInfo mDisplayAreaInfo;
+ Display mDisplay;
+ OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer;
+ OneHandedTutorialHandler mTutorialHandler;
+ OneHandedAnimationController.OneHandedTransitionAnimator mFakeAnimator;
+ WindowContainerToken mToken;
+ SurfaceControl mLeash;
+ @Mock
+ IWindowContainerToken mMockRealToken;
+ @Mock
+ OneHandedAnimationController mMockAnimationController;
+ @Mock
+ OneHandedAnimationController.OneHandedTransitionAnimator mMockAnimator;
+ @Mock
+ OneHandedSurfaceTransactionHelper mMockSurfaceTransactionHelper;
+ @Mock
+ DisplayController mMockDisplayController;
+ @Mock
+ SurfaceControl mMockLeash;
+ @Spy
+ Handler mUpdateHandler;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mToken = new WindowContainerToken(mMockRealToken);
+ mLeash = new SurfaceControl();
+ mDisplay = mContext.getDisplay();
+ mDisplayAreaInfo = new DisplayAreaInfo(mToken, DEFAULT_DISPLAY, FEATURE_ONE_HANDED);
+ mDisplayAreaInfo.configuration.orientation = Configuration.ORIENTATION_PORTRAIT;
+ when(mMockAnimationController.getAnimator(any(), any(), any())).thenReturn(null);
+ when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay);
+ when(mMockSurfaceTransactionHelper.translate(any(), any(), anyFloat())).thenReturn(
+ mMockSurfaceTransactionHelper);
+ when(mMockSurfaceTransactionHelper.crop(any(), any(), any())).thenReturn(
+ mMockSurfaceTransactionHelper);
+ when(mMockSurfaceTransactionHelper.round(any(), any())).thenReturn(
+ mMockSurfaceTransactionHelper);
+ when(mMockAnimator.isRunning()).thenReturn(true);
+ when(mMockAnimator.setDuration(anyInt())).thenReturn(mFakeAnimator);
+ when(mMockAnimator.setOneHandedAnimationCallbacks(any())).thenReturn(mFakeAnimator);
+ when(mMockAnimator.setTransitionDirection(anyInt())).thenReturn(mFakeAnimator);
+ when(mMockLeash.getWidth()).thenReturn(DISPLAY_WIDTH);
+ when(mMockLeash.getHeight()).thenReturn(DISPLAY_HEIGHT);
+
+ mDisplayAreaOrganizer = new OneHandedDisplayAreaOrganizer(mContext,
+ mMockDisplayController,
+ mMockAnimationController,
+ mTutorialHandler);
+ mUpdateHandler = mDisplayAreaOrganizer.getUpdateHandler();
+ }
+
+ @Test
+ public void testGetDisplayAreaUpdateHandler_isNotNull() {
+ assertThat(mUpdateHandler).isNotNull();
+ }
+
+ @Test
+ public void testOnDisplayAreaAppeared() {
+ mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash);
+
+ verify(mMockAnimationController, never()).getAnimator(any(), any(), any());
+ }
+
+ @Test
+ public void testOnDisplayAreaVanished() {
+ mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash);
+ mDisplayAreaOrganizer.onDisplayAreaVanished(mDisplayAreaInfo);
+ }
+
+ @Test
+ public void testOnDisplayAreaInfoChanged_updateDisplayAreaInfo() {
+ final DisplayAreaInfo newDisplayAreaInfo = new DisplayAreaInfo(mToken, DEFAULT_DISPLAY,
+ FEATURE_ONE_HANDED);
+ mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash);
+ mDisplayAreaOrganizer.onDisplayAreaInfoChanged(newDisplayAreaInfo);
+
+ assertThat(mDisplayAreaOrganizer.mDisplayAreaMap.containsKey(mDisplayAreaInfo)).isTrue();
+ }
+
+ @Ignore("b/160848002")
+ @Test
+ public void testScheduleOffset() {
+ final int xOffSet = 0;
+ final int yOffSet = 100;
+
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash);
+ mDisplayAreaOrganizer.scheduleOffset(xOffSet, yOffSet);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_OFFSET_ANIMATE)).isEqualTo(true);
+ }
+
+ @Ignore("b/160848002")
+ @Test
+ public void testRotation_portraitToLandscape() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 0 -> 90
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_90);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(true);
+
+ // Rotate 0 -> 270
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_270);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(true);
+
+ // Rotate 180 -> 90
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_90);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(true);
+
+ // Rotate 180 -> 270
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_270);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(true);
+ }
+
+ @Ignore("b/160848002")
+ @Test
+ public void testRotation_landscapeToPortrait() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 90 -> 0
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_0);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(true);
+
+ // Rotate 90 -> 180
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_180);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(true);
+
+ // Rotate 270 -> 0
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_0);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(true);
+
+ // Rotate 270 -> 180
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_180);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(true);
+ }
+
+ @Ignore("b/160848002")
+ @Test
+ public void testRotation_portraitToPortrait() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 0 -> 0
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_0);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(false);
+
+ // Rotate 0 -> 180
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_0, Surface.ROTATION_180);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(false);
+
+ // Rotate 180 -> 180
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_180);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(false);
+
+ // Rotate 180 -> 180
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_180, Surface.ROTATION_0);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(false);
+ }
+
+ @Ignore("b/160848002")
+ @Test
+ public void testRotation_landscapeToLandscape() {
+ when(mMockLeash.isValid()).thenReturn(false);
+ // Rotate 90 -> 90
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_90);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(false);
+
+ // Rotate 90 -> 270
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_90, Surface.ROTATION_270);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(false);
+
+ // Rotate 270 -> 270
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_270);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(false);
+
+ // Rotate 270 -> 90
+ TestableLooper.get(this).processAllMessages();
+ mDisplayAreaOrganizer.onRotateDisplay(Surface.ROTATION_270, Surface.ROTATION_90);
+
+ assertThat(mUpdateHandler.hasMessages(
+ OneHandedDisplayAreaOrganizer.MSG_RESET_IMMEDIATE)).isEqualTo(false);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java
new file mode 100644
index 0000000..492c34e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedEventsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.testing.UiEventLoggerFake;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+@SmallTest
+public class OneHandedEventsTest extends OneHandedTestCase {
+
+ private UiEventLoggerFake mUiEventLogger;
+
+ @Parameterized.Parameter
+ public int mTag;
+
+ @Parameterized.Parameter(1)
+ public String mExpectedMessage;
+
+ public UiEventLogger.UiEventEnum mUiEvent;
+
+ @Before
+ public void setFakeLoggers() {
+ mUiEventLogger = new UiEventLoggerFake();
+ OneHandedEvents.sUiEventLogger = mUiEventLogger;
+ }
+
+ @Test
+ public void testLogEvent() {
+ if (mUiEvent != null) {
+ assertEquals(1, mUiEventLogger.numLogs());
+ assertEquals(mUiEvent.getId(), mUiEventLogger.eventId(0));
+ }
+ }
+
+ @Parameterized.Parameters(name = "{index}: {2}")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][]{
+ // Triggers
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_IN,
+ "writeEvent one_handed_trigger_gesture_in"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT,
+ "writeEvent one_handed_trigger_gesture_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_OVERSPACE_OUT,
+ "writeEvent one_handed_trigger_overspace_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_POP_IME_OUT,
+ "writeEvent one_handed_trigger_pop_ime_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT,
+ "writeEvent one_handed_trigger_rotation_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_APP_TAPS_OUT,
+ "writeEvent one_handed_trigger_app_taps_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_TIMEOUT_OUT,
+ "writeEvent one_handed_trigger_timeout_out"},
+ {OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_SCREEN_OFF_OUT,
+ "writeEvent one_handed_trigger_screen_off_out"},
+ // Settings toggles
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_ON,
+ "writeEvent one_handed_settings_enabled_on"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_ENABLED_OFF,
+ "writeEvent one_handed_settings_enabled_off"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_ON,
+ "writeEvent one_handed_settings_app_taps_exit_on"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_APP_TAPS_EXIT_OFF,
+ "writeEvent one_handed_settings_app_taps_exit_off"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_ON,
+ "writeEvent one_handed_settings_timeout_exit_on"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_EXIT_OFF,
+ "writeEvent one_handed_settings_timeout_exit_off"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_NEVER,
+ "writeEvent one_handed_settings_timeout_seconds_never"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_4,
+ "writeEvent one_handed_settings_timeout_seconds_4"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_8,
+ "writeEvent one_handed_settings_timeout_seconds_8"},
+ {OneHandedEvents.EVENT_ONE_HANDED_SETTINGS_TIMEOUT_SECONDS_12,
+ "writeEvent one_handed_settings_timeout_seconds_12"}
+ });
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java
new file mode 100644
index 0000000..fb417c8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedGestureHandlerTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.DisplayController;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedGestureHandlerTest extends OneHandedTestCase {
+ OneHandedTutorialHandler mTutorialHandler;
+ OneHandedGestureHandler mGestureHandler;
+ @Mock
+ DisplayController mMockDisplayController;
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mTutorialHandler = new OneHandedTutorialHandler(mContext);
+ mGestureHandler = new OneHandedGestureHandler(mContext, mMockDisplayController);
+ }
+
+ @Test
+ public void testSetGestureEventListener() {
+ OneHandedGestureHandler.OneHandedGestureEventCallback callback =
+ new OneHandedGestureHandler.OneHandedGestureEventCallback() {
+ @Override
+ public void onStart() {}
+
+ @Override
+ public void onStop() {}
+ };
+
+ mGestureHandler.setGestureEventListener(callback);
+ assertThat(mGestureHandler.mGestureEventCallback).isEqualTo(callback);
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void testReceiveNewConfig_whenThreeButtonModeEnabled() {
+ mGestureHandler.onOneHandedEnabled(true);
+ mGestureHandler.onThreeButtonModeEnabled(true);
+
+ assertThat(mGestureHandler.mInputMonitor).isNotNull();
+ assertThat(mGestureHandler.mInputEventReceiver).isNotNull();
+ }
+
+ @Test
+ public void testOneHandedDisabled_shouldDisposeInputChannel() {
+ mGestureHandler.onOneHandedEnabled(false);
+
+ assertThat(mGestureHandler.mInputMonitor).isNull();
+ assertThat(mGestureHandler.mInputEventReceiver).isNull();
+ }
+
+ @Test
+ public void testChangeNavBarToNon3Button_shouldDisposeInputChannel() {
+ mGestureHandler.onOneHandedEnabled(true);
+ mGestureHandler.onThreeButtonModeEnabled(false);
+
+ assertThat(mGestureHandler.mInputMonitor).isNull();
+ assertThat(mGestureHandler.mInputEventReceiver).isNull();
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java
new file mode 100644
index 0000000..7c11138
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedSettingsUtilTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedSettingsUtilTest extends OneHandedTestCase {
+ ContentResolver mContentResolver;
+ ContentObserver mContentObserver;
+ boolean mOnChanged;
+
+ @Before
+ public void setUp() {
+ mContentResolver = mContext.getContentResolver();
+ mContentObserver = new ContentObserver(mContext.getMainThreadHandler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ mOnChanged = true;
+ }
+ };
+ }
+
+ @Test
+ public void testRegisterSecureKeyObserver() {
+ final Uri result = OneHandedSettingsUtil.registerSettingsKeyObserver(
+ Settings.Secure.TAPS_APP_TO_EXIT, mContentResolver, mContentObserver);
+
+ assertThat(result).isNotNull();
+
+ OneHandedSettingsUtil.registerSettingsKeyObserver(
+ Settings.Secure.TAPS_APP_TO_EXIT, mContentResolver, mContentObserver);
+ }
+
+ @Test
+ public void testUnregisterSecureKeyObserver() {
+ OneHandedSettingsUtil.registerSettingsKeyObserver(
+ Settings.Secure.TAPS_APP_TO_EXIT, mContentResolver, mContentObserver);
+ OneHandedSettingsUtil.unregisterSettingsKeyObserver(mContentResolver, mContentObserver);
+
+ assertThat(mOnChanged).isFalse();
+
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.TAPS_APP_TO_EXIT, 0);
+
+ assertThat(mOnChanged).isFalse();
+ }
+
+ @Test
+ public void testGetSettingsIsOneHandedModeEnabled() {
+ assertThat(OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ mContentResolver)).isAnyOf(true, false);
+ }
+
+ @Test
+ public void testGetSettingsTapsAppToExit() {
+ assertThat(OneHandedSettingsUtil.getSettingsTapsAppToExit(
+ mContentResolver)).isAnyOf(true, false);
+ }
+
+ @Test
+ public void testGetSettingsOneHandedModeTimeout() {
+ assertThat(OneHandedSettingsUtil.getSettingsOneHandedModeTimeout(
+ mContentResolver)).isAnyOf(
+ ONE_HANDED_TIMEOUT_NEVER,
+ ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS,
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS,
+ ONE_HANDED_TIMEOUT_LONG_IN_SECONDS);
+ }
+
+ @Test
+ public void testGetSettingsSwipeToNotificationEnabled() {
+ assertThat(OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ mContentResolver)).isAnyOf(true, false);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java
new file mode 100644
index 0000000..c7ae2a0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTestCase.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.wm.shell.onehanded.OneHandedController.SUPPORT_ONE_HANDED_MODE;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.SystemProperties;
+import android.provider.Settings;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * Base class that does One Handed specific setup.
+ */
+public abstract class OneHandedTestCase {
+ static boolean sOrigEnabled;
+ static boolean sOrigTapsAppToExitEnabled;
+ static int sOrigTimeout;
+ static boolean sOrigSwipeToNotification;
+
+ protected Context mContext;
+
+ @Before
+ public void setupSettings() {
+ final Context testContext =
+ InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final DisplayManager dm = testContext.getSystemService(DisplayManager.class);
+ mContext = testContext.createDisplayContext(dm.getDisplay(DEFAULT_DISPLAY));
+
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+
+ sOrigEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
+ getContext().getContentResolver());
+ sOrigTimeout = OneHandedSettingsUtil.getSettingsOneHandedModeTimeout(
+ getContext().getContentResolver());
+ sOrigTapsAppToExitEnabled = OneHandedSettingsUtil.getSettingsTapsAppToExit(
+ getContext().getContentResolver());
+ sOrigSwipeToNotification = OneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled(
+ getContext().getContentResolver());
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_ENABLED, 1);
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_TIMEOUT, ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.TAPS_APP_TO_EXIT, 1);
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1);
+ }
+
+ @Before
+ public void assumeOneHandedModeSupported() {
+ assumeTrue(SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false));
+ }
+
+ @After
+ public void restoreSettings() {
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_ENABLED, sOrigEnabled ? 1 : 0);
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.ONE_HANDED_MODE_TIMEOUT, sOrigTimeout);
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.TAPS_APP_TO_EXIT, sOrigTapsAppToExitEnabled ? 1 : 0);
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED,
+ sOrigSwipeToNotification ? 1 : 0);
+
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+}
+
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java
new file mode 100644
index 0000000..e2b70c3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTimeoutHandlerTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_LONG_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_NEVER;
+import static com.android.wm.shell.onehanded.OneHandedSettingsUtil.ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS;
+import static com.android.wm.shell.onehanded.OneHandedTimeoutHandler.ONE_HANDED_TIMEOUT_STOP_MSG;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedTimeoutHandlerTest extends OneHandedTestCase {
+ OneHandedTimeoutHandler mTimeoutHandler;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mTimeoutHandler = Mockito.spy(OneHandedTimeoutHandler.get());
+ }
+
+ @Test
+ public void testTimeoutHandler_isNotNull() {
+ assertThat(OneHandedTimeoutHandler.get()).isNotNull();
+ }
+
+ @Test
+ public void testTimeoutHandler_getTimeout_defaultMedium() {
+ assertThat(OneHandedTimeoutHandler.get().getTimeout()).isEqualTo(
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ }
+
+ @Test
+ public void testTimeoutHandler_setNewTime_resetTimer() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ verify(mTimeoutHandler).resetTimer();
+ assertThat(mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull();
+ }
+
+ @Test
+ public void testSetTimeoutNever_neverResetTimer() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_NEVER);
+ assertThat(!mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull();
+ }
+
+ @Test
+ public void testSetTimeoutShort() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_SHORT_IN_SECONDS);
+ verify(mTimeoutHandler).resetTimer();
+ assertThat(mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull();
+ }
+
+ @Test
+ public void testSetTimeoutMedium() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
+ verify(mTimeoutHandler).resetTimer();
+ assertThat(mTimeoutHandler.sHandler.hasMessages(
+ ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS)).isNotNull();
+ }
+
+ @Test
+ public void testSetTimeoutLong() {
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS);
+ assertThat(mTimeoutHandler.getTimeout()).isEqualTo(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS);
+ }
+
+ @Test
+ public void testDragging_shouldRemoveAndSendEmptyMessageDelay() {
+ final boolean isDragging = true;
+ mTimeoutHandler.setTimeout(ONE_HANDED_TIMEOUT_LONG_IN_SECONDS);
+ mTimeoutHandler.resetTimer();
+ TestableLooper.get(this).processAllMessages();
+ assertThat(mTimeoutHandler.sHandler.hasMessages(ONE_HANDED_TIMEOUT_STOP_MSG)).isNotNull();
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java
new file mode 100644
index 0000000..c69e385
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTouchHandlerTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedTouchHandlerTest extends OneHandedTestCase {
+ OneHandedTouchHandler mTouchHandler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTouchHandler = new OneHandedTouchHandler();
+ }
+
+ @Test
+ public void testRegisterTouchEventListener() {
+ OneHandedTouchHandler.OneHandedTouchEventCallback callback = () -> {
+ };
+ mTouchHandler.registerTouchEventListener(callback);
+
+ assertThat(mTouchHandler.mTouchEventCallback).isEqualTo(callback);
+ }
+
+ @Test
+ public void testOneHandedDisabled_shouldDisposeInputChannel() {
+ mTouchHandler.onOneHandedEnabled(false);
+
+ assertThat(mTouchHandler.mInputMonitor).isNull();
+ assertThat(mTouchHandler.mInputEventReceiver).isNull();
+ }
+
+ @Ignore("b/167943723, refactor it and fix it")
+ @Test
+ public void testOneHandedEnabled_monitorInputChannel() {
+ mTouchHandler.onOneHandedEnabled(true);
+
+ assertThat(mTouchHandler.mInputMonitor).isNotNull();
+ assertThat(mTouchHandler.mInputEventReceiver).isNotNull();
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java
new file mode 100644
index 0000000..4a133d39
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 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.onehanded;
+
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.DisplayController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class OneHandedTutorialHandlerTest extends OneHandedTestCase {
+ @Mock
+ OneHandedTouchHandler mTouchHandler;
+ OneHandedTutorialHandler mTutorialHandler;
+ OneHandedGestureHandler mGestureHandler;
+ OneHandedController mOneHandedController;
+ @Mock
+ DisplayController mMockDisplayController;
+ @Mock
+ OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTutorialHandler = new OneHandedTutorialHandler(mContext);
+ mGestureHandler = new OneHandedGestureHandler(mContext, mMockDisplayController);
+ mOneHandedController = new OneHandedController(
+ getContext(),
+ mMockDisplayController,
+ mMockDisplayAreaOrganizer,
+ mTouchHandler,
+ mTutorialHandler,
+ mGestureHandler);
+ }
+
+ @Test
+ public void testOneHandedManager_registerForDisplayAreaOrganizer() {
+ verify(mMockDisplayAreaOrganizer).registerTransitionCallback(mTutorialHandler);
+ }
+}