• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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