diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index d5e8351a23..bd71a9feb6 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -289,6 +289,7 @@ 72dp + 150dp 48dp 24dp 40dp diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index ad6ce7dbd2..2e1318b019 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -112,10 +112,23 @@ public class QuickstepModelDelegate extends ModelDelegate { } @Override - @WorkerThread - public void loadItems(UserManagerState ums, Map pinnedShortcuts) { + public void loadHotseatItems(UserManagerState ums, + Map pinnedShortcuts) { // TODO: Implement caching and preloading - super.loadItems(ums, pinnedShortcuts); + super.loadHotseatItems(ums, pinnedShortcuts); + + WorkspaceItemFactory hotseatFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, + mIDP.numDatabaseHotseatIcons, mHotseatState.containerId); + FixedContainerItems hotseatItems = new FixedContainerItems(mHotseatState.containerId, + mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get)); + mDataModel.extraItems.put(mHotseatState.containerId, hotseatItems); + } + + @Override + public void loadAllAppsItems(UserManagerState ums, + Map pinnedShortcuts) { + // TODO: Implement caching and preloading + super.loadAllAppsItems(ums, pinnedShortcuts); WorkspaceItemFactory allAppsFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, mIDP.numDatabaseAllAppsColumns, mAllAppsState.containerId); @@ -123,17 +136,22 @@ public class QuickstepModelDelegate extends ModelDelegate { mAllAppsState.containerId, mAllAppsState.storage.read(mApp.getContext(), allAppsFactory, ums.allUsers::get)); mDataModel.extraItems.put(mAllAppsState.containerId, allAppsPredictionItems); + } - WorkspaceItemFactory hotseatFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, - mIDP.numDatabaseHotseatIcons, mHotseatState.containerId); - FixedContainerItems hotseatItems = new FixedContainerItems(mHotseatState.containerId, - mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get)); - mDataModel.extraItems.put(mHotseatState.containerId, hotseatItems); + @Override + public void loadWidgetsRecommendationItems() { + // TODO: Implement caching and preloading + super.loadWidgetsRecommendationItems(); // Widgets prediction isn't used frequently. And thus, it is not persisted on disk. mDataModel.extraItems.put(mWidgetsRecommendationState.containerId, new FixedContainerItems(mWidgetsRecommendationState.containerId, new ArrayList<>())); + } + + @Override + public void markActive() { + super.markActive(); mActive = true; } diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java index 474dc3d338..6d778efbd7 100644 --- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java @@ -44,6 +44,17 @@ public class FallbackTaskbarUIController extends TaskbarUIController { getRecentsView().setTaskLaunchListener(toState == RecentsState.DEFAULT ? (() -> animateToRecentsState(RecentsState.BACKGROUND_APP)) : null); } + + @Override + public void onStateTransitionComplete(RecentsState finalState) { + boolean finalStateDefault = finalState == RecentsState.DEFAULT; + // TODO(b/268120202) Taskbar shows up on 3P home, currently we don't go to + // overview from 3P home. Either implement that or it'll change w/ contextual? + boolean disallowLongClick = finalState == RecentsState.OVERVIEW_SPLIT_SELECT; + Utilities.setOverviewDragState(mControllers, + finalStateDefault /*disallowGlobalDrag*/, disallowLongClick, + finalStateDefault /*allowInitialSplitSelection*/); + } }; public FallbackTaskbarUIController(RecentsActivity recentsActivity) { diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index 17cb0604f5..a12f169b66 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -315,6 +315,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { @Override public void onTaskbarIconLaunched(ItemInfo item) { + super.onTaskbarIconLaunched(item); InstanceId instanceId = new InstanceIdSequence().newInstanceId(); mLauncher.logAppLaunch(mControllers.taskbarActivityContext.getStatsLogManager(), item, instanceId); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index c0929d6b11..b126042c74 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -837,15 +837,17 @@ public class TaskbarActivityContext extends BaseTaskbarContext { launchFromTaskbarPreservingSplitIfVisible(recents, info); } - mControllers.uiController.onTaskbarIconLaunched(info); } catch (NullPointerException | ActivityNotFoundException | SecurityException e) { Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT) .show(); Log.e(TAG, "Unable to launch. tag=" + info + " intent=" + intent, e); + return; } + } + mControllers.uiController.onTaskbarIconLaunched(info); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } } else if (tag instanceof AppInfo) { @@ -859,8 +861,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { taskbarUIController.triggerSecondAppForSplit(info, info.intent, view); } else { launchFromTaskbarPreservingSplitIfVisible(recents, info); - mControllers.uiController.onTaskbarIconLaunched(info); } + mControllers.uiController.onTaskbarIconLaunched(info); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } else if (tag instanceof ItemClickProxy) { ((ItemClickProxy) tag).onItemClicked(view); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java index 178482e07f..80cdbe98cb 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -119,14 +119,11 @@ import java.util.StringJoiner; mLauncherState = finalState; updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, false); applyState(); - boolean disallowGlobalDrag = finalState instanceof OverviewState; + boolean finalStateOverview = finalState instanceof OverviewState; boolean disallowLongClick = finalState == LauncherState.OVERVIEW_SPLIT_SELECT; - mControllers.taskbarDragController.setDisallowGlobalDrag(disallowGlobalDrag); - mControllers.taskbarDragController.setDisallowLongClick(disallowLongClick); - mControllers.taskbarAllAppsController.setDisallowGlobalDrag(disallowGlobalDrag); - mControllers.taskbarAllAppsController.setDisallowLongClick(disallowLongClick); - mControllers.taskbarPopupController.setAllowInitialSplitSelection( - disallowGlobalDrag); + com.android.launcher3.taskbar.Utilities.setOverviewDragState( + mControllers, finalStateOverview /*disallowGlobalDrag*/, + disallowLongClick, finalStateOverview /*allowInitialSplitSelection*/); } }; diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java index 5e670d2946..e46e11b164 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java @@ -199,13 +199,6 @@ public class TaskbarModelCallbacks implements hotseatItemInfos = mControllers.taskbarRecentAppsController .updateHotseatItemInfos(hotseatItemInfos); mContainer.updateHotseatItems(hotseatItemInfos); - - final boolean finalIsHotseatEmpty = isHotseatEmpty; - mControllers.runAfterInit(() -> { - mControllers.taskbarStashController.updateStateForFlag( - TaskbarStashController.FLAG_STASHED_IN_APP_EMPTY, finalIsHotseatEmpty); - mControllers.taskbarStashController.applyState(); - }); } @Override diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index c95535ba4d..2f69b07456 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -72,21 +72,20 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba public static final int FLAG_IN_APP = 1 << 0; public static final int FLAG_STASHED_IN_APP_MANUAL = 1 << 1; // long press, persisted public static final int FLAG_STASHED_IN_SYSUI_STATE = 1 << 2; // app pinning, keyguard, etc. - public static final int FLAG_STASHED_IN_APP_EMPTY = 1 << 3; // no hotseat icons - public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 4; // setup wizard and AllSetActivity - public static final int FLAG_STASHED_IN_APP_IME = 1 << 5; // IME is visible - public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 6; - public static final int FLAG_STASHED_IN_TASKBAR_ALL_APPS = 1 << 7; // All apps is visible. - public static final int FLAG_IN_SETUP = 1 << 8; // In the Setup Wizard - public static final int FLAG_STASHED_SMALL_SCREEN = 1 << 9; // phone screen gesture nav, stashed - public static final int FLAG_STASHED_IN_APP_AUTO = 1 << 10; // Autohide (transient taskbar). + public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 3; // setup wizard and AllSetActivity + public static final int FLAG_STASHED_IN_APP_IME = 1 << 4; // IME is visible + public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 5; + public static final int FLAG_STASHED_IN_TASKBAR_ALL_APPS = 1 << 6; // All apps is visible. + public static final int FLAG_IN_SETUP = 1 << 7; // In the Setup Wizard + public static final int FLAG_STASHED_SMALL_SCREEN = 1 << 8; // phone screen gesture nav, stashed + public static final int FLAG_STASHED_IN_APP_AUTO = 1 << 9; // Autohide (transient taskbar). // If any of these flags are enabled, isInApp should return true. private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP; // If we're in an app and any of these flags are enabled, taskbar should be stashed. private static final int FLAGS_STASHED_IN_APP = FLAG_STASHED_IN_APP_MANUAL - | FLAG_STASHED_IN_SYSUI_STATE | FLAG_STASHED_IN_APP_EMPTY | FLAG_STASHED_IN_APP_SETUP + | FLAG_STASHED_IN_SYSUI_STATE | FLAG_STASHED_IN_APP_SETUP | FLAG_STASHED_IN_APP_IME | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IN_APP_AUTO; @@ -932,7 +931,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba appendFlag(sj, flags, FLAGS_IN_APP, "FLAG_IN_APP"); appendFlag(sj, flags, FLAG_STASHED_IN_APP_MANUAL, "FLAG_STASHED_IN_APP_MANUAL"); appendFlag(sj, flags, FLAG_STASHED_IN_SYSUI_STATE, "FLAG_STASHED_IN_SYSUI_STATE"); - appendFlag(sj, flags, FLAG_STASHED_IN_APP_EMPTY, "FLAG_STASHED_IN_APP_EMPTY"); appendFlag(sj, flags, FLAG_STASHED_IN_APP_SETUP, "FLAG_STASHED_IN_APP_SETUP"); appendFlag(sj, flags, FLAG_STASHED_IN_APP_IME, "FLAG_STASHED_IN_APP_IME"); appendFlag(sj, flags, FLAG_IN_STASHED_LAUNCHER_STATE, "FLAG_IN_STASHED_LAUNCHER_STATE"); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index 6324715410..76b8b6dcd8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -17,6 +17,8 @@ package com.android.launcher3.taskbar; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP; + import android.content.Intent; import android.graphics.drawable.BitmapDrawable; import android.view.MotionEvent; @@ -74,7 +76,13 @@ public class TaskbarUIController { protected void onStashedInAppChanged() { } /** Called when an icon is launched. */ - public void onTaskbarIconLaunched(ItemInfo item) { } + @CallSuper + public void onTaskbarIconLaunched(ItemInfo item) { + // When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately instead of + // waiting for onPause, to reduce potential visual noise during the app open transition. + mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true); + mControllers.taskbarStashController.applyState(); + } public View getRootView() { return mControllers.taskbarActivityContext.getDragLayer(); @@ -104,7 +112,7 @@ public class TaskbarUIController { public void onExpandPip() { if (mControllers != null) { final TaskbarStashController stashController = mControllers.taskbarStashController; - stashController.updateStateForFlag(TaskbarStashController.FLAG_IN_APP, true); + stashController.updateStateForFlag(FLAG_IN_APP, true); stashController.applyState(); } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 38351a991d..eddc278ddd 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -88,6 +88,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private View mQsb; + private float mTransientTaskbarMinWidth; + public TaskbarView(@NonNull Context context) { this(context, null); } @@ -108,6 +110,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar mIconLayoutBounds = mActivityContext.getTransientTaskbarBounds(); Resources resources = getResources(); mIsRtl = Utilities.isRtl(resources); + mTransientTaskbarMinWidth = mContext.getResources().getDimension( + R.dimen.transient_taskbar_min_width); int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing); int actualIconSize = mActivityContext.getDeviceProfile().iconSizePx; @@ -124,11 +128,9 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar mThemeIconsBackground = calculateThemeIconsBackground(); - if (FeatureFlags.ENABLE_ALL_APPS_IN_TASKBAR.get() - && !mActivityContext.getPackageManager().hasSystemFeature(FEATURE_PC)) { + if (!mActivityContext.getPackageManager().hasSystemFeature(FEATURE_PC)) { mAllAppsButton = (IconButtonView) LayoutInflater.from(context) .inflate(R.layout.taskbar_all_apps_button, this, false); - mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); mAllAppsButton.setScaleX(mIsRtl ? -1 : 1); mAllAppsButton.setForegroundTint(mActivityContext.getColor( DisplayController.isTransientTaskbar(mActivityContext) @@ -277,7 +279,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (mAllAppsButton != null) { addView(mAllAppsButton, mIsRtl ? getChildCount() : 0); - if (mTaskbarDivider != null) { + // if only all apps button present, don't include divider view. + if (mTaskbarDivider != null && getChildCount() > 1) { addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1); } } @@ -319,6 +322,11 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar int count = getChildCount(); DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); int spaceNeeded = getIconLayoutWidth(); + // We are removing the margin from taskbar divider item in taskbar, + // so remove it from spacing also. + if (FeatureFlags.ENABLE_TASKBAR_PINNING.get() && count > 1) { + spaceNeeded -= mIconTouchSize; + } int navSpaceNeeded = deviceProfile.hotseatBarEndOffset; boolean layoutRtl = isLayoutRtl(); int iconEnd = right - (right - left - spaceNeeded) / 2; @@ -362,7 +370,15 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar iconEnd = iconStart - mItemMarginLeftRight; } } + mIconLayoutBounds.left = iconEnd; + + if (mIconLayoutBounds.right - mIconLayoutBounds.left < mTransientTaskbarMinWidth) { + int center = mIconLayoutBounds.centerX(); + int distanceFromCenter = (int) mTransientTaskbarMinWidth / 2; + mIconLayoutBounds.right = center + distanceFromCenter; + mIconLayoutBounds.left = center - distanceFromCenter; + } } @Override @@ -519,6 +535,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar /** * Finds the first icon to match one of the given matchers, from highest to lowest priority. + * * @return The first match, or All Apps button if no match was found. */ public View getFirstMatch(Predicate... matchers) { diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index 9824fe0c65..6252e60dca 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -380,8 +380,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar for (int i = 0; i < mTaskbarView.getChildCount(); i++) { View child = mTaskbarView.getChildAt(i); int positionInHotseat; - boolean isAllAppsButton = FeatureFlags.ENABLE_ALL_APPS_IN_TASKBAR.get() - && child == mTaskbarView.getAllAppsButtonView(); + boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonView(); if (!mIsHotseatIconOnTopWhenAligned) { // When going to home, the EMPHASIZED interpolator in TaskbarLauncherStateController // plays iconAlignment to 1 really fast, therefore moving the fading towards the end @@ -630,7 +629,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar } public static final FloatProperty ICON_TRANSLATE_X = - new FloatProperty("taskbarAligmentTranslateX") { + new FloatProperty("taskbarAlignmentTranslateX") { @Override public void setValue(View view, float v) { diff --git a/quickstep/src/com/android/launcher3/taskbar/Utilities.java b/quickstep/src/com/android/launcher3/taskbar/Utilities.java index fda6453e7d..a2b3c967df 100644 --- a/quickstep/src/com/android/launcher3/taskbar/Utilities.java +++ b/quickstep/src/com/android/launcher3/taskbar/Utilities.java @@ -30,4 +30,18 @@ public final class Utilities { str.add(flagName); } } + + /** + * Sets drag, long-click, and split selection behavior on 1P and 3P launchers with Taskbar + */ + static void setOverviewDragState(TaskbarControllers controllers, + boolean disallowGlobalDrag, boolean disallowLongClick, + boolean allowInitialSplitSelection) { + controllers.taskbarDragController.setDisallowGlobalDrag(disallowGlobalDrag); + controllers.taskbarDragController.setDisallowLongClick(disallowLongClick); + controllers.taskbarAllAppsController.setDisallowGlobalDrag(disallowGlobalDrag); + controllers.taskbarAllAppsController.setDisallowLongClick(disallowLongClick); + controllers.taskbarPopupController.setAllowInitialSplitSelection( + allowInitialSplitSelection); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java index 7a34869e2a..4a95a8f718 100644 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java @@ -20,7 +20,6 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.R; import com.android.launcher3.appprediction.PredictionRowView; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.taskbar.TaskbarControllers; @@ -54,9 +53,6 @@ public final class TaskbarAllAppsController { /** Initialize the controller. */ public void init(TaskbarControllers controllers, boolean allAppsVisible) { - if (!FeatureFlags.ENABLE_ALL_APPS_IN_TASKBAR.get()) { - return; - } mControllers = controllers; /* @@ -70,10 +66,6 @@ public final class TaskbarAllAppsController { /** Updates the current {@link AppInfo} instances. */ public void setApps(AppInfo[] apps, int flags) { - if (!FeatureFlags.ENABLE_ALL_APPS_IN_TASKBAR.get()) { - return; - } - mApps = apps; mAppsModelFlags = flags; if (mAppsView != null) { @@ -91,10 +83,6 @@ public final class TaskbarAllAppsController { /** Updates the current predictions. */ public void setPredictedApps(List predictedApps) { - if (!FeatureFlags.ENABLE_ALL_APPS_IN_TASKBAR.get()) { - return; - } - mPredictedApps = predictedApps; if (mAppsView != null) { mAppsView.getFloatingHeaderView() diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt new file mode 100644 index 0000000000..5a53d38f8d --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.launcher3.taskbar + +import androidx.test.runner.AndroidJUnit4 +import com.android.launcher3.statemanager.StateManager +import com.android.quickstep.RecentsActivity +import com.android.quickstep.fallback.RecentsState +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations.initMocks +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class FallbackTaskbarUIControllerTest : TaskbarBaseTestCase() { + + lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController + lateinit var stateListener: StateManager.StateListener + + @Mock + lateinit var recentsActivity: RecentsActivity + @Mock + lateinit var stateManager: StateManager + + @Before + override fun setup() { + super.setup() + whenever(recentsActivity.stateManager).thenReturn(stateManager) + fallbackTaskbarUIController = FallbackTaskbarUIController(recentsActivity) + + // Capture registered state listener to send events to in our tests + val captor = ArgumentCaptor.forClass(StateManager.StateListener::class.java) + fallbackTaskbarUIController.init(taskbarControllers) + verify(stateManager).addStateListener(captor.capture()) + stateListener = captor.value as StateManager.StateListener + } + + @Test + fun stateTransitionComplete_stateDefault() { + stateListener.onStateTransitionComplete(RecentsState.DEFAULT) + // verify dragging disabled + verify(taskbarDragController, times(1)).setDisallowGlobalDrag(true) + verify(taskbarAllAppsController, times(1)).setDisallowGlobalDrag(true) + // verify long click enabled + verify(taskbarDragController, times(1)).setDisallowLongClick(false) + verify(taskbarAllAppsController, times(1)).setDisallowLongClick(false) + // verify split selection enabled + verify(taskbarPopupController, times(1)).setAllowInitialSplitSelection(true) + } + + @Test + fun stateTransitionComplete_stateSplitSelect() { + stateListener.onStateTransitionComplete(RecentsState.OVERVIEW_SPLIT_SELECT) + // verify dragging disabled + verify(taskbarDragController, times(1)).setDisallowGlobalDrag(false) + verify(taskbarAllAppsController, times(1)).setDisallowGlobalDrag(false) + // verify long click enabled + verify(taskbarDragController, times(1)).setDisallowLongClick(true) + verify(taskbarAllAppsController, times(1)).setDisallowLongClick(true) + // verify split selection enabled + verify(taskbarPopupController, times(1)).setAllowInitialSplitSelection(false) + } +} \ No newline at end of file diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt index 28229a6dbf..4cca24e19b 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt @@ -54,7 +54,7 @@ abstract class TaskbarBaseTestCase { @Mock lateinit var taskbarEduTooltipController: TaskbarEduTooltipController @Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController - lateinit var mTaskbarControllers: TaskbarControllers + lateinit var taskbarControllers: TaskbarControllers @Before open fun setup() { @@ -66,7 +66,7 @@ abstract class TaskbarBaseTestCase { * includes that method to allow mocking it. */ MockitoAnnotations.initMocks(this) - mTaskbarControllers = + taskbarControllers = TaskbarControllers( taskbarActivityContext, taskbarDragController, diff --git a/res/drawable/ic_all_apps_button.xml b/res/drawable/ic_all_apps_button.xml index 4f0b6a8fbf..47f2a5d73a 100644 --- a/res/drawable/ic_all_apps_button.xml +++ b/res/drawable/ic_all_apps_button.xml @@ -1,5 +1,5 @@ - - - - - - - - - - - - - + android:width="52dp" + android:height="52dp" + android:viewportWidth="52" + android:viewportHeight="52"> + + + + + + + + + diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 986334b780..60b77d6947 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -230,10 +230,6 @@ public final class FeatureFlags { "ENABLE_ICON_LABEL_AUTO_SCALING", true, "Enables scaling/spacing for icon labels to make more characters visible"); - public static final BooleanFlag ENABLE_ALL_APPS_IN_TASKBAR = getDebugFlag( - "ENABLE_ALL_APPS_IN_TASKBAR", true, - "Enables accessing All Apps from the system Taskbar."); - public static final BooleanFlag ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT = getDebugFlag( "ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT", false, "Enables displaying the all apps button in the hotseat."); diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java index 6c62b31cbe..0a6a7cda9c 100644 --- a/src/com/android/launcher3/model/LoaderCursor.java +++ b/src/com/android/launcher3/model/LoaderCursor.java @@ -37,7 +37,6 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; @@ -72,24 +71,32 @@ public class LoaderCursor extends CursorWrapper { private final IconCache mIconCache; private final InvariantDeviceProfile mIDP; - private final IntArray itemsToRemove = new IntArray(); - private final IntArray restoredRows = new IntArray(); - private final IntSparseArrayMap occupied = new IntSparseArrayMap<>(); + private final IntArray mItemsToRemove = new IntArray(); + private final IntArray mRestoredRows = new IntArray(); + private final IntSparseArrayMap mOccupied = new IntSparseArrayMap<>(); - private final int iconPackageIndex; - private final int iconResourceIndex; - private final int iconIndex; - public final int titleIndex; + private final int mIconPackageIndex; + private final int mIconResourceIndex; + private final int mIconIndex; + public final int mTitleIndex; - private final int idIndex; - private final int containerIndex; - private final int itemTypeIndex; - private final int screenIndex; - private final int cellXIndex; - private final int cellYIndex; - private final int profileIdIndex; - private final int restoredIndex; - private final int intentIndex; + private final int mIdIndex; + private final int mContainerIndex; + private final int mItemTypeIndex; + private final int mScreenIndex; + private final int mCellXIndex; + private final int mCellYIndex; + private final int mProfileIdIndex; + private final int mRestoredIndex; + private final int mIntentIndex; + + private final int mAppWidgetIdIndex; + private final int mAppWidgetProviderIndex; + private final int mSpanXIndex; + private final int mSpanYIndex; + private final int mRankIndex; + private final int mOptionsIndex; + private final int mAppWidgetSourceIndex; @Nullable private LauncherActivityInfo mActivityInfo; @@ -114,20 +121,28 @@ public class LoaderCursor extends CursorWrapper { mPM = mContext.getPackageManager(); // Init column indices - iconIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); - iconPackageIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); - iconResourceIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); - titleIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); + mIconIndex = getColumnIndexOrThrow(Favorites.ICON); + mIconPackageIndex = getColumnIndexOrThrow(Favorites.ICON_PACKAGE); + mIconResourceIndex = getColumnIndexOrThrow(Favorites.ICON_RESOURCE); + mTitleIndex = getColumnIndexOrThrow(Favorites.TITLE); - idIndex = getColumnIndexOrThrow(LauncherSettings.Favorites._ID); - containerIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); - itemTypeIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); - screenIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); - cellXIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); - cellYIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); - profileIdIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID); - restoredIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.RESTORED); - intentIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); + mIdIndex = getColumnIndexOrThrow(Favorites._ID); + mContainerIndex = getColumnIndexOrThrow(Favorites.CONTAINER); + mItemTypeIndex = getColumnIndexOrThrow(Favorites.ITEM_TYPE); + mScreenIndex = getColumnIndexOrThrow(Favorites.SCREEN); + mCellXIndex = getColumnIndexOrThrow(Favorites.CELLX); + mCellYIndex = getColumnIndexOrThrow(Favorites.CELLY); + mProfileIdIndex = getColumnIndexOrThrow(Favorites.PROFILE_ID); + mRestoredIndex = getColumnIndexOrThrow(Favorites.RESTORED); + mIntentIndex = getColumnIndexOrThrow(Favorites.INTENT); + + mAppWidgetIdIndex = getColumnIndexOrThrow(Favorites.APPWIDGET_ID); + mAppWidgetProviderIndex = getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER); + mSpanXIndex = getColumnIndexOrThrow(Favorites.SPANX); + mSpanYIndex = getColumnIndexOrThrow(Favorites.SPANY); + mRankIndex = getColumnIndexOrThrow(Favorites.RANK); + mOptionsIndex = getColumnIndexOrThrow(Favorites.OPTIONS); + mAppWidgetSourceIndex = getColumnIndexOrThrow(Favorites.APPWIDGET_SOURCE); } @Override @@ -137,18 +152,18 @@ public class LoaderCursor extends CursorWrapper { mActivityInfo = null; // Load common properties. - itemType = getInt(itemTypeIndex); - container = getInt(containerIndex); - id = getInt(idIndex); - serialNumber = getInt(profileIdIndex); + itemType = getInt(mItemTypeIndex); + container = getInt(mContainerIndex); + id = getInt(mIdIndex); + serialNumber = getInt(mProfileIdIndex); user = allUsers.get(serialNumber); - restoreFlag = getInt(restoredIndex); + restoreFlag = getInt(mRestoredIndex); } return result; } public Intent parseIntent() { - String intentDescription = getString(intentIndex); + String intentDescription = getString(mIntentIndex); try { return TextUtils.isEmpty(intentDescription) ? null : Intent.parseUri(intentDescription, 0); @@ -185,14 +200,14 @@ public class LoaderCursor extends CursorWrapper { public IconRequestInfo createIconRequestInfo( WorkspaceItemInfo wai, boolean useLowResIcon) { - String packageName = itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT - ? getString(iconPackageIndex) : null; - String resourceName = itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT - ? getString(iconResourceIndex) : null; - byte[] iconBlob = itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT + String packageName = itemType == Favorites.ITEM_TYPE_SHORTCUT + ? getString(mIconPackageIndex) : null; + String resourceName = itemType == Favorites.ITEM_TYPE_SHORTCUT + ? getString(mIconResourceIndex) : null; + byte[] iconBlob = itemType == Favorites.ITEM_TYPE_SHORTCUT || itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT || restoreFlag != 0 - ? getBlob(iconIndex) : null; + ? getBlob(mIconIndex) : null; return new IconRequestInfo<>( wai, mActivityInfo, packageName, resourceName, iconBlob, useLowResIcon); @@ -202,7 +217,70 @@ public class LoaderCursor extends CursorWrapper { * Returns the title or empty string */ private String getTitle() { - return Utilities.trim(getString(titleIndex)); + return Utilities.trim(getString(mTitleIndex)); + } + + /** + * When loading an app widget for the workspace, returns it's app widget id + */ + public int getAppWidgetId() { + return getInt(mAppWidgetIdIndex); + } + + /** + * When loading an app widget for the workspace, returns the widget provider + */ + public String getAppWidgetProvider() { + return getString(mAppWidgetProviderIndex); + } + + /** + * Returns the x position for the item in the cell layout's grid + */ + public int getSpanX() { + return getInt(mSpanXIndex); + } + + /** + * Returns the y position for the item in the cell layout's grid + */ + public int getSpanY() { + return getInt(mSpanYIndex); + } + + /** + * Returns the rank for the item + */ + public int getRank() { + return getInt(mRankIndex); + } + + /** + * Returns the options for the item + */ + public int getOptions() { + return getInt(mOptionsIndex); + } + + /** + * When loading an app widget for the workspace, returns it's app widget source + */ + public int getAppWidgetSource() { + return getInt(mAppWidgetSourceIndex); + } + + /** + * Returns the screen that the item is on + */ + public int getScreen() { + return getInt(mScreenIndex); + } + + /** + * Returns the UX container that the item is in + */ + public int getContainer() { + return getInt(mContainerIndex); } /** @@ -320,7 +398,7 @@ public class LoaderCursor extends CursorWrapper { */ public void markDeleted(String reason) { FileLog.e(TAG, reason); - itemsToRemove.add(id); + mItemsToRemove.add(id); } /** @@ -328,10 +406,10 @@ public class LoaderCursor extends CursorWrapper { * @return true is any item was removed. */ public boolean commitDeleted() { - if (itemsToRemove.size() > 0) { + if (mItemsToRemove.size() > 0) { // Remove dead items mContext.getContentResolver().delete(mContentUri, Utilities.createDbSelectionQuery( - LauncherSettings.Favorites._ID, itemsToRemove), null); + Favorites._ID, mItemsToRemove), null); return true; } return false; @@ -342,7 +420,7 @@ public class LoaderCursor extends CursorWrapper { */ public void markRestored() { if (restoreFlag != 0) { - restoredRows.add(id); + mRestoredRows.add(id); restoreFlag = 0; } } @@ -352,13 +430,13 @@ public class LoaderCursor extends CursorWrapper { } public void commitRestoredItems() { - if (restoredRows.size() > 0) { + if (mRestoredRows.size() > 0) { // Update restored items that no longer require special handling ContentValues values = new ContentValues(); - values.put(LauncherSettings.Favorites.RESTORED, 0); + values.put(Favorites.RESTORED, 0); mContext.getContentResolver().update(mContentUri, values, Utilities.createDbSelectionQuery( - LauncherSettings.Favorites._ID, restoredRows), null); + Favorites._ID, mRestoredRows), null); } } @@ -366,8 +444,7 @@ public class LoaderCursor extends CursorWrapper { * Returns true is the item is on workspace or hotseat */ public boolean isOnWorkspaceOrHotseat() { - return container == LauncherSettings.Favorites.CONTAINER_DESKTOP || - container == LauncherSettings.Favorites.CONTAINER_HOTSEAT; + return container == Favorites.CONTAINER_DESKTOP || container == Favorites.CONTAINER_HOTSEAT; } /** @@ -381,9 +458,9 @@ public class LoaderCursor extends CursorWrapper { public void applyCommonProperties(ItemInfo info) { info.id = id; info.container = container; - info.screenId = getInt(screenIndex); - info.cellX = getInt(cellXIndex); - info.cellY = getInt(cellYIndex); + info.screenId = getInt(mScreenIndex); + info.cellX = getInt(mCellXIndex); + info.cellY = getInt(mCellYIndex); } public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) { @@ -396,7 +473,7 @@ public class LoaderCursor extends CursorWrapper { */ public void checkAndAddItem( ItemInfo info, BgDataModel dataModel, LoaderMemoryLogger logger) { - if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + if (info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { // Ensure that it is a valid intent. An exception here will // cause the item loading to get skipped ShortcutKey.fromItemInfo(info); @@ -413,9 +490,9 @@ public class LoaderCursor extends CursorWrapper { */ protected boolean checkItemPlacement(ItemInfo item) { int containerIndex = item.screenId; - if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + if (item.container == Favorites.CONTAINER_HOTSEAT) { final GridOccupancy hotseatOccupancy = - occupied.get(LauncherSettings.Favorites.CONTAINER_HOTSEAT); + mOccupied.get(Favorites.CONTAINER_HOTSEAT); if (item.screenId >= mIDP.numDatabaseHotseatIcons) { Log.e(TAG, "Error loading shortcut " + item @@ -438,19 +515,18 @@ public class LoaderCursor extends CursorWrapper { } else { final GridOccupancy occupancy = new GridOccupancy(mIDP.numDatabaseHotseatIcons, 1); occupancy.cells[item.screenId][0] = true; - occupied.put(LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy); + mOccupied.put(Favorites.CONTAINER_HOTSEAT, occupancy); return true; } - } else if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP) { + } else if (item.container != Favorites.CONTAINER_DESKTOP) { // Skip further checking if it is not the hotseat or workspace container return true; } final int countX = mIDP.numColumns; final int countY = mIDP.numRows; - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && - item.cellX < 0 || item.cellY < 0 || - item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) { + if (item.container == Favorites.CONTAINER_DESKTOP && item.cellX < 0 || item.cellY < 0 + || item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) { Log.e(TAG, "Error loading shortcut " + item + " into cell (" + containerIndex + "-" + item.screenId + ":" + item.cellX + "," + item.cellY @@ -458,7 +534,7 @@ public class LoaderCursor extends CursorWrapper { return false; } - if (!occupied.containsKey(item.screenId)) { + if (!mOccupied.containsKey(item.screenId)) { GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1); if (item.screenId == Workspace.FIRST_SCREEN_ID && FeatureFlags.QSB_ON_FIRST_SCREEN) { // Mark the first X columns (X is width of the search container) in the first row as @@ -468,9 +544,9 @@ public class LoaderCursor extends CursorWrapper { int spanY = 1; screen.markCells(0, 0, spanX, spanY, true); } - occupied.put(item.screenId, screen); + mOccupied.put(item.screenId, screen); } - final GridOccupancy occupancy = occupied.get(item.screenId); + final GridOccupancy occupancy = mOccupied.get(item.screenId); // Check if any workspace icons overlap with each other if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) { diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 46a6a66c76..da9be499c8 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -58,7 +58,8 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; -import com.android.launcher3.LauncherSettings; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.LauncherSettings.Settings; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.Folder; @@ -198,7 +199,7 @@ public class LoaderTask implements Runnable { } Object traceToken = TraceHelper.INSTANCE.beginSection(TAG); - TimingLogger logger = new TimingLogger(TAG, "run"); + TimingLogger timingLogger = new TimingLogger(TAG, "run"); LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger(); try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) { List allShortcuts = new ArrayList<>(); @@ -208,7 +209,7 @@ public class LoaderTask implements Runnable { } finally { Trace.endSection(); } - logASplit(logger, "loadWorkspace"); + logASplit(timingLogger, "loadWorkspace"); // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db. // sanitizeData should not be invoked if the workspace is loaded from a db different @@ -216,22 +217,23 @@ public class LoaderTask implements Runnable { // (e.g. both grid preview and minimal device mode uses a different db) if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) { verifyNotStopped(); - sanitizeData(); - logASplit(logger, "sanitizeData"); + sanitizeFolders(mItemsDeleted); + sanitizeWidgetsShortcutsAndPackages(); + logASplit(timingLogger, "sanitizeData"); } verifyNotStopped(); mLauncherBinder.bindWorkspace(true /* incrementBindId */); - logASplit(logger, "bindWorkspace"); + logASplit(timingLogger, "bindWorkspace"); mModelDelegate.workspaceLoadComplete(); // Notify the installer packages of packages with active installs on the first screen. sendFirstScreenActiveInstallsBroadcast(); - logASplit(logger, "sendFirstScreenActiveInstallsBroadcast"); + logASplit(timingLogger, "sendFirstScreenActiveInstallsBroadcast"); // Take a break waitForIdle(); - logASplit(logger, "step 1 complete"); + logASplit(timingLogger, "step 1 complete"); verifyNotStopped(); // second step @@ -242,11 +244,11 @@ public class LoaderTask implements Runnable { } finally { Trace.endSection(); } - logASplit(logger, "loadAllApps"); + logASplit(timingLogger, "loadAllApps"); verifyNotStopped(); mLauncherBinder.bindAllApps(); - logASplit(logger, "bindAllApps"); + logASplit(timingLogger, "bindAllApps"); verifyNotStopped(); IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler(); @@ -254,69 +256,69 @@ public class LoaderTask implements Runnable { updateHandler.updateIcons(allActivityList, LauncherActivityCachingLogic.newInstance(mApp.getContext()), mApp.getModel()::onPackageIconsUpdated); - logASplit(logger, "update icon cache"); + logASplit(timingLogger, "update icon cache"); verifyNotStopped(); - logASplit(logger, "save shortcuts in icon cache"); + logASplit(timingLogger, "save shortcuts in icon cache"); updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(), mApp.getModel()::onPackageIconsUpdated); // Take a break waitForIdle(); - logASplit(logger, "step 2 complete"); + logASplit(timingLogger, "step 2 complete"); verifyNotStopped(); // third step List allDeepShortcuts = loadDeepShortcuts(); - logASplit(logger, "loadDeepShortcuts"); + logASplit(timingLogger, "loadDeepShortcuts"); verifyNotStopped(); mLauncherBinder.bindDeepShortcuts(); - logASplit(logger, "bindDeepShortcuts"); + logASplit(timingLogger, "bindDeepShortcuts"); verifyNotStopped(); - logASplit(logger, "save deep shortcuts in icon cache"); + logASplit(timingLogger, "save deep shortcuts in icon cache"); updateHandler.updateIcons(allDeepShortcuts, new ShortcutCachingLogic(), (pkgs, user) -> { }); // Take a break waitForIdle(); - logASplit(logger, "step 3 complete"); + logASplit(timingLogger, "step 3 complete"); verifyNotStopped(); // fourth step List allWidgetsList = mBgDataModel.widgetsModel.update(mApp, null); - logASplit(logger, "load widgets"); + logASplit(timingLogger, "load widgets"); verifyNotStopped(); mLauncherBinder.bindWidgets(); - logASplit(logger, "bindWidgets"); + logASplit(timingLogger, "bindWidgets"); verifyNotStopped(); updateHandler.updateIcons(allWidgetsList, new ComponentWithIconCachingLogic(mApp.getContext(), true), mApp.getModel()::onWidgetLabelsUpdated); - logASplit(logger, "save widgets in icon cache"); + logASplit(timingLogger, "save widgets in icon cache"); // fifth step loadFolderNames(); verifyNotStopped(); updateHandler.finish(); - logASplit(logger, "finish icon update"); + logASplit(timingLogger, "finish icon update"); mModelDelegate.modelLoadComplete(); transaction.commit(); memoryLogger.clearLogs(); } catch (CancellationException e) { // Loader stopped, ignore - logASplit(logger, "Cancelled"); + logASplit(timingLogger, "Cancelled"); } catch (Exception e) { memoryLogger.printLogs(); throw e; } finally { - logger.dumpToLog(); + timingLogger.dumpToLog(); } TraceHelper.INSTANCE.endSection(traceToken); } @@ -326,9 +328,10 @@ public class LoaderTask implements Runnable { this.notify(); } - private void loadWorkspace(List allDeepShortcuts, LoaderMemoryLogger logger) { - loadWorkspace(allDeepShortcuts, LauncherSettings.Favorites.CONTENT_URI, - null /* selection */, logger); + private void loadWorkspace( + List allDeepShortcuts, LoaderMemoryLogger memoryLogger) { + loadWorkspace(allDeepShortcuts, Favorites.CONTENT_URI, + null /* selection */, memoryLogger); } protected void loadWorkspace( @@ -340,7 +343,7 @@ public class LoaderTask implements Runnable { List allDeepShortcuts, Uri contentUri, String selection, - @Nullable LoaderMemoryLogger logger) { + @Nullable LoaderMemoryLogger memoryLogger) { final Context context = mApp.getContext(); final ContentResolver contentResolver = context.getContentResolver(); final PackageManagerHelper pmHelper = new PackageManagerHelper(context); @@ -356,13 +359,11 @@ public class LoaderTask implements Runnable { if (clearDb) { Log.d(TAG, "loadWorkspace: resetting launcher database"); - LauncherSettings.Settings.call(contentResolver, - LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); + Settings.call(contentResolver, Settings.METHOD_CREATE_EMPTY_DB); } Log.d(TAG, "loadWorkspace: loading default favorites"); - LauncherSettings.Settings.call(contentResolver, - LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES); + Settings.call(contentResolver, Settings.METHOD_LOAD_DEFAULT_FAVORITES); synchronized (mBgDataModel) { mBgDataModel.clear(); @@ -380,24 +381,8 @@ public class LoaderTask implements Runnable { contentResolver.query(contentUri, null, selection, null, null), contentUri, mApp, mUserManagerState); final Bundle extras = c.getExtras(); - mDbName = extras == null - ? null : extras.getString(LauncherSettings.Settings.EXTRA_DB_NAME); + mDbName = extras == null ? null : extras.getString(Settings.EXTRA_DB_NAME); try { - final int appWidgetIdIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.APPWIDGET_ID); - final int appWidgetProviderIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.APPWIDGET_PROVIDER); - final int spanXIndex = c.getColumnIndexOrThrow - (LauncherSettings.Favorites.SPANX); - final int spanYIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.SPANY); - final int rankIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.RANK); - final int optionsIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.OPTIONS); - final int sourceContainerIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.APPWIDGET_SOURCE); - final LongSparseArray unlockedUsers = new LongSparseArray<>(); mUserManagerState.init(mUserCache, mUserManager); @@ -425,437 +410,23 @@ public class LoaderTask implements Runnable { unlockedUsers.put(serialNo, userUnlocked); } - WorkspaceItemInfo info; - LauncherAppWidgetInfo appWidgetInfo; - LauncherAppWidgetProviderInfo widgetProviderInfo; - Intent intent; - String targetPkg; List> iconRequestInfos = new ArrayList<>(); while (!mStopped && c.moveToNext()) { - try { - if (c.user == null) { - // User has been deleted, remove the item. - c.markDeleted("User has been deleted"); - continue; - } - - boolean allowMissingTarget = false; - switch (c.itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: - case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: - case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: - intent = c.parseIntent(); - if (intent == null) { - c.markDeleted("Invalid or null intent"); - continue; - } - - int disabledState = mUserManagerState.isUserQuiet(c.serialNumber) - ? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0; - ComponentName cn = intent.getComponent(); - targetPkg = cn == null ? intent.getPackage() : cn.getPackageName(); - - if (TextUtils.isEmpty(targetPkg) && - c.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { - c.markDeleted("Only legacy shortcuts can have null package"); - continue; - } - - // If there is no target package, its an implicit intent - // (legacy shortcut) which is always valid - boolean validTarget = TextUtils.isEmpty(targetPkg) || - mLauncherApps.isPackageEnabled(targetPkg, c.user); - - // If it's a deep shortcut, we'll use pinned shortcuts to restore it - if (cn != null && validTarget && c.itemType - != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { - // If the apk is present and the shortcut points to a specific - // component. - - // If the component is already present - if (mLauncherApps.isActivityEnabled(cn, c.user)) { - // no special handling necessary for this item - c.markRestored(); - } else { - // Gracefully try to find a fallback activity. - intent = pmHelper.getAppLaunchIntent(targetPkg, c.user); - if (intent != null) { - c.restoreFlag = 0; - c.updater().put( - LauncherSettings.Favorites.INTENT, - intent.toUri(0)).commit(); - cn = intent.getComponent(); - } else { - c.markDeleted("Unable to find a launch target"); - continue; - } - } - } - // else if cn == null => can't infer much, leave it - // else if !validPkg => could be restored icon or missing sd-card - - if (!TextUtils.isEmpty(targetPkg) && !validTarget) { - // Points to a valid app (superset of cn != null) but the apk - // is not available. - - if (c.restoreFlag != 0) { - // Package is not yet available but might be - // installed later. - FileLog.d(TAG, "package not yet restored: " + targetPkg); - - tempPackageKey.update(targetPkg, c.user); - if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) { - // Restore has started once. - } else if (installingPkgs.containsKey(tempPackageKey)) { - // App restore has started. Update the flag - c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED; - c.updater().put(LauncherSettings.Favorites.RESTORED, - c.restoreFlag).commit(); - } else { - c.markDeleted("Unrestored app removed: " + targetPkg); - continue; - } - } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) { - // Package is present but not available. - disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE; - // Add the icon on the workspace anyway. - allowMissingTarget = true; - } else if (!isSdCardReady) { - // SdCard is not ready yet. Package might get available, - // once it is ready. - Log.d(TAG, "Missing pkg, will check later: " + targetPkg); - mPendingPackages.add(new PackageUserKey(targetPkg, c.user)); - // Add the icon on the workspace anyway. - allowMissingTarget = true; - } else { - // Do not wait for external media load anymore. - c.markDeleted("Invalid package removed: " + targetPkg); - continue; - } - } - - if ((c.restoreFlag & WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) != 0) { - validTarget = false; - } - - if (validTarget) { - // The shortcut points to a valid target (either no target - // or something which is ready to be used) - c.markRestored(); - } - - boolean useLowResIcon = !c.isOnWorkspaceOrHotseat(); - - if (c.restoreFlag != 0) { - // Already verified above that user is same as default user - info = c.getRestoredItemInfo(intent); - } else if (c.itemType == - LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { - info = c.getAppShortcutInfo( - intent, - allowMissingTarget, - useLowResIcon, - !FeatureFlags.ENABLE_BULK_WORKSPACE_ICON_LOADING.get()); - } else if (c.itemType == - LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { - - ShortcutKey key = ShortcutKey.fromIntent(intent, c.user); - if (unlockedUsers.get(c.serialNumber)) { - ShortcutInfo pinnedShortcut = - shortcutKeyToPinnedShortcuts.get(key); - if (pinnedShortcut == null) { - // The shortcut is no longer valid. - c.markDeleted("Pinned shortcut not found"); - continue; - } - info = new WorkspaceItemInfo(pinnedShortcut, context); - // If the pinned deep shortcut is no longer published, - // use the last saved icon instead of the default. - mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon); - - if (pmHelper.isAppSuspended( - pinnedShortcut.getPackage(), info.user)) { - info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED; - } - intent = info.getIntent(); - allDeepShortcuts.add(pinnedShortcut); - } else { - // Create a shortcut info in disabled mode for now. - info = c.loadSimpleWorkspaceItem(); - info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER; - } - } else { // item type == ITEM_TYPE_SHORTCUT - info = c.loadSimpleWorkspaceItem(); - - // Shortcuts are only available on the primary profile - if (!TextUtils.isEmpty(targetPkg) - && pmHelper.isAppSuspended(targetPkg, c.user)) { - disabledState |= FLAG_DISABLED_SUSPENDED; - } - info.options = c.getInt(optionsIndex); - - // App shortcuts that used to be automatically added to Launcher - // didn't always have the correct intent flags set, so do that - // here - if (intent.getAction() != null && - intent.getCategories() != null && - intent.getAction().equals(Intent.ACTION_MAIN) && - intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { - intent.addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); - } - } - - if (info != null) { - if (info.itemType - != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { - // Skip deep shortcuts; their title and icons have already been - // loaded above. - iconRequestInfos.add( - c.createIconRequestInfo(info, useLowResIcon)); - } - - c.applyCommonProperties(info); - - info.intent = intent; - info.rank = c.getInt(rankIndex); - info.spanX = 1; - info.spanY = 1; - info.runtimeStatusFlags |= disabledState; - if (isSafeMode && !isSystemApp(context, intent)) { - info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE; - } - LauncherActivityInfo activityInfo = c.getLauncherActivityInfo(); - if (activityInfo != null) { - info.setProgressLevel( - PackageManagerHelper - .getLoadingProgress(activityInfo), - PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); - } - - if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) { - tempPackageKey.update(targetPkg, c.user); - SessionInfo si = installingPkgs.get(tempPackageKey); - if (si == null) { - info.runtimeStatusFlags &= - ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; - } else if (activityInfo == null) { - int installProgress = (int) (si.getProgress() * 100); - - info.setProgressLevel( - installProgress, - PackageInstallInfo.STATUS_INSTALLING); - } - } - - c.checkAndAddItem(info, mBgDataModel, logger); - } else { - throw new RuntimeException("Unexpected null WorkspaceItemInfo"); - } - break; - - case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: - FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id); - c.applyCommonProperties(folderInfo); - - // Do not trim the folder label, as is was set by the user. - folderInfo.title = c.getString(c.titleIndex); - folderInfo.spanX = 1; - folderInfo.spanY = 1; - folderInfo.options = c.getInt(optionsIndex); - - // no special handling required for restored folders - c.markRestored(); - - c.checkAndAddItem(folderInfo, mBgDataModel, logger); - break; - - case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: - if (WidgetsModel.GO_DISABLE_WIDGETS) { - c.markDeleted("Only legacy shortcuts can have null package"); - continue; - } - // Follow through - case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: - // Read all Launcher-specific widget details - boolean customWidget = c.itemType == - LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; - - int appWidgetId = c.getInt(appWidgetIdIndex); - String savedProvider = c.getString(appWidgetProviderIndex); - final ComponentName component; - - boolean isSearchWidget = (c.getInt(optionsIndex) - & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0; - if (isSearchWidget) { - component = QsbContainerView.getSearchComponentName(context); - if (component == null) { - c.markDeleted("Discarding SearchWidget without packagename "); - continue; - } - } else { - component = ComponentName.unflattenFromString(savedProvider); - } - final boolean isIdValid = !c.hasRestoreFlag( - LauncherAppWidgetInfo.FLAG_ID_NOT_VALID); - final boolean wasProviderReady = !c.hasRestoreFlag( - LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY); - - ComponentKey providerKey = new ComponentKey(component, c.user); - if (!mWidgetProvidersMap.containsKey(providerKey)) { - mWidgetProvidersMap.put(providerKey, - widgetHelper.findProvider(component, c.user)); - } - final AppWidgetProviderInfo provider = - mWidgetProvidersMap.get(providerKey); - - final boolean isProviderReady = isValidProvider(provider); - if (!isSafeMode && !customWidget && - wasProviderReady && !isProviderReady) { - c.markDeleted( - "Deleting widget that isn't installed anymore: " - + provider); - } else { - if (isProviderReady) { - appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, - provider.provider); - - // The provider is available. So the widget is either - // available or not available. We do not need to track - // any future restore updates. - int status = c.restoreFlag & - ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED & - ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; - if (!wasProviderReady) { - // If provider was not previously ready, update the - // status and UI flag. - - // Id would be valid only if the widget restore broadcast was received. - if (isIdValid) { - status |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; - } - } - appWidgetInfo.restoreStatus = status; - } else { - Log.v(TAG, "Widget restore pending id=" + c.id - + " appWidgetId=" + appWidgetId - + " status =" + c.restoreFlag); - appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, - component); - appWidgetInfo.restoreStatus = c.restoreFlag; - - tempPackageKey.update(component.getPackageName(), c.user); - SessionInfo si = - installingPkgs.get(tempPackageKey); - Integer installProgress = si == null - ? null - : (int) (si.getProgress() * 100); - - if (c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED)) { - // Restore has started once. - } else if (installProgress != null) { - // App restore has started. Update the flag - appWidgetInfo.restoreStatus |= - LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; - } else if (!isSafeMode) { - c.markDeleted("Unrestored widget removed: " + component); - continue; - } - - appWidgetInfo.installProgress = - installProgress == null ? 0 : installProgress; - } - if (appWidgetInfo.hasRestoreFlag( - LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) { - appWidgetInfo.bindOptions = c.parseIntent(); - } - - c.applyCommonProperties(appWidgetInfo); - appWidgetInfo.spanX = c.getInt(spanXIndex); - appWidgetInfo.spanY = c.getInt(spanYIndex); - appWidgetInfo.options = c.getInt(optionsIndex); - appWidgetInfo.user = c.user; - appWidgetInfo.sourceContainer = c.getInt(sourceContainerIndex); - - if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) { - c.markDeleted("Widget has invalid size: " - + appWidgetInfo.spanX + "x" + appWidgetInfo.spanY); - continue; - } - widgetProviderInfo = - widgetHelper.getLauncherAppWidgetInfo(appWidgetId); - if (widgetProviderInfo != null - && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX - || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) { - FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent() - + " minSizes not meet: span=" + appWidgetInfo.spanX - + "x" + appWidgetInfo.spanY + " minSpan=" - + widgetProviderInfo.minSpanX + "x" - + widgetProviderInfo.minSpanY); - logWidgetInfo(mApp.getInvariantDeviceProfile(), - widgetProviderInfo); - } - if (!c.isOnWorkspaceOrHotseat()) { - c.markDeleted("Widget found where container != " + - "CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!"); - continue; - } - - if (!customWidget) { - String providerName = - appWidgetInfo.providerName.flattenToString(); - if (!providerName.equals(savedProvider) || - (appWidgetInfo.restoreStatus != c.restoreFlag)) { - c.updater() - .put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, - providerName) - .put(LauncherSettings.Favorites.RESTORED, - appWidgetInfo.restoreStatus) - .commit(); - } - } - - if (appWidgetInfo.restoreStatus != - LauncherAppWidgetInfo.RESTORE_COMPLETED) { - appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo( - mApp.getContext(), - appWidgetInfo.providerName, - appWidgetInfo.user); - mIconCache.getTitleAndIconForApp( - appWidgetInfo.pendingItemInfo, false); - } - - c.checkAndAddItem(appWidgetInfo, mBgDataModel); - } - break; - } - } catch (Exception e) { - Log.e(TAG, "Desktop items loading interrupted", e); - } - } - if (FeatureFlags.ENABLE_BULK_WORKSPACE_ICON_LOADING.get()) { - Trace.beginSection("LoadWorkspaceIconsInBulk"); - try { - mIconCache.getTitlesAndIconsInBulk(iconRequestInfos); - for (IconRequestInfo iconRequestInfo : - iconRequestInfos) { - WorkspaceItemInfo wai = iconRequestInfo.itemInfo; - if (mIconCache.isDefaultIcon(wai.bitmap, wai.user)) { - iconRequestInfo.loadWorkspaceIcon(mApp.getContext()); - } - } - } finally { - Trace.endSection(); - } + processWorkspaceItem(c, memoryLogger, installingPkgs, isSdCardReady, + tempPackageKey, widgetHelper, pmHelper, shortcutKeyToPinnedShortcuts, + iconRequestInfos, unlockedUsers, isSafeMode, allDeepShortcuts); } + maybeLoadWorkspaceIconsInBulk(iconRequestInfos); } finally { IOUtils.closeSilently(c); } // Load delegate items - mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts); + mModelDelegate.loadHotseatItems(mUserManagerState, shortcutKeyToPinnedShortcuts); + mModelDelegate.loadAllAppsItems(mUserManagerState, shortcutKeyToPinnedShortcuts); + mModelDelegate.loadWidgetsRecommendationItems(); + mModelDelegate.markActive(); // Load string cache mModelDelegate.loadStringCache(mBgDataModel.stringCache); @@ -885,7 +456,7 @@ public class LoaderTask implements Runnable { info.rank = rank; if (info.usingLowResIcon() - && info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION + && info.itemType == Favorites.ITEM_TYPE_APPLICATION && verifier.isItemInPreview(info.rank)) { mIconCache.getTitleAndIcon(info, false); } @@ -896,6 +467,418 @@ public class LoaderTask implements Runnable { } } + private void processWorkspaceItem(LoaderCursor c, + LoaderMemoryLogger memoryLogger, + HashMap installingPkgs, + boolean isSdCardReady, + PackageUserKey tempPackageKey, + WidgetManagerHelper widgetHelper, + PackageManagerHelper pmHelper, + Map shortcutKeyToPinnedShortcuts, + List> iconRequestInfos, + LongSparseArray unlockedUsers, + boolean isSafeMode, + List allDeepShortcuts) { + + try { + if (c.user == null) { + // User has been deleted, remove the item. + c.markDeleted("User has been deleted"); + return; + } + + boolean allowMissingTarget = false; + switch (c.itemType) { + case Favorites.ITEM_TYPE_SHORTCUT: + case Favorites.ITEM_TYPE_APPLICATION: + case Favorites.ITEM_TYPE_DEEP_SHORTCUT: + Intent intent = c.parseIntent(); + if (intent == null) { + c.markDeleted("Invalid or null intent"); + return; + } + + int disabledState = mUserManagerState.isUserQuiet(c.serialNumber) + ? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0; + ComponentName cn = intent.getComponent(); + String targetPkg = cn == null ? intent.getPackage() : cn.getPackageName(); + + if (TextUtils.isEmpty(targetPkg) + && c.itemType != Favorites.ITEM_TYPE_SHORTCUT) { + c.markDeleted("Only legacy shortcuts can have null package"); + return; + } + + // If there is no target package, it's an implicit intent + // (legacy shortcut) which is always valid + boolean validTarget = TextUtils.isEmpty(targetPkg) + || mLauncherApps.isPackageEnabled(targetPkg, c.user); + + // If it's a deep shortcut, we'll use pinned shortcuts to restore it + if (cn != null && validTarget && c.itemType + != Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + // If the apk is present and the shortcut points to a specific component. + + // If the component is already present + if (mLauncherApps.isActivityEnabled(cn, c.user)) { + // no special handling necessary for this item + c.markRestored(); + } else { + // Gracefully try to find a fallback activity. + intent = pmHelper.getAppLaunchIntent(targetPkg, c.user); + if (intent != null) { + c.restoreFlag = 0; + c.updater().put( + Favorites.INTENT, + intent.toUri(0)).commit(); + cn = intent.getComponent(); + } else { + c.markDeleted("Unable to find a launch target"); + return; + } + } + } + // else if cn == null => can't infer much, leave it + // else if !validPkg => could be restored icon or missing sd-card + + if (!TextUtils.isEmpty(targetPkg) && !validTarget) { + // Points to a valid app (superset of cn != null) but the apk + // is not available. + + if (c.restoreFlag != 0) { + // Package is not yet available but might be + // installed later. + FileLog.d(TAG, "package not yet restored: " + targetPkg); + + tempPackageKey.update(targetPkg, c.user); + if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) { + // Restore has started once. + } else if (installingPkgs.containsKey(tempPackageKey)) { + // App restore has started. Update the flag + c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED; + c.updater().put(Favorites.RESTORED, + c.restoreFlag).commit(); + } else { + c.markDeleted("Unrestored app removed: " + targetPkg); + return; + } + } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) { + // Package is present but not available. + disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE; + // Add the icon on the workspace anyway. + allowMissingTarget = true; + } else if (!isSdCardReady) { + // SdCard is not ready yet. Package might get available, + // once it is ready. + Log.d(TAG, "Missing pkg, will check later: " + targetPkg); + mPendingPackages.add(new PackageUserKey(targetPkg, c.user)); + // Add the icon on the workspace anyway. + allowMissingTarget = true; + } else { + // Do not wait for external media load anymore. + c.markDeleted("Invalid package removed: " + targetPkg); + return; + } + } + + if ((c.restoreFlag & WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) != 0) { + validTarget = false; + } + + if (validTarget) { + // The shortcut points to a valid target (either no target + // or something which is ready to be used) + c.markRestored(); + } + + boolean useLowResIcon = !c.isOnWorkspaceOrHotseat(); + + WorkspaceItemInfo info; + if (c.restoreFlag != 0) { + // Already verified above that user is same as default user + info = c.getRestoredItemInfo(intent); + } else if (c.itemType == Favorites.ITEM_TYPE_APPLICATION) { + info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, + !FeatureFlags.ENABLE_BULK_WORKSPACE_ICON_LOADING.get()); + } else if (c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + ShortcutKey key = ShortcutKey.fromIntent(intent, c.user); + if (unlockedUsers.get(c.serialNumber)) { + ShortcutInfo pinnedShortcut = shortcutKeyToPinnedShortcuts.get(key); + if (pinnedShortcut == null) { + // The shortcut is no longer valid. + c.markDeleted("Pinned shortcut not found"); + return; + } + info = new WorkspaceItemInfo(pinnedShortcut, mApp.getContext()); + // If the pinned deep shortcut is no longer published, + // use the last saved icon instead of the default. + mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon); + + if (pmHelper.isAppSuspended( + pinnedShortcut.getPackage(), info.user)) { + info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED; + } + intent = info.getIntent(); + allDeepShortcuts.add(pinnedShortcut); + } else { + // Create a shortcut info in disabled mode for now. + info = c.loadSimpleWorkspaceItem(); + info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER; + } + } else { // item type == ITEM_TYPE_SHORTCUT + info = c.loadSimpleWorkspaceItem(); + + // Shortcuts are only available on the primary profile + if (!TextUtils.isEmpty(targetPkg) + && pmHelper.isAppSuspended(targetPkg, c.user)) { + disabledState |= FLAG_DISABLED_SUSPENDED; + } + info.options = c.getOptions(); + + // App shortcuts that used to be automatically added to Launcher + // didn't always have the correct intent flags set, so do that here + if (intent.getAction() != null + && intent.getCategories() != null + && intent.getAction().equals(Intent.ACTION_MAIN) + && intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + } + } + + if (info != null) { + if (info.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT) { + // Skip deep shortcuts; their title and icons have already been + // loaded above. + iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon)); + } + + c.applyCommonProperties(info); + + info.intent = intent; + info.rank = c.getRank(); + info.spanX = 1; + info.spanY = 1; + info.runtimeStatusFlags |= disabledState; + if (isSafeMode && !isSystemApp(mApp.getContext(), intent)) { + info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE; + } + LauncherActivityInfo activityInfo = c.getLauncherActivityInfo(); + if (activityInfo != null) { + info.setProgressLevel( + PackageManagerHelper.getLoadingProgress(activityInfo), + PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING); + } + + if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) { + tempPackageKey.update(targetPkg, c.user); + SessionInfo si = installingPkgs.get(tempPackageKey); + if (si == null) { + info.runtimeStatusFlags + &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; + } else if (activityInfo == null) { + int installProgress = (int) (si.getProgress() * 100); + + info.setProgressLevel(installProgress, + PackageInstallInfo.STATUS_INSTALLING); + } + } + + c.checkAndAddItem(info, mBgDataModel, memoryLogger); + } else { + throw new RuntimeException("Unexpected null WorkspaceItemInfo"); + } + break; + + case Favorites.ITEM_TYPE_FOLDER: + FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id); + c.applyCommonProperties(folderInfo); + + // Do not trim the folder label, as is was set by the user. + folderInfo.title = c.getString(c.mTitleIndex); + folderInfo.spanX = 1; + folderInfo.spanY = 1; + folderInfo.options = c.getOptions(); + + // no special handling required for restored folders + c.markRestored(); + + c.checkAndAddItem(folderInfo, mBgDataModel, memoryLogger); + break; + + case Favorites.ITEM_TYPE_APPWIDGET: + if (WidgetsModel.GO_DISABLE_WIDGETS) { + c.markDeleted("Only legacy shortcuts can have null package"); + return; + } + // Follow through + case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: + // Read all Launcher-specific widget details + boolean customWidget = c.itemType + == Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + + int appWidgetId = c.getAppWidgetId(); + String savedProvider = c.getAppWidgetProvider(); + final ComponentName component; + + if ((c.getOptions() & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0) { + component = QsbContainerView.getSearchComponentName(mApp.getContext()); + if (component == null) { + c.markDeleted("Discarding SearchWidget without packagename "); + return; + } + } else { + component = ComponentName.unflattenFromString(savedProvider); + } + final boolean isIdValid = + !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID); + final boolean wasProviderReady = + !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY); + + ComponentKey providerKey = new ComponentKey(component, c.user); + if (!mWidgetProvidersMap.containsKey(providerKey)) { + mWidgetProvidersMap.put(providerKey, + widgetHelper.findProvider(component, c.user)); + } + final AppWidgetProviderInfo provider = mWidgetProvidersMap.get(providerKey); + + final boolean isProviderReady = isValidProvider(provider); + if (!isSafeMode && !customWidget && wasProviderReady && !isProviderReady) { + c.markDeleted("Deleting widget that isn't installed anymore: " + provider); + } else { + LauncherAppWidgetInfo appWidgetInfo; + if (isProviderReady) { + appWidgetInfo = + new LauncherAppWidgetInfo(appWidgetId, provider.provider); + + // The provider is available. So the widget is either + // available or not available. We do not need to track + // any future restore updates. + int status = c.restoreFlag + & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED + & ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; + if (!wasProviderReady) { + // If provider was not previously ready, update status and UI flag. + + // Id would be valid only if the widget restore broadcast received. + if (isIdValid) { + status |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; + } + } + appWidgetInfo.restoreStatus = status; + } else { + Log.v(TAG, "Widget restore pending id=" + c.id + + " appWidgetId=" + appWidgetId + + " status =" + c.restoreFlag); + appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component); + appWidgetInfo.restoreStatus = c.restoreFlag; + + tempPackageKey.update(component.getPackageName(), c.user); + SessionInfo si = installingPkgs.get(tempPackageKey); + Integer installProgress = si == null + ? null + : (int) (si.getProgress() * 100); + + if (c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED)) { + // Restore has started once. + } else if (installProgress != null) { + // App restore has started. Update the flag + appWidgetInfo.restoreStatus + |= LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; + } else if (!isSafeMode) { + c.markDeleted("Unrestored widget removed: " + component); + return; + } + + appWidgetInfo.installProgress = + installProgress == null ? 0 : installProgress; + } + if (appWidgetInfo.hasRestoreFlag( + LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) { + appWidgetInfo.bindOptions = c.parseIntent(); + } + + c.applyCommonProperties(appWidgetInfo); + appWidgetInfo.spanX = c.getSpanX(); + appWidgetInfo.spanY = c.getSpanY(); + appWidgetInfo.options = c.getOptions(); + appWidgetInfo.user = c.user; + appWidgetInfo.sourceContainer = c.getAppWidgetSource(); + + if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) { + c.markDeleted("Widget has invalid size: " + + appWidgetInfo.spanX + "x" + appWidgetInfo.spanY); + return; + } + LauncherAppWidgetProviderInfo widgetProviderInfo = + widgetHelper.getLauncherAppWidgetInfo(appWidgetId); + if (widgetProviderInfo != null + && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX + || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) { + FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent() + + " minSizes not meet: span=" + appWidgetInfo.spanX + + "x" + appWidgetInfo.spanY + " minSpan=" + + widgetProviderInfo.minSpanX + "x" + + widgetProviderInfo.minSpanY); + logWidgetInfo(mApp.getInvariantDeviceProfile(), + widgetProviderInfo); + } + if (!c.isOnWorkspaceOrHotseat()) { + c.markDeleted("Widget found where container != CONTAINER_DESKTOP" + + "nor CONTAINER_HOTSEAT - ignoring!"); + return; + } + + if (!customWidget) { + String providerName = appWidgetInfo.providerName.flattenToString(); + if (!providerName.equals(savedProvider) + || (appWidgetInfo.restoreStatus != c.restoreFlag)) { + c.updater() + .put(Favorites.APPWIDGET_PROVIDER, + providerName) + .put(Favorites.RESTORED, + appWidgetInfo.restoreStatus) + .commit(); + } + } + + if (appWidgetInfo.restoreStatus + != LauncherAppWidgetInfo.RESTORE_COMPLETED) { + appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo( + mApp.getContext(), + appWidgetInfo.providerName, + appWidgetInfo.user); + mIconCache.getTitleAndIconForApp( + appWidgetInfo.pendingItemInfo, false); + } + + c.checkAndAddItem(appWidgetInfo, mBgDataModel); + } + break; + } + } catch (Exception e) { + Log.e(TAG, "Desktop items loading interrupted", e); + } + } + + private void maybeLoadWorkspaceIconsInBulk( + List> iconRequestInfos) { + if (FeatureFlags.ENABLE_BULK_WORKSPACE_ICON_LOADING.get()) { + Trace.beginSection("LoadWorkspaceIconsInBulk"); + try { + mIconCache.getTitlesAndIconsInBulk(iconRequestInfos); + for (IconRequestInfo iconRequestInfo : iconRequestInfos) { + WorkspaceItemInfo wai = iconRequestInfo.itemInfo; + if (mIconCache.isDefaultIcon(wai.bitmap, wai.user)) { + iconRequestInfo.loadWorkspaceIcon(mApp.getContext()); + } + } + } finally { + Trace.endSection(); + } + } + } + private void setIgnorePackages(IconCacheUpdateHandler updateHandler) { // Ignore packages which have a promise icon. synchronized (mBgDataModel) { @@ -917,15 +900,12 @@ public class LoaderTask implements Runnable { } } - private void sanitizeData() { - Context context = mApp.getContext(); - ContentResolver contentResolver = context.getContentResolver(); - if (mItemsDeleted) { + private void sanitizeFolders(boolean itemsDeleted) { + if (itemsDeleted) { // Remove any empty folder - int[] deletedFolderIds = LauncherSettings.Settings - .call(contentResolver, - LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS) - .getIntArray(LauncherSettings.Settings.EXTRA_VALUE); + int[] deletedFolderIds = Settings.call(mApp.getContext().getContentResolver(), + Settings.METHOD_DELETE_EMPTY_FOLDERS) + .getIntArray(Settings.EXTRA_VALUE); synchronized (mBgDataModel) { for (int folderId : deletedFolderIds) { mBgDataModel.workspaceItems.remove(mBgDataModel.folders.get(folderId)); @@ -933,11 +913,16 @@ public class LoaderTask implements Runnable { mBgDataModel.itemsIdMap.remove(folderId); } } - } + } + + private void sanitizeWidgetsShortcutsAndPackages() { + Context context = mApp.getContext(); + ContentResolver contentResolver = context.getContentResolver(); + // Remove any ghost widgets - LauncherSettings.Settings.call(contentResolver, - LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS); + Settings.call(contentResolver, + Settings.METHOD_REMOVE_GHOST_WIDGETS); // Update pinned state of model shortcuts mBgDataModel.updateShortcutPinnedState(context); @@ -1107,10 +1092,12 @@ public class LoaderTask implements Runnable { FileLog.d(TAG, widgetDimension.toString()); } - private static void logASplit(final TimingLogger logger, final String label) { - logger.addSplit(label); - if (DEBUG) { - Log.d(TAG, label); + private static void logASplit(@Nullable TimingLogger timingLogger, String label) { + if (timingLogger != null) { + timingLogger.addSplit(label); + if (DEBUG) { + Log.d(TAG, label); + } } } } diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java index 3bd9470566..0639a6cbe2 100644 --- a/src/com/android/launcher3/model/ModelDelegate.java +++ b/src/com/android/launcher3/model/ModelDelegate.java @@ -80,10 +80,29 @@ public class ModelDelegate implements ResourceBasedOverride { } /** - * Load delegate items if any in the data model + * Load hot seat items if any in the data model */ @WorkerThread - public void loadItems(UserManagerState ums, Map pinnedShortcuts) { } + public void loadHotseatItems(UserManagerState ums, + Map pinnedShortcuts) { } + + /** + * Load all apps items if any in the data model + */ + @WorkerThread + public void loadAllAppsItems(UserManagerState ums, + Map pinnedShortcuts) { } + + /** + * Load widget recommendation items if any in the data model + */ + @WorkerThread + public void loadWidgetsRecommendationItems() { } + + /** + * Marks the ModelDelegate as active + */ + public void markActive() { } /** * Load String cache diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java index d31a6460db..0b756b6dfc 100644 --- a/src/com/android/launcher3/testing/TestInformationHandler.java +++ b/src/com/android/launcher3/testing/TestInformationHandler.java @@ -208,6 +208,11 @@ public class TestInformationHandler implements ResourceBasedOverride { ); } + case TestProtocol.REQUEST_WORKSPACE_CURRENT_PAGE_INDEX: { + return getLauncherUIProperty(Bundle::putInt, + launcher -> launcher.getWorkspace().getCurrentPage()); + } + case TestProtocol.REQUEST_HOTSEAT_CELL_CENTER: { final HotseatCellCenterRequest request = extra.getParcelable( TestProtocol.TEST_INFO_REQUEST_FIELD); diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java index 11363a25fe..65873f199e 100644 --- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java +++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java @@ -126,6 +126,9 @@ public final class TestProtocol { public static final String REQUEST_WORKSPACE_CELL_CENTER = "workspace-cell-center"; public static final String REQUEST_WORKSPACE_COLUMNS_ROWS = "workspace-columns-rows"; + public static final String REQUEST_WORKSPACE_CURRENT_PAGE_INDEX = + "workspace-current-page-index"; + public static final String REQUEST_HOTSEAT_CELL_CENTER = "hotseat-cell-center"; public static final String REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET = diff --git a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java index 3443fd9b1a..77fca96f04 100644 --- a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java +++ b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java @@ -114,6 +114,10 @@ public class ReorderWidgets extends AbstractLauncherUiTest { new FavoriteItemsTransaction(mTargetContext, this); transaction = buildWorkspaceFromBoards(testCase.mStart, transaction); transaction.commit(); + // resetLoaderState triggers the launcher to start loading the workspace which allows + // waitForLauncherCondition to wait for that condition, otherwise the condition would + // always be true and it wouldn't wait for the changes to be applied. + resetLoaderState(); waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading()); Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.getCellX(), mainWidgetCellPos.getCellY()); diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java index 6444ef6927..7ab86ad4fa 100644 --- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java +++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java @@ -18,6 +18,9 @@ package com.android.launcher3.model; import static androidx.test.InstrumentationRegistry.getContext; +import static com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_ID; +import static com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_PROVIDER; +import static com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_SOURCE; import static com.android.launcher3.LauncherSettings.Favorites.CELLX; import static com.android.launcher3.LauncherSettings.Favorites.CELLY; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER; @@ -30,9 +33,13 @@ import static com.android.launcher3.LauncherSettings.Favorites.INTENT; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; +import static com.android.launcher3.LauncherSettings.Favorites.OPTIONS; import static com.android.launcher3.LauncherSettings.Favorites.PROFILE_ID; +import static com.android.launcher3.LauncherSettings.Favorites.RANK; import static com.android.launcher3.LauncherSettings.Favorites.RESTORED; import static com.android.launcher3.LauncherSettings.Favorites.SCREEN; +import static com.android.launcher3.LauncherSettings.Favorites.SPANX; +import static com.android.launcher3.LauncherSettings.Favorites.SPANY; import static com.android.launcher3.LauncherSettings.Favorites.TITLE; import static com.android.launcher3.LauncherSettings.Favorites._ID; import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY; @@ -92,7 +99,9 @@ public class LoaderCursorTest { mCursor = new MatrixCursor(new String[] { ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE, _ID, CONTAINER, ITEM_TYPE, PROFILE_ID, - SCREEN, CELLX, CELLY, RESTORED, INTENT + SCREEN, CELLX, CELLY, RESTORED, INTENT, + APPWIDGET_ID, APPWIDGET_PROVIDER, SPANX, + SPANY, RANK, OPTIONS, APPWIDGET_SOURCE }); UserManagerState ums = new UserManagerState(); diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java index 473cfb9cb8..0197a11383 100644 --- a/tests/tapl/com/android/launcher3/tapl/Workspace.java +++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java @@ -67,6 +67,7 @@ public final class Workspace extends Home { "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W" + ".*?metaState=META_CTRL_ON"); static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick"); + public static final int MAX_WORKSPACE_DRAG_TRIES = 100; private final UiObject2 mHotseat; @@ -430,6 +431,12 @@ public final class Workspace extends Home { TestProtocol.TEST_INFO_RESPONSE_FIELD); } + /** Returns the index of the current page */ + static int geCurrentPage(LauncherInstrumentation launcher) { + return launcher.getTestInfo(TestProtocol.REQUEST_WORKSPACE_CURRENT_PAGE_INDEX).getInt( + TestProtocol.TEST_INFO_RESPONSE_FIELD); + } + /** * Finds folder icons in the current workspace. * @@ -569,21 +576,10 @@ public final class Workspace extends Home { expectLongClickEvents, /* runToSpringLoadedState= */ true); Point targetDest = getCellCenter(launcher, cellX, cellY, spanX, spanY); - int displayX = launcher.getRealDisplaySize().x; - // Since the destination can be on another page, we need to drag to the edge first // until we reach the target page - for (int i = 0; i < destinationWorkspace; i++) { - // Don't drag all the way to the edge to prevent touch events from getting out of - //screen bounds. - Point screenEdge = new Point(displayX - 1, targetDest.y); - Point finalDragStart = dragStart; - executeAndWaitForPageScroll(launcher, - () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS, - true, downTime, downTime, true, - LauncherInstrumentation.GestureScope.INSIDE)); - dragStart = screenEdge; - } + dragStart = dragToGivenWorkspace(launcher, dragStart, destinationWorkspace, + targetDest.y); // targetDest.x is now between 0 and displayX so we found the target page, // we just have to put move the icon to the destination and drop it @@ -595,6 +591,45 @@ public final class Workspace extends Home { } } + /** + * Given a drag that already started at currentPosition, drag the item to the given destination + * index defined by destinationWorkspaceIndex. + * + * @param launcher + * @param currentPosition + * @param destinationWorkspaceIndex + * @param y + * @return the finishing position of the drag. + */ + static Point dragToGivenWorkspace(LauncherInstrumentation launcher, Point currentPosition, + int destinationWorkspaceIndex, int y) { + final long downTime = SystemClock.uptimeMillis(); + int displayX = launcher.getRealDisplaySize().x; + int currentPage = Workspace.geCurrentPage(launcher); + int counter = 0; + while (currentPage != destinationWorkspaceIndex) { + counter++; + if (counter > MAX_WORKSPACE_DRAG_TRIES) { + throw new RuntimeException( + "Wrong destination workspace index " + destinationWorkspaceIndex + + ", desired workspace was never reached"); + } + // if the destination is greater than current page, set the display edge to be the + // right edge. Don't drag all the way to the edge to prevent touch events from + // getting out of screen bounds. + int displayEdge = destinationWorkspaceIndex > currentPage ? displayX - 1 : 1; + Point screenEdge = new Point(displayEdge, y); + Point finalDragStart = currentPosition; + executeAndWaitForPageScroll(launcher, + () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS, + true, downTime, downTime, true, + LauncherInstrumentation.GestureScope.INSIDE)); + currentPage = Workspace.geCurrentPage(launcher); + currentPosition = screenEdge; + } + return currentPosition; + } + private static void executeAndWaitForPageScroll(LauncherInstrumentation launcher, Runnable command) { launcher.executeAndWaitForEvent(command,