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