1 /* <lambda>null2 * Copyright (C) 2024 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 package com.android.wallpaper.customization.ui.binder 18 19 import android.app.Activity 20 import android.content.Context 21 import android.view.View 22 import android.view.ViewGroup 23 import android.widget.ImageView 24 import android.widget.TextView 25 import androidx.constraintlayout.widget.ConstraintSet 26 import androidx.core.graphics.drawable.DrawableCompat 27 import androidx.core.view.isVisible 28 import androidx.lifecycle.Lifecycle 29 import androidx.lifecycle.LifecycleOwner 30 import androidx.lifecycle.lifecycleScope 31 import androidx.lifecycle.repeatOnLifecycle 32 import com.android.customization.model.color.ColorOptionImpl 33 import com.android.customization.picker.clock.shared.ClockSize 34 import com.android.customization.picker.clock.ui.view.ClockConstraintLayoutHostView 35 import com.android.customization.picker.clock.ui.view.ClockConstraintLayoutHostView.Companion.addClockViews 36 import com.android.customization.picker.clock.ui.view.ClockViewFactory 37 import com.android.customization.picker.color.ui.binder.ColorOptionIconBinder2 38 import com.android.customization.picker.color.ui.view.ColorOptionIconView2 39 import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel 40 import com.android.customization.picker.settings.ui.binder.ColorContrastSectionViewBinder2 41 import com.android.systemui.plugins.clocks.ClockAxisStyle 42 import com.android.systemui.plugins.clocks.ClockPreviewConfig 43 import com.android.systemui.shared.Flags 44 import com.android.themepicker.R 45 import com.android.wallpaper.config.BaseFlags 46 import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption 47 import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerLockCustomizationOption 48 import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel 49 import com.android.wallpaper.picker.common.icon.ui.viewbinder.IconViewBinder 50 import com.android.wallpaper.picker.common.text.ui.viewbinder.TextViewBinder 51 import com.android.wallpaper.picker.customization.ui.binder.ColorUpdateBinder 52 import com.android.wallpaper.picker.customization.ui.binder.CustomizationOptionsBinder 53 import com.android.wallpaper.picker.customization.ui.binder.DefaultCustomizationOptionsBinder 54 import com.android.wallpaper.picker.customization.ui.util.CustomizationOptionUtil.CustomizationOption 55 import com.android.wallpaper.picker.customization.ui.util.ViewAlphaAnimator.animateToAlpha 56 import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel 57 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationOptionsViewModel 58 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel2 59 import com.google.android.material.materialswitch.MaterialSwitch 60 import javax.inject.Inject 61 import javax.inject.Singleton 62 import kotlinx.coroutines.Dispatchers 63 import kotlinx.coroutines.flow.collectLatest 64 import kotlinx.coroutines.flow.combine 65 import kotlinx.coroutines.launch 66 67 @Singleton 68 class ThemePickerCustomizationOptionsBinder 69 @Inject 70 constructor(private val defaultCustomizationOptionsBinder: DefaultCustomizationOptionsBinder) : 71 CustomizationOptionsBinder { 72 73 override fun bind( 74 view: View, 75 lockScreenCustomizationOptionEntries: List<Pair<CustomizationOption, View>>, 76 homeScreenCustomizationOptionEntries: List<Pair<CustomizationOption, View>>, 77 customizationOptionFloatingSheetViewMap: Map<CustomizationOption, View>?, 78 viewModel: CustomizationPickerViewModel2, 79 colorUpdateViewModel: ColorUpdateViewModel, 80 lifecycleOwner: LifecycleOwner, 81 navigateToMoreLockScreenSettingsActivity: () -> Unit, 82 navigateToColorContrastSettingsActivity: () -> Unit, 83 navigateToLockScreenNotificationsSettingsActivity: () -> Unit, 84 navigateToPackThemeActivity: () -> Unit, 85 ) { 86 defaultCustomizationOptionsBinder.bind( 87 view, 88 lockScreenCustomizationOptionEntries, 89 homeScreenCustomizationOptionEntries, 90 customizationOptionFloatingSheetViewMap, 91 viewModel, 92 colorUpdateViewModel, 93 lifecycleOwner, 94 navigateToMoreLockScreenSettingsActivity, 95 navigateToColorContrastSettingsActivity, 96 navigateToLockScreenNotificationsSettingsActivity, 97 navigateToPackThemeActivity, 98 ) 99 100 val isComposeRefactorEnabled = BaseFlags.get().isComposeRefactorEnabled() 101 102 val optionsViewModel = 103 viewModel.customizationOptionsViewModel as ThemePickerCustomizationOptionsViewModel 104 105 val isOnMainScreen = { optionsViewModel.selectedOption.value == null } 106 107 val allCustomizationOptionEntries = 108 lockScreenCustomizationOptionEntries + homeScreenCustomizationOptionEntries 109 allCustomizationOptionEntries.forEach { (_, view) -> 110 ColorUpdateBinder.bind( 111 setColor = { color -> 112 DrawableCompat.setTint(DrawableCompat.wrap(view.background), color) 113 }, 114 color = colorUpdateViewModel.colorSurfaceBright, 115 shouldAnimate = isOnMainScreen, 116 lifecycleOwner = lifecycleOwner, 117 ) 118 ColorUpdateBinder.bind( 119 setColor = { color -> 120 view 121 .findViewById<ViewGroup>(R.id.option_entry_icon_container) 122 ?.background 123 ?.let { DrawableCompat.setTint(DrawableCompat.wrap(it), color) } 124 }, 125 color = colorUpdateViewModel.colorSurfaceContainerHigh, 126 shouldAnimate = isOnMainScreen, 127 lifecycleOwner = lifecycleOwner, 128 ) 129 ColorUpdateBinder.bind( 130 setColor = { color -> 131 view.findViewById<TextView>(R.id.option_entry_title)?.setTextColor(color) 132 }, 133 color = colorUpdateViewModel.colorOnSurface, 134 shouldAnimate = isOnMainScreen, 135 lifecycleOwner = lifecycleOwner, 136 ) 137 ColorUpdateBinder.bind( 138 setColor = { color -> 139 view.findViewById<TextView>(R.id.option_entry_description)?.setTextColor(color) 140 }, 141 color = colorUpdateViewModel.colorOnSurfaceVariant, 142 shouldAnimate = isOnMainScreen, 143 lifecycleOwner = lifecycleOwner, 144 ) 145 } 146 147 val optionClock: View = 148 lockScreenCustomizationOptionEntries 149 .first { it.first == ThemePickerLockCustomizationOption.CLOCK } 150 .second 151 val optionClockIcon: ImageView = optionClock.requireViewById(R.id.option_entry_icon) 152 153 val optionShortcut: View = 154 lockScreenCustomizationOptionEntries 155 .first { it.first == ThemePickerLockCustomizationOption.SHORTCUTS } 156 .second 157 val optionShortcutDescription: TextView = 158 optionShortcut.requireViewById(R.id.option_entry_description) 159 val optionShortcutIcon1: ImageView = 160 optionShortcut.requireViewById(R.id.option_entry_icon_1) 161 val optionShortcutIcon2: ImageView = 162 optionShortcut.requireViewById(R.id.option_entry_icon_2) 163 164 val optionLockScreenNotificationsSettings: View = 165 lockScreenCustomizationOptionEntries 166 .first { it.first == ThemePickerLockCustomizationOption.LOCK_SCREEN_NOTIFICATIONS } 167 .second 168 optionLockScreenNotificationsSettings.setOnClickListener { 169 navigateToLockScreenNotificationsSettingsActivity.invoke() 170 } 171 172 val optionMoreLockScreenSettings: View = 173 lockScreenCustomizationOptionEntries 174 .first { it.first == ThemePickerLockCustomizationOption.MORE_LOCK_SCREEN_SETTINGS } 175 .second 176 optionMoreLockScreenSettings.setOnClickListener { 177 navigateToMoreLockScreenSettingsActivity.invoke() 178 } 179 180 var optionPackThemeIconHome: ImageView? = null 181 var optionPackThemeIconLock: ImageView? = null 182 183 if (BaseFlags.get().isPackThemeEnabled()) { 184 val optionPackThemeHome = 185 homeScreenCustomizationOptionEntries 186 .first { it.first == ThemePickerHomeCustomizationOption.PACK_THEME } 187 .second 188 optionPackThemeHome.setOnClickListener { navigateToPackThemeActivity.invoke() } 189 optionPackThemeIconHome = optionPackThemeHome.requireViewById(R.id.option_entry_icon) 190 191 val optionPackThemeLock = 192 lockScreenCustomizationOptionEntries 193 .first { it.first == ThemePickerHomeCustomizationOption.PACK_THEME } 194 .second 195 optionPackThemeLock.setOnClickListener { navigateToPackThemeActivity.invoke() } 196 optionPackThemeIconLock = optionPackThemeLock.requireViewById(R.id.option_entry_icon) 197 } 198 199 val optionColors: View = 200 homeScreenCustomizationOptionEntries 201 .first { it.first == ThemePickerHomeCustomizationOption.COLORS } 202 .second 203 val optionColorsIcon: ColorOptionIconView2 = 204 optionColors.requireViewById(R.id.option_entry_icon) 205 206 val optionShapeGrid: View = 207 homeScreenCustomizationOptionEntries 208 .first { it.first == ThemePickerHomeCustomizationOption.APP_SHAPE_GRID } 209 .second 210 val optionShapeGridDescription: TextView = 211 optionShapeGrid.requireViewById(R.id.option_entry_description) 212 val optionShapeGridIcon: ImageView = optionShapeGrid.requireViewById(R.id.option_entry_icon) 213 214 val optionColorContrast: View = 215 homeScreenCustomizationOptionEntries 216 .first { it.first == ThemePickerHomeCustomizationOption.COLOR_CONTRAST } 217 .second 218 optionColorContrast.setOnClickListener { navigateToColorContrastSettingsActivity.invoke() } 219 220 val optionThemedIcons = 221 homeScreenCustomizationOptionEntries 222 .first { it.first == ThemePickerHomeCustomizationOption.THEMED_ICONS } 223 .second 224 val optionThemedIconsSwitch = 225 optionThemedIcons.requireViewById<MaterialSwitch>(R.id.option_entry_switch) 226 227 ColorUpdateBinder.bind( 228 setColor = { color -> 229 optionClockIcon.setColorFilter(color) 230 optionShortcutIcon1.setColorFilter(color) 231 optionShortcutIcon2.setColorFilter(color) 232 optionShapeGridIcon.setColorFilter(color) 233 if (BaseFlags.get().isPackThemeEnabled()) { 234 optionPackThemeIconHome?.setColorFilter(color) 235 optionPackThemeIconLock?.setColorFilter(color) 236 } 237 }, 238 color = colorUpdateViewModel.colorOnSurfaceVariant, 239 shouldAnimate = isOnMainScreen, 240 lifecycleOwner = lifecycleOwner, 241 ) 242 243 lifecycleOwner.lifecycleScope.launch { 244 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 245 launch { 246 optionsViewModel.onCustomizeClockClicked.collect { 247 optionClock.setOnClickListener { _ -> it?.invoke() } 248 } 249 } 250 251 launch { 252 optionsViewModel.clockPickerViewModel.selectedClock.collect { 253 optionClockIcon.setImageDrawable(it.thumbnail) 254 } 255 } 256 257 launch { 258 optionsViewModel.onCustomizeShortcutClicked.collect { 259 optionShortcut.setOnClickListener { _ -> it?.invoke() } 260 } 261 } 262 263 launch { 264 optionsViewModel.keyguardQuickAffordancePickerViewModel2.summary.collect { 265 summary -> 266 optionShortcutDescription.let { 267 TextViewBinder.bind(view = it, viewModel = summary.description) 268 } 269 summary.icon1?.let { icon -> 270 optionShortcutIcon1.let { 271 IconViewBinder.bind(view = it, viewModel = icon) 272 } 273 } 274 optionShortcutIcon1.isVisible = summary.icon1 != null 275 276 summary.icon2?.let { icon -> 277 optionShortcutIcon2.let { 278 IconViewBinder.bind(view = it, viewModel = icon) 279 } 280 } 281 optionShortcutIcon2.isVisible = summary.icon2 != null 282 } 283 } 284 285 launch { 286 optionsViewModel.onCustomizeColorsClicked.collect { 287 optionColors.setOnClickListener { _ -> it?.invoke() } 288 } 289 } 290 291 launch { 292 optionsViewModel.onCustomizeShapeGridClicked.collect { 293 optionShapeGrid.setOnClickListener { _ -> it?.invoke() } 294 } 295 } 296 297 launch { 298 optionsViewModel.shapeGridPickerViewModel.selectedGridOption.collect { 299 gridOption -> 300 TextViewBinder.bind(optionShapeGridDescription, gridOption.text) 301 gridOption.payload?.let { optionShapeGridIcon.setImageDrawable(it) } 302 } 303 } 304 305 launch { 306 var binding: ColorContrastSectionViewBinder2.Binding? = null 307 optionsViewModel.colorContrastSectionViewModel.contrast.collectLatest { contrast 308 -> 309 binding?.destroy() 310 binding = 311 ColorContrastSectionViewBinder2.bind( 312 view = optionColorContrast, 313 contrast = contrast, 314 colorUpdateViewModel = colorUpdateViewModel, 315 shouldAnimateColor = isOnMainScreen, 316 lifecycleOwner = lifecycleOwner, 317 ) 318 } 319 } 320 321 launch { 322 var binding: ColorOptionIconBinder2.Binding? = null 323 optionsViewModel.colorPickerViewModel2.selectedColorOption.collect { colorOption 324 -> 325 (colorOption as? ColorOptionImpl)?.let { 326 binding?.destroy() 327 binding = 328 ColorOptionIconBinder2.bind( 329 view = optionColorsIcon, 330 viewModel = 331 ColorOptionIconViewModel.fromColorOption(colorOption), 332 darkTheme = view.resources.configuration.isNightModeActive, 333 colorUpdateViewModel = colorUpdateViewModel, 334 shouldAnimateColor = isOnMainScreen, 335 lifecycleOwner = lifecycleOwner, 336 ) 337 } 338 } 339 } 340 341 launch { 342 optionsViewModel.themedIconViewModel.isAvailable.collect { isAvailable -> 343 optionThemedIconsSwitch.isEnabled = isAvailable 344 } 345 } 346 347 launch { 348 var binding: SwitchColorBinder.Binding? = null 349 optionsViewModel.themedIconViewModel.isActivated.collect { 350 optionThemedIconsSwitch.isChecked = it 351 binding?.destroy() 352 binding = 353 SwitchColorBinder.bind( 354 switch = optionThemedIconsSwitch, 355 isChecked = it, 356 colorUpdateViewModel = colorUpdateViewModel, 357 shouldAnimateColor = isOnMainScreen, 358 lifecycleOwner = lifecycleOwner, 359 ) 360 } 361 } 362 363 launch { 364 optionsViewModel.themedIconViewModel.toggleThemedIcon.collect { 365 optionThemedIconsSwitch.setOnCheckedChangeListener { _, _ -> 366 launch { it.invoke() } 367 } 368 } 369 } 370 } 371 } 372 373 customizationOptionFloatingSheetViewMap 374 ?.get(ThemePickerLockCustomizationOption.CLOCK) 375 ?.let { 376 ClockFloatingSheetBinder.bind( 377 it, 378 optionsViewModel, 379 colorUpdateViewModel, 380 lifecycleOwner, 381 ) 382 } 383 384 customizationOptionFloatingSheetViewMap 385 ?.get(ThemePickerLockCustomizationOption.SHORTCUTS) 386 ?.let { 387 ShortcutFloatingSheetBinder.bind( 388 it, 389 optionsViewModel, 390 colorUpdateViewModel, 391 lifecycleOwner, 392 ) 393 } 394 395 if (!isComposeRefactorEnabled) { 396 customizationOptionFloatingSheetViewMap 397 ?.get(ThemePickerHomeCustomizationOption.COLORS) 398 ?.let { 399 ColorsFloatingSheetBinder.bind( 400 it, 401 optionsViewModel, 402 colorUpdateViewModel, 403 lifecycleOwner, 404 ) 405 } 406 } 407 408 customizationOptionFloatingSheetViewMap 409 ?.get(ThemePickerHomeCustomizationOption.APP_SHAPE_GRID) 410 ?.let { 411 ShapeGridFloatingSheetBinder.bind( 412 it, 413 optionsViewModel, 414 colorUpdateViewModel, 415 lifecycleOwner, 416 Dispatchers.IO, 417 ) 418 } 419 } 420 421 override fun bindClockPreview( 422 context: Context, 423 clockHostView: View, 424 clockFaceClickDelegateView: View, 425 viewModel: CustomizationPickerViewModel2, 426 colorUpdateViewModel: ColorUpdateViewModel, 427 lifecycleOwner: LifecycleOwner, 428 clockViewFactory: ClockViewFactory, 429 ) { 430 clockHostView as ClockConstraintLayoutHostView 431 val clockPickerViewModel = 432 (viewModel.customizationOptionsViewModel as ThemePickerCustomizationOptionsViewModel) 433 .clockPickerViewModel 434 435 lifecycleOwner.lifecycleScope.launch { 436 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 437 launch { 438 combine( 439 clockPickerViewModel.previewingClock, 440 clockPickerViewModel.previewingClockSize, 441 ) { clock, size -> 442 clock to size 443 } 444 .collect { (clock, size) -> 445 clockHostView.removeAllViews() 446 // For new customization picker, we should get views from clocklayout 447 if (Flags.newCustomizationPickerUi()) { 448 clockViewFactory.getController(clock.clockId)?.let { clockController 449 -> 450 val udfpsTop = 451 clockPickerViewModel.getUdfpsLocation()?.let { 452 it.centerY - it.radius 453 } 454 val previewConfig = 455 ClockPreviewConfig( 456 context = context, 457 isShadeLayoutWide = 458 clockPickerViewModel.getIsShadeLayoutWide(), 459 isSceneContainerFlagEnabled = false, 460 udfpsTop = udfpsTop, 461 ) 462 addClockViews(clockController, clockHostView, size) 463 val cs = ConstraintSet() 464 clockController.largeClock.layout.applyPreviewConstraints( 465 previewConfig, 466 cs, 467 ) 468 clockController.smallClock.layout.applyPreviewConstraints( 469 previewConfig, 470 cs, 471 ) 472 cs.applyTo(clockHostView) 473 } 474 } else { 475 val clockView = 476 when (size) { 477 ClockSize.DYNAMIC -> 478 clockViewFactory.getLargeView(clock.clockId) 479 ClockSize.SMALL -> 480 clockViewFactory.getSmallView(clock.clockId) 481 } 482 // The clock view might still be attached to an existing parent. 483 // Detach 484 // before adding to another parent. 485 (clockView.parent as? ViewGroup)?.removeView(clockView) 486 clockHostView.addView(clockView) 487 } 488 } 489 } 490 491 launch { 492 combine( 493 clockPickerViewModel.previewingSeedColor, 494 clockPickerViewModel.previewingClock, 495 clockPickerViewModel.previewingClockPresetIndexedStyle, 496 colorUpdateViewModel.systemColorsUpdated, 497 ::Quadruple, 498 ) 499 .collect { quadruple -> 500 val (color, clock, clockPresetIndexedStyle, _) = quadruple 501 clockViewFactory.updateColor(clock.clockId, color) 502 clockViewFactory.updateFontAxes( 503 clock.clockId, 504 clockPresetIndexedStyle?.style ?: ClockAxisStyle(), 505 ) 506 } 507 } 508 509 launch { 510 viewModel.lockPreviewAnimateToAlpha.collect { clockHostView.animateToAlpha(it) } 511 } 512 513 launch { 514 combine( 515 viewModel.customizationOptionsViewModel.selectedOption, 516 clockPickerViewModel.onClockFaceClicked, 517 ::Pair, 518 ) 519 .collect { (selectedOption, onClockFaceClicked) -> 520 clockFaceClickDelegateView.isVisible = 521 selectedOption == ThemePickerLockCustomizationOption.CLOCK 522 clockFaceClickDelegateView.setOnClickListener { 523 onClockFaceClicked.invoke() 524 } 525 } 526 } 527 } 528 } 529 } 530 531 override fun bindDiscardChangesDialog( 532 customizationOptionsViewModel: CustomizationOptionsViewModel, 533 lifecycleOwner: LifecycleOwner, 534 activity: Activity, 535 ) { 536 defaultCustomizationOptionsBinder.bindDiscardChangesDialog( 537 customizationOptionsViewModel, 538 lifecycleOwner, 539 activity, 540 ) 541 } 542 543 data class Quadruple<A, B, C, D>(val first: A, val second: B, val third: C, val fourth: D) 544 } 545