• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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.communal.ui.viewmodel
18 
19 import android.appwidget.AppWidgetManager
20 import android.appwidget.AppWidgetProviderInfo
21 import android.content.ComponentName
22 import android.content.Context
23 import android.content.Intent
24 import android.content.pm.PackageManager
25 import android.content.res.Resources
26 import android.os.UserHandle
27 import android.util.Log
28 import android.view.accessibility.AccessibilityEvent
29 import android.view.accessibility.AccessibilityManager
30 import com.android.internal.logging.UiEventLogger
31 import com.android.systemui.communal.dagger.CommunalModule.Companion.LAUNCHER_PACKAGE
32 import com.android.systemui.communal.data.model.CommunalWidgetCategories
33 import com.android.systemui.communal.domain.interactor.CommunalInteractor
34 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
35 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
36 import com.android.systemui.communal.domain.model.CommunalContentModel
37 import com.android.systemui.communal.shared.log.CommunalMetricsLogger
38 import com.android.systemui.communal.shared.log.CommunalUiEvent
39 import com.android.systemui.communal.shared.model.EditModeState
40 import com.android.systemui.communal.widgets.WidgetConfigurator
41 import com.android.systemui.dagger.SysUISingleton
42 import com.android.systemui.dagger.qualifiers.Application
43 import com.android.systemui.dagger.qualifiers.Background
44 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
45 import com.android.systemui.keyguard.shared.model.KeyguardState
46 import com.android.systemui.log.LogBuffer
47 import com.android.systemui.log.core.Logger
48 import com.android.systemui.log.dagger.CommunalLog
49 import com.android.systemui.media.controls.ui.controller.MediaCarouselController
50 import com.android.systemui.media.controls.ui.view.MediaHost
51 import com.android.systemui.media.dagger.MediaModule
52 import com.android.systemui.res.R
53 import com.android.systemui.scene.shared.model.Scenes
54 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
55 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
56 import javax.inject.Inject
57 import javax.inject.Named
58 import kotlinx.coroutines.CoroutineDispatcher
59 import kotlinx.coroutines.flow.Flow
60 import kotlinx.coroutines.flow.MutableStateFlow
61 import kotlinx.coroutines.flow.StateFlow
62 import kotlinx.coroutines.flow.filter
63 import kotlinx.coroutines.flow.first
64 import kotlinx.coroutines.flow.map
65 import kotlinx.coroutines.flow.onEach
66 import kotlinx.coroutines.withContext
67 
68 /** The view model for communal hub in edit mode. */
69 @SysUISingleton
70 class CommunalEditModeViewModel
71 @Inject
72 constructor(
73     communalSceneInteractor: CommunalSceneInteractor,
74     private val communalInteractor: CommunalInteractor,
75     private val communalSettingsInteractor: CommunalSettingsInteractor,
76     keyguardTransitionInteractor: KeyguardTransitionInteractor,
77     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
78     private val uiEventLogger: UiEventLogger,
79     @CommunalLog logBuffer: LogBuffer,
80     @Background private val backgroundDispatcher: CoroutineDispatcher,
81     private val metricsLogger: CommunalMetricsLogger,
82     @Application private val context: Context,
83     private val accessibilityManager: AccessibilityManager,
84     private val packageManager: PackageManager,
85     @Named(LAUNCHER_PACKAGE) private val launcherPackage: String,
86     mediaCarouselController: MediaCarouselController,
87 ) :
88     BaseCommunalViewModel(
89         communalSceneInteractor,
90         communalInteractor,
91         mediaHost,
92         mediaCarouselController,
93     ) {
94 
95     private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
96 
97     override val isEditMode = true
98 
99     override val isCommunalContentVisible: Flow<Boolean> =
100         communalSceneInteractor.editModeState.map { it == EditModeState.SHOWING }
101 
102     val showDisclaimer: Flow<Boolean> =
103         allOf(isCommunalContentVisible, not(communalInteractor.isDisclaimerDismissed))
104 
105     fun onDisclaimerDismissed() {
106         communalInteractor.setDisclaimerDismissed()
107     }
108 
109     /**
110      * Emits when edit mode activity can show, after we've transitioned to [KeyguardState.GONE] and
111      * edit mode is open.
112      */
113     val canShowEditMode =
114         allOf(
115                 keyguardTransitionInteractor.isFinishedIn(
116                     content = Scenes.Gone,
117                     stateWithoutSceneContainer = KeyguardState.GONE,
118                 ),
119                 communalInteractor.editModeOpen,
120             )
121             .filter { it }
122 
123     // Only widgets are editable.
124     override val communalContent: Flow<List<CommunalContentModel>> =
125         communalInteractor.widgetContent.onEach { models ->
126             logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
127         }
128 
129     private val _reorderingWidgets = MutableStateFlow(false)
130 
131     override val reorderingWidgets: StateFlow<Boolean>
132         get() = _reorderingWidgets
133 
134     override fun onAddWidget(
135         componentName: ComponentName,
136         user: UserHandle,
137         rank: Int?,
138         configurator: WidgetConfigurator?,
139     ) {
140         communalInteractor.addWidget(componentName, user, rank, configurator)
141         metricsLogger.logAddWidget(componentName.flattenToString(), rank)
142     }
143 
144     override fun onDeleteWidget(id: Int, key: String, componentName: ComponentName, rank: Int) {
145         if (selectedKey.value == key) {
146             setSelectedKey(null)
147         }
148         communalInteractor.deleteWidget(id)
149         metricsLogger.logRemoveWidget(componentName.flattenToString(), rank)
150     }
151 
152     override fun onReorderWidgets(widgetIdToRankMap: Map<Int, Int>) =
153         communalInteractor.updateWidgetOrder(widgetIdToRankMap)
154 
155     override fun onResizeWidget(
156         appWidgetId: Int,
157         spanY: Int,
158         widgetIdToRankMap: Map<Int, Int>,
159         componentName: ComponentName,
160         rank: Int,
161     ) {
162         communalInteractor.resizeWidget(appWidgetId, spanY, widgetIdToRankMap)
163         metricsLogger.logResizeWidget(
164             componentName = componentName.flattenToString(),
165             rank = rank,
166             spanY = spanY,
167         )
168     }
169 
170     override fun onReorderWidgetStart(draggingItemKey: String) {
171         setSelectedKey(draggingItemKey)
172         _reorderingWidgets.value = true
173         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START)
174     }
175 
176     override fun onReorderWidgetEnd() {
177         _reorderingWidgets.value = false
178         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH)
179     }
180 
181     override fun onReorderWidgetCancel() {
182         _reorderingWidgets.value = false
183         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
184     }
185 
186     override fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {
187         if (!accessibilityManager.isEnabled) {
188             return
189         }
190 
191         // Send an accessibility announcement for the newly added widget
192         val widgetLabel = provider.loadLabel(packageManager)
193         val announcementText =
194             context.getString(
195                 R.string.accessibility_announcement_communal_widget_added,
196                 widgetLabel,
197             )
198         accessibilityManager.sendAccessibilityEvent(
199             AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
200                 contentDescription = announcementText
201             }
202         )
203     }
204 
205     val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal
206 
207     /** Launch the widget picker activity using the given startActivity method. */
208     suspend fun onOpenWidgetPicker(
209         resources: Resources,
210         startActivity: (intent: Intent) -> Unit,
211     ): Boolean =
212         withContext(backgroundDispatcher) {
213             val widgets = communalInteractor.widgetContent.first()
214             val excludeList =
215                 widgets.filterIsInstance<CommunalContentModel.WidgetContent.Widget>().mapTo(
216                     ArrayList()
217                 ) {
218                     it.providerInfo
219                 }
220             getWidgetPickerActivityIntent(resources, excludeList)?.let {
221                 try {
222                     startActivity(it)
223                     return@withContext true
224                 } catch (e: Exception) {
225                     Log.e(TAG, "Failed to launch widget picker activity", e)
226                 }
227             }
228             false
229         }
230 
231     private fun getWidgetPickerActivityIntent(
232         resources: Resources,
233         excludeList: ArrayList<AppWidgetProviderInfo>,
234     ): Intent? {
235         return Intent(Intent.ACTION_PICK).apply {
236             setPackage(launcherPackage)
237             putExtra(
238                 EXTRA_DESIRED_WIDGET_WIDTH,
239                 resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width),
240             )
241             putExtra(
242                 EXTRA_DESIRED_WIDGET_HEIGHT,
243                 resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height),
244             )
245             putExtra(
246                 AppWidgetManager.EXTRA_CATEGORY_FILTER,
247                 CommunalWidgetCategories.includedCategories,
248             )
249             putExtra(EXTRA_CATEGORY_EXCLUSION_FILTER, CommunalWidgetCategories.excludedCategories)
250 
251             communalSettingsInteractor.workProfileUserDisallowedByDevicePolicy.value?.let {
252                 putExtra(EXTRA_USER_ID_FILTER, arrayListOf(it.id))
253             }
254             putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE)
255             putExtra(EXTRA_PICKER_TITLE, resources.getString(R.string.communal_widget_picker_title))
256             putExtra(
257                 EXTRA_PICKER_DESCRIPTION,
258                 resources.getString(R.string.communal_widget_picker_description),
259             )
260             putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList)
261         }
262     }
263 
264     /** Sets whether edit mode is currently open */
265     fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen)
266 
267     /**
268      * Sets whether the edit mode activity is currently showing.
269      *
270      * See [CommunalInteractor.editActivityShowing] for more info.
271      */
272     fun setEditActivityShowing(showing: Boolean) =
273         communalInteractor.setEditActivityShowing(showing)
274 
275     /** Called when exiting the edit mode, before transitioning back to the communal scene. */
276     fun cleanupEditModeState() {
277         communalSceneInteractor.setEditModeState(null)
278 
279         // Set the scroll position of the glanceable hub to match where we are now.
280         persistScrollPosition()
281     }
282 
283     companion object {
284         private const val TAG = "CommunalEditModeViewModel"
285 
286         private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
287         private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
288         private const val EXTRA_CATEGORY_EXCLUSION_FILTER = "category_exclusion_filter"
289         private const val EXTRA_PICKER_TITLE = "picker_title"
290         private const val EXTRA_PICKER_DESCRIPTION = "picker_description"
291         private const val EXTRA_UI_SURFACE_KEY = "ui_surface"
292         private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub"
293         private const val EXTRA_USER_ID_FILTER = "filtered_user_ids"
294         const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets"
295     }
296 }
297