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