diff options
Diffstat (limited to 'src/com/android/customization/picker')
57 files changed, 1648 insertions, 2882 deletions
diff --git a/src/com/android/customization/picker/WallpaperPreviewer.java b/src/com/android/customization/picker/WallpaperPreviewer.java index d74bfaea..18bc89c7 100644 --- a/src/com/android/customization/picker/WallpaperPreviewer.java +++ b/src/com/android/customization/picker/WallpaperPreviewer.java @@ -241,7 +241,7 @@ public class WallpaperPreviewer implements LifecycleObserver { () -> mFadeInScrim.setVisibility(View.INVISIBLE)); } } - }, mWallpaperSurface, WallpaperConnection.WHICH_PREVIEW.PREVIEW_CURRENT); + }, mWallpaperSurface, WallpaperConnection.WhichPreview.PREVIEW_CURRENT); mWallpaperConnection.setVisibility(true); mHomePreview.post(() -> { diff --git a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt index 004103f3..cc4079a1 100644 --- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt +++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt @@ -21,7 +21,7 @@ import androidx.annotation.ColorInt import androidx.annotation.IntRange import com.android.customization.picker.clock.shared.ClockSize import com.android.customization.picker.clock.shared.model.ClockMetadataModel -import com.android.systemui.plugins.ClockMetadata +import com.android.systemui.plugins.clocks.ClockMetadata import com.android.systemui.shared.clocks.ClockRegistry import com.android.wallpaper.settings.data.repository.SecureSettingsRepository import kotlinx.coroutines.CoroutineDispatcher @@ -187,7 +187,6 @@ class ClockPickerRepositoryImpl( ): ClockMetadataModel { return ClockMetadataModel( clockId = clockId, - name = name, isSelected = isSelected, selectedColorId = selectedColorId, colorToneProgress = colorTone, diff --git a/src/com/android/customization/picker/clock/shared/ClockSize.kt b/src/com/android/customization/picker/clock/shared/ClockSize.kt index 279ee54b..9b35f73f 100644 --- a/src/com/android/customization/picker/clock/shared/ClockSize.kt +++ b/src/com/android/customization/picker/clock/shared/ClockSize.kt @@ -16,7 +16,18 @@ */ package com.android.customization.picker.clock.shared +import android.stats.style.StyleEnums +import com.android.customization.module.logging.ThemesUserEventLogger + enum class ClockSize { DYNAMIC, SMALL, } + +@ThemesUserEventLogger.ClockSize +fun ClockSize.toClockSizeForLogging(): Int { + return when (this) { + ClockSize.DYNAMIC -> StyleEnums.CLOCK_SIZE_DYNAMIC + ClockSize.SMALL -> StyleEnums.CLOCK_SIZE_SMALL + } +} diff --git a/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt b/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt index 25225075..6e2bfb38 100644 --- a/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt +++ b/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt @@ -23,7 +23,6 @@ import androidx.annotation.IntRange /** Model for clock metadata. */ data class ClockMetadataModel( val clockId: String, - val name: String, val isSelected: Boolean, val selectedColorId: String?, @IntRange(from = 0, to = 100) val colorToneProgress: Int, 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 diff --git a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt index 6540ce06..942a8460 100644 --- a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt +++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt @@ -24,8 +24,8 @@ import com.android.customization.model.color.ColorOptionImpl import com.android.customization.picker.color.shared.model.ColorOptionModel import com.android.customization.picker.color.shared.model.ColorType import com.android.systemui.monet.Style -import com.android.wallpaper.model.WallpaperColorsModel -import com.android.wallpaper.model.WallpaperColorsViewModel +import com.android.wallpaper.picker.customization.data.repository.WallpaperColorsRepository +import com.android.wallpaper.picker.customization.shared.model.WallpaperColorsModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -37,14 +37,14 @@ import kotlinx.coroutines.suspendCancellableCoroutine // TODO (b/262924623): refactor to remove dependency on ColorCustomizationManager & ColorOption // TODO (b/268203200): Create test for ColorPickerRepositoryImpl class ColorPickerRepositoryImpl( - wallpaperColorsViewModel: WallpaperColorsViewModel, + wallpaperColorsRepository: WallpaperColorsRepository, private val colorManager: ColorCustomizationManager, ) : ColorPickerRepository { private val homeWallpaperColors: StateFlow<WallpaperColorsModel?> = - wallpaperColorsViewModel.homeWallpaperColors + wallpaperColorsRepository.homeWallpaperColors private val lockWallpaperColors: StateFlow<WallpaperColorsModel?> = - wallpaperColorsViewModel.lockWallpaperColors + wallpaperColorsRepository.lockWallpaperColors private var selectedColorOption: MutableStateFlow<ColorOptionModel> = MutableStateFlow(getCurrentColorOption()) @@ -78,7 +78,7 @@ class ColorPickerRepositoryImpl( homeColorsLoaded.colors, lockColorsLoaded.colors ) - colorManager.fetchRevampedUIOptions( + colorManager.fetchOptions( object : CustomizationManager.OptionsFetchedListener<ColorOption?> { override fun onOptionsLoaded(options: MutableList<ColorOption?>?) { val wallpaperColorOptions: MutableList<ColorOptionModel> = diff --git a/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt index bb2ef9d3..f35d934d 100644 --- a/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt +++ b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt @@ -19,10 +19,12 @@ package com.android.customization.picker.color.data.repository import android.content.Context import android.graphics.Color import android.text.TextUtils +import com.android.customization.model.ResourceConstants import com.android.customization.model.color.ColorOptionImpl import com.android.customization.model.color.ColorOptionsProvider import com.android.customization.picker.color.shared.model.ColorOptionModel import com.android.customization.picker.color.shared.model.ColorType +import com.android.systemui.monet.Style import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -49,6 +51,53 @@ class FakeColorPickerRepository(private val context: Context) : ColorPickerRepos } fun setOptions( + wallpaperOptions: List<ColorOptionImpl>, + presetOptions: List<ColorOptionImpl>, + selectedColorOptionType: ColorType, + selectedColorOptionIndex: Int + ) { + _colorOptions.value = + mapOf( + ColorType.WALLPAPER_COLOR to + buildList { + for ((index, colorOption) in wallpaperOptions.withIndex()) { + val isSelected = + selectedColorOptionType == ColorType.WALLPAPER_COLOR && + selectedColorOptionIndex == index + val colorOptionModel = + ColorOptionModel( + key = "${ColorType.WALLPAPER_COLOR}::$index", + colorOption = colorOption, + isSelected = isSelected + ) + if (isSelected) { + selectedColorOption = colorOptionModel + } + add(colorOptionModel) + } + }, + ColorType.PRESET_COLOR to + buildList { + for ((index, colorOption) in presetOptions.withIndex()) { + val isSelected = + selectedColorOptionType == ColorType.PRESET_COLOR && + selectedColorOptionIndex == index + val colorOptionModel = + ColorOptionModel( + key = "${ColorType.PRESET_COLOR}::$index", + colorOption = colorOption, + isSelected = isSelected + ) + if (isSelected) { + selectedColorOption = colorOptionModel + } + add(colorOptionModel) + } + }, + ) + } + + fun setOptions( numWallpaperOptions: Int, numPresetOptions: Int, selectedColorOptionType: ColorType, @@ -111,6 +160,22 @@ class FakeColorPickerRepository(private val context: Context) : ColorPickerRepos return builder.build() } + fun buildPresetOption(style: Style, seedColor: String): ColorOptionImpl { + val builder = ColorOptionImpl.Builder() + builder.lightColors = + intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT) + builder.darkColors = + intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT) + builder.type = ColorType.PRESET_COLOR + builder.source = ColorOptionsProvider.COLOR_SOURCE_PRESET + builder.style = style + builder.title = "Preset" + builder + .addOverlayPackage("TEST_PACKAGE_TYPE", "preset_color") + .addOverlayPackage(ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE, seedColor) + return builder.build() + } + private fun buildWallpaperOption(index: Int): ColorOptionImpl { val builder = ColorOptionImpl.Builder() builder.lightColors = @@ -127,6 +192,22 @@ class FakeColorPickerRepository(private val context: Context) : ColorPickerRepos return builder.build() } + fun buildWallpaperOption(source: String, style: Style, seedColor: String): ColorOptionImpl { + val builder = ColorOptionImpl.Builder() + builder.lightColors = + intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT) + builder.darkColors = + intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT) + builder.type = ColorType.WALLPAPER_COLOR + builder.source = source + builder.style = style + builder.title = "Dynamic" + builder + .addOverlayPackage("TEST_PACKAGE_TYPE", "wallpaper_color") + .addOverlayPackage(ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE, seedColor) + return builder.build() + } + override suspend fun select(colorOptionModel: ColorOptionModel) { val colorOptions = _colorOptions.value val wallpaperColorOptions = colorOptions[ColorType.WALLPAPER_COLOR]!! diff --git a/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt index 0f82f494..9838c317 100644 --- a/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt +++ b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt @@ -62,7 +62,7 @@ object ColorPickerBinder { colorTypeTabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP)) val colorOptionAdapter = OptionItemAdapter( - layoutResourceId = R.layout.color_option_2, + layoutResourceId = R.layout.color_option, lifecycleOwner = lifecycleOwner, bindIcon = { foregroundView: View, colorIcon: ColorOptionIconViewModel -> val colorOptionIconView = foregroundView as? ColorOptionIconView diff --git a/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt b/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt index 4ef29d6e..2c006090 100644 --- a/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt +++ b/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt @@ -33,11 +33,11 @@ import com.android.customization.model.mode.DarkModeSectionController import com.android.customization.module.ThemePickerInjector import com.android.customization.picker.color.ui.binder.ColorPickerBinder import com.android.wallpaper.R -import com.android.wallpaper.model.WallpaperColorsModel -import com.android.wallpaper.model.WallpaperColorsViewModel import com.android.wallpaper.module.CustomizationSections import com.android.wallpaper.module.InjectorProvider import com.android.wallpaper.picker.AppbarFragment +import com.android.wallpaper.picker.customization.data.repository.WallpaperColorsRepository +import com.android.wallpaper.picker.customization.shared.model.WallpaperColorsModel import com.android.wallpaper.picker.customization.ui.binder.ScreenPreviewBinder import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel import com.android.wallpaper.util.DisplayUtils @@ -76,7 +76,7 @@ class ColorPickerFragment : AppbarFragment() { val homeScreenView: CardView = view.requireViewById(R.id.home_preview) val wallpaperInfoFactory = injector.getCurrentWallpaperInfoFactory(requireContext()) val displayUtils: DisplayUtils = injector.getDisplayUtils(requireContext()) - val wcViewModel = injector.getWallpaperColorsViewModel() + val wallpaperColorsRepository = injector.getWallpaperColorsRepository() val wallpaperManager = WallpaperManager.getInstance(requireContext()) binding = @@ -87,7 +87,7 @@ class ColorPickerFragment : AppbarFragment() { requireActivity(), injector.getColorPickerViewModelFactory( context = requireContext(), - wallpaperColorsViewModel = wcViewModel, + wallpaperColorsRepository = wallpaperColorsRepository, ), ) .get(), @@ -114,27 +114,27 @@ class ColorPickerFragment : AppbarFragment() { wallpaperInfoProvider = { forceReload -> suspendCancellableCoroutine { continuation -> wallpaperInfoFactory.createCurrentWallpaperInfos( - { homeWallpaper, lockWallpaper, _ -> - lifecycleScope.launch { - if ( - wcViewModel.lockWallpaperColors.value - is WallpaperColorsModel.Loading - ) { - loadInitialColors( - wallpaperManager, - wcViewModel, - CustomizationSections.Screen.LOCK_SCREEN - ) - } - } - continuation.resume(lockWallpaper ?: homeWallpaper, null) - }, + context, forceReload, - ) + ) { homeWallpaper, lockWallpaper, _ -> + lifecycleScope.launch { + if ( + wallpaperColorsRepository.lockWallpaperColors.value + is WallpaperColorsModel.Loading + ) { + loadInitialColors( + wallpaperManager, + wallpaperColorsRepository, + CustomizationSections.Screen.LOCK_SCREEN + ) + } + } + continuation.resume(lockWallpaper ?: homeWallpaper, null) + } } }, onWallpaperColorChanged = { colors -> - wcViewModel.setLockWallpaperColors(colors) + wallpaperColorsRepository.setLockWallpaperColors(colors) }, wallpaperInteractor = injector.getWallpaperInteractor(requireContext()), screen = CustomizationSections.Screen.LOCK_SCREEN, @@ -165,27 +165,27 @@ class ColorPickerFragment : AppbarFragment() { wallpaperInfoProvider = { forceReload -> suspendCancellableCoroutine { continuation -> wallpaperInfoFactory.createCurrentWallpaperInfos( - { homeWallpaper, lockWallpaper, _ -> - lifecycleScope.launch { - if ( - wcViewModel.homeWallpaperColors.value - is WallpaperColorsModel.Loading - ) { - loadInitialColors( - wallpaperManager, - wcViewModel, - CustomizationSections.Screen.HOME_SCREEN - ) - } - } - continuation.resume(homeWallpaper ?: lockWallpaper, null) - }, + context, forceReload, - ) + ) { homeWallpaper, lockWallpaper, _ -> + lifecycleScope.launch { + if ( + wallpaperColorsRepository.homeWallpaperColors.value + is WallpaperColorsModel.Loading + ) { + loadInitialColors( + wallpaperManager, + wallpaperColorsRepository, + CustomizationSections.Screen.HOME_SCREEN + ) + } + } + continuation.resume(homeWallpaper ?: lockWallpaper, null) + } } }, onWallpaperColorChanged = { colors -> - wcViewModel.setHomeWallpaperColors(colors) + wallpaperColorsRepository.setHomeWallpaperColors(colors) }, wallpaperInteractor = injector.getWallpaperInteractor(requireContext()), screen = CustomizationSections.Screen.HOME_SCREEN, @@ -202,7 +202,8 @@ class ColorPickerFragment : AppbarFragment() { DarkModeSectionController( context, lifecycle, - injector.getDarkModeSnapshotRestorer(requireContext()) + injector.getDarkModeSnapshotRestorer(requireContext()), + injector.getUserEventLogger(requireContext()), ) .createView(requireContext()) darkModeSectionView.background = null @@ -218,7 +219,7 @@ class ColorPickerFragment : AppbarFragment() { private suspend fun loadInitialColors( wallpaperManager: WallpaperManager, - colorViewModel: WallpaperColorsViewModel, + colorViewModel: WallpaperColorsRepository, screen: CustomizationSections.Screen, ) { withContext(Dispatchers.IO) { diff --git a/src/com/android/customization/picker/color/ui/section/ColorSectionController2.kt b/src/com/android/customization/picker/color/ui/section/ColorSectionController.kt index f1c982b4..a36fd80a 100644 --- a/src/com/android/customization/picker/color/ui/section/ColorSectionController2.kt +++ b/src/com/android/customization/picker/color/ui/section/ColorSectionController.kt @@ -22,37 +22,37 @@ import android.view.LayoutInflater import androidx.lifecycle.LifecycleOwner import com.android.customization.picker.color.ui.binder.ColorSectionViewBinder import com.android.customization.picker.color.ui.fragment.ColorPickerFragment -import com.android.customization.picker.color.ui.view.ColorSectionView2 +import com.android.customization.picker.color.ui.view.ColorSectionView import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel import com.android.wallpaper.R import com.android.wallpaper.model.CustomizationSectionController import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController as NavigationController -class ColorSectionController2( +class ColorSectionController( private val navigationController: NavigationController, private val viewModel: ColorPickerViewModel, private val lifecycleOwner: LifecycleOwner -) : CustomizationSectionController<ColorSectionView2> { +) : CustomizationSectionController<ColorSectionView> { override fun isAvailable(context: Context): Boolean { return true } - override fun createView(context: Context): ColorSectionView2 { + override fun createView(context: Context): ColorSectionView { return createView(context, CustomizationSectionController.ViewCreationParams()) } override fun createView( context: Context, params: CustomizationSectionController.ViewCreationParams - ): ColorSectionView2 { + ): ColorSectionView { @SuppressWarnings("It is fine to inflate with null parent for our need.") val view = LayoutInflater.from(context) .inflate( - R.layout.color_section_view2, + R.layout.color_section_view, null, - ) as ColorSectionView2 + ) as ColorSectionView ColorSectionViewBinder.bind( view = view, viewModel = viewModel, diff --git a/src/com/android/customization/picker/color/ui/view/ColorSectionView2.kt b/src/com/android/customization/picker/color/ui/view/ColorSectionView.kt index 7a8f21af..a89292d8 100644 --- a/src/com/android/customization/picker/color/ui/view/ColorSectionView2.kt +++ b/src/com/android/customization/picker/color/ui/view/ColorSectionView.kt @@ -23,4 +23,4 @@ import com.android.wallpaper.picker.SectionView * The class inherits from {@link SectionView} as the view representing the color section of the * customization picker. It displays a list of color options and an overflow option. */ -class ColorSectionView2(context: Context, attrs: AttributeSet?) : SectionView(context, attrs) +class ColorSectionView(context: Context, attrs: AttributeSet?) : SectionView(context, attrs) diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt index 67c68387..ed83136e 100644 --- a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt +++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt @@ -21,6 +21,7 @@ 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.picker.color.domain.interactor.ColorPickerInteractor import com.android.customization.picker.color.shared.model.ColorType import com.android.wallpaper.R @@ -43,6 +44,7 @@ class ColorPickerViewModel private constructor( context: Context, private val interactor: ColorPickerInteractor, + private val logger: ThemesUserEventLogger, ) : ViewModel() { private val selectedColorTypeTabId = MutableStateFlow<ColorType?>(null) @@ -142,6 +144,14 @@ private constructor( { viewModelScope.launch { interactor.select(colorOptionModel) + logger.logThemeColorApplied( + colorOptionModel.colorOption + .sourceForLogging, + colorOptionModel.colorOption + .styleForLogging, + colorOptionModel.colorOption + .seedColorForLogging, + ) } } } @@ -205,12 +215,14 @@ private constructor( class Factory( private val context: Context, private val interactor: ColorPickerInteractor, + private val logger: ThemesUserEventLogger, ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return ColorPickerViewModel( context = context, interactor = interactor, + logger = logger, ) as T } diff --git a/src/com/android/customization/picker/grid/GridFragment.java b/src/com/android/customization/picker/grid/GridFragment.java deleted file mode 100644 index 4de1dab7..00000000 --- a/src/com/android/customization/picker/grid/GridFragment.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright (C) 2018 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.grid; - -import static com.android.wallpaper.widget.BottomActionBar.BottomAction.APPLY_TEXT; - -import android.content.Context; -import android.graphics.Point; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityManager; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.constraintlayout.widget.ConstraintSet; -import androidx.core.widget.ContentLoadingProgressBar; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.RecyclerView; - -import com.android.customization.model.CustomizationManager.Callback; -import com.android.customization.model.CustomizationManager.OptionsFetchedListener; -import com.android.customization.model.CustomizationOption; -import com.android.customization.model.grid.GridOption; -import com.android.customization.model.grid.GridOptionViewModel; -import com.android.customization.model.grid.GridOptionsManager; -import com.android.customization.module.ThemesUserEventLogger; -import com.android.customization.picker.WallpaperPreviewer; -import com.android.customization.widget.OptionSelectorController; -import com.android.customization.widget.OptionSelectorController.CheckmarkStyle; -import com.android.wallpaper.R; -import com.android.wallpaper.model.WallpaperInfo; -import com.android.wallpaper.module.CurrentWallpaperInfoFactory; -import com.android.wallpaper.module.InjectorProvider; -import com.android.wallpaper.picker.AppbarFragment; -import com.android.wallpaper.util.LaunchUtils; -import com.android.wallpaper.util.ScreenSizeCalculator; -import com.android.wallpaper.widget.BottomActionBar; - -import com.bumptech.glide.Glide; - -import java.util.List; -import java.util.Locale; - -/** - * Fragment that contains the UI for selecting and applying a GridOption. - */ -public class GridFragment extends AppbarFragment { - - private static final String TAG = "GridFragment"; - - private WallpaperInfo mHomeWallpaper; - private RecyclerView mOptionsContainer; - private OptionSelectorController<GridOption> mOptionsController; - private GridOptionsManager mGridManager; - private ContentLoadingProgressBar mLoading; - private ConstraintLayout mContent; - private View mError; - private BottomActionBar mBottomActionBar; - private ThemesUserEventLogger mEventLogger; - private GridOptionPreviewer mGridOptionPreviewer; - private GridOptionViewModel mGridOptionViewModel; - - private final Callback mApplyGridCallback = new Callback() { - @Override - public void onSuccess() { - mGridManager.fetchOptions(unused -> {}, true); - Toast.makeText(getContext(), R.string.applied_grid_msg, Toast.LENGTH_SHORT).show(); - getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - getActivity().finish(); - - // Go back to launcher home - LaunchUtils.launchHome(getContext()); - } - - @Override - public void onError(@Nullable Throwable throwable) { - // Since we disabled it when clicked apply button. - mBottomActionBar.enableActions(); - mBottomActionBar.hide(); - mGridOptionViewModel.setBottomActionBarVisible(false); - //TODO(chihhangchuang): handle - } - }; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mGridOptionViewModel = new ViewModelProvider(requireActivity()).get( - GridOptionViewModel.class); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View view = inflater.inflate( - R.layout.fragment_grid_picker, container, /* attachToRoot */ false); - setUpToolbar(view); - mContent = view.findViewById(R.id.content_section); - mOptionsContainer = view.findViewById(R.id.options_container); - AccessibilityManager accessibilityManager = - (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); - if (accessibilityManager.isEnabled()) { - // Make Talkback focus won't reset when notifyDataSetChange - mOptionsContainer.setItemAnimator(null); - } - - // Set aspect ratio on the preview card dynamically. - Point mScreenSize; - ScreenSizeCalculator screenSizeCalculator = ScreenSizeCalculator.getInstance(); - mScreenSize = screenSizeCalculator.getScreenSize( - requireActivity().getWindowManager().getDefaultDisplay()); - ConstraintSet set = new ConstraintSet(); - set.clone(mContent); - String ratio = String.format(Locale.US, "%d:%d", mScreenSize.x, mScreenSize.y); - set.setDimensionRatio(R.id.preview_card_container, ratio); - set.applyTo(mContent); - - mLoading = view.findViewById(R.id.loading_indicator); - mError = view.findViewById(R.id.error_section); - - // For nav bar edge-to-edge effect. - view.setOnApplyWindowInsetsListener((v, windowInsets) -> { - v.setPadding( - v.getPaddingLeft(), - windowInsets.getSystemWindowInsetTop(), - v.getPaddingRight(), - windowInsets.getSystemWindowInsetBottom()); - return windowInsets.consumeSystemWindowInsets(); - }); - - // Clear memory cache whenever grid fragment view is being loaded. - Glide.get(getContext()).clearMemory(); - - mGridManager = GridOptionsManager.getInstance(getContext()); - mEventLogger = (ThemesUserEventLogger) InjectorProvider.getInjector() - .getUserEventLogger(getContext()); - setUpOptions(); - - SurfaceView wallpaperSurface = view.findViewById(R.id.wallpaper_preview_surface); - WallpaperPreviewer wallpaperPreviewer = new WallpaperPreviewer(getLifecycle(), - getActivity(), view.findViewById(R.id.wallpaper_preview_image), wallpaperSurface, - view.findViewById(R.id.grid_fadein_scrim)); - // Loads current Wallpaper. - CurrentWallpaperInfoFactory factory = InjectorProvider.getInjector() - .getCurrentWallpaperInfoFactory(getContext().getApplicationContext()); - factory.createCurrentWallpaperInfos((homeWallpaper, lockWallpaper, presentationMode) -> { - mHomeWallpaper = homeWallpaper; - wallpaperPreviewer.setWallpaper(mHomeWallpaper, /* listener= */ null); - }, false); - - mGridOptionPreviewer = new GridOptionPreviewer(mGridManager, - view.findViewById(R.id.grid_preview_container)); - - return view; - } - - @Override - public boolean onBackPressed() { - mGridOptionViewModel.setSelectedOption(null); - mGridOptionViewModel.setBottomActionBarVisible(false); - return super.onBackPressed(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mGridOptionPreviewer != null) { - mGridOptionPreviewer.release(); - } - } - - @Override - public CharSequence getDefaultTitle() { - return getString(R.string.grid_title); - } - - @Override - protected void onBottomActionBarReady(BottomActionBar bottomActionBar) { - super.onBottomActionBarReady(bottomActionBar); - mBottomActionBar = bottomActionBar; - mBottomActionBar.showActionsOnly(APPLY_TEXT); - mBottomActionBar.setActionClickListener(APPLY_TEXT, - v -> applyGridOption(mGridOptionViewModel.getSelectedOption())); - mBottomActionBar.setActionAccessibilityTraversalAfter(APPLY_TEXT, - mOptionsContainer.getId()); - } - - private void applyGridOption(GridOption gridOption) { - mBottomActionBar.disableActions(); - mGridManager.apply(gridOption, mApplyGridCallback); - } - - private void setUpOptions() { - hideError(); - mLoading.show(); - mGridManager.fetchOptions(new OptionsFetchedListener<GridOption>() { - @Override - public void onOptionsLoaded(List<GridOption> options) { - mLoading.hide(); - mOptionsController = new OptionSelectorController<>( - mOptionsContainer, options, /* useGrid= */ false, - CheckmarkStyle.CENTER_CHANGE_COLOR_WHEN_NOT_SELECTED); - mOptionsController.initOptions(mGridManager); - GridOption previouslySelectedOption = findEquivalent(options, - mGridOptionViewModel.getSelectedOption()); - mGridOptionViewModel.setSelectedOption( - previouslySelectedOption != null - ? previouslySelectedOption - : getActiveOption(options)); - - mOptionsController.setSelectedOption(mGridOptionViewModel.getSelectedOption()); - onOptionSelected(mGridOptionViewModel.getSelectedOption()); - restoreBottomActionBarVisibility(); - - mOptionsController.addListener(selectedOption -> { - String title = selectedOption.getTitle(); - int stringId = R.string.option_previewed_description; - if (selectedOption.isActive(mGridManager)) { - stringId = R.string.option_applied_previewed_description; - } - CharSequence cd = getContext().getString(stringId, title); - mOptionsContainer.announceForAccessibility(cd); - onOptionSelected(selectedOption); - mBottomActionBar.show(); - mGridOptionViewModel.setBottomActionBarVisible(true); - }); - } - - @Override - public void onError(@Nullable Throwable throwable) { - if (throwable != null) { - Log.e(TAG, "Error loading grid options", throwable); - } - showError(); - } - }, /*reload= */ true); - } - - private GridOption getActiveOption(List<GridOption> options) { - return options.stream() - .filter(option -> option.isActive(mGridManager)) - .findAny() - // For development only, as there should always be a grid set. - .orElse(options.get(0)); - } - - @Nullable - private GridOption findEquivalent(List<GridOption> options, GridOption target) { - return options.stream() - .filter(option -> option.equals(target)) - .findAny() - .orElse(null); - } - - private void hideError() { - mContent.setVisibility(View.VISIBLE); - mError.setVisibility(View.GONE); - } - - private void showError() { - mLoading.hide(); - mContent.setVisibility(View.GONE); - mError.setVisibility(View.VISIBLE); - } - - private void onOptionSelected(CustomizationOption selectedOption) { - mGridOptionViewModel.setSelectedOption((GridOption) selectedOption); - mEventLogger.logGridSelected(mGridOptionViewModel.getSelectedOption()); - mGridOptionPreviewer.setGridOption(mGridOptionViewModel.getSelectedOption()); - } - - private void restoreBottomActionBarVisibility() { - if (mGridOptionViewModel.getBottomActionBarVisible()) { - mBottomActionBar.show(); - } else { - mBottomActionBar.hide(); - } - } -} diff --git a/src/com/android/customization/picker/grid/GridOptionPreviewer.java b/src/com/android/customization/picker/grid/GridOptionPreviewer.java deleted file mode 100644 index 7786d35c..00000000 --- a/src/com/android/customization/picker/grid/GridOptionPreviewer.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2020 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.grid; - -import android.content.Context; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.ViewGroup; - -import com.android.customization.model.grid.GridOption; -import com.android.customization.model.grid.GridOptionsManager; -import com.android.wallpaper.R; -import com.android.wallpaper.picker.WorkspaceSurfaceHolderCallback; -import com.android.wallpaper.util.PreviewUtils; -import com.android.wallpaper.util.SurfaceViewUtils; - -/** A class to load the {@link GridOption} preview to the view. */ -class GridOptionPreviewer { - - private final GridOptionsManager mGridManager; - private final ViewGroup mPreviewContainer; - - private SurfaceView mGridOptionSurface; - private GridOption mGridOption; - private GridOptionSurfaceHolderCallback mSurfaceCallback; - - GridOptionPreviewer(GridOptionsManager gridManager, ViewGroup previewContainer) { - mGridManager = gridManager; - mPreviewContainer = previewContainer; - } - - /** Loads the Grid option into the container view. */ - public void setGridOption(GridOption gridOption) { - mGridOption = gridOption; - if (mGridOption != null) { - updateWorkspacePreview(); - } - } - - /** Releases the view resource. */ - public void release() { - if (mGridOptionSurface != null) { - mSurfaceCallback.cleanUp(); - mGridOptionSurface = null; - } - mPreviewContainer.removeAllViews(); - } - - private void updateWorkspacePreview() { - // Reattach SurfaceView to trigger #surfaceCreated to update preview for different option. - mPreviewContainer.removeAllViews(); - if (mSurfaceCallback != null) { - mSurfaceCallback.cleanUp(); - mSurfaceCallback.resetLastSurface(); - if (mGridOptionSurface != null) { - mGridOptionSurface.getHolder().removeCallback(mSurfaceCallback); - } - } - mGridOptionSurface = new SurfaceView(mPreviewContainer.getContext()); - mGridOptionSurface.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - mGridOptionSurface.setZOrderMediaOverlay(true); - mSurfaceCallback = new GridOptionSurfaceHolderCallback(mGridOptionSurface, - mGridOptionSurface.getContext()); - mGridOptionSurface.getHolder().addCallback(mSurfaceCallback); - mPreviewContainer.addView(mGridOptionSurface); - } - - private class GridOptionSurfaceHolderCallback extends WorkspaceSurfaceHolderCallback { - private GridOptionSurfaceHolderCallback(SurfaceView workspaceSurface, Context context) { - super( - workspaceSurface, - new PreviewUtils( - context, context.getString(R.string.grid_control_metadata_name))); - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - if (mGridOption != null) { - super.surfaceCreated(holder); - } - } - - @Override - protected void requestPreview(SurfaceView workspaceSurface, - PreviewUtils.WorkspacePreviewCallback callback) { - mGridManager.renderPreview( - SurfaceViewUtils.createSurfaceViewRequest(workspaceSurface), - mGridOption.name, callback); - } - } -} diff --git a/src/com/android/customization/picker/grid/data/repository/GridRepository.kt b/src/com/android/customization/picker/grid/data/repository/GridRepository.kt new file mode 100644 index 00000000..f3844294 --- /dev/null +++ b/src/com/android/customization/picker/grid/data/repository/GridRepository.kt @@ -0,0 +1,181 @@ +/* + * 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.grid.data.repository + +import androidx.lifecycle.asFlow +import com.android.customization.model.CustomizationManager +import com.android.customization.model.CustomizationManager.Callback +import com.android.customization.model.grid.GridOption +import com.android.customization.model.grid.GridOptionsManager +import com.android.customization.picker.grid.shared.model.GridOptionItemModel +import com.android.customization.picker.grid.shared.model.GridOptionItemsModel +import kotlin.coroutines.resume +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext + +interface GridRepository { + suspend fun isAvailable(): Boolean + fun getOptionChanges(): Flow<Unit> + suspend fun getOptions(): GridOptionItemsModel + fun getSelectedOption(): GridOption? + fun applySelectedOption(callback: Callback) + fun clearSelectedOption() + fun isSelectedOptionApplied(): Boolean +} + +class GridRepositoryImpl( + private val applicationScope: CoroutineScope, + private val manager: GridOptionsManager, + private val backgroundDispatcher: CoroutineDispatcher, + private val isGridApplyButtonEnabled: Boolean, +) : GridRepository { + + override suspend fun isAvailable(): Boolean { + return withContext(backgroundDispatcher) { manager.isAvailable } + } + + override fun getOptionChanges(): Flow<Unit> = + manager.getOptionChangeObservable(/* handler= */ null).asFlow().map {} + + private val selectedOption = MutableStateFlow<GridOption?>(null) + + private var appliedOption: GridOption? = null + + override fun getSelectedOption() = selectedOption.value + + override suspend fun getOptions(): GridOptionItemsModel { + return withContext(backgroundDispatcher) { + suspendCancellableCoroutine { continuation -> + manager.fetchOptions( + object : CustomizationManager.OptionsFetchedListener<GridOption> { + override fun onOptionsLoaded(options: MutableList<GridOption>?) { + val optionsOrEmpty = options ?: emptyList() + // After Apply Button is added, we will rely on onSelected() method + // to update selectedOption. + if (!isGridApplyButtonEnabled || selectedOption.value == null) { + selectedOption.value = optionsOrEmpty.find { it.isActive(manager) } + } + if (isGridApplyButtonEnabled && appliedOption == null) { + appliedOption = selectedOption.value + } + continuation.resume( + GridOptionItemsModel.Loaded( + optionsOrEmpty.map { option -> toModel(option) } + ) + ) + } + + override fun onError(throwable: Throwable?) { + continuation.resume( + GridOptionItemsModel.Error( + throwable ?: Exception("Failed to load grid options!") + ), + ) + } + }, + /* reload= */ true, + ) + } + } + } + + private fun toModel(option: GridOption): GridOptionItemModel { + return GridOptionItemModel( + name = option.title, + rows = option.rows, + cols = option.cols, + isSelected = + selectedOption + .map { it.key() } + .map { selectedOptionKey -> option.key() == selectedOptionKey } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = false, + ), + onSelected = { onSelected(option) }, + ) + } + + private suspend fun onSelected(option: GridOption) { + withContext(backgroundDispatcher) { + suspendCancellableCoroutine { continuation -> + if (isGridApplyButtonEnabled) { + selectedOption.value?.setIsCurrent(false) + selectedOption.value = option + selectedOption.value?.setIsCurrent(true) + manager.preview(option) + continuation.resume(true) + } else { + manager.apply( + option, + object : CustomizationManager.Callback { + override fun onSuccess() { + continuation.resume(true) + } + + override fun onError(throwable: Throwable?) { + continuation.resume(false) + } + }, + ) + } + } + } + } + + override fun applySelectedOption(callback: Callback) { + val option = getSelectedOption() + manager.apply( + option, + if (isGridApplyButtonEnabled) { + object : Callback { + override fun onSuccess() { + callback.onSuccess() + appliedOption = option + } + + override fun onError(throwable: Throwable?) { + callback.onError(throwable) + } + } + } else callback + ) + } + + override fun clearSelectedOption() { + if (!isGridApplyButtonEnabled) { + return + } + selectedOption.value?.setIsCurrent(false) + selectedOption.value = null + } + + override fun isSelectedOptionApplied() = selectedOption.value?.name == appliedOption?.name + + private fun GridOption?.key(): String? { + return if (this != null) "${cols}x${rows}" else null + } +} diff --git a/src/com/android/customization/picker/grid/domain/interactor/GridInteractor.kt b/src/com/android/customization/picker/grid/domain/interactor/GridInteractor.kt new file mode 100644 index 00000000..02e16ddf --- /dev/null +++ b/src/com/android/customization/picker/grid/domain/interactor/GridInteractor.kt @@ -0,0 +1,110 @@ +/* + * 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.grid.domain.interactor + +import com.android.customization.model.CustomizationManager +import com.android.customization.model.grid.GridOption +import com.android.customization.picker.grid.data.repository.GridRepository +import com.android.customization.picker.grid.shared.model.GridOptionItemModel +import com.android.customization.picker.grid.shared.model.GridOptionItemsModel +import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn + +class GridInteractor( + private val applicationScope: CoroutineScope, + private val repository: GridRepository, + private val snapshotRestorer: Provider<GridSnapshotRestorer>, +) { + val options: Flow<GridOptionItemsModel> = + flow { emit(repository.isAvailable()) } + .flatMapLatest { isAvailable -> + if (isAvailable) { + // this upstream flow tells us each time the options are changed. + repository + .getOptionChanges() + // when we start, we pretend the options _just_ changed. This way, we load + // something as soon as possible into the flow so it's ready by the time the + // first observer starts to observe. + .onStart { emit(Unit) } + // each time the options changed, we load them. + .map { reload() } + // we place the loaded options in a SharedFlow so downstream observers all + // share the same flow and don't trigger a new one each time they want to + // start observing. + .shareIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) + } else { + emptyFlow() + } + } + + suspend fun setSelectedOption(model: GridOptionItemModel) { + model.onSelected.invoke() + } + + suspend fun getSelectedOption(): GridOptionItemModel? { + return (repository.getOptions() as? GridOptionItemsModel.Loaded)?.options?.firstOrNull { + optionItem -> + optionItem.isSelected.value + } + } + + fun getSelectOptionNonSuspend(): GridOption? = repository.getSelectedOption() + + fun clearSelectedOption() = repository.clearSelectedOption() + + fun isSelectedOptionApplied() = repository.isSelectedOptionApplied() + + fun applySelectedOption(callback: CustomizationManager.Callback) { + repository.applySelectedOption(callback) + } + + private suspend fun reload(): GridOptionItemsModel { + val model = repository.getOptions() + return if (model is GridOptionItemsModel.Loaded) { + GridOptionItemsModel.Loaded( + options = + model.options.map { option -> + GridOptionItemModel( + name = option.name, + cols = option.cols, + rows = option.rows, + isSelected = option.isSelected, + onSelected = { + option.onSelected() + snapshotRestorer.get().store(option) + }, + ) + } + ) + } else { + model + } + } +} diff --git a/src/com/android/customization/picker/grid/domain/interactor/GridSnapshotRestorer.kt b/src/com/android/customization/picker/grid/domain/interactor/GridSnapshotRestorer.kt new file mode 100644 index 00000000..74d77f75 --- /dev/null +++ b/src/com/android/customization/picker/grid/domain/interactor/GridSnapshotRestorer.kt @@ -0,0 +1,74 @@ +/* + * 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.grid.domain.interactor + +import android.util.Log +import com.android.customization.picker.grid.shared.model.GridOptionItemModel +import com.android.wallpaper.picker.undo.domain.interactor.SnapshotRestorer +import com.android.wallpaper.picker.undo.domain.interactor.SnapshotStore +import com.android.wallpaper.picker.undo.shared.model.RestorableSnapshot + +class GridSnapshotRestorer( + private val interactor: GridInteractor, +) : SnapshotRestorer { + + private var store: SnapshotStore = SnapshotStore.NOOP + private var originalOption: GridOptionItemModel? = null + + override suspend fun setUpSnapshotRestorer(store: SnapshotStore): RestorableSnapshot { + this.store = store + val option = interactor.getSelectedOption() + originalOption = option + return snapshot(option) + } + + override suspend fun restoreToSnapshot(snapshot: RestorableSnapshot) { + val optionNameFromSnapshot = snapshot.args[KEY_GRID_OPTION_NAME] + originalOption?.let { optionToRestore -> + if (optionToRestore.name != optionNameFromSnapshot) { + Log.wtf( + TAG, + """Original snapshot name was ${optionToRestore.name} but we're being told to + | restore to $optionNameFromSnapshot. The current implementation doesn't + | support undo, only a reset back to the original grid option.""" + .trimMargin(), + ) + } + + interactor.setSelectedOption(optionToRestore) + } + } + + fun store(option: GridOptionItemModel) { + store.store(snapshot(option)) + } + + private fun snapshot(option: GridOptionItemModel?): RestorableSnapshot { + return RestorableSnapshot( + args = + buildMap { + option?.name?.let { optionName -> put(KEY_GRID_OPTION_NAME, optionName) } + } + ) + } + + companion object { + private const val TAG = "GridSnapshotRestorer" + private const val KEY_GRID_OPTION_NAME = "grid_option" + } +} diff --git a/src/com/android/customization/picker/grid/shared/model/GridOptionItemModel.kt b/src/com/android/customization/picker/grid/shared/model/GridOptionItemModel.kt new file mode 100644 index 00000000..1fb01be0 --- /dev/null +++ b/src/com/android/customization/picker/grid/shared/model/GridOptionItemModel.kt @@ -0,0 +1,28 @@ +/* + * 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.grid.shared.model + +import kotlinx.coroutines.flow.StateFlow + +data class GridOptionItemModel( + val name: String, + val cols: Int, + val rows: Int, + val isSelected: StateFlow<Boolean>, + val onSelected: suspend () -> Unit, +) diff --git a/src/com/android/customization/picker/clock/utils/ClockDescriptionUtils.kt b/src/com/android/customization/picker/grid/shared/model/GridOptionItemsModel.kt index 28ea4a3f..e5b33c52 100644 --- a/src/com/android/customization/picker/clock/utils/ClockDescriptionUtils.kt +++ b/src/com/android/customization/picker/grid/shared/model/GridOptionItemsModel.kt @@ -12,15 +12,16 @@ * 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.utils -/** Provides clock description for accessibility purposes. */ -interface ClockDescriptionUtils { +package com.android.customization.picker.grid.shared.model - /** - * TODO (b/287507746) : Migrate the clock description to system UI or a shared library, instead - * of preserving at the Wallpaper Picker side. - */ - fun getDescription(clockId: String): String +sealed class GridOptionItemsModel { + data class Loaded( + val options: List<GridOptionItemModel>, + ) : GridOptionItemsModel() + data class Error( + val throwable: Throwable?, + ) : GridOptionItemsModel() } diff --git a/src/com/android/customization/picker/grid/ui/binder/GridIconViewBinder.kt b/src/com/android/customization/picker/grid/ui/binder/GridIconViewBinder.kt new file mode 100644 index 00000000..9fc88a0e --- /dev/null +++ b/src/com/android/customization/picker/grid/ui/binder/GridIconViewBinder.kt @@ -0,0 +1,17 @@ +package com.android.customization.picker.grid.ui.binder + +import android.widget.ImageView +import com.android.customization.picker.grid.ui.viewmodel.GridIconViewModel +import com.android.customization.widget.GridTileDrawable + +object GridIconViewBinder { + fun bind(view: ImageView, viewModel: GridIconViewModel) { + view.setImageDrawable( + GridTileDrawable( + viewModel.columns, + viewModel.rows, + viewModel.path, + ) + ) + } +} diff --git a/src/com/android/customization/picker/grid/ui/binder/GridScreenBinder.kt b/src/com/android/customization/picker/grid/ui/binder/GridScreenBinder.kt new file mode 100644 index 00000000..bcb37379 --- /dev/null +++ b/src/com/android/customization/picker/grid/ui/binder/GridScreenBinder.kt @@ -0,0 +1,91 @@ +/* + * 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.grid.ui.binder + +import android.view.View +import android.widget.Button +import android.widget.ImageView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.customization.picker.common.ui.view.ItemSpacing +import com.android.customization.picker.grid.ui.viewmodel.GridIconViewModel +import com.android.customization.picker.grid.ui.viewmodel.GridScreenViewModel +import com.android.wallpaper.R +import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter +import com.android.wallpaper.picker.option.ui.binder.OptionItemBinder +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.launch + +object GridScreenBinder { + fun bind( + view: View, + viewModel: GridScreenViewModel, + lifecycleOwner: LifecycleOwner, + backgroundDispatcher: CoroutineDispatcher, + onOptionsChanged: () -> Unit, + isGridApplyButtonEnabled: Boolean, + onOptionApplied: () -> Unit, + ) { + val optionView: RecyclerView = view.requireViewById(R.id.options) + optionView.layoutManager = + LinearLayoutManager( + view.context, + RecyclerView.HORIZONTAL, + /* reverseLayout= */ false, + ) + optionView.addItemDecoration(ItemSpacing(ItemSpacing.ITEM_SPACING_DP)) + val adapter = + OptionItemAdapter( + layoutResourceId = R.layout.grid_option, + lifecycleOwner = lifecycleOwner, + backgroundDispatcher = backgroundDispatcher, + foregroundTintSpec = + OptionItemBinder.TintSpec( + selectedColor = view.context.getColor(R.color.system_on_surface), + unselectedColor = view.context.getColor(R.color.system_on_surface), + ), + bindIcon = { foregroundView: View, gridIcon: GridIconViewModel -> + val imageView = foregroundView as? ImageView + imageView?.let { GridIconViewBinder.bind(imageView, gridIcon) } + } + ) + optionView.adapter = adapter + + if (isGridApplyButtonEnabled) { + val applyButton: Button = view.requireViewById(R.id.apply_button) + applyButton.visibility = View.VISIBLE + view.requireViewById<View>(R.id.apply_button_note).visibility = View.VISIBLE + applyButton.setOnClickListener { onOptionApplied() } + } + + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.optionItems.collect { options -> + adapter.setItems(options) + onOptionsChanged() + } + } + } + } + } +} diff --git a/src/com/android/customization/picker/grid/ui/fragment/GridFragment.kt b/src/com/android/customization/picker/grid/ui/fragment/GridFragment.kt new file mode 100644 index 00000000..2a301b40 --- /dev/null +++ b/src/com/android/customization/picker/grid/ui/fragment/GridFragment.kt @@ -0,0 +1,210 @@ +/* + * 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.grid.ui.fragment + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.lifecycle.ViewModelProvider +import androidx.transition.Transition +import androidx.transition.doOnStart +import com.android.customization.model.CustomizationManager.Callback +import com.android.customization.module.ThemePickerInjector +import com.android.customization.picker.grid.domain.interactor.GridInteractor +import com.android.customization.picker.grid.ui.binder.GridScreenBinder +import com.android.customization.picker.grid.ui.viewmodel.GridScreenViewModel +import com.android.wallpaper.R +import com.android.wallpaper.config.BaseFlags +import com.android.wallpaper.module.CurrentWallpaperInfoFactory +import com.android.wallpaper.module.CustomizationSections +import com.android.wallpaper.module.InjectorProvider +import com.android.wallpaper.picker.AppbarFragment +import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor +import com.android.wallpaper.picker.customization.ui.binder.ScreenPreviewBinder +import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel +import com.android.wallpaper.util.PreviewUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.suspendCancellableCoroutine + +private val TAG = GridFragment::class.java.simpleName + +@OptIn(ExperimentalCoroutinesApi::class) +class GridFragment : AppbarFragment() { + + private lateinit var gridInteractor: GridInteractor + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = + inflater.inflate( + R.layout.fragment_grid, + container, + false, + ) + setUpToolbar(view) + + val isGridApplyButtonEnabled = BaseFlags.get().isGridApplyButtonEnabled(requireContext()) + + val injector = InjectorProvider.getInjector() as ThemePickerInjector + + val wallpaperInfoFactory = injector.getCurrentWallpaperInfoFactory(requireContext()) + var screenPreviewBinding = + bindScreenPreview( + view, + wallpaperInfoFactory, + injector.getWallpaperInteractor(requireContext()), + injector.getGridInteractor(requireContext()) + ) + + val viewModelFactory = injector.getGridScreenViewModelFactory(requireContext()) + gridInteractor = injector.getGridInteractor(requireContext()) + GridScreenBinder.bind( + view = view, + viewModel = + ViewModelProvider( + this, + viewModelFactory, + )[GridScreenViewModel::class.java], + lifecycleOwner = this, + backgroundDispatcher = Dispatchers.IO, + onOptionsChanged = { + screenPreviewBinding.destroy() + screenPreviewBinding = + bindScreenPreview( + view, + wallpaperInfoFactory, + injector.getWallpaperInteractor(requireContext()), + gridInteractor, + ) + if (isGridApplyButtonEnabled) { + val applyButton: Button = view.requireViewById(R.id.apply_button) + applyButton.isEnabled = !gridInteractor.isSelectedOptionApplied() + } + }, + isGridApplyButtonEnabled = isGridApplyButtonEnabled, + onOptionApplied = { + gridInteractor.applySelectedOption( + object : Callback { + override fun onSuccess() { + Toast.makeText( + context, + getString( + R.string.toast_of_changing_grid, + gridInteractor.getSelectOptionNonSuspend()?.title + ), + Toast.LENGTH_SHORT + ) + .show() + val applyButton: Button = view.requireViewById(R.id.apply_button) + applyButton.isEnabled = false + } + + override fun onError(throwable: Throwable?) { + val errorMsg = + getString( + R.string.toast_of_failure_to_change_grid, + gridInteractor.getSelectOptionNonSuspend()?.title + ) + Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show() + Log.e(TAG, errorMsg, throwable) + } + } + ) + } + ) + + (returnTransition as? Transition)?.doOnStart { + view.requireViewById<View>(R.id.preview).isVisible = false + } + + return view + } + + override fun getDefaultTitle(): CharSequence { + return getString(R.string.grid_title) + } + + override fun getToolbarColorId(): Int { + return android.R.color.transparent + } + + override fun getToolbarTextColor(): Int { + return ContextCompat.getColor(requireContext(), R.color.system_on_surface) + } + + private fun bindScreenPreview( + view: View, + wallpaperInfoFactory: CurrentWallpaperInfoFactory, + wallpaperInteractor: WallpaperInteractor, + gridInteractor: GridInteractor + ): ScreenPreviewBinder.Binding { + return ScreenPreviewBinder.bind( + activity = requireActivity(), + previewView = view.requireViewById(R.id.preview), + viewModel = + ScreenPreviewViewModel( + previewUtils = + PreviewUtils( + context = requireContext(), + authorityMetadataKey = + requireContext() + .getString( + R.string.grid_control_metadata_name, + ), + ), + initialExtrasProvider = { + val bundle = Bundle() + bundle.putString("name", gridInteractor.getSelectOptionNonSuspend()?.name) + bundle + }, + wallpaperInfoProvider = { + suspendCancellableCoroutine { continuation -> + wallpaperInfoFactory.createCurrentWallpaperInfos( + context, + /* forceRefresh= */ true, + ) { homeWallpaper, lockWallpaper, _ -> + continuation.resume(homeWallpaper ?: lockWallpaper, null) + } + } + }, + wallpaperInteractor = wallpaperInteractor, + screen = CustomizationSections.Screen.HOME_SCREEN, + ), + lifecycleOwner = viewLifecycleOwner, + offsetToStart = false, + onWallpaperPreviewDirty = { activity?.recreate() }, + ) + } + + override fun onBackPressed(): Boolean { + if (BaseFlags.get().isGridApplyButtonEnabled(requireContext())) { + gridInteractor.clearSelectedOption() + } + return super.onBackPressed() + } +} diff --git a/src/com/android/customization/picker/grid/ui/section/GridSectionController.java b/src/com/android/customization/picker/grid/ui/section/GridSectionController.java new file mode 100644 index 00000000..6ae9acd9 --- /dev/null +++ b/src/com/android/customization/picker/grid/ui/section/GridSectionController.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2021 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.grid.ui.section; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.Observer; + +import com.android.customization.model.CustomizationManager.OptionsFetchedListener; +import com.android.customization.model.grid.GridOption; +import com.android.customization.model.grid.GridOptionsManager; +import com.android.customization.picker.grid.ui.fragment.GridFragment; +import com.android.customization.picker.grid.ui.view.GridSectionView; +import com.android.wallpaper.R; +import com.android.wallpaper.model.CustomizationSectionController; + +import java.util.List; + +/** A {@link CustomizationSectionController} for app grid. */ +public class GridSectionController implements CustomizationSectionController<GridSectionView> { + + private static final String TAG = "GridSectionController"; + + private final GridOptionsManager mGridOptionsManager; + private final CustomizationSectionNavigationController mSectionNavigationController; + private final Observer<Object> mOptionChangeObserver; + private final LifecycleOwner mLifecycleOwner; + private TextView mSectionDescription; + private View mSectionTile; + + public GridSectionController( + GridOptionsManager gridOptionsManager, + CustomizationSectionNavigationController sectionNavigationController, + LifecycleOwner lifecycleOwner, + boolean isRevampedUiEnabled) { + mGridOptionsManager = gridOptionsManager; + mSectionNavigationController = sectionNavigationController; + mLifecycleOwner = lifecycleOwner; + mOptionChangeObserver = o -> updateUi(/* reload= */ true); + } + + @Override + public boolean isAvailable(Context context) { + return mGridOptionsManager.isAvailable(); + } + + @Override + public GridSectionView createView(Context context) { + final GridSectionView gridSectionView = (GridSectionView) LayoutInflater.from(context) + .inflate(R.layout.grid_section_view, /* root= */ null); + mSectionDescription = gridSectionView.findViewById(R.id.grid_section_description); + mSectionTile = gridSectionView.findViewById(R.id.grid_section_tile); + + // Fetch grid options to show currently set grid. + updateUi(/* The result is getting when calling isAvailable(), so reload= */ false); + mGridOptionsManager.getOptionChangeObservable(/* handler= */ null).observe( + mLifecycleOwner, + mOptionChangeObserver); + + gridSectionView.setOnClickListener( + v -> { + final Fragment gridFragment = new GridFragment(); + mSectionNavigationController.navigateTo(gridFragment); + }); + + return gridSectionView; + } + + @Override + public void release() { + if (mGridOptionsManager.isAvailable()) { + mGridOptionsManager.getOptionChangeObservable(/* handler= */ null).removeObserver( + mOptionChangeObserver + ); + } + } + + @Override + public void onTransitionOut() { + CustomizationSectionController.super.onTransitionOut(); + } + + private void updateUi(final boolean reload) { + mGridOptionsManager.fetchOptions( + new OptionsFetchedListener<GridOption>() { + @Override + public void onOptionsLoaded(List<GridOption> options) { + final String title = getActiveOption(options).getTitle(); + mSectionDescription.setText(title); + } + + @Override + public void onError(@Nullable Throwable throwable) { + if (throwable != null) { + Log.e(TAG, "Error loading grid options", throwable); + } + mSectionDescription.setText(R.string.something_went_wrong); + mSectionTile.setVisibility(View.GONE); + } + }, + reload); + } + + private GridOption getActiveOption(List<GridOption> options) { + return options.stream() + .filter(option -> option.isActive(mGridOptionsManager)) + .findAny() + // For development only, as there should always be a grid set. + .orElse(options.get(0)); + } +} diff --git a/src/com/android/customization/picker/grid/GridSectionView.java b/src/com/android/customization/picker/grid/ui/view/GridSectionView.java index 58468e10..545ef197 100644 --- a/src/com/android/customization/picker/grid/GridSectionView.java +++ b/src/com/android/customization/picker/grid/ui/view/GridSectionView.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.customization.picker.grid; +package com.android.customization.picker.grid.ui.view; import android.content.Context; import android.util.AttributeSet; diff --git a/src/com/android/customization/picker/clock/utils/ThemePickerClockDescriptionUtils.kt b/src/com/android/customization/picker/grid/ui/viewmodel/GridIconViewModel.kt index a04ebfff..d12dc6c3 100644 --- a/src/com/android/customization/picker/clock/utils/ThemePickerClockDescriptionUtils.kt +++ b/src/com/android/customization/picker/grid/ui/viewmodel/GridIconViewModel.kt @@ -12,11 +12,13 @@ * 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.utils -class ThemePickerClockDescriptionUtils : ClockDescriptionUtils { - override fun getDescription(clockId: String): String { - return "" - } -} +package com.android.customization.picker.grid.ui.viewmodel + +data class GridIconViewModel( + val columns: Int, + val rows: Int, + val path: String, +) diff --git a/src/com/android/customization/picker/grid/ui/viewmodel/GridScreenViewModel.kt b/src/com/android/customization/picker/grid/ui/viewmodel/GridScreenViewModel.kt new file mode 100644 index 00000000..179127d1 --- /dev/null +++ b/src/com/android/customization/picker/grid/ui/viewmodel/GridScreenViewModel.kt @@ -0,0 +1,106 @@ +/* + * 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.grid.ui.viewmodel + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.android.customization.model.ResourceConstants +import com.android.customization.picker.grid.domain.interactor.GridInteractor +import com.android.customization.picker.grid.shared.model.GridOptionItemsModel +import com.android.wallpaper.picker.common.text.ui.viewmodel.Text +import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +class GridScreenViewModel( + context: Context, + private val interactor: GridInteractor, +) : ViewModel() { + + @SuppressLint("StaticFieldLeak") // We're not leaking this context as it is the app context. + private val applicationContext = context.applicationContext + + val optionItems: Flow<List<OptionItemViewModel<GridIconViewModel>>> = + interactor.options.map { model -> toViewModel(model) } + + private fun toViewModel( + model: GridOptionItemsModel, + ): List<OptionItemViewModel<GridIconViewModel>> { + val iconShapePath = + applicationContext.resources.getString( + Resources.getSystem() + .getIdentifier( + ResourceConstants.CONFIG_ICON_MASK, + "string", + ResourceConstants.ANDROID_PACKAGE, + ) + ) + + return when (model) { + is GridOptionItemsModel.Loaded -> + model.options.map { option -> + val text = Text.Loaded(option.name) + OptionItemViewModel<GridIconViewModel>( + key = + MutableStateFlow("${option.cols}x${option.rows}") as StateFlow<String>, + payload = + GridIconViewModel( + columns = option.cols, + rows = option.rows, + path = iconShapePath, + ), + text = text, + isSelected = option.isSelected, + onClicked = + option.isSelected.map { isSelected -> + if (!isSelected) { + { viewModelScope.launch { option.onSelected() } } + } else { + null + } + }, + ) + } + is GridOptionItemsModel.Error -> emptyList() + } + } + + class Factory( + context: Context, + private val interactor: GridInteractor, + ) : ViewModelProvider.Factory { + + private val applicationContext = context.applicationContext + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create(modelClass: Class<T>): T { + return GridScreenViewModel( + context = applicationContext, + interactor = interactor, + ) + as T + } + } +} diff --git a/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModel.kt b/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModel.kt index 954efa24..1a5254f8 100644 --- a/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModel.kt +++ b/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModel.kt @@ -21,6 +21,7 @@ import androidx.annotation.VisibleForTesting import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.android.customization.module.logging.ThemesUserEventLogger import com.android.customization.picker.notifications.domain.interactor.NotificationsInteractor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -31,6 +32,7 @@ class NotificationSectionViewModel @VisibleForTesting constructor( private val interactor: NotificationsInteractor, + private val logger: ThemesUserEventLogger, ) : ViewModel() { /** Whether the switch should be on. */ @@ -39,16 +41,23 @@ constructor( /** Notifies that the section has been clicked. */ fun onClicked() { - viewModelScope.launch { interactor.toggleShowNotificationsOnLockScreenEnabled() } + viewModelScope.launch { + interactor.toggleShowNotificationsOnLockScreenEnabled() + logger.logLockScreenNotificationApplied( + interactor.getSettings().isShowNotificationsOnLockScreenEnabled + ) + } } class Factory( private val interactor: NotificationsInteractor, + private val logger: ThemesUserEventLogger, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return NotificationSectionViewModel( interactor = interactor, + logger = logger, ) as T } diff --git a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt index 71dfe1da..eb25af7a 100644 --- a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt +++ b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt @@ -42,10 +42,10 @@ import com.android.customization.picker.color.domain.interactor.ColorPickerInter import com.android.wallpaper.R import com.android.wallpaper.model.CustomizationSectionController import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController -import com.android.wallpaper.model.WallpaperColorsViewModel import com.android.wallpaper.model.WallpaperPreviewNavigator import com.android.wallpaper.module.CurrentWallpaperInfoFactory import com.android.wallpaper.module.CustomizationSections +import com.android.wallpaper.picker.customization.data.repository.WallpaperColorsRepository import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewClickView import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewSectionController @@ -64,7 +64,7 @@ class PreviewWithClockCarouselSectionController( private val lifecycleOwner: LifecycleOwner, private val screen: CustomizationSections.Screen, wallpaperInfoFactory: CurrentWallpaperInfoFactory, - colorViewModel: WallpaperColorsViewModel, + wallpaperColorsRepository: WallpaperColorsRepository, displayUtils: DisplayUtils, clockCarouselViewModelFactory: ClockCarouselViewModel.Factory, private val clockViewFactory: ClockViewFactory, @@ -82,7 +82,7 @@ class PreviewWithClockCarouselSectionController( lifecycleOwner, screen, wallpaperInfoFactory, - colorViewModel, + wallpaperColorsRepository, displayUtils, wallpaperPreviewNavigator, wallpaperInteractor, diff --git a/src/com/android/customization/picker/preview/ui/section/PreviewWithThemeSectionController.kt b/src/com/android/customization/picker/preview/ui/section/PreviewWithThemeSectionController.kt index c4d6be45..b3e778ba 100644 --- a/src/com/android/customization/picker/preview/ui/section/PreviewWithThemeSectionController.kt +++ b/src/com/android/customization/picker/preview/ui/section/PreviewWithThemeSectionController.kt @@ -25,16 +25,17 @@ import com.android.customization.model.themedicon.domain.interactor.ThemedIconIn import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor import com.android.customization.picker.preview.ui.viewmodel.PreviewWithThemeViewModel import com.android.wallpaper.R -import com.android.wallpaper.model.WallpaperColorsViewModel import com.android.wallpaper.model.WallpaperPreviewNavigator import com.android.wallpaper.module.CurrentWallpaperInfoFactory import com.android.wallpaper.module.CustomizationSections +import com.android.wallpaper.picker.customization.data.repository.WallpaperColorsRepository import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewSectionController import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel import com.android.wallpaper.util.DisplayUtils import com.android.wallpaper.util.PreviewUtils +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.suspendCancellableCoroutine /** @@ -46,7 +47,7 @@ open class PreviewWithThemeSectionController( lifecycleOwner: LifecycleOwner, private val screen: CustomizationSections.Screen, private val wallpaperInfoFactory: CurrentWallpaperInfoFactory, - private val colorViewModel: WallpaperColorsViewModel, + private val wallpaperColorsRepository: WallpaperColorsRepository, displayUtils: DisplayUtils, wallpaperPreviewNavigator: WallpaperPreviewNavigator, private val wallpaperInteractor: WallpaperInteractor, @@ -61,7 +62,7 @@ open class PreviewWithThemeSectionController( lifecycleOwner, screen, wallpaperInfoFactory, - colorViewModel, + wallpaperColorsRepository, displayUtils, wallpaperPreviewNavigator, wallpaperInteractor, @@ -69,6 +70,7 @@ open class PreviewWithThemeSectionController( isTwoPaneAndSmallWidth, customizationPickerViewModel, ) { + @OptIn(ExperimentalCoroutinesApi::class) override fun createScreenPreviewViewModel(context: Context): ScreenPreviewViewModel { return PreviewWithThemeViewModel( previewUtils = @@ -92,28 +94,28 @@ open class PreviewWithThemeSectionController( wallpaperInfoProvider = { forceReload -> suspendCancellableCoroutine { continuation -> wallpaperInfoFactory.createCurrentWallpaperInfos( - { homeWallpaper, lockWallpaper, _ -> - val wallpaper = - if (isOnLockScreen) { - lockWallpaper ?: homeWallpaper - } else { - homeWallpaper ?: lockWallpaper - } - loadInitialColors( - context = context, - screen = screen, - ) - continuation.resume(wallpaper, null) - }, + context, forceReload, - ) + ) { homeWallpaper, lockWallpaper, _ -> + val wallpaper = + if (isOnLockScreen) { + lockWallpaper ?: homeWallpaper + } else { + homeWallpaper ?: lockWallpaper + } + loadInitialColors( + context = context, + screen = screen, + ) + continuation.resume(wallpaper, null) + } } }, onWallpaperColorChanged = { colors -> if (isOnLockScreen) { - colorViewModel.setLockWallpaperColors(colors) + wallpaperColorsRepository.setLockWallpaperColors(colors) } else { - colorViewModel.setHomeWallpaperColors(colors) + wallpaperColorsRepository.setHomeWallpaperColors(colors) } }, initialExtrasProvider = { getInitialExtras(isOnLockScreen) }, diff --git a/src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt b/src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt index b17af80d..6bfe3484 100644 --- a/src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt +++ b/src/com/android/customization/picker/quickaffordance/data/repository/KeyguardQuickAffordancePickerRepository.kt @@ -21,11 +21,11 @@ import com.android.customization.picker.quickaffordance.shared.model.KeyguardQui import com.android.customization.picker.quickaffordance.shared.model.KeyguardQuickAffordancePickerSelectionModel as SelectionModel import com.android.customization.picker.quickaffordance.shared.model.KeyguardQuickAffordancePickerSlotModel as SlotModel import com.android.systemui.shared.customization.data.content.CustomizationProviderClient as Client -import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract -import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.shareIn /** * Abstracts access to application state related to functionality for selecting, picking, or setting @@ -33,39 +33,25 @@ import kotlinx.coroutines.withContext */ class KeyguardQuickAffordancePickerRepository( private val client: Client, - private val backgroundDispatcher: CoroutineDispatcher, + private val scope: CoroutineScope ) { - /** Whether the feature is enabled. */ - val isFeatureEnabled: Flow<Boolean> = - client.observeFlags().map { flags -> flags.isFeatureEnabled() } - /** List of slots available on the device. */ val slots: Flow<List<SlotModel>> = client.observeSlots().map { slots -> slots.map { slot -> slot.toModel() } } /** List of all available quick affordances. */ val affordances: Flow<List<AffordanceModel>> = - client.observeAffordances().map { affordances -> - affordances.map { affordance -> affordance.toModel() } - } + client + .observeAffordances() + .map { affordances -> affordances.map { affordance -> affordance.toModel() } } + .shareIn(scope, replay = 1, started = SharingStarted.Lazily) /** List of slot-affordance pairs, modeling what the user has currently chosen for each slot. */ val selections: Flow<List<SelectionModel>> = - client.observeSelections().map { selections -> - selections.map { selection -> selection.toModel() } - } - - suspend fun isFeatureEnabled(): Boolean { - return withContext(backgroundDispatcher) { client.queryFlags().isFeatureEnabled() } - } - - private fun List<Client.Flag>.isFeatureEnabled(): Boolean { - return find { flag -> - flag.name == - Contract.FlagsTable.FLAG_NAME_CUSTOM_LOCK_SCREEN_QUICK_AFFORDANCES_ENABLED - } - ?.value == true - } + client + .observeSelections() + .map { selections -> selections.map { selection -> selection.toModel() } } + .shareIn(scope, replay = 1, started = SharingStarted.Lazily) private fun Client.Slot.toModel(): SlotModel { return SlotModel( diff --git a/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt b/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt index f154de65..3eca6241 100644 --- a/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt +++ b/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordancePickerInteractor.kt @@ -64,7 +64,7 @@ class KeyguardQuickAffordancePickerInteractor( } /** Unselects all affordances from the slot with the given ID. */ - suspend fun unselectAll(slotId: String) { + suspend fun unselectAllFromSlot(slotId: String) { client.deleteAllSelections( slotId = slotId, ) @@ -72,15 +72,15 @@ class KeyguardQuickAffordancePickerInteractor( snapshotRestorer.get().storeSnapshot() } + /** Unselects all affordances from all slots. */ + suspend fun unselectAll() { + client.querySlots().forEach { client.deleteAllSelections(it.id) } + } + /** Returns a [Drawable] for the given resource ID, from the system UI package. */ suspend fun getAffordanceIcon( @DrawableRes iconResourceId: Int, ): Drawable { return client.getAffordanceIcon(iconResourceId) } - - /** Returns `true` if the feature is enabled; `false` otherwise. */ - suspend fun isFeatureEnabled(): Boolean { - return repository.isFeatureEnabled() - } } diff --git a/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordanceSnapshotRestorer.kt b/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordanceSnapshotRestorer.kt index 3c7928ce..fee0cb51 100644 --- a/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordanceSnapshotRestorer.kt +++ b/src/com/android/customization/picker/quickaffordance/domain/interactor/KeyguardQuickAffordanceSnapshotRestorer.kt @@ -42,9 +42,14 @@ class KeyguardQuickAffordanceSnapshotRestorer( } override suspend fun restoreToSnapshot(snapshot: RestorableSnapshot) { + // reset all current selections + interactor.unselectAll() + + val allSelections = checkNotNull(snapshot.args[KEY_SELECTIONS]) + if (allSelections.isEmpty()) return + val selections: List<Pair<String, String>> = - checkNotNull(snapshot.args[KEY_SELECTIONS]).split(SELECTION_SEPARATOR).map { selection - -> + allSelections.split(SELECTION_SEPARATOR).map { selection -> val (slotId, affordanceId) = selection.split(SLOT_AFFORDANCE_SEPARATOR) slotId to affordanceId } diff --git a/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt b/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt index 8891b03f..0e3b7167 100644 --- a/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt +++ b/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt @@ -67,7 +67,9 @@ class SlotTabAdapter : RecyclerView.Adapter<SlotTabAdapter.ViewHolder>() { .find { it.isSelected.value } ?.text ?.asString(holder.itemView.context) - stateDescription?.let { holder.itemView.stateDescription = it } + holder.itemView.stateDescription = + stateDescription + ?: holder.itemView.resources.getString(R.string.keyguard_affordance_none) } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { diff --git a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt index 091f484e..3ac52ad5 100644 --- a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt +++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt @@ -20,7 +20,11 @@ package com.android.customization.picker.quickaffordance.ui.binder import android.app.Dialog import android.content.Context import android.view.View +import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent import android.widget.ImageView +import androidx.core.view.AccessibilityDelegateCompat +import androidx.core.view.ViewCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope @@ -62,6 +66,26 @@ object KeyguardQuickAffordancePickerBinder { slotTabView.layoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false) slotTabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP)) + + // Setting a custom accessibility delegate so that the default content descriptions + // for items in a list aren't announced (for left & right shortcuts). We populate + // the content description for these shortcuts later on with the right (expected) + // values. + val slotTabViewDelegate: AccessibilityDelegateCompat = + object : AccessibilityDelegateCompat() { + override fun onRequestSendAccessibilityEvent( + host: ViewGroup, + child: View, + event: AccessibilityEvent + ): Boolean { + if (event.eventType != AccessibilityEvent.TYPE_VIEW_FOCUSED) { + child.contentDescription = null + } + return super.onRequestSendAccessibilityEvent(host, child, event) + } + } + + ViewCompat.setAccessibilityDelegate(slotTabView, slotTabViewDelegate) val affordancesAdapter = OptionItemAdapter( layoutResourceId = R.layout.keyguard_quick_affordance, diff --git a/src/com/android/customization/picker/quickaffordance/ui/section/KeyguardQuickAffordanceSectionController.kt b/src/com/android/customization/picker/quickaffordance/ui/section/KeyguardQuickAffordanceSectionController.kt index e0beeff0..0c7b250d 100644 --- a/src/com/android/customization/picker/quickaffordance/ui/section/KeyguardQuickAffordanceSectionController.kt +++ b/src/com/android/customization/picker/quickaffordance/ui/section/KeyguardQuickAffordanceSectionController.kt @@ -20,27 +20,23 @@ package com.android.customization.picker.quickaffordance.ui.section import android.content.Context import android.view.LayoutInflater import androidx.lifecycle.LifecycleOwner -import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor import com.android.customization.picker.quickaffordance.ui.binder.KeyguardQuickAffordanceSectionViewBinder import com.android.customization.picker.quickaffordance.ui.fragment.KeyguardQuickAffordancePickerFragment import com.android.customization.picker.quickaffordance.ui.view.KeyguardQuickAffordanceSectionView import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel import com.android.wallpaper.R +import com.android.wallpaper.config.BaseFlags import com.android.wallpaper.model.CustomizationSectionController import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController as NavigationController -import kotlinx.coroutines.runBlocking class KeyguardQuickAffordanceSectionController( private val navigationController: NavigationController, - private val interactor: KeyguardQuickAffordancePickerInteractor, private val viewModel: KeyguardQuickAffordancePickerViewModel, private val lifecycleOwner: LifecycleOwner, ) : CustomizationSectionController<KeyguardQuickAffordanceSectionView> { - private val isFeatureEnabled: Boolean = runBlocking { interactor.isFeatureEnabled() } - override fun isAvailable(context: Context): Boolean { - return isFeatureEnabled + return BaseFlags.get().isKeyguardQuickAffordanceEnabled(context) } override fun createView(context: Context): KeyguardQuickAffordanceSectionView { diff --git a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt index f832cdeb..260c0d3b 100644 --- a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt +++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt @@ -26,6 +26,7 @@ import androidx.annotation.DrawableRes import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.android.customization.module.logging.ThemesUserEventLogger import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants @@ -63,6 +64,7 @@ private constructor( private val quickAffordanceInteractor: KeyguardQuickAffordancePickerInteractor, private val wallpaperInteractor: WallpaperInteractor, private val wallpaperInfoFactory: CurrentWallpaperInfoFactory, + private val logger: ThemesUserEventLogger, ) : ViewModel() { @SuppressLint("StaticFieldLeak") private val applicationContext = context.applicationContext @@ -92,11 +94,11 @@ private constructor( wallpaperInfoProvider = { forceReload -> suspendCancellableCoroutine { continuation -> wallpaperInfoFactory.createCurrentWallpaperInfos( - { homeWallpaper, lockWallpaper, _ -> - continuation.resume(lockWallpaper ?: homeWallpaper, null) - }, + context, forceReload, - ) + ) { homeWallpaper, lockWallpaper, _ -> + continuation.resume(lockWallpaper ?: homeWallpaper, null) + } } }, wallpaperInteractor = wallpaperInteractor, @@ -158,7 +160,8 @@ private constructor( Icon.Loaded( drawable = getAffordanceIcon(affordanceModel.iconResourceId), - contentDescription = null, + contentDescription = + Text.Loaded(getSlotContentDescription(slot.id)), ), text = Text.Loaded(affordanceModel.name), isSelected = MutableStateFlow(true) as StateFlow<Boolean>, @@ -214,7 +217,13 @@ private constructor( if (!isSelected) { { viewModelScope.launch { - quickAffordanceInteractor.unselectAll(selectedSlotId) + quickAffordanceInteractor.unselectAllFromSlot( + selectedSlotId + ) + logger.logShortcutApplied( + shortcut = "none", + shortcutSlotId = selectedSlotId, + ) } } } else { @@ -250,6 +259,10 @@ private constructor( slotId = selectedSlotId, affordanceId = affordance.id, ) + logger.logShortcutApplied( + shortcut = affordance.id, + shortcutSlotId = selectedSlotId, + ) } } } else { @@ -423,6 +436,18 @@ private constructor( ) } + private fun getSlotContentDescription(slotId: String): String { + return applicationContext.getString( + when (slotId) { + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START -> + R.string.keyguard_slot_name_bottom_start + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END -> + R.string.keyguard_slot_name_bottom_end + else -> error("No accessibility label for slot with ID \"$slotId\"!") + } + ) + } + private suspend fun getAffordanceIcon(@DrawableRes iconResourceId: Int): Drawable { return quickAffordanceInteractor.getAffordanceIcon(iconResourceId) } @@ -463,6 +488,7 @@ private constructor( private val quickAffordanceInteractor: KeyguardQuickAffordancePickerInteractor, private val wallpaperInteractor: WallpaperInteractor, private val wallpaperInfoFactory: CurrentWallpaperInfoFactory, + private val logger: ThemesUserEventLogger, ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") @@ -471,6 +497,7 @@ private constructor( quickAffordanceInteractor = quickAffordanceInteractor, wallpaperInteractor = wallpaperInteractor, wallpaperInfoFactory = wallpaperInfoFactory, + logger = logger, ) as T } diff --git a/src/com/android/customization/picker/theme/CustomThemeActivity.java b/src/com/android/customization/picker/theme/CustomThemeActivity.java deleted file mode 100644 index 62a2f266..00000000 --- a/src/com/android/customization/picker/theme/CustomThemeActivity.java +++ /dev/null @@ -1,421 +0,0 @@ -/* - * Copyright (C) 2019 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.theme; - -import android.app.AlertDialog.Builder; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; - -import com.android.customization.model.CustomizationManager.Callback; -import com.android.customization.model.theme.DefaultThemeProvider; -import com.android.customization.model.theme.OverlayManagerCompat; -import com.android.customization.model.theme.ThemeBundle; -import com.android.customization.model.theme.ThemeBundleProvider; -import com.android.customization.model.theme.ThemeManager; -import com.android.customization.model.theme.custom.ColorOptionsProvider; -import com.android.customization.model.theme.custom.CustomTheme; -import com.android.customization.model.theme.custom.CustomThemeManager; -import com.android.customization.model.theme.custom.FontOptionsProvider; -import com.android.customization.model.theme.custom.IconOptionsProvider; -import com.android.customization.model.theme.custom.ShapeOptionsProvider; -import com.android.customization.model.theme.custom.ThemeComponentOption; -import com.android.customization.model.theme.custom.ThemeComponentOption.ColorOption; -import com.android.customization.model.theme.custom.ThemeComponentOption.FontOption; -import com.android.customization.model.theme.custom.ThemeComponentOption.IconOption; -import com.android.customization.model.theme.custom.ThemeComponentOption.ShapeOption; -import com.android.customization.model.theme.custom.ThemeComponentOptionProvider; -import com.android.customization.module.CustomizationInjector; -import com.android.customization.module.ThemesUserEventLogger; -import com.android.customization.picker.theme.CustomThemeStepFragment.CustomThemeComponentStepHost; -import com.android.wallpaper.R; -import com.android.wallpaper.module.InjectorProvider; -import com.android.wallpaper.picker.AppbarFragment.AppbarFragmentHost; - -import org.json.JSONException; - -import java.util.ArrayList; -import java.util.List; - -public class CustomThemeActivity extends FragmentActivity implements - AppbarFragmentHost, CustomThemeComponentStepHost { - public static final String EXTRA_THEME_ID = "CustomThemeActivity.ThemeId"; - public static final String EXTRA_THEME_TITLE = "CustomThemeActivity.ThemeTitle"; - public static final String EXTRA_THEME_PACKAGES = "CustomThemeActivity.ThemePackages"; - public static final int REQUEST_CODE_CUSTOM_THEME = 1; - public static final int RESULT_THEME_DELETED = 10; - public static final int RESULT_THEME_APPLIED = 20; - - private static final String TAG = "CustomThemeActivity"; - private static final String KEY_STATE_CURRENT_STEP = "CustomThemeActivity.currentStep"; - - private ThemesUserEventLogger mUserEventLogger; - private List<ComponentStep<?>> mSteps; - private int mCurrentStep; - private CustomThemeManager mCustomThemeManager; - private ThemeManager mThemeManager; - private TextView mNextButton; - private TextView mPreviousButton; - - @Override - protected void onCreate(Bundle savedInstanceState) { - CustomizationInjector injector = (CustomizationInjector) InjectorProvider.getInjector(); - mUserEventLogger = (ThemesUserEventLogger) injector.getUserEventLogger(this); - ThemeBundleProvider themeProvider = - new DefaultThemeProvider(this, injector.getCustomizationPreferences(this)); - Intent intent = getIntent(); - CustomTheme customTheme = null; - if (intent != null && intent.hasExtra(EXTRA_THEME_PACKAGES) - && intent.hasExtra(EXTRA_THEME_TITLE) && intent.hasExtra(EXTRA_THEME_ID)) { - try { - CustomTheme.Builder themeBuilder = themeProvider.parseCustomTheme( - intent.getStringExtra(EXTRA_THEME_PACKAGES)); - if (themeBuilder != null) { - themeBuilder.setId(intent.getStringExtra(EXTRA_THEME_ID)); - themeBuilder.setTitle(intent.getStringExtra(EXTRA_THEME_TITLE)); - customTheme = themeBuilder.build(this); - } - } catch (JSONException e) { - Log.w(TAG, "Couldn't parse provided custom theme, will override it"); - } - } - - mThemeManager = injector.getThemeManager( - new DefaultThemeProvider(this, injector.getCustomizationPreferences(this)), - this, - new OverlayManagerCompat(this), - mUserEventLogger); - mThemeManager.fetchOptions(null, false); - mCustomThemeManager = CustomThemeManager.create(customTheme, mThemeManager); - if (savedInstanceState != null) { - mCustomThemeManager.readCustomTheme(themeProvider, savedInstanceState); - } - - int currentStep = 0; - if (savedInstanceState != null) { - currentStep = savedInstanceState.getInt(KEY_STATE_CURRENT_STEP); - } - initSteps(currentStep); - - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_custom_theme); - mNextButton = findViewById(R.id.next_button); - mNextButton.setOnClickListener(view -> onNextOrApply()); - mPreviousButton = findViewById(R.id.previous_button); - mPreviousButton.setOnClickListener(view -> onBackPressed()); - - FragmentManager fm = getSupportFragmentManager(); - Fragment fragment = fm.findFragmentById(R.id.fragment_container); - if (fragment == null) { - // Navigate to the first step - navigateToStep(0); - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt(KEY_STATE_CURRENT_STEP, mCurrentStep); - if (mCustomThemeManager != null) { - mCustomThemeManager.saveCustomTheme(this, outState); - } - } - - private void navigateToStep(int i) { - FragmentManager fragmentManager = getSupportFragmentManager(); - ComponentStep step = mSteps.get(i); - Fragment fragment = step.getFragment(mCustomThemeManager.getOriginalTheme().getTitle()); - - FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); - fragmentTransaction.replace(R.id.fragment_container, fragment); - // Don't add step 0 to the back stack so that going back from it just finishes the Activity - if (i > 0) { - fragmentTransaction.addToBackStack("Step " + i); - } - fragmentTransaction.commit(); - fragmentManager.executePendingTransactions(); - updateNavigationButtonLabels(); - } - - private void initSteps(int currentStep) { - mSteps = new ArrayList<>(); - OverlayManagerCompat manager = new OverlayManagerCompat(this); - mSteps.add(new FontStep(new FontOptionsProvider(this, manager), 0)); - mSteps.add(new IconStep(new IconOptionsProvider(this, manager), 1)); - mSteps.add(new ColorStep(new ColorOptionsProvider(this, manager, mCustomThemeManager), 2)); - mSteps.add(new ShapeStep(new ShapeOptionsProvider(this, manager), 3)); - mSteps.add(new NameStep(4)); - mCurrentStep = currentStep; - } - - private void onNextOrApply() { - CustomThemeStepFragment stepFragment = getCurrentStepFragment(); - if (stepFragment instanceof CustomThemeComponentFragment) { - CustomThemeComponentFragment fragment = (CustomThemeComponentFragment) stepFragment; - mCustomThemeManager.apply(fragment.getSelectedOption(), new Callback() { - @Override - public void onSuccess() { - navigateToStep(mCurrentStep + 1); - } - - @Override - public void onError(@Nullable Throwable throwable) { - Log.w(TAG, "Error applying custom theme component", throwable); - Toast.makeText(CustomThemeActivity.this, R.string.apply_theme_error_msg, - Toast.LENGTH_LONG).show(); - } - }); - } else if (stepFragment instanceof CustomThemeNameFragment) { - CustomThemeNameFragment fragment = (CustomThemeNameFragment) stepFragment; - CustomTheme originalTheme = mCustomThemeManager.getOriginalTheme(); - - // We're on the last step, apply theme and leave - CustomTheme themeToApply = mCustomThemeManager.buildPartialCustomTheme(this, - originalTheme.getId(), fragment.getThemeName()); - - // If the current theme is equal to the original theme being edited, then - // don't search for an equivalent, let the user apply the same one by keeping - // it null. - ThemeBundle equivalent = (originalTheme.isEquivalent(themeToApply)) - ? null : mThemeManager.findThemeByPackages(themeToApply); - - if (equivalent != null) { - Builder builder = - new Builder(CustomThemeActivity.this); - builder.setTitle(getString(R.string.use_style_instead_title, - equivalent.getTitle())) - .setMessage(getString(R.string.use_style_instead_body, - equivalent.getTitle())) - .setPositiveButton(getString(R.string.use_style_button, - equivalent.getTitle()), - (dialogInterface, i) -> applyTheme(equivalent)) - .setNegativeButton(R.string.no_thanks, null) - .create() - .show(); - } else { - applyTheme(themeToApply); - } - } else { - throw new IllegalStateException("Unknown CustomThemeStepFragment"); - } - } - - private void applyTheme(ThemeBundle themeToApply) { - mThemeManager.apply(themeToApply, new Callback() { - @Override - public void onSuccess() { - overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - Toast.makeText(getApplicationContext(), R.string.applied_theme_msg, - Toast.LENGTH_LONG).show(); - setResult(RESULT_THEME_APPLIED); - finish(); - } - - @Override - public void onError(@Nullable Throwable throwable) { - Log.w(TAG, "Error applying custom theme", throwable); - Toast.makeText(CustomThemeActivity.this, - R.string.apply_theme_error_msg, - Toast.LENGTH_LONG).show(); - } - }); - } - - private CustomThemeStepFragment getCurrentStepFragment() { - return (CustomThemeStepFragment) - getSupportFragmentManager().findFragmentById(R.id.fragment_container); - } - - @Override - public void setCurrentStep(int i) { - mCurrentStep = i; - updateNavigationButtonLabels(); - } - - private void updateNavigationButtonLabels() { - mPreviousButton.setVisibility(mCurrentStep == 0 ? View.INVISIBLE : View.VISIBLE); - mNextButton.setText((mCurrentStep < mSteps.size() -1) ? R.string.custom_theme_next - : R.string.apply_btn); - } - - @Override - public void delete() { - mThemeManager.removeCustomTheme(mCustomThemeManager.getOriginalTheme()); - setResult(RESULT_THEME_DELETED); - finish(); - } - - @Override - public void cancel() { - finish(); - } - - @Override - public ThemeComponentOptionProvider<? extends ThemeComponentOption> getComponentOptionProvider( - int position) { - return mSteps.get(position).provider; - } - - @Override - public CustomThemeManager getCustomThemeManager() { - return mCustomThemeManager; - } - - @Override - public void onUpArrowPressed() { - // Skip it because CustomThemeStepFragment will implement cancel button - // (instead of up arrow) on action bar. - } - - @Override - public boolean isUpArrowSupported() { - // Skip it because CustomThemeStepFragment will implement cancel button - // (instead of up arrow) on action bar. - return false; - } - - /** - * Represents a step in selecting a custom theme, picking a particular component (eg font, - * color, shape, etc). - * Each step has a Fragment instance associated that instances of this class will provide. - */ - private static abstract class ComponentStep<T extends ThemeComponentOption> { - @StringRes final int titleResId; - @StringRes final int accessibilityResId; - final ThemeComponentOptionProvider<T> provider; - final int position; - private CustomThemeStepFragment mFragment; - - protected ComponentStep(@StringRes int titleResId, @StringRes int accessibilityResId, - ThemeComponentOptionProvider<T> provider, int position) { - this.titleResId = titleResId; - this.accessibilityResId = accessibilityResId; - this.provider = provider; - this.position = position; - } - - CustomThemeStepFragment getFragment(String title) { - if (mFragment == null) { - mFragment = createFragment(title); - } - return mFragment; - } - - /** - * @return a newly created fragment that will handle this step's UI. - */ - abstract CustomThemeStepFragment createFragment(String title); - } - - private class FontStep extends ComponentStep<FontOption> { - - protected FontStep(ThemeComponentOptionProvider<FontOption> provider, - int position) { - super(R.string.font_component_title, R.string.accessibility_custom_font_title, provider, - position); - } - - @Override - CustomThemeComponentFragment createFragment(String title) { - return CustomThemeComponentFragment.newInstance( - title, - position, - titleResId, - accessibilityResId); - } - } - - private class IconStep extends ComponentStep<IconOption> { - - protected IconStep(ThemeComponentOptionProvider<IconOption> provider, - int position) { - super(R.string.icon_component_title, R.string.accessibility_custom_icon_title, provider, - position); - } - - @Override - CustomThemeComponentFragment createFragment(String title) { - return CustomThemeComponentFragment.newInstance( - title, - position, - titleResId, - accessibilityResId); - } - } - - private class ColorStep extends ComponentStep<ColorOption> { - - protected ColorStep(ThemeComponentOptionProvider<ColorOption> provider, - int position) { - super(R.string.color_component_title, R.string.accessibility_custom_color_title, - provider, position); - } - - @Override - CustomThemeComponentFragment createFragment(String title) { - return CustomThemeComponentFragment.newInstance( - title, - position, - titleResId, - accessibilityResId); - } - } - - private class ShapeStep extends ComponentStep<ShapeOption> { - - protected ShapeStep(ThemeComponentOptionProvider<ShapeOption> provider, - int position) { - super(R.string.shape_component_title, R.string.accessibility_custom_shape_title, - provider, position); - } - - @Override - CustomThemeComponentFragment createFragment(String title) { - return CustomThemeComponentFragment.newInstance( - title, - position, - titleResId, - accessibilityResId); - } - } - - private class NameStep extends ComponentStep { - - protected NameStep(int position) { - super(R.string.name_component_title, R.string.accessibility_custom_name_title, null, - position); - } - - @Override - CustomThemeNameFragment createFragment(String title) { - return CustomThemeNameFragment.newInstance( - title, - position, - titleResId, - accessibilityResId); - } - } -} diff --git a/src/com/android/customization/picker/theme/CustomThemeComponentFragment.java b/src/com/android/customization/picker/theme/CustomThemeComponentFragment.java deleted file mode 100644 index a1e99677..00000000 --- a/src/com/android/customization/picker/theme/CustomThemeComponentFragment.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2019 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.theme; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -import com.android.customization.model.theme.custom.ThemeComponentOption; -import com.android.customization.model.theme.custom.ThemeComponentOptionProvider; -import com.android.customization.widget.OptionSelectorController; -import com.android.customization.widget.OptionSelectorController.CheckmarkStyle; -import com.android.wallpaper.R; -import com.android.wallpaper.picker.AppbarFragment; - -public class CustomThemeComponentFragment extends CustomThemeStepFragment { - private static final String ARG_USE_GRID_LAYOUT = "CustomThemeComponentFragment.use_grid";; - - public static CustomThemeComponentFragment newInstance(CharSequence toolbarTitle, int position, - int titleResId, int accessibilityResId) { - return newInstance(toolbarTitle, position, titleResId, accessibilityResId, false); - } - - public static CustomThemeComponentFragment newInstance(CharSequence toolbarTitle, int position, - int titleResId, int accessibilityResId, boolean allowGridLayout) { - CustomThemeComponentFragment fragment = new CustomThemeComponentFragment(); - Bundle arguments = AppbarFragment.createArguments(toolbarTitle); - arguments.putInt(ARG_KEY_POSITION, position); - arguments.putInt(ARG_KEY_TITLE_RES_ID, titleResId); - arguments.putInt(ARG_KEY_ACCESSIBILITY_RES_ID, accessibilityResId); - arguments.putBoolean(ARG_USE_GRID_LAYOUT, allowGridLayout); - fragment.setArguments(arguments); - return fragment; - } - - private ThemeComponentOptionProvider<? extends ThemeComponentOption> mProvider; - private boolean mUseGridLayout; - - private RecyclerView mOptionsContainer; - private OptionSelectorController<ThemeComponentOption> mOptionsController; - private ThemeComponentOption mSelectedOption; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mUseGridLayout = getArguments().getBoolean(ARG_USE_GRID_LAYOUT); - mProvider = mHost.getComponentOptionProvider(mPosition); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View view = super.onCreateView(inflater, container, savedInstanceState); - mOptionsContainer = view.findViewById(R.id.options_container); - mPreviewContainer = view.findViewById(R.id.component_preview_content); - mTitle = view.findViewById(R.id.component_options_title); - mTitle.setText(mTitleResId); - setUpOptions(); - - return view; - } - - @Override - protected int getFragmentLayoutResId() { - return R.layout.fragment_custom_theme_component; - } - - public ThemeComponentOption getSelectedOption() { - return mSelectedOption; - } - - private void bindPreview() { - mSelectedOption.bindPreview(mPreviewContainer); - } - - private void setUpOptions() { - mProvider.fetch(options -> { - mOptionsController = new OptionSelectorController( - mOptionsContainer, options, mUseGridLayout, CheckmarkStyle.NONE); - - mOptionsController.addListener(selected -> { - mSelectedOption = (ThemeComponentOption) selected; - bindPreview(); - // Preview and apply. The selection will be kept whatever user goes to previous page - // or encounter system config changes, the current selection can be recovered. - mCustomThemeManager.apply(mSelectedOption, /* callback= */ null); - }); - mOptionsController.initOptions(mCustomThemeManager); - - for (ThemeComponentOption option : options) { - if (option.isActive(mCustomThemeManager)) { - mSelectedOption = option; - break; - } - } - if (mSelectedOption == null) { - mSelectedOption = options.get(0); - } - mOptionsController.setSelectedOption(mSelectedOption); - }, false); - } -} diff --git a/src/com/android/customization/picker/theme/CustomThemeNameFragment.java b/src/com/android/customization/picker/theme/CustomThemeNameFragment.java deleted file mode 100644 index ea9099fc..00000000 --- a/src/com/android/customization/picker/theme/CustomThemeNameFragment.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2019 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.theme; - -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.customization.model.theme.ThemeBundle.PreviewInfo; -import com.android.customization.model.theme.custom.CustomTheme; -import com.android.customization.module.CustomizationInjector; -import com.android.customization.module.CustomizationPreferences; -import com.android.customization.picker.WallpaperPreviewer; -import com.android.wallpaper.R; -import com.android.wallpaper.module.CurrentWallpaperInfoFactory; -import com.android.wallpaper.module.InjectorProvider; -import com.android.wallpaper.picker.AppbarFragment; - -import org.json.JSONArray; -import org.json.JSONException; - -/** Fragment of naming a custom theme. */ -public class CustomThemeNameFragment extends CustomThemeStepFragment { - - private static final String TAG = "CustomThemeNameFragment"; - - public static CustomThemeNameFragment newInstance(CharSequence toolbarTitle, int position, - int titleResId, int accessibilityResId) { - CustomThemeNameFragment fragment = new CustomThemeNameFragment(); - Bundle arguments = AppbarFragment.createArguments(toolbarTitle); - arguments.putInt(ARG_KEY_POSITION, position); - arguments.putInt(ARG_KEY_TITLE_RES_ID, titleResId); - arguments.putInt(ARG_KEY_ACCESSIBILITY_RES_ID, accessibilityResId); - fragment.setArguments(arguments); - return fragment; - } - - private EditText mNameEditor; - private ImageView mWallpaperImage; - private ThemeOptionPreviewer mThemeOptionPreviewer; - private CustomizationPreferences mCustomizationPreferences; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View view = super.onCreateView(inflater, container, savedInstanceState); - mTitle = view.findViewById(R.id.component_options_title); - mTitle.setText(mTitleResId); - CurrentWallpaperInfoFactory currentWallpaperFactory = InjectorProvider.getInjector() - .getCurrentWallpaperInfoFactory(getActivity().getApplicationContext()); - CustomizationInjector injector = (CustomizationInjector) InjectorProvider.getInjector(); - mCustomizationPreferences = injector.getCustomizationPreferences(getContext()); - - // Set theme option. - ViewGroup previewContainer = view.findViewById(R.id.theme_preview_container); - mThemeOptionPreviewer = new ThemeOptionPreviewer(getLifecycle(), getContext(), - previewContainer); - PreviewInfo previewInfo = mCustomThemeManager.buildCustomThemePreviewInfo(getContext()); - mThemeOptionPreviewer.setPreviewInfo(previewInfo); - - // Set wallpaper background. - mWallpaperImage = view.findViewById(R.id.wallpaper_preview_image); - final WallpaperPreviewer wallpaperPreviewer = new WallpaperPreviewer( - getLifecycle(), - getActivity(), - mWallpaperImage, - view.findViewById(R.id.wallpaper_preview_surface)); - currentWallpaperFactory.createCurrentWallpaperInfos( - (homeWallpaper, lockWallpaper, presentationMode) -> { - wallpaperPreviewer.setWallpaper(homeWallpaper, - mThemeOptionPreviewer::updateColorForLauncherWidgets); - }, false); - - // Set theme default name. - mNameEditor = view.findViewById(R.id.custom_theme_name); - mNameEditor.setText(getOriginalThemeName()); - return view; - } - - private String getOriginalThemeName() { - CustomTheme originalTheme = mCustomThemeManager.getOriginalTheme(); - if (originalTheme == null || !originalTheme.isDefined()) { - // For new custom theme. use custom themes amount plus 1 as default naming. - String serializedThemes = mCustomizationPreferences.getSerializedCustomThemes(); - int customThemesCount = 0; - if (!TextUtils.isEmpty(serializedThemes)) { - try { - JSONArray customThemes = new JSONArray(serializedThemes); - customThemesCount = customThemes.length(); - } catch (JSONException e) { - Log.w(TAG, "Couldn't read stored custom theme"); - } - } - return getContext().getString( - R.string.custom_theme_title, customThemesCount + 1); - } else { - // For existing custom theme, keep its name as default naming. - return originalTheme.getTitle(); - } - } - - @Override - protected int getFragmentLayoutResId() { - return R.layout.fragment_custom_theme_name; - } - - public String getThemeName() { - return mNameEditor.getText().toString(); - } -} diff --git a/src/com/android/customization/picker/theme/CustomThemeStepFragment.java b/src/com/android/customization/picker/theme/CustomThemeStepFragment.java deleted file mode 100644 index 3f07431d..00000000 --- a/src/com/android/customization/picker/theme/CustomThemeStepFragment.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.android.customization.picker.theme; - -import android.app.AlertDialog; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; - -import com.android.customization.model.theme.custom.CustomThemeManager; -import com.android.customization.model.theme.custom.ThemeComponentOption; -import com.android.customization.model.theme.custom.ThemeComponentOptionProvider; -import com.android.wallpaper.R; -import com.android.wallpaper.picker.AppbarFragment; - -abstract class CustomThemeStepFragment extends AppbarFragment { - protected static final String ARG_KEY_POSITION = "CustomThemeStepFragment.position"; - protected static final String ARG_KEY_TITLE_RES_ID = "CustomThemeStepFragment.title_res"; - protected static final String ARG_KEY_ACCESSIBILITY_RES_ID = - "CustomThemeStepFragment.accessibility_res"; - protected CustomThemeComponentStepHost mHost; - protected CustomThemeManager mCustomThemeManager; - protected int mPosition; - protected ViewGroup mPreviewContainer; - protected TextView mTitle; - @StringRes - protected int mTitleResId; - @StringRes - protected int mAccessibilityResId; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mHost = (CustomThemeComponentStepHost) context; - } - - @Override - public void onResume() { - super.onResume(); - mHost.setCurrentStep(mPosition); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mPosition = getArguments().getInt(ARG_KEY_POSITION); - mTitleResId = getArguments().getInt(ARG_KEY_TITLE_RES_ID); - mAccessibilityResId = getArguments().getInt(ARG_KEY_ACCESSIBILITY_RES_ID); - mCustomThemeManager = mHost.getCustomThemeManager(); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View view = inflater.inflate( - getFragmentLayoutResId(), container, /* attachToRoot */ false); - // No original theme means it's a new one, so no toolbar icon for deleting it is needed - if (mCustomThemeManager.getOriginalTheme() == null - || !mCustomThemeManager.getOriginalTheme().isDefined()) { - setUpToolbar(view); - } else { - setUpToolbar(view, R.menu.custom_theme_editor_menu); - mToolbar.getMenu().getItem(0).setIconTintList( - getContext().getColorStateList(R.color.toolbar_icon_tint)); - } - Drawable closeIcon = getResources().getDrawable(R.drawable.ic_close_24px, null).mutate(); - closeIcon.setTintList(getResources().getColorStateList(R.color.toolbar_icon_tint, null)); - mToolbar.setNavigationIcon(closeIcon); - - mToolbar.setNavigationContentDescription(R.string.cancel); - mToolbar.setNavigationOnClickListener(v -> mHost.cancel()); - - mPreviewContainer = view.findViewById(R.id.component_preview_content); - return view; - } - - @Override - protected String getAccessibilityTitle() { - return getString(mAccessibilityResId); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (item.getItemId() == R.id.custom_theme_delete) { - AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setMessage(R.string.delete_custom_theme_confirmation) - .setPositiveButton(R.string.delete_custom_theme_button, - (dialogInterface, i) -> mHost.delete()) - .setNegativeButton(R.string.cancel, null) - .create() - .show(); - return true; - } - return super.onMenuItemClick(item); - } - - protected abstract int getFragmentLayoutResId(); - - public interface CustomThemeComponentStepHost { - void delete(); - void cancel(); - ThemeComponentOptionProvider<? extends ThemeComponentOption> getComponentOptionProvider( - int position); - - CustomThemeManager getCustomThemeManager(); - - void setCurrentStep(int step); - } -} diff --git a/src/com/android/customization/picker/theme/ThemeFragment.java b/src/com/android/customization/picker/theme/ThemeFragment.java deleted file mode 100644 index 3a9a56f5..00000000 --- a/src/com/android/customization/picker/theme/ThemeFragment.java +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Copyright (C) 2018 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.theme; - -import static android.app.Activity.RESULT_OK; - -import static com.android.wallpaper.widget.BottomActionBar.BottomAction.APPLY; -import static com.android.wallpaper.widget.BottomActionBar.BottomAction.CUSTOMIZE; -import static com.android.wallpaper.widget.BottomActionBar.BottomAction.INFORMATION; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.widget.ContentLoadingProgressBar; -import androidx.recyclerview.widget.RecyclerView; - -import com.android.customization.model.CustomizationManager.Callback; -import com.android.customization.model.CustomizationManager.OptionsFetchedListener; -import com.android.customization.model.CustomizationOption; -import com.android.customization.model.theme.ThemeBundle; -import com.android.customization.model.theme.ThemeManager; -import com.android.customization.model.theme.custom.CustomTheme; -import com.android.customization.module.ThemesUserEventLogger; -import com.android.customization.picker.WallpaperPreviewer; -import com.android.customization.widget.OptionSelectorController; -import com.android.wallpaper.R; -import com.android.wallpaper.model.WallpaperInfo; -import com.android.wallpaper.module.CurrentWallpaperInfoFactory; -import com.android.wallpaper.module.InjectorProvider; -import com.android.wallpaper.picker.AppbarFragment; -import com.android.wallpaper.widget.BottomActionBar; -import com.android.wallpaper.widget.BottomActionBar.AccessibilityCallback; -import com.android.wallpaper.widget.BottomActionBar.BottomSheetContent; - -import java.util.List; - -/** - * Fragment that contains the main UI for selecting and applying a ThemeBundle. - */ -public class ThemeFragment extends AppbarFragment { - - private static final String TAG = "ThemeFragment"; - private static final String KEY_SELECTED_THEME = "ThemeFragment.SelectedThemeBundle"; - private static final String KEY_STATE_BOTTOM_ACTION_BAR_VISIBLE = - "ThemeFragment.bottomActionBarVisible"; - private static final int FULL_PREVIEW_REQUEST_CODE = 1000; - - /** - * Interface to be implemented by an Activity hosting a {@link ThemeFragment} - */ - public interface ThemeFragmentHost { - ThemeManager getThemeManager(); - } - public static ThemeFragment newInstance(CharSequence title) { - ThemeFragment fragment = new ThemeFragment(); - fragment.setArguments(AppbarFragment.createArguments(title)); - return fragment; - } - - private RecyclerView mOptionsContainer; - private OptionSelectorController<ThemeBundle> mOptionsController; - private ThemeManager mThemeManager; - private ThemesUserEventLogger mEventLogger; - private ThemeBundle mSelectedTheme; - private ContentLoadingProgressBar mLoading; - private View mContent; - private View mError; - private WallpaperInfo mCurrentHomeWallpaper; - private CurrentWallpaperInfoFactory mCurrentWallpaperFactory; - private BottomActionBar mBottomActionBar; - private WallpaperPreviewer mWallpaperPreviewer; - private ImageView mWallpaperImage; - private ThemeOptionPreviewer mThemeOptionPreviewer; - private ThemeInfoView mThemeInfoView; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mThemeManager = ((ThemeFragmentHost) context).getThemeManager(); - mEventLogger = (ThemesUserEventLogger) - InjectorProvider.getInjector().getUserEventLogger(context); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View view = inflater.inflate( - R.layout.fragment_theme_picker, container, /* attachToRoot */ false); - setUpToolbar(view); - - mContent = view.findViewById(R.id.content_section); - mLoading = view.findViewById(R.id.loading_indicator); - mError = view.findViewById(R.id.error_section); - mCurrentWallpaperFactory = InjectorProvider.getInjector() - .getCurrentWallpaperInfoFactory(getActivity().getApplicationContext()); - mOptionsContainer = view.findViewById(R.id.options_container); - - mThemeOptionPreviewer = new ThemeOptionPreviewer( - getLifecycle(), - getContext(), - view.findViewById(R.id.theme_preview_container)); - - // Set Wallpaper background. - mWallpaperImage = view.findViewById(R.id.wallpaper_preview_image); - mWallpaperPreviewer = new WallpaperPreviewer( - getLifecycle(), - getActivity(), - mWallpaperImage, - view.findViewById(R.id.wallpaper_preview_surface)); - mCurrentWallpaperFactory.createCurrentWallpaperInfos( - (homeWallpaper, lockWallpaper, presentationMode) -> { - mCurrentHomeWallpaper = homeWallpaper; - mWallpaperPreviewer.setWallpaper(mCurrentHomeWallpaper, - mThemeOptionPreviewer::updateColorForLauncherWidgets); - }, false); - return view; - } - - @Override - protected void onBottomActionBarReady(BottomActionBar bottomActionBar) { - super.onBottomActionBarReady(bottomActionBar); - mBottomActionBar = bottomActionBar; - mBottomActionBar.showActionsOnly(INFORMATION, APPLY); - mBottomActionBar.setActionClickListener(APPLY, v -> { - mBottomActionBar.disableActions(); - applyTheme(); - }); - - mBottomActionBar.bindBottomSheetContentWithAction( - new ThemeInfoContent(getContext()), INFORMATION); - mBottomActionBar.setActionClickListener(CUSTOMIZE, this::onCustomizeClicked); - - // Update target view's accessibility param since it will be blocked by the bottom sheet - // when expanded. - mBottomActionBar.setAccessibilityCallback(new AccessibilityCallback() { - @Override - public void onBottomSheetCollapsed() { - mOptionsContainer.setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - - @Override - public void onBottomSheetExpanded() { - mOptionsContainer.setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - } - }); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - // Setup options here when all views are ready(including BottomActionBar), since we need to - // update views after options are loaded. - setUpOptions(savedInstanceState); - } - - private void applyTheme() { - mThemeManager.apply(mSelectedTheme, new Callback() { - @Override - public void onSuccess() { - Toast.makeText(getContext(), R.string.applied_theme_msg, Toast.LENGTH_LONG).show(); - getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - getActivity().finish(); - } - - @Override - public void onError(@Nullable Throwable throwable) { - Log.w(TAG, "Error applying theme", throwable); - // Since we disabled it when clicked apply button. - mBottomActionBar.enableActions(); - mBottomActionBar.hide(); - Toast.makeText(getContext(), R.string.apply_theme_error_msg, - Toast.LENGTH_LONG).show(); - } - }); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (mSelectedTheme != null && !mSelectedTheme.isActive(mThemeManager)) { - outState.putString(KEY_SELECTED_THEME, mSelectedTheme.getSerializedPackages()); - } - if (mBottomActionBar != null) { - outState.putBoolean(KEY_STATE_BOTTOM_ACTION_BAR_VISIBLE, mBottomActionBar.isVisible()); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == CustomThemeActivity.REQUEST_CODE_CUSTOM_THEME) { - if (resultCode == CustomThemeActivity.RESULT_THEME_DELETED) { - mSelectedTheme = null; - reloadOptions(); - } else if (resultCode == CustomThemeActivity.RESULT_THEME_APPLIED) { - getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - getActivity().finish(); - } else { - if (mSelectedTheme != null) { - mOptionsController.setSelectedOption(mSelectedTheme); - // Set selected option above will show BottomActionBar, - // hide BottomActionBar for the mis-trigger. - mBottomActionBar.hide(); - } else { - reloadOptions(); - } - } - } else if (requestCode == FULL_PREVIEW_REQUEST_CODE && resultCode == RESULT_OK) { - applyTheme(); - } - super.onActivityResult(requestCode, resultCode, data); - } - - private void onCustomizeClicked(View view) { - if (mSelectedTheme instanceof CustomTheme) { - navigateToCustomTheme((CustomTheme) mSelectedTheme); - } - } - - private void hideError() { - mContent.setVisibility(View.VISIBLE); - mError.setVisibility(View.GONE); - } - - private void showError() { - mLoading.hide(); - mContent.setVisibility(View.GONE); - mError.setVisibility(View.VISIBLE); - } - - private void setUpOptions(@Nullable Bundle savedInstanceState) { - hideError(); - mLoading.show(); - mThemeManager.fetchOptions(new OptionsFetchedListener<ThemeBundle>() { - @Override - public void onOptionsLoaded(List<ThemeBundle> options) { - mOptionsController = new OptionSelectorController<>(mOptionsContainer, options); - mOptionsController.initOptions(mThemeManager); - - // Find out the selected theme option. - // 1. Find previously selected theme. - String previouslySelected = savedInstanceState != null - ? savedInstanceState.getString(KEY_SELECTED_THEME) : null; - ThemeBundle previouslySelectedTheme = null; - ThemeBundle activeTheme = null; - for (ThemeBundle theme : options) { - if (previouslySelected != null - && previouslySelected.equals(theme.getSerializedPackages())) { - previouslySelectedTheme = theme; - } - if (theme.isActive(mThemeManager)) { - activeTheme = theme; - } - } - // 2. Use active theme if no previously selected theme. - mSelectedTheme = previouslySelectedTheme != null - ? previouslySelectedTheme - : activeTheme; - // 3. Select the first system theme(default theme currently) - // if there is no matching custom enabled theme. - if (mSelectedTheme == null) { - mSelectedTheme = findFirstSystemThemeBundle(options); - } - - mOptionsController.setSelectedOption(mSelectedTheme); - onOptionSelected(mSelectedTheme); - restoreBottomActionBarVisibility(savedInstanceState); - - mOptionsController.addListener(selectedOption -> { - onOptionSelected(selectedOption); - if (!isAddCustomThemeOption(selectedOption)) { - mBottomActionBar.show(); - } - }); - mLoading.hide(); - } - @Override - public void onError(@Nullable Throwable throwable) { - if (throwable != null) { - Log.e(TAG, "Error loading theme bundles", throwable); - } - showError(); - } - }, false); - } - - private void reloadOptions() { - mThemeManager.fetchOptions(options -> { - mOptionsController.resetOptions(options); - for (ThemeBundle theme : options) { - if (theme.isActive(mThemeManager)) { - mSelectedTheme = theme; - break; - } - } - if (mSelectedTheme == null) { - mSelectedTheme = findFirstSystemThemeBundle(options); - } - mOptionsController.setSelectedOption(mSelectedTheme); - // Set selected option above will show BottomActionBar, - // hide BottomActionBar for the mis-trigger. - mBottomActionBar.hide(); - }, true); - } - - private ThemeBundle findFirstSystemThemeBundle(List<ThemeBundle> options) { - for (ThemeBundle bundle : options) { - if (!(bundle instanceof CustomTheme)) { - return bundle; - } - } - return null; - } - - private void onOptionSelected(CustomizationOption selectedOption) { - if (isAddCustomThemeOption(selectedOption)) { - navigateToCustomTheme((CustomTheme) selectedOption); - } else { - mSelectedTheme = (ThemeBundle) selectedOption; - mSelectedTheme.setOverrideThemeWallpaper(mCurrentHomeWallpaper); - mEventLogger.logThemeSelected(mSelectedTheme, - selectedOption instanceof CustomTheme); - mThemeOptionPreviewer.setPreviewInfo(mSelectedTheme.getPreviewInfo()); - if (mThemeInfoView != null && mSelectedTheme != null) { - mThemeInfoView.populateThemeInfo(mSelectedTheme); - } - - if (selectedOption instanceof CustomTheme) { - mBottomActionBar.showActionsOnly(INFORMATION, CUSTOMIZE, APPLY); - } else { - mBottomActionBar.showActionsOnly(INFORMATION, APPLY); - } - } - } - - private void restoreBottomActionBarVisibility(@Nullable Bundle savedInstanceState) { - boolean isBottomActionBarVisible = savedInstanceState != null - && savedInstanceState.getBoolean(KEY_STATE_BOTTOM_ACTION_BAR_VISIBLE); - if (isBottomActionBarVisible) { - mBottomActionBar.show(); - } else { - mBottomActionBar.hide(); - } - } - - private boolean isAddCustomThemeOption(CustomizationOption option) { - return option instanceof CustomTheme && !((CustomTheme) option).isDefined(); - } - - private void navigateToCustomTheme(CustomTheme themeToEdit) { - Intent intent = new Intent(getActivity(), CustomThemeActivity.class); - intent.putExtra(CustomThemeActivity.EXTRA_THEME_TITLE, themeToEdit.getTitle()); - intent.putExtra(CustomThemeActivity.EXTRA_THEME_ID, themeToEdit.getId()); - intent.putExtra(CustomThemeActivity.EXTRA_THEME_PACKAGES, - themeToEdit.getSerializedPackages()); - startActivityForResult(intent, CustomThemeActivity.REQUEST_CODE_CUSTOM_THEME); - } - - private final class ThemeInfoContent extends BottomSheetContent<ThemeInfoView> { - - private ThemeInfoContent(Context context) { - super(context); - } - - @Override - public int getViewId() { - return R.layout.theme_info_view; - } - - @Override - public void onViewCreated(ThemeInfoView view) { - mThemeInfoView = view; - if (mSelectedTheme != null) { - mThemeInfoView.populateThemeInfo(mSelectedTheme); - } - } - } -} diff --git a/src/com/android/customization/picker/theme/ThemeFullPreviewFragment.java b/src/com/android/customization/picker/theme/ThemeFullPreviewFragment.java deleted file mode 100644 index 3ba64ecc..00000000 --- a/src/com/android/customization/picker/theme/ThemeFullPreviewFragment.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2020 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.theme; - -import static android.app.Activity.RESULT_OK; - -import static com.android.wallpaper.widget.BottomActionBar.BottomAction.APPLY; -import static com.android.wallpaper.widget.BottomActionBar.BottomAction.INFORMATION; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.customization.model.theme.DefaultThemeProvider; -import com.android.customization.model.theme.ThemeBundle; -import com.android.customization.model.theme.ThemeBundleProvider; -import com.android.customization.module.CustomizationInjector; -import com.android.customization.picker.WallpaperPreviewer; -import com.android.wallpaper.R; -import com.android.wallpaper.model.WallpaperInfo; -import com.android.wallpaper.module.InjectorProvider; -import com.android.wallpaper.picker.AppbarFragment; -import com.android.wallpaper.widget.BottomActionBar; -import com.android.wallpaper.widget.BottomActionBar.BottomSheetContent; - -import com.bumptech.glide.Glide; - -import org.json.JSONException; - -/** A Fragment for theme full preview page. */ -public class ThemeFullPreviewFragment extends AppbarFragment { - private static final String TAG = "ThemeFullPreviewFragment"; - - public static final String EXTRA_THEME_OPTION_TITLE = "theme_option_title"; - protected static final String EXTRA_THEME_OPTION = "theme_option"; - protected static final String EXTRA_WALLPAPER_INFO = "wallpaper_info"; - protected static final String EXTRA_CAN_APPLY_FROM_FULL_PREVIEW = "can_apply"; - - private WallpaperInfo mWallpaper; - private ThemeBundle mThemeBundle; - private boolean mCanApplyFromFullPreview; - - /** - * Returns a new {@link ThemeFullPreviewFragment} with the provided title and bundle arguments - * set. - */ - public static ThemeFullPreviewFragment newInstance(CharSequence title, Bundle intentBundle) { - ThemeFullPreviewFragment fragment = new ThemeFullPreviewFragment(); - Bundle bundle = new Bundle(); - bundle.putAll(AppbarFragment.createArguments(title)); - bundle.putAll(intentBundle); - fragment.setArguments(bundle); - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mWallpaper = getArguments().getParcelable(EXTRA_WALLPAPER_INFO); - mCanApplyFromFullPreview = getArguments().getBoolean(EXTRA_CAN_APPLY_FROM_FULL_PREVIEW); - CustomizationInjector injector = (CustomizationInjector) InjectorProvider.getInjector(); - ThemeBundleProvider themeProvider = new DefaultThemeProvider( - getContext(), injector.getCustomizationPreferences(getContext())); - try { - ThemeBundle.Builder builder = themeProvider.parseThemeBundle( - getArguments().getString(EXTRA_THEME_OPTION)); - if (builder != null) { - builder.setTitle(getArguments().getString(EXTRA_THEME_OPTION_TITLE)); - mThemeBundle = builder.build(getContext()); - } - } catch (JSONException e) { - Log.w(TAG, "Couldn't parse provided custom theme, will override it"); - // TODO(chihhangchuang): Handle the error case. - } - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View view = inflater.inflate( - R.layout.fragment_theme_full_preview, container, /* attachToRoot */ false); - setUpToolbar(view); - Glide.get(getContext()).clearMemory(); - - // Set theme option. - final ThemeOptionPreviewer themeOptionPreviewer = new ThemeOptionPreviewer( - getLifecycle(), - getContext(), - view.findViewById(R.id.theme_preview_container)); - themeOptionPreviewer.setPreviewInfo(mThemeBundle.getPreviewInfo()); - - // Set wallpaper background. - ImageView wallpaperImageView = view.findViewById(R.id.wallpaper_preview_image); - final WallpaperPreviewer wallpaperPreviewer = new WallpaperPreviewer( - getLifecycle(), - getActivity(), - wallpaperImageView, - view.findViewById(R.id.wallpaper_preview_surface)); - wallpaperPreviewer.setWallpaper(mWallpaper, - themeOptionPreviewer::updateColorForLauncherWidgets); - return view; - } - - @Override - protected void onBottomActionBarReady(BottomActionBar bottomActionBar) { - super.onBottomActionBarReady(bottomActionBar); - if (mCanApplyFromFullPreview) { - bottomActionBar.showActionsOnly(INFORMATION, APPLY); - bottomActionBar.setActionClickListener(APPLY, v -> finishActivityWithResultOk()); - } else { - bottomActionBar.showActionsOnly(INFORMATION); - } - bottomActionBar.bindBottomSheetContentWithAction( - new ThemeInfoContent(getContext()), INFORMATION); - bottomActionBar.show(); - } - - private void finishActivityWithResultOk() { - Activity activity = requireActivity(); - activity.overridePendingTransition(R.anim.fade_in, R.anim.fade_out); - Intent intent = new Intent(); - activity.setResult(RESULT_OK, intent); - activity.finish(); - } - - private final class ThemeInfoContent extends BottomSheetContent<ThemeInfoView> { - - private ThemeInfoContent(Context context) { - super(context); - } - - @Override - public int getViewId() { - return R.layout.theme_info_view; - } - - @Override - public void onViewCreated(ThemeInfoView view) { - view.populateThemeInfo(mThemeBundle); - } - } -} diff --git a/src/com/android/customization/picker/theme/ThemeInfoView.java b/src/com/android/customization/picker/theme/ThemeInfoView.java deleted file mode 100644 index e929c4d2..00000000 --- a/src/com/android/customization/picker/theme/ThemeInfoView.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2020 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.theme; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.util.AttributeSet; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.customization.model.theme.ThemeBundle; -import com.android.wallpaper.R; - -/** A view for displaying style info. */ -public class ThemeInfoView extends LinearLayout { - private static final int WIFI_ICON_PREVIEW_INDEX = 0; - private static final int SHAPE_PREVIEW_INDEX = 0; - - private TextView mTitle; - private TextView mFontPreviewTextView; - private ImageView mIconPreviewImageView; - private ImageView mAppPreviewImageView; - private ImageView mShapePreviewImageView; - - public ThemeInfoView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mTitle = findViewById(R.id.style_info_title); - mFontPreviewTextView = findViewById(R.id.font_preview); - mIconPreviewImageView = findViewById(R.id.qs_preview_icon); - mAppPreviewImageView = findViewById(R.id.app_preview_icon); - mShapePreviewImageView = findViewById(R.id.shape_preview_icon); - } - - /** Populates theme info. */ - public void populateThemeInfo(@NonNull ThemeBundle selectedTheme) { - ThemeBundle.PreviewInfo previewInfo = selectedTheme.getPreviewInfo(); - - if (previewInfo != null) { - mTitle.setText(getContext().getString(R.string.style_info_description)); - if (previewInfo.headlineFontFamily != null) { - mTitle.setTypeface(previewInfo.headlineFontFamily); - mFontPreviewTextView.setTypeface(previewInfo.headlineFontFamily); - } - - if (previewInfo.icons.get(WIFI_ICON_PREVIEW_INDEX) != null) { - mIconPreviewImageView.setImageDrawable( - previewInfo.icons.get(WIFI_ICON_PREVIEW_INDEX)); - } - - if (previewInfo.shapeAppIcons.get(SHAPE_PREVIEW_INDEX) != null) { - mAppPreviewImageView.setBackground( - previewInfo.shapeAppIcons.get(SHAPE_PREVIEW_INDEX).getDrawableCopy()); - } - - if (previewInfo.shapeDrawable != null) { - mShapePreviewImageView.setImageDrawable(previewInfo.shapeDrawable); - mShapePreviewImageView.setImageTintList( - ColorStateList.valueOf(previewInfo.resolveAccentColor(getResources()))); - } - } - } -} diff --git a/src/com/android/customization/picker/theme/ThemeOptionPreviewer.java b/src/com/android/customization/picker/theme/ThemeOptionPreviewer.java deleted file mode 100644 index 14b53ec4..00000000 --- a/src/com/android/customization/picker/theme/ThemeOptionPreviewer.java +++ /dev/null @@ -1,405 +0,0 @@ -/* - * Copyright (C) 2020 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.theme; - -import static android.view.View.MeasureSpec.EXACTLY; -import static android.view.View.MeasureSpec.makeMeasureSpec; - -import android.app.WallpaperColors; -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.text.format.DateFormat; -import android.util.DisplayMetrics; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AnimationUtils; -import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.Switch; -import android.widget.TextView; - -import androidx.annotation.MainThread; -import androidx.annotation.Nullable; -import androidx.cardview.widget.CardView; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.OnLifecycleEvent; - -import com.android.customization.model.theme.ThemeBundle; -import com.android.customization.model.theme.ThemeBundle.PreviewInfo; -import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon; -import com.android.wallpaper.R; -import com.android.wallpaper.util.ResourceUtils; -import com.android.wallpaper.util.ScreenSizeCalculator; -import com.android.wallpaper.util.TimeUtils; -import com.android.wallpaper.util.TimeUtils.TimeTicker; - -import java.util.Calendar; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -/** A class to load the {@link ThemeBundle} preview to the view. */ -class ThemeOptionPreviewer implements LifecycleObserver { - private static final String DATE_FORMAT = "EEEE, MMM d"; - - // Maps which icon from ResourceConstants#ICONS_FOR_PREVIEW. - private static final int ICON_WIFI = 0; - private static final int ICON_BLUETOOTH = 1; - private static final int ICON_FLASHLIGHT = 3; - private static final int ICON_AUTO_ROTATE = 4; - private static final int ICON_CELLULAR_SIGNAL = 6; - private static final int ICON_BATTERY = 7; - - // Icons in the top bar (fake "status bar") with the particular order. - private static final int [] sTopBarIconToPreviewIcon = new int [] { - ICON_WIFI, ICON_CELLULAR_SIGNAL, ICON_BATTERY }; - - // Ids of app icon shape preview. - private int[] mShapeAppIconIds = { - R.id.shape_preview_icon_0, R.id.shape_preview_icon_1, - R.id.shape_preview_icon_2, R.id.shape_preview_icon_3 - }; - private int[] mShapeIconAppNameIds = { - R.id.shape_preview_icon_app_name_0, R.id.shape_preview_icon_app_name_1, - R.id.shape_preview_icon_app_name_2, R.id.shape_preview_icon_app_name_3 - }; - - // Ids of color/icons section. - private int[][] mColorTileIconIds = { - new int[] { R.id.preview_color_qs_0_icon, ICON_WIFI}, - new int[] { R.id.preview_color_qs_1_icon, ICON_BLUETOOTH}, - new int[] { R.id.preview_color_qs_2_icon, ICON_FLASHLIGHT}, - new int[] { R.id.preview_color_qs_3_icon, ICON_AUTO_ROTATE}, - }; - private int[] mColorTileIds = { - R.id.preview_color_qs_0_bg, R.id.preview_color_qs_1_bg, - R.id.preview_color_qs_2_bg, R.id.preview_color_qs_3_bg - }; - private int[] mColorButtonIds = { - R.id.preview_check_selected, R.id.preview_radio_selected, R.id.preview_toggle_selected - }; - - private final Context mContext; - - private View mContentView; - private TextView mStatusBarClock; - private TextView mSmartSpaceDate; - private TimeTicker mTicker; - - private boolean mHasPreviewInfoSet; - private boolean mHasWallpaperColorSet; - - ThemeOptionPreviewer(Lifecycle lifecycle, Context context, ViewGroup previewContainer) { - lifecycle.addObserver(this); - - mContext = context; - mContentView = LayoutInflater.from(context).inflate( - R.layout.theme_preview_content, /* root= */ null); - mContentView.setVisibility(View.INVISIBLE); - mStatusBarClock = mContentView.findViewById(R.id.theme_preview_clock); - mSmartSpaceDate = mContentView.findViewById(R.id.smart_space_date); - updateTime(); - final float screenAspectRatio = - ScreenSizeCalculator.getInstance().getScreenAspectRatio(mContext); - Configuration config = mContext.getResources().getConfiguration(); - final boolean directionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; - previewContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { - @Override - public void onLayoutChange(View view, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - // Calculate the full preview card height and width. - final int fullPreviewCardHeight = getFullPreviewCardHeight(); - final int fullPreviewCardWidth = (int) (fullPreviewCardHeight / screenAspectRatio); - - // Relayout the content view to match full preview card size. - mContentView.measure( - makeMeasureSpec(fullPreviewCardWidth, EXACTLY), - makeMeasureSpec(fullPreviewCardHeight, EXACTLY)); - mContentView.layout(0, 0, fullPreviewCardWidth, fullPreviewCardHeight); - - // Scale the content view from full preview size to the container size. For full - // preview, the scale value is 1. - float scale = (float) previewContainer.getMeasuredHeight() / fullPreviewCardHeight; - mContentView.setScaleX(scale); - mContentView.setScaleY(scale); - // The pivot point is centered by default, set to (0, 0). - mContentView.setPivotX(directionLTR ? 0f : mContentView.getMeasuredWidth()); - mContentView.setPivotY(0f); - - // Ensure there will be only one content view in the container. - previewContainer.removeAllViews(); - // Finally, add the content view to the container. - previewContainer.addView( - mContentView, - mContentView.getMeasuredWidth(), - mContentView.getMeasuredHeight()); - - previewContainer.removeOnLayoutChangeListener(this); - } - }); - } - - /** Loads the Theme option preview into the container view. */ - public void setPreviewInfo(PreviewInfo previewInfo) { - setHeadlineFont(previewInfo.headlineFontFamily); - setBodyFont(previewInfo.bodyFontFamily); - setTopBarIcons(previewInfo.icons); - setAppIconShape(previewInfo.shapeAppIcons); - setColorAndIconsSection(previewInfo.icons, previewInfo.shapeDrawable, - previewInfo.resolveAccentColor(mContext.getResources())); - setColorAndIconsBoxRadius(previewInfo.bottomSheeetCornerRadius); - setQsbRadius(previewInfo.bottomSheeetCornerRadius); - mHasPreviewInfoSet = true; - showPreviewIfHasAllConfigSet(); - } - - /** - * Updates the color of widgets in launcher (like top status bar, smart space, and app name - * text) which will change its content color according to different wallpapers. - * - * @param colors the {@link WallpaperColors} of the wallpaper, or {@code null} to use light - * color as default - */ - public void updateColorForLauncherWidgets(@Nullable WallpaperColors colors) { - boolean useLightTextColor = colors == null - || (colors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0; - int textColor = mContext.getColor(useLightTextColor - ? android.R.color.white - : android.R.color.black); - int textShadowColor = mContext.getColor(useLightTextColor - ? android.R.color.tertiary_text_dark - : android.R.color.transparent); - // Update the top status bar clock text color. - mStatusBarClock.setTextColor(textColor); - // Update the top status bar icon color. - ViewGroup iconsContainer = mContentView.findViewById(R.id.theme_preview_top_bar_icons); - for (int i = 0; i < iconsContainer.getChildCount(); i++) { - ((ImageView) iconsContainer.getChildAt(i)) - .setImageTintList(ColorStateList.valueOf(textColor)); - } - // Update smart space date color. - mSmartSpaceDate.setTextColor(textColor); - mSmartSpaceDate.setShadowLayer( - mContext.getResources().getDimension( - R.dimen.smartspace_preview_key_ambient_shadow_blur), - /* dx = */ 0, - /* dy = */ 0, - textShadowColor); - - // Update shape app icon name text color. - for (int id : mShapeIconAppNameIds) { - TextView appName = mContentView.findViewById(id); - appName.setTextColor(textColor); - appName.setShadowLayer( - mContext.getResources().getDimension( - R.dimen.preview_theme_app_name_key_ambient_shadow_blur), - /* dx = */ 0, - /* dy = */ 0, - textShadowColor); - } - - mHasWallpaperColorSet = true; - showPreviewIfHasAllConfigSet(); - } - - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - @MainThread - public void onResume() { - mTicker = TimeTicker.registerNewReceiver(mContext, this::updateTime); - updateTime(); - } - - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) - @MainThread - public void onPause() { - if (mContext != null) { - mContext.unregisterReceiver(mTicker); - } - } - - private void showPreviewIfHasAllConfigSet() { - if (mHasPreviewInfoSet && mHasWallpaperColorSet - && mContentView.getVisibility() != View.VISIBLE) { - mContentView.setAlpha(0f); - mContentView.setVisibility(View.VISIBLE); - mContentView.animate().alpha(1f) - .setStartDelay(50) - .setDuration(200) - .setInterpolator(AnimationUtils.loadInterpolator(mContext, - android.R.interpolator.fast_out_linear_in)) - .start(); - } - } - - private void setHeadlineFont(Typeface headlineFont) { - mStatusBarClock.setTypeface(headlineFont); - mSmartSpaceDate.setTypeface(headlineFont); - - // Update font of color/icons section title. - TextView colorIconsSectionTitle = mContentView.findViewById(R.id.color_icons_section_title); - colorIconsSectionTitle.setTypeface(headlineFont); - } - - private void setBodyFont(Typeface bodyFont) { - // Update font of app names. - for (int id : mShapeIconAppNameIds) { - TextView appName = mContentView.findViewById(id); - appName.setTypeface(bodyFont); - } - } - - private void setTopBarIcons(List<Drawable> icons) { - ViewGroup iconsContainer = mContentView.findViewById(R.id.theme_preview_top_bar_icons); - for (int i = 0; i < iconsContainer.getChildCount(); i++) { - int iconIndex = sTopBarIconToPreviewIcon[i]; - if (iconIndex < icons.size()) { - ((ImageView) iconsContainer.getChildAt(i)) - .setImageDrawable(icons.get(iconIndex).getConstantState() - .newDrawable().mutate()); - } else { - iconsContainer.getChildAt(i).setVisibility(View.GONE); - } - } - } - - private void setAppIconShape(List<ShapeAppIcon> appIcons) { - for (int i = 0; i < mShapeAppIconIds.length && i < mShapeIconAppNameIds.length - && i < appIcons.size(); i++) { - ShapeAppIcon icon = appIcons.get(i); - // Set app icon. - ImageView iconView = mContentView.findViewById(mShapeAppIconIds[i]); - iconView.setBackground(icon.getDrawableCopy()); - // Set app name. - TextView appName = mContentView.findViewById(mShapeIconAppNameIds[i]); - appName.setText(icon.getAppName()); - } - } - - private void setColorAndIconsSection(List<Drawable> icons, Drawable shapeDrawable, - int accentColor) { - // Set QS icons and background. - for (int i = 0; i < mColorTileIconIds.length && i < icons.size(); i++) { - Drawable icon = icons.get(mColorTileIconIds[i][1]).getConstantState() - .newDrawable().mutate(); - Drawable bgShape = shapeDrawable.getConstantState().newDrawable(); - bgShape.setTint(accentColor); - - ImageView bg = mContentView.findViewById(mColorTileIds[i]); - bg.setImageDrawable(bgShape); - ImageView fg = mContentView.findViewById(mColorTileIconIds[i][0]); - fg.setImageDrawable(icon); - } - - // Set color for Buttons (CheckBox, RadioButton, and Switch). - ColorStateList tintList = getColorStateList(accentColor); - for (int mColorButtonId : mColorButtonIds) { - CompoundButton button = mContentView.findViewById(mColorButtonId); - button.setButtonTintList(tintList); - if (button instanceof Switch) { - ((Switch) button).setThumbTintList(tintList); - ((Switch) button).setTrackTintList(tintList); - } - } - } - - private void setColorAndIconsBoxRadius(int cornerRadius) { - ((CardView) mContentView.findViewById(R.id.color_icons_section)).setRadius(cornerRadius); - } - - private void setQsbRadius(int cornerRadius) { - View qsb = mContentView.findViewById(R.id.theme_qsb); - if (qsb != null && qsb.getVisibility() == View.VISIBLE) { - if (qsb.getBackground() instanceof GradientDrawable) { - GradientDrawable bg = (GradientDrawable) qsb.getBackground(); - float radius = useRoundedQSB(cornerRadius) - ? (float) qsb.getLayoutParams().height / 2 : cornerRadius; - bg.setCornerRadii(new float[]{ - radius, radius, radius, radius, - radius, radius, radius, radius}); - } - } - } - - private void updateTime() { - Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); - if (mStatusBarClock != null) { - mStatusBarClock.setText(TimeUtils.getFormattedTime(mContext, calendar)); - } - if (mSmartSpaceDate != null) { - String datePattern = - DateFormat.getBestDateTimePattern(Locale.getDefault(), DATE_FORMAT); - mSmartSpaceDate.setText(DateFormat.format(datePattern, calendar)); - } - } - - private boolean useRoundedQSB(int cornerRadius) { - return cornerRadius >= mContext.getResources().getDimensionPixelSize( - R.dimen.roundCornerThreshold); - } - - private ColorStateList getColorStateList(int accentColor) { - int controlGreyColor = - ResourceUtils.getColorAttr(mContext, android.R.attr.textColorTertiary); - return new ColorStateList( - new int[][]{ - new int[]{android.R.attr.state_selected}, - new int[]{android.R.attr.state_checked}, - new int[]{-android.R.attr.state_enabled}, - }, - new int[] { - accentColor, - accentColor, - controlGreyColor - } - ); - } - - /** - * Gets the screen height which does not include the system status bar and bottom navigation - * bar. - */ - private int getDisplayHeight() { - final DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); - return dm.heightPixels; - } - - // The height of top tool bar (R.layout.section_header). - private int getTopToolBarHeight() { - final TypedValue typedValue = new TypedValue(); - return mContext.getTheme().resolveAttribute( - android.R.attr.actionBarSize, typedValue, true) - ? TypedValue.complexToDimensionPixelSize( - typedValue.data, mContext.getResources().getDisplayMetrics()) - : 0; - } - - private int getFullPreviewCardHeight() { - final Resources res = mContext.getResources(); - return getDisplayHeight() - - getTopToolBarHeight() - - res.getDimensionPixelSize(R.dimen.bottom_actions_height) - - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_top) - - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_bottom); - } -} diff --git a/src/com/android/customization/picker/themedicon/ThemedIconSectionView.java b/src/com/android/customization/picker/themedicon/ThemedIconSectionView.java index 3e03a41c..f83da8c1 100644 --- a/src/com/android/customization/picker/themedicon/ThemedIconSectionView.java +++ b/src/com/android/customization/picker/themedicon/ThemedIconSectionView.java @@ -40,18 +40,16 @@ public class ThemedIconSectionView extends SectionView { protected void onFinishInflate() { super.onFinishInflate(); mSwitchView = findViewById(R.id.themed_icon_toggle); - setOnClickListener(v -> mSwitchView.toggle()); - mSwitchView.setOnCheckedChangeListener((buttonView, isChecked) -> viewActivated(isChecked)); + setOnClickListener(v -> { + mSwitchView.toggle(); + if (mSectionViewListener != null) { + mSectionViewListener.onViewActivated(getContext(), mSwitchView.isChecked()); + } + }); } /** Gets the switch view. */ public Switch getSwitch() { return mSwitchView; } - - private void viewActivated(boolean isChecked) { - if (mSectionViewListener != null) { - mSectionViewListener.onViewActivated(getContext(), isChecked); - } - } } |