1 /* 2 * Copyright (C) 2018 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 android.service.appprediction; 17 18 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 19 20 import android.annotation.CallSuper; 21 import android.annotation.MainThread; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.SystemApi; 25 import android.app.Service; 26 import android.app.prediction.AppPredictionContext; 27 import android.app.prediction.AppPredictionSessionId; 28 import android.app.prediction.AppTarget; 29 import android.app.prediction.AppTargetEvent; 30 import android.app.prediction.AppTargetId; 31 import android.app.prediction.IPredictionCallback; 32 import android.content.Intent; 33 import android.content.pm.ParceledListSlice; 34 import android.os.CancellationSignal; 35 import android.os.Handler; 36 import android.os.IBinder; 37 import android.os.Looper; 38 import android.os.RemoteException; 39 import android.service.appprediction.IPredictionService.Stub; 40 import android.util.ArrayMap; 41 import android.util.Log; 42 import android.util.Slog; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.function.Consumer; 47 48 /** 49 * A service used to predict app and shortcut usage. 50 * 51 * @hide 52 */ 53 @SystemApi 54 public abstract class AppPredictionService extends Service { 55 56 private static final String TAG = "AppPredictionService"; 57 58 /** 59 * The {@link Intent} that must be declared as handled by the service. 60 * 61 * <p>The service must also require the {@link android.permission#MANAGE_APP_PREDICTIONS} 62 * permission. 63 * 64 * @hide 65 */ 66 public static final String SERVICE_INTERFACE = 67 "android.service.appprediction.AppPredictionService"; 68 69 private final ArrayMap<AppPredictionSessionId, ArrayList<CallbackWrapper>> mSessionCallbacks = 70 new ArrayMap<>(); 71 private Handler mHandler; 72 73 private final IPredictionService mInterface = new Stub() { 74 75 @Override 76 public void onCreatePredictionSession(AppPredictionContext context, 77 AppPredictionSessionId sessionId) { 78 mHandler.sendMessage( 79 obtainMessage(AppPredictionService::doCreatePredictionSession, 80 AppPredictionService.this, context, sessionId)); 81 } 82 83 @Override 84 public void notifyAppTargetEvent(AppPredictionSessionId sessionId, AppTargetEvent event) { 85 mHandler.sendMessage( 86 obtainMessage(AppPredictionService::onAppTargetEvent, 87 AppPredictionService.this, sessionId, event)); 88 } 89 90 @Override 91 public void notifyLaunchLocationShown(AppPredictionSessionId sessionId, 92 String launchLocation, ParceledListSlice targetIds) { 93 mHandler.sendMessage( 94 obtainMessage(AppPredictionService::onLaunchLocationShown, 95 AppPredictionService.this, sessionId, launchLocation, 96 targetIds.getList())); 97 } 98 99 @Override 100 public void sortAppTargets(AppPredictionSessionId sessionId, ParceledListSlice targets, 101 IPredictionCallback callback) { 102 mHandler.sendMessage( 103 obtainMessage(AppPredictionService::onSortAppTargets, 104 AppPredictionService.this, sessionId, targets.getList(), null, 105 new CallbackWrapper(callback, null))); 106 } 107 108 @Override 109 public void registerPredictionUpdates(AppPredictionSessionId sessionId, 110 IPredictionCallback callback) { 111 mHandler.sendMessage( 112 obtainMessage(AppPredictionService::doRegisterPredictionUpdates, 113 AppPredictionService.this, sessionId, callback)); 114 } 115 116 @Override 117 public void unregisterPredictionUpdates(AppPredictionSessionId sessionId, 118 IPredictionCallback callback) { 119 mHandler.sendMessage( 120 obtainMessage(AppPredictionService::doUnregisterPredictionUpdates, 121 AppPredictionService.this, sessionId, callback)); 122 } 123 124 @Override 125 public void requestPredictionUpdate(AppPredictionSessionId sessionId) { 126 mHandler.sendMessage( 127 obtainMessage(AppPredictionService::doRequestPredictionUpdate, 128 AppPredictionService.this, sessionId)); 129 } 130 131 @Override 132 public void onDestroyPredictionSession(AppPredictionSessionId sessionId) { 133 mHandler.sendMessage( 134 obtainMessage(AppPredictionService::doDestroyPredictionSession, 135 AppPredictionService.this, sessionId)); 136 } 137 }; 138 139 @CallSuper 140 @Override onCreate()141 public void onCreate() { 142 super.onCreate(); 143 mHandler = new Handler(Looper.getMainLooper(), null, true); 144 } 145 146 @Override 147 @NonNull onBind(@onNull Intent intent)148 public final IBinder onBind(@NonNull Intent intent) { 149 if (SERVICE_INTERFACE.equals(intent.getAction())) { 150 return mInterface.asBinder(); 151 } 152 Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent); 153 return null; 154 } 155 156 /** 157 * Called by a client app to indicate a target launch 158 */ 159 @MainThread onAppTargetEvent(@onNull AppPredictionSessionId sessionId, @NonNull AppTargetEvent event)160 public abstract void onAppTargetEvent(@NonNull AppPredictionSessionId sessionId, 161 @NonNull AppTargetEvent event); 162 163 /** 164 * Called by a client app to indication a particular location has been shown to the user. 165 */ 166 @MainThread onLaunchLocationShown(@onNull AppPredictionSessionId sessionId, @NonNull String launchLocation, @NonNull List<AppTargetId> targetIds)167 public abstract void onLaunchLocationShown(@NonNull AppPredictionSessionId sessionId, 168 @NonNull String launchLocation, @NonNull List<AppTargetId> targetIds); 169 doCreatePredictionSession(@onNull AppPredictionContext context, @NonNull AppPredictionSessionId sessionId)170 private void doCreatePredictionSession(@NonNull AppPredictionContext context, 171 @NonNull AppPredictionSessionId sessionId) { 172 mSessionCallbacks.put(sessionId, new ArrayList<>()); 173 onCreatePredictionSession(context, sessionId); 174 } 175 176 /** 177 * Creates a new interaction session. 178 * 179 * @param context interaction context 180 * @param sessionId the session's Id 181 */ onCreatePredictionSession(@onNull AppPredictionContext context, @NonNull AppPredictionSessionId sessionId)182 public void onCreatePredictionSession(@NonNull AppPredictionContext context, 183 @NonNull AppPredictionSessionId sessionId) {} 184 185 /** 186 * Called by the client app to request sorting of targets based on prediction rank. 187 */ 188 @MainThread onSortAppTargets(@onNull AppPredictionSessionId sessionId, @NonNull List<AppTarget> targets, @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<List<AppTarget>> callback)189 public abstract void onSortAppTargets(@NonNull AppPredictionSessionId sessionId, 190 @NonNull List<AppTarget> targets, @NonNull CancellationSignal cancellationSignal, 191 @NonNull Consumer<List<AppTarget>> callback); 192 doRegisterPredictionUpdates(@onNull AppPredictionSessionId sessionId, @NonNull IPredictionCallback callback)193 private void doRegisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, 194 @NonNull IPredictionCallback callback) { 195 final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); 196 if (callbacks == null) { 197 Slog.e(TAG, "Failed to register for updates for unknown session: " + sessionId); 198 return; 199 } 200 201 final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); 202 if (wrapper == null) { 203 callbacks.add(new CallbackWrapper(callback, 204 callbackWrapper -> 205 mHandler.post(() -> removeCallbackWrapper(callbacks, callbackWrapper)))); 206 if (callbacks.size() == 1) { 207 onStartPredictionUpdates(); 208 } 209 } 210 } 211 212 /** 213 * Called when any continuous prediction callback is registered. 214 */ 215 @MainThread onStartPredictionUpdates()216 public void onStartPredictionUpdates() {} 217 doUnregisterPredictionUpdates(@onNull AppPredictionSessionId sessionId, @NonNull IPredictionCallback callback)218 private void doUnregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, 219 @NonNull IPredictionCallback callback) { 220 final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); 221 if (callbacks == null) { 222 Slog.e(TAG, "Failed to unregister for updates for unknown session: " + sessionId); 223 return; 224 } 225 226 final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); 227 removeCallbackWrapper(callbacks, wrapper); 228 } 229 removeCallbackWrapper(@ullable ArrayList<CallbackWrapper> callbacks, @Nullable CallbackWrapper wrapper)230 private void removeCallbackWrapper(@Nullable ArrayList<CallbackWrapper> callbacks, 231 @Nullable CallbackWrapper wrapper) { 232 if (callbacks == null || wrapper == null) { 233 return; 234 } 235 callbacks.remove(wrapper); 236 wrapper.destroy(); 237 if (callbacks.isEmpty()) { 238 onStopPredictionUpdates(); 239 } 240 } 241 242 /** 243 * Called when there are no longer any continuous prediction callbacks registered. 244 */ 245 @MainThread onStopPredictionUpdates()246 public void onStopPredictionUpdates() {} 247 doRequestPredictionUpdate(@onNull AppPredictionSessionId sessionId)248 private void doRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) { 249 // Just an optimization, if there are no callbacks, then don't bother notifying the service 250 final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); 251 if (callbacks != null && !callbacks.isEmpty()) { 252 onRequestPredictionUpdate(sessionId); 253 } 254 } 255 256 /** 257 * Called by the client app to request target predictions. This method is only called if there 258 * are one or more prediction callbacks registered. 259 * 260 * @see #updatePredictions(AppPredictionSessionId, List) 261 */ 262 @MainThread onRequestPredictionUpdate(@onNull AppPredictionSessionId sessionId)263 public abstract void onRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId); 264 doDestroyPredictionSession(@onNull AppPredictionSessionId sessionId)265 private void doDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) { 266 final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.remove(sessionId); 267 if (callbacks != null) callbacks.forEach(CallbackWrapper::destroy); 268 onDestroyPredictionSession(sessionId); 269 } 270 271 /** 272 * Destroys the interaction session. 273 * 274 * @param sessionId the id of the session to destroy 275 */ 276 @MainThread onDestroyPredictionSession(@onNull AppPredictionSessionId sessionId)277 public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {} 278 279 /** 280 * Used by the prediction factory to send back results the client app. The can be called 281 * in response to {@link #onRequestPredictionUpdate(AppPredictionSessionId)} or proactively as 282 * a result of changes in predictions. 283 */ updatePredictions(@onNull AppPredictionSessionId sessionId, @NonNull List<AppTarget> targets)284 public final void updatePredictions(@NonNull AppPredictionSessionId sessionId, 285 @NonNull List<AppTarget> targets) { 286 List<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); 287 if (callbacks != null) { 288 for (CallbackWrapper callback : callbacks) { 289 callback.accept(targets); 290 } 291 } 292 } 293 294 /** 295 * Finds the callback wrapper for the given callback. 296 */ findCallbackWrapper(ArrayList<CallbackWrapper> callbacks, IPredictionCallback callback)297 private CallbackWrapper findCallbackWrapper(ArrayList<CallbackWrapper> callbacks, 298 IPredictionCallback callback) { 299 for (int i = callbacks.size() - 1; i >= 0; i--) { 300 if (callbacks.get(i).isCallback(callback)) { 301 return callbacks.get(i); 302 } 303 } 304 return null; 305 } 306 307 private static final class CallbackWrapper implements Consumer<List<AppTarget>>, 308 IBinder.DeathRecipient { 309 310 private IPredictionCallback mCallback; 311 private final Consumer<CallbackWrapper> mOnBinderDied; 312 CallbackWrapper(IPredictionCallback callback, @Nullable Consumer<CallbackWrapper> onBinderDied)313 CallbackWrapper(IPredictionCallback callback, 314 @Nullable Consumer<CallbackWrapper> onBinderDied) { 315 mCallback = callback; 316 mOnBinderDied = onBinderDied; 317 if (mOnBinderDied != null) { 318 try { 319 mCallback.asBinder().linkToDeath(this, 0); 320 } catch (RemoteException e) { 321 Slog.e(TAG, "Failed to link to death: " + e); 322 } 323 } 324 } 325 isCallback(@onNull IPredictionCallback callback)326 public boolean isCallback(@NonNull IPredictionCallback callback) { 327 if (mCallback == null) { 328 Slog.e(TAG, "Callback is null, likely the binder has died."); 329 return false; 330 } 331 return mCallback.asBinder().equals(callback.asBinder()); 332 } 333 destroy()334 public void destroy() { 335 if (mCallback != null && mOnBinderDied != null) { 336 mCallback.asBinder().unlinkToDeath(this, 0); 337 } 338 } 339 340 @Override accept(List<AppTarget> ts)341 public void accept(List<AppTarget> ts) { 342 try { 343 if (mCallback != null) { 344 mCallback.onResult(new ParceledListSlice(ts)); 345 } 346 } catch (RemoteException e) { 347 Slog.e(TAG, "Error sending result:" + e); 348 } 349 } 350 351 @Override binderDied()352 public void binderDied() { 353 destroy(); 354 mCallback = null; 355 if (mOnBinderDied != null) { 356 mOnBinderDied.accept(this); 357 } 358 } 359 } 360 } 361