1 /* 2 * 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 114 private fun setNoConversationsContent(view: ViewGroup, onGotItClicked: () -> Unit) { 115 // This should never happen. 116 if (view.childCount > 1) { 117 error("view has ${view.childCount} children, it should have maximum 1") 118 } 119 120 // The static content for no conversations is already shown. 121 if (view.findViewById<View>(R.id.top_level_no_conversations) != null) { 122 return 123 } 124 125 // If we were showing the content with conversations earlier, remove it. 126 if (view.childCount == 1) { 127 view.removeViewAt(0) 128 } 129 130 val context = view.context 131 val noConversationsView = 132 LayoutInflater.from(context) 133 .inflate(R.layout.people_space_activity_no_conversations, /* root= */ view) 134 135 noConversationsView.requireViewById<View>(R.id.got_it_button).setOnClickListener { 136 onGotItClicked() 137 } 138 139 // The Tile preview has colorBackground as its background. Change it so it's different than 140 // the activity's background. 141 val item = noConversationsView.requireViewById<LinearLayout>(android.R.id.background) 142 val shape = item.background as GradientDrawable 143 val ta = 144 context.theme.obtainStyledAttributes( 145 intArrayOf(com.android.internal.R.attr.colorSurface) 146 ) 147 shape.setColor(ta.getColor(0, Color.WHITE)) 148 ta.recycle() 149 } 150 151 private fun setConversationsContent( 152 view: ViewGroup, 153 priorityTiles: List<PeopleTileViewModel>, 154 recentTiles: List<PeopleTileViewModel>, 155 onTileClicked: (PeopleTileViewModel) -> Unit, 156 ) { 157 // This should never happen. 158 if (view.childCount > 1) { 159 error("view has ${view.childCount} children, it should have maximum 1") 160 } 161 162 // Inflate the content with conversations, if it's not already. 163 if (view.findViewById<View>(R.id.top_level_with_conversations) == null) { 164 // If we were showing the content without conversations earlier, remove it. 165 if (view.childCount == 1) { 166 view.removeViewAt(0) 167 } 168 169 LayoutInflater.from(view.context) 170 .inflate(R.layout.people_space_activity_with_conversations, /* root= */ view) 171 } 172 173 // TODO(b/193782241): Replace the NestedScrollView + 2x LinearLayout from this layout into a 174 // single RecyclerView once this screen is tested by screenshot tests. Introduce a 175 // PeopleSpaceTileViewBinder that will properly create and bind the View associated to a 176 // PeopleSpaceTileViewModel (and remove the PeopleSpaceTileView class). 177 val conversationsView = view.requireViewById<View>(R.id.top_level_with_conversations) 178 setTileViews( 179 conversationsView, 180 R.id.priority, 181 R.id.priority_tiles, 182 priorityTiles, 183 onTileClicked, 184 ) 185 186 setTileViews( 187 conversationsView, 188 R.id.recent, 189 R.id.recent_tiles, 190 recentTiles, 191 onTileClicked, 192 ) 193 } 194 195 /** Sets a [PeopleSpaceTileView]s for each conversation. */ 196 private fun setTileViews( 197 root: View, 198 tilesListId: Int, 199 tilesId: Int, 200 tiles: List<PeopleTileViewModel>, 201 onTileClicked: (PeopleTileViewModel) -> Unit, 202 ) { 203 // Remove any previously added tile. 204 // TODO(b/193782241): Once this list is a big RecyclerView, set the current list and use 205 // DiffUtil to do as less addView/removeView as possible. 206 val layout = root.requireViewById<ViewGroup>(tilesId) 207 layout.removeAllViews() 208 layout.outlineProvider = ViewOutlineProvider 209 210 val tilesListView = root.requireViewById<LinearLayout>(tilesListId) 211 if (tiles.isEmpty()) { 212 tilesListView.visibility = View.GONE 213 return 214 } 215 tilesListView.visibility = View.VISIBLE 216 217 // Add each tile. 218 tiles.forEachIndexed { i, tile -> 219 val tileView = 220 PeopleSpaceTileView(root.context, layout, tile.key.shortcutId, i == tiles.size - 1) 221 bindTileView(tileView, tile, onTileClicked) 222 } 223 } 224 225 /** Sets [tileView] with the data in [conversation]. */ 226 private fun bindTileView( 227 tileView: PeopleSpaceTileView, 228 tile: PeopleTileViewModel, 229 onTileClicked: (PeopleTileViewModel) -> Unit, 230 ) { 231 try { 232 tileView.setName(tile.username) 233 tileView.setPersonIcon(tile.icon) 234 tileView.setOnClickListener { onTileClicked(tile) } 235 } catch (e: Exception) { 236 Log.e(TAG, "Couldn't retrieve shortcut information", e) 237 } 238 } 239 } 240