diff options
author | Xin Li <delphij@google.com> | 2024-03-06 09:30:06 -0800 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2024-03-06 09:30:06 -0800 |
commit | 0a19d853fa8042e184788c2da2ed7d3b889b165a (patch) | |
tree | f86037efa286448f802ffcc1fce929e01f5bbe02 /src/com/android/customization/picker/clock/ui | |
parent | e97783b38925690aba7633c21effa56fbc2e0070 (diff) | |
parent | e224e186edb36908ad02a1cbaf338614ce5c7d69 (diff) | |
download | ThemePicker-0a19d853fa8042e184788c2da2ed7d3b889b165a.tar.gz |
Merge Android 14 QPR2 to AOSP main
Bug: 319669529
Merged-In: I08772f7df3f3c78e2b7c94bcadce1e0784255a77
Change-Id: I964ee4c686efede980f593364e4de38e3330eba0
Diffstat (limited to 'src/com/android/customization/picker/clock/ui')
13 files changed, 393 insertions, 480 deletions
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt index 6bd717b7..e2616c76 100644 --- a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt +++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt @@ -58,11 +58,6 @@ object ClockCarouselViewBinder { carouselView.transitionToPrevious() } ) - screenPreviewClickView.accessibilityDelegate = carouselAccessibilityDelegate - screenPreviewClickView.setOnSideClickedListener { isStart -> - if (isStart) carouselView.scrollToPrevious() else carouselView.scrollToNext() - } - lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { @@ -76,6 +71,11 @@ object ClockCarouselViewBinder { }, isTwoPaneAndSmallWidth = isTwoPaneAndSmallWidth, ) + screenPreviewClickView.accessibilityDelegate = carouselAccessibilityDelegate + screenPreviewClickView.setOnSideClickedListener { isStart -> + if (isStart) carouselView.scrollToPrevious() + else carouselView.scrollToNext() + } } } diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockSectionViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockSectionViewBinder.kt deleted file mode 100644 index 7dc0d0c4..00000000 --- a/src/com/android/customization/picker/clock/ui/binder/ClockSectionViewBinder.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.customization.picker.clock.ui.binder - -import android.view.View -import android.widget.TextView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel -import com.android.wallpaper.R -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -object ClockSectionViewBinder { - fun bind( - view: View, - viewModel: ClockSectionViewModel, - lifecycleOwner: LifecycleOwner, - onClicked: () -> Unit, - ) { - view.setOnClickListener { onClicked() } - - val selectedClockColorAndSize: TextView = - view.requireViewById(R.id.selected_clock_color_and_size) - - lifecycleOwner.lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.selectedClockColorAndSizeText.collectLatest { - selectedClockColorAndSize.text = it - } - } - } - } - } -} diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt index 6e745d54..d17cdf8a 100644 --- a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt +++ b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt @@ -15,11 +15,18 @@ */ package com.android.customization.picker.clock.ui.binder +import android.content.Context import android.content.res.Configuration +import android.text.Spannable +import android.text.SpannableString +import android.text.style.TextAppearanceSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.RadioGroup.OnCheckedChangeListener import android.widget.SeekBar import androidx.core.view.doOnPreDraw import androidx.core.view.isInvisible @@ -35,7 +42,6 @@ import com.android.customization.picker.clock.shared.ClockSize import com.android.customization.picker.clock.ui.adapter.ClockSettingsTabAdapter import com.android.customization.picker.clock.ui.view.ClockCarouselView import com.android.customization.picker.clock.ui.view.ClockHostView -import com.android.customization.picker.clock.ui.view.ClockSizeRadioButtonGroup import com.android.customization.picker.clock.ui.view.ClockViewFactory import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel import com.android.customization.picker.color.ui.binder.ColorOptionIconBinder @@ -83,14 +89,27 @@ object ClockSettingsBinder { } ) - val sizeOptions = - view.requireViewById<ClockSizeRadioButtonGroup>(R.id.clock_size_radio_button_group) - sizeOptions.onRadioButtonClickListener = - object : ClockSizeRadioButtonGroup.OnRadioButtonClickListener { - override fun onClick(size: ClockSize) { - viewModel.setClockSize(size) - } + val onCheckedChangeListener = OnCheckedChangeListener { _, id -> + when (id) { + R.id.radio_dynamic -> viewModel.setClockSize(ClockSize.DYNAMIC) + R.id.radio_small -> viewModel.setClockSize(ClockSize.SMALL) } + } + val clockSizeRadioGroup = + view.requireViewById<RadioGroup>(R.id.clock_size_radio_button_group) + clockSizeRadioGroup.setOnCheckedChangeListener(onCheckedChangeListener) + view.requireViewById<RadioButton>(R.id.radio_dynamic).text = + getRadioText( + view.context.applicationContext, + view.resources.getString(R.string.clock_size_dynamic), + view.resources.getString(R.string.clock_size_dynamic_description) + ) + view.requireViewById<RadioButton>(R.id.radio_small).text = + getRadioText( + view.context.applicationContext, + view.resources.getString(R.string.clock_size_small), + view.resources.getString(R.string.clock_size_small_description) + ) val colorOptionContainer = view.requireViewById<View>(R.id.color_picker_container) lifecycleOwner.lifecycleScope.launch { @@ -110,11 +129,11 @@ object ClockSettingsBinder { when (tab) { ClockSettingsViewModel.Tab.COLOR -> { colorOptionContainer.isVisible = true - sizeOptions.isInvisible = true + clockSizeRadioGroup.isInvisible = true } ClockSettingsViewModel.Tab.SIZE -> { colorOptionContainer.isInvisible = true - sizeOptions.isVisible = true + clockSizeRadioGroup.isVisible = true } } } @@ -122,6 +141,7 @@ object ClockSettingsBinder { launch { viewModel.colorOptions.collect { colorOptions -> + colorOptionContainerListView.removeAllViews() colorOptions.forEachIndexed { index, colorOption -> colorOption.payload?.let { payload -> val item = @@ -189,16 +209,28 @@ object ClockSettingsBinder { clockHostView.addView(clockView) when (size) { ClockSize.DYNAMIC -> { - sizeOptions.radioButtonDynamic.isChecked = true - sizeOptions.radioButtonSmall.isChecked = false + // When clock size data flow emits clock size signal, we want + // to update the view without triggering on checked change, + // which is supposed to be triggered by user interaction only. + clockSizeRadioGroup.setOnCheckedChangeListener(null) + clockSizeRadioGroup.check(R.id.radio_dynamic) + clockSizeRadioGroup.setOnCheckedChangeListener( + onCheckedChangeListener + ) clockHostView.doOnPreDraw { it.pivotX = it.width / 2F it.pivotY = it.height / 2F } } ClockSize.SMALL -> { - sizeOptions.radioButtonDynamic.isChecked = false - sizeOptions.radioButtonSmall.isChecked = true + // When clock size data flow emits clock size signal, we want + // to update the view without triggering on checked change, + // which is supposed to be triggered by user interaction only. + clockSizeRadioGroup.setOnCheckedChangeListener(null) + clockSizeRadioGroup.check(R.id.radio_small) + clockSizeRadioGroup.setOnCheckedChangeListener( + onCheckedChangeListener + ) clockHostView.doOnPreDraw { it.pivotX = ClockCarouselView.getCenteredHostViewPivotX(it) it.pivotY = 0F @@ -238,4 +270,25 @@ object ClockSettingsBinder { } ) } + + private fun getRadioText( + context: Context, + title: String, + description: String + ): SpannableString { + val text = SpannableString(title + "\n" + description) + text.setSpan( + TextAppearanceSpan(context, R.style.SectionTitleTextStyle), + 0, + title.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + text.setSpan( + TextAppearanceSpan(context, R.style.SectionSubtitleTextStyle), + title.length + 1, + title.length + 1 + description.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + return text + } } diff --git a/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt b/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt index 4805f376..dc70633e 100644 --- a/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt +++ b/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt @@ -68,7 +68,7 @@ class ClockSettingsFragment : AppbarFragment() { val injector = InjectorProvider.getInjector() as ThemePickerInjector val lockScreenView: CardView = view.requireViewById(R.id.lock_preview) - val colorViewModel = injector.getWallpaperColorsViewModel() + val wallpaperColorsRepository = injector.getWallpaperColorsRepository() val displayUtils = injector.getDisplayUtils(context) ScreenPreviewBinder.bind( activity = activity, @@ -88,18 +88,18 @@ class ClockSettingsFragment : AppbarFragment() { injector .getCurrentWallpaperInfoFactory(context) .createCurrentWallpaperInfos( - { homeWallpaper, lockWallpaper, _ -> - continuation.resume( - lockWallpaper ?: homeWallpaper, - null, - ) - }, + context, forceReload, - ) + ) { homeWallpaper, lockWallpaper, _ -> + continuation.resume( + lockWallpaper ?: homeWallpaper, + null, + ) + } } }, onWallpaperColorChanged = { colors -> - colorViewModel.setLockWallpaperColors(colors) + wallpaperColorsRepository.setLockWallpaperColors(colors) }, initialExtrasProvider = { Bundle().apply { @@ -125,7 +125,7 @@ class ClockSettingsFragment : AppbarFragment() { this, injector.getClockSettingsViewModelFactory( context, - injector.getWallpaperColorsViewModel(), + injector.getWallpaperColorsRepository(), injector.getClockViewFactory(activity), ), ) diff --git a/src/com/android/customization/picker/clock/ui/section/ClockSectionController.kt b/src/com/android/customization/picker/clock/ui/section/ClockSectionController.kt deleted file mode 100644 index b47c2433..00000000 --- a/src/com/android/customization/picker/clock/ui/section/ClockSectionController.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.customization.picker.clock.ui.section - -import android.content.Context -import android.view.LayoutInflater -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import com.android.customization.picker.clock.ui.binder.ClockSectionViewBinder -import com.android.customization.picker.clock.ui.fragment.ClockSettingsFragment -import com.android.customization.picker.clock.ui.view.ClockSectionView -import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel -import com.android.wallpaper.R -import com.android.wallpaper.config.BaseFlags -import com.android.wallpaper.model.CustomizationSectionController -import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController -import kotlinx.coroutines.launch - -/** A [CustomizationSectionController] for clock customization. */ -class ClockSectionController( - private val navigationController: CustomizationSectionNavigationController, - private val lifecycleOwner: LifecycleOwner, - private val flag: BaseFlags, - private val viewModel: ClockSectionViewModel, -) : CustomizationSectionController<ClockSectionView> { - - override fun isAvailable(context: Context): Boolean { - return flag.isCustomClocksEnabled(context!!) - } - - override fun createView(context: Context): ClockSectionView { - val view = - LayoutInflater.from(context) - .inflate( - R.layout.clock_section_view, - null, - ) as ClockSectionView - lifecycleOwner.lifecycleScope.launch { - ClockSectionViewBinder.bind( - view = view, - viewModel = viewModel, - lifecycleOwner = lifecycleOwner - ) { - navigationController.navigateTo(ClockSettingsFragment()) - } - } - return view - } -} diff --git a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt index d4f501b7..cae4e06b 100644 --- a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt +++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt @@ -31,7 +31,7 @@ import androidx.core.view.get import androidx.core.view.isNotEmpty import com.android.customization.picker.clock.shared.ClockSize import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselItemViewModel -import com.android.systemui.plugins.ClockController +import com.android.systemui.plugins.clocks.ClockController import com.android.wallpaper.R import com.android.wallpaper.picker.FixedWidthDisplayRatioFrameLayout import java.lang.Float.max @@ -384,7 +384,7 @@ class ClockCarouselView( ) : Carousel.Adapter { fun getContentDescription(index: Int, resources: Resources): String { - return clocks[index].getContentDescription(resources) + return clocks[index].contentDescription } override fun count(): Int { diff --git a/src/com/android/customization/picker/clock/ui/view/ClockSizeRadioButtonGroup.kt b/src/com/android/customization/picker/clock/ui/view/ClockSizeRadioButtonGroup.kt deleted file mode 100644 index 909491a3..00000000 --- a/src/com/android/customization/picker/clock/ui/view/ClockSizeRadioButtonGroup.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.customization.picker.clock.ui.view - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.FrameLayout -import android.widget.RadioButton -import com.android.customization.picker.clock.shared.ClockSize -import com.android.wallpaper.R - -/** The radio button group to pick the clock size. */ -class ClockSizeRadioButtonGroup( - context: Context, - attrs: AttributeSet?, -) : FrameLayout(context, attrs) { - - interface OnRadioButtonClickListener { - fun onClick(size: ClockSize) - } - - val radioButtonDynamic: RadioButton - val radioButtonSmall: RadioButton - var onRadioButtonClickListener: OnRadioButtonClickListener? = null - - init { - LayoutInflater.from(context).inflate(R.layout.clock_size_radio_button_group, this, true) - radioButtonDynamic = requireViewById(R.id.radio_button_dynamic) - val buttonDynamic = requireViewById<View>(R.id.button_container_dynamic) - buttonDynamic.setOnClickListener { onRadioButtonClickListener?.onClick(ClockSize.DYNAMIC) } - radioButtonSmall = requireViewById(R.id.radio_button_large) - val buttonLarge = requireViewById<View>(R.id.button_container_small) - buttonLarge.setOnClickListener { onRadioButtonClickListener?.onClick(ClockSize.SMALL) } - } -} diff --git a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt index 3f6f423f..2ab162d3 100644 --- a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt +++ b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt @@ -15,226 +15,38 @@ */ package com.android.customization.picker.clock.ui.view -import android.app.WallpaperColors -import android.app.WallpaperManager -import android.content.Context -import android.content.res.Resources -import android.graphics.Point -import android.graphics.Rect import android.view.View -import android.widget.FrameLayout import androidx.annotation.ColorInt -import androidx.core.text.util.LocalePreferences import androidx.lifecycle.LifecycleOwner -import com.android.systemui.plugins.ClockController -import com.android.systemui.plugins.WeatherData -import com.android.systemui.shared.clocks.ClockRegistry -import com.android.wallpaper.R -import com.android.wallpaper.util.TimeUtils.TimeTicker -import java.util.concurrent.ConcurrentHashMap +import com.android.systemui.plugins.clocks.ClockController -/** - * Provide reusable clock view and related util functions. - * - * @property screenSize The Activity or Fragment's window size. - */ -class ClockViewFactory( - private val appContext: Context, - val screenSize: Point, - private val wallpaperManager: WallpaperManager, - private val registry: ClockRegistry, -) { - private val resources = appContext.resources - private val timeTickListeners: ConcurrentHashMap<Int, TimeTicker> = ConcurrentHashMap() - private val clockControllers: HashMap<String, ClockController> = HashMap() - private val smallClockFrames: HashMap<String, FrameLayout> = HashMap() +interface ClockViewFactory { - fun getController(clockId: String): ClockController { - return clockControllers[clockId] - ?: initClockController(clockId).also { clockControllers[clockId] = it } - } + fun getController(clockId: String): ClockController /** * Reset the large view to its initial state when getting the view. This is because some view * configs, e.g. animation state, might change during the reuse of the clock view in the app. */ - fun getLargeView(clockId: String): View { - return getController(clockId).largeClock.let { - it.animations.onPickerCarouselSwiping(1F) - it.view - } - } + fun getLargeView(clockId: String): View /** * Reset the small view to its initial state when getting the view. This is because some view * configs, e.g. translation X, might change during the reuse of the clock view in the app. */ - fun getSmallView(clockId: String): View { - val smallClockFrame = - smallClockFrames[clockId] - ?: createSmallClockFrame().also { - it.addView(getController(clockId).smallClock.view) - smallClockFrames[clockId] = it - } - smallClockFrame.translationX = 0F - smallClockFrame.translationY = 0F - return smallClockFrame - } - - private fun createSmallClockFrame(): FrameLayout { - val smallClockFrame = FrameLayout(appContext) - val layoutParams = - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - resources.getDimensionPixelSize(R.dimen.small_clock_height) - ) - layoutParams.topMargin = getSmallClockTopMargin() - layoutParams.marginStart = getSmallClockStartPadding() - smallClockFrame.layoutParams = layoutParams - smallClockFrame.clipChildren = false - return smallClockFrame - } - - private fun getSmallClockTopMargin() = - getStatusBarHeight(appContext.resources) + - appContext.resources.getDimensionPixelSize(R.dimen.small_clock_padding_top) - - private fun getSmallClockStartPadding() = - appContext.resources.getDimensionPixelSize(R.dimen.clock_padding_start) - - fun updateColorForAllClocks(@ColorInt seedColor: Int?) { - clockControllers.values.forEach { it.events.onSeedColorChanged(seedColor = seedColor) } - } - - fun updateColor(clockId: String, @ColorInt seedColor: Int?) { - clockControllers[clockId]?.events?.onSeedColorChanged(seedColor) - } - - fun updateRegionDarkness() { - val isRegionDark = isLockscreenWallpaperDark() - clockControllers.values.forEach { - it.largeClock.events.onRegionDarknessChanged(isRegionDark) - it.smallClock.events.onRegionDarknessChanged(isRegionDark) - } - } - - private fun isLockscreenWallpaperDark(): Boolean { - val colors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK) - return (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) == 0 - } - - fun updateTimeFormat(clockId: String) { - getController(clockId) - .events - .onTimeFormatChanged(android.text.format.DateFormat.is24HourFormat(appContext)) - } + fun getSmallView(clockId: String): View - fun registerTimeTicker(owner: LifecycleOwner) { - val hashCode = owner.hashCode() - if (timeTickListeners.keys.contains(hashCode)) { - return - } + fun updateColorForAllClocks(@ColorInt seedColor: Int?) - timeTickListeners[hashCode] = TimeTicker.registerNewReceiver(appContext) { onTimeTick() } - } + fun updateColor(clockId: String, @ColorInt seedColor: Int?) - fun onDestroy() { - timeTickListeners.forEach { (_, timeTicker) -> appContext.unregisterReceiver(timeTicker) } - timeTickListeners.clear() - clockControllers.clear() - smallClockFrames.clear() - } + fun updateRegionDarkness() - private fun onTimeTick() { - clockControllers.values.forEach { - it.largeClock.events.onTimeTick() - it.smallClock.events.onTimeTick() - } - } + fun updateTimeFormat(clockId: String) - fun unregisterTimeTicker(owner: LifecycleOwner) { - val hashCode = owner.hashCode() - timeTickListeners[hashCode]?.let { - appContext.unregisterReceiver(it) - timeTickListeners.remove(hashCode) - } - } - - private fun initClockController(clockId: String): ClockController { - val controller = - registry.createExampleClock(clockId).also { it?.initialize(resources, 0f, 0f) } - checkNotNull(controller) - - val isWallpaperDark = isLockscreenWallpaperDark() - // Initialize large clock - controller.largeClock.events.onRegionDarknessChanged(isWallpaperDark) - controller.largeClock.events.onFontSettingChanged( - resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat() - ) - controller.largeClock.events.onTargetRegionChanged(getLargeClockRegion()) - - // Initialize small clock - controller.smallClock.events.onRegionDarknessChanged(isWallpaperDark) - controller.smallClock.events.onFontSettingChanged( - resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat() - ) - controller.smallClock.events.onTargetRegionChanged(getSmallClockRegion()) - - // Use placeholder for weather clock preview in picker. - // Use locale default temp unit since assistant default is not available in this context. - val useCelsius = - LocalePreferences.getTemperatureUnit() == LocalePreferences.TemperatureUnit.CELSIUS - controller.events.onWeatherDataChanged( - WeatherData( - description = DESCRIPTION_PLACEHODLER, - state = WEATHERICON_PLACEHOLDER, - temperature = - if (useCelsius) TEMPERATURE_CELSIUS_PLACEHOLDER - else TEMPERATURE_FAHRENHEIT_PLACEHOLDER, - useCelsius = useCelsius, - ) - ) - return controller - } - - /** - * Simulate the function of getLargeClockRegion in KeyguardClockSwitch so that we can get a - * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale - * and position the clock view - */ - private fun getLargeClockRegion(): Rect { - val largeClockTopMargin = - resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin) - val targetHeight = resources.getDimensionPixelSize(R.dimen.large_clock_text_size) * 2 - val top = (screenSize.y / 2 - targetHeight / 2 + largeClockTopMargin / 2) - return Rect(0, top, screenSize.x, (top + targetHeight)) - } - - /** - * Simulate the function of getSmallClockRegion in KeyguardClockSwitch so that we can get a - * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale - * and position the clock view - */ - private fun getSmallClockRegion(): Rect { - val topMargin = getSmallClockTopMargin() - val targetHeight = resources.getDimensionPixelSize(R.dimen.small_clock_height) - return Rect(getSmallClockStartPadding(), topMargin, screenSize.x, topMargin + targetHeight) - } + fun registerTimeTicker(owner: LifecycleOwner) - companion object { - const val DESCRIPTION_PLACEHODLER = "" - const val TEMPERATURE_FAHRENHEIT_PLACEHOLDER = 58 - const val TEMPERATURE_CELSIUS_PLACEHOLDER = 21 - val WEATHERICON_PLACEHOLDER = WeatherData.WeatherStateIcon.MOSTLY_SUNNY - const val USE_CELSIUS_PLACEHODLER = false + fun onDestroy() - private fun getStatusBarHeight(resource: Resources): Int { - var result = 0 - val resourceId: Int = resource.getIdentifier("status_bar_height", "dimen", "android") - if (resourceId > 0) { - result = resource.getDimensionPixelSize(resourceId) - } - return result - } - } + fun unregisterTimeTicker(owner: LifecycleOwner) } diff --git a/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt b/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt new file mode 100644 index 00000000..5caea58d --- /dev/null +++ b/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.customization.picker.clock.ui.view + +import android.app.WallpaperColors +import android.app.WallpaperManager +import android.content.Context +import android.content.res.Resources +import android.graphics.Point +import android.graphics.Rect +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.ColorInt +import androidx.core.text.util.LocalePreferences +import androidx.lifecycle.LifecycleOwner +import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.shared.clocks.ClockRegistry +import com.android.wallpaper.R +import com.android.wallpaper.util.TimeUtils.TimeTicker +import java.util.concurrent.ConcurrentHashMap + +/** + * Provide reusable clock view and related util functions. + * + * @property screenSize The Activity or Fragment's window size. + */ +class ClockViewFactoryImpl( + private val appContext: Context, + val screenSize: Point, + private val wallpaperManager: WallpaperManager, + private val registry: ClockRegistry, +) : ClockViewFactory { + private val resources = appContext.resources + private val timeTickListeners: ConcurrentHashMap<Int, TimeTicker> = ConcurrentHashMap() + private val clockControllers: HashMap<String, ClockController> = HashMap() + private val smallClockFrames: HashMap<String, FrameLayout> = HashMap() + + override fun getController(clockId: String): ClockController { + return clockControllers[clockId] + ?: initClockController(clockId).also { clockControllers[clockId] = it } + } + + /** + * Reset the large view to its initial state when getting the view. This is because some view + * configs, e.g. animation state, might change during the reuse of the clock view in the app. + */ + override fun getLargeView(clockId: String): View { + return getController(clockId).largeClock.let { + it.animations.onPickerCarouselSwiping(1F) + it.view + } + } + + /** + * Reset the small view to its initial state when getting the view. This is because some view + * configs, e.g. translation X, might change during the reuse of the clock view in the app. + */ + override fun getSmallView(clockId: String): View { + val smallClockFrame = + smallClockFrames[clockId] + ?: createSmallClockFrame().also { + it.addView(getController(clockId).smallClock.view) + smallClockFrames[clockId] = it + } + smallClockFrame.translationX = 0F + smallClockFrame.translationY = 0F + return smallClockFrame + } + + private fun createSmallClockFrame(): FrameLayout { + val smallClockFrame = FrameLayout(appContext) + val layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + resources.getDimensionPixelSize(R.dimen.small_clock_height) + ) + layoutParams.topMargin = getSmallClockTopMargin() + layoutParams.marginStart = getSmallClockStartPadding() + smallClockFrame.layoutParams = layoutParams + smallClockFrame.clipChildren = false + return smallClockFrame + } + + private fun getSmallClockTopMargin() = + getStatusBarHeight(appContext.resources) + + appContext.resources.getDimensionPixelSize(R.dimen.small_clock_padding_top) + + private fun getSmallClockStartPadding() = + appContext.resources.getDimensionPixelSize(R.dimen.clock_padding_start) + + override fun updateColorForAllClocks(@ColorInt seedColor: Int?) { + clockControllers.values.forEach { it.events.onSeedColorChanged(seedColor = seedColor) } + } + + override fun updateColor(clockId: String, @ColorInt seedColor: Int?) { + clockControllers[clockId]?.events?.onSeedColorChanged(seedColor) + } + + override fun updateRegionDarkness() { + val isRegionDark = isLockscreenWallpaperDark() + clockControllers.values.forEach { + it.largeClock.events.onRegionDarknessChanged(isRegionDark) + it.smallClock.events.onRegionDarknessChanged(isRegionDark) + } + } + + private fun isLockscreenWallpaperDark(): Boolean { + val colors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK) + return (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) == 0 + } + + override fun updateTimeFormat(clockId: String) { + getController(clockId) + .events + .onTimeFormatChanged(android.text.format.DateFormat.is24HourFormat(appContext)) + } + + override fun registerTimeTicker(owner: LifecycleOwner) { + val hashCode = owner.hashCode() + if (timeTickListeners.keys.contains(hashCode)) { + return + } + + timeTickListeners[hashCode] = TimeTicker.registerNewReceiver(appContext) { onTimeTick() } + } + + override fun onDestroy() { + timeTickListeners.forEach { (_, timeTicker) -> appContext.unregisterReceiver(timeTicker) } + timeTickListeners.clear() + clockControllers.clear() + smallClockFrames.clear() + } + + private fun onTimeTick() { + clockControllers.values.forEach { + it.largeClock.events.onTimeTick() + it.smallClock.events.onTimeTick() + } + } + + override fun unregisterTimeTicker(owner: LifecycleOwner) { + val hashCode = owner.hashCode() + timeTickListeners[hashCode]?.let { + appContext.unregisterReceiver(it) + timeTickListeners.remove(hashCode) + } + } + + private fun initClockController(clockId: String): ClockController { + val controller = + registry.createExampleClock(clockId).also { it?.initialize(resources, 0f, 0f) } + checkNotNull(controller) + + val isWallpaperDark = isLockscreenWallpaperDark() + // Initialize large clock + controller.largeClock.events.onRegionDarknessChanged(isWallpaperDark) + controller.largeClock.events.onFontSettingChanged( + resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat() + ) + controller.largeClock.events.onTargetRegionChanged(getLargeClockRegion()) + + // Initialize small clock + controller.smallClock.events.onRegionDarknessChanged(isWallpaperDark) + controller.smallClock.events.onFontSettingChanged( + resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat() + ) + controller.smallClock.events.onTargetRegionChanged(getSmallClockRegion()) + + // Use placeholder for weather clock preview in picker. + // Use locale default temp unit since assistant default is not available in this context. + val useCelsius = + LocalePreferences.getTemperatureUnit() == LocalePreferences.TemperatureUnit.CELSIUS + controller.events.onWeatherDataChanged( + WeatherData( + description = DESCRIPTION_PLACEHODLER, + state = WEATHERICON_PLACEHOLDER, + temperature = + if (useCelsius) TEMPERATURE_CELSIUS_PLACEHOLDER + else TEMPERATURE_FAHRENHEIT_PLACEHOLDER, + useCelsius = useCelsius, + ) + ) + return controller + } + + /** + * Simulate the function of getLargeClockRegion in KeyguardClockSwitch so that we can get a + * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale + * and position the clock view + */ + private fun getLargeClockRegion(): Rect { + val largeClockTopMargin = + resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin) + val targetHeight = resources.getDimensionPixelSize(R.dimen.large_clock_text_size) * 2 + val top = (screenSize.y / 2 - targetHeight / 2 + largeClockTopMargin / 2) + return Rect(0, top, screenSize.x, (top + targetHeight)) + } + + /** + * Simulate the function of getSmallClockRegion in KeyguardClockSwitch so that we can get a + * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale + * and position the clock view + */ + private fun getSmallClockRegion(): Rect { + val topMargin = getSmallClockTopMargin() + val targetHeight = resources.getDimensionPixelSize(R.dimen.small_clock_height) + return Rect(getSmallClockStartPadding(), topMargin, screenSize.x, topMargin + targetHeight) + } + + companion object { + const val DESCRIPTION_PLACEHODLER = "" + const val TEMPERATURE_FAHRENHEIT_PLACEHOLDER = 58 + const val TEMPERATURE_CELSIUS_PLACEHOLDER = 21 + val WEATHERICON_PLACEHOLDER = WeatherData.WeatherStateIcon.MOSTLY_SUNNY + const val USE_CELSIUS_PLACEHODLER = false + + private fun getStatusBarHeight(resource: Resources): Int { + var result = 0 + val resourceId: Int = resource.getIdentifier("status_bar_height", "dimen", "android") + if (resourceId > 0) { + result = resource.getDimensionPixelSize(resourceId) + } + return result + } + } +} diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselItemViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselItemViewModel.kt index 98114260..e5ac953c 100644 --- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselItemViewModel.kt +++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselItemViewModel.kt @@ -15,20 +15,8 @@ */ package com.android.customization.picker.clock.ui.viewmodel -import android.content.res.Resources -import com.android.customization.module.CustomizationInjector -import com.android.wallpaper.R -import com.android.wallpaper.module.InjectorProvider - -class ClockCarouselItemViewModel(val clockId: String, val isSelected: Boolean) { - - /** Description for accessibility purposes when a clock is selected. */ - fun getContentDescription(resources: Resources): String { - val clockContent = - (InjectorProvider.getInjector() as? CustomizationInjector) - ?.getClockDescriptionUtils(resources) - ?.getDescription(clockId) - ?: "" - return resources.getString(R.string.select_clock_action_description, clockContent) - } -} +class ClockCarouselItemViewModel( + val clockId: String, + val isSelected: Boolean, + val contentDescription: String, +) diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt index 27c25a20..3f6394be 100644 --- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt +++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt @@ -15,12 +15,15 @@ */ package com.android.customization.picker.clock.ui.viewmodel +import android.content.res.Resources import android.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.android.customization.module.logging.ThemesUserEventLogger import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor import com.android.customization.picker.clock.shared.ClockSize +import com.android.customization.picker.clock.ui.view.ClockViewFactory import com.android.wallpaper.R import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -43,6 +46,9 @@ import kotlinx.coroutines.launch class ClockCarouselViewModel( private val interactor: ClockPickerInteractor, private val backgroundDispatcher: CoroutineDispatcher, + private val clockViewFactory: ClockViewFactory, + private val resources: Resources, + private val logger: ThemesUserEventLogger, ) : ViewModel() { @OptIn(ExperimentalCoroutinesApi::class) val allClocks: StateFlow<List<ClockCarouselItemViewModel>> = @@ -50,7 +56,14 @@ class ClockCarouselViewModel( .mapLatest { allClocks -> // Delay to avoid the case that the full list of clocks is not initiated. delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS) - allClocks.map { ClockCarouselItemViewModel(it.clockId, it.isSelected) } + allClocks.map { + val contentDescription = + resources.getString( + R.string.select_clock_action_description, + clockViewFactory.getController(it.clockId).config.description + ) + ClockCarouselItemViewModel(it.clockId, it.isSelected, contentDescription) + } } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) @@ -111,18 +124,27 @@ class ClockCarouselViewModel( fun setSelectedClock(clockId: String) { setSelectedClockJob?.cancel() setSelectedClockJob = - viewModelScope.launch(backgroundDispatcher) { interactor.setSelectedClock(clockId) } + viewModelScope.launch(backgroundDispatcher) { + interactor.setSelectedClock(clockId) + logger.logClockApplied(clockId) + } } class Factory( private val interactor: ClockPickerInteractor, private val backgroundDispatcher: CoroutineDispatcher, + private val clockViewFactory: ClockViewFactory, + private val resources: Resources, + private val logger: ThemesUserEventLogger, ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return ClockCarouselViewModel( interactor = interactor, backgroundDispatcher = backgroundDispatcher, + clockViewFactory = clockViewFactory, + resources = resources, + logger = logger, ) as T } diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt deleted file mode 100644 index 8a655225..00000000 --- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSectionViewModel.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package com.android.customization.picker.clock.ui.viewmodel - -import android.content.Context -import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor -import com.android.customization.picker.clock.shared.ClockSize -import com.android.wallpaper.R -import java.util.Locale -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map - -/** View model for the clock section view on the lockscreen customization surface. */ -class ClockSectionViewModel(context: Context, interactor: ClockPickerInteractor) { - val appContext: Context = context.applicationContext - val clockColorMap: Map<String, ClockColorViewModel> = - ClockColorViewModel.getPresetColorMap(appContext.resources) - val selectedClockColorAndSizeText: Flow<String> = - combine(interactor.selectedColorId, interactor.selectedClockSize, ::Pair).map { - (selectedColorId, selectedClockSize) -> - val colorText = - clockColorMap[selectedColorId]?.colorName - ?: appContext.getString(R.string.default_theme_title) - val sizeText = - when (selectedClockSize) { - ClockSize.SMALL -> appContext.getString(R.string.clock_size_small) - ClockSize.DYNAMIC -> appContext.getString(R.string.clock_size_dynamic) - } - appContext - .getString(R.string.clock_color_and_size_description, colorText, sizeText) - .lowercase() - .replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() - } - } -} diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt index a498c716..d0e4f8fe 100644 --- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt +++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt @@ -21,9 +21,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.android.customization.model.color.ColorOptionImpl +import com.android.customization.module.logging.ThemesUserEventLogger +import com.android.customization.module.logging.ThemesUserEventLogger.Companion.NULL_SEED_COLOR import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor import com.android.customization.picker.clock.shared.ClockSize import com.android.customization.picker.clock.shared.model.ClockMetadataModel +import com.android.customization.picker.clock.shared.toClockSizeForLogging import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor import com.android.customization.picker.color.shared.model.ColorOptionModel import com.android.customization.picker.color.shared.model.ColorType @@ -53,6 +56,7 @@ private constructor( private val clockPickerInteractor: ClockPickerInteractor, private val colorPickerInteractor: ColorPickerInteractor, private val getIsReactiveToTone: (clockId: String?) -> Boolean, + private val logger: ThemesUserEventLogger, ) : ViewModel() { enum class Tab { @@ -106,15 +110,17 @@ private constructor( suspend fun onSliderProgressStop(progress: Int) { val selectedColorId = selectedColorId.value ?: return val clockColorViewModel = colorMap[selectedColorId] ?: return + val seedColor = + blendColorWithTone( + color = clockColorViewModel.color, + colorTone = clockColorViewModel.getColorTone(progress), + ) clockPickerInteractor.setClockColor( selectedColorId = selectedColorId, colorToneProgress = progress, - seedColor = - blendColorWithTone( - color = clockColorViewModel.color, - colorTone = clockColorViewModel.getColorTone(progress), - ) + seedColor = seedColor, ) + logger.logClockColorApplied(seedColor) } @OptIn(ExperimentalCoroutinesApi::class) @@ -169,18 +175,20 @@ private constructor( } else { { viewModelScope.launch { + val seedColor = + blendColorWithTone( + color = colorModel.color, + colorTone = + colorModel.getColorTone( + colorToneProgress, + ), + ) clockPickerInteractor.setClockColor( selectedColorId = colorModel.colorId, colorToneProgress = colorToneProgress, - seedColor = - blendColorWithTone( - color = colorModel.color, - colorTone = - colorModel.getColorTone( - colorToneProgress, - ), - ), + seedColor = seedColor, ) + logger.logClockColorApplied(seedColor) } } } @@ -244,6 +252,7 @@ private constructor( ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS, seedColor = null, ) + logger.logClockColorApplied(NULL_SEED_COLOR) } } } @@ -254,7 +263,10 @@ private constructor( val selectedClockSize: Flow<ClockSize> = clockPickerInteractor.selectedClockSize fun setClockSize(size: ClockSize) { - viewModelScope.launch { clockPickerInteractor.setClockSize(size) } + viewModelScope.launch { + clockPickerInteractor.setClockSize(size) + logger.logClockSizeApplied(size.toClockSizeForLogging()) + } } private val _selectedTabPosition = MutableStateFlow(Tab.COLOR) @@ -304,6 +316,7 @@ private constructor( private val context: Context, private val clockPickerInteractor: ClockPickerInteractor, private val colorPickerInteractor: ColorPickerInteractor, + private val logger: ThemesUserEventLogger, private val getIsReactiveToTone: (clockId: String?) -> Boolean, ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { @@ -312,6 +325,7 @@ private constructor( context = context, clockPickerInteractor = clockPickerInteractor, colorPickerInteractor = colorPickerInteractor, + logger = logger, getIsReactiveToTone = getIsReactiveToTone, ) as T |