From a7a5bf31013e5285fe164371fd094d5629107e14 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Sun, 5 Jan 2020 15:35:29 +0530 Subject: [PATCH] Adding support for multiple Model clients Bug: 137568159 Change-Id: Ia4db800b19cc80c695fcb9ea28e07709dfd08c6a --- .../launcher3/model/LoaderResults.java | 14 +- .../hybridhotseat/HotseatEduController.java | 2 +- .../model/AddWorkspaceItemsTaskTest.java | 2 +- .../model/DefaultLayoutProviderTest.java | 52 +--- .../model/GridSizeMigrationTaskTest.java | 4 +- .../launcher3/model/LoaderCursorTest.java | 7 +- .../model/ModelMultiCallbacksTest.java | 238 ++++++++++++++++ .../shadows/LShadowLauncherApps.java | 23 ++ .../launcher3/util/LauncherModelHelper.java | 55 ++++ .../android/launcher3/DeleteDropTarget.java | 3 +- src/com/android/launcher3/Launcher.java | 49 ++-- .../android/launcher3/LauncherAppState.java | 5 - src/com/android/launcher3/LauncherModel.java | 150 ++++++---- src/com/android/launcher3/PagedView.java | 4 +- .../graphics/LauncherPreviewRenderer.java | 3 +- .../launcher3/model/BaseLoaderResults.java | 266 ++++++++++-------- .../launcher3/model/BaseModelUpdateTask.java | 15 +- .../android/launcher3/model/BgDataModel.java | 7 +- .../android/launcher3/model/ModelPreload.java | 7 +- .../android/launcher3/model/ModelWriter.java | 16 +- .../launcher3/util/ViewOnDrawExecutor.java | 8 +- .../launcher3/model/LoaderResults.java | 13 +- 22 files changed, 655 insertions(+), 288 deletions(-) create mode 100644 robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java diff --git a/go/src/com/android/launcher3/model/LoaderResults.java b/go/src/com/android/launcher3/model/LoaderResults.java index 26c3313185..713053191d 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 @@ import java.lang.ref.WeakReference; public class LoaderResults extends BaseLoaderResults { public LoaderResults(LauncherAppState app, BgDataModel dataModel, - AllAppsList allAppsList, int pageToBindFirst, WeakReference 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 0fd4aacc23..923e0506ef 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 @@ public class HotseatEduController { } 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 ea7c137d4e..b7f224310f 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 @@ public class AddWorkspaceItemsTaskTest { @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 e0ddcb1298..f8ac010ee1 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.Context; 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.junit.runner.RunWith; 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 @@ import java.util.ArrayList; @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 @@ public class DefaultLayoutProviderTest { @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 @@ public class DefaultLayoutProviderTest { } 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 8dd7588c27..1ed4bca3ee 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 @@ public class GridSizeMigrationTaskTest { @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 @@ public class GridSizeMigrationTaskTest { } /** - * 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 485431412e..7fa3ee96bf 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 static org.robolectric.Shadows.shadowOf; 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 @@ public class LoaderCursorTest { private MatrixCursor mCursor; private InvariantDeviceProfile mIDP; private Context mContext; - private LauncherApps mLauncherApps; private LoaderCursor mLoaderCursor; @@ -86,7 +84,6 @@ public class LoaderCursorTest { 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 @@ public class LoaderCursorTest { 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 @@ public class LoaderCursorTest { 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 0000000000..c7979b21d2 --- /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 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 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 ccbc18ad3c..166e28bfc3 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 org.robolectric.shadows.ShadowLauncherApps; 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 @@ public class LShadowLauncherApps extends ShadowLauncherApps { return RuntimeEnvironment.application.getPackageManager() .getApplicationInfo(packageName, flags); } + + @Implementation + public List 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 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 1a03f9f411..655055c9cf 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 com.android.launcher3.util.Executors.MODEL_EXECUTOR; 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.mockito.ArgumentCaptor; 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 class LauncherModelHelper { 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> mFieldCache = new HashMap<>(); public final TestLauncherProvider provider; @@ -285,4 +302,42 @@ public class LauncherModelHelper { 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 bd48aec1ca..423f2bbf1c 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -126,7 +126,8 @@ public class DeleteDropTarget extends ButtonDropTarget { 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 a7bb9ee259..f5fafbfcd6 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -292,6 +292,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, 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 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, 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 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, // 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 @@ -521,15 +518,6 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, getStateManager().reapplyState(cancelCurrentAnimation); } - @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 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, // initialized properly. onSaveInstanceState(new Bundle()); if (oldWallpaperProfile != getWallpaperDeviceProfile()) { - rebindModel(); + mModel.rebindCallbacks(); } } @@ -1543,13 +1531,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, 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 { @@ -1956,12 +1938,22 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, mOnDeferredActivityLaunchCallback = callback; } + /** + * 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 class Launcher extends BaseDraggingActivity implements LauncherExterns, public void onPageBoundSynchronously(int page) { mSynchronouslyBoundPage = page; + mWorkspace.setCurrentPage(page); + mPageToBindSynchronously = PagedView.INVALID_PAGE; } @Override @@ -2403,6 +2397,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, // 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 c6946cacf1..4cd038d42e 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -160,11 +160,6 @@ public class LauncherAppState { } } - 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 63b0e1eab5..cf978b52a1 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.pm.UserCache; 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.CancellationException; 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 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi 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 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi } } - @Thunk WeakReference mCallbacks; + private final ArrayList mCallbacksList = new ArrayList<>(1); // < only access in worker thread > private final AllAppsList mBgAllAppsList; @@ -141,9 +141,8 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi * Adds the provided items to the workspace. */ public void addAndBindAddedWorkspaceItems(List> itemList) { - Callbacks callbacks = getCallback(); - if (callbacks != null) { - callbacks.preAddApps(); + for (Callbacks cb : getCallbacks()) { + cb.preAddApps(); } enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList)); } @@ -153,16 +152,6 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi 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 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi } } } 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 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi // 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 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi /** * 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 class LauncherModel extends LauncherApps.Callback implements InstallSessi } 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 @@ public class LauncherModel extends LauncherApps.Callback implements InstallSessi 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 ff2b400387..a1888bfd08 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -64,7 +64,7 @@ public abstract class PagedView extends ViewGrou 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 @@ public abstract class PagedView extends ViewGrou 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 bfe7351c0a..def76e8d30 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.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; @@ -377,7 +378,7 @@ public class LauncherPreviewRenderer implements Callable { 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 76c2951310..0d12183ade 100644 --- a/src/com/android/launcher3/model/BaseLoaderResults.java +++ b/src/com/android/launcher3/model/BaseLoaderResults.java @@ -18,9 +18,7 @@ package com.android.launcher3.model; 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.LauncherModel.CallbackTask; 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 @@ public abstract class BaseLoaderResults { 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 mCallbacks; + private final Callbacks[] mCallbacksList; private int mMyBindingId; public BaseLoaderResults(LauncherAppState app, BgDataModel dataModel, - AllAppsList allAppsList, int pageToBindFirst, WeakReference 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 workspaceItems = new ArrayList<>(); ArrayList appWidgets = new ArrayList<>(); @@ -96,97 +84,9 @@ public abstract class BaseLoaderResults { 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 currentWorkspaceItems = new ArrayList<>(); - ArrayList otherWorkspaceItems = new ArrayList<>(); - ArrayList currentAppWidgets = new ArrayList<>(); - ArrayList 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 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 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 @@ public abstract class BaseLoaderResults { 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 mWorkspaceItems; + private final ArrayList mAppWidgets; + private final IntArray mOrderedScreenIds; + + + WorkspaceBinder(Callbacks callbacks, + Executor uiExecutor, + LauncherAppState app, + BgDataModel bgDataModel, + int myBindingId, + ArrayList workspaceItems, + ArrayList 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 currentWorkspaceItems = new ArrayList<>(); + ArrayList otherWorkspaceItems = new ArrayList<>(); + ArrayList currentAppWidgets = new ArrayList<>(); + ArrayList 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 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 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 e12633bcd1..5a7b4d37bc 100644 --- a/src/com/android/launcher3/model/BaseModelUpdateTask.java +++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java @@ -20,17 +20,16 @@ import android.util.Log; 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 @@ public abstract class BaseModelUpdateTask implements ModelUpdateTask { * 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 88f2a09a80..c24b9397d8 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -436,9 +436,10 @@ public class BgDataModel { } 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 shortcuts, boolean forceAnimateIcons); diff --git a/src/com/android/launcher3/model/ModelPreload.java b/src/com/android/launcher3/model/ModelPreload.java index 2bd6cd4db6..713492b9fa 100644 --- a/src/com/android/launcher3/model/ModelPreload.java +++ b/src/com/android/launcher3/model/ModelPreload.java @@ -18,14 +18,15 @@ package com.android.launcher3.model; 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 @@ public class ModelPreload implements ModelUpdateTask { @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 bdf3a6918d..ccd1554b23 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.Utilities; 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 @@ public class ModelWriter { 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 @@ public class ModelWriter { } void verifyModel() { - if (!mVerifyChanges || mModel.getCallback() == null) { + if (!mVerifyChanges || !mModel.hasCallbacks()) { return; } @@ -488,11 +490,9 @@ public class ModelWriter { // 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 5a131c83f8..451ae28e4a 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; 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 @@ public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable, 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 789bfd8688..dcb4636bfc 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 @@ import java.util.HashMap; public class LoaderResults extends BaseLoaderResults { public LoaderResults(LauncherAppState app, BgDataModel dataModel, - AllAppsList allAppsList, int pageToBindFirst, WeakReference 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