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