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