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