1 /*
<lambda>null2  * 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 package com.android.customization.picker.clock.ui.view
17 
18 import android.app.WallpaperColors
19 import android.app.WallpaperManager
20 import android.content.Context
21 import android.content.res.Resources
22 import android.graphics.Point
23 import android.graphics.Rect
24 import android.view.View
25 import android.widget.FrameLayout
26 import androidx.annotation.ColorInt
27 import androidx.core.text.util.LocalePreferences
28 import androidx.lifecycle.LifecycleOwner
29 import com.android.systemui.plugins.clocks.ClockController
30 import com.android.systemui.plugins.clocks.WeatherData
31 import com.android.systemui.shared.clocks.ClockRegistry
32 import com.android.themepicker.R
33 import com.android.wallpaper.config.BaseFlags
34 import com.android.wallpaper.util.TimeUtils.TimeTicker
35 import java.util.concurrent.ConcurrentHashMap
36 
37 /**
38  * Provide reusable clock view and related util functions.
39  *
40  * @property screenSize The Activity or Fragment's window size.
41  */
42 class ClockViewFactoryImpl(
43     private val appContext: Context,
44     val screenSize: Point,
45     private val wallpaperManager: WallpaperManager,
46     private val registry: ClockRegistry,
47 ) : ClockViewFactory {
48     private val resources = appContext.resources
49     private val timeTickListeners: ConcurrentHashMap<Int, TimeTicker> = ConcurrentHashMap()
50     private val clockControllers: ConcurrentHashMap<String, ClockController> = ConcurrentHashMap()
51     private val smallClockFrames: HashMap<String, FrameLayout> = HashMap()
52 
53     override fun getController(clockId: String): ClockController {
54         return clockControllers[clockId]
55             ?: initClockController(clockId).also { clockControllers[clockId] = it }
56     }
57 
58     /**
59      * Reset the large view to its initial state when getting the view. This is because some view
60      * configs, e.g. animation state, might change during the reuse of the clock view in the app.
61      */
62     override fun getLargeView(clockId: String): View {
63         return getController(clockId).largeClock.let {
64             it.animations.onPickerCarouselSwiping(1F)
65             it.view
66         }
67     }
68 
69     /**
70      * Reset the small view to its initial state when getting the view. This is because some view
71      * configs, e.g. translation X, might change during the reuse of the clock view in the app.
72      */
73     override fun getSmallView(clockId: String): View {
74         val smallClockFrame =
75             smallClockFrames[clockId]
76                 ?: createSmallClockFrame().also {
77                     it.addView(getController(clockId).smallClock.view)
78                     smallClockFrames[clockId] = it
79                 }
80         smallClockFrame.translationX = 0F
81         smallClockFrame.translationY = 0F
82         return smallClockFrame
83     }
84 
85     /** Enables or disables the reactive swipe interaction */
86     override fun setReactiveTouchInteractionEnabled(clockId: String, enable: Boolean) {
87         check(BaseFlags.get().isClockReactiveVariantsEnabled()) {
88             "isClockReactiveVariantsEnabled is disabled"
89         }
90         getController(clockId).events.isReactiveTouchInteractionEnabled = enable
91     }
92 
93     private fun createSmallClockFrame(): FrameLayout {
94         val smallClockFrame = FrameLayout(appContext)
95         val layoutParams =
96             FrameLayout.LayoutParams(
97                 FrameLayout.LayoutParams.WRAP_CONTENT,
98                 resources.getDimensionPixelSize(
99                     com.android.systemui.customization.R.dimen.small_clock_height
100                 )
101             )
102         layoutParams.topMargin = getSmallClockTopMargin()
103         layoutParams.marginStart = getSmallClockStartPadding()
104         smallClockFrame.layoutParams = layoutParams
105         smallClockFrame.clipChildren = false
106         return smallClockFrame
107     }
108 
109     private fun getSmallClockTopMargin() =
110         getStatusBarHeight(appContext.resources) +
111             appContext.resources.getDimensionPixelSize(
112                 com.android.systemui.customization.R.dimen.small_clock_padding_top
113             )
114 
115     private fun getSmallClockStartPadding() =
116         appContext.resources.getDimensionPixelSize(
117             com.android.systemui.customization.R.dimen.clock_padding_start
118         )
119 
120     override fun updateColorForAllClocks(@ColorInt seedColor: Int?) {
121         clockControllers.values.forEach { it.events.onSeedColorChanged(seedColor = seedColor) }
122     }
123 
124     override fun updateColor(clockId: String, @ColorInt seedColor: Int?) {
125         clockControllers[clockId]?.events?.onSeedColorChanged(seedColor)
126     }
127 
128     override fun updateRegionDarkness() {
129         val isRegionDark = isLockscreenWallpaperDark()
130         clockControllers.values.forEach {
131             it.largeClock.events.onRegionDarknessChanged(isRegionDark)
132             it.smallClock.events.onRegionDarknessChanged(isRegionDark)
133         }
134     }
135 
136     private fun isLockscreenWallpaperDark(): Boolean {
137         val colors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK)
138         return (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) == 0
139     }
140 
141     override fun updateTimeFormat(clockId: String) {
142         getController(clockId)
143             .events
144             .onTimeFormatChanged(android.text.format.DateFormat.is24HourFormat(appContext))
145     }
146 
147     override fun registerTimeTicker(owner: LifecycleOwner) {
148         val hashCode = owner.hashCode()
149         if (timeTickListeners.keys.contains(hashCode)) {
150             return
151         }
152 
153         timeTickListeners[hashCode] = TimeTicker.registerNewReceiver(appContext) { onTimeTick() }
154     }
155 
156     override fun onDestroy() {
157         timeTickListeners.forEach { (_, timeTicker) -> appContext.unregisterReceiver(timeTicker) }
158         timeTickListeners.clear()
159         clockControllers.clear()
160         smallClockFrames.clear()
161     }
162 
163     private fun onTimeTick() {
164         clockControllers.values.forEach {
165             it.largeClock.events.onTimeTick()
166             it.smallClock.events.onTimeTick()
167         }
168     }
169 
170     override fun unregisterTimeTicker(owner: LifecycleOwner) {
171         val hashCode = owner.hashCode()
172         timeTickListeners[hashCode]?.let {
173             appContext.unregisterReceiver(it)
174             timeTickListeners.remove(hashCode)
175         }
176     }
177 
178     private fun initClockController(clockId: String): ClockController {
179         val controller =
180             registry.createExampleClock(clockId).also { it?.initialize(resources, 0f, 0f) }
181         checkNotNull(controller)
182 
183         val isWallpaperDark = isLockscreenWallpaperDark()
184         // Initialize large clock
185         controller.largeClock.events.onRegionDarknessChanged(isWallpaperDark)
186         controller.largeClock.events.onFontSettingChanged(
187             resources
188                 .getDimensionPixelSize(
189                     com.android.systemui.customization.R.dimen.large_clock_text_size
190                 )
191                 .toFloat()
192         )
193         controller.largeClock.events.onTargetRegionChanged(getLargeClockRegion())
194 
195         // Initialize small clock
196         controller.smallClock.events.onRegionDarknessChanged(isWallpaperDark)
197         controller.smallClock.events.onFontSettingChanged(
198             resources
199                 .getDimensionPixelSize(
200                     com.android.systemui.customization.R.dimen.small_clock_text_size
201                 )
202                 .toFloat()
203         )
204         controller.smallClock.events.onTargetRegionChanged(getSmallClockRegion())
205 
206         // Use placeholder for weather clock preview in picker.
207         // Use locale default temp unit since assistant default is not available in this context.
208         val useCelsius =
209             LocalePreferences.getTemperatureUnit() == LocalePreferences.TemperatureUnit.CELSIUS
210         controller.events.onWeatherDataChanged(
211             WeatherData(
212                 description = DESCRIPTION_PLACEHODLER,
213                 state = WEATHERICON_PLACEHOLDER,
214                 temperature =
215                     if (useCelsius) TEMPERATURE_CELSIUS_PLACEHOLDER
216                     else TEMPERATURE_FAHRENHEIT_PLACEHOLDER,
217                 useCelsius = useCelsius,
218             )
219         )
220         return controller
221     }
222 
223     /**
224      * Simulate the function of getLargeClockRegion in KeyguardClockSwitch so that we can get a
225      * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale
226      * and position the clock view
227      */
228     private fun getLargeClockRegion(): Rect {
229         val largeClockTopMargin =
230             resources.getDimensionPixelSize(
231                 com.android.systemui.customization.R.dimen.keyguard_large_clock_top_margin
232             )
233         val targetHeight =
234             resources.getDimensionPixelSize(
235                 com.android.systemui.customization.R.dimen.large_clock_text_size
236             ) * 2
237         val top = (screenSize.y / 2 - targetHeight / 2 + largeClockTopMargin / 2)
238         return Rect(0, top, screenSize.x, (top + targetHeight))
239     }
240 
241     /**
242      * Simulate the function of getSmallClockRegion in KeyguardClockSwitch so that we can get a
243      * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale
244      * and position the clock view
245      */
246     private fun getSmallClockRegion(): Rect {
247         val topMargin = getSmallClockTopMargin()
248         val targetHeight =
249             resources.getDimensionPixelSize(
250                 com.android.systemui.customization.R.dimen.small_clock_height
251             )
252         return Rect(getSmallClockStartPadding(), topMargin, screenSize.x, topMargin + targetHeight)
253     }
254 
255     companion object {
256         const val DESCRIPTION_PLACEHODLER = ""
257         const val TEMPERATURE_FAHRENHEIT_PLACEHOLDER = 58
258         const val TEMPERATURE_CELSIUS_PLACEHOLDER = 21
259         val WEATHERICON_PLACEHOLDER = WeatherData.WeatherStateIcon.MOSTLY_SUNNY
260         const val USE_CELSIUS_PLACEHODLER = false
261 
262         private fun getStatusBarHeight(resource: Resources): Int {
263             var result = 0
264             val resourceId: Int = resource.getIdentifier("status_bar_height", "dimen", "android")
265             if (resourceId > 0) {
266                 result = resource.getDimensionPixelSize(resourceId)
267             }
268             return result
269         }
270     }
271 }
272