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.Intent
22 import android.content.pm.PackageManager
23 import android.content.res.Resources
24 import android.util.Log
25 import androidx.activity.result.ActivityResultLauncher
26 import com.android.internal.logging.UiEventLogger
27 import com.android.systemui.Flags.enableWidgetPickerSizeFilter
28 import com.android.systemui.communal.domain.interactor.CommunalInteractor
29 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
30 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
31 import com.android.systemui.communal.domain.model.CommunalContentModel
32 import com.android.systemui.communal.shared.log.CommunalUiEvent
33 import com.android.systemui.communal.shared.model.EditModeState
34 import com.android.systemui.dagger.SysUISingleton
35 import com.android.systemui.dagger.qualifiers.Background
36 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
37 import com.android.systemui.keyguard.shared.model.KeyguardState
38 import com.android.systemui.log.LogBuffer
39 import com.android.systemui.log.core.Logger
40 import com.android.systemui.log.dagger.CommunalLog
41 import com.android.systemui.media.controls.ui.view.MediaHost
42 import com.android.systemui.media.dagger.MediaModule
43 import com.android.systemui.res.R
44 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
45 import javax.inject.Inject
46 import javax.inject.Named
47 import kotlinx.coroutines.CoroutineDispatcher
48 import kotlinx.coroutines.flow.Flow
49 import kotlinx.coroutines.flow.MutableStateFlow
50 import kotlinx.coroutines.flow.StateFlow
51 import kotlinx.coroutines.flow.filter
52 import kotlinx.coroutines.flow.first
53 import kotlinx.coroutines.flow.map
54 import kotlinx.coroutines.flow.onEach
55 import kotlinx.coroutines.withContext
56 
57 /** The view model for communal hub in edit mode. */
58 @SysUISingleton
59 class CommunalEditModeViewModel
60 @Inject
61 constructor(
62     communalSceneInteractor: CommunalSceneInteractor,
63     private val communalInteractor: CommunalInteractor,
64     private val communalSettingsInteractor: CommunalSettingsInteractor,
65     keyguardTransitionInteractor: KeyguardTransitionInteractor,
66     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
67     private val uiEventLogger: UiEventLogger,
68     @CommunalLog logBuffer: LogBuffer,
69     @Background private val backgroundDispatcher: CoroutineDispatcher,
70 ) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) {
71 
72     private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
73 
74     override val isEditMode = true
75 
76     override val isCommunalContentVisible: Flow<Boolean> =
77         communalSceneInteractor.editModeState.map { it == EditModeState.SHOWING }
78 
79     /**
80      * Emits when edit mode activity can show, after we've transitioned to [KeyguardState.GONE]
81      * and edit mode is open.
82      */
83     val canShowEditMode =
84         allOf(
85                 keyguardTransitionInteractor.isFinishedInState(KeyguardState.GONE),
86                 communalInteractor.editModeOpen
87             )
88             .filter { it }
89 
90     // Only widgets are editable.
91     override val communalContent: Flow<List<CommunalContentModel>> =
92         communalInteractor.widgetContent.onEach { models ->
93             logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
94         }
95 
96     private val _reorderingWidgets = MutableStateFlow(false)
97 
98     override val reorderingWidgets: StateFlow<Boolean>
99         get() = _reorderingWidgets
100 
101     override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
102 
103     override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) =
104         communalInteractor.updateWidgetOrder(widgetIdToPriorityMap)
105 
106     override fun onReorderWidgetStart() {
107         // Clear selection status
108         setSelectedKey(null)
109         _reorderingWidgets.value = true
110         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START)
111     }
112 
113     override fun onReorderWidgetEnd() {
114         _reorderingWidgets.value = false
115         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH)
116     }
117 
118     override fun onReorderWidgetCancel() {
119         _reorderingWidgets.value = false
120         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
121     }
122 
123     val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal
124 
125     /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */
126     suspend fun onOpenWidgetPicker(
127         resources: Resources,
128         packageManager: PackageManager,
129         activityLauncher: ActivityResultLauncher<Intent>
130     ): Boolean =
131         withContext(backgroundDispatcher) {
132             val widgets = communalInteractor.widgetContent.first()
133             val excludeList =
134                 widgets.filterIsInstance<CommunalContentModel.WidgetContent.Widget>().mapTo(
135                     ArrayList()
136                 ) {
137                     it.providerInfo
138                 }
139             getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let {
140                 try {
141                     activityLauncher.launch(it)
142                     return@withContext true
143                 } catch (e: Exception) {
144                     Log.e(TAG, "Failed to launch widget picker activity", e)
145                 }
146             }
147             false
148         }
149 
150     private fun getWidgetPickerActivityIntent(
151         resources: Resources,
152         packageManager: PackageManager,
153         excludeList: ArrayList<AppWidgetProviderInfo>
154     ): Intent? {
155         val packageName =
156             getLauncherPackageName(packageManager)
157                 ?: run {
158                     Log.e(TAG, "Couldn't resolve launcher package name")
159                     return@getWidgetPickerActivityIntent null
160                 }
161 
162         return Intent(Intent.ACTION_PICK).apply {
163             setPackage(packageName)
164             if (enableWidgetPickerSizeFilter()) {
165                 putExtra(
166                     EXTRA_DESIRED_WIDGET_WIDTH,
167                     resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width)
168                 )
169                 putExtra(
170                     EXTRA_DESIRED_WIDGET_HEIGHT,
171                     resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height)
172                 )
173             }
174             putExtra(
175                 AppWidgetManager.EXTRA_CATEGORY_FILTER,
176                 communalSettingsInteractor.communalWidgetCategories.value
177             )
178             putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE)
179             putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList)
180         }
181     }
182 
183     private fun getLauncherPackageName(packageManager: PackageManager): String? {
184         return packageManager
185             .resolveActivity(
186                 Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) },
187                 PackageManager.MATCH_DEFAULT_ONLY
188             )
189             ?.activityInfo
190             ?.packageName
191     }
192 
193     /** Sets whether edit mode is currently open */
194     fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen)
195 
196     /** Called when exiting the edit mode, before transitioning back to the communal scene. */
197     fun cleanupEditModeState() {
198         communalSceneInteractor.setEditModeState(null)
199     }
200 
201     companion object {
202         private const val TAG = "CommunalEditModeViewModel"
203 
204         private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
205         private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
206         private const val EXTRA_UI_SURFACE_KEY = "ui_surface"
207         private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub"
208         const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets"
209     }
210 }
211