Adding support for multiple Model clients

Bug: 137568159
Change-Id: Ia4db800b19cc80c695fcb9ea28e07709dfd08c6a
This commit is contained in:
Sunny Goyal 2020-01-05 15:35:29 +05:30
parent 4823b0c99b
commit a7a5bf3101
22 changed files with 655 additions and 288 deletions

View File

@ -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> 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

View File

@ -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();
}

View File

@ -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);

View File

@ -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(),

View File

@ -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.
*/

View File

@ -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(

View File

@ -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)));
}
}
}

View File

@ -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<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();
}
}

View File

@ -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<Class, HashMap<String, Field>> 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);
}
}

View File

@ -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,

View File

@ -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,

View File

@ -160,11 +160,6 @@ public class LauncherAppState {
}
}
LauncherModel setLauncher(Launcher launcher) {
mModel.initialize(launcher);
return mModel;
}
public IconCache getIconCache() {
return mIconCache;
}

View File

@ -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<Callbacks> mCallbacks;
private final ArrayList<Callbacks> 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<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 @@ 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()]);
}
}
}

View File

@ -64,7 +64,7 @@ public abstract class PagedView<T extends View & PageIndicator> 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<T extends View & PageIndicator> 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;

View File

@ -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<Bitmap> {
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;

View File

@ -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<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 @@ 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<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 @@ 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<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);
});
}
}
}

View File

@ -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() {

View File

@ -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<ItemInfo> shortcuts, boolean forceAnimateIcons);

View File

@ -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());
}

View File

@ -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();
});
}
}

View File

@ -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();
}

View File

@ -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> 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