1 /*
<lambda>null2  * 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 package com.android.customization.picker.clock.ui.binder
17 
18 import android.content.Context
19 import android.content.res.Configuration
20 import android.text.Spannable
21 import android.text.SpannableString
22 import android.text.style.TextAppearanceSpan
23 import android.view.LayoutInflater
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.LinearLayout
27 import android.widget.RadioButton
28 import android.widget.RadioGroup
29 import android.widget.RadioGroup.OnCheckedChangeListener
30 import android.widget.SeekBar
31 import androidx.core.view.doOnPreDraw
32 import androidx.core.view.isInvisible
33 import androidx.core.view.isVisible
34 import androidx.lifecycle.Lifecycle
35 import androidx.lifecycle.LifecycleEventObserver
36 import androidx.lifecycle.LifecycleOwner
37 import androidx.lifecycle.lifecycleScope
38 import androidx.lifecycle.repeatOnLifecycle
39 import androidx.recyclerview.widget.LinearLayoutManager
40 import androidx.recyclerview.widget.RecyclerView
41 import com.android.customization.picker.clock.shared.ClockSize
42 import com.android.customization.picker.clock.ui.adapter.ClockSettingsTabAdapter
43 import com.android.customization.picker.clock.ui.view.ClockCarouselView
44 import com.android.customization.picker.clock.ui.view.ClockViewFactory
45 import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel
46 import com.android.customization.picker.color.ui.binder.ColorOptionIconBinder
47 import com.android.systemui.shared.Flags
48 import com.android.themepicker.R
49 import com.android.wallpaper.picker.common.ui.view.ItemSpacing
50 import com.android.wallpaper.picker.option.ui.binder.OptionItemBinder
51 import kotlinx.coroutines.flow.combine
52 import kotlinx.coroutines.flow.mapNotNull
53 import kotlinx.coroutines.launch
54 
55 /** Bind between the clock settings screen and its view model. */
56 object ClockSettingsBinder {
57     private const val SLIDER_ENABLED_ALPHA = 1f
58     private const val SLIDER_DISABLED_ALPHA = .3f
59     private const val COLOR_PICKER_ITEM_PREFIX_ID = 1234
60 
61     fun bind(
62         view: View,
63         viewModel: ClockSettingsViewModel,
64         clockViewFactory: ClockViewFactory,
65         lifecycleOwner: LifecycleOwner,
66     ) {
67         if (Flags.newCustomizationPickerUi()) {
68             return
69         }
70         val clockHostView: ViewGroup = view.requireViewById(R.id.clock_host_view)
71         val tabView: RecyclerView = view.requireViewById(R.id.tabs)
72         val tabAdapter = ClockSettingsTabAdapter()
73         tabView.adapter = tabAdapter
74         tabView.layoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
75         tabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP))
76         val colorOptionContainerListView: LinearLayout = view.requireViewById(R.id.color_options)
77         val slider: SeekBar = view.requireViewById(R.id.slider)
78         slider.setOnSeekBarChangeListener(
79             object : SeekBar.OnSeekBarChangeListener {
80                 override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) {
81                     if (fromUser) {
82                         viewModel.onSliderProgressChanged(progress)
83                     }
84                 }
85 
86                 override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
87 
88                 override fun onStopTrackingTouch(seekBar: SeekBar?) {
89                     seekBar?.progress?.let {
90                         lifecycleOwner.lifecycleScope.launch { viewModel.onSliderProgressStop(it) }
91                     }
92                 }
93             }
94         )
95 
96         val onCheckedChangeListener = OnCheckedChangeListener { _, id ->
97             when (id) {
98                 R.id.radio_dynamic -> viewModel.setClockSize(ClockSize.DYNAMIC)
99                 R.id.radio_small -> viewModel.setClockSize(ClockSize.SMALL)
100             }
101         }
102         val clockSizeRadioGroup =
103             view.requireViewById<RadioGroup>(R.id.clock_size_radio_button_group)
104         clockSizeRadioGroup.setOnCheckedChangeListener(onCheckedChangeListener)
105         view.requireViewById<RadioButton>(R.id.radio_dynamic).text =
106             getRadioText(
107                 view.context.applicationContext,
108                 view.resources.getString(R.string.clock_size_dynamic),
109                 view.resources.getString(R.string.clock_size_dynamic_description),
110             )
111         view.requireViewById<RadioButton>(R.id.radio_small).text =
112             getRadioText(
113                 view.context.applicationContext,
114                 view.resources.getString(R.string.clock_size_small),
115                 view.resources.getString(R.string.clock_size_small_description),
116             )
117 
118         val colorOptionContainer = view.requireViewById<View>(R.id.color_picker_container)
119         lifecycleOwner.lifecycleScope.launch {
120             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
121                 launch {
122                     viewModel.seedColor.collect { seedColor ->
123                         viewModel.selectedClockId.value?.let { selectedClockId ->
124                             clockViewFactory.updateColor(selectedClockId, seedColor)
125                         }
126                     }
127                 }
128 
129                 launch { viewModel.tabs.collect { tabAdapter.setItems(it) } }
130 
131                 launch {
132                     viewModel.selectedTab.collect { tab ->
133                         when (tab) {
134                             ClockSettingsViewModel.Tab.COLOR -> {
135                                 colorOptionContainer.isVisible = true
136                                 clockSizeRadioGroup.isInvisible = true
137                             }
138                             ClockSettingsViewModel.Tab.SIZE -> {
139                                 colorOptionContainer.isInvisible = true
140                                 clockSizeRadioGroup.isVisible = true
141                             }
142                         }
143                     }
144                 }
145 
146                 launch {
147                     viewModel.colorOptions.collect { colorOptions ->
148                         colorOptionContainerListView.removeAllViews()
149                         colorOptions.forEachIndexed { index, colorOption ->
150                             colorOption.payload?.let { payload ->
151                                 val item =
152                                     LayoutInflater.from(view.context)
153                                         .inflate(
154                                             R.layout.clock_color_option,
155                                             colorOptionContainerListView,
156                                             false,
157                                         ) as LinearLayout
158                                 val darkMode =
159                                     (view.resources.configuration.uiMode and
160                                         Configuration.UI_MODE_NIGHT_MASK ==
161                                         Configuration.UI_MODE_NIGHT_YES)
162                                 ColorOptionIconBinder.bind(
163                                     item.requireViewById(R.id.foreground),
164                                     payload,
165                                     darkMode,
166                                 )
167                                 OptionItemBinder.bind(
168                                     view = item,
169                                     viewModel = colorOptions[index],
170                                     lifecycleOwner = lifecycleOwner,
171                                     foregroundTintSpec = null,
172                                 )
173 
174                                 val id = COLOR_PICKER_ITEM_PREFIX_ID + index
175                                 item.id = id
176                                 colorOptionContainerListView.addView(item)
177                             }
178                         }
179                     }
180                 }
181 
182                 launch {
183                     viewModel.selectedColorOptionPosition.collect { selectedPosition ->
184                         if (selectedPosition != -1) {
185                             val colorOptionContainerListView: LinearLayout =
186                                 view.requireViewById(R.id.color_options)
187 
188                             val selectedView =
189                                 colorOptionContainerListView.findViewById<View>(
190                                     COLOR_PICKER_ITEM_PREFIX_ID + selectedPosition
191                                 )
192                             selectedView?.parent?.requestChildFocus(selectedView, selectedView)
193                         }
194                     }
195                 }
196 
197                 launch {
198                     combine(
199                             viewModel.selectedClockId.mapNotNull { it },
200                             viewModel.selectedClockSize,
201                             ::Pair,
202                         )
203                         .collect { (clockId, size) ->
204                             clockHostView.removeAllViews()
205                             val clockView =
206                                 when (size) {
207                                     ClockSize.DYNAMIC -> clockViewFactory.getLargeView(clockId)
208                                     ClockSize.SMALL -> clockViewFactory.getSmallView(clockId)
209                                 }
210                             // The clock view might still be attached to an existing parent.
211                             // Detach before adding to another parent.
212                             (clockView.parent as? ViewGroup)?.removeView(clockView)
213                             clockHostView.addView(clockView)
214 
215                             when (size) {
216                                 ClockSize.DYNAMIC -> {
217                                     // When clock size data flow emits clock size signal, we want
218                                     // to update the view without triggering on checked change,
219                                     // which is supposed to be triggered by user interaction only.
220                                     clockSizeRadioGroup.setOnCheckedChangeListener(null)
221                                     clockSizeRadioGroup.check(R.id.radio_dynamic)
222                                     clockSizeRadioGroup.setOnCheckedChangeListener(
223                                         onCheckedChangeListener
224                                     )
225                                     clockHostView.doOnPreDraw {
226                                         it.pivotX = it.width / 2F
227                                         it.pivotY = it.height / 2F
228                                     }
229                                 }
230                                 ClockSize.SMALL -> {
231                                     // When clock size data flow emits clock size signal, we want
232                                     // to update the view without triggering on checked change,
233                                     // which is supposed to be triggered by user interaction only.
234                                     clockSizeRadioGroup.setOnCheckedChangeListener(null)
235                                     clockSizeRadioGroup.check(R.id.radio_small)
236                                     clockSizeRadioGroup.setOnCheckedChangeListener(
237                                         onCheckedChangeListener
238                                     )
239                                     clockHostView.doOnPreDraw {
240                                         it.pivotX = ClockCarouselView.getCenteredHostViewPivotX(it)
241                                         it.pivotY = 0F
242                                     }
243                                 }
244                             }
245                         }
246                 }
247 
248                 launch {
249                     viewModel.sliderProgress.collect { progress ->
250                         slider.setProgress(progress, true)
251                     }
252                 }
253 
254                 launch {
255                     viewModel.isSliderEnabled.collect { isEnabled ->
256                         slider.isEnabled = isEnabled
257                         slider.alpha =
258                             if (isEnabled) SLIDER_ENABLED_ALPHA else SLIDER_DISABLED_ALPHA
259                     }
260                 }
261             }
262         }
263 
264         lifecycleOwner.lifecycle.addObserver(
265             LifecycleEventObserver { source, event ->
266                 when (event) {
267                     Lifecycle.Event.ON_RESUME -> {
268                         clockViewFactory.registerTimeTicker(source)
269                     }
270                     Lifecycle.Event.ON_PAUSE -> {
271                         clockViewFactory.unregisterTimeTicker(source)
272                     }
273                     else -> {}
274                 }
275             }
276         )
277     }
278 
279     private fun getRadioText(
280         context: Context,
281         title: String,
282         description: String,
283     ): SpannableString {
284         val text = SpannableString(title + "\n" + description)
285         text.setSpan(
286             TextAppearanceSpan(context, R.style.SectionTitleTextStyle),
287             0,
288             title.length,
289             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
290         )
291         text.setSpan(
292             TextAppearanceSpan(context, R.style.SectionSubtitleTextStyle),
293             title.length + 1,
294             title.length + 1 + description.length,
295             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
296         )
297         return text
298     }
299 }
300