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