diff options
Diffstat (limited to 'src/com/android/customization/picker')
30 files changed, 345 insertions, 389 deletions
diff --git a/src/com/android/customization/picker/CustomizationPickerApplication.java b/src/com/android/customization/picker/CustomizationPickerApplication.java deleted file mode 100644 index 178cfbfc..00000000 --- a/src/com/android/customization/picker/CustomizationPickerApplication.java +++ /dev/null @@ -1,31 +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; - -import android.app.Application; - -import com.android.customization.module.ThemePickerInjector; -import com.android.wallpaper.module.InjectorProvider; - -public class CustomizationPickerApplication extends Application { - @Override - public void onCreate() { - super.onCreate(); - - // Initialize the injector. - InjectorProvider.setInjector(new ThemePickerInjector()); - } -} diff --git a/src/com/android/customization/picker/HorizontalTouchMovementAwareNestedScrollView.kt b/src/com/android/customization/picker/HorizontalTouchMovementAwareNestedScrollView.kt deleted file mode 100644 index 06cf7539..00000000 --- a/src/com/android/customization/picker/HorizontalTouchMovementAwareNestedScrollView.kt +++ /dev/null @@ -1,64 +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 - -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.ViewConfiguration -import androidx.core.widget.NestedScrollView -import kotlin.math.abs - -/** - * This nested scroll view will detect horizontal touch movements and stop vertical scrolls when a - * horizontal touch movement is detected. - */ -class HorizontalTouchMovementAwareNestedScrollView(context: Context, attrs: AttributeSet?) : - NestedScrollView(context, attrs) { - - private var startXPosition = 0f - private var startYPosition = 0f - private var isHorizontalTouchMovement = false - - override fun onInterceptTouchEvent(event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - startXPosition = event.x - startYPosition = event.y - isHorizontalTouchMovement = false - } - MotionEvent.ACTION_MOVE -> { - val xMoveDistance = abs(event.x - startXPosition) - val yMoveDistance = abs(event.y - startYPosition) - if ( - !isHorizontalTouchMovement && - xMoveDistance > yMoveDistance && - xMoveDistance > ViewConfiguration.get(context).scaledTouchSlop - ) { - isHorizontalTouchMovement = true - } - } - else -> {} - } - return if (isHorizontalTouchMovement) { - // We only want to intercept the touch event when the touch moves more vertically than - // horizontally. So we return false. - false - } else { - super.onInterceptTouchEvent(event) - } - } -} diff --git a/src/com/android/customization/picker/WallpaperPreviewer.java b/src/com/android/customization/picker/WallpaperPreviewer.java index 1b9ea9fc..d74bfaea 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); + }, mWallpaperSurface, WallpaperConnection.WHICH_PREVIEW.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 be6c6cbd..004103f3 100644 --- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt +++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest @@ -52,11 +53,12 @@ class ClockPickerRepositoryImpl( override val allClocks: Flow<List<ClockMetadataModel>> = callbackFlow { fun send() { + val activeClockId = registry.activeClockId val allClocks = - registry - .getClocks() - .filter { "NOT_IN_USE" !in it.clockId } - .map { it.toModel() } + registry.getClocks().map { + it.toModel(isSelected = it.clockId == activeClockId) + } + trySend(allClocks) } @@ -83,13 +85,14 @@ class ClockPickerRepositoryImpl( override val selectedClock: Flow<ClockMetadataModel> = callbackFlow { fun send() { - val currentClockId = registry.currentClockId + val activeClockId = registry.activeClockId val metadata = registry.settings?.metadata val model = registry .getClocks() - .find { clockMetadata -> clockMetadata.clockId == currentClockId } + .find { clockMetadata -> clockMetadata.clockId == activeClockId } ?.toModel( + isSelected = true, selectedColorId = metadata?.getSelectedColorId(), colorTone = metadata?.getColorTone() ?: ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS, @@ -146,9 +149,10 @@ class ClockPickerRepositoryImpl( ) .map { setting -> setting == 1 } .map { isDynamic -> if (isDynamic) ClockSize.DYNAMIC else ClockSize.SMALL } + .distinctUntilChanged() .shareIn( scope = scope, - started = SharingStarted.WhileSubscribed(), + started = SharingStarted.Eagerly, replay = 1, ) @@ -176,6 +180,7 @@ class ClockPickerRepositoryImpl( /** By default, [ClockMetadataModel] has no color information unless specified. */ private fun ClockMetadata.toModel( + isSelected: Boolean, selectedColorId: String? = null, @IntRange(from = 0, to = 100) colorTone: Int = 0, @ColorInt seedColor: Int? = null, @@ -183,6 +188,7 @@ class ClockPickerRepositoryImpl( return ClockMetadataModel( clockId = clockId, name = name, + isSelected = isSelected, selectedColorId = selectedColorId, colorToneProgress = colorTone, seedColor = seedColor, diff --git a/src/com/android/customization/picker/clock/data/repository/ClockRegistryProvider.kt b/src/com/android/customization/picker/clock/data/repository/ClockRegistryProvider.kt index 652ffdd2..b197edf9 100644 --- a/src/com/android/customization/picker/clock/data/repository/ClockRegistryProvider.kt +++ b/src/com/android/customization/picker/clock/data/repository/ClockRegistryProvider.kt @@ -29,6 +29,7 @@ import com.android.systemui.shared.plugins.PluginInstance import com.android.systemui.shared.plugins.PluginManagerImpl import com.android.systemui.shared.plugins.PluginPrefs import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager_Factory +import com.android.wallpaper.module.InjectorProvider import java.util.concurrent.Executors import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -55,6 +56,8 @@ class ClockRegistryProvider( DefaultClockProvider(context, LayoutInflater.from(context), context.resources), keepAllLoaded = true, subTag = "Picker", + isTransitClockEnabled = + InjectorProvider.getInjector().getFlags().isTransitClockEnabled(context) ) } 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 bd87ba6e..25225075 100644 --- a/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt +++ b/src/com/android/customization/picker/clock/shared/model/ClockMetadataModel.kt @@ -24,6 +24,7 @@ import androidx.annotation.IntRange data class ClockMetadataModel( val clockId: String, val name: String, + val isSelected: Boolean, val selectedColorId: String?, @IntRange(from = 0, to = 100) val colorToneProgress: Int, @ColorInt val seedColor: 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 89fac894..6bd717b7 100644 --- a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt +++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt @@ -16,8 +16,8 @@ package com.android.customization.picker.clock.ui.binder import android.content.Context -import android.view.ViewGroup -import android.widget.FrameLayout +import android.content.res.Configuration +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -27,7 +27,6 @@ import androidx.lifecycle.repeatOnLifecycle import com.android.customization.picker.clock.ui.view.ClockCarouselView import com.android.customization.picker.clock.ui.view.ClockViewFactory import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel -import com.android.wallpaper.R import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewClickView import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @@ -38,7 +37,6 @@ object ClockCarouselViewBinder { fun bind( context: Context, carouselView: ClockCarouselView, - singleClockView: ViewGroup, screenPreviewClickView: ScreenPreviewClickView, viewModel: ClockCarouselViewModel, clockViewFactory: ClockViewFactory, @@ -46,6 +44,7 @@ object ClockCarouselViewBinder { isTwoPaneAndSmallWidth: Boolean, ) { carouselView.setClockViewFactory(clockViewFactory) + carouselView.isVisible = true clockViewFactory.updateRegionDarkness() val carouselAccessibilityDelegate = CarouselAccessibilityDelegate( @@ -60,13 +59,12 @@ object ClockCarouselViewBinder { } ) screenPreviewClickView.accessibilityDelegate = carouselAccessibilityDelegate + screenPreviewClickView.setOnSideClickedListener { isStart -> + if (isStart) carouselView.scrollToPrevious() else carouselView.scrollToNext() + } - val singleClockHostView = - singleClockView.requireViewById<FrameLayout>(R.id.single_clock_host_view) lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { viewModel.isCarouselVisible.collect { carouselView.isVisible = it } } - launch { combine(viewModel.selectedClockSize, viewModel.allClocks, ::Pair).collect { (size, allClocks) -> @@ -100,17 +98,11 @@ object ClockCarouselViewBinder { } launch { - viewModel.isSingleClockViewVisible.collect { singleClockView.isVisible = it } - } - - launch { - viewModel.clockId.collect { clockId -> - singleClockHostView.removeAllViews() - val clockView = clockViewFactory.getLargeView(clockId) - // The clock view might still be attached to an existing parent. Detach - // before adding to another parent. - (clockView.parent as? ViewGroup)?.removeView(clockView) - singleClockHostView.addView(clockView) + val night = + (context.resources.configuration.uiMode and + Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) + viewModel.getClockCardColorResId(night).collect { + carouselView.setCarouselCardColor(ContextCompat.getColor(context, 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 4f4bd1bb..6e745d54 100644 --- a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt +++ b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView 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 @@ -199,7 +200,7 @@ object ClockSettingsBinder { sizeOptions.radioButtonDynamic.isChecked = false sizeOptions.radioButtonSmall.isChecked = true clockHostView.doOnPreDraw { - it.pivotX = 0F + it.pivotX = ClockCarouselView.getCenteredHostViewPivotX(it) it.pivotY = 0F } } diff --git a/src/com/android/customization/picker/clock/ui/fragment/ClockCustomDemoFragment.kt b/src/com/android/customization/picker/clock/ui/fragment/ClockCustomDemoFragment.kt deleted file mode 100644 index f138d6a4..00000000 --- a/src/com/android/customization/picker/clock/ui/fragment/ClockCustomDemoFragment.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.android.customization.picker.clock.ui.fragment - -import android.content.Context -import android.os.Bundle -import android.util.TypedValue -import android.view.ContextThemeWrapper -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.widget.FrameLayout -import android.widget.TextView -import android.widget.Toast -import androidx.core.view.setPadding -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.android.customization.module.ThemePickerInjector -import com.android.internal.annotations.VisibleForTesting -import com.android.systemui.plugins.ClockMetadata -import com.android.systemui.shared.clocks.ClockRegistry -import com.android.wallpaper.R -import com.android.wallpaper.module.InjectorProvider -import com.android.wallpaper.picker.AppbarFragment - -class ClockCustomDemoFragment : AppbarFragment() { - @VisibleForTesting lateinit var recyclerView: RecyclerView - @VisibleForTesting lateinit var clockRegistry: ClockRegistry - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val view = inflater.inflate(R.layout.fragment_clock_custom_picker_demo, container, false) - setUpToolbar(view) - clockRegistry = - (InjectorProvider.getInjector() as ThemePickerInjector).getClockRegistry( - requireContext(), - ) - val listInUse = clockRegistry.getClocks().filter { "NOT_IN_USE" !in it.clockId } - - recyclerView = view.requireViewById(R.id.clock_preview_card_list_demo) - recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - recyclerView.adapter = - ClockRecyclerAdapter(listInUse, requireContext()) { - clockRegistry.currentClockId = it.clockId - Toast.makeText(context, "${it.name} selected", Toast.LENGTH_SHORT).show() - } - return view - } - - override fun getDefaultTitle(): CharSequence { - return getString(R.string.clock_title) - } - - internal class ClockRecyclerAdapter( - val list: List<ClockMetadata>, - val context: Context, - val onClockSelected: (ClockMetadata) -> Unit - ) : RecyclerView.Adapter<ClockRecyclerAdapter.ViewHolder>() { - class ViewHolder(val view: View, val textView: TextView, val onItemClicked: (Int) -> Unit) : - RecyclerView.ViewHolder(view) { - init { - itemView.setOnClickListener { onItemClicked(absoluteAdapterPosition) } - } - } - - override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { - val rootView = FrameLayout(viewGroup.context) - val textView = - TextView(ContextThemeWrapper(viewGroup.context, R.style.SectionTitleTextStyle)) - textView.setPadding(ITEM_PADDING) - rootView.addView(textView) - val outValue = TypedValue() - context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true) - rootView.setBackgroundResource(outValue.resourceId) - val lp = RecyclerView.LayoutParams(MATCH_PARENT, WRAP_CONTENT) - rootView.layoutParams = lp - return ViewHolder(rootView, textView) { onClockSelected(list[it]) } - } - - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - viewHolder.textView.text = list[position].name - } - - override fun getItemCount() = list.size - - companion object { - val ITEM_PADDING = 40 - } - } -} 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 f4684d88..4805f376 100644 --- a/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt +++ b/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt @@ -20,8 +20,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get +import androidx.transition.Transition +import androidx.transition.doOnStart import com.android.customization.module.ThemePickerInjector import com.android.customization.picker.clock.ui.binder.ClockSettingsBinder import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants @@ -130,6 +134,8 @@ class ClockSettingsFragment : AppbarFragment() { viewLifecycleOwner, ) + (returnTransition as? Transition)?.doOnStart { lockScreenView.isVisible = false } + return view } @@ -140,4 +146,8 @@ class ClockSettingsFragment : AppbarFragment() { override fun getToolbarColorId(): Int { return android.R.color.transparent } + + override fun getToolbarTextColor(): Int { + return ContextCompat.getColor(requireContext(), R.color.system_on_surface) + } } 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 8764e541..d4f501b7 100644 --- a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt +++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt @@ -16,6 +16,7 @@ package com.android.customization.picker.clock.ui.view import android.content.Context +import android.content.res.ColorStateList import android.content.res.Resources import android.util.AttributeSet import android.view.LayoutInflater @@ -71,17 +72,54 @@ class ClockCarouselView( clockViewFactory = factory } + // This function is for the custom accessibility action to trigger a transition to the next + // carousel item. If the current item is the last item in the carousel, the next item + // will be the first item. fun transitionToNext() { - val index = (carousel.currentIndex + 1) % carousel.count - if (index < carousel.count && index > 0) { - carousel.transitionToIndex(index, 0) + if (carousel.count != 0) { + val index = (carousel.currentIndex + 1) % carousel.count + carousel.jumpToIndex(index) + // Explicitly called this since using transitionToIndex(index) leads to + // race-condition between announcement of content description of the correct clock-face + // and the selection of clock face itself + adapter.onNewItem(index) } } + // This function is for the custom accessibility action to trigger a transition to + // the previous carousel item. If the current item is the first item in the carousel, + // the previous item will be the last item. fun transitionToPrevious() { - val index = (carousel.currentIndex - 1) % carousel.count - if (index < carousel.count && index > 0) { - carousel.transitionToIndex(index, 0) + if (carousel.count != 0) { + val index = (carousel.currentIndex + carousel.count - 1) % carousel.count + carousel.jumpToIndex(index) + // Explicitly called this since using transitionToIndex(index) leads to + // race-condition between announcement of content description of the correct clock-face + // and the selection of clock face itself + adapter.onNewItem(index) + } + } + + fun scrollToNext() { + if ( + carousel.count <= 1 || + (!carousel.isInfinite && carousel.currentIndex == carousel.count - 1) + ) { + // No need to scroll if the count is equal or less than 1 + return + } + if (motionLayout.currentState == R.id.start) { + motionLayout.transitionToState(R.id.next, TRANSITION_DURATION) + } + } + + fun scrollToPrevious() { + if (carousel.count <= 1 || (!carousel.isInfinite && carousel.currentIndex == 0)) { + // No need to scroll if the count is equal or less than 1 + return + } + if (motionLayout.currentState == R.id.start) { + motionLayout.transitionToState(R.id.previous, TRANSITION_DURATION) } } @@ -100,8 +138,15 @@ class ClockCarouselView( } adapter = ClockCarouselAdapter(clockSize, clocks, clockViewFactory, onClockSelected) + carousel.isInfinite = clocks.size >= MIN_CLOCKS_TO_ENABLE_INFINITE_CAROUSEL carousel.setAdapter(adapter) - carousel.refresh() + val indexOfSelectedClock = + clocks + .indexOfFirst { it.isSelected } + // If not found, default to the first clock as selected: + .takeIf { it != -1 } + ?: 0 + carousel.jumpToIndex(indexOfSelectedClock) motionLayout.setTransitionListener( object : MotionLayout.TransitionListener { @@ -211,11 +256,13 @@ class ClockCarouselView( } ?: return offCenterClockHostView.doOnPreDraw { - it.pivotX = progress * it.width / 2 + it.pivotX = + progress * it.width / 2 + (1 - progress) * getCenteredHostViewPivotX(it) it.pivotY = progress * it.height / 2 } toCenterClockHostView.doOnPreDraw { - it.pivotX = (1 - progress) * it.width / 2 + it.pivotX = + (1 - progress) * it.width / 2 + progress * getCenteredHostViewPivotX(it) it.pivotY = (1 - progress) * it.height / 2 } offCenterClockFrame.translationX = @@ -265,13 +312,25 @@ class ClockCarouselView( fun setSelectedClockIndex( index: Int, ) { - // jumpToIndex to the same position can cause the views unnecessarily populate again. - // Only call jumpToIndex when the jump-to index is different from the current carousel. - if (index != carousel.currentIndex) { + // 1. setUpClockCarouselView() can possibly not be called before setSelectedClockIndex(). + // We need to check if index out of bound. + // 2. jumpToIndex() to the same position can cause the views unnecessarily populate again. + // We only call jumpToIndex when the index is different from the current carousel. + if (index < carousel.count && index != carousel.currentIndex) { carousel.jumpToIndex(index) } } + fun setCarouselCardColor(color: Int) { + itemViewIds.forEach { id -> + val cardViewId = getClockCardViewId(id) + cardViewId?.let { + val cardView = motionLayout.requireViewById<View>(it) + cardView.backgroundTintList = ColorStateList.valueOf(color) + } + } + } + private fun overrideScreenPreviewWidth() { val overrideWidth = context.resources.getDimensionPixelSize( @@ -417,7 +476,7 @@ class ClockCarouselView( ) { clockHostView.doOnPreDraw { if (isMiddleView) { - it.pivotX = 0F + it.pivotX = getCenteredHostViewPivotX(it) it.pivotY = 0F clockView.translationX = 0F clockView.translationY = 0F @@ -446,7 +505,10 @@ class ClockCarouselView( } companion object { + // The carousel needs to have at least 5 different clock faces to be infinite + const val MIN_CLOCKS_TO_ENABLE_INFINITE_CAROUSEL = 5 const val CLOCK_CAROUSEL_VIEW_SCALE = 0.5f + const val TRANSITION_DURATION = 250 val itemViewIds = listOf( @@ -507,6 +569,10 @@ class ClockCarouselView( return rootViewId == R.id.item_view_2 } + fun getCenteredHostViewPivotX(hostView: View): Float { + return if (hostView.isLayoutRtl) hostView.width.toFloat() else 0F + } + private fun getTranslationDistance( hostLength: Int, frameLength: Int, 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 708fa2f9..98114260 100644 --- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselItemViewModel.kt +++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselItemViewModel.kt @@ -20,15 +20,14 @@ import com.android.customization.module.CustomizationInjector import com.android.wallpaper.R import com.android.wallpaper.module.InjectorProvider -class ClockCarouselItemViewModel(val clockId: String) { +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() - ?.getDescriptionResId(clockId) - ?.let { resources.getString(it) } + ?.getClockDescriptionUtils(resources) + ?.getDescription(clockId) ?: "" return resources.getString(R.string.select_clock_action_description, clockContent) } 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 a4f9cc4a..27c25a20 100644 --- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt +++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt @@ -15,11 +15,13 @@ */ package com.android.customization.picker.clock.ui.viewmodel +import android.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor import com.android.customization.picker.clock.shared.ClockSize +import com.android.wallpaper.R import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -27,7 +29,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest @@ -39,8 +40,7 @@ import kotlinx.coroutines.launch * Clock carousel view model that provides data for the carousel of clock previews. When there is * only one item, we should show a single clock preview instead of a carousel. */ -class ClockCarouselViewModel -constructor( +class ClockCarouselViewModel( private val interactor: ClockPickerInteractor, private val backgroundDispatcher: CoroutineDispatcher, ) : ViewModel() { @@ -50,7 +50,7 @@ constructor( .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) } + allClocks.map { ClockCarouselItemViewModel(it.clockId, it.isSelected) } } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) @@ -58,7 +58,37 @@ constructor( val seedColor: Flow<Int?> = interactor.seedColor - val isCarouselVisible: Flow<Boolean> = allClocks.map { it.size > 1 }.distinctUntilChanged() + fun getClockCardColorResId(isDarkThemeEnabled: Boolean): Flow<Int> { + return interactor.seedColor.map { + it.let { seedColor -> + // if seedColor is null, default clock color is selected + if (seedColor == null) { + if (isDarkThemeEnabled) { + // In dark mode, use darkest surface container color + R.color.system_surface_container_high + } else { + // In light mode, use lightest surface container color + R.color.system_surface_bright + } + } else { + val luminance = Color.luminance(seedColor) + if (isDarkThemeEnabled) { + if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME) { + R.color.system_surface_bright + } else { + R.color.system_surface_container_high + } + } else { + if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME) { + R.color.system_surface_bright + } else { + R.color.system_surface_container_highest + } + } + } + } + } + } @OptIn(ExperimentalCoroutinesApi::class) val selectedIndex: Flow<Int> = @@ -77,15 +107,6 @@ constructor( } .mapNotNull { it } - // Handle the case when there is only one clock in the carousel - val isSingleClockViewVisible: Flow<Boolean> = - allClocks.map { it.size == 1 }.distinctUntilChanged() - - val clockId: Flow<String> = - allClocks - .map { allClockIds -> if (allClockIds.size == 1) allClockIds[0].clockId else null } - .mapNotNull { it } - private var setSelectedClockJob: Job? = null fun setSelectedClock(clockId: String) { setSelectedClockJob?.cancel() @@ -109,5 +130,7 @@ constructor( companion object { const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100 + const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME: Float = 0.85f + const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME: Float = 0.03f } } diff --git a/src/com/android/customization/picker/clock/utils/ClockDescriptionUtils.kt b/src/com/android/customization/picker/clock/utils/ClockDescriptionUtils.kt index 9a0b66f1..28ea4a3f 100644 --- a/src/com/android/customization/picker/clock/utils/ClockDescriptionUtils.kt +++ b/src/com/android/customization/picker/clock/utils/ClockDescriptionUtils.kt @@ -15,14 +15,12 @@ */ package com.android.customization.picker.clock.utils -import androidx.annotation.StringRes - /** Provides clock description for accessibility purposes. */ interface ClockDescriptionUtils { /** - * TODO (b/287507746) : Migrate description res ID to system UI or a shared library, instead of - * preserving the clock description at the Wallpaper Picker side. + * TODO (b/287507746) : Migrate the clock description to system UI or a shared library, instead + * of preserving at the Wallpaper Picker side. */ - @StringRes fun getDescriptionResId(clockId: String): Int + fun getDescription(clockId: String): String } diff --git a/src/com/android/customization/picker/clock/utils/ThemePickerClockDescriptionUtils.kt b/src/com/android/customization/picker/clock/utils/ThemePickerClockDescriptionUtils.kt index 43f19b39..a04ebfff 100644 --- a/src/com/android/customization/picker/clock/utils/ThemePickerClockDescriptionUtils.kt +++ b/src/com/android/customization/picker/clock/utils/ThemePickerClockDescriptionUtils.kt @@ -15,12 +15,8 @@ */ package com.android.customization.picker.clock.utils -import androidx.annotation.StringRes -import com.android.wallpaper.R - class ThemePickerClockDescriptionUtils : ClockDescriptionUtils { - @StringRes - override fun getDescriptionResId(clockId: String): Int { - return R.string.clock_title + override fun getDescription(clockId: String): String { + return "" } } diff --git a/src/com/android/customization/picker/color/ColorSectionView.java b/src/com/android/customization/picker/color/ColorSectionView.java deleted file mode 100644 index b8ba2e4e..00000000 --- a/src/com/android/customization/picker/color/ColorSectionView.java +++ /dev/null @@ -1,33 +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.color; - -import android.content.Context; -import android.util.AttributeSet; - -import androidx.annotation.Nullable; - -import com.android.wallpaper.picker.SectionView; - -/** - * The class inherits from {@link SectionView} as the view representing the color section of the - * customization picker. - */ -public final class ColorSectionView extends SectionView { - public ColorSectionView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } -} diff --git a/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt b/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt index 7cf9fd03..fccaa658 100644 --- a/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt +++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt @@ -19,12 +19,15 @@ package com.android.customization.picker.color.data.repository import com.android.customization.picker.color.shared.model.ColorOptionModel import com.android.customization.picker.color.shared.model.ColorType import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** * Abstracts access to application state related to functionality for selecting, picking, or setting * system color. */ interface ColorPickerRepository { + /** Whether the system color is in the process of being updated */ + val isApplyingSystemColor: StateFlow<Boolean> /** List of wallpaper and preset color options on the device, categorized by Color Type */ val colorOptions: Flow<Map<ColorType, List<ColorOptionModel>>> 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 41ef3a57..6540ce06 100644 --- a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt +++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt @@ -27,7 +27,9 @@ import com.android.systemui.monet.Style import com.android.wallpaper.model.WallpaperColorsModel import com.android.wallpaper.model.WallpaperColorsViewModel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.suspendCancellableCoroutine @@ -43,7 +45,13 @@ class ColorPickerRepositoryImpl( wallpaperColorsViewModel.homeWallpaperColors private val lockWallpaperColors: StateFlow<WallpaperColorsModel?> = wallpaperColorsViewModel.lockWallpaperColors + private var selectedColorOption: MutableStateFlow<ColorOptionModel> = + MutableStateFlow(getCurrentColorOption()) + private val _isApplyingSystemColor = MutableStateFlow(false) + override val isApplyingSystemColor = _isApplyingSystemColor.asStateFlow() + + // TODO (b/299510645): update color options on selected option change after restart is disabled override val colorOptions: Flow<Map<ColorType, List<ColorOptionModel>>> = combine(homeWallpaperColors, lockWallpaperColors) { homeColors, lockColors -> homeColors to lockColors @@ -109,17 +117,21 @@ class ColorPickerRepositoryImpl( } } - override suspend fun select(colorOptionModel: ColorOptionModel) = + override suspend fun select(colorOptionModel: ColorOptionModel) { + _isApplyingSystemColor.value = true suspendCancellableCoroutine { continuation -> colorManager.apply( colorOptionModel.colorOption, object : CustomizationManager.Callback { override fun onSuccess() { + _isApplyingSystemColor.value = false + selectedColorOption.value = colorOptionModel continuation.resumeWith(Result.success(Unit)) } override fun onError(throwable: Throwable?) { Log.w(TAG, "Apply theme with error", throwable) + _isApplyingSystemColor.value = false continuation.resumeWith( Result.failure(throwable ?: Throwable("Error loading theme bundles")) ) @@ -127,6 +139,7 @@ class ColorPickerRepositoryImpl( } ) } + } override fun getCurrentColorOption(): ColorOptionModel { val overlays = colorManager.currentOverlays 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 714129df..bb2ef9d3 100644 --- a/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt +++ b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt @@ -29,6 +29,9 @@ import kotlinx.coroutines.flow.asStateFlow class FakeColorPickerRepository(private val context: Context) : ColorPickerRepository { + private val _isApplyingSystemColor = MutableStateFlow(false) + override val isApplyingSystemColor = _isApplyingSystemColor.asStateFlow() + private lateinit var selectedColorOption: ColorOptionModel private val _colorOptions = diff --git a/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt b/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt index 8c7a4b72..d3b2ebad 100644 --- a/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt +++ b/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt @@ -26,6 +26,8 @@ class ColorPickerInteractor( private val repository: ColorPickerRepository, private val snapshotRestorer: Provider<ColorPickerSnapshotRestorer>, ) { + val isApplyingSystemColor = repository.isApplyingSystemColor + /** * The newly selected color option for overwriting the current active option during an * optimistic update, the value is set to null when update fails 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 cd9dd540..0f82f494 100644 --- a/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt +++ b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt @@ -93,29 +93,21 @@ object ColorPickerBinder { launch { viewModel.colorOptions.collect { colorOptions -> - colorOptionAdapter.setItems(colorOptions) - // the same recycler view is used for different color types tabs - // the scroll state of each tab should be independent of others - if (layoutManagerSavedState != null) { - colorOptionContainerView.post { + // only set or restore instance state on a recycler view once data binding + // is complete to ensure scroll position is reflected correctly + colorOptionAdapter.setItems(colorOptions) { + // the same recycler view is used for different color types tabs + // the scroll state of each tab should be independent of others + if (layoutManagerSavedState != null) { (colorOptionContainerView.layoutManager as LinearLayoutManager) .onRestoreInstanceState(layoutManagerSavedState) layoutManagerSavedState = null + } else { + var indexToFocus = colorOptions.indexOfFirst { it.isSelected.value } + indexToFocus = if (indexToFocus < 0) 0 else indexToFocus + (colorOptionContainerView.layoutManager as LinearLayoutManager) + .scrollToPositionWithOffset(indexToFocus, 0) } - } else { - var indexToFocus = colorOptions.indexOfFirst { it.isSelected.value } - indexToFocus = if (indexToFocus < 0) 0 else indexToFocus - val linearLayoutManager = - object : LinearLayoutManager(view.context, HORIZONTAL, false) { - override fun onLayoutCompleted(state: RecyclerView.State?) { - super.onLayoutCompleted(state) - // scrollToPosition seems to be inconsistently moving - // selected - // color to different positions - scrollToPositionWithOffset(indexToFocus, 0) - } - } - colorOptionContainerView.layoutManager = linearLayoutManager } } } @@ -123,9 +115,13 @@ object ColorPickerBinder { } return object : Binding { override fun saveInstanceState(savedState: Bundle) { + // as a workaround for the picker restarting twice after a config change, if the + // picker restarts before the saved state was applied and set to null, + // re-use the same saved state savedState.putParcelable( LAYOUT_MANAGER_SAVED_STATE, - colorOptionContainerView.layoutManager?.onSaveInstanceState() + layoutManagerSavedState + ?: colorOptionContainerView.layoutManager?.onSaveInstanceState() ) } diff --git a/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt index 2daefe47..ad816143 100644 --- a/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt +++ b/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt @@ -112,22 +112,20 @@ object ColorSectionViewBinder { val optionSelectedView = itemView.requireViewById<ImageView>(R.id.option_selected) lifecycleOwner.lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - item.isSelected.collect { isSelected -> - optionSelectedView.isVisible = isSelected - } + launch { + item.isSelected.collect { isSelected -> + optionSelectedView.isVisible = isSelected } - launch { - item.onClicked.collect { onClicked -> - itemView.setOnClickListener( - if (onClicked != null) { - View.OnClickListener { onClicked.invoke() } - } else { - null - } - ) - } + } + launch { + item.onClicked.collect { onClicked -> + itemView.setOnClickListener( + if (onClicked != null) { + View.OnClickListener { onClicked.invoke() } + } else { + null + } + ) } } } 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 78bfa43e..4ef29d6e 100644 --- a/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt +++ b/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt @@ -22,9 +22,13 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get import androidx.lifecycle.lifecycleScope +import androidx.transition.Transition +import androidx.transition.doOnStart import com.android.customization.model.mode.DarkModeSectionController import com.android.customization.module.ThemePickerInjector import com.android.customization.picker.color.ui.binder.ColorPickerBinder @@ -92,53 +96,58 @@ class ColorPickerFragment : AppbarFragment() { savedInstanceState?.let { binding?.restoreInstanceState(it) } - ScreenPreviewBinder.bind( - activity = requireActivity(), - previewView = lockScreenView, - viewModel = - ScreenPreviewViewModel( - previewUtils = - PreviewUtils( - context = requireContext(), - authority = - requireContext() - .getString( - R.string.lock_screen_preview_provider_authority, - ), - ), - wallpaperInfoProvider = { forceReload -> - suspendCancellableCoroutine { continuation -> - wallpaperInfoFactory.createCurrentWallpaperInfos( - { homeWallpaper, lockWallpaper, _ -> - lifecycleScope.launch { - if ( - wcViewModel.lockWallpaperColors.value - is WallpaperColorsModel.Loading - ) { - loadInitialColors( - wallpaperManager, - wcViewModel, - CustomizationSections.Screen.LOCK_SCREEN - ) + val lockScreenPreviewBinder = + ScreenPreviewBinder.bind( + activity = requireActivity(), + previewView = lockScreenView, + viewModel = + ScreenPreviewViewModel( + previewUtils = + PreviewUtils( + context = requireContext(), + authority = + requireContext() + .getString( + R.string.lock_screen_preview_provider_authority, + ), + ), + 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) - }, - forceReload, - ) - } - }, - onWallpaperColorChanged = { colors -> - wcViewModel.setLockWallpaperColors(colors) - }, - wallpaperInteractor = injector.getWallpaperInteractor(requireContext()), - screen = CustomizationSections.Screen.LOCK_SCREEN, - ), - lifecycleOwner = this, - offsetToStart = - displayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(requireActivity()), - onWallpaperPreviewDirty = { activity?.recreate() }, - ) + continuation.resume(lockWallpaper ?: homeWallpaper, null) + }, + forceReload, + ) + } + }, + onWallpaperColorChanged = { colors -> + wcViewModel.setLockWallpaperColors(colors) + }, + wallpaperInteractor = injector.getWallpaperInteractor(requireContext()), + screen = CustomizationSections.Screen.LOCK_SCREEN, + ), + lifecycleOwner = this, + offsetToStart = + displayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(requireActivity()), + onWallpaperPreviewDirty = { activity?.recreate() }, + ) + val shouldMirrorHomePreview = + wallpaperManager.getWallpaperInfo(WallpaperManager.FLAG_SYSTEM) != null && + wallpaperManager.getWallpaperId(WallpaperManager.FLAG_LOCK) < 0 + val mirrorSurface = if (shouldMirrorHomePreview) lockScreenPreviewBinder.surface() else null ScreenPreviewBinder.bind( activity = requireActivity(), previewView = homeScreenView, @@ -185,6 +194,7 @@ class ColorPickerFragment : AppbarFragment() { offsetToStart = displayUtils.isSingleDisplayOrUnfoldedHorizontalHinge(requireActivity()), onWallpaperPreviewDirty = { activity?.recreate() }, + mirrorSurface = mirrorSurface, ) val darkModeToggleContainerView: FrameLayout = view.requireViewById(R.id.dark_mode_toggle_container) @@ -197,6 +207,12 @@ class ColorPickerFragment : AppbarFragment() { .createView(requireContext()) darkModeSectionView.background = null darkModeToggleContainerView.addView(darkModeSectionView) + + (returnTransition as? Transition)?.doOnStart { + lockScreenView.isVisible = false + homeScreenView.isVisible = false + } + return view } @@ -236,4 +252,8 @@ class ColorPickerFragment : AppbarFragment() { override fun getToolbarColorId(): Int { return android.R.color.transparent } + + override fun getToolbarTextColor(): Int { + return ContextCompat.getColor(requireContext(), R.color.system_on_surface) + } } 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 a828f837..71dfe1da 100644 --- a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt +++ b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt @@ -23,7 +23,6 @@ import android.graphics.Rect import android.view.TouchDelegate import android.view.View import android.view.View.OnAttachStateChangeListener -import android.view.ViewGroup import android.view.ViewStub import androidx.activity.ComponentActivity import androidx.constraintlayout.helper.widget.Carousel @@ -39,7 +38,9 @@ import com.android.customization.picker.clock.ui.fragment.ClockSettingsFragment import com.android.customization.picker.clock.ui.view.ClockCarouselView import com.android.customization.picker.clock.ui.view.ClockViewFactory import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel +import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor 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 @@ -49,6 +50,7 @@ import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInt import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewClickView import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewSectionController import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewView +import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel import com.android.wallpaper.util.DisplayUtils import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -70,8 +72,10 @@ class PreviewWithClockCarouselSectionController( private val navigationController: CustomizationSectionNavigationController, wallpaperInteractor: WallpaperInteractor, themedIconInteractor: ThemedIconInteractor, + colorPickerInteractor: ColorPickerInteractor, wallpaperManager: WallpaperManager, private val isTwoPaneAndSmallWidth: Boolean, + customizationPickerViewModel: CustomizationPickerViewModel, ) : PreviewWithThemeSectionController( activity, @@ -83,8 +87,10 @@ class PreviewWithClockCarouselSectionController( wallpaperPreviewNavigator, wallpaperInteractor, themedIconInteractor, + colorPickerInteractor, wallpaperManager, isTwoPaneAndSmallWidth, + customizationPickerViewModel, ) { private val viewModel = @@ -98,8 +104,11 @@ class PreviewWithClockCarouselSectionController( override val hideLockScreenClockPreview = true - override fun createView(context: Context): ScreenPreviewView { - val view = super.createView(context) + override fun createView( + context: Context, + params: CustomizationSectionController.ViewCreationParams, + ): ScreenPreviewView { + val view = super.createView(context, params) if (screen == CustomizationSections.Screen.LOCK_SCREEN) { val screenPreviewClickView: ScreenPreviewClickView = view.requireViewById(R.id.screen_preview_click_view) @@ -146,12 +155,6 @@ class PreviewWithClockCarouselSectionController( guidelineEnd.layoutParams = layoutParams } - // TODO (b/270716937) We should handle the single clock case in the clock carousel - // itself - val singleClockViewStub: ViewStub = view.requireViewById(R.id.single_clock_view_stub) - singleClockViewStub.layoutResource = R.layout.single_clock_view - val singleClockView = singleClockViewStub.inflate() as ViewGroup - /** * Only bind after [Carousel.onAttachedToWindow]. This is to avoid the race condition * that the flow emits before attached to window where [Carousel.mMotionLayout] is still @@ -167,7 +170,6 @@ class PreviewWithClockCarouselSectionController( ClockCarouselViewBinder.bind( context = context, carouselView = carouselView, - singleClockView = singleClockView, screenPreviewClickView = screenPreviewClickView, viewModel = viewModel, clockViewFactory = clockViewFactory, 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 56c6c30a..c4d6be45 100644 --- a/src/com/android/customization/picker/preview/ui/section/PreviewWithThemeSectionController.kt +++ b/src/com/android/customization/picker/preview/ui/section/PreviewWithThemeSectionController.kt @@ -22,6 +22,7 @@ import android.app.WallpaperManager import android.content.Context import androidx.lifecycle.LifecycleOwner import com.android.customization.model.themedicon.domain.interactor.ThemedIconInteractor +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 @@ -30,6 +31,7 @@ import com.android.wallpaper.module.CurrentWallpaperInfoFactory import com.android.wallpaper.module.CustomizationSections 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 @@ -49,8 +51,10 @@ open class PreviewWithThemeSectionController( wallpaperPreviewNavigator: WallpaperPreviewNavigator, private val wallpaperInteractor: WallpaperInteractor, private val themedIconInteractor: ThemedIconInteractor, + private val colorPickerInteractor: ColorPickerInteractor, wallpaperManager: WallpaperManager, isTwoPaneAndSmallWidth: Boolean, + customizationPickerViewModel: CustomizationPickerViewModel, ) : ScreenPreviewSectionController( activity, @@ -62,7 +66,8 @@ open class PreviewWithThemeSectionController( wallpaperPreviewNavigator, wallpaperInteractor, wallpaperManager, - isTwoPaneAndSmallWidth + isTwoPaneAndSmallWidth, + customizationPickerViewModel, ) { override fun createScreenPreviewViewModel(context: Context): ScreenPreviewViewModel { return PreviewWithThemeViewModel( @@ -114,6 +119,7 @@ open class PreviewWithThemeSectionController( initialExtrasProvider = { getInitialExtras(isOnLockScreen) }, wallpaperInteractor = wallpaperInteractor, themedIconInteractor = themedIconInteractor, + colorPickerInteractor = colorPickerInteractor, screen = screen, ) } diff --git a/src/com/android/customization/picker/preview/ui/viewmodel/PreviewWithThemeViewModel.kt b/src/com/android/customization/picker/preview/ui/viewmodel/PreviewWithThemeViewModel.kt index 435878dc..83f986da 100644 --- a/src/com/android/customization/picker/preview/ui/viewmodel/PreviewWithThemeViewModel.kt +++ b/src/com/android/customization/picker/preview/ui/viewmodel/PreviewWithThemeViewModel.kt @@ -20,12 +20,14 @@ package com.android.customization.picker.preview.ui.viewmodel import android.app.WallpaperColors import android.os.Bundle import com.android.customization.model.themedicon.domain.interactor.ThemedIconInteractor +import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor import com.android.wallpaper.model.WallpaperInfo import com.android.wallpaper.module.CustomizationSections import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel import com.android.wallpaper.util.PreviewUtils import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine /** A ThemePicker version of the [ScreenPreviewViewModel] */ class PreviewWithThemeViewModel( @@ -35,6 +37,7 @@ class PreviewWithThemeViewModel( onWallpaperColorChanged: (WallpaperColors?) -> Unit = {}, wallpaperInteractor: WallpaperInteractor, private val themedIconInteractor: ThemedIconInteractor? = null, + colorPickerInteractor: ColorPickerInteractor? = null, screen: CustomizationSections.Screen, ) : ScreenPreviewViewModel( @@ -46,4 +49,16 @@ class PreviewWithThemeViewModel( screen, ) { override fun workspaceUpdateEvents(): Flow<Boolean>? = themedIconInteractor?.isActivated + + private val wallpaperIsLoading = super.isLoading + + override val isLoading: Flow<Boolean> = + colorPickerInteractor?.let { + combine(wallpaperIsLoading, colorPickerInteractor.isApplyingSystemColor) { + wallpaperIsLoading, + colorIsLoading -> + wallpaperIsLoading || colorIsLoading + } + } + ?: wallpaperIsLoading } 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 6879ffc8..8891b03f 100644 --- a/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt +++ b/src/com/android/customization/picker/quickaffordance/ui/adapter/SlotTabAdapter.kt @@ -62,6 +62,12 @@ class SlotTabAdapter : RecyclerView.Adapter<SlotTabAdapter.ViewHolder>() { null } ) + val stateDescription = + item.selectedQuickAffordances + .find { it.isSelected.value } + ?.text + ?.asString(holder.itemView.context) + stateDescription?.let { holder.itemView.stateDescription = it } } 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 68367c8b..091f484e 100644 --- a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt +++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt @@ -37,6 +37,7 @@ import com.android.wallpaper.picker.common.icon.ui.viewbinder.IconViewBinder import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collectIndexed import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -99,13 +100,18 @@ object KeyguardQuickAffordancePickerBinder { selectedFlags.indexOfFirst { it } } } - .collect { selectedPosition -> + .collectIndexed { index, selectedPosition -> // Scroll the view to show the first selected affordance. if (selectedPosition != -1) { // We use "post" because we need to give the adapter item a pass to // update the view. affordancesView.post { - affordancesView.smoothScrollToPosition(selectedPosition) + if (index == 0) { + // don't animate on initial collection + affordancesView.scrollToPosition(selectedPosition) + } else { + affordancesView.smoothScrollToPosition(selectedPosition) + } } } } diff --git a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordanceSectionViewBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordanceSectionViewBinder.kt index 28ad51ac..7e1f4d3c 100644 --- a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordanceSectionViewBinder.kt +++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordanceSectionViewBinder.kt @@ -48,7 +48,7 @@ object KeyguardQuickAffordanceSectionViewBinder { lifecycleOwner.lifecycleScope.launch { viewModel.summary - .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.RESUMED) + .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) .collectLatest { summary -> TextViewBinder.bind( view = descriptionView, diff --git a/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt b/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt index d5f0d33d..467e5a07 100644 --- a/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt +++ b/src/com/android/customization/picker/quickaffordance/ui/fragment/KeyguardQuickAffordancePickerFragment.kt @@ -21,8 +21,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get +import androidx.transition.Transition +import androidx.transition.doOnStart import com.android.customization.module.ThemePickerInjector import com.android.customization.picker.quickaffordance.ui.binder.KeyguardQuickAffordancePickerBinder import com.android.customization.picker.quickaffordance.ui.binder.KeyguardQuickAffordancePreviewBinder @@ -30,9 +34,7 @@ import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQui import com.android.wallpaper.R import com.android.wallpaper.module.InjectorProvider import com.android.wallpaper.picker.AppbarFragment -import kotlinx.coroutines.ExperimentalCoroutinesApi -@OptIn(ExperimentalCoroutinesApi::class) class KeyguardQuickAffordancePickerFragment : AppbarFragment() { companion object { const val DESTINATION_ID = "quick_affordances" @@ -77,6 +79,12 @@ class KeyguardQuickAffordancePickerFragment : AppbarFragment() { viewModel = viewModel, lifecycleOwner = this, ) + postponeEnterTransition() + view.post { startPostponedEnterTransition() } + (returnTransition as? Transition)?.doOnStart { + // Hide preview during exit transition animation + view?.findViewById<View>(R.id.preview)?.isVisible = false + } return view } @@ -87,4 +95,8 @@ class KeyguardQuickAffordancePickerFragment : AppbarFragment() { override fun getToolbarColorId(): Int { return android.R.color.transparent } + + override fun getToolbarTextColor(): Int { + return ContextCompat.getColor(requireContext(), R.color.system_on_surface) + } } |