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