• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.view.View
21 import android.view.ViewGroup
22 import android.view.WindowInsets
23 import android.widget.FrameLayout
24 import androidx.annotation.IdRes
25 import androidx.core.view.children
26 import androidx.core.view.updateLayoutParams
27 import androidx.lifecycle.Lifecycle
28 import androidx.lifecycle.LifecycleOwner
29 import androidx.lifecycle.lifecycleScope
30 import androidx.lifecycle.repeatOnLifecycle
31 import com.android.wallpaper.R
32 import com.android.wallpaper.model.CustomizationSectionController
33 import com.android.wallpaper.picker.SectionView
34 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel
35 import com.android.wallpaper.picker.undo.ui.binder.RevertToolbarButtonBinder
36 import kotlinx.coroutines.DisposableHandle
37 import kotlinx.coroutines.launch
38 
39 typealias SectionController = CustomizationSectionController<*>
40 
41 /** Binds view to view-model for the customization picker. */
42 object CustomizationPickerBinder {
43 
44     /**
45      * Binds the given view and view-model, keeping the UI up-to-date and listening to user input.
46      *
47      * @param view The root of the UI to keep up-to-date and observe for user input.
48      * @param toolbarViewId The view ID of the toolbar view.
49      * @param viewModel The view-model to observe UI state from and report user input to.
50      * @param lifecycleOwner An owner of the lifecycle, so we can stop doing work when the lifecycle
51      *   cleans up.
52      * @param sectionControllerProvider A function that can provide the list of [SectionController]
53      *   instances to show, based on the given passed-in value of "isOnLockScreen".
54      * @return A [DisposableHandle] to use to dispose of the binding before another binding is about
55      *   to be created by a subsequent call to this function.
56      */
57     @JvmStatic
58     fun bind(
59         view: View,
60         @IdRes toolbarViewId: Int,
61         viewModel: CustomizationPickerViewModel,
62         lifecycleOwner: LifecycleOwner,
63         sectionControllerProvider: (isOnLockScreen: Boolean) -> List<SectionController>,
64     ): DisposableHandle {
65         RevertToolbarButtonBinder.bind(
66             view = view.requireViewById(toolbarViewId),
67             viewModel = viewModel.undo,
68             lifecycleOwner = lifecycleOwner,
69         )
70 
71         CustomizationPickerTabsBinder.bind(
72             view = view,
73             viewModel = viewModel,
74             lifecycleOwner = lifecycleOwner,
75         )
76 
77         val sectionContainer = view.findViewById<ViewGroup>(R.id.section_container)
78         sectionContainer.setOnApplyWindowInsetsListener { v: View, windowInsets: WindowInsets ->
79             v.setPadding(
80                 v.paddingLeft,
81                 v.paddingTop,
82                 v.paddingRight,
83                 windowInsets.systemWindowInsetBottom
84             )
85             windowInsets.consumeSystemWindowInsets()
86         }
87         sectionContainer.updateLayoutParams<FrameLayout.LayoutParams> {
88             // We don't want the top margin from the XML because our tabs have that as padding so
89             // they can be collapsed into the toolbar with spacing from the actual title text.
90             topMargin = 0
91         }
92 
93         val job =
94             lifecycleOwner.lifecycleScope.launch {
95                 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
96                     launch {
97                         viewModel.isOnLockScreen.collect { isOnLockScreen ->
98                             // These are the available section controllers we should use now.
99                             val newSectionControllers =
100                                 sectionControllerProvider.invoke(isOnLockScreen).filter {
101                                     it.isAvailable(view.context)
102                                 }
103 
104                             check(
105                                 newSectionControllers[0].shouldRetainInstanceWhenSwitchingTabs()
106                             ) {
107                                 "We are not recreating the first section when the users switching" +
108                                     " between the home screen and lock screen tab. The first" +
109                                     " section should always retain."
110                             }
111 
112                             val firstTime = sectionContainer.childCount == 0
113                             if (!firstTime) {
114                                 // Remove all views, except the very first one, which we assume is
115                                 // for
116                                 // the wallpaper preview section.
117                                 sectionContainer.removeViews(1, sectionContainer.childCount - 1)
118 
119                                 // The old controllers for the removed views should be released,
120                                 // except
121                                 // for the very first one, which is for the wallpaper preview
122                                 // section;
123                                 // that one we keep but just tell it that we switched screens.
124                                 sectionContainer.children
125                                     .mapNotNull { it.tag as? SectionController }
126                                     .forEachIndexed { index, oldController ->
127                                         if (index == 0) {
128                                             // We assume that index 0 is the wallpaper preview
129                                             // section.
130                                             // We keep it because it's an expensive section (as it
131                                             // needs
132                                             // to maintain a wallpaper connection that seems to be
133                                             // making assumptions about its SurfaceView always
134                                             // remaining
135                                             // attached to the window).
136                                             oldController.onScreenSwitched(isOnLockScreen)
137                                         } else {
138                                             // All other old controllers will be thrown out so let's
139                                             // release them.
140                                             oldController.release()
141                                         }
142                                     }
143                             }
144 
145                             // Let's add the new controllers and views.
146                             newSectionControllers.forEachIndexed { index, controller ->
147                                 if (firstTime || index > 0) {
148                                     val addedView =
149                                         controller.createView(
150                                             view.context,
151                                             CustomizationSectionController.ViewCreationParams(
152                                                 isOnLockScreen = isOnLockScreen,
153                                             )
154                                         )
155                                     addedView?.tag = controller
156                                     sectionContainer.addView(addedView)
157                                 }
158                             }
159                         }
160                     }
161                 }
162 
163                 // This happens when the lifecycle is stopped.
164                 sectionContainer.children
165                     .mapNotNull { it.tag as? CustomizationSectionController<out SectionView> }
166                     .forEach { controller -> controller.release() }
167                 sectionContainer.removeAllViews()
168             }
169         return DisposableHandle { job.cancel() }
170     }
171 }
172