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