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