1 /*
2  * 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.car.media.CarMediaManager
20 import android.content.ComponentName
21 import android.content.Context
22 import android.graphics.Color
23 import android.graphics.ColorMatrix
24 import android.graphics.ColorMatrixColorFilter
25 import android.graphics.Point
26 import android.graphics.PorterDuff
27 import android.graphics.PorterDuffColorFilter
28 import android.os.Build
29 import android.util.TypedValue
30 import android.view.View
31 import androidx.recyclerview.widget.RecyclerView
32 import com.android.car.docklib.DockInterface
33 import com.android.car.docklib.R
34 import com.android.car.docklib.data.DockAppItem
35 import com.google.android.material.imageview.ShapeableImageView
36 import java.util.concurrent.Callable
37 import java.util.concurrent.Executors
38 import java.util.concurrent.Future
39 import java.util.concurrent.TimeUnit
40 
41 /**
42  * ViewHolder of {@link DockAppItem}
43  */
44 class DockItemViewHolder(
45         private val dockController: DockInterface,
46         itemView: View,
47         private val userContext: Context,
48         private val carMediaManager: CarMediaManager?
49 ) : RecyclerView.ViewHolder(itemView) {
50 
51     companion object {
52         private const val TAG = "DockItemViewHolder"
53         private val DEBUG = Build.isDebuggable()
54 
55         /**
56          * Cleanup callback is used to reset/remove any pending views so it should be called after
57          * the new item is ready to be shown. This delay ensures new view is ready before the
58          * cleanup.
59          *
60          * todo(b/319285942): Remove fixed timer
61          */
62         private const val CLEANUP_DELAY = 500L
63         private const val MAX_WAIT_TO_COMPUTE_ICON_COLOR_MS = 500L
64     }
65 
66     private val staticIconStrokeWidth = itemView.resources
67             .getDimension(R.dimen.icon_stroke_width_static)
68     private val defaultIconColor = itemView.resources.getColor(
69             R.color.icon_default_color,
70             null // theme
71     )
72     private val appIcon: ShapeableImageView = itemView.requireViewById(R.id.dock_app_icon)
73     private val iconColorExecutor = Executors.newSingleThreadExecutor()
74     private val dockDragListener: DockDragListener
75     private val dockItemViewController: DockItemViewController
76     private var dockItemClickListener: DockItemClickListener? = null
77     private var dockItemLongClickListener: DockItemLongClickListener? = null
78     private var droppedIconColor: Int = defaultIconColor
79     private var iconColorFuture: Future<Int>? = null
80 
81     init {
82         val typedValue = TypedValue()
83         itemView.resources.getValue(
84                 R.dimen.icon_colorFilter_alpha_excited,
85                 typedValue,
86                 true // resolveRefs
87         )
88         val excitedIconColorFilterAlpha = typedValue.float
89 
90         dockItemViewController = DockItemViewController(
91             staticIconStrokeWidth,
92             dynamicIconStrokeWidth = itemView.resources
93                 .getDimension(R.dimen.icon_stroke_width_dynamic),
94             excitedIconStrokeWidth = itemView.resources
95                 .getDimension(R.dimen.icon_stroke_width_excited),
96             staticIconStrokeColor = itemView.resources.getColor(
97                 R.color.icon_static_stroke_color,
98                 null // theme
99             ),
100             excitedIconStrokeColor = itemView.resources.getColor(
101                 R.color.icon_excited_stroke_color,
102                 null // theme
103             ),
104             restrictedIconStrokeColor = itemView.resources.getColor(
105                 R.color.icon_restricted_stroke_color,
106                 null // theme
107             ),
108             defaultIconColor,
109             excitedColorFilter = PorterDuffColorFilter(
110                 Color.argb(excitedIconColorFilterAlpha, 0f, 0f, 0f),
111                 PorterDuff.Mode.DARKEN
112             ),
113             restrictedColorFilter = ColorMatrixColorFilter(
<lambda>null114                     ColorMatrix().apply { setSaturation(0f) }
115             ),
116             excitedIconColorFilterAlpha,
117             exciteAnimationDuration = itemView.resources
118                 .getInteger(R.integer.excite_icon_animation_duration_ms)
119         )
120 
121         dockDragListener = DockDragListener(
122                 resources = this.itemView.resources,
123                 object : DockDragListener.Callback {
dropSuccessfulnull124                     override fun dropSuccessful(
125                             componentName: ComponentName,
126                             cleanupCallback: Runnable?
127                     ) {
128                         (bindingAdapter as? DockAdapter)
129                                 ?.setCallback(bindingAdapterPosition, cleanupCallback)
130                         dockController.appPinned(componentName, bindingAdapterPosition)
131                     }
132 
dropAnimationsStartingnull133                     override fun dropAnimationsStarting(componentName: ComponentName) {
134                         dockItemViewController.setExcited(isExcited = false)
135                         // todo(b/320543972): Increase efficiency of dropping
136                         iconColorFuture = iconColorExecutor.submit(
137                                 Callable { dockController.getIconColorWithScrim(componentName) }
138                         )
139                     }
140 
dropAnimationScaleDownCompletenull141                     override fun dropAnimationScaleDownComplete(componentName: ComponentName) {
142                         droppedIconColor = iconColorFuture?.get(
143                                 MAX_WAIT_TO_COMPUTE_ICON_COLOR_MS,
144                                 TimeUnit.MILLISECONDS
145                         ) ?: defaultIconColor
146                         if (dockItemViewController.setUpdating(
147                             isUpdating = true,
148                             updatingColor = droppedIconColor
149                         )) {
150                             dockItemViewController.updateViewBasedOnState(appIcon)
151                         }
152                     }
153 
dropAnimationCompletenull154                     override fun dropAnimationComplete(componentName: ComponentName) {
155                         dockItemViewController.setUpdating(isUpdating = false, updatingColor = null)
156                     }
157 
exciteViewnull158                     override fun exciteView() {
159                         if (dockItemViewController.setExcited(isExcited = true)) {
160                             dockItemViewController.animateAppIconExcited(appIcon)
161                         }
162                     }
163 
resetViewnull164                     override fun resetView() {
165                         if (dockItemViewController.setExcited(isExcited = false)) {
166                             dockItemViewController.animateAppIconExcited(appIcon)
167                         }
168                     }
169 
getDropContainerLocationnull170                     override fun getDropContainerLocation(): Point {
171                         val containerLocation = itemView.locationOnScreen
172                         return Point(containerLocation[0], containerLocation[1])
173                     }
174 
getDropLocationnull175                     override fun getDropLocation(): Point {
176                         val iconLocation = appIcon.locationOnScreen
177                         return Point(
178                                 (iconLocation[0] + staticIconStrokeWidth.toInt()),
179                                 (iconLocation[1] + staticIconStrokeWidth.toInt())
180                         )
181                     }
182 
getDropWidthnull183                     override fun getDropWidth(): Float {
184                         return (appIcon.width.toFloat() - staticIconStrokeWidth * 2)
185                     }
186 
getDropHeightnull187                     override fun getDropHeight(): Float {
188                         return (appIcon.height.toFloat() - staticIconStrokeWidth * 2)
189                     }
190                 })
191     }
192 
193     /**
194      * @param callback [Runnable] to be called after the new item is bound
195      */
bindnull196     fun bind(
197         dockAppItem: DockAppItem,
198         isUxRestrictionEnabled: Boolean,
199         callback: Runnable? = null,
200         hasActiveMediaSessions: Boolean
201     ) {
202         itemTypeChanged(dockAppItem)
203         appIcon.contentDescription = dockAppItem.name
204         appIcon.setImageDrawable(dockAppItem.icon)
205         appIcon.postDelayed({ callback?.run() }, CLEANUP_DELAY)
206         dockItemClickListener = DockItemClickListener(
207             dockController,
208             dockAppItem,
209             isRestricted = !dockAppItem.isDistractionOptimized && isUxRestrictionEnabled
210         )
211         appIcon.setOnClickListener(dockItemClickListener)
212         setUxRestrictions(dockAppItem, isUxRestrictionEnabled)
213         setHasActiveMediaSession(hasActiveMediaSessions)
214         dockItemLongClickListener = DockItemLongClickListener(
215                 dockAppItem,
216                 pinItemClickDelegate = { dockController.appPinned(dockAppItem.id) },
217                 unpinItemClickDelegate = { dockController.appUnpinned(dockAppItem.id) },
218             dockAppItem.component,
219             userContext,
220             carMediaManager,
221             dockController.getMediaServiceComponents()
222         )
223         appIcon.onLongClickListener = dockItemLongClickListener
224 
225         itemView.setOnDragListener(dockDragListener)
226     }
227 
itemTypeChangednull228     fun itemTypeChanged(dockAppItem: DockAppItem) {
229         when (dockAppItem.type) {
230             DockAppItem.Type.DYNAMIC ->
231                 dockItemViewController.setDynamic(dockAppItem.iconColorWithScrim)
232 
233             DockAppItem.Type.STATIC -> dockItemViewController.setStatic()
234         }
235         dockItemViewController.updateViewBasedOnState(appIcon)
236         dockItemLongClickListener?.setDockAppItem(dockAppItem)
237     }
238 
239     /** Set if the Ux restrictions are enabled */
setUxRestrictionsnull240     fun setUxRestrictions(dockAppItem: DockAppItem, isUxRestrictionEnabled: Boolean) {
241         val shouldBeRestricted = !dockAppItem.isDistractionOptimized && isUxRestrictionEnabled
242         if (dockItemViewController.setRestricted(shouldBeRestricted)) {
243             dockItemViewController.updateViewBasedOnState(appIcon)
244             dockItemClickListener?.setIsRestricted(dockItemViewController.shouldBeRestricted())
245         }
246     }
247 
248     /** Set if item has an active media session */
setHasActiveMediaSessionnull249     fun setHasActiveMediaSession(hasActiveMediaSession: Boolean) {
250         if (dockItemViewController.setHasActiveMediaSession(hasActiveMediaSession)) {
251             dockItemViewController.updateViewBasedOnState(appIcon)
252             dockItemClickListener?.setIsRestricted(dockItemViewController.shouldBeRestricted())
253         }
254     }
255     // TODO: b/301484526 Add animation when app icon is changed
256 }
257