1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.appprediction; 18 19 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 20 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; 21 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; 22 import static com.android.launcher3.LauncherState.BACKGROUND_APP; 23 import static com.android.launcher3.LauncherState.OVERVIEW; 24 25 import android.app.prediction.AppPredictor; 26 import android.app.prediction.AppTarget; 27 import android.content.ComponentName; 28 import android.content.Context; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 33 import com.android.launcher3.InvariantDeviceProfile; 34 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener; 35 import com.android.launcher3.Launcher; 36 import com.android.launcher3.LauncherAppState; 37 import com.android.launcher3.LauncherSettings; 38 import com.android.launcher3.LauncherState; 39 import com.android.launcher3.Utilities; 40 import com.android.launcher3.allapps.AllAppsContainerView; 41 import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener; 42 import com.android.launcher3.hybridhotseat.HotseatPredictionController; 43 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; 44 import com.android.launcher3.model.data.ItemInfo; 45 import com.android.launcher3.model.data.ItemInfoWithIcon; 46 import com.android.launcher3.shortcuts.ShortcutKey; 47 import com.android.launcher3.statemanager.StateManager.StateListener; 48 import com.android.launcher3.userevent.nano.LauncherLogProto; 49 import com.android.launcher3.util.ComponentKey; 50 import com.android.launcher3.util.MainThreadInitializedObject; 51 52 import java.util.ArrayList; 53 import java.util.Collections; 54 import java.util.List; 55 import java.util.OptionalInt; 56 import java.util.stream.IntStream; 57 58 /** 59 * Handler responsible to updating the UI due to predicted apps changes. Operations: 60 * 1) Pushes the predicted apps to all-apps. If all-apps is visible, waits until it becomes 61 * invisible again before applying the changes. This ensures that the UI does not change abruptly 62 * in front of the user, even if an app launched and user pressed back button to return to the 63 * all-apps UI again. 64 * 2) Prefetch high-res icons for predicted apps. This ensures that we have the icons in memory 65 * even if all-apps is not opened as they are shown in search UI as well 66 * 3) Load instant app if it is not already in memory. As predictions are persisted on disk, 67 * instant app will not be in memory when launcher starts. 68 * 4) Maintains the current active client id (for the predictions) and all updates are performed on 69 * that client id. 70 */ 71 public class PredictionUiStateManager implements StateListener<LauncherState>, 72 ItemInfoUpdateReceiver, OnIDPChangeListener, OnUpdateListener { 73 74 public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state"; 75 76 // TODO (b/129421797): Update the client constants 77 public enum Client { 78 HOME("home"), 79 OVERVIEW("overview"); 80 81 public final String id; 82 Client(String id)83 Client(String id) { 84 this.id = id; 85 } 86 } 87 88 public static final MainThreadInitializedObject<PredictionUiStateManager> INSTANCE = 89 new MainThreadInitializedObject<>(PredictionUiStateManager::new); 90 91 private final Context mContext; 92 93 private final DynamicItemCache mDynamicItemCache; 94 private final List[] mPredictionServicePredictions; 95 96 private int mMaxIconsPerRow; 97 private Client mActiveClient; 98 99 private AllAppsContainerView mAppsView; 100 101 private PredictionState mPendingState; 102 private PredictionState mCurrentState; 103 104 private boolean mGettingValidPredictionResults; 105 PredictionUiStateManager(Context context)106 private PredictionUiStateManager(Context context) { 107 mContext = context; 108 109 mDynamicItemCache = new DynamicItemCache(context, this::onAppsUpdated); 110 111 mActiveClient = Client.HOME; 112 113 InvariantDeviceProfile idp = LauncherAppState.getIDP(context); 114 mMaxIconsPerRow = idp.numColumns; 115 116 idp.addOnChangeListener(this); 117 mPredictionServicePredictions = new List[Client.values().length]; 118 for (int i = 0; i < mPredictionServicePredictions.length; i++) { 119 mPredictionServicePredictions[i] = Collections.emptyList(); 120 } 121 mGettingValidPredictionResults = Utilities.getDevicePrefs(context) 122 .getBoolean(LAST_PREDICTION_ENABLED_STATE, true); 123 124 // Call this last 125 mCurrentState = parseLastState(); 126 } 127 128 @Override onIdpChanged(int changeFlags, InvariantDeviceProfile profile)129 public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) { 130 mMaxIconsPerRow = profile.numColumns; 131 } 132 getClient()133 public Client getClient() { 134 return mActiveClient; 135 } 136 switchClient(Client client)137 public void switchClient(Client client) { 138 if (client == mActiveClient) { 139 return; 140 } 141 mActiveClient = client; 142 dispatchOnChange(true); 143 } 144 setTargetAppsView(AllAppsContainerView appsView)145 public void setTargetAppsView(AllAppsContainerView appsView) { 146 if (mAppsView != null) { 147 mAppsView.getAppsStore().removeUpdateListener(this); 148 } 149 mAppsView = appsView; 150 if (mAppsView != null) { 151 mAppsView.getAppsStore().addUpdateListener(this); 152 } 153 if (mPendingState != null) { 154 applyState(mPendingState); 155 mPendingState = null; 156 } else { 157 applyState(mCurrentState); 158 } 159 updateDependencies(mCurrentState); 160 } 161 162 @Override reapplyItemInfo(ItemInfoWithIcon info)163 public void reapplyItemInfo(ItemInfoWithIcon info) { } 164 165 @Override onStateTransitionComplete(LauncherState state)166 public void onStateTransitionComplete(LauncherState state) { 167 if (mAppsView == null) { 168 return; 169 } 170 if (mPendingState != null && canApplyPredictions(mPendingState)) { 171 applyState(mPendingState); 172 mPendingState = null; 173 } 174 if (mPendingState == null) { 175 Launcher.getLauncher(mAppsView.getContext()).getStateManager() 176 .removeStateListener(this); 177 } 178 } 179 scheduleApplyPredictedApps(PredictionState state)180 private void scheduleApplyPredictedApps(PredictionState state) { 181 boolean registerListener = mPendingState == null; 182 mPendingState = state; 183 if (registerListener) { 184 // Add a listener and wait until appsView is invisible again. 185 Launcher.getLauncher(mAppsView.getContext()).getStateManager().addStateListener(this); 186 } 187 } 188 applyState(PredictionState state)189 private void applyState(PredictionState state) { 190 mCurrentState = state; 191 if (mAppsView != null) { 192 mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class) 193 .setPredictedApps(mCurrentState.apps); 194 } 195 } 196 updatePredictionStateAfterCallback()197 private void updatePredictionStateAfterCallback() { 198 boolean validResults = false; 199 for (List l : mPredictionServicePredictions) { 200 validResults |= l != null && !l.isEmpty(); 201 } 202 if (validResults != mGettingValidPredictionResults) { 203 mGettingValidPredictionResults = validResults; 204 Utilities.getDevicePrefs(mContext).edit() 205 .putBoolean(LAST_PREDICTION_ENABLED_STATE, true) 206 .apply(); 207 } 208 dispatchOnChange(true); 209 } 210 appPredictorCallback(Client client)211 public AppPredictor.Callback appPredictorCallback(Client client) { 212 return targets -> { 213 mPredictionServicePredictions[client.ordinal()] = targets; 214 updatePredictionStateAfterCallback(); 215 }; 216 } 217 dispatchOnChange(boolean changed)218 private void dispatchOnChange(boolean changed) { 219 PredictionState newState = changed 220 ? parseLastState() 221 : mPendingState != null && canApplyPredictions(mPendingState) 222 ? mPendingState 223 : mCurrentState; 224 if (changed && mAppsView != null && !canApplyPredictions(newState)) { 225 scheduleApplyPredictedApps(newState); 226 } else { 227 applyState(newState); 228 } 229 } 230 parseLastState()231 private PredictionState parseLastState() { 232 PredictionState state = new PredictionState(); 233 state.isEnabled = mGettingValidPredictionResults; 234 if (!state.isEnabled) { 235 state.apps = Collections.EMPTY_LIST; 236 return state; 237 } 238 239 state.apps = new ArrayList<>(); 240 241 List<AppTarget> appTargets = mPredictionServicePredictions[mActiveClient.ordinal()]; 242 if (!appTargets.isEmpty()) { 243 for (AppTarget appTarget : appTargets) { 244 ComponentKey key; 245 if (appTarget.getShortcutInfo() != null) { 246 key = ShortcutKey.fromInfo(appTarget.getShortcutInfo()); 247 } else { 248 key = new ComponentKey(new ComponentName(appTarget.getPackageName(), 249 appTarget.getClassName()), appTarget.getUser()); 250 } 251 state.apps.add(new ComponentKeyMapper(key, mDynamicItemCache)); 252 } 253 } 254 updateDependencies(state); 255 return state; 256 } 257 updateDependencies(PredictionState state)258 private void updateDependencies(PredictionState state) { 259 if (!state.isEnabled || mAppsView == null) { 260 return; 261 } 262 mDynamicItemCache.updateDependencies(state.apps, mAppsView.getAppsStore(), this, 263 mMaxIconsPerRow); 264 } 265 266 @Override onAppsUpdated()267 public void onAppsUpdated() { 268 dispatchOnChange(false); 269 } 270 canApplyPredictions(PredictionState newState)271 private boolean canApplyPredictions(PredictionState newState) { 272 if (mAppsView == null) { 273 // If there is no apps view, no need to schedule. 274 return true; 275 } 276 Launcher launcher = Launcher.getLauncher(mAppsView.getContext()); 277 PredictionRowView predictionRow = mAppsView.getFloatingHeaderView(). 278 findFixedRowByType(PredictionRowView.class); 279 if (!predictionRow.isShown() || predictionRow.getAlpha() == 0 || 280 launcher.isForceInvisible()) { 281 return true; 282 } 283 284 if (mCurrentState.isEnabled != newState.isEnabled 285 || mCurrentState.apps.isEmpty() != newState.apps.isEmpty()) { 286 // If the visibility of the prediction row is changing, apply immediately. 287 return true; 288 } 289 290 if (launcher.getDeviceProfile().isVerticalBarLayout()) { 291 // If we are here & mAppsView.isShown() = true, we are probably in all-apps or mid way 292 return false; 293 } 294 if (!launcher.isInState(OVERVIEW) && !launcher.isInState(BACKGROUND_APP)) { 295 // Just a fallback as we dont need to apply instantly, if we are not in the swipe-up UI 296 return false; 297 } 298 299 // Instead of checking against 1, we should check against (1 + delta), where delta accounts 300 // for the nav-bar height (as app icon can still be visible under the nav-bar). Checking 301 // against 1, keeps the logic simple :) 302 return launcher.getAllAppsController().getProgress() > 1; 303 } 304 getCurrentState()305 public PredictionState getCurrentState() { 306 return mCurrentState; 307 } 308 309 /** 310 * Returns ranking info for the app within all apps prediction. 311 * Only applicable when {@link ItemInfo#itemType} is one of the followings: 312 * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION}, 313 * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT}, 314 * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT} 315 */ getAllAppsRank(@ullable ItemInfo itemInfo)316 public OptionalInt getAllAppsRank(@Nullable ItemInfo itemInfo) { 317 if (itemInfo == null || itemInfo.getTargetComponent() == null || itemInfo.user == null) { 318 return OptionalInt.empty(); 319 } 320 321 if (itemInfo.itemType == ITEM_TYPE_APPLICATION 322 || itemInfo.itemType == ITEM_TYPE_SHORTCUT 323 || itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) { 324 ComponentKey key = new ComponentKey(itemInfo.getTargetComponent(), 325 itemInfo.user); 326 final List<ComponentKeyMapper> apps = getCurrentState().apps; 327 return IntStream.range(0, apps.size()) 328 .filter(index -> key.equals(apps.get(index).getComponentKey())) 329 .findFirst(); 330 } 331 332 return OptionalInt.empty(); 333 } 334 335 /** 336 * Fill in predicted_rank field based on app prediction. 337 * Only applicable when {@link ItemInfo#itemType} is one of the followings: 338 * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION}, 339 * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT}, 340 * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT} 341 */ fillInPredictedRank( @onNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target)342 public static void fillInPredictedRank( 343 @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) { 344 345 final PredictionUiStateManager manager = PredictionUiStateManager.INSTANCE.getNoCreate(); 346 if (manager == null || itemInfo.getTargetComponent() == null || itemInfo.user == null 347 || (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION 348 && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT 349 && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)) { 350 return; 351 } 352 if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) { 353 HotseatPredictionController.encodeHotseatLayoutIntoPredictionRank(itemInfo, target); 354 return; 355 } 356 357 final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user); 358 final List<ComponentKeyMapper> predictedApps = manager.getCurrentState().apps; 359 IntStream.range(0, predictedApps.size()) 360 .filter((i) -> k.equals(predictedApps.get(i).getComponentKey())) 361 .findFirst() 362 .ifPresent((rank) -> target.predictedRank = 0 - rank); 363 } 364 365 public static class PredictionState { 366 367 public boolean isEnabled; 368 public List<ComponentKeyMapper> apps; 369 } 370 } 371