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