/* * 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 android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.formatElapsedTime; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import static com.android.launcher3.Utilities.getDevicePrefs; import static com.android.launcher3.hybridhotseat.HotseatPredictionModel.convertDataModelToAppTargetBundle; import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo; import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static java.util.stream.Collectors.toCollection; import android.app.StatsManager; import android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionManager; 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.SharedPreferences; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; import android.os.Bundle; import android.os.UserHandle; import android.util.Log; import android.util.StatsEvent; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; 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.IntSparseArrayMap; import com.android.launcher3.util.PersistedItemArray; import com.android.quickstep.logging.SettingsChangeLogger; import com.android.quickstep.logging.StatsLogCompatManager; import com.android.systemui.shared.system.SysUiStatsLog; import java.util.ArrayList; 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 */ public class QuickstepModelDelegate extends ModelDelegate { public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state"; private static final String LAST_SNAPSHOT_TIME_MILLIS = "LAST_SNAPSHOT_TIME_MILLIS"; private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets"; private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20; private static final boolean IS_DEBUG = false; private static final String TAG = "QuickstepModelDelegate"; private final PredictorState mAllAppsState = new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions"); private final PredictorState mHotseatState = new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions"); private final PredictorState mWidgetsRecommendationState = new PredictorState(CONTAINER_WIDGETS_PREDICTION, "widgets_prediction"); private final InvariantDeviceProfile mIDP; private final AppEventProducer mAppEventProducer; private final StatsManager mStatsManager; private final Context mContext; protected boolean mActive = false; public QuickstepModelDelegate(Context context) { mContext = context; mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent); mIDP = InvariantDeviceProfile.INSTANCE.get(context); StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer); mStatsManager = context.getSystemService(StatsManager.class); } @Override @WorkerThread public void loadItems(UserManagerState ums, Map pinnedShortcuts) { // TODO: Implement caching and preloading super.loadItems(ums, pinnedShortcuts); WorkspaceItemFactory allAppsFactory = new WorkspaceItemFactory( mApp, ums, pinnedShortcuts, mIDP.numDatabaseAllAppsColumns); mAllAppsState.items.setItems( mAllAppsState.storage.read(mApp.getContext(), allAppsFactory, ums.allUsers::get)); mDataModel.extraItems.put(CONTAINER_PREDICTION, mAllAppsState.items); WorkspaceItemFactory hotseatFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, mIDP.numDatabaseHotseatIcons); mHotseatState.items.setItems( mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get)); mDataModel.extraItems.put(CONTAINER_HOTSEAT_PREDICTION, mHotseatState.items); // Widgets prediction isn't used frequently. And thus, it is not persisted on disk. mDataModel.extraItems.put(CONTAINER_WIDGETS_PREDICTION, mWidgetsRecommendationState.items); mActive = true; } @Override public void workspaceLoadComplete() { super.workspaceLoadComplete(); recreatePredictors(); } @Override @WorkerThread public void modelLoadComplete() { super.modelLoadComplete(); // Log snapshot of the model SharedPreferences prefs = getDevicePrefs(mApp.getContext()); long lastSnapshotTimeMillis = prefs.getLong(LAST_SNAPSHOT_TIME_MILLIS, 0); // Log snapshot only if previous snapshot was older than a day long now = System.currentTimeMillis(); if (now - lastSnapshotTimeMillis < DAY_IN_MILLIS) { if (IS_DEBUG) { String elapsedTime = formatElapsedTime((now - lastSnapshotTimeMillis) / 1000); Log.d(TAG, String.format( "Skipped snapshot logging since previous snapshot was %s old.", elapsedTime)); } } else { IntSparseArrayMap itemsIdMap; synchronized (mDataModel) { itemsIdMap = mDataModel.itemsIdMap.clone(); } InstanceId instanceId = new InstanceIdSequence().newInstanceId(); for (ItemInfo info : itemsIdMap) { FolderInfo parent = getContainer(info, itemsIdMap); StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId); } additionalSnapshotEvents(instanceId); prefs.edit().putLong(LAST_SNAPSHOT_TIME_MILLIS, now).apply(); } // Only register for launcher snapshot logging if this is the primary ModelDelegate // instance, as there will be additional instances that may be destroyed at any time. if (mIsPrimaryInstance) { registerSnapshotLoggingCallback(); } } protected void additionalSnapshotEvents(InstanceId snapshotInstanceId){} /** * Registers a callback to log launcher workspace layout using Statsd pulled atom. */ protected void registerSnapshotLoggingCallback() { if (mStatsManager == null) { Log.d(TAG, "Failed to get StatsManager"); } try { mStatsManager.setPullAtomCallback( SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT, null /* PullAtomMetadata */, MODEL_EXECUTOR, (i, eventList) -> { InstanceId instanceId = new InstanceIdSequence().newInstanceId(); IntSparseArrayMap itemsIdMap; synchronized (mDataModel) { itemsIdMap = mDataModel.itemsIdMap.clone(); } for (ItemInfo info : itemsIdMap) { FolderInfo parent = getContainer(info, itemsIdMap); LauncherAtom.ItemInfo itemInfo = info.buildProto(parent); Log.d(TAG, itemInfo.toString()); StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo, instanceId); eventList.add(statsEvent); } Log.d(TAG, String.format( "Successfully logged %d workspace items with instanceId=%d", itemsIdMap.size(), instanceId.getId())); additionalSnapshotEvents(instanceId); SettingsChangeLogger.INSTANCE.get(mContext).logSnapshot(instanceId); return StatsManager.PULL_SUCCESS; } ); Log.d(TAG, "Successfully registered for launcher snapshot logging!"); } catch (RuntimeException e) { Log.e(TAG, "Failed to register launcher snapshot logging callback with StatsManager", e); } } private static FolderInfo getContainer(ItemInfo info, IntSparseArrayMap itemsIdMap) { if (info.container > 0) { ItemInfo containerInfo = itemsIdMap.get(info.container); if (!(containerInfo instanceof FolderInfo)) { Log.e(TAG, String.format( "Item info: %s found with invalid container: %s", info, containerInfo)); } else { return (FolderInfo) containerInfo; } } return null; } @Override public void validateData() { super.validateData(); if (mAllAppsState.predictor != null) { mAllAppsState.predictor.requestPredictionUpdate(); } if (mWidgetsRecommendationState.predictor != null) { mWidgetsRecommendationState.predictor.requestPredictionUpdate(); } } @Override public void destroy() { super.destroy(); mActive = false; StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer); if (mIsPrimaryInstance) { mStatsManager.clearPullAtomCallback(SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT); } destroyPredictors(); } private void destroyPredictors() { mAllAppsState.destroyPredictor(); mHotseatState.destroyPredictor(); mWidgetsRecommendationState.destroyPredictor(); } @WorkerThread private void recreatePredictors() { destroyPredictors(); if (!mActive) { return; } Context context = mApp.getContext(); AppPredictionManager apm = context.getSystemService(AppPredictionManager.class); if (apm == null) { return; } registerPredictor(mAllAppsState, apm.createAppPredictionSession( new AppPredictionContext.Builder(context) .setUiSurface("home") .setPredictedTargetCount(mIDP.numDatabaseAllAppsColumns) .build())); // TODO: get bundle registerPredictor(mHotseatState, apm.createAppPredictionSession( new AppPredictionContext.Builder(context) .setUiSurface("hotseat") .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons) .setExtras(convertDataModelToAppTargetBundle(context, mDataModel)) .build())); registerWidgetsPredictor(apm.createAppPredictionSession( new AppPredictionContext.Builder(context) .setUiSurface("widgets") .setExtras(getBundleForWidgetsOnWorkspace(context, mDataModel)) .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION) .build())); } private void registerPredictor(PredictorState state, AppPredictor predictor) { state.predictor = predictor; state.predictor.registerPredictionUpdates( MODEL_EXECUTOR, t -> handleUpdate(state, t)); state.predictor.requestPredictionUpdate(); } private void handleUpdate(PredictorState state, List targets) { if (state.setTargets(targets)) { // No diff, skip return; } mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets)); } private void registerWidgetsPredictor(AppPredictor predictor) { mWidgetsRecommendationState.predictor = predictor; mWidgetsRecommendationState.predictor.registerPredictionUpdates( MODEL_EXECUTOR, targets -> { if (mWidgetsRecommendationState.setTargets(targets)) { // No diff, skip return; } mApp.getModel().enqueueModelUpdateTask( new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets)); }); mWidgetsRecommendationState.predictor.requestPredictionUpdate(); } private void onAppTargetEvent(AppTargetEvent event, int client) { PredictorState state; switch(client) { case CONTAINER_PREDICTION: state = mAllAppsState; break; case CONTAINER_WIDGETS_PREDICTION: state = mWidgetsRecommendationState; break; case CONTAINER_HOTSEAT_PREDICTION: default: state = mHotseatState; break; } if (state.predictor != null) { state.predictor.notifyAppTargetEvent(event); Log.d(TAG, "notifyAppTargetEvent action=" + event.getAction() + " launchLocation=" + event.getLaunchLocation()); } } private Bundle getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel) { Bundle bundle = new Bundle(); ArrayList widgetEvents = dataModel.getAllWorkspaceItems().stream() .filter(PredictionHelper::isTrackedForWidgetPrediction) .map(item -> { AppTarget target = getAppTargetFromItemInfo(context, item); if (target == null) return null; return wrapAppTargetWithItemLocation( target, AppTargetEvent.ACTION_PIN, item); }) .filter(Objects::nonNull) .collect(toCollection(ArrayList::new)); bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, widgetEvents); return bundle; } 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 the same as 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; } } }