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