• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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