blob: 83c34ce72588faac19a640f5403ff4e94206ca5d [file] [log] [blame]
Sebastian Francobd7919c2023-09-19 10:55:37 -07001package com.android.launcher3
2
Sebastian Franco0b461ef2023-12-08 12:20:07 -06003import android.annotation.TargetApi
4import android.os.Build
5import android.os.Trace
Sihua Maa44f4ac2024-06-03 18:35:39 +00006import android.util.Log
Sebastian Francobd7919c2023-09-19 10:55:37 -07007import androidx.annotation.UiThread
fbaron9d08e0f2024-04-24 11:26:01 -07008import com.android.launcher3.Flags.enableSmartspaceRemovalToggle
Sebastian Franco0b461ef2023-12-08 12:20:07 -06009import com.android.launcher3.LauncherConstants.TraceEvents
fbaron9d08e0f2024-04-24 11:26:01 -070010import com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET
Sebastian Franco6dda7c72023-11-17 18:03:00 -060011import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID
Sebastian Francoe4965122023-12-01 13:27:42 -060012import com.android.launcher3.allapps.AllAppsStore
Sebastian Franco6dda7c72023-11-17 18:03:00 -060013import com.android.launcher3.config.FeatureFlags
Sebastian Franco224f67a2024-04-18 18:40:40 -070014import com.android.launcher3.debug.TestEvent
15import com.android.launcher3.debug.TestEventEmitter
Sebastian Francobd7919c2023-09-19 10:55:37 -070016import com.android.launcher3.model.BgDataModel
Sebastian Franco6dda7c72023-11-17 18:03:00 -060017import com.android.launcher3.model.StringCache
Sebastian Francobd7919c2023-09-19 10:55:37 -070018import com.android.launcher3.model.data.AppInfo
19import com.android.launcher3.model.data.ItemInfo
20import com.android.launcher3.model.data.LauncherAppWidgetInfo
21import com.android.launcher3.model.data.WorkspaceItemInfo
22import com.android.launcher3.popup.PopupContainerWithArrow
23import com.android.launcher3.util.ComponentKey
Sebastian Franco69fd7422023-10-06 16:48:58 -070024import com.android.launcher3.util.IntArray as LIntArray
25import com.android.launcher3.util.IntSet as LIntSet
Sebastian Francobd7919c2023-09-19 10:55:37 -070026import com.android.launcher3.util.PackageUserKey
27import com.android.launcher3.util.Preconditions
Sebastian Franco0b461ef2023-12-08 12:20:07 -060028import com.android.launcher3.util.RunnableList
Sebastian Francoe4965122023-12-01 13:27:42 -060029import com.android.launcher3.util.TraceHelper
30import com.android.launcher3.util.ViewOnDrawExecutor
Sebastian Franco50b74c32023-11-16 11:36:49 -060031import com.android.launcher3.widget.PendingAddWidgetInfo
Sebastian Francobd7919c2023-09-19 10:55:37 -070032import com.android.launcher3.widget.model.WidgetsListBaseEntry
33import java.util.function.Predicate
34
Sihua Maa44f4ac2024-06-03 18:35:39 +000035private const val TAG = "ModelCallbacks"
36
Sebastian Francobd7919c2023-09-19 10:55:37 -070037class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks {
Sebastian Franco69fd7422023-10-06 16:48:58 -070038
39 var synchronouslyBoundPages = LIntSet()
40 var pagesToBindSynchronously = LIntSet()
41
Sebastian Francoe4965122023-12-01 13:27:42 -060042 private var isFirstPagePinnedItemEnabled =
fbaron9d08e0f2024-04-24 11:26:01 -070043 (BuildConfig.QSB_ON_FIRST_SCREEN && !enableSmartspaceRemovalToggle())
Sebastian Franco6dda7c72023-11-17 18:03:00 -060044
45 var stringCache: StringCache? = null
46
Sebastian Francoe4965122023-12-01 13:27:42 -060047 var pendingExecutor: ViewOnDrawExecutor? = null
48
49 var workspaceLoading = true
50
51 /**
52 * Refreshes the shortcuts shown on the workspace.
53 *
54 * Implementation of the method from LauncherModel.Callbacks.
55 */
56 override fun startBinding() {
57 TraceHelper.INSTANCE.beginSection("startBinding")
58 // Floating panels (except the full widget sheet) are associated with individual icons. If
59 // we are starting a fresh bind, close all such panels as all the icons are about
60 // to go away.
61 AbstractFloatingView.closeOpenViews(
62 launcher,
63 true,
64 AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv()
65 )
66 workspaceLoading = true
67
68 // Clear the workspace because it's going to be rebound
69 launcher.dragController.cancelDrag()
70 launcher.workspace.clearDropTargets()
71 launcher.workspace.removeAllWorkspaceScreens()
Sihua Maf418b2e2024-03-13 12:17:27 -070072 // Avoid clearing the widget update listeners for staying up-to-date with widget info
73 launcher.appWidgetHolder.clearWidgetViews()
Sihua Maa44f4ac2024-06-03 18:35:39 +000074 // TODO(b/335141365): Remove this log after the bug is fixed.
75 Log.d(
76 TAG,
77 "startBinding: " +
78 "hotseat layout was vertical: ${launcher.hotseat?.isHasVerticalHotseat}" +
79 " and is setting to ${launcher.deviceProfile.isVerticalBarLayout}"
80 )
Sebastian Francoe4965122023-12-01 13:27:42 -060081 launcher.hotseat?.resetLayout(launcher.deviceProfile.isVerticalBarLayout)
82 TraceHelper.INSTANCE.endSection()
83 }
84
Sebastian Franco0b461ef2023-12-08 12:20:07 -060085 @TargetApi(Build.VERSION_CODES.S)
86 override fun onInitialBindComplete(
87 boundPages: LIntSet,
88 pendingTasks: RunnableList,
Sunny Goyal72a74662024-02-14 12:32:59 -080089 onCompleteSignal: RunnableList,
Sebastian Franco0b461ef2023-12-08 12:20:07 -060090 workspaceItemCount: Int,
91 isBindSync: Boolean
92 ) {
Sunny Goyale337a802024-02-14 15:07:26 -080093 if (Utilities.ATLEAST_S) {
94 Trace.endAsyncSection(
95 TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME,
96 TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE
97 )
98 }
Sebastian Franco0b461ef2023-12-08 12:20:07 -060099 synchronouslyBoundPages = boundPages
100 pagesToBindSynchronously = LIntSet()
101 clearPendingBinds()
Sebastian Franco0b461ef2023-12-08 12:20:07 -0600102 if (!launcher.isInState(LauncherState.ALL_APPS)) {
103 launcher.appsView.appsStore.enableDeferUpdates(AllAppsStore.DEFER_UPDATES_NEXT_DRAW)
104 pendingTasks.add {
105 launcher.appsView.appsStore.disableDeferUpdates(
106 AllAppsStore.DEFER_UPDATES_NEXT_DRAW
107 )
108 }
109 }
Sunny Goyale337a802024-02-14 15:07:26 -0800110 val executor =
111 ViewOnDrawExecutor(pendingTasks) {
112 if (pendingExecutor == it) {
113 pendingExecutor = null
Sebastian Franco0b461ef2023-12-08 12:20:07 -0600114 }
115 }
Sunny Goyale337a802024-02-14 15:07:26 -0800116 pendingExecutor = executor
Sunny Goyal72a74662024-02-14 12:32:59 -0800117
118 if (Flags.enableWorkspaceInflation()) {
119 // Finish the executor as soon as the pending inflation is completed
120 onCompleteSignal.add(executor::markCompleted)
121 } else {
122 // Pending executor is already completed, wait until first draw to run the tasks
123 executor.attachTo(launcher)
124 }
Sunny Goyale337a802024-02-14 15:07:26 -0800125 launcher.bindComplete(workspaceItemCount, isBindSync)
Sebastian Franco0b461ef2023-12-08 12:20:07 -0600126 }
127
Sebastian Francoe4965122023-12-01 13:27:42 -0600128 /**
Sebastian Franco6196b902023-12-06 11:39:52 -0600129 * Callback saying that there aren't any more items to bind.
130 *
131 * Implementation of the method from LauncherModel.Callbacks.
132 */
133 override fun finishBindingItems(pagesBoundFirst: LIntSet?) {
134 TraceHelper.INSTANCE.beginSection("finishBindingItems")
135 val deviceProfile = launcher.deviceProfile
136 launcher.workspace.restoreInstanceStateForRemainingPages()
137 workspaceLoading = false
138 launcher.processActivityResult()
139 val currentPage =
140 if (pagesBoundFirst != null && !pagesBoundFirst.isEmpty)
141 launcher.workspace.getPageIndexForScreenId(pagesBoundFirst.array[0])
142 else PagedView.INVALID_PAGE
143 // When undoing the removal of the last item on a page, return to that page.
144 // Since we are just resetting the current page without user interaction,
145 // override the previous page so we don't log the page switch.
146 launcher.workspace.setCurrentPage(currentPage, currentPage /* overridePrevPage */)
Sebastian Franco0b461ef2023-12-08 12:20:07 -0600147 pagesToBindSynchronously = LIntSet()
Sebastian Franco6196b902023-12-06 11:39:52 -0600148
149 // Cache one page worth of icons
150 launcher.viewCache.setCacheSize(
151 R.layout.folder_application,
Thales Lima1faa4ed2023-12-01 16:48:19 +0000152 deviceProfile.numFolderColumns * deviceProfile.numFolderRows
Sebastian Franco6196b902023-12-06 11:39:52 -0600153 )
154 launcher.viewCache.setCacheSize(R.layout.folder_page, 2)
155 TraceHelper.INSTANCE.endSection()
156 launcher.workspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true)
Sihua Maa44f4ac2024-06-03 18:35:39 +0000157 launcher.workspace.pageIndicator.setPauseScroll(
158 /*pause=*/ false,
159 deviceProfile.isTwoPanels
160 )
Sebastian Franco224f67a2024-04-18 18:40:40 -0700161 TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
Sebastian Franco6196b902023-12-06 11:39:52 -0600162 }
163
164 /**
Sebastian Francoe4965122023-12-01 13:27:42 -0600165 * Clear any pending bind callbacks. This is called when is loader is planning to perform a full
166 * rebind from scratch.
167 */
168 override fun clearPendingBinds() {
169 pendingExecutor?.cancel() ?: return
170 pendingExecutor = null
171
172 // We might have set this flag previously and forgot to clear it.
173 launcher.appsView.appsStore.disableDeferUpdatesSilently(
174 AllAppsStore.DEFER_UPDATES_NEXT_DRAW
175 )
176 }
177
Sebastian Francobd7919c2023-09-19 10:55:37 -0700178 override fun preAddApps() {
179 // If there's an undo snackbar, force it to complete to ensure empty screens are removed
180 // before trying to add new items.
181 launcher.modelWriter.commitDelete()
182 val snackbar =
183 AbstractFloatingView.getOpenView<AbstractFloatingView>(
184 launcher,
185 AbstractFloatingView.TYPE_SNACKBAR
186 )
187 snackbar?.post { snackbar.close(true) }
188 }
189
190 @UiThread
191 override fun bindAllApplications(
192 apps: Array<AppInfo?>?,
193 flags: Int,
194 packageUserKeytoUidMap: Map<PackageUserKey?, Int?>?
195 ) {
196 Preconditions.assertUIThread()
197 val hadWorkApps = launcher.appsView.shouldShowTabs()
198 launcher.appsView.appsStore.setApps(apps, flags, packageUserKeytoUidMap)
199 PopupContainerWithArrow.dismissInvalidPopup(launcher)
Alex Chau61d19382024-02-12 17:36:57 +0000200 if (
201 hadWorkApps != launcher.appsView.shouldShowTabs() &&
202 launcher.stateManager.state == LauncherState.ALL_APPS
203 ) {
Sebastian Francobd7919c2023-09-19 10:55:37 -0700204 launcher.stateManager.goToState(LauncherState.NORMAL)
205 }
206 }
207
208 /**
209 * Copies LauncherModel's map of activities to shortcut counts to Launcher's. This is necessary
210 * because LauncherModel's map is updated in the background, while Launcher runs on the UI.
211 */
212 override fun bindDeepShortcutMap(deepShortcutMapCopy: HashMap<ComponentKey?, Int?>?) {
213 launcher.popupDataProvider.setDeepShortcutMap(deepShortcutMapCopy)
214 }
215
216 override fun bindIncrementalDownloadProgressUpdated(app: AppInfo?) {
217 launcher.appsView.appsStore.updateProgressBar(app)
218 }
219
220 override fun bindWidgetsRestored(widgets: ArrayList<LauncherAppWidgetInfo?>?) {
221 launcher.workspace.widgetsRestored(widgets)
222 }
223
224 /**
225 * Some shortcuts were updated in the background. Implementation of the method from
226 * LauncherModel.Callbacks.
227 *
228 * @param updated list of shortcuts which have changed.
229 */
230 override fun bindWorkspaceItemsChanged(updated: List<WorkspaceItemInfo?>) {
231 if (updated.isNotEmpty()) {
232 launcher.workspace.updateWorkspaceItems(updated, launcher)
233 PopupContainerWithArrow.dismissInvalidPopup(launcher)
234 }
235 }
236
237 /**
238 * Update the state of a package, typically related to install state. Implementation of the
239 * method from LauncherModel.Callbacks.
240 */
241 override fun bindRestoreItemsChange(updates: HashSet<ItemInfo?>?) {
242 launcher.workspace.updateRestoreItems(updates, launcher)
243 }
244
245 /**
246 * A package was uninstalled/updated. We take both the super set of packageNames in addition to
247 * specific applications to remove, the reason being that this can be called when a package is
248 * updated as well. In that scenario, we only remove specific components from the workspace and
249 * hotseat, where as package-removal should clear all items by package name.
250 */
251 override fun bindWorkspaceComponentsRemoved(matcher: Predicate<ItemInfo?>?) {
252 launcher.workspace.removeItemsByMatcher(matcher)
253 launcher.dragController.onAppsRemoved(matcher)
254 PopupContainerWithArrow.dismissInvalidPopup(launcher)
255 }
256
257 override fun bindAllWidgets(allWidgets: List<WidgetsListBaseEntry?>?) {
258 launcher.popupDataProvider.allWidgets = allWidgets
259 }
Sebastian Franco69fd7422023-10-06 16:48:58 -0700260
261 /** Returns the ids of the workspaces to bind. */
262 override fun getPagesToBindSynchronously(orderedScreenIds: LIntArray): LIntSet {
263 // If workspace binding is still in progress, getCurrentPageScreenIds won't be
264 // accurate, and we should use mSynchronouslyBoundPages that's set during initial binding.
265 val visibleIds =
266 when {
267 !pagesToBindSynchronously.isEmpty -> pagesToBindSynchronously
Sebastian Francoe4965122023-12-01 13:27:42 -0600268 !workspaceLoading -> launcher.workspace.currentPageScreenIds
Sebastian Franco69fd7422023-10-06 16:48:58 -0700269 else -> synchronouslyBoundPages
270 }
271 // Launcher IntArray has the same name as Kotlin IntArray
272 val result = LIntSet()
273 if (visibleIds.isEmpty) {
274 return result
275 }
276 val actualIds = orderedScreenIds.clone()
277 val firstId = visibleIds.first()
278 val pairId = launcher.workspace.getScreenPair(firstId)
279 // Double check that actual screenIds contains the visibleId, as empty screens are hidden
280 // in single panel.
281 if (actualIds.contains(firstId)) {
282 result.add(firstId)
283 if (launcher.deviceProfile.isTwoPanels && actualIds.contains(pairId)) {
284 result.add(pairId)
285 }
286 } else if (
287 LauncherAppState.getIDP(launcher).supportedProfiles.any(DeviceProfile::isTwoPanels) &&
288 actualIds.contains(pairId)
289 ) {
290 // Add the right panel if left panel is hidden when switching display, due to empty
291 // pages being hidden in single panel.
292 result.add(pairId)
293 }
294 return result
295 }
Sebastian Franco50b74c32023-11-16 11:36:49 -0600296
297 override fun bindSmartspaceWidget() {
298 val cl: CellLayout? =
299 launcher.workspace.getScreenWithId(WorkspaceLayoutManager.FIRST_SCREEN_ID)
300 val spanX = InvariantDeviceProfile.INSTANCE.get(launcher).numSearchContainerColumns
301
302 if (cl?.isRegionVacant(0, 0, spanX, 1) != true) {
303 return
304 }
305
306 val widgetsListBaseEntry: WidgetsListBaseEntry =
307 launcher.popupDataProvider.allWidgets.firstOrNull { item: WidgetsListBaseEntry ->
308 item.mPkgItem.packageName == BuildConfig.APPLICATION_ID
Sihua Maa44f4ac2024-06-03 18:35:39 +0000309 } ?: return
Sebastian Franco50b74c32023-11-16 11:36:49 -0600310
311 val info =
312 PendingAddWidgetInfo(
313 widgetsListBaseEntry.mWidgets[0].widgetInfo,
314 LauncherSettings.Favorites.CONTAINER_DESKTOP
315 )
316 launcher.addPendingItem(
317 info,
318 info.container,
319 WorkspaceLayoutManager.FIRST_SCREEN_ID,
320 intArrayOf(0, 0),
321 info.spanX,
322 info.spanY
323 )
324 }
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600325
Sebastian Franco6196b902023-12-06 11:39:52 -0600326 override fun bindScreens(orderedScreenIds: LIntArray) {
Shamali Pef4b1022024-02-19 18:05:27 +0000327 launcher.workspace.pageIndicator.setPauseScroll(
328 /*pause=*/ true,
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600329 launcher.deviceProfile.isTwoPanels
330 )
331 val firstScreenPosition = 0
332 if (
Sihua Maa44f4ac2024-06-03 18:35:39 +0000333 (isFirstPagePinnedItemEnabled && !SHOULD_SHOW_FIRST_PAGE_WIDGET) &&
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600334 orderedScreenIds.indexOf(FIRST_SCREEN_ID) != firstScreenPosition
335 ) {
336 orderedScreenIds.removeValue(FIRST_SCREEN_ID)
337 orderedScreenIds.add(firstScreenPosition, FIRST_SCREEN_ID)
338 } else if (
Sihua Maa44f4ac2024-06-03 18:35:39 +0000339 (!isFirstPagePinnedItemEnabled || SHOULD_SHOW_FIRST_PAGE_WIDGET) &&
340 orderedScreenIds.isEmpty
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600341 ) {
342 // If there are no screens, we need to have an empty screen
343 launcher.workspace.addExtraEmptyScreens()
344 }
345 bindAddScreens(orderedScreenIds)
346
347 // After we have added all the screens, if the wallpaper was locked to the default state,
348 // then notify to indicate that it can be released and a proper wallpaper offset can be
349 // computed before the next layout
350 launcher.workspace.unlockWallpaperFromDefaultPageOnNextLayout()
351 }
352
353 override fun bindAppsAdded(
Sebastian Franco6196b902023-12-06 11:39:52 -0600354 newScreens: LIntArray?,
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600355 addNotAnimated: java.util.ArrayList<ItemInfo?>?,
356 addAnimated: java.util.ArrayList<ItemInfo?>?
357 ) {
358 // Add the new screens
359 if (newScreens != null) {
360 // newScreens can contain an empty right panel that is already bound, but not known
361 // by BgDataModel.
362 newScreens.removeAllValues(launcher.workspace.mScreenOrder)
363 bindAddScreens(newScreens)
364 }
365
366 // We add the items without animation on non-visible pages, and with
367 // animations on the new page (which we will try and snap to).
368 if (!addNotAnimated.isNullOrEmpty()) {
369 launcher.bindItems(addNotAnimated, false)
370 }
371 if (!addAnimated.isNullOrEmpty()) {
372 launcher.bindItems(addAnimated, true)
373 }
374
375 // Remove the extra empty screen
376 launcher.workspace.removeExtraEmptyScreen(false)
377 }
378
Sebastian Franco6196b902023-12-06 11:39:52 -0600379 private fun bindAddScreens(orderedScreenIdsArg: LIntArray) {
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600380 var orderedScreenIds = orderedScreenIdsArg
381 if (launcher.deviceProfile.isTwoPanels) {
382 if (FeatureFlags.FOLDABLE_SINGLE_PAGE.get()) {
383 orderedScreenIds = filterTwoPanelScreenIds(orderedScreenIds)
384 } else {
385 // Some empty pages might have been removed while the phone was in a single panel
386 // mode, so we want to add those empty pages back.
Sebastian Franco0b461ef2023-12-08 12:20:07 -0600387 val screenIds = LIntSet.wrap(orderedScreenIds)
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600388 orderedScreenIds.forEach { screenId: Int ->
389 screenIds.add(launcher.workspace.getScreenPair(screenId))
390 }
391 orderedScreenIds = screenIds.array
392 }
393 }
394 orderedScreenIds
395 .filterNot { screenId ->
Sihua Maa44f4ac2024-06-03 18:35:39 +0000396 isFirstPagePinnedItemEnabled &&
fbaron9d08e0f2024-04-24 11:26:01 -0700397 !SHOULD_SHOW_FIRST_PAGE_WIDGET &&
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600398 screenId == WorkspaceLayoutManager.FIRST_SCREEN_ID
399 }
400 .forEach { screenId ->
401 launcher.workspace.insertNewWorkspaceScreenBeforeEmptyScreen(screenId)
402 }
403 }
404
405 /**
406 * Remove odd number because they are already included when isTwoPanels and add the pair screen
407 * if not present.
408 */
Sebastian Franco6196b902023-12-06 11:39:52 -0600409 private fun filterTwoPanelScreenIds(orderedScreenIds: LIntArray): LIntArray {
Sebastian Franco0b461ef2023-12-08 12:20:07 -0600410 val screenIds = LIntSet.wrap(orderedScreenIds)
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600411 orderedScreenIds
412 .filter { screenId -> screenId % 2 == 1 }
413 .forEach { screenId ->
414 screenIds.remove(screenId)
415 // In case the pair is not added, add it
416 if (!launcher.workspace.containsScreenId(screenId - 1)) {
417 screenIds.add(screenId - 1)
418 }
419 }
420 return screenIds.array
421 }
422
423 override fun setIsFirstPagePinnedItemEnabled(isFirstPagePinnedItemEnabled: Boolean) {
424 this.isFirstPagePinnedItemEnabled = isFirstPagePinnedItemEnabled
425 launcher.workspace.bindAndInitFirstWorkspaceScreen()
426 }
427
428 override fun bindStringCache(cache: StringCache) {
429 stringCache = cache
430 launcher.appsView.updateWorkUI()
431 }
432
433 fun getIsFirstPagePinnedItemEnabled(): Boolean = isFirstPagePinnedItemEnabled
Sunny Goyal72a74662024-02-14 12:32:59 -0800434
435 override fun getItemInflater() = launcher.itemInflater
Sebastian Francobd7919c2023-09-19 10:55:37 -0700436}