• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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