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 package com.android.launcher3.appprediction;
17 
18 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
19 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
20 
21 import android.annotation.TargetApi;
22 import android.app.prediction.AppPredictionContext;
23 import android.app.prediction.AppPredictionManager;
24 import android.app.prediction.AppPredictor;
25 import android.app.prediction.AppTarget;
26 import android.app.prediction.AppTargetEvent;
27 import android.app.prediction.AppTargetId;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.Message;
34 import android.os.UserHandle;
35 import android.util.Log;
36 
37 import androidx.annotation.Nullable;
38 import androidx.annotation.UiThread;
39 import androidx.annotation.WorkerThread;
40 
41 import com.android.launcher3.InvariantDeviceProfile;
42 import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
43 import com.android.launcher3.model.AppLaunchTracker;
44 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
45 import com.android.systemui.plugins.AppLaunchEventsPlugin;
46 import com.android.systemui.plugins.PluginListener;
47 
48 import java.util.ArrayList;
49 import java.util.List;
50 
51 /**
52  * Subclass of app tracker which publishes the data to the prediction engine and gets back results.
53  */
54 @TargetApi(Build.VERSION_CODES.Q)
55 public class PredictionAppTracker extends AppLaunchTracker
56         implements PluginListener<AppLaunchEventsPlugin> {
57 
58     private static final String TAG = "PredictionAppTracker";
59     private static final boolean DBG = false;
60 
61     private static final int MSG_INIT = 0;
62     private static final int MSG_DESTROY = 1;
63     private static final int MSG_LAUNCH = 2;
64     private static final int MSG_PREDICT = 3;
65 
66     protected final Context mContext;
67     private final Handler mMessageHandler;
68     private final List<AppLaunchEventsPlugin> mAppLaunchEventsPluginsList;
69 
70     // Accessed only on worker thread
71     private AppPredictor mHomeAppPredictor;
72     private AppPredictor mRecentsOverviewPredictor;
73 
PredictionAppTracker(Context context)74     public PredictionAppTracker(Context context) {
75         mContext = context;
76         mMessageHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessage);
77         InvariantDeviceProfile.INSTANCE.get(mContext).addOnChangeListener(this::onIdpChanged);
78 
79         mMessageHandler.sendEmptyMessage(MSG_INIT);
80 
81         mAppLaunchEventsPluginsList = new ArrayList<>();
82         PluginManagerWrapper.INSTANCE.get(context)
83                 .addPluginListener(this, AppLaunchEventsPlugin.class, true);
84     }
85 
86     @UiThread
onIdpChanged(int changeFlags, InvariantDeviceProfile profile)87     private void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
88         if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
89             // Reinitialize everything
90             mMessageHandler.sendEmptyMessage(MSG_INIT);
91         }
92     }
93 
94     @WorkerThread
destroy()95     private void destroy() {
96         if (mHomeAppPredictor != null) {
97             mHomeAppPredictor.destroy();
98             mHomeAppPredictor = null;
99         }
100         if (mRecentsOverviewPredictor != null) {
101             mRecentsOverviewPredictor.destroy();
102             mRecentsOverviewPredictor = null;
103         }
104     }
105 
106     @WorkerThread
createPredictor(Client client, int count)107     private AppPredictor createPredictor(Client client, int count) {
108         AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
109 
110         if (apm == null) {
111             return null;
112         }
113 
114         AppPredictor predictor = apm.createAppPredictionSession(
115                 new AppPredictionContext.Builder(mContext)
116                         .setUiSurface(client.id)
117                         .setPredictedTargetCount(count)
118                         .setExtras(getAppPredictionContextExtras(client))
119                         .build());
120         predictor.registerPredictionUpdates(mContext.getMainExecutor(),
121                 PredictionUiStateManager.INSTANCE.get(mContext).appPredictorCallback(client));
122         predictor.requestPredictionUpdate();
123         return predictor;
124     }
125 
126     /**
127      * Override to add custom extras.
128      */
129     @WorkerThread
130     @Nullable
getAppPredictionContextExtras(Client client)131     public Bundle getAppPredictionContextExtras(Client client) {
132         return null;
133     }
134 
135     @WorkerThread
handleMessage(Message msg)136     private boolean handleMessage(Message msg) {
137         switch (msg.what) {
138             case MSG_INIT: {
139                 // Destroy any existing clients
140                 destroy();
141 
142                 // Initialize the clients
143                 int count = InvariantDeviceProfile.INSTANCE.get(mContext).numAllAppsColumns;
144                 mHomeAppPredictor = createPredictor(Client.HOME, count);
145                 mRecentsOverviewPredictor = createPredictor(Client.OVERVIEW, count);
146                 return true;
147             }
148             case MSG_DESTROY: {
149                 destroy();
150                 return true;
151             }
152             case MSG_LAUNCH: {
153                 if (mHomeAppPredictor != null) {
154                     mHomeAppPredictor.notifyAppTargetEvent((AppTargetEvent) msg.obj);
155                 }
156                 return true;
157             }
158             case MSG_PREDICT: {
159                 if (mHomeAppPredictor != null) {
160                     String client = (String) msg.obj;
161                     if (Client.HOME.id.equals(client)) {
162                         mHomeAppPredictor.requestPredictionUpdate();
163                     } else {
164                         mRecentsOverviewPredictor.requestPredictionUpdate();
165                     }
166                 }
167                 return true;
168             }
169         }
170         return false;
171     }
172 
173     @Override
174     @UiThread
onReturnedToHome()175     public void onReturnedToHome() {
176         String client = Client.HOME.id;
177         mMessageHandler.removeMessages(MSG_PREDICT, client);
178         Message.obtain(mMessageHandler, MSG_PREDICT, client).sendToTarget();
179         if (DBG) {
180             Log.d(TAG, String.format("Sent immediate message to update %s", client));
181         }
182 
183         // Relay onReturnedToHome to every plugin.
184         mAppLaunchEventsPluginsList.forEach(AppLaunchEventsPlugin::onReturnedToHome);
185     }
186 
187     @Override
188     @UiThread
onStartShortcut(String packageName, String shortcutId, UserHandle user, String container)189     public void onStartShortcut(String packageName, String shortcutId, UserHandle user,
190                                 String container) {
191         // TODO: Use the full shortcut info
192         AppTarget target = new AppTarget.Builder(
193                 new AppTargetId("shortcut:" + shortcutId), packageName, user)
194                 .setClassName(shortcutId)
195                 .build();
196 
197         sendLaunch(target, container);
198 
199         // Relay onStartShortcut info to every connected plugin.
200         mAppLaunchEventsPluginsList
201                 .forEach(plugin -> plugin.onStartShortcut(
202                         packageName,
203                         shortcutId,
204                         user,
205                         container != null ? container : CONTAINER_DEFAULT)
206         );
207 
208     }
209 
210     @Override
211     @UiThread
onStartApp(ComponentName cn, UserHandle user, String container)212     public void onStartApp(ComponentName cn, UserHandle user, String container) {
213         if (cn != null) {
214             AppTarget target = new AppTarget.Builder(
215                     new AppTargetId("app:" + cn), cn.getPackageName(), user)
216                     .setClassName(cn.getClassName())
217                     .build();
218             sendLaunch(target, container);
219 
220             // Relay onStartApp to every connected plugin.
221             mAppLaunchEventsPluginsList
222                     .forEach(plugin -> plugin.onStartApp(
223                             cn,
224                             user,
225                             container != null ? container : CONTAINER_DEFAULT)
226             );
227         }
228     }
229 
230     @Override
231     @UiThread
onDismissApp(ComponentName cn, UserHandle user, String container)232     public void onDismissApp(ComponentName cn, UserHandle user, String container) {
233         if (cn == null) return;
234         AppTarget target = new AppTarget.Builder(
235                 new AppTargetId("app: " + cn), cn.getPackageName(), user)
236                 .setClassName(cn.getClassName())
237                 .build();
238         sendDismiss(target, container);
239 
240         // Relay onDismissApp to every connected plugin.
241         mAppLaunchEventsPluginsList
242                 .forEach(plugin -> plugin.onDismissApp(
243                         cn,
244                         user,
245                         container != null ? container : CONTAINER_DEFAULT)
246         );
247     }
248 
249     @UiThread
sendEvent(AppTarget target, String container, int eventId)250     private void sendEvent(AppTarget target, String container, int eventId) {
251         AppTargetEvent event = new AppTargetEvent.Builder(target, eventId)
252                 .setLaunchLocation(container == null ? CONTAINER_DEFAULT : container)
253                 .build();
254         Message.obtain(mMessageHandler, MSG_LAUNCH, event).sendToTarget();
255     }
256 
257     @UiThread
sendLaunch(AppTarget target, String container)258     private void sendLaunch(AppTarget target, String container) {
259         sendEvent(target, container, AppTargetEvent.ACTION_LAUNCH);
260     }
261 
262     @UiThread
sendDismiss(AppTarget target, String container)263     private void sendDismiss(AppTarget target, String container) {
264         sendEvent(target, container, AppTargetEvent.ACTION_DISMISS);
265     }
266 
267     @Override
onPluginConnected(AppLaunchEventsPlugin appLaunchEventsPlugin, Context context)268     public void onPluginConnected(AppLaunchEventsPlugin appLaunchEventsPlugin, Context context) {
269         mAppLaunchEventsPluginsList.add(appLaunchEventsPlugin);
270     }
271 
272     @Override
onPluginDisconnected(AppLaunchEventsPlugin appLaunchEventsPlugin)273     public void onPluginDisconnected(AppLaunchEventsPlugin appLaunchEventsPlugin) {
274         mAppLaunchEventsPluginsList.remove(appLaunchEventsPlugin);
275     }
276 }
277