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.viewmodel
18 
19 import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
20 import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
21 import android.content.Context
22 import android.content.Intent
23 import android.util.Log
24 import androidx.lifecycle.ViewModel
25 import androidx.lifecycle.ViewModelProvider
26 import com.android.systemui.R
27 import com.android.systemui.dagger.qualifiers.Application
28 import com.android.systemui.people.PeopleSpaceUtils
29 import com.android.systemui.people.PeopleTileViewHelper
30 import com.android.systemui.people.data.model.PeopleTileModel
31 import com.android.systemui.people.data.repository.PeopleTileRepository
32 import com.android.systemui.people.data.repository.PeopleWidgetRepository
33 import javax.inject.Inject
34 import kotlinx.coroutines.flow.MutableStateFlow
35 import kotlinx.coroutines.flow.StateFlow
36 import kotlinx.coroutines.flow.asStateFlow
37 
38 /**
39  * Models UI state for the people space, allowing the user to select which conversation should be
40  * associated to a new or existing Conversation widget.
41  */
42 class PeopleViewModel(
43     @Application private val context: Context,
44     private val tileRepository: PeopleTileRepository,
45     private val widgetRepository: PeopleWidgetRepository,
46 ) : ViewModel() {
47     /**
48      * The list of the priority tiles/conversations.
49      *
50      * Important: Even though this is a Flow, the underlying API used to populate this Flow is not
51      * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
52      */
53     private val _priorityTiles = MutableStateFlow(priorityTiles())
54     val priorityTiles: StateFlow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
55 
56     /**
57      * The list of the priority tiles/conversations.
58      *
59      * Important: Even though this is a Flow, the underlying API used to populate this Flow is not
60      * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
61      */
62     private val _recentTiles = MutableStateFlow(recentTiles())
63     val recentTiles: StateFlow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
64 
65     /** The ID of the widget currently being edited/added. */
66     private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
67     val appWidgetId: StateFlow<Int> = _appWidgetId.asStateFlow()
68 
69     /** The result of this user journey. */
70     private val _result = MutableStateFlow<Result?>(null)
71     val result: StateFlow<Result?> = _result.asStateFlow()
72 
73     /** Refresh the [priorityTiles] and [recentTiles]. */
74     fun onTileRefreshRequested() {
75         _priorityTiles.value = priorityTiles()
76         _recentTiles.value = recentTiles()
77     }
78 
79     /** Called when the [appWidgetId] should be changed to [widgetId]. */
80     fun onWidgetIdChanged(widgetId: Int) {
81         _appWidgetId.value = widgetId
82     }
83 
84     /** Clear [result], setting it to null. */
85     fun clearResult() {
86         _result.value = null
87     }
88 
89     /** Called when a tile is clicked. */
90     fun onTileClicked(tile: PeopleTileViewModel) {
91         val widgetId = _appWidgetId.value
92         if (PeopleSpaceUtils.DEBUG) {
93             Log.d(
94                 TAG,
95                 "Put ${tile.username}'s shortcut ID: ${tile.key.shortcutId} for widget ID $widgetId"
96             )
97         }
98         widgetRepository.setWidgetTile(widgetId, tile.key)
99         _result.value =
100             Result.Success(Intent().apply { putExtra(EXTRA_APPWIDGET_ID, appWidgetId.value) })
101     }
102 
103     /** Called when this user journey is cancelled. */
104     fun onUserJourneyCancelled() {
105         _result.value = Result.Cancelled
106     }
107 
108     private fun priorityTiles(): List<PeopleTileViewModel> {
109         return try {
110             tileRepository.priorityTiles().map { it.toViewModel() }
111         } catch (e: Exception) {
112             Log.e(TAG, "Couldn't retrieve priority conversations", e)
113             emptyList()
114         }
115     }
116 
117     private fun recentTiles(): List<PeopleTileViewModel> {
118         return try {
119             tileRepository.recentTiles().map { it.toViewModel() }
120         } catch (e: Exception) {
121             Log.e(TAG, "Couldn't retrieve recent conversations", e)
122             emptyList()
123         }
124     }
125 
126     private fun PeopleTileModel.toViewModel(): PeopleTileViewModel {
127         val icon =
128             PeopleTileViewHelper.getPersonIconBitmap(
129                 context,
130                 this,
131                 PeopleTileViewHelper.getSizeInDp(
132                     context,
133                     R.dimen.avatar_size_for_medium,
134                     context.resources.displayMetrics.density,
135                 )
136             )
137         return PeopleTileViewModel(key, icon, username)
138     }
139 
140     /** The Factory that should be used to create a [PeopleViewModel]. */
141     class Factory
142     @Inject
143     constructor(
144         @Application private val context: Context,
145         private val tileRepository: PeopleTileRepository,
146         private val widgetRepository: PeopleWidgetRepository,
147     ) : ViewModelProvider.Factory {
148         override fun <T : ViewModel> create(modelClass: Class<T>): T {
149             check(modelClass == PeopleViewModel::class.java)
150             return PeopleViewModel(context, tileRepository, widgetRepository) as T
151         }
152     }
153 
154     sealed class Result {
155         class Success(val data: Intent) : Result()
156         object Cancelled : Result()
157     }
158 
159     companion object {
160         private const val TAG = "PeopleViewModel"
161     }
162 }
163