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.systemui.statusbar.phone 18 19 import android.content.res.Configuration 20 import android.content.res.Resources 21 import android.graphics.Color 22 import android.graphics.drawable.PaintDrawable 23 import android.util.TypedValue 24 import android.view.MotionEvent 25 import android.view.View 26 import android.view.View.OnHoverListener 27 import androidx.annotation.ColorInt 28 import androidx.lifecycle.Lifecycle 29 import androidx.lifecycle.lifecycleScope 30 import androidx.lifecycle.repeatOnLifecycle 31 import com.android.app.tracing.coroutines.launchTraced as launch 32 import com.android.systemui.dagger.qualifiers.Main 33 import com.android.systemui.lifecycle.repeatWhenAttached 34 import com.android.systemui.plugins.DarkIconDispatcher 35 import com.android.systemui.res.R 36 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController 37 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerStore 38 import com.android.systemui.statusbar.data.repository.SysuiDarkIconDispatcherStore 39 import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange 40 import com.android.systemui.statusbar.policy.ConfigurationController 41 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener 42 import javax.inject.Inject 43 import kotlinx.coroutines.flow.Flow 44 import kotlinx.coroutines.flow.StateFlow 45 import kotlinx.coroutines.flow.flowOf 46 import kotlinx.coroutines.flow.map 47 48 class StatusOverlayHoverListenerFactory 49 @Inject 50 constructor( 51 @Main private val resources: Resources, 52 private val configurationController: ConfigurationController, 53 private val darkIconDispatcherStore: SysuiDarkIconDispatcherStore, 54 private val statusBarConfigurationControllerStore: StatusBarConfigurationControllerStore, 55 ) { 56 57 /** Creates listener always using the same light color for overlay */ createListenernull58 fun createListener(view: View): StatusOverlayHoverListener = 59 StatusOverlayHoverListener( 60 view, 61 configurationController, 62 resources, 63 flowOf(HoverTheme.LIGHT), 64 ) 65 66 /** 67 * Creates listener using [DarkIconDispatcher] to determine light or dark color of the overlay 68 */ 69 fun createDarkAwareListener(view: View): StatusOverlayHoverListener? { 70 val darkIconDispatcher = view.darkIconDispatcher ?: return null 71 return createDarkAwareListener(view, darkIconDispatcher.darkChangeFlow()) 72 } 73 74 /** 75 * Creates listener using [DarkIconDispatcher] to determine light or dark color of the overlay 76 * Also sets margins for hover background relative to view bounds 77 */ createDarkAwareListenernull78 fun createDarkAwareListener( 79 view: View, 80 leftHoverMargin: Int = 0, 81 rightHoverMargin: Int = 0, 82 topHoverMargin: Int = 0, 83 bottomHoverMargin: Int = 0, 84 ): StatusOverlayHoverListener? { 85 val darkIconDispatcher = view.darkIconDispatcher ?: return null 86 return createDarkAwareListener( 87 view, 88 darkIconDispatcher.darkChangeFlow(), 89 leftHoverMargin, 90 rightHoverMargin, 91 topHoverMargin, 92 bottomHoverMargin, 93 ) 94 } 95 96 /** 97 * Creates listener using provided [DarkChange] producer to determine light or dark color of the 98 * overlay 99 */ createDarkAwareListenernull100 fun createDarkAwareListener( 101 view: View, 102 darkFlow: StateFlow<DarkChange>, 103 ): StatusOverlayHoverListener? { 104 val configurationController = view.statusBarConfigurationController ?: return null 105 return StatusOverlayHoverListener( 106 view, 107 configurationController, 108 view.resources, 109 darkFlow.map { toHoverTheme(view, it) }, 110 ) 111 } 112 createDarkAwareListenernull113 private fun createDarkAwareListener( 114 view: View, 115 darkFlow: StateFlow<DarkChange>, 116 leftHoverMargin: Int = 0, 117 rightHoverMargin: Int = 0, 118 topHoverMargin: Int = 0, 119 bottomHoverMargin: Int = 0, 120 ): StatusOverlayHoverListener? { 121 val configurationController = view.statusBarConfigurationController ?: return null 122 return StatusOverlayHoverListener( 123 view, 124 configurationController, 125 view.resources, 126 darkFlow.map { toHoverTheme(view, it) }, 127 leftHoverMargin, 128 rightHoverMargin, 129 topHoverMargin, 130 bottomHoverMargin, 131 ) 132 } 133 134 private val View.statusBarConfigurationController: StatusBarConfigurationController? 135 get() = statusBarConfigurationControllerStore.forDisplay(context.displayId) 136 137 private val View.darkIconDispatcher: SysuiDarkIconDispatcher? 138 get() = darkIconDispatcherStore.forDisplay(context.displayId) 139 toHoverThemenull140 private fun toHoverTheme(view: View, darkChange: DarkChange): HoverTheme { 141 val calculatedTint = DarkIconDispatcher.getTint(darkChange.areas, view, darkChange.tint) 142 // currently calculated tint is either white or some shade of black. 143 // So checking for Color.WHITE is deterministic compared to checking for Color.BLACK. 144 // In the future checking Color.luminance() might be more appropriate. 145 return if (calculatedTint == Color.WHITE) HoverTheme.LIGHT else HoverTheme.DARK 146 } 147 } 148 149 /** 150 * theme of hover drawable - it's different from device theme. This theme depends on view's 151 * background and/or dark value returned from [DarkIconDispatcher] 152 */ 153 enum class HoverTheme { 154 LIGHT, 155 DARK, 156 } 157 158 /** 159 * [OnHoverListener] that adds [Drawable] overlay on top of the status icons when cursor/stylus 160 * starts hovering over them and removes overlay when status icons are no longer hovered 161 */ 162 class StatusOverlayHoverListener( 163 view: View, 164 configurationController: ConfigurationController, 165 private val resources: Resources, 166 private val themeFlow: Flow<HoverTheme>, 167 private val leftHoverMargin: Int = 0, 168 private val rightHoverMargin: Int = 0, 169 private val topHoverMargin: Int = 0, 170 private val bottomHoverMargin: Int = 0, 171 ) : OnHoverListener { 172 173 @ColorInt private var darkColor: Int = 0 174 @ColorInt private var lightColor: Int = 0 175 private var cornerRadius = 0f 176 private var leftHoverMarginInPx: Int = 0 177 private var rightHoverMarginInPx: Int = 0 178 private var topHoverMarginInPx: Int = 0 179 private var bottomHoverMarginInPx: Int = 0 180 181 private var lastTheme = HoverTheme.LIGHT 182 183 val backgroundColor 184 get() = if (lastTheme == HoverTheme.LIGHT) lightColor else darkColor 185 186 init { <lambda>null187 view.repeatWhenAttached { 188 lifecycleScope.launch { 189 val configurationListener = 190 object : ConfigurationListener { 191 override fun onConfigChanged(newConfig: Configuration?) { 192 updateResources() 193 } 194 } 195 repeatOnLifecycle(Lifecycle.State.CREATED) { 196 configurationController.addCallback(configurationListener) 197 } 198 configurationController.removeCallback(configurationListener) 199 } 200 lifecycleScope.launch { themeFlow.collect { lastTheme = it } } 201 } 202 updateResources() 203 } 204 onHovernull205 override fun onHover(v: View, event: MotionEvent): Boolean { 206 if (event.action == MotionEvent.ACTION_HOVER_ENTER) { 207 val drawable = 208 PaintDrawable(backgroundColor).apply { 209 setCornerRadius(cornerRadius) 210 setBounds( 211 /*left = */ 0 + leftHoverMarginInPx, 212 /*top = */ 0 + topHoverMarginInPx, 213 /*right = */ v.width - rightHoverMarginInPx, 214 /*bottom = */ v.height - bottomHoverMarginInPx, 215 ) 216 } 217 v.overlay.add(drawable) 218 } else if (event.action == MotionEvent.ACTION_HOVER_EXIT) { 219 v.overlay.clear() 220 } 221 return true 222 } 223 updateResourcesnull224 private fun updateResources() { 225 lightColor = resources.getColor(R.color.status_bar_icons_hover_color_light) 226 darkColor = resources.getColor(R.color.status_bar_icons_hover_color_dark) 227 cornerRadius = resources.getDimension(R.dimen.status_icons_hover_state_background_radius) 228 leftHoverMarginInPx = leftHoverMargin.dpToPx(resources) 229 rightHoverMarginInPx = rightHoverMargin.dpToPx(resources) 230 topHoverMarginInPx = topHoverMargin.dpToPx(resources) 231 bottomHoverMarginInPx = bottomHoverMargin.dpToPx(resources) 232 } 233 dpToPxnull234 private fun Int.dpToPx(resources: Resources): Int { 235 return TypedValue.applyDimension( 236 TypedValue.COMPLEX_UNIT_DIP, 237 toFloat(), 238 resources.displayMetrics, 239 ) 240 .toInt() 241 } 242 } 243