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

Commit 7e63e11d authored by Bill Lin's avatar Bill Lin
Browse files

TouchHandler to handle touch events inside & outside displayArea (9/N)

1) Monitor the region out of displayArea when start one handed
2) When user is operating in displayArea, reset timer

Test: atest OneHandedDisplayAreaOrganizerTest
Test: atest OneHandedManagerImplTest
Test: atest OneHandedUITest
Test: atest OneHandedSettingsUtilTest
Test: atest SystemUITests
Test: atest DisplayAreaPolicyBuilderTest
Bug: 150747909
Change-Id: I60265fc8547a23a010c692d11ae63a2101102706
parent f98a1873
Loading
Loading
Loading
Loading
+28 −0
Original line number Diff line number Diff line
@@ -49,6 +49,8 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable {
    private DisplayController mDisplayController;
    private OneHandedDisplayAreaOrganizer mDisplayAreaOrganizer;
    private OneHandedTimeoutHandler mTimeoutHandler;
    private OneHandedTouchHandler mTouchHandler;
    private OneHandedTouchHandler.OneHandedTouchEventCallback mTouchEventCallback;
    private OneHandedTransitionCallback mTransitionCallback;
    private SysUiState mSysUiFlagContainer;

@@ -59,6 +61,7 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable {
    public OneHandedManagerImpl(Context context,
            DisplayController displayController,
            OneHandedDisplayAreaOrganizer displayAreaOrganizer,
            OneHandedTouchHandler touchHandler,
            SysUiState sysUiState) {

        mDisplayAreaOrganizer = displayAreaOrganizer;
@@ -69,6 +72,7 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable {
        mIsOneHandedEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
                context.getContentResolver());
        mTimeoutHandler = OneHandedTimeoutHandler.get();
        mTouchHandler = touchHandler;
        updateOneHandedEnabled();
        setupGestures();
    }
@@ -105,6 +109,29 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable {
    }

    private void setupGestures() {
        mTouchEventCallback = new OneHandedTouchHandler.OneHandedTouchEventCallback() {
            @Override
            public boolean onStart() {
                boolean result = false;
                if (!mDisplayAreaOrganizer.isInOneHanded()) {
                    startOneHanded();
                    result = true;
                }
                return result;
            }

            @Override
            public boolean onStop() {
                boolean result = false;
                if (mDisplayAreaOrganizer.isInOneHanded()) {
                    stopOneHanded();
                    result = true;
                }
                return result;
            }
        };
        mTouchHandler.registerTouchEventListener(mTouchEventCallback);

        mTransitionCallback = new OneHandedTransitionCallback() {
            @Override
            public void onStartFinished(Rect bounds) {
@@ -119,6 +146,7 @@ public class OneHandedManagerImpl implements OneHandedManager, Dumpable {
            }
        };
        mDisplayAreaOrganizer.registerTransitionCallback(mTransitionCallback);
        mDisplayAreaOrganizer.registerTransitionCallback(mTouchHandler);
    }

    /**
+184 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.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 com.android.systemui.Dumpable;

import java.io.FileDescriptor;
import java.io.PrintWriter;

import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * 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
 */
@Singleton
public class OneHandedTouchHandler implements OneHandedTransitionCallback, Dumpable {
    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 mIsInOutsideRegion;

    @Inject
    public OneHandedTouchHandler() {
        mTimeoutHandler = OneHandedTimeoutHandler.get();
        updateIsEnabled();
    }

    /**
     * Notified by {@link OneHandedManagerImpl}, 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) {
                    mTouchEventCallback.onStop();
                }
                // 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 SysUiInputEventReceiver(
                    mInputMonitor.getInputChannel(), Looper.getMainLooper());
        }
    }

    @Override
    public void onStartFinished(Rect bounds) {
        mLastUpdatedBounds.set(bounds);
    }

    @Override
    public void onStopFinished(Rect bounds) {
        mLastUpdatedBounds.set(bounds);
    }

    @Override
    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
        final String innerPrefix = "  ";
        pw.println(TAG + "states: ");
        pw.print(innerPrefix + "mLastUpdatedBounds=");
        pw.println(mLastUpdatedBounds);
    }

    private class SysUiInputEventReceiver extends InputEventReceiver {
        SysUiInputEventReceiver(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 OneHandedManager} start or stop one handed
     */
    public interface OneHandedTouchEventCallback {
        /**
         * Handle the start event event, and return whether the event was consumed.
         */
        boolean onStart();

        /**
         * Handle the exit event event, and return whether the event was consumed.
         */
        boolean onStop();
    }
}
+10 −0
Original line number Diff line number Diff line
@@ -55,6 +55,8 @@ public class OneHandedManagerImplTest extends OneHandedTestCase {
    @Mock
    OneHandedSurfaceTransactionHelper mMockSurfaceTransactionHelper;
    @Mock
    OneHandedTouchHandler mMockTouchHandler;
    @Mock
    SysUiState mMockSysUiState;

    @Before
@@ -63,6 +65,7 @@ public class OneHandedManagerImplTest extends OneHandedTestCase {
        mOneHandedManagerImpl = new OneHandedManagerImpl(getContext(),
                mMockDisplayController,
                mMockDisplayAreaOrganizer,
                mMockTouchHandler,
                mMockSysUiState);
        mTimeoutHandler = Mockito.spy(OneHandedTimeoutHandler.get());

@@ -115,5 +118,12 @@ public class OneHandedManagerImplTest extends OneHandedTestCase {
        verify(mTimeoutHandler, times(1)).removeTimer();
    }

    @Test
    public void testUpdateIsEnabled() {
        final boolean enabled = true;
        mOneHandedManagerImpl.setOneHandedEnabled(enabled);

        verify(mMockTouchHandler, atLeastOnce()).onOneHandedEnabled(enabled);
    }

}
+2 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import org.junit.Before;
public abstract class OneHandedTestCase extends SysuiTestCase {
    static boolean sOrigEnabled;
    static int sOrigTimeout;

    @Before
    public void setupSettings() {
        sOrigEnabled = OneHandedSettingsUtil.getSettingsOneHandedModeEnabled(
@@ -41,6 +42,7 @@ public abstract class OneHandedTestCase extends SysuiTestCase {
        Settings.Secure.putInt(getContext().getContentResolver(),
                Settings.Secure.ONE_HANDED_MODE_TIMEOUT, ONE_HANDED_TIMEOUT_MEDIUM_IN_SECONDS);
    }

    @After
    public void restoreSettings() {
        Settings.Secure.putInt(getContext().getContentResolver(),
+101 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.onehanded;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.Instrumentation;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;

import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.systemui.model.SysUiState;
import com.android.systemui.wm.DisplayController;

import org.junit.Before;
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 OneHandedTouchHandlerTest extends OneHandedTestCase {
    Instrumentation mInstrumentation;
    OneHandedTouchHandler mTouchHandler;
    OneHandedManagerImpl mOneHandedManagerImpl;
    @Mock
    DisplayController mMockDisplayController;
    @Mock
    OneHandedDisplayAreaOrganizer mMockDisplayAreaOrganizer;
    @Mock
    SysUiState mMockSysUiState;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mTouchHandler = Mockito.spy(new OneHandedTouchHandler());
        mOneHandedManagerImpl = new OneHandedManagerImpl(mInstrumentation.getContext(),
                mMockDisplayController,
                mMockDisplayAreaOrganizer,
                mTouchHandler,
                mMockSysUiState);
    }

    @Test
    public void testOneHandedManager_registerForDisplayAreaOrganizer() {
        verify(mMockDisplayAreaOrganizer, times(1)).registerTransitionCallback(mTouchHandler);
    }

    @Test
    public void testOneHandedManager_registerTouchEventListener() {
        verify(mTouchHandler).registerTouchEventListener(any());
        assertThat(mTouchHandler.mTouchEventCallback).isNotNull();
    }

    @Test
    public void testOneHandedDisabled_shouldDisposeInputChannel() {
        mOneHandedManagerImpl.setOneHandedEnabled(false);
        assertThat(mTouchHandler.mInputMonitor).isNull();
        assertThat(mTouchHandler.mInputEventReceiver).isNull();
    }

    @Test
    public void testOneHandedEnabled_monitorInputChannel() {
        mOneHandedManagerImpl.setOneHandedEnabled(true);
        assertThat(mTouchHandler.mInputMonitor).isNotNull();
        assertThat(mTouchHandler.mInputEventReceiver).isNotNull();
    }

    @Test
    public void testReceiveNewConfig_whenSetOneHandedEnabled() {
        // 1st called at init
        verify(mTouchHandler).onOneHandedEnabled(true);
        mOneHandedManagerImpl.setOneHandedEnabled(true);
        // 2nd called by setOneHandedEnabled()
        verify(mTouchHandler, times(2)).onOneHandedEnabled(true);
    }
}