1 /*
2  * Copyright (C) 2021 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.car.carlauncher.displayarea;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 
21 import android.graphics.Rect;
22 import android.hardware.input.InputManager;
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 com.android.wm.shell.common.ShellExecutor;
31 
32 /**
33  * Manages all the touch handling for display area, including user tap outside region
34  * to exit.
35  */
36 public class CarDisplayAreaTouchHandler {
37     private static final int CLICK_ACTION_THRESHOLD = 10;
38 
39     private final ShellExecutor mMainExecutor;
40 
41     private InputMonitor mInputMonitor;
42     private InputEventReceiver mInputEventReceiver;
43     private OnClickDisplayAreaListener mOnClickDisplayAreaListener;
44     private OnDragDisplayAreaListener mOnTouchTitleBarListener;
45     private boolean mIsEnabled;
46     private float mStartX;
47     private float mStartY;
48     private Rect mTitleBarBounds;
49     private boolean mIsTitleBarVisible;
50     private boolean mIsTitleBarDragged;
51 
CarDisplayAreaTouchHandler(ShellExecutor mainExecutor)52     public CarDisplayAreaTouchHandler(ShellExecutor mainExecutor) {
53         mMainExecutor = mainExecutor;
54     }
55 
56     /**
57      * Notified by {@link CarDisplayAreaController}, to update settings of Enabled or Disabled.
58      */
enable(boolean isEnabled)59     public void enable(boolean isEnabled) {
60         mIsEnabled = isEnabled;
61         updateIsEnabled();
62     }
63 
64     /**
65      * Register {@link OnClickDisplayAreaListener} to receive onClick() callback
66      */
registerOnClickListener(OnClickDisplayAreaListener listener)67     public void registerOnClickListener(OnClickDisplayAreaListener listener) {
68         mOnClickDisplayAreaListener = listener;
69     }
70 
71     /**
72      * Register {@link OnDragDisplayAreaListener} to receive onTouch() callbacks
73      */
registerTouchEventListener(OnDragDisplayAreaListener listener)74     public void registerTouchEventListener(OnDragDisplayAreaListener listener) {
75         mOnTouchTitleBarListener = listener;
76     }
77 
78     /**
79      * Updated whether the titleBar is visible or not.
80      */
updateTitleBarVisibility(boolean isTitleBarVisible)81     public void updateTitleBarVisibility(boolean isTitleBarVisible) {
82         mIsTitleBarVisible = isTitleBarVisible;
83     }
84 
setTitleBarBounds(Rect titleBarBounds)85     public void setTitleBarBounds(Rect titleBarBounds) {
86         mTitleBarBounds = titleBarBounds;
87     }
88 
onMotionEvent(MotionEvent event)89     private void onMotionEvent(MotionEvent event) {
90         switch (event.getAction()) {
91             case MotionEvent.ACTION_DOWN:
92                 mStartX = event.getX();
93                 mStartY = event.getY();
94                 if (mOnTouchTitleBarListener != null && mIsTitleBarVisible) {
95                     mOnTouchTitleBarListener.onStart(mStartX, mStartY);
96                 }
97                 mIsTitleBarDragged = false;
98                 break;
99             case MotionEvent.ACTION_MOVE:
100                 if (mIsTitleBarVisible && isTitleBarGrabbed() && mOnTouchTitleBarListener != null) {
101                     mOnTouchTitleBarListener.onMove(event.getX(), event.getY());
102                     mIsTitleBarDragged = true;
103                 }
104 
105                 break;
106             case MotionEvent.ACTION_UP:
107                 float endX = event.getX();
108                 float endY = event.getY();
109                 // TODO: use utility functions from gesture class.
110                 if (isAClick(mStartX, endX, mStartY, endY)) {
111                     mOnClickDisplayAreaListener.onClick(endX, endY);
112                 }
113                 if (mIsTitleBarDragged && mOnTouchTitleBarListener != null) {
114                     mOnTouchTitleBarListener.onFinish(event.getX(), event.getY());
115                 }
116                 mIsTitleBarDragged = false;
117                 break;
118             default:
119                 mIsTitleBarDragged = false;
120         }
121     }
122 
isAClick(float startX, float endX, float startY, float endY)123     private static boolean isAClick(float startX, float endX, float startY, float endY) {
124         float differenceX = Math.abs(startX - endX);
125         float differenceY = Math.abs(startY - endY);
126         return !(differenceX > CLICK_ACTION_THRESHOLD || differenceY > CLICK_ACTION_THRESHOLD);
127     }
128 
isTitleBarGrabbed()129     private boolean isTitleBarGrabbed() {
130         return mStartX >= mTitleBarBounds.left && mStartX <= mTitleBarBounds.right
131                 && mStartY >= mTitleBarBounds.top && mStartY <= mTitleBarBounds.bottom;
132     }
133 
disposeInputChannel()134     private void disposeInputChannel() {
135         if (mInputEventReceiver != null) {
136             mInputEventReceiver.dispose();
137             mInputEventReceiver = null;
138         }
139         if (mInputMonitor != null) {
140             mInputMonitor.dispose();
141             mInputMonitor = null;
142         }
143     }
144 
onInputEvent(InputEvent ev)145     private void onInputEvent(InputEvent ev) {
146         if (ev instanceof MotionEvent) {
147             onMotionEvent((MotionEvent) ev);
148         }
149     }
150 
updateIsEnabled()151     private void updateIsEnabled() {
152         disposeInputChannel();
153         if (mIsEnabled) {
154             mInputMonitor = InputManager.getInstance().monitorGestureInput(
155                     "car-display-area-touch", DEFAULT_DISPLAY);
156             try {
157                 mMainExecutor.executeBlocking(() -> {
158                     mInputEventReceiver = new EventReceiver(
159                             mInputMonitor.getInputChannel(), Looper.myLooper());
160                 });
161             } catch (InterruptedException e) {
162                 throw new RuntimeException("Failed to create input event receiver", e);
163             }
164         }
165     }
166 
167     private class EventReceiver extends InputEventReceiver {
EventReceiver(InputChannel channel, Looper looper)168         EventReceiver(InputChannel channel, Looper looper) {
169             super(channel, looper);
170         }
171 
onInputEvent(InputEvent event)172         public void onInputEvent(InputEvent event) {
173             CarDisplayAreaTouchHandler.this.onInputEvent(event);
174             finishInputEvent(event, true);
175         }
176     }
177 
178     /**
179      * Callback invoked when a user clicks anywhere on the display area.
180      */
181     interface OnClickDisplayAreaListener {
182 
183         /**
184          * Called when a user clicks on the display area. Returns the co-ordinate of the click.
185          */
onClick(float x, float y)186         void onClick(float x, float y);
187     }
188 
189     /**
190      * Callback invoked when a user touches anywhere on the display area.
191      */
192     interface OnDragDisplayAreaListener {
193 
194         /**
195          * Called for ACTION_MOVE touch events on the title bar. Returns the co-ordinate of the
196          * touch. This is only called when the title bar is visible.
197          */
onMove(float x, float y)198         void onMove(float x, float y);
199 
200         /**
201          * Called for ACTION_UP touch events on the title bar. Returns the co-ordinate of the
202          * touch. This is only called when the title bar is visible.
203          */
onFinish(float x, float y)204         void onFinish(float x, float y);
205 
206         /**
207          * Called for ACTION_DOWN touch events on the title bar. Returns the co-ordinate of the
208          * touch. This is only called when the title bar is visible.
209          */
onStart(float x, float y)210         void onStart(float x, float y);
211     }
212 }
213