1 /*
<lambda>null2  * Copyright (C) 2023 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.docklib.view
18 
19 import android.content.ClipData
20 import android.content.ComponentName
21 import android.content.res.Resources
22 import android.graphics.Point
23 import android.os.Build
24 import android.util.Log
25 import android.view.DragEvent
26 import android.view.SurfaceControl
27 import android.view.SurfaceControl.Transaction
28 import android.view.View
29 import androidx.annotation.OpenForTesting
30 import androidx.annotation.VisibleForTesting
31 import androidx.core.animation.Animator
32 import androidx.core.animation.PathInterpolator
33 import androidx.core.animation.PropertyValuesHolder
34 import androidx.core.animation.ValueAnimator
35 import com.android.car.docklib.R
36 import java.util.function.Consumer
37 
38 /**
39  * [View.OnDragListener] for Dock. Receives a drop and moves it to correct location,
40  * transformed to the given size. This should be applied to all individual items in the dock that
41  * wants to receive a drop.
42  */
43 @OpenForTesting
44 open class DockDragListener(
45     resources: Resources,
46     private val callback: Callback
47 ) : View.OnDragListener {
48     companion object {
49         @VisibleForTesting
50         const val APP_ITEM_DRAG_TAG = "com.android.car.launcher.APP_ITEM_DRAG_TAG"
51 
52         @VisibleForTesting
53         const val PVH_POSITION_X = "PVH_POSITION_X"
54 
55         @VisibleForTesting
56         const val PVH_POSITION_Y = "PVH_POSITION_Y"
57 
58         @VisibleForTesting
59         const val PVH_SCALE_X = "PVH_SCALE_X"
60 
61         @VisibleForTesting
62         const val PVH_SCALE_Y = "PVH_SCALE_Y"
63 
64         private const val TAG = "DockDragListener"
65         private val DEBUG = Build.isDebuggable()
66     }
67 
68     private val scaleDownDuration =
69         resources.getInteger(R.integer.drop_animation_scale_down_duration_ms).toLong()
70     private val scaleUpDuration =
71         resources.getInteger(R.integer.drop_animation_scale_up_duration_ms).toLong()
72     private val scaleDownWidth =
73         resources.getDimension(R.dimen.drop_animation_scale_down_width)
74     private var surfaceControl: SurfaceControl? = null
75 
76     override fun onDrag(view: View, dragEvent: DragEvent): Boolean {
77         when (dragEvent.action) {
78             DragEvent.ACTION_DRAG_STARTED ->
79                 return APP_ITEM_DRAG_TAG.contentEquals(dragEvent.clipDescription?.label)
80 
81             DragEvent.ACTION_DRAG_ENTERED -> {
82                 callback.exciteView()
83                 return true
84             }
85 
86             DragEvent.ACTION_DRAG_EXITED -> {
87                 callback.resetView()
88                 return true
89             }
90 
91             DragEvent.ACTION_DROP -> {
92                 val item: ClipData.Item
93                 try {
94                     item = dragEvent.clipData.getItemAt(0)
95                     if (item.text == null) throw NullPointerException("ClipData item text is null")
96                 } catch (e: Exception) {
97                     when (e) {
98                         is IndexOutOfBoundsException, is NullPointerException -> {
99                             if (DEBUG) Log.d(TAG, "No/Invalid clipData sent with the drop: $e")
100                             callback.resetView()
101                             return false
102                         }
103 
104                         else -> {
105                             throw e
106                         }
107                     }
108                 }
109 
110                 val component: ComponentName? =
111                     ComponentName.unflattenFromString(item.text.toString())
112                 if (component == null) {
113                     if (DEBUG) {
114                         Log.d(TAG, "Invalid component string sent with drop: " + item.text)
115                     }
116                     callback.resetView()
117                     return false
118                 }
119                 if (DEBUG) Log.d(TAG, "Dropped component: $component")
120 
121                 // todo(b/312718542): hidden api(dragEvent.dragSurface) usage
122                 dragEvent.dragSurface?.let {
123                     surfaceControl = it
124                     animateSurfaceIn(it, dragEvent, component)
125                     return true
126                 } ?: run {
127                     if (DEBUG) Log.d(TAG, "Could not retrieve the drag surface")
128                     // drag is success but animation is not possible since there is no dragSurface
129                     callback.dropSuccessful(component)
130                     return false
131                 }
132             }
133         }
134         return false
135     }
136 
137     /**
138      * Animates the surface from where the it was dropped to the final position and size. Also
139      * responsible for cleaning up the surface after the animation.
140      */
141     private fun animateSurfaceIn(
142         surfaceControl: SurfaceControl,
143         dragEvent: DragEvent,
144         component: ComponentName
145     ) {
146         callback.dropAnimationsStarting(component)
147         val dropContainerLocation = callback.getDropContainerLocation()
148         // todo(b/312718542): hidden api(offsetX and offsetY) usage
149         val fromX: Float = dropContainerLocation.x + (dragEvent.x - dragEvent.offsetX)
150         val fromY: Float = dropContainerLocation.y + (dragEvent.y - dragEvent.offsetY)
151 
152         val dropLocation = callback.getDropLocation()
153         val toFinalX: Float = dropLocation.x.toFloat()
154         val toFinalY: Float = dropLocation.y.toFloat()
155         val toScaleDownLocationX = toFinalX + scaleDownWidth
156         val toScaleDownLocationY = toFinalY + scaleDownWidth
157 
158         val toFinalWidth: Float = callback.getDropWidth()
159         val toFinalHeight: Float = callback.getDropHeight()
160         val toFinalScaleX: Float = toFinalWidth / surfaceControl.width
161         val toFinalScaleY: Float = toFinalHeight / surfaceControl.height
162         val toScaleDownX: Float =
163             ((toFinalWidth - (scaleDownWidth * 2)) / surfaceControl.width).coerceAtLeast(0f)
164         val toScaleDownY: Float =
165             ((toFinalHeight - (scaleDownWidth * 2)) / surfaceControl.height).coerceAtLeast(0f)
166         if (DEBUG && (toScaleDownX <= 0 || toScaleDownY <= 0)) {
167             Log.w(
168                 TAG,
169                 "Reached negative/zero scale, decrease the value of " +
170                         "drop_animation_scale_down_width"
171             )
172         }
173 
174         val scaleDownAnimator = getAnimator(
175             surfaceControl,
176             fromX = fromX,
177             fromY = fromY,
178             toX = toScaleDownLocationX,
179             toY = toScaleDownLocationY,
180             toScaleX = toScaleDownX,
181             toScaleY = toScaleDownY,
182             animationDuration = scaleDownDuration,
183         )
184         val scaleUpAnimator = getAnimator(
185             surfaceControl,
186             fromX = toScaleDownLocationX,
187             fromY = toScaleDownLocationY,
188             toX = toFinalX,
189             toY = toFinalY,
190             fromScaleX = toScaleDownX,
191             fromScaleY = toScaleDownY,
192             toScaleX = toFinalScaleX,
193             toScaleY = toFinalScaleY,
194             animationDuration = scaleUpDuration,
195         )
196 
197         scaleDownAnimator.addListener(getAnimatorListener(onAnimationEnd = { isCancelled ->
198             if (!isCancelled) {
199                 callback.dropAnimationScaleDownComplete(component)
200                 scaleUpAnimator.start()
201             }
202         }))
203         scaleUpAnimator.addListener(getAnimatorListener(onAnimationEnd = { isCancelled ->
204             callback.dropAnimationComplete(component)
205             if (!isCancelled) callback.dropSuccessful(component, getCleanUpCallback(surfaceControl))
206         }))
207 
208         scaleDownAnimator.start()
209     }
210 
211     /**
212      * Get the animator responsible for animating the [surfaceControl] from [fromX], [fromY]
213      * to its final position [toX], [toY] with correct scale [toScaleX], [toScaleY].
214      * Default values are added to make this method easier to test. Generally all parameters are
215      * expected to be sent by the caller.
216      */
217     @VisibleForTesting
218     open fun getAnimator(
219         surfaceControl: SurfaceControl,
220         fromX: Float = 0f,
221         fromY: Float = 0f,
222         toX: Float = 0f,
223         toY: Float = 0f,
224         fromScaleX: Float = 1f,
225         fromScaleY: Float = 1f,
226         toScaleX: Float = 1f,
227         toScaleY: Float = 1f,
228         animationDuration: Long = 0L,
229     ): ValueAnimator {
230         if (DEBUG) {
231             Log.d(
232                 TAG,
233                 "getAnimator{ " +
234                         "surfaceControl: $surfaceControl, " +
235                         "(fromX: $fromX, fromY: $fromY), " +
236                         "(toX: $toX, toY: $toY), " +
237                         "(fromScaleX: $fromScaleX, fromScaleY: $fromScaleY), " +
238                         "(toScaleX: $toScaleX, toScaleY: $toScaleY) " +
239                         "}"
240             )
241         }
242 
243         val pvhX: PropertyValuesHolder =
244             PropertyValuesHolder.ofFloat(PVH_POSITION_X, fromX, toX)
245         val pvhY: PropertyValuesHolder =
246             PropertyValuesHolder.ofFloat(PVH_POSITION_Y, fromY, toY)
247         val pvhScaleX =
248             PropertyValuesHolder.ofFloat(PVH_SCALE_X, fromScaleX, toScaleX)
249         val pvhScaleY =
250             PropertyValuesHolder.ofFloat(PVH_SCALE_Y, fromScaleY, toScaleY)
251 
252         val animator: ValueAnimator =
253             ValueAnimator.ofPropertyValuesHolder(
254                 pvhX,
255                 pvhY,
256                 pvhScaleX,
257                 pvhScaleY
258             )
259         animator.setDuration(animationDuration)
260         animator.interpolator = PathInterpolator(0f, 0f, 0f, 1f)
261         val trx = Transaction()
262         animator.addUpdateListener(getAnimatorUpdateListener(surfaceControl, trx))
263         animator.addListener(getAnimatorListener { trx.close() })
264         return animator
265     }
266 
267     /**
268      * Not expected to be used directly or overridden.
269      *
270      * @param trx Transaction used to animate the [surfaceControl] in place.
271      */
272     @VisibleForTesting
273     fun getAnimatorUpdateListener(
274         surfaceControl: SurfaceControl,
275         trx: Transaction
276     ): Animator.AnimatorUpdateListener {
277         return Animator.AnimatorUpdateListener { updatedAnimation ->
278             if (updatedAnimation is ValueAnimator) {
279                 trx.setPosition(
280                     surfaceControl,
281                     updatedAnimation.getAnimatedValue(PVH_POSITION_X) as Float,
282                     updatedAnimation.getAnimatedValue(PVH_POSITION_Y) as Float
283                 ).setScale(
284                     surfaceControl,
285                     updatedAnimation.getAnimatedValue(PVH_SCALE_X) as Float,
286                     updatedAnimation.getAnimatedValue(PVH_SCALE_Y) as Float
287                 ).apply()
288             }
289         }
290     }
291 
292     /**
293      * @param onAnimationEnd called with boolean(isCancelled) set to false when animation is ended
294      * and to true when cancelled.
295      */
296     @VisibleForTesting
297     fun getAnimatorListener(
298         onAnimationEnd: Consumer<Boolean>
299     ): Animator.AnimatorListener {
300         return object : Animator.AnimatorListener {
301             private var isCancelled = false
302             override fun onAnimationStart(var1: Animator) {
303                 isCancelled = false
304             }
305 
306             override fun onAnimationEnd(var1: Animator) {
307                 if (!isCancelled) {
308                     onAnimationEnd.accept(isCancelled)
309                 }
310             }
311 
312             override fun onAnimationCancel(var1: Animator) {
313                 isCancelled = true
314                 onAnimationEnd.accept(isCancelled)
315             }
316 
317             override fun onAnimationRepeat(var1: Animator) {
318                 // no-op
319             }
320         }
321     }
322 
323     private fun getCleanUpCallback(surfaceControl: SurfaceControl): () -> Unit {
324         return {
325             if (DEBUG) Log.d(TAG, "cleanup callback called")
326             if (surfaceControl.isValid) {
327                 if (DEBUG) Log.d(TAG, "Surface is valid")
328                 val cleanupTrx = Transaction()
329                 cleanupTrx.hide(surfaceControl)
330                 cleanupTrx.remove(surfaceControl)
331                 cleanupTrx.apply()
332                 cleanupTrx.close()
333             }
334         }
335     }
336 
337     /**
338      * [DockDragListener] communicates events back and requests data from the caller using
339      * this callback.
340      */
341     interface Callback {
342         /**
343          * Drop is accepted/successful for the [componentName]
344          *
345          * @param cleanupCallback [Runnable] to be called when the dropped item is ready/drawn.
346          */
347         fun dropSuccessful(componentName: ComponentName, cleanupCallback: Runnable? = null) {}
348 
349         /**
350          * Drop animations about to start.
351          */
352         fun dropAnimationsStarting(componentName: ComponentName) {}
353 
354         /**
355          * Drop animation scale down completed.
356          */
357         fun dropAnimationScaleDownComplete(componentName: ComponentName) {}
358 
359         /**
360          * Drop animation completed.
361          */
362         fun dropAnimationComplete(componentName: ComponentName) {}
363 
364         /**
365          * Excite the view to indicate the item can be dropped in this position when dragged inside
366          * the drop bounds.
367          */
368         fun exciteView() {}
369 
370         /**
371          * Reset the view after a drop or if the drop failed or if the item is dragged outside the
372          * drop bounds.
373          */
374         fun resetView() {}
375 
376         /**
377          * Get the location of the container that holds the dropped item
378          */
379         fun getDropContainerLocation(): Point
380 
381         /**
382          * Get the final location of the dropped item
383          */
384         fun getDropLocation(): Point
385 
386         /**
387          * Get the final width of the dropped item
388          */
389         fun getDropWidth(): Float
390 
391         /**
392          * Get the final height of the dropped item
393          */
394         fun getDropHeight(): Float
395     }
396 }
397