1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.customization.picker.preview.ui.section 19 20 import android.app.WallpaperManager 21 import android.content.Context 22 import android.graphics.Rect 23 import android.view.TouchDelegate 24 import android.view.View 25 import android.view.View.OnAttachStateChangeListener 26 import android.view.ViewStub 27 import androidx.activity.ComponentActivity 28 import androidx.constraintlayout.helper.widget.Carousel 29 import androidx.constraintlayout.widget.ConstraintLayout 30 import androidx.constraintlayout.widget.Guideline 31 import androidx.lifecycle.LifecycleOwner 32 import androidx.lifecycle.ViewModelProvider 33 import androidx.lifecycle.get 34 import androidx.lifecycle.lifecycleScope 35 import com.android.customization.model.themedicon.domain.interactor.ThemedIconInteractor 36 import com.android.customization.picker.clock.ui.binder.ClockCarouselViewBinder 37 import com.android.customization.picker.clock.ui.fragment.ClockSettingsFragment 38 import com.android.customization.picker.clock.ui.view.ClockCarouselView 39 import com.android.customization.picker.clock.ui.view.ClockViewFactory 40 import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel 41 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor 42 import com.android.wallpaper.R 43 import com.android.wallpaper.model.CustomizationSectionController 44 import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController 45 import com.android.wallpaper.model.WallpaperColorsViewModel 46 import com.android.wallpaper.model.WallpaperPreviewNavigator 47 import com.android.wallpaper.module.CurrentWallpaperInfoFactory 48 import com.android.wallpaper.module.CustomizationSections 49 import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor 50 import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewClickView 51 import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewSectionController 52 import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewView 53 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel 54 import com.android.wallpaper.util.DisplayUtils 55 import kotlinx.coroutines.Job 56 import kotlinx.coroutines.launch 57 58 /** 59 * A ThemePicker version of the [ScreenPreviewSectionController] that adjusts the preview for the 60 * clock carousel, and also updates the preview on theme changes. 61 */ 62 class PreviewWithClockCarouselSectionController( 63 activity: ComponentActivity, 64 private val lifecycleOwner: LifecycleOwner, 65 private val screen: CustomizationSections.Screen, 66 wallpaperInfoFactory: CurrentWallpaperInfoFactory, 67 colorViewModel: WallpaperColorsViewModel, 68 displayUtils: DisplayUtils, 69 clockCarouselViewModelFactory: ClockCarouselViewModel.Factory, 70 private val clockViewFactory: ClockViewFactory, 71 wallpaperPreviewNavigator: WallpaperPreviewNavigator, 72 private val navigationController: CustomizationSectionNavigationController, 73 wallpaperInteractor: WallpaperInteractor, 74 themedIconInteractor: ThemedIconInteractor, 75 colorPickerInteractor: ColorPickerInteractor, 76 wallpaperManager: WallpaperManager, 77 private val isTwoPaneAndSmallWidth: Boolean, 78 customizationPickerViewModel: CustomizationPickerViewModel, 79 ) : 80 PreviewWithThemeSectionController( 81 activity, 82 lifecycleOwner, 83 screen, 84 wallpaperInfoFactory, 85 colorViewModel, 86 displayUtils, 87 wallpaperPreviewNavigator, 88 wallpaperInteractor, 89 themedIconInteractor, 90 colorPickerInteractor, 91 wallpaperManager, 92 isTwoPaneAndSmallWidth, 93 customizationPickerViewModel, 94 ) { 95 96 private val viewModel = 97 ViewModelProvider( 98 activity, 99 clockCarouselViewModelFactory, 100 ) 101 .get() as ClockCarouselViewModel 102 103 private var clockColorAndSizeButton: View? = null 104 105 override val hideLockScreenClockPreview = true 106 createViewnull107 override fun createView( 108 context: Context, 109 params: CustomizationSectionController.ViewCreationParams, 110 ): ScreenPreviewView { 111 val view = super.createView(context, params) 112 if (screen == CustomizationSections.Screen.LOCK_SCREEN) { 113 val screenPreviewClickView: ScreenPreviewClickView = 114 view.findViewById(R.id.screen_preview_click_view) 115 val clockColorAndSizeButtonStub: ViewStub = 116 view.requireViewById(R.id.clock_color_and_size_button) 117 clockColorAndSizeButtonStub.layoutResource = R.layout.clock_color_and_size_button 118 clockColorAndSizeButton = clockColorAndSizeButtonStub.inflate() as View 119 clockColorAndSizeButton?.setOnClickListener { 120 navigationController.navigateTo(ClockSettingsFragment()) 121 } 122 // clockColorAndSizeButton's touch target has to be increased programmatically 123 // rather than with padding because this button only appears in the lock screen tab. 124 view.post { 125 val rect = Rect() 126 clockColorAndSizeButton?.getHitRect(rect) 127 val padding = 128 context 129 .getResources() 130 .getDimensionPixelSize(R.dimen.screen_preview_section_vertical_space) 131 rect.top -= padding 132 rect.bottom += padding 133 val touchDelegate = TouchDelegate(rect, clockColorAndSizeButton) 134 view.setTouchDelegate(touchDelegate) 135 } 136 137 val carouselViewStub: ViewStub = view.requireViewById(R.id.clock_carousel_view_stub) 138 carouselViewStub.layoutResource = R.layout.clock_carousel_view 139 val carouselView = carouselViewStub.inflate() as ClockCarouselView 140 141 if (isTwoPaneAndSmallWidth) { 142 val guidelineMargin = 143 context.resources.getDimensionPixelSize( 144 R.dimen.clock_carousel_guideline_margin_for_2_pane_small_width 145 ) 146 147 val guidelineStart = carouselView.requireViewById<Guideline>(R.id.guideline_start) 148 var layoutParams = guidelineStart.layoutParams as ConstraintLayout.LayoutParams 149 layoutParams.guideBegin = guidelineMargin 150 guidelineStart.layoutParams = layoutParams 151 152 val guidelineEnd = carouselView.requireViewById<Guideline>(R.id.guideline_end) 153 layoutParams = guidelineEnd.layoutParams as ConstraintLayout.LayoutParams 154 layoutParams.guideEnd = guidelineMargin 155 guidelineEnd.layoutParams = layoutParams 156 } 157 158 /** 159 * Only bind after [Carousel.onAttachedToWindow]. This is to avoid the race condition 160 * that the flow emits before attached to window where [Carousel.mMotionLayout] is still 161 * null. 162 */ 163 var onAttachStateChangeListener: OnAttachStateChangeListener? = null 164 var bindJob: Job? = null 165 onAttachStateChangeListener = 166 object : OnAttachStateChangeListener { 167 override fun onViewAttachedToWindow(view: View?) { 168 bindJob = 169 lifecycleOwner.lifecycleScope.launch { 170 ClockCarouselViewBinder.bind( 171 context = context, 172 carouselView = carouselView, 173 screenPreviewClickView = screenPreviewClickView, 174 viewModel = viewModel, 175 clockViewFactory = clockViewFactory, 176 lifecycleOwner = lifecycleOwner, 177 isTwoPaneAndSmallWidth = isTwoPaneAndSmallWidth, 178 ) 179 if (onAttachStateChangeListener != null) { 180 carouselView.carousel.removeOnAttachStateChangeListener( 181 onAttachStateChangeListener, 182 ) 183 } 184 } 185 } 186 187 override fun onViewDetachedFromWindow(view: View?) { 188 bindJob?.cancel() 189 } 190 } 191 carouselView.carousel.addOnAttachStateChangeListener(onAttachStateChangeListener) 192 } 193 194 return view 195 } 196 } 197