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