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.view.MotionEvent 24 import android.view.View 25 import android.view.View.OnHoverListener 26 import androidx.annotation.ColorInt 27 import androidx.lifecycle.Lifecycle 28 import androidx.lifecycle.lifecycleScope 29 import androidx.lifecycle.repeatOnLifecycle 30 import com.android.systemui.res.R 31 import com.android.systemui.dagger.qualifiers.Main 32 import com.android.systemui.lifecycle.repeatWhenAttached 33 import com.android.systemui.plugins.DarkIconDispatcher 34 import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange 35 import com.android.systemui.statusbar.policy.ConfigurationController 36 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener 37 import javax.inject.Inject 38 import kotlinx.coroutines.flow.Flow 39 import kotlinx.coroutines.flow.StateFlow 40 import kotlinx.coroutines.flow.flowOf 41 import kotlinx.coroutines.flow.map 42 import kotlinx.coroutines.launch 43 44 class StatusOverlayHoverListenerFactory 45 @Inject 46 constructor( 47 @Main private val resources: Resources, 48 private val configurationController: ConfigurationController, 49 private val darkIconDispatcher: SysuiDarkIconDispatcher, 50 ) { 51 52 /** Creates listener always using the same light color for overlay */ createListenernull53 fun createListener(view: View) = 54 StatusOverlayHoverListener( 55 view, 56 configurationController, 57 resources, 58 flowOf(HoverTheme.LIGHT), 59 ) 60 61 /** 62 * Creates listener using [DarkIconDispatcher] to determine light or dark color of the overlay 63 */ 64 fun createDarkAwareListener(view: View) = 65 createDarkAwareListener(view, darkIconDispatcher.darkChangeFlow()) 66 67 /** 68 * Creates listener using provided [DarkChange] producer to determine light or dark color of the 69 * overlay 70 */ 71 fun createDarkAwareListener(view: View, darkFlow: StateFlow<DarkChange>) = 72 StatusOverlayHoverListener( 73 view, 74 configurationController, 75 resources, 76 darkFlow.map { toHoverTheme(view, it) }, 77 ) 78 toHoverThemenull79 private fun toHoverTheme(view: View, darkChange: DarkChange): HoverTheme { 80 val calculatedTint = DarkIconDispatcher.getTint(darkChange.areas, view, darkChange.tint) 81 // currently calculated tint is either white or some shade of black. 82 // So checking for Color.WHITE is deterministic compared to checking for Color.BLACK. 83 // In the future checking Color.luminance() might be more appropriate. 84 return if (calculatedTint == Color.WHITE) HoverTheme.LIGHT else HoverTheme.DARK 85 } 86 } 87 88 /** 89 * theme of hover drawable - it's different from device theme. This theme depends on view's 90 * background and/or dark value returned from [DarkIconDispatcher] 91 */ 92 enum class HoverTheme { 93 LIGHT, 94 DARK 95 } 96 97 /** 98 * [OnHoverListener] that adds [Drawable] overlay on top of the status icons when cursor/stylus 99 * starts hovering over them and removes overlay when status icons are no longer hovered 100 */ 101 class StatusOverlayHoverListener( 102 view: View, 103 configurationController: ConfigurationController, 104 private val resources: Resources, 105 private val themeFlow: Flow<HoverTheme>, 106 ) : OnHoverListener { 107 108 @ColorInt private var darkColor: Int = 0 109 @ColorInt private var lightColor: Int = 0 110 private var cornerRadius = 0f 111 112 private var lastTheme = HoverTheme.LIGHT 113 114 val backgroundColor 115 get() = if (lastTheme == HoverTheme.LIGHT) lightColor else darkColor 116 117 init { <lambda>null118 view.repeatWhenAttached { 119 lifecycleScope.launch { 120 val configurationListener = 121 object : ConfigurationListener { 122 override fun onConfigChanged(newConfig: Configuration?) { 123 updateResources() 124 } 125 } 126 repeatOnLifecycle(Lifecycle.State.CREATED) { 127 configurationController.addCallback(configurationListener) 128 } 129 configurationController.removeCallback(configurationListener) 130 } 131 lifecycleScope.launch { themeFlow.collect { lastTheme = it } } 132 } 133 updateResources() 134 } 135 onHovernull136 override fun onHover(v: View, event: MotionEvent): Boolean { 137 if (event.action == MotionEvent.ACTION_HOVER_ENTER) { 138 val drawable = 139 PaintDrawable(backgroundColor).apply { 140 setCornerRadius(cornerRadius) 141 setBounds(0, 0, v.width, v.height) 142 } 143 v.overlay.add(drawable) 144 } else if (event.action == MotionEvent.ACTION_HOVER_EXIT) { 145 v.overlay.clear() 146 } 147 return true 148 } 149 updateResourcesnull150 private fun updateResources() { 151 lightColor = resources.getColor(R.color.status_bar_icons_hover_color_light) 152 darkColor = resources.getColor(R.color.status_bar_icons_hover_color_dark) 153 cornerRadius = resources.getDimension(R.dimen.status_icons_hover_state_background_radius) 154 } 155 } 156