/* * 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 com.android.wm.shell.common.ShellExecutor; 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. */ public class OneHandedTouchHandler implements OneHandedTransitionCallback { private static final String TAG = "OneHandedTouchHandler"; private final Rect mLastUpdatedBounds = new Rect(); private final OneHandedTimeoutHandler mTimeoutHandler; private final ShellExecutor mMainExecutor; @VisibleForTesting InputMonitor mInputMonitor; @VisibleForTesting InputEventReceiver mInputEventReceiver; @VisibleForTesting OneHandedTouchEventCallback mTouchEventCallback; private boolean mIsEnabled; private boolean mIsOnStopTransitioning; private boolean mIsInOutsideRegion; public OneHandedTouchHandler(OneHandedTimeoutHandler timeoutHandler, ShellExecutor mainExecutor) { mTimeoutHandler = timeoutHandler; mMainExecutor = mainExecutor; 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); try { mMainExecutor.executeBlocking(() -> { mInputEventReceiver = new EventReceiver( mInputMonitor.getInputChannel(), Looper.myLooper()); }); } catch (InterruptedException e) { throw new RuntimeException("Failed to create input event receiver", e); } } } @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); pw.print(innerPrefix + "mLastUpdatedBounds="); pw.println(mLastUpdatedBounds); } // TODO: Use BatchedInputEventReceiver 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(); } }