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 package com.android.systemui.people.ui.view 18 19 import android.content.Context 20 import android.graphics.Color 21 import android.graphics.Outline 22 import android.graphics.drawable.GradientDrawable 23 import android.util.Log 24 import android.view.LayoutInflater 25 import android.view.View 26 import android.view.ViewGroup 27 import android.view.ViewOutlineProvider 28 import android.widget.LinearLayout 29 import androidx.lifecycle.Lifecycle 30 import androidx.lifecycle.Lifecycle.State.CREATED 31 import androidx.lifecycle.LifecycleOwner 32 import androidx.lifecycle.lifecycleScope 33 import androidx.lifecycle.repeatOnLifecycle 34 import com.android.systemui.R 35 import com.android.systemui.people.PeopleSpaceTileView 36 import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel 37 import com.android.systemui.people.ui.viewmodel.PeopleViewModel 38 import kotlinx.coroutines.flow.collect 39 import kotlinx.coroutines.flow.combine 40 import kotlinx.coroutines.launch 41 42 /** A ViewBinder for [PeopleViewModel]. */ 43 object PeopleViewBinder { 44 private const val TAG = "PeopleViewBinder" 45 46 /** 47 * The [ViewOutlineProvider] used to clip the corner radius of the recent and priority lists. 48 */ 49 private val ViewOutlineProvider = 50 object : ViewOutlineProvider() { 51 override fun getOutline(view: View, outline: Outline) { 52 outline.setRoundRect( 53 0, 54 0, 55 view.width, 56 view.height, 57 view.context.resources.getDimension(R.dimen.people_space_widget_radius), 58 ) 59 } 60 } 61 62 /** Create a [View] that can later be [bound][bind] to a [PeopleViewModel]. */ 63 @JvmStatic 64 fun create(context: Context): ViewGroup { 65 return LayoutInflater.from(context) 66 .inflate(R.layout.people_space_activity, /* root= */ null) as ViewGroup 67 } 68 69 /** Bind [view] to [viewModel]. */ 70 @JvmStatic 71 fun bind( 72 view: ViewGroup, 73 viewModel: PeopleViewModel, 74 lifecycleOwner: LifecycleOwner, 75 onResult: (PeopleViewModel.Result) -> Unit, 76 ) { 77 // Call [onResult] as soon as a result is available. 78 lifecycleOwner.lifecycleScope.launch { 79 lifecycleOwner.repeatOnLifecycle(CREATED) { 80 viewModel.result.collect { result -> 81 if (result != null) { 82 viewModel.clearResult() 83 onResult(result) 84 } 85 } 86 } 87 } 88 89 // Start collecting the UI data once the Activity is STARTED. 90 lifecycleOwner.lifecycleScope.launch { 91 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 92 combine( 93 viewModel.priorityTiles, 94 viewModel.recentTiles, 95 ) { priority, recent -> 96 priority to recent 97 } 98 .collect { (priorityTiles, recentTiles) -> 99 if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) { 100 setConversationsContent( 101 view, 102 priorityTiles, 103 recentTiles, 104 viewModel::onTileClicked, 105 ) 106 } else { 107 setNoConversationsContent(view, viewModel::onUserJourneyCancelled) 108 } 109 } 110 } 111 } 112 113 // Make sure to refresh the tiles/conversations when the Activity is resumed, so that it 114 // updates them when going back to the Activity after leaving it. 115 lifecycleOwner.lifecycleScope.launch { 116 lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { 117 viewModel.onTileRefreshRequested() 118 } 119 } 120 } 121 122 private fun setNoConversationsContent(view: ViewGroup, onGotItClicked: () -> Unit) { 123 // This should never happen. 124 if (view.childCount > 1) { 125 error("view has ${view.childCount} children, it should have maximum 1") 126 } 127 128 // The static content for no conversations is already shown. 129 if (view.findViewById<View>(R.id.top_level_no_conversations) != null) { 130 return 131 } 132 133 // If we were showing the content with conversations earlier, remove it. 134 if (view.childCount == 1) { 135 view.removeViewAt(0) 136 } 137 138 val context = view.context 139 val noConversationsView = 140 LayoutInflater.from(context) 141 .inflate(R.layout.people_space_activity_no_conversations, /* root= */ view) 142 143 noConversationsView.findViewById<View>(R.id.got_it_button).setOnClickListener { 144 onGotItClicked() 145 } 146 147 // The Tile preview has colorBackground as its background. Change it so it's different than 148 // the activity's background. 149 val item = noConversationsView.findViewById<LinearLayout>(android.R.id.background) 150 val shape = item.background as GradientDrawable 151 val ta = 152 context.theme.obtainStyledAttributes( 153 intArrayOf(com.android.internal.R.attr.colorSurface) 154 ) 155 shape.setColor(ta.getColor(0, Color.WHITE)) 156 ta.recycle() 157 } 158 159 private fun setConversationsContent( 160 view: ViewGroup, 161 priorityTiles: List<PeopleTileViewModel>, 162 recentTiles: List<PeopleTileViewModel>, 163 onTileClicked: (PeopleTileViewModel) -> Unit, 164 ) { 165 // This should never happen. 166 if (view.childCount > 1) { 167 error("view has ${view.childCount} children, it should have maximum 1") 168 } 169 170 // Inflate the content with conversations, if it's not already. 171 if (view.findViewById<View>(R.id.top_level_with_conversations) == null) { 172 // If we were showing the content without conversations earlier, remove it. 173 if (view.childCount == 1) { 174 view.removeViewAt(0) 175 } 176 177 LayoutInflater.from(view.context) 178 .inflate(R.layout.people_space_activity_with_conversations, /* root= */ view) 179 } 180 181 // TODO(b/193782241): Replace the NestedScrollView + 2x LinearLayout from this layout into a 182 // single RecyclerView once this screen is tested by screenshot tests. Introduce a 183 // PeopleSpaceTileViewBinder that will properly create and bind the View associated to a 184 // PeopleSpaceTileViewModel (and remove the PeopleSpaceTileView class). 185 val conversationsView = view.requireViewById<View>(R.id.top_level_with_conversations) 186 setTileViews( 187 conversationsView, 188 R.id.priority, 189 R.id.priority_tiles, 190 priorityTiles, 191 onTileClicked, 192 ) 193 194 setTileViews( 195 conversationsView, 196 R.id.recent, 197 R.id.recent_tiles, 198 recentTiles, 199 onTileClicked, 200 ) 201 } 202 203 /** Sets a [PeopleSpaceTileView]s for each conversation. */ 204 private fun setTileViews( 205 root: View, 206 tilesListId: Int, 207 tilesId: Int, 208 tiles: List<PeopleTileViewModel>, 209 onTileClicked: (PeopleTileViewModel) -> Unit, 210 ) { 211 // Remove any previously added tile. 212 // TODO(b/193782241): Once this list is a big RecyclerView, set the current list and use 213 // DiffUtil to do as less addView/removeView as possible. 214 val layout = root.requireViewById<ViewGroup>(tilesId) 215 layout.removeAllViews() 216 layout.outlineProvider = ViewOutlineProvider 217 218 val tilesListView = root.requireViewById<LinearLayout>(tilesListId) 219 if (tiles.isEmpty()) { 220 tilesListView.visibility = View.GONE 221 return 222 } 223 tilesListView.visibility = View.VISIBLE 224 225 // Add each tile. 226 tiles.forEachIndexed { i, tile -> 227 val tileView = 228 PeopleSpaceTileView(root.context, layout, tile.key.shortcutId, i == tiles.size - 1) 229 bindTileView(tileView, tile, onTileClicked) 230 } 231 } 232 233 /** Sets [tileView] with the data in [conversation]. */ 234 private fun bindTileView( 235 tileView: PeopleSpaceTileView, 236 tile: PeopleTileViewModel, 237 onTileClicked: (PeopleTileViewModel) -> Unit, 238 ) { 239 try { 240 tileView.setName(tile.username) 241 tileView.setPersonIcon(tile.icon) 242 tileView.setOnClickListener { onTileClicked(tile) } 243 } catch (e: Exception) { 244 Log.e(TAG, "Couldn't retrieve shortcut information", e) 245 } 246 } 247 } 248