From 762d06136cc7c91afe8cdfdac5426b58c174edd8 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Wed, 29 Jul 2020 15:03:46 -0700 Subject: [PATCH] Caching last predictions and loading it with model Adding support for persisting itemInfos on disk. This uses a separate xml file. Unlike prefs, it does not keep the items in memory and is just a wraper over reading/writing a file. Bug: 160748731 Change-Id: Iaccab9928ab8f30127fb3c2d630ca8ca83f0bd05 --- .../launcher3/model/PredictionUpdateTask.java | 40 ++-- .../model/QuickstepModelDelegate.java | 177 +++++++++++++++-- .../launcher3/model/LoaderCursorTest.java | 8 +- .../android/launcher3/AutoInstallsLayout.java | 2 +- .../launcher3/model/BaseLoaderResults.java | 12 +- .../android/launcher3/model/BgDataModel.java | 8 + .../android/launcher3/model/LoaderCursor.java | 2 +- .../android/launcher3/model/LoaderTask.java | 15 +- .../launcher3/model/ModelDelegate.java | 7 +- .../launcher3/model/data/ItemInfo.java | 6 - .../model/data/LauncherAppWidgetInfo.java | 10 - src/com/android/launcher3/util/IOUtils.java | 27 +-- .../launcher3/util/PersistedItemArray.java | 180 ++++++++++++++++++ 13 files changed, 393 insertions(+), 101 deletions(-) create mode 100644 src/com/android/launcher3/util/PersistedItemArray.java diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java index 721e2bebd5..b0fba3d5ae 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java @@ -21,6 +21,7 @@ import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKE import android.app.prediction.AppTarget; import android.content.ComponentName; +import android.content.Context; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; @@ -29,6 +30,7 @@ import android.os.UserHandle; import com.android.launcher3.LauncherAppState; import com.android.launcher3.Utilities; import com.android.launcher3.model.BgDataModel.FixedContainerItems; +import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -43,27 +45,22 @@ import java.util.stream.Collectors; public class PredictionUpdateTask extends BaseModelUpdateTask { private final List mTargets; - private final int mContainerId; + private final PredictorState mPredictorState; - PredictionUpdateTask(int containerId, List targets) { - mContainerId = containerId; + PredictionUpdateTask(PredictorState predictorState, List targets) { + mPredictorState = predictorState; mTargets = targets; } @Override public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { - // TODO: persist the whole list - Utilities.getDevicePrefs(app.getContext()).edit() + Context context = app.getContext(); + + // TODO: remove this + Utilities.getDevicePrefs(context).edit() .putBoolean(LAST_PREDICTION_ENABLED_STATE, !mTargets.isEmpty()).apply(); - FixedContainerItems fci; - synchronized (dataModel) { - fci = dataModel.extraItems.get(mContainerId); - if (fci == null) { - return; - } - } - + FixedContainerItems fci = mPredictorState.items; Set usersForChangedShortcuts = new HashSet<>(fci.items.stream() .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT) .map(info -> info.user) @@ -75,7 +72,7 @@ public class PredictionUpdateTask extends BaseModelUpdateTask { ShortcutInfo si = target.getShortcutInfo(); if (si != null) { usersForChangedShortcuts.add(si.getUserHandle()); - itemInfo = new WorkspaceItemInfo(si, app.getContext()); + itemInfo = new WorkspaceItemInfo(si, context); app.getIconCache().getShortcutIcon(itemInfo, si); } else { String className = target.getClassName(); @@ -87,16 +84,18 @@ public class PredictionUpdateTask extends BaseModelUpdateTask { UserHandle user = target.getUser(); itemInfo = apps.data.stream() .filter(info -> user.equals(info.user) && cn.equals(info.componentName)) - .map(AppInfo::makeWorkspaceItem) + .map(ai -> { + app.getIconCache().getTitleAndIcon(ai, false); + return ai.makeWorkspaceItem(); + }) .findAny() .orElseGet(() -> { - LauncherActivityInfo lai = app.getContext() - .getSystemService(LauncherApps.class) + LauncherActivityInfo lai = context.getSystemService(LauncherApps.class) .resolveActivity(AppInfo.makeLaunchIntent(cn), user); if (lai == null) { return null; } - AppInfo ai = new AppInfo(app.getContext(), lai, user); + AppInfo ai = new AppInfo(context, lai, user); app.getIconCache().getTitleAndIcon(ai, lai, false); return ai.makeWorkspaceItem(); }); @@ -106,12 +105,15 @@ public class PredictionUpdateTask extends BaseModelUpdateTask { } } - itemInfo.container = mContainerId; + itemInfo.container = fci.containerId; fci.items.add(itemInfo); } bindExtraContainerItems(fci); usersForChangedShortcuts.forEach( u -> dataModel.updateShortcutPinnedState(app.getContext(), u)); + + // Save to disk + mPredictorState.storage.write(context, fci.items); } } diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java index b5164695d5..166cb6cc4d 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -17,6 +17,8 @@ package com.android.launcher3.model; import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionManager; @@ -24,16 +26,32 @@ import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; +import android.os.UserHandle; +import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.model.BgDataModel.FixedContainerItems; +import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.Executors; +import com.android.launcher3.util.PersistedItemArray; import com.android.quickstep.logging.StatsLogCompatManager; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.IntStream; /** * Model delegate which loads prediction items @@ -42,10 +60,12 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state"; + private final PredictorState mAllAppsState = + new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions"); + private final InvariantDeviceProfile mIDP; private final AppEventProducer mAppEventProducer; - private AppPredictor mAllAppsPredictor; private boolean mActive = false; public QuickstepModelDelegate(Context context) { @@ -57,11 +77,15 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange } @Override - public void loadItems() { + public void loadItems(UserManagerState ums, Map pinnedShortcuts) { // TODO: Implement caching and preloading - super.loadItems(); - mDataModel.extraItems.put( - CONTAINER_PREDICTION, new FixedContainerItems(CONTAINER_PREDICTION)); + super.loadItems(ums, pinnedShortcuts); + + WorkspaceItemFactory factory = + new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, mIDP.numAllAppsColumns); + mAllAppsState.items.setItems( + mAllAppsState.storage.read(mApp.getContext(), factory, ums.allUsers::get)); + mDataModel.extraItems.put(CONTAINER_PREDICTION, mAllAppsState.items); mActive = true; recreatePredictors(); @@ -70,8 +94,8 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange @Override public void validateData() { super.validateData(); - if (mAllAppsPredictor != null) { - mAllAppsPredictor.requestPredictionUpdate(); + if (mAllAppsState.predictor != null) { + mAllAppsState.predictor.requestPredictionUpdate(); } } @@ -86,10 +110,7 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange } private void destroyPredictors() { - if (mAllAppsPredictor != null) { - mAllAppsPredictor.destroy(); - mAllAppsPredictor = null; - } + mAllAppsState.destroyPredictor(); } @WorkerThread @@ -98,7 +119,6 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange if (!mActive) { return; } - Context context = mApp.getContext(); AppPredictionManager apm = context.getSystemService(AppPredictionManager.class); if (apm == null) { @@ -107,19 +127,23 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange int count = mIDP.numAllAppsColumns; - mAllAppsPredictor = apm.createAppPredictionSession( + mAllAppsState.predictor = apm.createAppPredictionSession( new AppPredictionContext.Builder(context) .setUiSurface("home") .setPredictedTargetCount(count) .build()); - mAllAppsPredictor.registerPredictionUpdates( - Executors.MODEL_EXECUTOR, this::onAllAppsPredictionChanged); - mAllAppsPredictor.requestPredictionUpdate(); + mAllAppsState.predictor.registerPredictionUpdates( + Executors.MODEL_EXECUTOR, t -> handleUpdate(mAllAppsState, t)); + mAllAppsState.predictor.requestPredictionUpdate(); } - private void onAllAppsPredictionChanged(List targets) { - mApp.getModel().enqueueModelUpdateTask( - new PredictionUpdateTask(CONTAINER_PREDICTION, targets)); + + private void handleUpdate(PredictorState state, List targets) { + if (state.setTargets(targets)) { + // No diff, skip + return; + } + mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets)); } @Override @@ -131,8 +155,119 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange } private void onAppTargetEvent(AppTargetEvent event) { - if (mAllAppsPredictor != null) { - mAllAppsPredictor.notifyAppTargetEvent(event); + if (mAllAppsState.predictor != null) { + mAllAppsState.predictor.notifyAppTargetEvent(event); + } + } + + static class PredictorState { + + public final FixedContainerItems items; + public final PersistedItemArray storage; + public AppPredictor predictor; + + private List mLastTargets; + + PredictorState(int container, String storageName) { + items = new FixedContainerItems(container); + storage = new PersistedItemArray(storageName); + mLastTargets = Collections.emptyList(); + } + + public void destroyPredictor() { + if (predictor != null) { + predictor.destroy(); + predictor = null; + } + } + + /** + * Sets the new targets and returns true if it was different than before. + */ + boolean setTargets(List newTargets) { + List oldTargets = mLastTargets; + mLastTargets = newTargets; + + int size = oldTargets.size(); + return size == newTargets.size() && IntStream.range(0, size) + .allMatch(i -> areAppTargetsSame(oldTargets.get(i), newTargets.get(i))); + } + } + + /** + * Compares two targets for the properties which we care about + */ + private static boolean areAppTargetsSame(AppTarget t1, AppTarget t2) { + if (!Objects.equals(t1.getPackageName(), t2.getPackageName()) + || !Objects.equals(t1.getUser(), t2.getUser()) + || !Objects.equals(t1.getClassName(), t2.getClassName())) { + return false; + } + + ShortcutInfo s1 = t1.getShortcutInfo(); + ShortcutInfo s2 = t2.getShortcutInfo(); + if (s1 != null) { + if (s2 == null || !Objects.equals(s1.getId(), s2.getId())) { + return false; + } + } else if (s2 != null) { + return false; + } + return true; + } + + private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory { + + private final LauncherAppState mAppState; + private final UserManagerState mUMS; + private final Map mPinnedShortcuts; + private final int mMaxCount; + + private int mReadCount = 0; + + protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums, + Map pinnedShortcuts, int maxCount) { + mAppState = appState; + mUMS = ums; + mPinnedShortcuts = pinnedShortcuts; + mMaxCount = maxCount; + } + + @Nullable + @Override + public ItemInfo createInfo(int itemType, UserHandle user, Intent intent) { + if (mReadCount >= mMaxCount) { + return null; + } + switch (itemType) { + case ITEM_TYPE_APPLICATION: { + LauncherActivityInfo lai = mAppState.getContext() + .getSystemService(LauncherApps.class) + .resolveActivity(intent, user); + if (lai == null) { + return null; + } + AppInfo info = new AppInfo(lai, user, mUMS.isUserQuiet(user)); + mAppState.getIconCache().getTitleAndIcon(info, lai, false); + mReadCount++; + return info.makeWorkspaceItem(); + } + case ITEM_TYPE_DEEP_SHORTCUT: { + ShortcutKey key = ShortcutKey.fromIntent(intent, user); + if (key == null) { + return null; + } + ShortcutInfo si = mPinnedShortcuts.get(key); + if (si == null) { + return null; + } + WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext()); + mAppState.getIconCache().getShortcutIcon(wii, si); + mReadCount++; + return wii; + } + } + return null; } } } diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java index 0e760f97db..fb08c56a40 100644 --- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java +++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java @@ -51,7 +51,7 @@ import android.os.Process; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.LauncherSettings; +import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.Executors; @@ -92,9 +92,9 @@ public class LoaderCursorTest { SCREEN, CELLX, CELLY, RESTORED, INTENT }); - mLoaderCursor = new LoaderCursor(mCursor, LauncherSettings.Favorites.CONTENT_URI, mApp, - new UserManagerState()); - mLoaderCursor.allUsers.put(0, Process.myUserHandle()); + UserManagerState ums = new UserManagerState(); + mLoaderCursor = new LoaderCursor(mCursor, Favorites.CONTENT_URI, mApp, ums); + ums.allUsers.put(0, Process.myUserHandle()); } private void initCursor(int itemType, String title) { diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java index 432073ebaf..f61bc055e2 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -658,7 +658,7 @@ public class AutoInstallsLayout { } } - protected static void beginDocument(XmlPullParser parser, String firstElementName) + public static void beginDocument(XmlPullParser parser, String firstElementName) throws XmlPullParserException, IOException { int type; while ((type = parser.next()) != XmlPullParser.START_TAG diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java index 8b0ef7b97f..586333fd2e 100644 --- a/src/com/android/launcher3/model/BaseLoaderResults.java +++ b/src/com/android/launcher3/model/BaseLoaderResults.java @@ -27,6 +27,7 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel.CallbackTask; import com.android.launcher3.PagedView; import com.android.launcher3.model.BgDataModel.Callbacks; +import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -76,18 +77,20 @@ public abstract class BaseLoaderResults { ArrayList workspaceItems = new ArrayList<>(); ArrayList appWidgets = new ArrayList<>(); final IntArray orderedScreenIds = new IntArray(); + ArrayList extraItems = new ArrayList<>(); synchronized (mBgDataModel) { workspaceItems.addAll(mBgDataModel.workspaceItems); appWidgets.addAll(mBgDataModel.appWidgets); orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens()); + mBgDataModel.extraItems.forEach(extraItems::add); mBgDataModel.lastBindId++; mMyBindingId = mBgDataModel.lastBindId; } for (Callbacks cb : mCallbacksList) { new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId, - workspaceItems, appWidgets, orderedScreenIds).bind(); + workspaceItems, appWidgets, extraItems, orderedScreenIds).bind(); } } @@ -135,7 +138,7 @@ public abstract class BaseLoaderResults { private final ArrayList mWorkspaceItems; private final ArrayList mAppWidgets; private final IntArray mOrderedScreenIds; - + private final ArrayList mExtraItems; WorkspaceBinder(Callbacks callbacks, Executor uiExecutor, @@ -144,6 +147,7 @@ public abstract class BaseLoaderResults { int myBindingId, ArrayList workspaceItems, ArrayList appWidgets, + ArrayList extraItems, IntArray orderedScreenIds) { mCallbacks = callbacks; mUiExecutor = uiExecutor; @@ -152,6 +156,7 @@ public abstract class BaseLoaderResults { mMyBindingId = myBindingId; mWorkspaceItems = workspaceItems; mAppWidgets = appWidgets; + mExtraItems = extraItems; mOrderedScreenIds = orderedScreenIds; } @@ -198,6 +203,8 @@ public abstract class BaseLoaderResults { // Load items on the current page. bindWorkspaceItems(currentWorkspaceItems, mainExecutor); bindAppWidgets(currentAppWidgets, mainExecutor); + mExtraItems.forEach(item -> + executeCallbacksTask(c -> c.bindExtraContainerItems(item), mainExecutor)); // Locate available spots for prediction using currentWorkspaceItems IntArray gaps = getMissingHotseatRanks(currentWorkspaceItems, idp.numHotseatIcons); @@ -207,6 +214,7 @@ public abstract class BaseLoaderResults { // 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; diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index fd8520dcdf..140342f98c 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -424,6 +424,14 @@ public class BgDataModel { public FixedContainerItems clone() { return new FixedContainerItems(containerId, new ArrayList<>(items)); } + + public void setItems(List newItems) { + items.clear(); + newItems.forEach(item -> { + item.container = containerId; + items.add(item); + }); + } } diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java index 165d1eab6b..a27ac2391d 100644 --- a/src/com/android/launcher3/model/LoaderCursor.java +++ b/src/com/android/launcher3/model/LoaderCursor.java @@ -65,7 +65,7 @@ public class LoaderCursor extends CursorWrapper { private static final String TAG = "LoaderCursor"; - public final LongSparseArray allUsers; + private final LongSparseArray allUsers; private final Uri mContentUri; private final Context mContext; diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index e89031e61d..1dd8c112c6 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -360,15 +360,12 @@ public class LoaderTask implements Runnable { final int optionsIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.OPTIONS); - final LongSparseArray allUsers = c.allUsers; final LongSparseArray unlockedUsers = new LongSparseArray<>(); mUserManagerState.init(mUserCache, mUserManager); for (UserHandle user : mUserCache.getUserProfiles()) { long serialNo = mUserCache.getSerialNumberForUser(user); - allUsers.put(serialNo, user); - boolean userUnlocked = mUserManager.isUserUnlocked(user); // We can only query for shortcuts when the user is unlocked. @@ -419,16 +416,6 @@ public class LoaderTask implements Runnable { ComponentName cn = intent.getComponent(); targetPkg = cn == null ? intent.getPackage() : cn.getPackageName(); - if (allUsers.indexOfValue(c.user) < 0) { - if (c.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { - c.markDeleted("Legacy shortcuts are only allowed for current users"); - continue; - } else if (c.restoreFlag != 0) { - // Don't restore items for other profiles. - c.markDeleted("Restore from other profiles not supported"); - continue; - } - } if (TextUtils.isEmpty(targetPkg) && c.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { c.markDeleted("Only legacy shortcuts can have null package"); @@ -773,7 +760,7 @@ public class LoaderTask implements Runnable { } // Load delegate items - mModelDelegate.loadItems(); + mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts); // Break early if we've stopped loading if (mStopped) { diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java index ce4eed5bbd..53e8a86078 100644 --- a/src/com/android/launcher3/model/ModelDelegate.java +++ b/src/com/android/launcher3/model/ModelDelegate.java @@ -18,13 +18,17 @@ package com.android.launcher3.model; import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission; import android.content.Context; +import android.content.pm.ShortcutInfo; import androidx.annotation.WorkerThread; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; +import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.ResourceBasedOverride; +import java.util.Map; + /** * Class to extend LauncherModel functionality to provide extra data */ @@ -65,11 +69,12 @@ public class ModelDelegate implements ResourceBasedOverride { * Load delegate items if any in the data model */ @WorkerThread - public void loadItems() { } + public void loadItems(UserManagerState ums, Map pinnedShortcuts) { } /** * Called when the delegate is no loner needed */ @WorkerThread public void destroy() { } + } diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index 59233cd4d8..e03fd72449 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -257,12 +257,6 @@ public class ItemInfo { return container == CONTAINER_HOTSEAT_PREDICTION || container == CONTAINER_PREDICTION; } - /** - * Can be overridden by inherited classes to fill in {@link LauncherAtom.ItemInfo} - */ - public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) { - } - /** * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info. */ diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java index b0d19a609a..c04b7f0ffa 100644 --- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java @@ -26,7 +26,6 @@ import androidx.annotation.Nullable; import com.android.launcher3.AppWidgetResizeFrame; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings; -import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.util.ContentWriter; /** @@ -195,13 +194,4 @@ public class LauncherAppWidgetInfo extends ItemInfo { public final boolean hasOptionFlag(int option) { return (options & option) != 0; } - - @Override - public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) { - builder.setWidget(LauncherAtom.Widget.newBuilder() - .setSpanX(spanX) - .setSpanY(spanY) - .setComponentName(providerName.toString()) - .setPackageName(providerName.getPackageName())); - } } diff --git a/src/com/android/launcher3/util/IOUtils.java b/src/com/android/launcher3/util/IOUtils.java index fcb96d7db5..1cec0ecc10 100644 --- a/src/com/android/launcher3/util/IOUtils.java +++ b/src/com/android/launcher3/util/IOUtils.java @@ -16,20 +16,19 @@ package com.android.launcher3.util; -import android.content.Context; +import android.os.FileUtils; import android.util.Log; +import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.UUID; /** * Supports various IO utility functions @@ -52,6 +51,9 @@ public class IOUtils { } public static long copy(InputStream from, OutputStream to) throws IOException { + if (Utilities.ATLEAST_Q) { + return FileUtils.copy(from, to); + } byte[] buf = new byte[BUF_SIZE]; long total = 0; int r; @@ -62,25 +64,6 @@ public class IOUtils { return total; } - /** - * Utility method to debug binary data - */ - public static String createTempFile(Context context, byte[] data) { - if (!FeatureFlags.IS_STUDIO_BUILD) { - throw new IllegalStateException("Method only allowed in development mode"); - } - - String name = UUID.randomUUID().toString(); - File file = new File(context.getCacheDir(), name); - try (FileOutputStream fo = new FileOutputStream(file)) { - fo.write(data); - fo.flush(); - } catch (Exception e) { - throw new RuntimeException(e); - } - return file.getAbsolutePath(); - } - public static void closeSilently(Closeable c) { if (c != null) { try { diff --git a/src/com/android/launcher3/util/PersistedItemArray.java b/src/com/android/launcher3/util/PersistedItemArray.java new file mode 100644 index 0000000000..ae20638e31 --- /dev/null +++ b/src/com/android/launcher3/util/PersistedItemArray.java @@ -0,0 +1,180 @@ +/* + * 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.util; + +import android.content.Context; +import android.content.Intent; +import android.os.UserHandle; +import android.util.AtomicFile; +import android.util.Log; +import android.util.Xml; + +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import com.android.launcher3.AutoInstallsLayout; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.pm.UserCache; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.LongFunction; + +/** + * Utility class to read/write a list of {@link com.android.launcher3.model.data.ItemInfo} on disk. + * This class is not thread safe, the caller should ensure proper threading + */ +public class PersistedItemArray { + + private static final String TAG = "PersistedItemArray"; + + private static final String TAG_ROOT = "items"; + private static final String TAG_ENTRY = "entry"; + + private final String mFileName; + + public PersistedItemArray(String fileName) { + mFileName = fileName + ".xml"; + } + + /** + * Writes the provided list of items on the disk + */ + @WorkerThread + public void write(Context context, List items) { + AtomicFile file = new AtomicFile(context.getFileStreamPath(mFileName)); + + FileOutputStream fos; + try { + fos = file.startWrite(); + } catch (IOException e) { + Log.e(TAG, "Unable to persist items in " + mFileName, e); + return; + } + + UserCache userCache = UserCache.INSTANCE.get(context); + + try { + XmlSerializer out = Xml.newSerializer(); + out.setOutput(fos, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, TAG_ROOT); + for (T item : items) { + Intent intent = item.getIntent(); + if (intent == null) { + continue; + } + + out.startTag(null, TAG_ENTRY); + out.attribute(null, Favorites.ITEM_TYPE, Integer.toString(item.itemType)); + out.attribute(null, Favorites.PROFILE_ID, + Long.toString(userCache.getSerialNumberForUser(item.user))); + out.attribute(null, Favorites.INTENT, intent.toUri(0)); + out.endTag(null, TAG_ENTRY); + } + out.endTag(null, TAG_ROOT); + out.endDocument(); + } catch (IOException e) { + file.failWrite(fos); + Log.e(TAG, "Unable to persist items in " + mFileName, e); + return; + } + + file.finishWrite(fos); + } + + /** + * Reads the items from the disk + */ + @WorkerThread + public List read(Context context, ItemFactory factory) { + return read(context, factory, UserCache.INSTANCE.get(context)::getUserForSerialNumber); + } + + /** + * Reads the items from the disk + * @param userFn method to provide user handle for a given user serial + */ + @WorkerThread + public List read(Context context, ItemFactory factory, LongFunction userFn) { + List result = new ArrayList<>(); + AtomicFile file = new AtomicFile(context.getFileStreamPath(mFileName)); + + try (FileInputStream fis = file.openRead()) { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(new InputStreamReader(fis, StandardCharsets.UTF_8)); + + AutoInstallsLayout.beginDocument(parser, TAG_ROOT); + final int depth = parser.getDepth(); + + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG + || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG || !TAG_ENTRY.equals(parser.getName())) { + continue; + } + try { + int itemType = Integer.parseInt( + parser.getAttributeValue(null, Favorites.ITEM_TYPE)); + UserHandle user = userFn.apply(Long.parseLong( + parser.getAttributeValue(null, Favorites.PROFILE_ID))); + Intent intent = Intent.parseUri( + parser.getAttributeValue(null, Favorites.INTENT), 0); + + if (user != null && intent != null) { + T item = factory.createInfo(itemType, user, intent); + if (item != null) { + result.add(item); + } + } + } catch (Exception e) { + // Ignore this entry + } + } + } catch (FileNotFoundException e) { + // Ignore + } catch (IOException | XmlPullParserException e) { + Log.e(TAG, "Unable to read items in " + mFileName, e); + return Collections.emptyList(); + } + return result; + } + + /** + * Interface to create an ItemInfo during parsing + */ + public interface ItemFactory { + + /** + * Returns an item info or null in which case the entry is ignored + */ + @Nullable + T createInfo(int itemType, UserHandle user, Intent intent); + } +}