blob: 51729992d45cddf5cdb6fee996b6d3e79489295e [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
6import android.view.ViewTreeObserver.OnDrawListener
Sebastian Francobd7919c2023-09-19 10:55:37 -07007import androidx.annotation.UiThread
Sebastian Franco0b461ef2023-12-08 12:20:07 -06008import com.android.launcher3.LauncherConstants.TraceEvents
Sebastian Franco6dda7c72023-11-17 18:03:00 -06009import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID
Sebastian Francoe4965122023-12-01 13:27:42 -060010import com.android.launcher3.allapps.AllAppsStore
Sebastian Franco6dda7c72023-11-17 18:03:00 -060011import com.android.launcher3.config.FeatureFlags
12import com.android.launcher3.config.FeatureFlags.shouldShowFirstPageWidget
Sebastian Francobd7919c2023-09-19 10:55:37 -070013import com.android.launcher3.model.BgDataModel
Sebastian Franco6dda7c72023-11-17 18:03:00 -060014import com.android.launcher3.model.StringCache
Sebastian Francobd7919c2023-09-19 10:55:37 -070015import com.android.launcher3.model.data.AppInfo
16import com.android.launcher3.model.data.ItemInfo
17import com.android.launcher3.model.data.LauncherAppWidgetInfo
18import com.android.launcher3.model.data.WorkspaceItemInfo
19import com.android.launcher3.popup.PopupContainerWithArrow
20import com.android.launcher3.util.ComponentKey
Sebastian Franco0b461ef2023-12-08 12:20:07 -060021import com.android.launcher3.util.Executors
Sebastian Franco69fd7422023-10-06 16:48:58 -070022import com.android.launcher3.util.IntArray as LIntArray
23import com.android.launcher3.util.IntSet as LIntSet
Sebastian Francobd7919c2023-09-19 10:55:37 -070024import com.android.launcher3.util.PackageUserKey
25import com.android.launcher3.util.Preconditions
Sebastian Franco0b461ef2023-12-08 12:20:07 -060026import com.android.launcher3.util.RunnableList
Sebastian Francoe4965122023-12-01 13:27:42 -060027import com.android.launcher3.util.TraceHelper
28import com.android.launcher3.util.ViewOnDrawExecutor
Sebastian Franco50b74c32023-11-16 11:36:49 -060029import com.android.launcher3.widget.PendingAddWidgetInfo
Sebastian Francobd7919c2023-09-19 10:55:37 -070030import com.android.launcher3.widget.model.WidgetsListBaseEntry
31import java.util.function.Predicate
32
33class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks {
Sebastian Franco69fd7422023-10-06 16:48:58 -070034
35 var synchronouslyBoundPages = LIntSet()
36 var pagesToBindSynchronously = LIntSet()
37
Sebastian Francoe4965122023-12-01 13:27:42 -060038 private var isFirstPagePinnedItemEnabled =
Sebastian Franco6dda7c72023-11-17 18:03:00 -060039 (BuildConfig.QSB_ON_FIRST_SCREEN && !FeatureFlags.ENABLE_SMARTSPACE_REMOVAL.get())
40
41 var stringCache: StringCache? = null
42
Sebastian Francoe4965122023-12-01 13:27:42 -060043 var pendingExecutor: ViewOnDrawExecutor? = null
44
45 var workspaceLoading = true
46
47 /**
48 * Refreshes the shortcuts shown on the workspace.
49 *
50 * Implementation of the method from LauncherModel.Callbacks.
51 */
52 override fun startBinding() {
53 TraceHelper.INSTANCE.beginSection("startBinding")
54 // Floating panels (except the full widget sheet) are associated with individual icons. If
55 // we are starting a fresh bind, close all such panels as all the icons are about
56 // to go away.
57 AbstractFloatingView.closeOpenViews(
58 launcher,
59 true,
60 AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv()
61 )
62 workspaceLoading = true
63
64 // Clear the workspace because it's going to be rebound
65 launcher.dragController.cancelDrag()
66 launcher.workspace.clearDropTargets()
67 launcher.workspace.removeAllWorkspaceScreens()
68 launcher.appWidgetHolder.clearViews()
69 launcher.hotseat?.resetLayout(launcher.deviceProfile.isVerticalBarLayout)
70 TraceHelper.INSTANCE.endSection()
71 }
72
Sebastian Franco0b461ef2023-12-08 12:20:07 -060073 @TargetApi(Build.VERSION_CODES.S)
74 override fun onInitialBindComplete(
75 boundPages: LIntSet,
76 pendingTasks: RunnableList,
77 workspaceItemCount: Int,
78 isBindSync: Boolean
79 ) {
80 synchronouslyBoundPages = boundPages
81 pagesToBindSynchronously = LIntSet()
82 clearPendingBinds()
83 val executor = ViewOnDrawExecutor(pendingTasks)
84 pendingExecutor = executor
85 if (!launcher.isInState(LauncherState.ALL_APPS)) {
86 launcher.appsView.appsStore.enableDeferUpdates(AllAppsStore.DEFER_UPDATES_NEXT_DRAW)
87 pendingTasks.add {
88 launcher.appsView.appsStore.disableDeferUpdates(
89 AllAppsStore.DEFER_UPDATES_NEXT_DRAW
90 )
91 }
92 }
93 executor.onLoadAnimationCompleted()
94 executor.attachTo(launcher)
95 if (Utilities.ATLEAST_S) {
96 Trace.endAsyncSection(
97 TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME,
98 TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE
99 )
100 }
101 launcher.bindComplete(workspaceItemCount, isBindSync)
102 launcher.rootView.viewTreeObserver.addOnDrawListener(
103 object : OnDrawListener {
104 override fun onDraw() {
105 Executors.MAIN_EXECUTOR.handler.postAtFrontOfQueue {
106 launcher.rootView.getViewTreeObserver().removeOnDrawListener(this)
107 }
108 }
109 }
110 )
111 }
112
Sebastian Francoe4965122023-12-01 13:27:42 -0600113 /**
Sebastian Franco6196b902023-12-06 11:39:52 -0600114 * Callback saying that there aren't any more items to bind.
115 *
116 * Implementation of the method from LauncherModel.Callbacks.
117 */
118 override fun finishBindingItems(pagesBoundFirst: LIntSet?) {
119 TraceHelper.INSTANCE.beginSection("finishBindingItems")
120 val deviceProfile = launcher.deviceProfile
121 launcher.workspace.restoreInstanceStateForRemainingPages()
122 workspaceLoading = false
123 launcher.processActivityResult()
124 val currentPage =
125 if (pagesBoundFirst != null && !pagesBoundFirst.isEmpty)
126 launcher.workspace.getPageIndexForScreenId(pagesBoundFirst.array[0])
127 else PagedView.INVALID_PAGE
128 // When undoing the removal of the last item on a page, return to that page.
129 // Since we are just resetting the current page without user interaction,
130 // override the previous page so we don't log the page switch.
131 launcher.workspace.setCurrentPage(currentPage, currentPage /* overridePrevPage */)
Sebastian Franco0b461ef2023-12-08 12:20:07 -0600132 pagesToBindSynchronously = LIntSet()
Sebastian Franco6196b902023-12-06 11:39:52 -0600133
134 // Cache one page worth of icons
135 launcher.viewCache.setCacheSize(
136 R.layout.folder_application,
Thales Lima1faa4ed2023-12-01 16:48:19 +0000137 deviceProfile.numFolderColumns * deviceProfile.numFolderRows
Sebastian Franco6196b902023-12-06 11:39:52 -0600138 )
139 launcher.viewCache.setCacheSize(R.layout.folder_page, 2)
140 TraceHelper.INSTANCE.endSection()
141 launcher.workspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true)
142 launcher.workspace.pageIndicator.setAreScreensBinding(false, deviceProfile.isTwoPanels)
143 }
144
145 /**
Sebastian Francoe4965122023-12-01 13:27:42 -0600146 * Clear any pending bind callbacks. This is called when is loader is planning to perform a full
147 * rebind from scratch.
148 */
149 override fun clearPendingBinds() {
150 pendingExecutor?.cancel() ?: return
151 pendingExecutor = null
152
153 // We might have set this flag previously and forgot to clear it.
154 launcher.appsView.appsStore.disableDeferUpdatesSilently(
155 AllAppsStore.DEFER_UPDATES_NEXT_DRAW
156 )
157 }
158
Sebastian Francobd7919c2023-09-19 10:55:37 -0700159 override fun preAddApps() {
160 // If there's an undo snackbar, force it to complete to ensure empty screens are removed
161 // before trying to add new items.
162 launcher.modelWriter.commitDelete()
163 val snackbar =
164 AbstractFloatingView.getOpenView<AbstractFloatingView>(
165 launcher,
166 AbstractFloatingView.TYPE_SNACKBAR
167 )
168 snackbar?.post { snackbar.close(true) }
169 }
170
171 @UiThread
172 override fun bindAllApplications(
173 apps: Array<AppInfo?>?,
174 flags: Int,
175 packageUserKeytoUidMap: Map<PackageUserKey?, Int?>?
176 ) {
177 Preconditions.assertUIThread()
178 val hadWorkApps = launcher.appsView.shouldShowTabs()
179 launcher.appsView.appsStore.setApps(apps, flags, packageUserKeytoUidMap)
180 PopupContainerWithArrow.dismissInvalidPopup(launcher)
181 if (hadWorkApps != launcher.appsView.shouldShowTabs()) {
182 launcher.stateManager.goToState(LauncherState.NORMAL)
183 }
184 }
185
186 /**
187 * Copies LauncherModel's map of activities to shortcut counts to Launcher's. This is necessary
188 * because LauncherModel's map is updated in the background, while Launcher runs on the UI.
189 */
190 override fun bindDeepShortcutMap(deepShortcutMapCopy: HashMap<ComponentKey?, Int?>?) {
191 launcher.popupDataProvider.setDeepShortcutMap(deepShortcutMapCopy)
192 }
193
194 override fun bindIncrementalDownloadProgressUpdated(app: AppInfo?) {
195 launcher.appsView.appsStore.updateProgressBar(app)
196 }
197
198 override fun bindWidgetsRestored(widgets: ArrayList<LauncherAppWidgetInfo?>?) {
199 launcher.workspace.widgetsRestored(widgets)
200 }
201
202 /**
203 * Some shortcuts were updated in the background. Implementation of the method from
204 * LauncherModel.Callbacks.
205 *
206 * @param updated list of shortcuts which have changed.
207 */
208 override fun bindWorkspaceItemsChanged(updated: List<WorkspaceItemInfo?>) {
209 if (updated.isNotEmpty()) {
210 launcher.workspace.updateWorkspaceItems(updated, launcher)
211 PopupContainerWithArrow.dismissInvalidPopup(launcher)
212 }
213 }
214
215 /**
216 * Update the state of a package, typically related to install state. Implementation of the
217 * method from LauncherModel.Callbacks.
218 */
219 override fun bindRestoreItemsChange(updates: HashSet<ItemInfo?>?) {
220 launcher.workspace.updateRestoreItems(updates, launcher)
221 }
222
223 /**
224 * A package was uninstalled/updated. We take both the super set of packageNames in addition to
225 * specific applications to remove, the reason being that this can be called when a package is
226 * updated as well. In that scenario, we only remove specific components from the workspace and
227 * hotseat, where as package-removal should clear all items by package name.
228 */
229 override fun bindWorkspaceComponentsRemoved(matcher: Predicate<ItemInfo?>?) {
230 launcher.workspace.removeItemsByMatcher(matcher)
231 launcher.dragController.onAppsRemoved(matcher)
232 PopupContainerWithArrow.dismissInvalidPopup(launcher)
233 }
234
235 override fun bindAllWidgets(allWidgets: List<WidgetsListBaseEntry?>?) {
236 launcher.popupDataProvider.allWidgets = allWidgets
237 }
Sebastian Franco69fd7422023-10-06 16:48:58 -0700238
239 /** Returns the ids of the workspaces to bind. */
240 override fun getPagesToBindSynchronously(orderedScreenIds: LIntArray): LIntSet {
241 // If workspace binding is still in progress, getCurrentPageScreenIds won't be
242 // accurate, and we should use mSynchronouslyBoundPages that's set during initial binding.
243 val visibleIds =
244 when {
245 !pagesToBindSynchronously.isEmpty -> pagesToBindSynchronously
Sebastian Francoe4965122023-12-01 13:27:42 -0600246 !workspaceLoading -> launcher.workspace.currentPageScreenIds
Sebastian Franco69fd7422023-10-06 16:48:58 -0700247 else -> synchronouslyBoundPages
248 }
249 // Launcher IntArray has the same name as Kotlin IntArray
250 val result = LIntSet()
251 if (visibleIds.isEmpty) {
252 return result
253 }
254 val actualIds = orderedScreenIds.clone()
255 val firstId = visibleIds.first()
256 val pairId = launcher.workspace.getScreenPair(firstId)
257 // Double check that actual screenIds contains the visibleId, as empty screens are hidden
258 // in single panel.
259 if (actualIds.contains(firstId)) {
260 result.add(firstId)
261 if (launcher.deviceProfile.isTwoPanels && actualIds.contains(pairId)) {
262 result.add(pairId)
263 }
264 } else if (
265 LauncherAppState.getIDP(launcher).supportedProfiles.any(DeviceProfile::isTwoPanels) &&
266 actualIds.contains(pairId)
267 ) {
268 // Add the right panel if left panel is hidden when switching display, due to empty
269 // pages being hidden in single panel.
270 result.add(pairId)
271 }
272 return result
273 }
Sebastian Franco50b74c32023-11-16 11:36:49 -0600274
275 override fun bindSmartspaceWidget() {
276 val cl: CellLayout? =
277 launcher.workspace.getScreenWithId(WorkspaceLayoutManager.FIRST_SCREEN_ID)
278 val spanX = InvariantDeviceProfile.INSTANCE.get(launcher).numSearchContainerColumns
279
280 if (cl?.isRegionVacant(0, 0, spanX, 1) != true) {
281 return
282 }
283
284 val widgetsListBaseEntry: WidgetsListBaseEntry =
285 launcher.popupDataProvider.allWidgets.firstOrNull { item: WidgetsListBaseEntry ->
286 item.mPkgItem.packageName == BuildConfig.APPLICATION_ID
287 }
288 ?: return
289
290 val info =
291 PendingAddWidgetInfo(
292 widgetsListBaseEntry.mWidgets[0].widgetInfo,
293 LauncherSettings.Favorites.CONTAINER_DESKTOP
294 )
295 launcher.addPendingItem(
296 info,
297 info.container,
298 WorkspaceLayoutManager.FIRST_SCREEN_ID,
299 intArrayOf(0, 0),
300 info.spanX,
301 info.spanY
302 )
303 }
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600304
Sebastian Franco6196b902023-12-06 11:39:52 -0600305 override fun bindScreens(orderedScreenIds: LIntArray) {
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600306 launcher.workspace.pageIndicator.setAreScreensBinding(
307 true,
308 launcher.deviceProfile.isTwoPanels
309 )
310 val firstScreenPosition = 0
311 if (
312 (FeatureFlags.QSB_ON_FIRST_SCREEN &&
313 isFirstPagePinnedItemEnabled &&
314 !shouldShowFirstPageWidget()) &&
315 orderedScreenIds.indexOf(FIRST_SCREEN_ID) != firstScreenPosition
316 ) {
317 orderedScreenIds.removeValue(FIRST_SCREEN_ID)
318 orderedScreenIds.add(firstScreenPosition, FIRST_SCREEN_ID)
319 } else if (
320 (!FeatureFlags.QSB_ON_FIRST_SCREEN && !isFirstPagePinnedItemEnabled ||
321 shouldShowFirstPageWidget()) && orderedScreenIds.isEmpty
322 ) {
323 // If there are no screens, we need to have an empty screen
324 launcher.workspace.addExtraEmptyScreens()
325 }
326 bindAddScreens(orderedScreenIds)
327
328 // After we have added all the screens, if the wallpaper was locked to the default state,
329 // then notify to indicate that it can be released and a proper wallpaper offset can be
330 // computed before the next layout
331 launcher.workspace.unlockWallpaperFromDefaultPageOnNextLayout()
332 }
333
334 override fun bindAppsAdded(
Sebastian Franco6196b902023-12-06 11:39:52 -0600335 newScreens: LIntArray?,
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600336 addNotAnimated: java.util.ArrayList<ItemInfo?>?,
337 addAnimated: java.util.ArrayList<ItemInfo?>?
338 ) {
339 // Add the new screens
340 if (newScreens != null) {
341 // newScreens can contain an empty right panel that is already bound, but not known
342 // by BgDataModel.
343 newScreens.removeAllValues(launcher.workspace.mScreenOrder)
344 bindAddScreens(newScreens)
345 }
346
347 // We add the items without animation on non-visible pages, and with
348 // animations on the new page (which we will try and snap to).
349 if (!addNotAnimated.isNullOrEmpty()) {
350 launcher.bindItems(addNotAnimated, false)
351 }
352 if (!addAnimated.isNullOrEmpty()) {
353 launcher.bindItems(addAnimated, true)
354 }
355
356 // Remove the extra empty screen
357 launcher.workspace.removeExtraEmptyScreen(false)
358 }
359
Sebastian Franco6196b902023-12-06 11:39:52 -0600360 private fun bindAddScreens(orderedScreenIdsArg: LIntArray) {
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600361 var orderedScreenIds = orderedScreenIdsArg
362 if (launcher.deviceProfile.isTwoPanels) {
363 if (FeatureFlags.FOLDABLE_SINGLE_PAGE.get()) {
364 orderedScreenIds = filterTwoPanelScreenIds(orderedScreenIds)
365 } else {
366 // Some empty pages might have been removed while the phone was in a single panel
367 // mode, so we want to add those empty pages back.
Sebastian Franco0b461ef2023-12-08 12:20:07 -0600368 val screenIds = LIntSet.wrap(orderedScreenIds)
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600369 orderedScreenIds.forEach { screenId: Int ->
370 screenIds.add(launcher.workspace.getScreenPair(screenId))
371 }
372 orderedScreenIds = screenIds.array
373 }
374 }
375 orderedScreenIds
376 .filterNot { screenId ->
377 FeatureFlags.QSB_ON_FIRST_SCREEN &&
378 isFirstPagePinnedItemEnabled &&
379 !FeatureFlags.shouldShowFirstPageWidget() &&
380 screenId == WorkspaceLayoutManager.FIRST_SCREEN_ID
381 }
382 .forEach { screenId ->
383 launcher.workspace.insertNewWorkspaceScreenBeforeEmptyScreen(screenId)
384 }
385 }
386
387 /**
388 * Remove odd number because they are already included when isTwoPanels and add the pair screen
389 * if not present.
390 */
Sebastian Franco6196b902023-12-06 11:39:52 -0600391 private fun filterTwoPanelScreenIds(orderedScreenIds: LIntArray): LIntArray {
Sebastian Franco0b461ef2023-12-08 12:20:07 -0600392 val screenIds = LIntSet.wrap(orderedScreenIds)
Sebastian Franco6dda7c72023-11-17 18:03:00 -0600393 orderedScreenIds
394 .filter { screenId -> screenId % 2 == 1 }
395 .forEach { screenId ->
396 screenIds.remove(screenId)
397 // In case the pair is not added, add it
398 if (!launcher.workspace.containsScreenId(screenId - 1)) {
399 screenIds.add(screenId - 1)
400 }
401 }
402 return screenIds.array
403 }
404
405 override fun setIsFirstPagePinnedItemEnabled(isFirstPagePinnedItemEnabled: Boolean) {
406 this.isFirstPagePinnedItemEnabled = isFirstPagePinnedItemEnabled
407 launcher.workspace.bindAndInitFirstWorkspaceScreen()
408 }
409
410 override fun bindStringCache(cache: StringCache) {
411 stringCache = cache
412 launcher.appsView.updateWorkUI()
413 }
414
415 fun getIsFirstPagePinnedItemEnabled(): Boolean = isFirstPagePinnedItemEnabled
Sebastian Francobd7919c2023-09-19 10:55:37 -0700416}