blob: d8a429e5bb1a1adbc962b31ede3239a7950a8a85 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.people.ui.view
import android.content.Context
import android.graphics.Color
import android.graphics.Outline
import android.graphics.drawable.GradientDrawable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.widget.LinearLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.State.CREATED
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R
import com.android.systemui.people.PeopleSpaceTileView
import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
/** A ViewBinder for [PeopleViewModel]. */
object PeopleViewBinder {
private const val TAG = "PeopleViewBinder"
/**
* The [ViewOutlineProvider] used to clip the corner radius of the recent and priority lists.
*/
private val ViewOutlineProvider =
object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
0,
0,
view.width,
view.height,
view.context.resources.getDimension(R.dimen.people_space_widget_radius),
)
}
}
/** Create a [View] that can later be [bound][bind] to a [PeopleViewModel]. */
@JvmStatic
fun create(context: Context): ViewGroup {
return LayoutInflater.from(context)
.inflate(R.layout.people_space_activity, /* root= */ null) as ViewGroup
}
/** Bind [view] to [viewModel]. */
@JvmStatic
fun bind(
view: ViewGroup,
viewModel: PeopleViewModel,
lifecycleOwner: LifecycleOwner,
onResult: (PeopleViewModel.Result) -> Unit,
) {
// Call [onResult] as soon as a result is available.
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(CREATED) {
viewModel.result.collect { result ->
if (result != null) {
viewModel.clearResult()
onResult(result)
}
}
}
}
// Start collecting the UI data once the Activity is STARTED.
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
combine(
viewModel.priorityTiles,
viewModel.recentTiles,
) { priority, recent ->
priority to recent
}
.collect { (priorityTiles, recentTiles) ->
if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
setConversationsContent(
view,
priorityTiles,
recentTiles,
viewModel::onTileClicked,
)
} else {
setNoConversationsContent(view, viewModel::onUserJourneyCancelled)
}
}
}
}
// Make sure to refresh the tiles/conversations when the Activity is resumed, so that it
// updates them when going back to the Activity after leaving it.
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.onTileRefreshRequested()
}
}
}
private fun setNoConversationsContent(view: ViewGroup, onGotItClicked: () -> Unit) {
// This should never happen.
if (view.childCount > 1) {
error("view has ${view.childCount} children, it should have maximum 1")
}
// The static content for no conversations is already shown.
if (view.findViewById<View>(R.id.top_level_no_conversations) != null) {
return
}
// If we were showing the content with conversations earlier, remove it.
if (view.childCount == 1) {
view.removeViewAt(0)
}
val context = view.context
val noConversationsView =
LayoutInflater.from(context)
.inflate(R.layout.people_space_activity_no_conversations, /* root= */ view)
noConversationsView.findViewById<View>(R.id.got_it_button).setOnClickListener {
onGotItClicked()
}
// The Tile preview has colorBackground as its background. Change it so it's different than
// the activity's background.
val item = noConversationsView.findViewById<LinearLayout>(android.R.id.background)
val shape = item.background as GradientDrawable
val ta =
context.theme.obtainStyledAttributes(
intArrayOf(com.android.internal.R.attr.colorSurface)
)
shape.setColor(ta.getColor(0, Color.WHITE))
ta.recycle()
}
private fun setConversationsContent(
view: ViewGroup,
priorityTiles: List<PeopleTileViewModel>,
recentTiles: List<PeopleTileViewModel>,
onTileClicked: (PeopleTileViewModel) -> Unit,
) {
// This should never happen.
if (view.childCount > 1) {
error("view has ${view.childCount} children, it should have maximum 1")
}
// Inflate the content with conversations, if it's not already.
if (view.findViewById<View>(R.id.top_level_with_conversations) == null) {
// If we were showing the content without conversations earlier, remove it.
if (view.childCount == 1) {
view.removeViewAt(0)
}
LayoutInflater.from(view.context)
.inflate(R.layout.people_space_activity_with_conversations, /* root= */ view)
}
// TODO(b/193782241): Replace the NestedScrollView + 2x LinearLayout from this layout into a
// single RecyclerView once this screen is tested by screenshot tests. Introduce a
// PeopleSpaceTileViewBinder that will properly create and bind the View associated to a
// PeopleSpaceTileViewModel (and remove the PeopleSpaceTileView class).
val conversationsView = view.requireViewById<View>(R.id.top_level_with_conversations)
setTileViews(
conversationsView,
R.id.priority,
R.id.priority_tiles,
priorityTiles,
onTileClicked,
)
setTileViews(
conversationsView,
R.id.recent,
R.id.recent_tiles,
recentTiles,
onTileClicked,
)
}
/** Sets a [PeopleSpaceTileView]s for each conversation. */
private fun setTileViews(
root: View,
tilesListId: Int,
tilesId: Int,
tiles: List<PeopleTileViewModel>,
onTileClicked: (PeopleTileViewModel) -> Unit,
) {
// Remove any previously added tile.
// TODO(b/193782241): Once this list is a big RecyclerView, set the current list and use
// DiffUtil to do as less addView/removeView as possible.
val layout = root.requireViewById<ViewGroup>(tilesId)
layout.removeAllViews()
layout.outlineProvider = ViewOutlineProvider
val tilesListView = root.requireViewById<LinearLayout>(tilesListId)
if (tiles.isEmpty()) {
tilesListView.visibility = View.GONE
return
}
tilesListView.visibility = View.VISIBLE
// Add each tile.
tiles.forEachIndexed { i, tile ->
val tileView =
PeopleSpaceTileView(root.context, layout, tile.key.shortcutId, i == tiles.size - 1)
bindTileView(tileView, tile, onTileClicked)
}
}
/** Sets [tileView] with the data in [conversation]. */
private fun bindTileView(
tileView: PeopleSpaceTileView,
tile: PeopleTileViewModel,
onTileClicked: (PeopleTileViewModel) -> Unit,
) {
try {
tileView.setName(tile.username)
tileView.setPersonIcon(tile.icon)
tileView.setOnClickListener { onTileClicked(tile) }
} catch (e: Exception) {
Log.e(TAG, "Couldn't retrieve shortcut information", e)
}
}
}