1 /*
<lambda>null2  * Copyright (C) 2022 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.wallpaper.picker.customization.ui.binder
19 
20 import android.content.Context
21 import android.view.View
22 import android.view.ViewGroup
23 import android.view.WindowInsets
24 import android.widget.FrameLayout
25 import androidx.annotation.IdRes
26 import androidx.core.view.children
27 import androidx.core.view.isInvisible
28 import androidx.core.view.updateLayoutParams
29 import androidx.core.widget.NestedScrollView
30 import androidx.lifecycle.Lifecycle
31 import androidx.lifecycle.LifecycleOwner
32 import androidx.lifecycle.lifecycleScope
33 import androidx.lifecycle.repeatOnLifecycle
34 import com.android.wallpaper.R
35 import com.android.wallpaper.model.CustomizationSectionController
36 import com.android.wallpaper.module.InjectorProvider
37 import com.android.wallpaper.picker.SectionView
38 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel
39 import com.android.wallpaper.picker.undo.ui.binder.RevertToolbarButtonBinder
40 import kotlinx.coroutines.DisposableHandle
41 import kotlinx.coroutines.launch
42 
43 typealias SectionController = CustomizationSectionController<*>
44 
45 /** Binds view to view-model for the customization picker. */
46 object CustomizationPickerBinder {
47 
48     /**
49      * Binds the given view and view-model, keeping the UI up-to-date and listening to user input.
50      *
51      * @param view The root of the UI to keep up-to-date and observe for user input.
52      * @param toolbarViewId The view ID of the toolbar view.
53      * @param viewModel The view-model to observe UI state from and report user input to.
54      * @param lifecycleOwner An owner of the lifecycle, so we can stop doing work when the lifecycle
55      *   cleans up.
56      * @param sectionControllerProvider A function that can provide the list of [SectionController]
57      *   instances to show, based on the given passed-in value of "isOnLockScreen".
58      * @return A [DisposableHandle] to use to dispose of the binding before another binding is about
59      *   to be created by a subsequent call to this function.
60      */
61     @JvmStatic
62     fun bind(
63         view: View,
64         @IdRes toolbarViewId: Int,
65         viewModel: CustomizationPickerViewModel,
66         lifecycleOwner: LifecycleOwner,
67         sectionControllerProvider: (isOnLockScreen: Boolean) -> List<SectionController>,
68     ): DisposableHandle {
69         RevertToolbarButtonBinder.bind(
70             view = view.requireViewById(toolbarViewId),
71             viewModel = viewModel.undo,
72             lifecycleOwner = lifecycleOwner,
73         )
74 
75         CustomizationPickerTabsBinder.bind(
76             view = view,
77             viewModel = viewModel,
78             lifecycleOwner = lifecycleOwner,
79         )
80 
81         val lockScrollContainer = view.requireViewById<NestedScrollView>(R.id.lock_scroll_container)
82         val homeScrollContainer = view.requireViewById<NestedScrollView>(R.id.home_scroll_container)
83 
84         val lockSectionContainer = view.requireViewById<ViewGroup>(R.id.lock_section_container)
85         lockSectionContainer.setOnApplyWindowInsetsListener { v: View, windowInsets: WindowInsets ->
86             v.setPadding(
87                 v.paddingLeft,
88                 v.paddingTop,
89                 v.paddingRight,
90                 windowInsets.systemWindowInsetBottom
91             )
92             windowInsets.consumeSystemWindowInsets()
93         }
94         lockSectionContainer.updateLayoutParams<FrameLayout.LayoutParams> {
95             // We don't want the top margin from the XML because our tabs have that as padding so
96             // they can be collapsed into the toolbar with spacing from the actual title text.
97             topMargin = 0
98         }
99 
100         val homeSectionContainer = view.requireViewById<ViewGroup>(R.id.home_section_container)
101         homeSectionContainer.setOnApplyWindowInsetsListener { v: View, windowInsets: WindowInsets ->
102             v.setPadding(
103                 v.paddingLeft,
104                 v.paddingTop,
105                 v.paddingRight,
106                 windowInsets.systemWindowInsetBottom
107             )
108             windowInsets.consumeSystemWindowInsets()
109         }
110         homeSectionContainer.updateLayoutParams<FrameLayout.LayoutParams> {
111             // We don't want the top margin from the XML because our tabs have that as padding so
112             // they can be collapsed into the toolbar with spacing from the actual title text.
113             topMargin = 0
114         }
115 
116         // create and add sections to both the lock and home screen tabs ahead of time, since
117         // the lock and home screen preview sections are both needed to load initial wallpaper
118         // colors for the correct functioning of the color picker
119         createAndAddSections(
120             view.context,
121             homeSectionContainer,
122             isOnLockScreen = false,
123             sectionControllerProvider
124         )
125         createAndAddSections(
126             view.context,
127             lockSectionContainer,
128             isOnLockScreen = true,
129             sectionControllerProvider
130         )
131 
132         val job =
133             lifecycleOwner.lifecycleScope.launch {
134                 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
135                     launch {
136                         viewModel.isOnLockScreen.collect { isOnLockScreen ->
137                             InjectorProvider.getInjector()
138                                 .getPreviewActivityIntentFactory()
139                                 .setViewAsHome(!isOnLockScreen)
140                             InjectorProvider.getInjector()
141                                 .getViewOnlyPreviewActivityIntentFactory()
142                                 .setViewAsHome(!isOnLockScreen)
143                             // Offset the scroll position of both tabs
144                             lockScrollContainer.scrollTo(0, 0)
145                             homeScrollContainer.scrollTo(0, 0)
146 
147                             lockScrollContainer.isInvisible = !isOnLockScreen
148                             homeScrollContainer.isInvisible = isOnLockScreen
149                         }
150                     }
151                 }
152 
153                 // This happens when the lifecycle is stopped.
154                 lockSectionContainer.children
155                     .map { Pair(it, it.tag as? CustomizationSectionController<out SectionView>) }
156                     .forEach {
157                         it.second?.release()
158                         it.first?.tag = null
159                     }
160                 lockSectionContainer.removeAllViews()
161                 homeSectionContainer.children
162                     .map { Pair(it, it.tag as? CustomizationSectionController<out SectionView>) }
163                     .forEach {
164                         it.second?.release()
165                         it.first?.tag = null
166                     }
167                 homeSectionContainer.removeAllViews()
168             }
169         return DisposableHandle { job.cancel() }
170     }
171 
172     private fun createAndAddSections(
173         context: Context,
174         container: ViewGroup,
175         isOnLockScreen: Boolean,
176         sectionControllerProvider: (isOnLockScreen: Boolean) -> List<SectionController>,
177     ) {
178         sectionControllerProvider
179             .invoke(isOnLockScreen)
180             .filter { it.isAvailable(context) }
181             .forEach { controller ->
182                 val viewToAdd =
183                     controller.createView(
184                         context,
185                         CustomizationSectionController.ViewCreationParams(
186                             isOnLockScreen = isOnLockScreen,
187                             isWallpaperVisibilityControlledByTab = true,
188                         )
189                     )
190                 viewToAdd.tag = controller
191                 container.addView(viewToAdd)
192             }
193     }
194 }
195