Merge "Track bottom of the app window when ENABLE_OVERVIEW_ACTIONS is true" into ub-launcher3-master
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index f3db20e..9123959 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,2 +1,2 @@
[Hook Scripts]
-checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT}
diff --git a/go/src/com/android/launcher3/model/LoaderResults.java b/go/src/com/android/launcher3/model/LoaderResults.java
index 26c3313..7130531 100644
--- a/go/src/com/android/launcher3/model/LoaderResults.java
+++ b/go/src/com/android/launcher3/model/LoaderResults.java
@@ -16,10 +16,11 @@
package com.android.launcher3.model;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.model.BgDataModel.Callbacks;
-
-import java.lang.ref.WeakReference;
+import com.android.launcher3.util.LooperExecutor;
/**
* Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}.
@@ -27,8 +28,13 @@
public class LoaderResults extends BaseLoaderResults {
public LoaderResults(LauncherAppState app, BgDataModel dataModel,
- AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
- super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+ AllAppsList allAppsList, Callbacks[] callbacks) {
+ this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+ }
+
+ public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+ AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+ super(app, dataModel, allAppsList, callbacks, executor);
}
@Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
index 0fd4aac..923e050 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -77,7 +77,7 @@
}
void finishOnboarding() {
- mLauncher.rebindModel();
+ mLauncher.getModel().rebindCallbacks();
mLauncher.getSharedPrefs().edit().putBoolean(KEY_HOTSEAT_EDU_SEEN, true).apply();
removeNotification();
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 27ac284..bd89626 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -47,7 +47,7 @@
private static final float RING_EFFECT_RATIO = 0.11f;
- private DeviceProfile mDeviceProfile;
+ private final DeviceProfile mDeviceProfile;
private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private boolean mIsPinned = false;
private int mNormalizedIconRadius;
@@ -65,7 +65,7 @@
mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
setOnClickListener(ItemClickHandler.INSTANCE);
- setOnFocusChangeListener(Launcher.getLauncher(context).mFocusHandler);
+ setOnFocusChangeListener(Launcher.getLauncher(context).getFocusHandler());
}
@Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index bd37e56..73c0c97 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -173,7 +173,7 @@
@Override
public String getDescription(Launcher launcher) {
- return launcher.getString(R.string.accessibility_desc_recent_apps);
+ return launcher.getString(R.string.accessibility_recent_apps);
}
public static float getDefaultSwipeHeight(Launcher launcher) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index ad02de1..32855d7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -18,7 +18,7 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
@@ -286,7 +286,7 @@
}
});
}
- if (QUICKSTEP_SPRINGS.get()) {
+ if (UNSTABLE_SPRINGS.get()) {
mCurrentAnimation.dispatchOnStartWithVelocity(goingToEnd ? 1f : 0f, velocity);
}
anim.start();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
index 1b60404..8b5283e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -21,7 +21,7 @@
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
@@ -973,7 +973,7 @@
}
mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
- if (QUICKSTEP_SPRINGS.get()) {
+ if (UNSTABLE_SPRINGS.get()) {
mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y);
}
mLauncherTransitionController.getAnimationPlayer().start();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 29df5cc..71568b3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -23,6 +23,7 @@
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.config.FeatureFlags.FAKE_LANDSCAPE_UI;
import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
@@ -715,6 +716,7 @@
pw.println("FeatureFlags:");
pw.println(" APPLY_CONFIG_AT_RUNTIME=" + APPLY_CONFIG_AT_RUNTIME.get());
pw.println(" QUICKSTEP_SPRINGS=" + QUICKSTEP_SPRINGS.get());
+ pw.println(" UNSTABLE_SPRINGS=" + UNSTABLE_SPRINGS.get());
pw.println(" ADAPTIVE_ICON_WINDOW_ANIM=" + ADAPTIVE_ICON_WINDOW_ANIM.get());
pw.println(" ENABLE_QUICKSTEP_LIVE_TILE=" + ENABLE_QUICKSTEP_LIVE_TILE.get());
pw.println(" ENABLE_HINTS_IN_OVERVIEW=" + ENABLE_HINTS_IN_OVERVIEW.get());
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index 47bc31a..c836791 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -31,7 +31,7 @@
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
@@ -1105,13 +1105,13 @@
private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
- if (QUICKSTEP_SPRINGS.get() && taskView instanceof TaskView)
+ if (UNSTABLE_SPRINGS.get() && taskView instanceof TaskView) {
addAnim(new SpringObjectAnimator<>(taskView, VIEW_TRANSLATE_Y,
MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
SpringForce.STIFFNESS_MEDIUM,
0, -taskView.getHeight()),
duration, LINEAR, anim);
- else {
+ } else {
addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
duration, LINEAR, anim);
}
@@ -1185,7 +1185,7 @@
}
int scrollDiff = newScroll[i] - oldScroll[i] + offset;
if (scrollDiff != 0) {
- if (QUICKSTEP_SPRINGS.get() && child instanceof TaskView) {
+ if (UNSTABLE_SPRINGS.get() && child instanceof TaskView) {
addAnim(new SpringObjectAnimator<>(child, VIEW_TRANSLATE_X,
MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
SpringForce.STIFFNESS_MEDIUM,
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index ce87527..2b21df8 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -29,9 +29,6 @@
<!-- Title for an option to enter freeform mode for a given app -->
<string name="recent_task_option_freeform">Freeform</string>
- <!-- Content description for the recent apps panel (not shown on the screen). [CHAR LIMIT=NONE] -->
- <string name="accessibility_desc_recent_apps">Overview</string>
-
<!-- Recents: The empty recents string. [CHAR LIMIT=NONE] -->
<string name="recents_empty_message">No recent items</string>
@@ -70,19 +67,19 @@
<string name="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
<!-- Hotseat migration notification title -->
- <string translatable="false" name="hotseat_migrate_prompt_title">Your Hotseat just got smarter</string>
+ <string translatable="false" name="hotseat_migrate_prompt_title">Get suggested apps on the home screen</string>
<!-- Hotseat migration notification content -->
- <string translatable="false" name="hotseat_migrate_prompt_content">Tap here to setup and learn more</string>
+ <string translatable="false" name="hotseat_migrate_prompt_content">Tap to set up</string>
<!-- Hotseat migration wizard title -->
- <string translatable="false" name="hotseat_migrate_title">Pixel Suggests apps you\'ll need next</string>
+ <string translatable="false" name="hotseat_migrate_title">Suggested apps replace the bottom row of apps</string>
<!-- Hotseat migration wizard message -->
- <string translatable="false" name="hotseat_migrate_message">Suggested apps will replace the bottom row of apps. To pin an app, drag it over a suggested app. Touch & hold an app to hide it.</string>
+ <string translatable="false" name="hotseat_migrate_message">To pin a favorite app, drag it over a suggested app. To hide a suggested app, touch & hold it.</string>
<!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
<string translatable="false" name="hotseat_items_migrated">Your hotseat items have been moved to the last page.</string>
<!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
<string translatable="false" name="hotseat_no_migration">You can remove items from the hotseat manually to see suggested apps in their spot.</string>
<!-- Button text to opt in for fully predicted hotseat -->
- <string translatable="false" name="hotseat_migrate_accept">Migrate</string>
+ <string translatable="false" name="hotseat_migrate_accept">I\'m in</string>
<!-- Button text to dismiss opt in for fully predicted hotseat -->
<string translatable="false" name="hotseat_migrate_dismiss">No thanks</string>
<!-- Hotseat onboard notification title -->
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 99b2a81..d5ce734 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -25,7 +25,7 @@
import static com.android.launcher3.anim.Interpolators.ACCEL;
import static com.android.launcher3.anim.Interpolators.DEACCEL;
import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
import android.animation.TimeInterpolator;
@@ -277,7 +277,7 @@
private void handleFirstSwipeToOverview(final ValueAnimator animator,
final long expectedDuration, final LauncherState targetState, final float velocity,
final boolean isFling) {
- if (QUICKSTEP_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
+ if (UNSTABLE_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
&& targetState == OVERVIEW) {
mFinishFastOnSecondTouch = true;
} else if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) {
diff --git a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
new file mode 100644
index 0000000..f769055
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 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.folder;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.shadows.LShadowUserManager;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+
+@RunWith(LauncherRoboTestRunner.class)
+public final class FolderNameProviderTest {
+ private Context mContext;
+ private WorkspaceItemInfo mItem1;
+ private WorkspaceItemInfo mItem2;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mItem1 = new WorkspaceItemInfo(new AppInfo(
+ new ComponentName("a.b.c", "a.b.c/a.b.c.d"),
+ "title1",
+ LShadowUserManager.newUserHandle(10),
+ new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d"))
+ ));
+ mItem2 = new WorkspaceItemInfo(new AppInfo(
+ new ComponentName("a.b.c", "a.b.c/a.b.c.d"),
+ "title2",
+ LShadowUserManager.newUserHandle(10),
+ new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d"))
+ ));
+ }
+
+ @Test
+ public void getSuggestedFolderName_workAssignedToEnd() {
+ ArrayList<WorkspaceItemInfo> list = new ArrayList<>();
+ list.add(mItem1);
+ list.add(mItem2);
+ String[] suggestedNameOut = new String[FolderNameProvider.SUGGEST_MAX];
+ new FolderNameProvider().getSuggestedFolderName(mContext, list, suggestedNameOut);
+ assertTrue(suggestedNameOut[0].equals("Work"));
+
+ suggestedNameOut[0] = "candidate1";
+ suggestedNameOut[1] = "candidate2";
+ suggestedNameOut[2] = "candidate3";
+ new FolderNameProvider().getSuggestedFolderName(mContext, list, suggestedNameOut);
+ assertTrue(suggestedNameOut[3].equals("Work"));
+
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
index ea7c137..b7f2243 100644
--- a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
@@ -127,7 +127,7 @@
@Test
public void testAddItem_some_items_added() throws Exception {
Callbacks callbacks = mock(Callbacks.class);
- mModelHelper.getModel().initialize(callbacks);
+ mModelHelper.getModel().addCallbacks(callbacks);
WorkspaceItemInfo info = new WorkspaceItemInfo();
info.intent = new Intent().setComponent(mComponent1);
diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index e0ddcb1..f8ac010 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -16,8 +16,9 @@
package com.android.launcher3.model;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.util.ReflectionHelpers.setField;
@@ -26,14 +27,10 @@
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageInstaller.SessionParams;
-import android.net.Uri;
-import android.provider.Settings;
import com.android.launcher3.FolderInfo;
-import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherProvider;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.model.BgDataModel.Callbacks;
@@ -48,12 +45,7 @@
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
-import org.robolectric.shadows.ShadowPackageManager;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStreamWriter;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
/**
@@ -63,40 +55,22 @@
@LooperMode(Mode.PAUSED)
public class DefaultLayoutProviderTest {
- private static final String SETTINGS_APP = "com.android.settings";
- private static final String TEST_PROVIDER_AUTHORITY =
- DefaultLayoutProviderTest.class.getName().toLowerCase();
-
- private static final int BITMAP_SIZE = 10;
- private static final int GRID_SIZE = 4;
-
private LauncherModelHelper mModelHelper;
private Context mTargetContext;
- private InvariantDeviceProfile mIdp;
@Before
public void setUp() {
mModelHelper = new LauncherModelHelper();
mTargetContext = RuntimeEnvironment.application;
- mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
- mIdp.numRows = mIdp.numColumns = mIdp.numHotseatIcons = GRID_SIZE;
- mIdp.iconBitmapSize = BITMAP_SIZE;
-
- mModelHelper.provider.setAllowLoadDefaultFavorites(true);
- Settings.Secure.putString(mTargetContext.getContentResolver(),
- "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
-
- ShadowPackageManager spm = shadowOf(mTargetContext.getPackageManager());
- spm.addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
- TEST_PROVIDER_AUTHORITY;
- spm.addActivityIfNotPresent(new ComponentName(SETTINGS_APP, SETTINGS_APP));
+ shadowOf(mTargetContext.getPackageManager())
+ .addActivityIfNotPresent(new ComponentName(TEST_PACKAGE, TEST_PACKAGE));
}
@Test
public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0)
- .putApp(SETTINGS_APP, SETTINGS_APP));
+ .putApp(TEST_PACKAGE, TEST_PACKAGE));
// Verify one item in hotseat
assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
@@ -108,9 +82,9 @@
@Test
public void testCustomProfileLoaded_with_folder() throws Exception {
writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
- .addApp(SETTINGS_APP, SETTINGS_APP)
- .addApp(SETTINGS_APP, SETTINGS_APP)
- .addApp(SETTINGS_APP, SETTINGS_APP)
+ .addApp(TEST_PACKAGE, TEST_PACKAGE)
+ .addApp(TEST_PACKAGE, TEST_PACKAGE)
+ .addApp(TEST_PACKAGE, TEST_PACKAGE)
.build());
// Verify folder
@@ -146,19 +120,13 @@
}
private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- builder.build(new OutputStreamWriter(bos));
-
- Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, mTargetContext);
- shadowOf(mTargetContext.getContentResolver()).registerInputStream(layoutUri,
- new ByteArrayInputStream(bos.toByteArray()));
+ mModelHelper.setupDefaultLayoutProvider(builder);
LoaderResults results = new LoaderResults(
LauncherAppState.getInstance(mTargetContext),
mModelHelper.getBgDataModel(),
mModelHelper.getAllAppsList(),
- 0,
- new WeakReference<>(mock(Callbacks.class)));
+ new Callbacks[0]);
LoaderTask task = new LoaderTask(
LauncherAppState.getInstance(mTargetContext),
mModelHelper.getAllAppsList(),
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index 8dd7588..1ed4bca 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -190,7 +190,7 @@
@Test
public void testWorkspace_items_not_merged_in_next_screen() throws Exception {
- // First screen has 2 items that need to be moved, but second screen has only one
+ // First screen has 2 mItems that need to be moved, but second screen has only one
// empty space after migration (top-left corner)
int[][][] ids = mModelHelper.createGrid(new int[][][]{{
{ 0, 0, 0, 1},
@@ -277,7 +277,7 @@
}
/**
- * Verifies that the workspace items are arranged in the provided order.
+ * Verifies that the workspace mItems are arranged in the provided order.
* @param ids A 3d array where the first dimension represents the screen, and the rest two
* represent the workspace grid.
*/
diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 4854314..7fa3ee9 100644
--- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -46,7 +46,6 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.LauncherApps;
import android.database.MatrixCursor;
import android.os.Process;
@@ -77,7 +76,6 @@
private MatrixCursor mCursor;
private InvariantDeviceProfile mIDP;
private Context mContext;
- private LauncherApps mLauncherApps;
private LoaderCursor mLoaderCursor;
@@ -86,7 +84,6 @@
mContext = RuntimeEnvironment.application;
mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
mApp = LauncherAppState.getInstance(mContext);
- mLauncherApps = mContext.getSystemService(LauncherApps.class);
mCursor = new MatrixCursor(new String[] {
ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE,
@@ -174,7 +171,7 @@
mIDP.numColumns = 4;
mIDP.numHotseatIcons = 3;
- // Overlapping items are not placed
+ // Overlapping mItems are not placed
assertTrue(mLoaderCursor.checkItemPlacement(
newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1)));
assertFalse(mLoaderCursor.checkItemPlacement(
@@ -200,7 +197,7 @@
mIDP.numColumns = 4;
mIDP.numHotseatIcons = 3;
- // Hotseat items are only placed based on screenId
+ // Hotseat mItems are only placed based on screenId
assertTrue(mLoaderCursor.checkItemPlacement(
newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1)));
assertTrue(mLoaderCursor.checkItemPlacement(
diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
new file mode 100644
index 0000000..c7979b2
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 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.model;
+
+import static com.android.launcher3.util.Executors.createAndStartNewForegroundLooper;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.spy;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Process;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+import com.android.launcher3.util.LooperExecutor;
+import com.android.launcher3.util.ViewOnDrawExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Tests to verify multiple callbacks in Loader
+ */
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class ModelMultiCallbacksTest {
+
+ private LauncherModelHelper mModelHelper;
+
+ private ShadowPackageManager mSpm;
+ private LooperExecutor mTempMainExecutor;
+
+ @Before
+ public void setUp() throws Exception {
+ mModelHelper = new LauncherModelHelper();
+ mModelHelper.installApp(TEST_PACKAGE);
+
+ mSpm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+
+ // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
+ // so that we can wait appropriately for the loader to complete.
+ mTempMainExecutor = new LooperExecutor(createAndStartNewForegroundLooper("tempMain"));
+ ReflectionHelpers.setField(mModelHelper.getModel(), "mMainExecutor", mTempMainExecutor);
+ }
+
+ @Test
+ public void testTwoCallbacks_loadedTogether() throws Exception {
+ setupWorkspacePages(3);
+
+ MyCallbacks cb1 = spy(MyCallbacks.class);
+ mModelHelper.getModel().addCallbacksAndLoad(cb1);
+
+ waitForLoaderAndTempMainThread();
+ cb1.verifySynchronouslyBound(3);
+
+ // Add a new callback
+ cb1.reset();
+ MyCallbacks cb2 = spy(MyCallbacks.class);
+ cb2.mPageToBindSync = 2;
+ mModelHelper.getModel().addCallbacksAndLoad(cb2);
+
+ waitForLoaderAndTempMainThread();
+ cb1.verifySynchronouslyBound(3);
+ cb2.verifySynchronouslyBound(3);
+
+ // Remove callbacks
+ cb1.reset();
+ cb2.reset();
+
+ // No effect on callbacks when removing an callback
+ mModelHelper.getModel().removeCallbacks(cb2);
+ waitForLoaderAndTempMainThread();
+ assertNull(cb1.mDeferredExecutor);
+ assertNull(cb2.mDeferredExecutor);
+
+ // Reloading only loads registered callbacks
+ mModelHelper.getModel().startLoader();
+ waitForLoaderAndTempMainThread();
+ cb1.verifySynchronouslyBound(3);
+ assertNull(cb2.mDeferredExecutor);
+ }
+
+ @Test
+ public void testTwoCallbacks_receiveUpdates() throws Exception {
+ setupWorkspacePages(1);
+
+ MyCallbacks cb1 = spy(MyCallbacks.class);
+ MyCallbacks cb2 = spy(MyCallbacks.class);
+ mModelHelper.getModel().addCallbacksAndLoad(cb1);
+ mModelHelper.getModel().addCallbacksAndLoad(cb2);
+ waitForLoaderAndTempMainThread();
+
+ cb1.verifyApps(TEST_PACKAGE);
+ cb2.verifyApps(TEST_PACKAGE);
+
+ // Install package 1
+ String pkg1 = "com.test.pkg1";
+ mModelHelper.installApp(pkg1);
+ mModelHelper.getModel().onPackageAdded(pkg1, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ cb1.verifyApps(TEST_PACKAGE, pkg1);
+ cb2.verifyApps(TEST_PACKAGE, pkg1);
+
+ // Install package 2
+ String pkg2 = "com.test.pkg2";
+ mModelHelper.installApp(pkg2);
+ mModelHelper.getModel().onPackageAdded(pkg2, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ cb1.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+ cb2.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+
+ // Uninstall package 2
+ mSpm.removePackage(pkg1);
+ mModelHelper.getModel().onPackageRemoved(pkg1, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ cb1.verifyApps(TEST_PACKAGE, pkg2);
+ cb2.verifyApps(TEST_PACKAGE, pkg2);
+
+ // Unregister a callback and verify updates no longer received
+ mModelHelper.getModel().removeCallbacks(cb2);
+ mSpm.removePackage(pkg2);
+ mModelHelper.getModel().onPackageRemoved(pkg2, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ cb1.verifyApps(TEST_PACKAGE);
+ cb2.verifyApps(TEST_PACKAGE, pkg2);
+ }
+
+ private void waitForLoaderAndTempMainThread() throws Exception {
+ Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+ mTempMainExecutor.submit(() -> { }).get();
+ }
+
+ private void setupWorkspacePages(int pageCount) throws Exception {
+ // Create a layout with 3 pages
+ LauncherLayoutBuilder builder = new LauncherLayoutBuilder();
+ for (int i = 0; i < pageCount; i++) {
+ builder.atWorkspace(1, 1, i).putApp(TEST_PACKAGE, TEST_PACKAGE);
+ }
+ mModelHelper.setupDefaultLayoutProvider(builder);
+ }
+
+ private abstract static class MyCallbacks implements Callbacks {
+
+ final List<ItemInfo> mItems = new ArrayList<>();
+ int mPageToBindSync = 0;
+ int mPageBoundSync = PagedView.INVALID_PAGE;
+ ViewOnDrawExecutor mDeferredExecutor;
+ AppInfo[] mAppInfos;
+
+ MyCallbacks() { }
+
+ @Override
+ public void onPageBoundSynchronously(int page) {
+ mPageBoundSync = page;
+ }
+
+ @Override
+ public void executeOnNextDraw(ViewOnDrawExecutor executor) {
+ mDeferredExecutor = executor;
+ }
+
+ @Override
+ public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
+ mItems.addAll(shortcuts);
+ }
+
+ @Override
+ public void bindAllApplications(AppInfo[] apps) {
+ mAppInfos = apps;
+ }
+
+ @Override
+ public int getPageToBindSynchronously() {
+ return mPageToBindSync;
+ }
+
+ public void reset() {
+ mItems.clear();
+ mPageBoundSync = PagedView.INVALID_PAGE;
+ mDeferredExecutor = null;
+ mAppInfos = null;
+ }
+
+ public void verifySynchronouslyBound(int totalItems) {
+ // Verify that the requested page is bound synchronously
+ assertEquals(mPageBoundSync, mPageToBindSync);
+ assertEquals(mItems.size(), 1);
+ assertEquals(mItems.get(0).screenId, mPageBoundSync);
+ assertNotNull(mDeferredExecutor);
+
+ // Verify that all other pages are bound properly
+ mDeferredExecutor.runAllTasks();
+ assertEquals(mItems.size(), totalItems);
+ }
+
+ public void verifyApps(String... apps) {
+ assertEquals(apps.length, mAppInfos.length);
+ assertEquals(Arrays.stream(mAppInfos)
+ .map(ai -> ai.getTargetComponent().getPackageName())
+ .collect(Collectors.toSet()),
+ new HashSet<>(Arrays.asList(apps)));
+ }
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
index ccbc18a..166e28b 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
@@ -43,6 +43,7 @@
import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
/**
* Extension of {@link ShadowLauncherApps} with missing shadow methods
@@ -93,4 +94,26 @@
return RuntimeEnvironment.application.getPackageManager()
.getApplicationInfo(packageName, flags);
}
+
+ @Implementation
+ public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
+ Intent intent = new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_LAUNCHER)
+ .setPackage(packageName);
+ return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
+ .stream()
+ .map(ri -> getLauncherActivityInfo(ri.activityInfo))
+ .collect(Collectors.toList());
+ }
+
+ @Implementation
+ public boolean hasShortcutHostPermission() {
+ return true;
+ }
+
+ @Override
+ protected List<LauncherActivityInfo> getShortcutConfigActivityList(String packageName,
+ UserHandle user) {
+ return Collections.emptyList();
+ }
}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
index edf8edb..576ddbd 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
@@ -16,6 +16,7 @@
package com.android.launcher3.shadows;
+import android.os.Parcel;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.SparseBooleanArray;
@@ -50,4 +51,12 @@
public void setUserLocked(UserHandle userHandle, boolean enabled) {
mLockedUsers.put(userHandle.hashCode(), enabled);
}
+
+ // Create user handle from parcel since UserHandle.of() was only added in later APIs.
+ public static UserHandle newUserHandle(int uid) {
+ Parcel userParcel = Parcel.obtain();
+ userParcel.writeInt(uid);
+ userParcel.setDataPosition(0);
+ return new UserHandle(userParcel);
+ }
}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 1a03f9f..655055c 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -20,13 +20,19 @@
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.provider.Settings;
import com.android.launcher3.AppInfo;
+import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
@@ -40,10 +46,14 @@
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.util.ReflectionHelpers;
import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
@@ -63,6 +73,13 @@
public static final int NO__ICON = -1;
public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
+ // Authority for providing a dummy default-workspace-layout data.
+ private static final String TEST_PROVIDER_AUTHORITY =
+ LauncherModelHelper.class.getName().toLowerCase();
+ private static final int DEFAULT_BITMAP_SIZE = 10;
+ private static final int DEFAULT_GRID_SIZE = 4;
+
+
private final HashMap<Class, HashMap<String, Field>> mFieldCache = new HashMap<>();
public final TestLauncherProvider provider;
@@ -285,4 +302,42 @@
return ids;
}
+
+ /**
+ * Sets up a dummy provider to load the provided layout by default, next time the layout loads
+ */
+ public void setupDefaultLayoutProvider(LauncherLayoutBuilder builder) throws Exception {
+ Context context = RuntimeEnvironment.application;
+ InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
+ idp.numRows = idp.numColumns = idp.numHotseatIcons = DEFAULT_GRID_SIZE;
+ idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
+
+ provider.setAllowLoadDefaultFavorites(true);
+ Settings.Secure.putString(context.getContentResolver(),
+ "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
+
+ shadowOf(context.getPackageManager())
+ .addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
+ TEST_PROVIDER_AUTHORITY;
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ builder.build(new OutputStreamWriter(bos));
+ Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, context);
+ shadowOf(context.getContentResolver()).registerInputStream(layoutUri,
+ new ByteArrayInputStream(bos.toByteArray()));
+ }
+
+ /**
+ * Simulates an apk install with a default main activity with same class and package name
+ */
+ public void installApp(String component) throws NameNotFoundException {
+ ShadowPackageManager spm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+ ComponentName cn = new ComponentName(component, component);
+ spm.addActivityIfNotPresent(cn);
+
+ IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
+ filter.addCategory(Intent.CATEGORY_LAUNCHER);
+ filter.addCategory(Intent.CATEGORY_DEFAULT);
+ spm.addIntentFilterForActivity(cn, filter);
+ }
}
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index af219ba..f76ca50 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -26,6 +26,8 @@
import android.os.UserHandle;
import android.os.UserManager;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageManagerHelper;
@@ -89,6 +91,15 @@
runtimeStatusFlags = info.runtimeStatusFlags;
}
+ @VisibleForTesting
+ public AppInfo(ComponentName componentName, CharSequence title,
+ UserHandle user, Intent intent) {
+ this.componentName = componentName;
+ this.title = title;
+ this.user = user;
+ this.intent = intent;
+ }
+
@Override
protected String dumpProperties() {
return super.dumpProperties() + " componentName=" + componentName;
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index bd48aec..423f2bb 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -126,7 +126,8 @@
onAccessibilityDrop(null, item);
ModelWriter modelWriter = mLauncher.getModelWriter();
Runnable onUndoClicked = () -> {
- modelWriter.abortDelete(itemPage);
+ mLauncher.setPageToBindSynchronously(itemPage);
+ modelWriter.abortDelete();
mLauncher.getUserEventDispatcher().logActionOnControl(TAP, UNDO);
};
Snackbar.show(mLauncher, R.string.item_removed, R.string.undo,
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 8b6d209..5b453c3 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -44,7 +44,7 @@
* Implemented by listeners of the back key.
*/
public interface OnBackKeyListener {
- public boolean onBackKey();
+ boolean onBackKey();
}
private OnBackKeyListener mBackKeyListener;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index d8c4c5c..f5fafbf 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -292,6 +292,7 @@
private PopupDataProvider mPopupDataProvider;
private int mSynchronouslyBoundPage = PagedView.INVALID_PAGE;
+ private int mPageToBindSynchronously = PagedView.INVALID_PAGE;
// We only want to get the SharedPreferences once since it does an FS stat each time we get
// it from the context.
@@ -307,7 +308,7 @@
// Request id for any pending activity result
protected int mPendingActivityRequestCode = -1;
- public ViewGroupFocusHelper mFocusHandler;
+ private ViewGroupFocusHelper mFocusHandler;
private RotationHelper mRotationHelper;
@@ -348,7 +349,7 @@
LauncherAppState app = LauncherAppState.getInstance(this);
mOldConfig = new Configuration(getResources().getConfiguration());
- mModel = app.setLauncher(this);
+ mModel = app.getModel();
mRotationHelper = new RotationHelper(this);
InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
initDeviceProfile(idp);
@@ -386,22 +387,18 @@
// We only load the page synchronously if the user rotates (or triggers a
// configuration change) while launcher is in the foreground
- int currentScreen = PagedView.INVALID_RESTORE_PAGE;
+ int currentScreen = PagedView.INVALID_PAGE;
if (savedInstanceState != null) {
currentScreen = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, currentScreen);
}
+ mPageToBindSynchronously = currentScreen;
- if (!mModel.startLoader(currentScreen)) {
+ if (!mModel.addCallbacksAndLoad(this)) {
if (!internalStateHandled) {
// If we are not binding synchronously, show a fade in animation when
// the first page bind completes.
mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).setValue(0);
}
- } else {
- // Pages bound synchronously.
- mWorkspace.setCurrentPage(currentScreen);
-
- setWorkspaceLoading(true);
}
// For handling default keys
@@ -522,15 +519,6 @@
}
@Override
- public void rebindModel() {
- int currentPage = mWorkspace.getNextPage();
- if (mModel.startLoader(currentPage)) {
- mWorkspace.setCurrentPage(currentPage);
- setWorkspaceLoading(true);
- }
- }
-
- @Override
public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
onIdpChanged(idp);
}
@@ -548,7 +536,7 @@
// initialized properly.
onSaveInstanceState(new Bundle());
if (oldWallpaperProfile != getWallpaperDeviceProfile()) {
- rebindModel();
+ mModel.rebindCallbacks();
}
}
@@ -617,6 +605,10 @@
return mRotationHelper;
}
+ public ViewGroupFocusHelper getFocusHandler() {
+ return mFocusHandler;
+ }
+
public LauncherStateManager getStateManager() {
return mStateManager;
}
@@ -1539,13 +1531,7 @@
mWorkspace.removeFolderListeners();
PluginManagerWrapper.INSTANCE.get(this).removePluginListener(this);
- // Stop callbacks from LauncherModel
- // It's possible to receive onDestroy after a new Launcher activity has
- // been created. In this case, don't interfere with the new Launcher.
- if (mModel.isCurrentCallbacks(this)) {
- mModel.stopLoader();
- LauncherAppState.getInstance(this).setLauncher(null);
- }
+ mModel.removeCallbacks(this);
mRotationHelper.destroy();
try {
@@ -1740,7 +1726,7 @@
getModelWriter().addItemToDatabase(folderInfo, container, screenId, cellX, cellY);
// Create the view
- FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, layout, folderInfo);
+ FolderIcon newFolder = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this, layout, folderInfo);
mWorkspace.addInScreen(newFolder, folderInfo);
// Force measure the new folder icon
CellLayout parent = mWorkspace.getParentCellLayoutForView(newFolder);
@@ -1953,11 +1939,21 @@
}
/**
+ * Sets the next page to bind synchronously on next bind.
+ * @param page
+ */
+ public void setPageToBindSynchronously(int page) {
+ mPageToBindSynchronously = page;
+ }
+
+ /**
* Implementation of the method from LauncherModel.Callbacks.
*/
@Override
- public int getCurrentWorkspaceScreen() {
- if (mWorkspace != null) {
+ public int getPageToBindSynchronously() {
+ if (mPageToBindSynchronously != PagedView.INVALID_PAGE) {
+ return mPageToBindSynchronously;
+ } else if (mWorkspace != null) {
return mWorkspace.getCurrentPage();
} else {
return 0;
@@ -2101,7 +2097,7 @@
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
- view = FolderIcon.fromXml(R.layout.folder_icon, this,
+ view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this,
(ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
(FolderInfo) item);
break;
@@ -2335,6 +2331,8 @@
public void onPageBoundSynchronously(int page) {
mSynchronouslyBoundPage = page;
+ mWorkspace.setCurrentPage(page);
+ mPageToBindSynchronously = PagedView.INVALID_PAGE;
}
@Override
@@ -2399,6 +2397,7 @@
// Since we are just resetting the current page without user interaction,
// override the previous page so we don't log the page switch.
mWorkspace.setCurrentPage(pageBoundFirst, pageBoundFirst /* overridePrevPage */);
+ mPageToBindSynchronously = PagedView.INVALID_PAGE;
// Cache one page worth of icons
getViewCache().setCacheSize(R.layout.folder_application,
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index c6946ca..4cd038d 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -160,11 +160,6 @@
}
}
- LauncherModel setLauncher(Launcher launcher) {
- mModel.initialize(launcher);
- return mModel;
- }
-
public IconCache getIconCache() {
return mIconCache;
}
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index e005320..cf978b5 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -58,16 +58,16 @@
import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.IntSparseArrayMap;
import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.Thunk;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
@@ -82,11 +82,12 @@
static final String TAG = "Launcher.Model";
- @Thunk final LauncherAppState mApp;
- @Thunk final Object mLock = new Object();
- @Thunk
- LoaderTask mLoaderTask;
- @Thunk boolean mIsLoaderTaskRunning;
+ private final LauncherAppState mApp;
+ private final Object mLock = new Object();
+ private final LooperExecutor mMainExecutor = MAIN_EXECUTOR;
+
+ private LoaderTask mLoaderTask;
+ private boolean mIsLoaderTaskRunning;
// Indicates whether the current model data is valid or not.
// We start off with everything not loaded. After that, we assume that
@@ -99,7 +100,7 @@
}
}
- @Thunk WeakReference<Callbacks> mCallbacks;
+ private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
// < only access in worker thread >
private final AllAppsList mBgAllAppsList;
@@ -127,12 +128,21 @@
}
/**
+ * Returns AppInfo with corresponding package name.
+ * TODO: move to enqueueModelTask
+ */
+ public Optional<AppInfo> getAppInfoByPackageName(String pkg) {
+ return mBgAllAppsList.data.stream()
+ .filter(info -> info.componentName.getPackageName().equals(pkg))
+ .findAny();
+ }
+
+ /**
* Adds the provided items to the workspace.
*/
public void addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList) {
- Callbacks callbacks = getCallback();
- if (callbacks != null) {
- callbacks.preAddApps();
+ for (Callbacks cb : getCallbacks()) {
+ cb.preAddApps();
}
enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
}
@@ -142,16 +152,6 @@
hasVerticalHotseat, verifyChanges);
}
- /**
- * Set this as the current Launcher activity object for the loader.
- */
- public void initialize(Callbacks callbacks) {
- synchronized (mLock) {
- Preconditions.assertUIThread();
- mCallbacks = new WeakReference<>(callbacks);
- }
- }
-
@Override
public void onPackageChanged(String packageName, UserHandle user) {
int op = PackageUpdatedTask.OP_UPDATE;
@@ -251,21 +251,19 @@
}
}
} else if (IS_DOGFOOD_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
- Launcher l = (Launcher) getCallback();
- l.reload();
+ for (Callbacks cb : getCallbacks()) {
+ if (cb instanceof Launcher) {
+ ((Launcher) cb).recreate();
+ }
+ }
}
}
- public void forceReload() {
- forceReload(-1);
- }
-
/**
* Reloads the workspace items from the DB and re-binds the workspace. This should generally
* not be called as DB updates are automatically followed by UI update
- * @param synchronousBindPage The page to bind first. Can pass -1 to use the current page.
*/
- public void forceReload(int synchronousBindPage) {
+ public void forceReload() {
synchronized (mLock) {
// Stop any existing loaders first, so they don't set mModelLoaded to true later
stopLoader();
@@ -274,37 +272,77 @@
// Start the loader if launcher is already running, otherwise the loader will run,
// the next time launcher starts
- Callbacks callbacks = getCallback();
- if (callbacks != null) {
- if (synchronousBindPage < 0) {
- synchronousBindPage = callbacks.getCurrentWorkspaceScreen();
- }
- startLoader(synchronousBindPage);
+ if (hasCallbacks()) {
+ startLoader();
}
}
- public boolean isCurrentCallbacks(Callbacks callbacks) {
- return (mCallbacks != null && mCallbacks.get() == callbacks);
+ /**
+ * Rebinds all existing callbacks with already loaded model
+ */
+ public void rebindCallbacks() {
+ if (hasCallbacks()) {
+ startLoader();
+ }
+ }
+
+ /**
+ * Removes an existing callback
+ */
+ public void removeCallbacks(Callbacks callbacks) {
+ synchronized (mCallbacksList) {
+ Preconditions.assertUIThread();
+ if (mCallbacksList.remove(callbacks)) {
+ if (stopLoader()) {
+ // Rebind existing callbacks
+ startLoader();
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a callbacks to receive model updates
+ * @return true if workspace load was performed synchronously
+ */
+ public boolean addCallbacksAndLoad(Callbacks callbacks) {
+ synchronized (mLock) {
+ addCallbacks(callbacks);
+ return startLoader();
+
+ }
+ }
+
+ /**
+ * Adds a callbacks to receive model updates
+ */
+ public void addCallbacks(Callbacks callbacks) {
+ Preconditions.assertUIThread();
+ synchronized (mCallbacksList) {
+ mCallbacksList.add(callbacks);
+ }
}
/**
* Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
* @return true if the page could be bound synchronously.
*/
- public boolean startLoader(int synchronousBindPage) {
+ public boolean startLoader() {
// Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
synchronized (mLock) {
// Don't bother to start the thread if we know it's not going to do anything
- if (mCallbacks != null && mCallbacks.get() != null) {
- final Callbacks oldCallbacks = mCallbacks.get();
+ final Callbacks[] callbacksList = getCallbacks();
+ if (callbacksList.length > 0) {
// Clear any pending bind-runnables from the synchronized load process.
- MAIN_EXECUTOR.execute(oldCallbacks::clearPendingBinds);
+ for (Callbacks cb : callbacksList) {
+ mMainExecutor.execute(cb::clearPendingBinds);
+ }
// If there is already one running, tell it to stop.
stopLoader();
- LoaderResults loaderResults = new LoaderResults(mApp, mBgDataModel,
- mBgAllAppsList, synchronousBindPage, mCallbacks);
+ LoaderResults loaderResults = new LoaderResults(
+ mApp, mBgDataModel, mBgAllAppsList, callbacksList, mMainExecutor);
if (mModelLoaded && !mIsLoaderTaskRunning) {
// Divide the set of loaded items into those that we are binding synchronously,
// and everything else that is to be bound normally (asynchronously).
@@ -325,14 +363,17 @@
/**
* If there is already a loader task running, tell it to stop.
+ * @return true if an existing loader was stopped.
*/
- public void stopLoader() {
+ public boolean stopLoader() {
synchronized (mLock) {
LoaderTask oldTask = mLoaderTask;
mLoaderTask = null;
if (oldTask != null) {
oldTask.stopLocked();
+ return true;
}
+ return false;
}
}
@@ -487,7 +528,7 @@
}
public void enqueueModelUpdateTask(ModelUpdateTask task) {
- task.init(mApp, this, mBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
+ task.init(mApp, this, mBgDataModel, mBgAllAppsList, mMainExecutor);
MODEL_EXECUTOR.execute(task);
}
@@ -561,7 +602,21 @@
mBgDataModel.dump(prefix, fd, writer, args);
}
- public Callbacks getCallback() {
- return mCallbacks != null ? mCallbacks.get() : null;
+ /**
+ * Returns true if there are any callbacks attached to the model
+ */
+ public boolean hasCallbacks() {
+ synchronized (mCallbacksList) {
+ return !mCallbacksList.isEmpty();
+ }
+ }
+
+ /**
+ * Returns an array of currently attached callbacks
+ */
+ public Callbacks[] getCallbacks() {
+ synchronized (mCallbacksList) {
+ return mCallbacksList.toArray(new Callbacks[mCallbacksList.size()]);
+ }
}
}
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index ff2b400..a1888bf 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -64,7 +64,7 @@
private static final String TAG = "PagedView";
private static final boolean DEBUG = false;
- protected static final int INVALID_PAGE = -1;
+ public static final int INVALID_PAGE = -1;
protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE;
public static final int PAGE_SNAP_ANIMATION_DURATION = 750;
@@ -84,8 +84,6 @@
private static final int MIN_SNAP_VELOCITY = 1500;
private static final int MIN_FLING_VELOCITY = 250;
- public static final int INVALID_RESTORE_PAGE = -1001;
-
private boolean mFreeScroll = false;
protected int mFlingThresholdVelocity;
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index f96e735..9a3a379 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -83,7 +83,6 @@
import com.android.launcher3.pageindicators.WorkspacePageIndicator;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.WorkspaceTouchListener;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -2547,7 +2546,7 @@
view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info);
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
- view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout,
+ view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, mLauncher, cellLayout,
(FolderInfo) info);
break;
default:
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 08ce9c2..0681919 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -11,10 +11,12 @@
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
import android.util.FloatProperty;
import android.view.animation.Interpolator;
@@ -183,8 +185,11 @@
}
public Animator createSpringAnimation(float... progressValues) {
- return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
- SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
+ if (UNSTABLE_SPRINGS.get()) {
+ return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
+ SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
+ }
+ return ObjectAnimator.ofFloat(this, ALL_APPS_PROGRESS, progressValues);
}
private void setAlphas(LauncherState toState, AnimationConfig config,
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 003ca82..81dcba3 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -85,7 +85,10 @@
"APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
public static final TogglableFlag QUICKSTEP_SPRINGS = new TogglableFlag("QUICKSTEP_SPRINGS",
- false, "Enable springs for quickstep animations");
+ true, "Enable springs for quickstep animations");
+
+ public static final TogglableFlag UNSTABLE_SPRINGS = new TogglableFlag("UNSTABLE_SPRINGS",
+ false, "Enable unstable springs for quickstep animations");
public static final TogglableFlag ADAPTIVE_ICON_WINDOW_ANIM = new TogglableFlag(
"ADAPTIVE_ICON_WINDOW_ANIM", true,
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index f59a192..844189f 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -297,16 +297,22 @@
}
public void startEditingFolderName() {
- post(new Runnable() {
- @Override
- public void run() {
- mFolderName.setHint("");
- mIsEditingName = true;
+ post(() -> {
+ if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+ if (TextUtils.isEmpty(mFolderName.getText())) {
+ final String[] suggestedNames = new String[FolderNameProvider.SUGGEST_MAX];
+ mLauncher.getFolderNameProvider().getSuggestedFolderName(getContext(),
+ mInfo.contents, suggestedNames);
+ mFolderName.setText(suggestedNames[0]);
+ mFolderName.displayCompletions(Arrays.asList(suggestedNames).subList(1,
+ suggestedNames.length));
+ }
}
+ mFolderName.setHint("");
+ mIsEditingName = true;
});
}
-
@Override
public boolean onBackKey() {
// Convert to a string here to ensure that no other state associated with the text field
@@ -316,10 +322,18 @@
mFolderIcon.onTitleChanged(newTitle);
mLauncher.getModelWriter().updateItemInDatabase(mInfo);
- if (TextUtils.isEmpty(mInfo.title)) {
- mFolderName.setHint(R.string.folder_hint_text);
+ if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+ mFolderName.setText(mInfo.title);
+ // TODO: depending on whether the title was manually edited or automatically
+ // suggested, apply different hint.
+ mFolderName.setHint("");
} else {
- mFolderName.setHint(null);
+ if (TextUtils.isEmpty(mInfo.title)) {
+ mFolderName.setHint(R.string.folder_hint_text);
+ mFolderName.setText("");
+ } else {
+ mFolderName.setHint(null);
+ }
}
sendCustomAccessibilityEvent(
@@ -403,7 +417,11 @@
mFolderName.setHint(null);
} else {
mFolderName.setText("");
- mFolderName.setHint(R.string.folder_hint_text);
+ if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+ mFolderName.setHint("");
+ } else {
+ mFolderName.setHint(R.string.folder_hint_text);
+ }
}
// In case any children didn't come across during loading, clean up the folder accordingly
mFolderIcon.post(() -> {
@@ -420,10 +438,10 @@
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()
&& TextUtils.isEmpty(mFolderName.getText().toString())) {
if (suggestName.length > 0 && !TextUtils.isEmpty(suggestName[0])) {
- mFolderName.setHint(suggestName[0]);
+ mFolderName.setHint("");
mFolderName.setText(suggestName[0]);
mInfo.title = suggestName[0];
- animateOpen();
+ animateOpen(mInfo.contents, 0, true);
mFolderName.showKeyboard();
mFolderName.displayCompletions(
Arrays.asList(suggestName).subList(1, suggestName.length));
@@ -519,12 +537,24 @@
* is played.
*/
private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) {
+ animateOpen(items, pageNo, false);
+ }
+
+ /**
+ * Opens the user folder described by the specified tag. The opening of the folder
+ * is animated relative to the specified View. If the View is null, no animation
+ * is played.
+ */
+ private void animateOpen(List<WorkspaceItemInfo> items, int pageNo, boolean skipUserEventLog) {
Folder openFolder = getOpen(mLauncher);
if (openFolder != null && openFolder != this) {
// Close any open folder before opening a folder.
openFolder.close(true);
}
+ if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+ mLauncher.getFolderNameProvider().load(getContext());
+ }
mContent.bindItems(items);
centerAboutIcon();
mItemsInvalidated = true;
@@ -565,10 +595,13 @@
mState = STATE_OPEN;
announceAccessibilityChanges();
- mLauncher.getUserEventDispatcher().logActionOnItem(
+ if (!skipUserEventLog) {
+ mLauncher.getUserEventDispatcher().logActionOnItem(
Touch.TAP,
Direction.NONE,
ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY);
+ }
+
mContent.setFocusOnFirstChild();
}
@@ -1338,6 +1371,7 @@
return itemsOnCurrentPage;
}
+ @Override
public void onFocusChange(View v, boolean hasFocus) {
if (v == mFolderName) {
if (hasFocus) {
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index f322061..8c56823 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -67,6 +67,7 @@
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.IconLabelDotView;
import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -79,7 +80,7 @@
*/
public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView {
- @Thunk Launcher mLauncher;
+ @Thunk ActivityContext mActivity;
@Thunk Folder mFolder;
private FolderInfo mInfo;
@@ -153,7 +154,21 @@
mDotParams = new DotRenderer.DrawParams();
}
- public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
+ public static FolderIcon inflateFolderAndIcon(int resId, Launcher launcher, ViewGroup group,
+ FolderInfo folderInfo) {
+ Folder folder = Folder.fromXml(launcher);
+ folder.setDragController(launcher.getDragController());
+
+ FolderIcon icon = inflateIcon(resId, launcher, group, folderInfo);
+ folder.setFolderIcon(icon);
+ folder.bind(folderInfo);
+ icon.setFolder(folder);
+
+ icon.setOnFocusChangeListener(launcher.getFocusHandler());
+ return icon;
+ }
+
+ public static FolderIcon inflateIcon(int resId, ActivityContext activity, ViewGroup group,
FolderInfo folderInfo) {
@SuppressWarnings("all") // suppress dead code warning
final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
@@ -163,7 +178,7 @@
"is dependent on this");
}
- DeviceProfile grid = launcher.getWallpaperDeviceProfile();
+ DeviceProfile grid = activity.getWallpaperDeviceProfile();
FolderIcon icon = (FolderIcon) LayoutInflater.from(group.getContext())
.inflate(resId, group, false);
@@ -177,27 +192,27 @@
icon.setTag(folderInfo);
icon.setOnClickListener(ItemClickHandler.INSTANCE);
icon.mInfo = folderInfo;
- icon.mLauncher = launcher;
+ icon.mActivity = activity;
icon.mDotRenderer = grid.mDotRendererWorkSpace;
- icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
+
+ icon.setContentDescription(
+ group.getContext().getString(R.string.folder_name_format, folderInfo.title));
// Keep the notification dot up to date with the sum of all the content's dots.
FolderDotInfo folderDotInfo = new FolderDotInfo();
for (WorkspaceItemInfo si : folderInfo.contents) {
- folderDotInfo.addDotInfo(launcher.getDotInfoForItem(si));
+ folderDotInfo.addDotInfo(activity.getDotInfoForItem(si));
}
icon.setDotInfo(folderDotInfo);
- Folder folder = Folder.fromXml(launcher);
- folder.setDragController(launcher.getDragController());
- folder.setFolderIcon(icon);
- folder.bind(folderInfo);
- icon.setFolder(folder);
- icon.setAccessibilityDelegate(launcher.getAccessibilityDelegate());
+ icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
+
+ icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
+ icon.mPreviewVerifier.setFolderInfo(folderInfo);
+ icon.updatePreviewItems(false);
folderInfo.addListener(icon);
- icon.setOnFocusChangeListener(launcher.mFocusHandler);
return icon;
}
@@ -225,9 +240,6 @@
private void setFolder(Folder folder) {
mFolder = folder;
- mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
- mPreviewVerifier.setFolderInfo(mFolder.getInfo());
- updatePreviewItems(false);
}
private boolean willAcceptItem(ItemInfo item) {
@@ -309,14 +321,15 @@
// Typically, the animateView corresponds to the DragView; however, if this is being done
// after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
// will not have a view to animate
- if (animateView != null) {
- DragLayer dragLayer = mLauncher.getDragLayer();
+ if (animateView != null && mActivity instanceof Launcher) {
+ final Launcher launcher = (Launcher) mActivity;
+ DragLayer dragLayer = launcher.getDragLayer();
Rect from = new Rect();
dragLayer.getViewRectRelativeToSelf(animateView, from);
Rect to = finalRect;
if (to == null) {
to = new Rect();
- Workspace workspace = mLauncher.getWorkspace();
+ Workspace workspace = launcher.getWorkspace();
// Set cellLayout and this to it's final state to compute final animation locations
workspace.setFinalTransitionTransform();
float scaleX = getScaleX();
@@ -382,7 +395,7 @@
String[] suggestedNameOut = new String[FolderNameProvider.SUGGEST_MAX];
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
Executors.UI_HELPER_EXECUTOR.post(() -> {
- mLauncher.getFolderNameProvider().getSuggestedFolderName(
+ launcher.getFolderNameProvider().getSuggestedFolderName(
getContext(), mInfo.contents, suggestedNameOut);
showFinalView(finalIndex, item, suggestedNameOut);
});
@@ -547,7 +560,7 @@
if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) {
Rect iconBounds = mDotParams.iconBounds;
BubbleTextView.getIconBounds(this, iconBounds,
- mLauncher.getWallpaperDeviceProfile().iconSizePx);
+ mActivity.getWallpaperDeviceProfile().iconSizePx);
float iconScale = (float) mBackground.previewSize / iconBounds.width();
Utilities.scaleRectAboutCenter(iconBounds, iconScale);
@@ -605,7 +618,7 @@
@Override
public void onAdd(WorkspaceItemInfo item, int rank) {
boolean wasDotted = mDotInfo.hasDot();
- mDotInfo.addDotInfo(mLauncher.getDotInfoForItem(item));
+ mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item));
boolean isDotted = mDotInfo.hasDot();
updateDotScale(wasDotted, isDotted);
invalidate();
@@ -615,7 +628,7 @@
@Override
public void onRemove(WorkspaceItemInfo item) {
boolean wasDotted = mDotInfo.hasDot();
- mDotInfo.subtractDotInfo(mLauncher.getDotInfoForItem(item));
+ mDotInfo.subtractDotInfo(mActivity.getDotInfoForItem(item));
boolean isDotted = mDotInfo.hasDot();
updateDotScale(wasDotted, isDotted);
invalidate();
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index 37aa815..d76b73f 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -15,53 +15,117 @@
*/
package com.android.launcher3.folder;
-import android.content.ComponentName;
import android.content.Context;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
-import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.config.FeatureFlags;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
/**
* Locates provider for the folder name.
*/
public class FolderNameProvider {
+ private static final String TAG = FeatureFlags.FOLDER_NAME_SUGGEST.getKey();
+ private static final boolean DEBUG = FeatureFlags.FOLDER_NAME_SUGGEST.get();
+
/**
- * IME usually has up to 3 suggest slots. Adding one as in Launcher, there are folder
- * name edit box that we can also provide suggestion.
+ * IME usually has up to 3 suggest slots. In total, there are 4 suggest slots as the folder
+ * name edit box can also be used to provide suggestion.
*/
public static final int SUGGEST_MAX = 4;
/**
- * Returns suggested folder name.
+ * When inheriting class requires precaching, override this method.
*/
- public CharSequence getSuggestedFolderName(Context context,
- ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] suggestName) {
- // Currently only run the algorithm on initial folder creation.
- // For more than 2 items in the folder, the ranking algorithm for finding
- // candidate folder name should be rewritten.
- if (workspaceItemInfos.size() == 2) {
- ComponentName cmp1 = workspaceItemInfos.get(0).getTargetComponent();
- ComponentName cmp2 = workspaceItemInfos.get(1).getTargetComponent();
+ public void load(Context context) {}
- String pkgName0 = cmp1 == null ? "" : cmp1.getPackageName();
- String pkgName1 = cmp2 == null ? "" : cmp2.getPackageName();
- // If the two icons are from the same package,
- // then assign the main icon's name
- if (pkgName0.equals(pkgName1)) {
- WorkspaceItemInfo wInfo0 = workspaceItemInfos.get(0);
- WorkspaceItemInfo wInfo1 = workspaceItemInfos.get(1);
- if (workspaceItemInfos.get(0).itemType == Favorites.ITEM_TYPE_APPLICATION) {
- suggestName[0] = wInfo0.title;
- } else if (wInfo1.itemType == Favorites.ITEM_TYPE_APPLICATION) {
- suggestName[0] = wInfo1.title;
- }
- return suggestName[0];
- // two icons are all shortcuts. Don't assign title
+ public CharSequence getSuggestedFolderName(Context context,
+ ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] candidates) {
+
+ if (DEBUG) {
+ Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates));
+ }
+ // If all the icons are from work profile,
+ // Then, suggest "Work" as the folder name
+ List<WorkspaceItemInfo> distinctItemInfos = workspaceItemInfos.stream()
+ .filter(distinctByKey(p-> p.user))
+ .collect(Collectors.toList());
+
+ if (distinctItemInfos.size() == 1
+ && !distinctItemInfos.get(0).user.equals(Process.myUserHandle())) {
+ // Place it as last viable suggestion
+ setAsLastSuggestion(candidates,
+ context.getResources().getString(R.string.work_folder_name));
+ }
+
+ // If all the icons are from same package (e.g., main icon, shortcut, shortcut)
+ // Then, suggest the package's title as the folder name
+ distinctItemInfos = workspaceItemInfos.stream()
+ .filter(distinctByKey(p-> p.getTargetComponent() != null
+ ? p.getTargetComponent().getPackageName() : ""))
+ .collect(Collectors.toList());
+
+ if (distinctItemInfos.size() == 1) {
+ Optional<AppInfo> info = LauncherAppState.getInstance(context).getModel()
+ .getAppInfoByPackageName(distinctItemInfos.get(0).getTargetComponent()
+ .getPackageName());
+ // Place it as first viable suggestion and shift everything else
+ info.ifPresent(i -> setAsFirstSuggestion(candidates, i.title.toString()));
+ }
+ if (DEBUG) {
+ Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates));
+ }
+ return candidates[0];
+ }
+
+ private void setAsFirstSuggestion(CharSequence[] candidatesOut, CharSequence candidate) {
+ if (contains(candidatesOut, candidate)) {
+ return;
+ }
+ for (int i = candidatesOut.length - 1; i > 0; i--) {
+ if (!TextUtils.isEmpty(candidatesOut[i - 1])) {
+ candidatesOut[i] = candidatesOut[i - 1];
}
}
- return suggestName[0];
+ candidatesOut[0] = candidate;
+ }
+
+ private void setAsLastSuggestion(CharSequence[] candidatesOut, CharSequence candidate) {
+ if (contains(candidatesOut, candidate)) {
+ return;
+ }
+ for (int i = 0; i < candidate.length(); i++) {
+ if (TextUtils.isEmpty(candidatesOut[i])) {
+ candidatesOut[i] = candidate;
+ }
+ }
+ }
+
+ private boolean contains(CharSequence[] list, CharSequence key) {
+ return Arrays.asList(list).stream()
+ .filter(s -> s != null)
+ .anyMatch(s -> s.toString().equalsIgnoreCase(key.toString()));
+ }
+
+ // This method can be moved to some Utility class location.
+ private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
+ Map<Object, Boolean> map = new ConcurrentHashMap<>();
+ return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
}
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 5b3a05e..27aa43e 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -37,10 +37,10 @@
import androidx.annotation.NonNull;
-import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.views.ActivityContext;
import java.util.ArrayList;
import java.util.List;
@@ -94,7 +94,8 @@
public PreviewItemManager(FolderIcon icon) {
mContext = icon.getContext();
mIcon = icon;
- mIconSize = Launcher.getLauncher(mContext).getDeviceProfile().folderChildIconSizePx;
+ mIconSize = ActivityContext.lookupContext(
+ mContext).getDeviceProfile().folderChildIconSizePx;
}
/**
@@ -132,7 +133,7 @@
mTotalWidth = totalSize;
mPrevTopPadding = mIcon.getPaddingTop();
- mIcon.mBackground.setup(mIcon.mLauncher, mIcon.mLauncher, mIcon, mTotalWidth,
+ mIcon.mBackground.setup(mIcon.getContext(), mIcon.mActivity, mIcon, mTotalWidth,
mIcon.getPaddingTop());
mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize,
Utilities.isRtl(mIcon.getResources()));
@@ -152,7 +153,7 @@
}
private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
- float iconSize = mIcon.mLauncher.getDeviceProfile().iconSizePx;
+ float iconSize = mIcon.mActivity.getDeviceProfile().iconSizePx;
final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
final float trans = (mIcon.mBackground.previewSize - iconSize) / 2;
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 0c5535f..def76e8 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -49,6 +49,7 @@
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.FolderInfo;
import com.android.launcher3.Hotseat;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.InvariantDeviceProfile;
@@ -63,11 +64,13 @@
import com.android.launcher3.WorkspaceLayoutManager;
import com.android.launcher3.allapps.SearchUiManager;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.BitmapRenderer;
import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.LoaderResults;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
@@ -239,6 +242,12 @@
addInScreenFromBind(icon, info);
}
+ private void inflateAndAddFolder(FolderInfo info) {
+ FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace,
+ info);
+ addInScreenFromBind(folderIcon, info);
+ }
+
private void dispatchVisibilityAggregated(View view, boolean isVisible) {
// Similar to View.dispatchVisibilityAggregated implementation.
final boolean thisVisible = view.getVisibility() == VISIBLE;
@@ -288,7 +297,7 @@
inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
- // TODO: for folder implementation here.
+ inflateAndAddFolder((FolderInfo) itemInfo);
break;
default:
break;
@@ -369,7 +378,7 @@
if (!mModel.isModelLoaded()) {
Log.d(TAG, "Workspace not loaded, loading now");
mModel.startLoaderForResults(
- new LoaderResults(mApp, mBgDataModel, mAllAppsList, 0, null));
+ new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
return new ArrayList<>();
}
return mBgDataModel.workspaceItems;
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 76c2951..0d12183 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -18,9 +18,7 @@
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import android.os.Looper;
import android.util.Log;
import com.android.launcher3.AppInfo;
@@ -32,12 +30,13 @@
import com.android.launcher3.PagedView;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.LooperIdleLock;
import com.android.launcher3.util.ViewOnDrawExecutor;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Executor;
/**
@@ -49,40 +48,29 @@
protected static final int INVALID_SCREEN_ID = -1;
private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons
- protected final Executor mUiExecutor;
+ protected final LooperExecutor mUiExecutor;
protected final LauncherAppState mApp;
protected final BgDataModel mBgDataModel;
private final AllAppsList mBgAllAppsList;
- protected final int mPageToBindFirst;
- protected final WeakReference<Callbacks> mCallbacks;
+ private final Callbacks[] mCallbacksList;
private int mMyBindingId;
public BaseLoaderResults(LauncherAppState app, BgDataModel dataModel,
- AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
- mUiExecutor = MAIN_EXECUTOR;
+ AllAppsList allAppsList, Callbacks[] callbacksList, LooperExecutor uiExecutor) {
+ mUiExecutor = uiExecutor;
mApp = app;
mBgDataModel = dataModel;
mBgAllAppsList = allAppsList;
- mPageToBindFirst = pageToBindFirst;
- mCallbacks = callbacks == null ? new WeakReference<>(null) : callbacks;
+ mCallbacksList = callbacksList;
}
/**
* Binds all loaded data to actual views on the main thread.
*/
public void bindWorkspace() {
- Callbacks callbacks = mCallbacks.get();
- // Don't use these two variables in any of the callback runnables.
- // Otherwise we hold a reference to them.
- if (callbacks == null) {
- // This launcher has exited and nobody bothered to tell us. Just bail.
- Log.w(TAG, "LoaderTask running with no launcher");
- return;
- }
-
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
@@ -96,97 +84,9 @@
mMyBindingId = mBgDataModel.lastBindId;
}
- final int currentScreen;
- {
- int currScreen = mPageToBindFirst != PagedView.INVALID_RESTORE_PAGE
- ? mPageToBindFirst : callbacks.getCurrentWorkspaceScreen();
- if (currScreen >= orderedScreenIds.size()) {
- // There may be no workspace screens (just hotseat items and an empty page).
- currScreen = PagedView.INVALID_RESTORE_PAGE;
- }
- currentScreen = currScreen;
- }
- final boolean validFirstPage = currentScreen >= 0;
- final int currentScreenId =
- validFirstPage ? orderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
-
- // Separate the items that are on the current screen, and all the other remaining items
- ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
- ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
- ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
- ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
-
- filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
- otherWorkspaceItems);
- filterCurrentWorkspaceItems(currentScreenId, appWidgets, currentAppWidgets,
- otherAppWidgets);
- final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
- sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
- sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
-
- // Tell the workspace that we're about to start binding items
- executeCallbacksTask(c -> {
- c.clearPendingBinds();
- c.startBinding();
- }, mUiExecutor);
-
- // Bind workspace screens
- executeCallbacksTask(c -> c.bindScreens(orderedScreenIds), mUiExecutor);
-
- Executor mainExecutor = mUiExecutor;
- // Load items on the current page.
- bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
- bindAppWidgets(currentAppWidgets, mainExecutor);
- // In case of validFirstPage, only bind the first screen, and defer binding the
- // remaining screens after first onDraw (and an optional the fade animation whichever
- // happens later).
- // This ensures that the first screen is immediately visible (eg. during rotation)
- // In case of !validFirstPage, bind all pages one after other.
- final Executor deferredExecutor =
- validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
-
- executeCallbacksTask(c -> c.finishFirstPageBind(
- validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
-
- bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
- bindAppWidgets(otherAppWidgets, deferredExecutor);
- // Tell the workspace that we're done binding items
- executeCallbacksTask(c -> c.finishBindingItems(mPageToBindFirst), deferredExecutor);
-
- if (validFirstPage) {
- executeCallbacksTask(c -> {
- // We are loading synchronously, which means, some of the pages will be
- // bound after first draw. Inform the callbacks that page binding is
- // not complete, and schedule the remaining pages.
- if (currentScreen != PagedView.INVALID_RESTORE_PAGE) {
- c.onPageBoundSynchronously(currentScreen);
- }
- c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
-
- }, mUiExecutor);
- }
- }
-
- protected void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems,
- final Executor executor) {
- // Bind the workspace items
- int N = workspaceItems.size();
- for (int i = 0; i < N; i += ITEMS_CHUNK) {
- final int start = i;
- final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
- executeCallbacksTask(
- c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
- executor);
- }
- }
-
- private void bindAppWidgets(ArrayList<LauncherAppWidgetInfo> appWidgets, Executor executor) {
- int N;// Bind the widgets, one at a time
- N = appWidgets.size();
- for (int i = 0; i < N; i++) {
- final ItemInfo widget = appWidgets.get(i);
- executeCallbacksTask(
- c -> c.bindItems(Collections.singletonList(widget), false), executor);
+ for (Callbacks cb : mCallbacksList) {
+ new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
+ workspaceItems, appWidgets, orderedScreenIds).bind();
}
}
@@ -206,19 +106,155 @@
Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
return;
}
- Callbacks callbacks = mCallbacks.get();
- if (callbacks != null) {
- task.execute(callbacks);
+ for (Callbacks cb : mCallbacksList) {
+ task.execute(cb);
}
});
}
public LooperIdleLock newIdleLock(Object lock) {
- LooperIdleLock idleLock = new LooperIdleLock(lock, Looper.getMainLooper());
+ LooperIdleLock idleLock = new LooperIdleLock(lock, mUiExecutor.getLooper());
// If we are not binding or if the main looper is already idle, there is no reason to wait
- if (mCallbacks.get() == null || Looper.getMainLooper().getQueue().isIdle()) {
+ if (mUiExecutor.getLooper().getQueue().isIdle()) {
idleLock.queueIdle();
}
return idleLock;
}
+
+ private static class WorkspaceBinder {
+
+ private final Executor mUiExecutor;
+ private final Callbacks mCallbacks;
+
+ private final LauncherAppState mApp;
+ private final BgDataModel mBgDataModel;
+
+ private final int mMyBindingId;
+ private final ArrayList<ItemInfo> mWorkspaceItems;
+ private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
+ private final IntArray mOrderedScreenIds;
+
+
+ WorkspaceBinder(Callbacks callbacks,
+ Executor uiExecutor,
+ LauncherAppState app,
+ BgDataModel bgDataModel,
+ int myBindingId,
+ ArrayList<ItemInfo> workspaceItems,
+ ArrayList<LauncherAppWidgetInfo> appWidgets,
+ IntArray orderedScreenIds) {
+ mCallbacks = callbacks;
+ mUiExecutor = uiExecutor;
+ mApp = app;
+ mBgDataModel = bgDataModel;
+ mMyBindingId = myBindingId;
+ mWorkspaceItems = workspaceItems;
+ mAppWidgets = appWidgets;
+ mOrderedScreenIds = orderedScreenIds;
+ }
+
+ private void bind() {
+ final int currentScreen;
+ {
+ // Create an anonymous scope to calculate currentScreen as it has to be a
+ // final variable.
+ int currScreen = mCallbacks.getPageToBindSynchronously();
+ if (currScreen >= mOrderedScreenIds.size()) {
+ // There may be no workspace screens (just hotseat items and an empty page).
+ currScreen = PagedView.INVALID_PAGE;
+ }
+ currentScreen = currScreen;
+ }
+ final boolean validFirstPage = currentScreen >= 0;
+ final int currentScreenId =
+ validFirstPage ? mOrderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
+
+ // Separate the items that are on the current screen, and all the other remaining items
+ ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
+ ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
+ ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
+ ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
+
+ filterCurrentWorkspaceItems(currentScreenId, mWorkspaceItems, currentWorkspaceItems,
+ otherWorkspaceItems);
+ filterCurrentWorkspaceItems(currentScreenId, mAppWidgets, currentAppWidgets,
+ otherAppWidgets);
+ final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
+ sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
+ sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
+
+ // Tell the workspace that we're about to start binding items
+ executeCallbacksTask(c -> {
+ c.clearPendingBinds();
+ c.startBinding();
+ }, mUiExecutor);
+
+ // Bind workspace screens
+ executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
+
+ Executor mainExecutor = mUiExecutor;
+ // Load items on the current page.
+ bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
+ bindAppWidgets(currentAppWidgets, mainExecutor);
+ // In case of validFirstPage, only bind the first screen, and defer binding the
+ // remaining screens after first onDraw (and an optional the fade animation whichever
+ // happens later).
+ // This ensures that the first screen is immediately visible (eg. during rotation)
+ // In case of !validFirstPage, bind all pages one after other.
+ final Executor deferredExecutor =
+ validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
+
+ executeCallbacksTask(c -> c.finishFirstPageBind(
+ validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
+
+ bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
+ bindAppWidgets(otherAppWidgets, deferredExecutor);
+ // Tell the workspace that we're done binding items
+ executeCallbacksTask(c -> c.finishBindingItems(currentScreen), deferredExecutor);
+
+ if (validFirstPage) {
+ executeCallbacksTask(c -> {
+ // We are loading synchronously, which means, some of the pages will be
+ // bound after first draw. Inform the mCallbacks that page binding is
+ // not complete, and schedule the remaining pages.
+ c.onPageBoundSynchronously(currentScreen);
+ c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
+
+ }, mUiExecutor);
+ }
+ }
+
+ private void bindWorkspaceItems(
+ final ArrayList<ItemInfo> workspaceItems, final Executor executor) {
+ // Bind the workspace items
+ int count = workspaceItems.size();
+ for (int i = 0; i < count; i += ITEMS_CHUNK) {
+ final int start = i;
+ final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
+ executeCallbacksTask(
+ c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
+ executor);
+ }
+ }
+
+ private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets, Executor executor) {
+ // Bind the widgets, one at a time
+ int count = appWidgets.size();
+ for (int i = 0; i < count; i++) {
+ final ItemInfo widget = appWidgets.get(i);
+ executeCallbacksTask(
+ c -> c.bindItems(Collections.singletonList(widget), false), executor);
+ }
+ }
+
+ protected void executeCallbacksTask(CallbackTask task, Executor executor) {
+ executor.execute(() -> {
+ if (mMyBindingId != mBgDataModel.lastBindId) {
+ Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
+ return;
+ }
+ task.execute(mCallbacks);
+ });
+ }
+ }
}
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index e12633b..5a7b4d3 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -20,17 +20,16 @@
import com.android.launcher3.AppInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.LauncherModel.ModelUpdateTask;
import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.widget.WidgetListRowEntry;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.List;
import java.util.concurrent.Executor;
/**
@@ -78,13 +77,9 @@
* Schedules a {@param task} to be executed on the current callbacks.
*/
public final void scheduleCallbackTask(final CallbackTask task) {
- final Callbacks callbacks = mModel.getCallback();
- mUiExecutor.execute(() -> {
- Callbacks cb = mModel.getCallback();
- if (callbacks == cb && cb != null) {
- task.execute(callbacks);
- }
- });
+ for (final Callbacks cb : mModel.getCallbacks()) {
+ mUiExecutor.execute(() -> task.execute(cb));
+ }
}
public ModelWriter getModelWriter() {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 88f2a09..c24b939 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -436,9 +436,10 @@
}
public interface Callbacks {
- void rebindModel();
-
- int getCurrentWorkspaceScreen();
+ /**
+ * Returns the page number to bind first, synchronously if possible or -1
+ */
+ int getPageToBindSynchronously();
void clearPendingBinds();
void startBinding();
void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
diff --git a/src/com/android/launcher3/model/ModelPreload.java b/src/com/android/launcher3/model/ModelPreload.java
index 2bd6cd4..713492b 100644
--- a/src/com/android/launcher3/model/ModelPreload.java
+++ b/src/com/android/launcher3/model/ModelPreload.java
@@ -18,14 +18,15 @@
import android.content.Context;
import android.util.Log;
+import androidx.annotation.WorkerThread;
+
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.model.BgDataModel.Callbacks;
import java.util.concurrent.Executor;
-import androidx.annotation.WorkerThread;
-
/**
* Utility class to preload LauncherModel
*/
@@ -50,7 +51,7 @@
@Override
public final void run() {
mModel.startLoaderForResultsIfNotLoaded(
- new LoaderResults(mApp, mBgDataModel, mAllAppsList, 0, null));
+ new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
Log.d(TAG, "Preload completed : " + mModel.isModelLoaded());
onComplete(mModel.isModelLoaded());
}
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index bdf3a69..ccd1554 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -41,7 +41,6 @@
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.ContentWriter;
import com.android.launcher3.util.ItemInfoMatcher;
@@ -350,12 +349,15 @@
mDeleteRunnables.clear();
}
- public void abortDelete(int pageToBindFirst) {
+ /**
+ * Aborts a previous delete operation pending commit
+ */
+ public void abortDelete() {
mPreparingToUndo = false;
mDeleteRunnables.clear();
// We do a full reload here instead of just a rebind because Folders change their internal
// state when dragging an item out, which clobbers the rebind unless we load from the DB.
- mModel.forceReload(pageToBindFirst);
+ mModel.forceReload();
}
private class UpdateItemRunnable extends UpdateItemBaseRunnable {
@@ -472,7 +474,7 @@
}
void verifyModel() {
- if (!mVerifyChanges || mModel.getCallback() == null) {
+ if (!mVerifyChanges || !mModel.hasCallbacks()) {
return;
}
@@ -488,11 +490,9 @@
// Bound model has not changed during the job
return;
}
+
// Bound model was changed between submitting the job and executing the job
- Callbacks callbacks = mModel.getCallback();
- if (callbacks != null) {
- callbacks.rebindModel();
- }
+ mModel.rebindCallbacks();
});
}
}
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 40e267b..ecfc77c 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -24,6 +24,7 @@
import android.graphics.Color;
import android.os.Bundle;
import android.os.Debug;
+import android.system.Os;
import android.view.View;
import androidx.annotation.Keep;
@@ -136,6 +137,11 @@
break;
}
+ case TestProtocol.REQUEST_PID: {
+ response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, Os.getpid());
+ break;
+ }
+
case TestProtocol.REQUEST_TOTAL_PSS_KB: {
runGcAndFinalizersSync();
Debug.MemoryInfo mem = new Debug.MemoryInfo();
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index dd8df88..929315a 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -73,6 +73,7 @@
public static final String REQUEST_APPS_LIST_SCROLL_Y = "apps-list-scroll-y";
public static final String REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN = "overview-left-margin";
public static final String REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN = "overview-right-margin";
+ public static final String REQUEST_PID = "pid";
public static final String REQUEST_TOTAL_PSS_KB = "total_pss";
public static final String REQUEST_JAVA_LEAK = "java-leak";
public static final String REQUEST_NATIVE_LEAK = "native-leak";
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index f40f976..f470edb 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -23,7 +23,7 @@
import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_SCALE_COMPONENT;
import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
import android.animation.Animator;
@@ -434,7 +434,7 @@
updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()),
targetState, velocity, fling);
mCurrentAnimation.dispatchOnStartWithVelocity(endProgress, velocity);
- if (fling && targetState == LauncherState.ALL_APPS && !QUICKSTEP_SPRINGS.get()) {
+ if (fling && targetState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
}
anim.start();
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
index 5a131c8..451ae28 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -23,6 +23,8 @@
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewTreeObserver.OnDrawListener;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.Launcher;
import java.util.ArrayList;
@@ -118,7 +120,11 @@
return mCompleted;
}
- protected void runAllTasks() {
+ /**
+ * Executes all tasks immediately
+ */
+ @VisibleForTesting
+ public void runAllTasks() {
for (final Runnable r : mTasks) {
r.run();
}
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 88d34da..5ba931d 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -47,7 +47,6 @@
import java.util.ArrayList;
import java.util.List;
-
/**
* Popup shown on long pressing an empty space in launcher
*/
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
index 789bfd8..dcb4636 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
@@ -16,12 +16,14 @@
package com.android.launcher3.model;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.widget.WidgetListRowEntry;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
@@ -31,8 +33,13 @@
public class LoaderResults extends BaseLoaderResults {
public LoaderResults(LauncherAppState app, BgDataModel dataModel,
- AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
- super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+ AllAppsList allAppsList, Callbacks[] callbacks) {
+ this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+ }
+
+ public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+ AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+ super(app, dataModel, allAppsList, callbacks, executor);
}
@Override
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 56eca6d..1c8f095 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<uses-permission android:name="android.permission.READ_LOGS"/>
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
<application android:debuggable="true">
<uses-library android:name="android.test.runner"/>
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index d7096b0..61f5150 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -18,6 +18,9 @@
import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.UNBUNDLED_POSTSUBMIT;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -36,10 +39,12 @@
import com.android.launcher3.tapl.AppIconMenuItem;
import com.android.launcher3.tapl.Widgets;
import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
import com.android.launcher3.views.OptionsPopupView;
import com.android.launcher3.widget.WidgetsFullSheet;
import com.android.launcher3.widget.WidgetsRecyclerView;
+import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -50,10 +55,18 @@
public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
private static final String APP_NAME = "LauncherTestApp";
+ private int mLauncherPid;
+
@Before
public void setUp() throws Exception {
super.setUp();
initialize(this);
+ mLauncherPid = mLauncher.getPid();
+ }
+
+ @After
+ public void teardown() {
+ assertEquals("Launcher crashed, pid mismatch:", mLauncherPid, mLauncher.getPid());
}
public static void initialize(AbstractLauncherUiTest test) throws Exception {
@@ -100,6 +113,16 @@
mLauncher.pressHome();
}
+ // b/146432215: remove @Stability after 2/1/2020 if this test doesn't flake
+ @Test
+ @Stability(flavors = LOCAL | UNBUNDLED_POSTSUBMIT)
+ public void testOpenHomeSettingsFromWorkspace() {
+ mDevice.pressMenu();
+ mDevice.waitForIdle();
+ mLauncher.getOptionsPopupMenu().getMenuItem("Home settings")
+ .launch(mDevice.getLauncherPackageName());
+ }
+
@Test
@Ignore
public void testPressHomeOnAllAppsContextMenu() throws Exception {
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index f98957e..b394bcb 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -102,14 +102,15 @@
final String launcherVersion;
try {
+ final String launcherPackageName = UiDevice.getInstance(getInstrumentation())
+ .getLauncherPackageName();
+ Log.d(TAG, "Launcher package: " + launcherPackageName);
+
launcherVersion = getInstrumentation().
getContext().
getPackageManager().
- getPackageInfo(
- UiDevice.getInstance(getInstrumentation()).
- getLauncherPackageName(),
- 0).
- versionName;
+ getPackageInfo(launcherPackageName, 0)
+ .versionName;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
@@ -148,7 +149,8 @@
Log.d(TAG, "PLATFORM PRESUBMIT");
runFlavor = PLATFORM_PRESUBMIT;
} else if (launcherBuildMatcher.group("platform") != null
- && platformBuildMatcher.group("postsubmit") != null) {
+ && (platformBuildMatcher.group("postsubmit") != null
+ || platformBuildMatcher.group("commandLine") != null)) {
Log.d(TAG, "PLATFORM POSTSUBMIT");
runFlavor = PLATFORM_POSTSUBMIT;
} else {
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 44fc3f7..2da6344 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,9 +16,6 @@
package com.android.launcher3.tapl;
-import android.graphics.Point;
-import android.os.SystemClock;
-import android.view.MotionEvent;
import android.widget.TextView;
import androidx.test.uiautomator.By;
@@ -41,14 +38,8 @@
* Long-clicks the icon to open its menu.
*/
public AppIconMenu openMenu() {
- final Point iconCenter = mObject.getVisibleCenter();
- final long downTime = SystemClock.uptimeMillis();
- mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, iconCenter);
- final UiObject2 deepShortcutsContainer = mLauncher.waitForLauncherObject(
- "deep_shortcuts_container");
- mLauncher.sendPointer(
- downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, iconCenter);
- return new AppIconMenu(mLauncher, deepShortcutsContainer);
+ return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
+ mObject, "deep_shortcuts_container"));
}
@Override
diff --git a/tests/tapl/com/android/launcher3/tapl/Folder.java b/tests/tapl/com/android/launcher3/tapl/Folder.java
deleted file mode 100644
index 6e6734d..0000000
--- a/tests/tapl/com/android/launcher3/tapl/Folder.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2019 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.tapl;
-
-import android.widget.FrameLayout;
-
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.BySelector;
-import androidx.test.uiautomator.UiObject2;
-
-/**
- * App folder in workspace/
- */
-public final class Folder {
- Folder(LauncherInstrumentation launcher, UiObject2 icon) {
- }
-
- static BySelector getSelector(String folderName, LauncherInstrumentation launcher) {
- return By.clazz(FrameLayout.class).desc(folderName).pkg(launcher.getLauncherPackageName());
- }
-}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 28c9e7a..95c4997 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -184,13 +184,8 @@
.authority(testProviderAuthority)
.build();
- try {
- mDevice.executeShellCommand("pm grant " + testPackage +
- " android.permission.WRITE_SECURE_SETTINGS");
- } catch (IOException e) {
- fail(e.toString());
- }
-
+ mInstrumentation.getUiAutomation().grantRuntimePermission(
+ testPackage, "android.permission.WRITE_SECURE_SETTINGS");
PackageManager pm = getContext().getPackageManager();
ProviderInfo pi = pm.resolveContentProvider(
@@ -261,9 +256,9 @@
Closable addContextLayer(String piece) {
mDiagnosticContext.addLast(piece);
- log("Added context: " + getContextDescription());
+ log("Entering context: " + piece);
return () -> {
- log("Removing context: " + getContextDescription());
+ log("Leaving context: " + piece);
mDiagnosticContext.removeLast();
};
}
@@ -346,14 +341,11 @@
private String getSystemHealthMessage() {
final String testPackage = getContext().getPackageName();
- try {
- mDevice.executeShellCommand("pm grant " + testPackage +
- " android.permission.READ_LOGS");
- mDevice.executeShellCommand("pm grant " + testPackage +
- " android.permission.PACKAGE_USAGE_STATS");
- } catch (IOException e) {
- e.printStackTrace();
- }
+
+ mInstrumentation.getUiAutomation().grantRuntimePermission(
+ testPackage, "android.permission.READ_LOGS");
+ mInstrumentation.getUiAutomation().grantRuntimePermission(
+ testPackage, "android.permission.PACKAGE_USAGE_STATS");
return mSystemHealthSupplier != null
? mSystemHealthSupplier.apply(START_TIME)
@@ -691,6 +683,20 @@
}
}
+ /**
+ * Gets the Options Popup Menu object if the current state is showing the popup menu. Fails if
+ * the launcher is not in that state.
+ *
+ * @return Options Popup Menu object.
+ */
+ @NonNull
+ public OptionsPopupMenu getOptionsPopupMenu() {
+ try (LauncherInstrumentation.Closable c = addContextLayer(
+ "want to get context menu object")) {
+ return new OptionsPopupMenu(this);
+ }
+ }
+
void waitUntilGone(String resId) {
assertTrue("Unexpected launcher object visible: " + resId,
mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
@@ -999,6 +1005,16 @@
return getSystemIntegerRes(context, "config_navBarInteractionMode");
}
+ @NonNull
+ UiObject2 clickAndGet(@NonNull final UiObject2 target, @NonNull String resName) {
+ final Point targetCenter = target.getVisibleCenter();
+ final long downTime = SystemClock.uptimeMillis();
+ sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter);
+ final UiObject2 result = waitForLauncherObject(resName);
+ sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter);
+ return result;
+ }
+
private static int getSystemIntegerRes(Context context, String resName) {
Resources res = context.getResources();
int resId = res.getIdentifier(resName, "integer", "android");
@@ -1058,6 +1074,10 @@
getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
+ public int getPid() {
+ return getTestInfo(TestProtocol.REQUEST_PID).getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ }
+
public void produceJavaLeak() {
getTestInfo(TestProtocol.REQUEST_JAVA_LEAK);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
new file mode 100644
index 0000000..282fca9
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+
+public class OptionsPopupMenu {
+
+ private final LauncherInstrumentation mLauncher;
+ private final UiObject2 mDeepShortcutsContainer;
+
+ OptionsPopupMenu(LauncherInstrumentation launcher) {
+ mLauncher = launcher;
+ mDeepShortcutsContainer = launcher.waitForLauncherObject("deep_shortcuts_container");
+ }
+
+ /**
+ * Returns a menu item with a given label. Fails if it doesn't exist.
+ */
+ @NonNull
+ public OptionsPopupMenuItem getMenuItem(@NonNull final String label) {
+ final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
+ By.text(label));
+ return new OptionsPopupMenuItem(mLauncher, menuItem);
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
new file mode 100644
index 0000000..8527d05
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+public class OptionsPopupMenuItem {
+
+ private final LauncherInstrumentation mLauncher;
+ private final UiObject2 mObject;
+
+ OptionsPopupMenuItem(@NonNull LauncherInstrumentation launcher, @NonNull UiObject2 shortcut) {
+ mLauncher = launcher;
+ mObject = shortcut;
+ }
+
+ /**
+ * Clicks the option.
+ */
+ @NonNull
+ public void launch(@NonNull String expectedPackageName) {
+ LauncherInstrumentation.log("OptionsPopupMenuItem before click "
+ + mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
+ mObject.click();
+ mLauncher.assertTrue(
+ "App didn't start: " + By.pkg(expectedPackageName),
+ mLauncher.getDevice().wait(Until.hasObject(By.pkg(expectedPackageName)),
+ LauncherInstrumentation.WAIT_TIME_MS));
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 3299d5d..af7e552 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -183,12 +183,6 @@
mHotseat, AppIcon.getAppIconSelector(appName, mLauncher)));
}
- @NonNull
- public Folder getHotseatFolder(String appName) {
- return new Folder(mLauncher, mLauncher.waitForObjectInContainer(
- mHotseat, Folder.getSelector(appName, mLauncher)));
- }
-
static void dragIconToWorkspace(
LauncherInstrumentation launcher, Launchable launchable, Point dest,
String longPressIndicator) {
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
new file mode 100644
index 0000000..0f4163d
--- /dev/null
+++ b/tools/checkstyle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd" [
+ <!ENTITY defaultCopyrightCheck SYSTEM "../../../../prebuilts/checkstyle/default-copyright-check.xml">
+ <!ENTITY defaultJavadocChecks SYSTEM "../../../../prebuilts/checkstyle/default-javadoc-checks.xml">
+ <!ENTITY defaultTreewalkerChecks SYSTEM "../../../../prebuilts/checkstyle/default-treewalker-checks.xml">
+ <!ENTITY defaultModuleChecks SYSTEM "../../../../prebuilts/checkstyle/default-module-checks.xml">
+]>
+
+<module name="Checker">
+ &defaultModuleChecks;
+ &defaultCopyrightCheck;
+ <module name="TreeWalker">
+ &defaultJavadocChecks;
+ &defaultTreewalkerChecks;
+ </module>
+
+ <module name="SuppressionFilter">
+ <property name="file" value="tools/checkstyle_suppression.xml" />
+ </module>
+</module>
diff --git a/tools/checkstyle_suppression.xml b/tools/checkstyle_suppression.xml
new file mode 100644
index 0000000..799e750
--- /dev/null
+++ b/tools/checkstyle_suppression.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN" "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
+<suppressions>
+
+ <!-- Robolectric uses magic method names like `__constructor__` -->
+ <suppress files="/robolectric_tests" checks="MethodName|JavadocType|JavadocMethod" />
+
+</suppressions>
diff --git a/print_db.py b/tools/print_db.py
similarity index 100%
rename from print_db.py
rename to tools/print_db.py