1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.onehanded; 18 19 import static android.view.Display.DEFAULT_DISPLAY; 20 21 import android.graphics.Rect; 22 import android.hardware.input.InputManagerGlobal; 23 import android.os.Looper; 24 import android.view.InputChannel; 25 import android.view.InputEvent; 26 import android.view.InputEventReceiver; 27 import android.view.InputMonitor; 28 import android.view.MotionEvent; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.VisibleForTesting; 32 33 import com.android.wm.shell.common.ShellExecutor; 34 35 import java.io.PrintWriter; 36 37 /** 38 * Manages all the touch handling for One Handed on the Phone, including user tap outside region 39 * to exit, reset timer when user is in one-handed mode. 40 */ 41 public class OneHandedTouchHandler implements OneHandedTransitionCallback { 42 private static final String TAG = "OneHandedTouchHandler"; 43 private final Rect mLastUpdatedBounds = new Rect(); 44 45 private final OneHandedTimeoutHandler mTimeoutHandler; 46 private final ShellExecutor mMainExecutor; 47 48 @VisibleForTesting 49 InputMonitor mInputMonitor; 50 @VisibleForTesting 51 InputEventReceiver mInputEventReceiver; 52 @VisibleForTesting 53 OneHandedTouchEventCallback mTouchEventCallback; 54 55 private boolean mIsEnabled; 56 private boolean mIsOnStopTransitioning; 57 private boolean mIsInOutsideRegion; 58 OneHandedTouchHandler(OneHandedTimeoutHandler timeoutHandler, ShellExecutor mainExecutor)59 public OneHandedTouchHandler(OneHandedTimeoutHandler timeoutHandler, 60 ShellExecutor mainExecutor) { 61 mTimeoutHandler = timeoutHandler; 62 mMainExecutor = mainExecutor; 63 updateIsEnabled(); 64 } 65 66 /** 67 * Notified by {@link OneHandedController}, when user update settings of Enabled or Disabled 68 * 69 * @param isEnabled is one handed settings enabled or not 70 */ onOneHandedEnabled(boolean isEnabled)71 public void onOneHandedEnabled(boolean isEnabled) { 72 mIsEnabled = isEnabled; 73 updateIsEnabled(); 74 } 75 76 /** 77 * Register {@link OneHandedTouchEventCallback} to receive onEnter(), onExit() callback 78 */ registerTouchEventListener(OneHandedTouchEventCallback callback)79 public void registerTouchEventListener(OneHandedTouchEventCallback callback) { 80 mTouchEventCallback = callback; 81 } 82 onMotionEvent(MotionEvent ev)83 private boolean onMotionEvent(MotionEvent ev) { 84 mIsInOutsideRegion = isWithinTouchOutsideRegion(ev.getX(), ev.getY()); 85 switch (ev.getAction()) { 86 case MotionEvent.ACTION_DOWN: 87 case MotionEvent.ACTION_MOVE: { 88 if (!mIsInOutsideRegion) { 89 mTimeoutHandler.resetTimer(); 90 } 91 break; 92 } 93 case MotionEvent.ACTION_UP: 94 case MotionEvent.ACTION_CANCEL: { 95 mTimeoutHandler.resetTimer(); 96 if (mIsInOutsideRegion && !mIsOnStopTransitioning) { 97 mTouchEventCallback.onStop(); 98 mIsOnStopTransitioning = true; 99 } 100 // Reset flag for next operation 101 mIsInOutsideRegion = false; 102 break; 103 } 104 } 105 return true; 106 } 107 disposeInputChannel()108 private void disposeInputChannel() { 109 if (mInputEventReceiver != null) { 110 mInputEventReceiver.dispose(); 111 mInputEventReceiver = null; 112 } 113 if (mInputMonitor != null) { 114 mInputMonitor.dispose(); 115 mInputMonitor = null; 116 } 117 } 118 isWithinTouchOutsideRegion(float x, float y)119 private boolean isWithinTouchOutsideRegion(float x, float y) { 120 return Math.round(y) < mLastUpdatedBounds.top; 121 } 122 onInputEvent(InputEvent ev)123 private void onInputEvent(InputEvent ev) { 124 if (ev instanceof MotionEvent) { 125 onMotionEvent((MotionEvent) ev); 126 } 127 } 128 updateIsEnabled()129 private void updateIsEnabled() { 130 disposeInputChannel(); 131 if (mIsEnabled) { 132 mInputMonitor = InputManagerGlobal.getInstance().monitorGestureInput( 133 "onehanded-touch", DEFAULT_DISPLAY); 134 try { 135 mMainExecutor.executeBlocking(() -> { 136 mInputEventReceiver = new EventReceiver( 137 mInputMonitor.getInputChannel(), Looper.myLooper()); 138 }); 139 } catch (InterruptedException e) { 140 throw new RuntimeException("Failed to create input event receiver", e); 141 } 142 } 143 } 144 145 @Override onStartFinished(Rect bounds)146 public void onStartFinished(Rect bounds) { 147 mLastUpdatedBounds.set(bounds); 148 } 149 150 @Override onStopFinished(Rect bounds)151 public void onStopFinished(Rect bounds) { 152 mLastUpdatedBounds.set(bounds); 153 mIsOnStopTransitioning = false; 154 } 155 dump(@onNull PrintWriter pw)156 void dump(@NonNull PrintWriter pw) { 157 final String innerPrefix = " "; 158 pw.println(TAG); 159 pw.print(innerPrefix + "mLastUpdatedBounds="); 160 pw.println(mLastUpdatedBounds); 161 } 162 163 // TODO: Use BatchedInputEventReceiver 164 private class EventReceiver extends InputEventReceiver { EventReceiver(InputChannel channel, Looper looper)165 EventReceiver(InputChannel channel, Looper looper) { 166 super(channel, looper); 167 } 168 onInputEvent(InputEvent event)169 public void onInputEvent(InputEvent event) { 170 OneHandedTouchHandler.this.onInputEvent(event); 171 finishInputEvent(event, true); 172 } 173 } 174 175 /** 176 * The touch(gesture) events to notify {@link OneHandedController} start or stop one handed 177 */ 178 public interface OneHandedTouchEventCallback { 179 /** 180 * Handle the exit event. 181 */ onStop()182 void onStop(); 183 } 184 } 185