Adding support for multiple Model clients
Bug: 137568159
Change-Id: Ia4db800b19cc80c695fcb9ea28e07709dfd08c6a
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/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/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/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/Launcher.java b/src/com/android/launcher3/Launcher.java
index a7bb9ee..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.
@@ -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();
}
}
@@ -1543,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 {
@@ -1957,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;
@@ -2339,6 +2331,8 @@
public void onPageBoundSynchronously(int page) {
mSynchronouslyBoundPage = page;
+ mWorkspace.setCurrentPage(page);
+ mPageToBindSynchronously = PagedView.INVALID_PAGE;
}
@Override
@@ -2403,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 63b0e1e..cf978b5 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -16,6 +16,12 @@
package com.android.launcher3;
+import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
+import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
@@ -52,13 +58,12 @@
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;
@@ -67,12 +72,6 @@
import java.util.concurrent.Executor;
import java.util.function.Supplier;
-import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
-import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
-
/**
* Maintains in-memory state of the Launcher. It is expected that there should be only one
* LauncherModel object held in a static. Also provide APIs for updating the database state
@@ -83,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
@@ -100,7 +100,7 @@
}
}
- @Thunk WeakReference<Callbacks> mCallbacks;
+ private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
// < only access in worker thread >
private final AllAppsList mBgAllAppsList;
@@ -141,9 +141,8 @@
* 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));
}
@@ -153,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;
@@ -262,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();
@@ -285,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).
@@ -336,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;
}
}
@@ -498,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);
}
@@ -572,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/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index bfe7351..def76e8 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -70,6 +70,7 @@
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;
@@ -377,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/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_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