1 /*
<lambda>null2  * Copyright 2019 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 androidx.compose.ui.input.pointer
18 
19 import android.os.Build
20 import android.util.SparseBooleanArray
21 import android.util.SparseLongArray
22 import android.view.MotionEvent
23 import android.view.MotionEvent.ACTION_CANCEL
24 import android.view.MotionEvent.ACTION_DOWN
25 import android.view.MotionEvent.ACTION_HOVER_ENTER
26 import android.view.MotionEvent.ACTION_HOVER_EXIT
27 import android.view.MotionEvent.ACTION_HOVER_MOVE
28 import android.view.MotionEvent.ACTION_OUTSIDE
29 import android.view.MotionEvent.ACTION_POINTER_DOWN
30 import android.view.MotionEvent.ACTION_POINTER_UP
31 import android.view.MotionEvent.ACTION_SCROLL
32 import android.view.MotionEvent.ACTION_UP
33 import android.view.MotionEvent.TOOL_TYPE_ERASER
34 import android.view.MotionEvent.TOOL_TYPE_FINGER
35 import android.view.MotionEvent.TOOL_TYPE_MOUSE
36 import android.view.MotionEvent.TOOL_TYPE_STYLUS
37 import android.view.MotionEvent.TOOL_TYPE_UNKNOWN
38 import androidx.annotation.RequiresApi
39 import androidx.annotation.VisibleForTesting
40 import androidx.compose.ui.geometry.Offset
41 import androidx.compose.ui.util.fastIsFinite
42 
43 /** Converts Android framework [MotionEvent]s into Compose [PointerInputEvent]s. */
44 internal class MotionEventAdapter {
45 
46     private var nextId = 0L
47 
48     /**
49      * Whenever a new MotionEvent pointer is added, we create a new PointerId that is associated
50      * with it. This holds that association.
51      */
52     @VisibleForTesting internal val motionEventToComposePointerIdMap = SparseLongArray()
53 
54     private val activeHoverIds = SparseBooleanArray()
55 
56     private val pointers = mutableListOf<PointerInputEventData>()
57 
58     /**
59      * The previous event's tool type. This is used in combination with [previousSource] to
60      * determine when a different device was used to send events.
61      */
62     private var previousToolType = -1
63 
64     /**
65      * The previous event's source. This is used in combination with [previousToolType] to determine
66      * when a different device was used to send events.
67      */
68     private var previousSource = -1
69 
70     /**
71      * Converts a single [MotionEvent] from an Android event stream into a [PointerInputEvent], or
72      * null if the [MotionEvent.getActionMasked] is [ACTION_CANCEL].
73      *
74      * All MotionEvents should be passed to this method so that it can correctly maintain it's
75      * internal state.
76      *
77      * @param motionEvent The MotionEvent to process.
78      * @return The PointerInputEvent or null if the event action was ACTION_CANCEL.
79      */
80     internal fun convertToPointerInputEvent(
81         motionEvent: MotionEvent,
82         positionCalculator: PositionCalculator
83     ): PointerInputEvent? {
84         val action = motionEvent.actionMasked
85         if (action == ACTION_CANCEL || action == ACTION_OUTSIDE) {
86             motionEventToComposePointerIdMap.clear()
87             activeHoverIds.clear()
88             return null
89         }
90         clearOnDeviceChange(motionEvent)
91 
92         addFreshIds(motionEvent)
93 
94         val isHover =
95             action == ACTION_HOVER_ENTER ||
96                 action == ACTION_HOVER_MOVE ||
97                 action == ACTION_HOVER_EXIT
98 
99         val isScroll = action == ACTION_SCROLL
100 
101         if (isHover) {
102             val hoverId = motionEvent.getPointerId(motionEvent.actionIndex)
103             activeHoverIds.put(hoverId, true)
104         }
105 
106         val upIndex =
107             when (action) {
108                 ACTION_UP -> 0
109                 ACTION_POINTER_UP -> motionEvent.actionIndex
110                 else -> -1
111             }
112 
113         pointers.clear()
114 
115         // This converts the MotionEvent into a list of PointerInputEventData, and updates
116         // internal record keeping.
117         for (i in 0 until motionEvent.pointerCount) {
118             pointers.add(
119                 createPointerInputEventData(
120                     positionCalculator,
121                     motionEvent,
122                     i,
123                     // "pressed" means:
124                     // 1. we're not hovered
125                     // 2. we didn't get UP event for a pointer
126                     // 3. button on the mouse is pressed BUT it's not a "scroll" simulated button
127                     !isHover && i != upIndex && (!isScroll || motionEvent.buttonState != 0)
128                 )
129             )
130         }
131 
132         removeStaleIds(motionEvent)
133 
134         return PointerInputEvent(motionEvent.eventTime, pointers, motionEvent)
135     }
136 
137     /**
138      * An ACTION_DOWN or ACTION_POINTER_DOWN was received, but not handled, so the stream should be
139      * considered ended.
140      */
141     fun endStream(pointerId: Int) {
142         activeHoverIds.delete(pointerId)
143         motionEventToComposePointerIdMap.delete(pointerId)
144     }
145 
146     /** Add any new pointer IDs. */
147     private fun addFreshIds(motionEvent: MotionEvent) {
148         when (motionEvent.actionMasked) {
149             ACTION_HOVER_ENTER -> {
150                 val pointerId = motionEvent.getPointerId(0)
151                 if (motionEventToComposePointerIdMap.indexOfKey(pointerId) < 0) {
152                     motionEventToComposePointerIdMap.put(pointerId, nextId++)
153                 }
154             }
155             ACTION_DOWN,
156             ACTION_POINTER_DOWN -> {
157                 val actionIndex = motionEvent.actionIndex
158                 val pointerId = motionEvent.getPointerId(actionIndex)
159                 if (motionEventToComposePointerIdMap.indexOfKey(pointerId) < 0) {
160                     motionEventToComposePointerIdMap.put(pointerId, nextId++)
161                     if (motionEvent.getToolType(actionIndex) == TOOL_TYPE_MOUSE) {
162                         activeHoverIds.put(pointerId, true)
163                     }
164                 }
165             }
166         }
167     }
168 
169     /**
170      * Remove any raised pointers if they didn't previously hover. Anything that hovers will stay
171      * until a different event causes it to be removed.
172      */
173     private fun removeStaleIds(motionEvent: MotionEvent) {
174         when (motionEvent.actionMasked) {
175             ACTION_POINTER_UP,
176             ACTION_UP -> {
177                 val actionIndex = motionEvent.actionIndex
178                 val pointerId = motionEvent.getPointerId(actionIndex)
179                 if (!activeHoverIds.get(pointerId, false)) {
180                     motionEventToComposePointerIdMap.delete(pointerId)
181                     activeHoverIds.delete(pointerId)
182                 }
183             }
184         }
185 
186         // Remove any IDs that don't currently exist in the MotionEvent.
187         // This can happen, for example, when a mouse cursor disappears and the next
188         // event is a touch event.
189         if (motionEventToComposePointerIdMap.size() > motionEvent.pointerCount) {
190             for (i in motionEventToComposePointerIdMap.size() - 1 downTo 0) {
191                 val pointerId = motionEventToComposePointerIdMap.keyAt(i)
192                 if (!motionEvent.hasPointerId(pointerId)) {
193                     motionEventToComposePointerIdMap.removeAt(i)
194                     activeHoverIds.delete(pointerId)
195                 }
196             }
197         }
198     }
199 
200     private fun MotionEvent.hasPointerId(pointerId: Int): Boolean {
201         for (i in 0 until pointerCount) {
202             if (getPointerId(i) == pointerId) {
203                 return true
204             }
205         }
206         return false
207     }
208 
209     private fun getComposePointerId(motionEventPointerId: Int): PointerId {
210         val pointerIndex = motionEventToComposePointerIdMap.indexOfKey(motionEventPointerId)
211         val id =
212             if (pointerIndex >= 0) {
213                 motionEventToComposePointerIdMap.valueAt(pointerIndex)
214             } else {
215                 // An unexpected pointer was added or we may have previously removed it
216                 val newId = nextId++
217                 motionEventToComposePointerIdMap.put(motionEventPointerId, newId)
218                 newId
219             }
220         return PointerId(id)
221     }
222 
223     /**
224      * When the device has changed (noted by source and tool type), we don't need to track any of
225      * the previous pointers.
226      */
227     private fun clearOnDeviceChange(motionEvent: MotionEvent) {
228         if (motionEvent.pointerCount != 1) {
229             return
230         }
231         val toolType = motionEvent.getToolType(0)
232         val source = motionEvent.source
233 
234         if (toolType != previousToolType || source != previousSource) {
235             previousToolType = toolType
236             previousSource = source
237             activeHoverIds.clear()
238             motionEventToComposePointerIdMap.clear()
239         }
240     }
241 
242     /** Creates a new PointerInputEventData. */
243     private fun createPointerInputEventData(
244         positionCalculator: PositionCalculator,
245         motionEvent: MotionEvent,
246         index: Int,
247         pressed: Boolean
248     ): PointerInputEventData {
249 
250         val motionEventPointerId = motionEvent.getPointerId(index)
251 
252         val pointerId = getComposePointerId(motionEventPointerId)
253 
254         val pressure = motionEvent.getPressure(index)
255 
256         var position = Offset(motionEvent.getX(index), motionEvent.getY(index))
257         val originalPositionEventPosition = position.copy()
258         val rawPosition: Offset
259         if (index == 0) {
260             rawPosition = Offset(motionEvent.rawX, motionEvent.rawY)
261             position = positionCalculator.screenToLocal(rawPosition)
262         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
263             rawPosition = MotionEventHelper.toRawOffset(motionEvent, index)
264             position = positionCalculator.screenToLocal(rawPosition)
265         } else {
266             rawPosition = positionCalculator.localToScreen(position)
267         }
268         val toolType =
269             when (motionEvent.getToolType(index)) {
270                 TOOL_TYPE_UNKNOWN -> PointerType.Unknown
271                 TOOL_TYPE_FINGER -> PointerType.Touch
272                 TOOL_TYPE_STYLUS -> PointerType.Stylus
273                 TOOL_TYPE_MOUSE -> PointerType.Mouse
274                 TOOL_TYPE_ERASER -> PointerType.Eraser
275                 else -> PointerType.Unknown
276             }
277 
278         val historical = ArrayList<HistoricalChange>(motionEvent.historySize)
279         with(motionEvent) {
280             repeat(historySize) { pos ->
281                 val x = getHistoricalX(index, pos)
282                 val y = getHistoricalY(index, pos)
283                 if (x.fastIsFinite() && y.fastIsFinite()) {
284                     val originalEventPosition = Offset(x, y) // hit path will convert to local
285                     val historicalChange =
286                         HistoricalChange(
287                             getHistoricalEventTime(pos),
288                             originalEventPosition,
289                             originalEventPosition
290                         )
291                     historical.add(historicalChange)
292                 }
293             }
294         }
295         val scrollDelta =
296             if (motionEvent.actionMasked == ACTION_SCROLL) {
297                 val x = motionEvent.getAxisValue(MotionEvent.AXIS_HSCROLL)
298                 val y = motionEvent.getAxisValue(MotionEvent.AXIS_VSCROLL)
299                 // NOTE: we invert the y scroll offset because android is special compared to other
300                 // platforms and uses the opposite sign for vertical mouse wheel scrolls. In order
301                 // to
302                 // support better x-platform mouse scroll, we invert the y-offset to be in line with
303                 // desktop and web.
304                 //
305                 // This looks more natural, because when we scroll mouse wheel up,
306                 // we move the wheel point (that touches the finger) up. And if we work in the usual
307                 // coordinate system, it means we move that point by "-1".
308                 //
309                 // Web also behaves this way. See deltaY:
310                 // https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
311                 // https://jsfiddle.net/27zwteog
312                 // (wheelDelta on the other hand is deprecated and inverted)
313                 //
314                 // We then add 0f to prevent injecting -0.0f into the pipeline, which can be
315                 // problematic when doing comparisons.
316                 Offset(x, -y + 0f)
317             } else {
318                 Offset.Zero
319             }
320 
321         val activeHover = activeHoverIds.get(motionEvent.getPointerId(index), false)
322         return PointerInputEventData(
323             pointerId,
324             motionEvent.eventTime,
325             rawPosition,
326             position,
327             pressed,
328             pressure,
329             toolType,
330             activeHover,
331             historical,
332             scrollDelta,
333             originalPositionEventPosition,
334         )
335     }
336 }
337 
338 /**
339  * This class is here to ensure that the classes that use this API will get verified and can be AOT
340  * compiled. It is expected that this class will soft-fail verification, but the classes which use
341  * this method will pass.
342  */
343 @RequiresApi(Build.VERSION_CODES.Q)
344 private object MotionEventHelper {
toRawOffsetnull345     fun toRawOffset(motionEvent: MotionEvent, index: Int): Offset {
346         return Offset(motionEvent.getRawX(index), motionEvent.getRawY(index))
347     }
348 }
349