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.customization.picker.quickaffordance.ui.binder 19 20 import android.app.Dialog 21 import android.content.Context 22 import android.view.View 23 import android.view.ViewGroup 24 import android.view.accessibility.AccessibilityEvent 25 import android.widget.ImageView 26 import androidx.core.view.AccessibilityDelegateCompat 27 import androidx.core.view.ViewCompat 28 import androidx.lifecycle.Lifecycle 29 import androidx.lifecycle.LifecycleOwner 30 import androidx.lifecycle.lifecycleScope 31 import androidx.lifecycle.repeatOnLifecycle 32 import androidx.recyclerview.widget.LinearLayoutManager 33 import androidx.recyclerview.widget.RecyclerView 34 import com.android.customization.picker.common.ui.view.ItemSpacing 35 import com.android.customization.picker.quickaffordance.ui.adapter.SlotTabAdapter 36 import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel 37 import com.android.themepicker.R 38 import com.android.wallpaper.picker.common.dialog.ui.viewbinder.DialogViewBinder 39 import com.android.wallpaper.picker.common.dialog.ui.viewmodel.DialogViewModel 40 import com.android.wallpaper.picker.common.icon.ui.viewbinder.IconViewBinder 41 import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon 42 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter 43 import kotlinx.coroutines.ExperimentalCoroutinesApi 44 import kotlinx.coroutines.flow.collectIndexed 45 import kotlinx.coroutines.flow.combine 46 import kotlinx.coroutines.flow.distinctUntilChanged 47 import kotlinx.coroutines.flow.flatMapLatest 48 import kotlinx.coroutines.flow.map 49 import kotlinx.coroutines.launch 50 51 @OptIn(ExperimentalCoroutinesApi::class) 52 object KeyguardQuickAffordancePickerBinder { 53 54 /** Binds view with view-model for a lock screen quick affordance picker experience. */ 55 @JvmStatic 56 fun bind( 57 view: View, 58 viewModel: KeyguardQuickAffordancePickerViewModel, 59 lifecycleOwner: LifecycleOwner, 60 ) { 61 val slotTabView: RecyclerView = view.requireViewById(R.id.slot_tabs) 62 val affordancesView: RecyclerView = view.requireViewById(R.id.affordances) 63 64 val slotTabAdapter = SlotTabAdapter() 65 slotTabView.adapter = slotTabAdapter 66 slotTabView.layoutManager = 67 LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false) 68 slotTabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP)) 69 70 // Setting a custom accessibility delegate so that the default content descriptions 71 // for items in a list aren't announced (for left & right shortcuts). We populate 72 // the content description for these shortcuts later on with the right (expected) 73 // values. 74 val slotTabViewDelegate: AccessibilityDelegateCompat = 75 object : AccessibilityDelegateCompat() { 76 override fun onRequestSendAccessibilityEvent( 77 host: ViewGroup, 78 child: View, 79 event: AccessibilityEvent 80 ): Boolean { 81 if (event.eventType != AccessibilityEvent.TYPE_VIEW_FOCUSED) { 82 child.contentDescription = null 83 } 84 return super.onRequestSendAccessibilityEvent(host, child, event) 85 } 86 } 87 88 ViewCompat.setAccessibilityDelegate(slotTabView, slotTabViewDelegate) 89 val affordancesAdapter = 90 OptionItemAdapter( 91 layoutResourceId = R.layout.keyguard_quick_affordance, 92 lifecycleOwner = lifecycleOwner, 93 bindIcon = { foregroundView: View, gridIcon: Icon -> 94 val imageView = foregroundView as? ImageView 95 imageView?.let { IconViewBinder.bind(imageView, gridIcon) } 96 } 97 ) 98 affordancesView.adapter = affordancesAdapter 99 affordancesView.layoutManager = 100 LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false) 101 affordancesView.addItemDecoration(ItemSpacing(ItemSpacing.ITEM_SPACING_DP)) 102 103 var dialog: Dialog? = null 104 105 lifecycleOwner.lifecycleScope.launch { 106 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 107 launch { 108 viewModel.slots 109 .map { slotById -> slotById.values } 110 .collect { slots -> slotTabAdapter.setItems(slots.toList()) } 111 } 112 113 launch { 114 viewModel.quickAffordances.collect { affordances -> 115 affordancesAdapter.setItems(affordances) 116 } 117 } 118 119 launch { 120 viewModel.quickAffordances 121 .flatMapLatest { affordances -> 122 combine(affordances.map { affordance -> affordance.isSelected }) { 123 selectedFlags -> 124 selectedFlags.indexOfFirst { it } 125 } 126 } 127 .collectIndexed { index, selectedPosition -> 128 // Scroll the view to show the first selected affordance. 129 if (selectedPosition != -1) { 130 // We use "post" because we need to give the adapter item a pass to 131 // update the view. 132 affordancesView.post { 133 if (index == 0) { 134 // don't animate on initial collection 135 affordancesView.scrollToPosition(selectedPosition) 136 } else { 137 affordancesView.smoothScrollToPosition(selectedPosition) 138 } 139 } 140 } 141 } 142 } 143 144 launch { 145 viewModel.dialog.distinctUntilChanged().collect { dialogRequest -> 146 dialog?.dismiss() 147 dialog = 148 if (dialogRequest != null) { 149 showDialog( 150 context = view.context, 151 request = dialogRequest, 152 onDismissed = viewModel::onDialogDismissed 153 ) 154 } else { 155 null 156 } 157 } 158 } 159 160 launch { 161 viewModel.activityStartRequests.collect { intent -> 162 if (intent != null) { 163 view.context.startActivity(intent) 164 viewModel.onActivityStarted() 165 } 166 } 167 } 168 } 169 } 170 } 171 172 private fun showDialog( 173 context: Context, 174 request: DialogViewModel, 175 onDismissed: () -> Unit, 176 ): Dialog { 177 return DialogViewBinder.show( 178 context = context, 179 viewModel = request, 180 onDismissed = onDismissed, 181 ) 182 } 183 } 184