| package com.android.launcher3 |
| |
| import android.annotation.TargetApi |
| import android.os.Build |
| import android.os.Trace |
| import android.view.ViewTreeObserver.OnDrawListener |
| import androidx.annotation.UiThread |
| import com.android.launcher3.LauncherConstants.TraceEvents |
| import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID |
| import com.android.launcher3.allapps.AllAppsStore |
| import com.android.launcher3.config.FeatureFlags |
| import com.android.launcher3.config.FeatureFlags.shouldShowFirstPageWidget |
| import com.android.launcher3.model.BgDataModel |
| import com.android.launcher3.model.StringCache |
| import com.android.launcher3.model.data.AppInfo |
| import com.android.launcher3.model.data.ItemInfo |
| import com.android.launcher3.model.data.LauncherAppWidgetInfo |
| import com.android.launcher3.model.data.WorkspaceItemInfo |
| import com.android.launcher3.popup.PopupContainerWithArrow |
| import com.android.launcher3.util.ComponentKey |
| import com.android.launcher3.util.Executors |
| import com.android.launcher3.util.IntArray as LIntArray |
| import com.android.launcher3.util.IntSet as LIntSet |
| import com.android.launcher3.util.PackageUserKey |
| import com.android.launcher3.util.Preconditions |
| import com.android.launcher3.util.RunnableList |
| import com.android.launcher3.util.TraceHelper |
| import com.android.launcher3.util.ViewOnDrawExecutor |
| import com.android.launcher3.widget.PendingAddWidgetInfo |
| import com.android.launcher3.widget.model.WidgetsListBaseEntry |
| import java.util.function.Predicate |
| |
| class ModelCallbacks(private var launcher: Launcher) : BgDataModel.Callbacks { |
| |
| var synchronouslyBoundPages = LIntSet() |
| var pagesToBindSynchronously = LIntSet() |
| |
| private var isFirstPagePinnedItemEnabled = |
| (BuildConfig.QSB_ON_FIRST_SCREEN && !FeatureFlags.ENABLE_SMARTSPACE_REMOVAL.get()) |
| |
| var stringCache: StringCache? = null |
| |
| var pendingExecutor: ViewOnDrawExecutor? = null |
| |
| var workspaceLoading = true |
| |
| /** |
| * Refreshes the shortcuts shown on the workspace. |
| * |
| * Implementation of the method from LauncherModel.Callbacks. |
| */ |
| override fun startBinding() { |
| TraceHelper.INSTANCE.beginSection("startBinding") |
| // Floating panels (except the full widget sheet) are associated with individual icons. If |
| // we are starting a fresh bind, close all such panels as all the icons are about |
| // to go away. |
| AbstractFloatingView.closeOpenViews( |
| launcher, |
| true, |
| AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv() |
| ) |
| workspaceLoading = true |
| |
| // Clear the workspace because it's going to be rebound |
| launcher.dragController.cancelDrag() |
| launcher.workspace.clearDropTargets() |
| launcher.workspace.removeAllWorkspaceScreens() |
| launcher.appWidgetHolder.clearViews() |
| launcher.hotseat?.resetLayout(launcher.deviceProfile.isVerticalBarLayout) |
| TraceHelper.INSTANCE.endSection() |
| } |
| |
| @TargetApi(Build.VERSION_CODES.S) |
| override fun onInitialBindComplete( |
| boundPages: LIntSet, |
| pendingTasks: RunnableList, |
| workspaceItemCount: Int, |
| isBindSync: Boolean |
| ) { |
| synchronouslyBoundPages = boundPages |
| pagesToBindSynchronously = LIntSet() |
| clearPendingBinds() |
| val executor = ViewOnDrawExecutor(pendingTasks) |
| pendingExecutor = executor |
| if (!launcher.isInState(LauncherState.ALL_APPS)) { |
| launcher.appsView.appsStore.enableDeferUpdates(AllAppsStore.DEFER_UPDATES_NEXT_DRAW) |
| pendingTasks.add { |
| launcher.appsView.appsStore.disableDeferUpdates( |
| AllAppsStore.DEFER_UPDATES_NEXT_DRAW |
| ) |
| } |
| } |
| executor.onLoadAnimationCompleted() |
| executor.attachTo(launcher) |
| if (Utilities.ATLEAST_S) { |
| Trace.endAsyncSection( |
| TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME, |
| TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE |
| ) |
| } |
| launcher.bindComplete(workspaceItemCount, isBindSync) |
| launcher.rootView.viewTreeObserver.addOnDrawListener( |
| object : OnDrawListener { |
| override fun onDraw() { |
| Executors.MAIN_EXECUTOR.handler.postAtFrontOfQueue { |
| launcher.rootView.getViewTreeObserver().removeOnDrawListener(this) |
| } |
| } |
| } |
| ) |
| } |
| |
| /** |
| * Callback saying that there aren't any more items to bind. |
| * |
| * Implementation of the method from LauncherModel.Callbacks. |
| */ |
| override fun finishBindingItems(pagesBoundFirst: LIntSet?) { |
| TraceHelper.INSTANCE.beginSection("finishBindingItems") |
| val deviceProfile = launcher.deviceProfile |
| launcher.workspace.restoreInstanceStateForRemainingPages() |
| workspaceLoading = false |
| launcher.processActivityResult() |
| val currentPage = |
| if (pagesBoundFirst != null && !pagesBoundFirst.isEmpty) |
| launcher.workspace.getPageIndexForScreenId(pagesBoundFirst.array[0]) |
| else PagedView.INVALID_PAGE |
| // When undoing the removal of the last item on a page, return to that page. |
| // Since we are just resetting the current page without user interaction, |
| // override the previous page so we don't log the page switch. |
| launcher.workspace.setCurrentPage(currentPage, currentPage /* overridePrevPage */) |
| pagesToBindSynchronously = LIntSet() |
| |
| // Cache one page worth of icons |
| launcher.viewCache.setCacheSize( |
| R.layout.folder_application, |
| deviceProfile.numFolderColumns * deviceProfile.numFolderRows |
| ) |
| launcher.viewCache.setCacheSize(R.layout.folder_page, 2) |
| TraceHelper.INSTANCE.endSection() |
| launcher.workspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true) |
| launcher.workspace.pageIndicator.setAreScreensBinding(false, deviceProfile.isTwoPanels) |
| } |
| |
| /** |
| * Clear any pending bind callbacks. This is called when is loader is planning to perform a full |
| * rebind from scratch. |
| */ |
| override fun clearPendingBinds() { |
| pendingExecutor?.cancel() ?: return |
| pendingExecutor = null |
| |
| // We might have set this flag previously and forgot to clear it. |
| launcher.appsView.appsStore.disableDeferUpdatesSilently( |
| AllAppsStore.DEFER_UPDATES_NEXT_DRAW |
| ) |
| } |
| |
| override fun preAddApps() { |
| // If there's an undo snackbar, force it to complete to ensure empty screens are removed |
| // before trying to add new items. |
| launcher.modelWriter.commitDelete() |
| val snackbar = |
| AbstractFloatingView.getOpenView<AbstractFloatingView>( |
| launcher, |
| AbstractFloatingView.TYPE_SNACKBAR |
| ) |
| snackbar?.post { snackbar.close(true) } |
| } |
| |
| @UiThread |
| override fun bindAllApplications( |
| apps: Array<AppInfo?>?, |
| flags: Int, |
| packageUserKeytoUidMap: Map<PackageUserKey?, Int?>? |
| ) { |
| Preconditions.assertUIThread() |
| val hadWorkApps = launcher.appsView.shouldShowTabs() |
| launcher.appsView.appsStore.setApps(apps, flags, packageUserKeytoUidMap) |
| PopupContainerWithArrow.dismissInvalidPopup(launcher) |
| if (hadWorkApps != launcher.appsView.shouldShowTabs()) { |
| launcher.stateManager.goToState(LauncherState.NORMAL) |
| } |
| } |
| |
| /** |
| * Copies LauncherModel's map of activities to shortcut counts to Launcher's. This is necessary |
| * because LauncherModel's map is updated in the background, while Launcher runs on the UI. |
| */ |
| override fun bindDeepShortcutMap(deepShortcutMapCopy: HashMap<ComponentKey?, Int?>?) { |
| launcher.popupDataProvider.setDeepShortcutMap(deepShortcutMapCopy) |
| } |
| |
| override fun bindIncrementalDownloadProgressUpdated(app: AppInfo?) { |
| launcher.appsView.appsStore.updateProgressBar(app) |
| } |
| |
| override fun bindWidgetsRestored(widgets: ArrayList<LauncherAppWidgetInfo?>?) { |
| launcher.workspace.widgetsRestored(widgets) |
| } |
| |
| /** |
| * Some shortcuts were updated in the background. Implementation of the method from |
| * LauncherModel.Callbacks. |
| * |
| * @param updated list of shortcuts which have changed. |
| */ |
| override fun bindWorkspaceItemsChanged(updated: List<WorkspaceItemInfo?>) { |
| if (updated.isNotEmpty()) { |
| launcher.workspace.updateWorkspaceItems(updated, launcher) |
| PopupContainerWithArrow.dismissInvalidPopup(launcher) |
| } |
| } |
| |
| /** |
| * Update the state of a package, typically related to install state. Implementation of the |
| * method from LauncherModel.Callbacks. |
| */ |
| override fun bindRestoreItemsChange(updates: HashSet<ItemInfo?>?) { |
| launcher.workspace.updateRestoreItems(updates, launcher) |
| } |
| |
| /** |
| * A package was uninstalled/updated. We take both the super set of packageNames in addition to |
| * specific applications to remove, the reason being that this can be called when a package is |
| * updated as well. In that scenario, we only remove specific components from the workspace and |
| * hotseat, where as package-removal should clear all items by package name. |
| */ |
| override fun bindWorkspaceComponentsRemoved(matcher: Predicate<ItemInfo?>?) { |
| launcher.workspace.removeItemsByMatcher(matcher) |
| launcher.dragController.onAppsRemoved(matcher) |
| PopupContainerWithArrow.dismissInvalidPopup(launcher) |
| } |
| |
| override fun bindAllWidgets(allWidgets: List<WidgetsListBaseEntry?>?) { |
| launcher.popupDataProvider.allWidgets = allWidgets |
| } |
| |
| /** Returns the ids of the workspaces to bind. */ |
| override fun getPagesToBindSynchronously(orderedScreenIds: LIntArray): LIntSet { |
| // If workspace binding is still in progress, getCurrentPageScreenIds won't be |
| // accurate, and we should use mSynchronouslyBoundPages that's set during initial binding. |
| val visibleIds = |
| when { |
| !pagesToBindSynchronously.isEmpty -> pagesToBindSynchronously |
| !workspaceLoading -> launcher.workspace.currentPageScreenIds |
| else -> synchronouslyBoundPages |
| } |
| // Launcher IntArray has the same name as Kotlin IntArray |
| val result = LIntSet() |
| if (visibleIds.isEmpty) { |
| return result |
| } |
| val actualIds = orderedScreenIds.clone() |
| val firstId = visibleIds.first() |
| val pairId = launcher.workspace.getScreenPair(firstId) |
| // Double check that actual screenIds contains the visibleId, as empty screens are hidden |
| // in single panel. |
| if (actualIds.contains(firstId)) { |
| result.add(firstId) |
| if (launcher.deviceProfile.isTwoPanels && actualIds.contains(pairId)) { |
| result.add(pairId) |
| } |
| } else if ( |
| LauncherAppState.getIDP(launcher).supportedProfiles.any(DeviceProfile::isTwoPanels) && |
| actualIds.contains(pairId) |
| ) { |
| // Add the right panel if left panel is hidden when switching display, due to empty |
| // pages being hidden in single panel. |
| result.add(pairId) |
| } |
| return result |
| } |
| |
| override fun bindSmartspaceWidget() { |
| val cl: CellLayout? = |
| launcher.workspace.getScreenWithId(WorkspaceLayoutManager.FIRST_SCREEN_ID) |
| val spanX = InvariantDeviceProfile.INSTANCE.get(launcher).numSearchContainerColumns |
| |
| if (cl?.isRegionVacant(0, 0, spanX, 1) != true) { |
| return |
| } |
| |
| val widgetsListBaseEntry: WidgetsListBaseEntry = |
| launcher.popupDataProvider.allWidgets.firstOrNull { item: WidgetsListBaseEntry -> |
| item.mPkgItem.packageName == BuildConfig.APPLICATION_ID |
| } |
| ?: return |
| |
| val info = |
| PendingAddWidgetInfo( |
| widgetsListBaseEntry.mWidgets[0].widgetInfo, |
| LauncherSettings.Favorites.CONTAINER_DESKTOP |
| ) |
| launcher.addPendingItem( |
| info, |
| info.container, |
| WorkspaceLayoutManager.FIRST_SCREEN_ID, |
| intArrayOf(0, 0), |
| info.spanX, |
| info.spanY |
| ) |
| } |
| |
| override fun bindScreens(orderedScreenIds: LIntArray) { |
| launcher.workspace.pageIndicator.setAreScreensBinding( |
| true, |
| launcher.deviceProfile.isTwoPanels |
| ) |
| val firstScreenPosition = 0 |
| if ( |
| (FeatureFlags.QSB_ON_FIRST_SCREEN && |
| isFirstPagePinnedItemEnabled && |
| !shouldShowFirstPageWidget()) && |
| orderedScreenIds.indexOf(FIRST_SCREEN_ID) != firstScreenPosition |
| ) { |
| orderedScreenIds.removeValue(FIRST_SCREEN_ID) |
| orderedScreenIds.add(firstScreenPosition, FIRST_SCREEN_ID) |
| } else if ( |
| (!FeatureFlags.QSB_ON_FIRST_SCREEN && !isFirstPagePinnedItemEnabled || |
| shouldShowFirstPageWidget()) && orderedScreenIds.isEmpty |
| ) { |
| // If there are no screens, we need to have an empty screen |
| launcher.workspace.addExtraEmptyScreens() |
| } |
| bindAddScreens(orderedScreenIds) |
| |
| // After we have added all the screens, if the wallpaper was locked to the default state, |
| // then notify to indicate that it can be released and a proper wallpaper offset can be |
| // computed before the next layout |
| launcher.workspace.unlockWallpaperFromDefaultPageOnNextLayout() |
| } |
| |
| override fun bindAppsAdded( |
| newScreens: LIntArray?, |
| addNotAnimated: java.util.ArrayList<ItemInfo?>?, |
| addAnimated: java.util.ArrayList<ItemInfo?>? |
| ) { |
| // Add the new screens |
| if (newScreens != null) { |
| // newScreens can contain an empty right panel that is already bound, but not known |
| // by BgDataModel. |
| newScreens.removeAllValues(launcher.workspace.mScreenOrder) |
| bindAddScreens(newScreens) |
| } |
| |
| // We add the items without animation on non-visible pages, and with |
| // animations on the new page (which we will try and snap to). |
| if (!addNotAnimated.isNullOrEmpty()) { |
| launcher.bindItems(addNotAnimated, false) |
| } |
| if (!addAnimated.isNullOrEmpty()) { |
| launcher.bindItems(addAnimated, true) |
| } |
| |
| // Remove the extra empty screen |
| launcher.workspace.removeExtraEmptyScreen(false) |
| } |
| |
| private fun bindAddScreens(orderedScreenIdsArg: LIntArray) { |
| var orderedScreenIds = orderedScreenIdsArg |
| if (launcher.deviceProfile.isTwoPanels) { |
| if (FeatureFlags.FOLDABLE_SINGLE_PAGE.get()) { |
| orderedScreenIds = filterTwoPanelScreenIds(orderedScreenIds) |
| } else { |
| // Some empty pages might have been removed while the phone was in a single panel |
| // mode, so we want to add those empty pages back. |
| val screenIds = LIntSet.wrap(orderedScreenIds) |
| orderedScreenIds.forEach { screenId: Int -> |
| screenIds.add(launcher.workspace.getScreenPair(screenId)) |
| } |
| orderedScreenIds = screenIds.array |
| } |
| } |
| orderedScreenIds |
| .filterNot { screenId -> |
| FeatureFlags.QSB_ON_FIRST_SCREEN && |
| isFirstPagePinnedItemEnabled && |
| !FeatureFlags.shouldShowFirstPageWidget() && |
| screenId == WorkspaceLayoutManager.FIRST_SCREEN_ID |
| } |
| .forEach { screenId -> |
| launcher.workspace.insertNewWorkspaceScreenBeforeEmptyScreen(screenId) |
| } |
| } |
| |
| /** |
| * Remove odd number because they are already included when isTwoPanels and add the pair screen |
| * if not present. |
| */ |
| private fun filterTwoPanelScreenIds(orderedScreenIds: LIntArray): LIntArray { |
| val screenIds = LIntSet.wrap(orderedScreenIds) |
| orderedScreenIds |
| .filter { screenId -> screenId % 2 == 1 } |
| .forEach { screenId -> |
| screenIds.remove(screenId) |
| // In case the pair is not added, add it |
| if (!launcher.workspace.containsScreenId(screenId - 1)) { |
| screenIds.add(screenId - 1) |
| } |
| } |
| return screenIds.array |
| } |
| |
| override fun setIsFirstPagePinnedItemEnabled(isFirstPagePinnedItemEnabled: Boolean) { |
| this.isFirstPagePinnedItemEnabled = isFirstPagePinnedItemEnabled |
| launcher.workspace.bindAndInitFirstWorkspaceScreen() |
| } |
| |
| override fun bindStringCache(cache: StringCache) { |
| stringCache = cache |
| launcher.appsView.updateWorkUI() |
| } |
| |
| fun getIsFirstPagePinnedItemEnabled(): Boolean = isFirstPagePinnedItemEnabled |
| } |