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.app.prediction; 17 18 import android.annotation.CallbackExecutor; 19 import android.annotation.FlaggedApi; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SystemApi; 23 import android.annotation.TestApi; 24 import android.app.prediction.IPredictionCallback.Stub; 25 import android.content.Context; 26 import android.content.pm.ParceledListSlice; 27 import android.os.Binder; 28 import android.os.Bundle; 29 import android.os.IBinder; 30 import android.os.IRemoteCallback; 31 import android.os.RemoteException; 32 import android.os.ServiceManager; 33 import android.service.appprediction.flags.Flags; 34 import android.util.ArrayMap; 35 import android.util.Log; 36 37 import com.android.internal.annotations.GuardedBy; 38 39 import dalvik.system.CloseGuard; 40 41 import java.util.List; 42 import java.util.UUID; 43 import java.util.concurrent.Executor; 44 import java.util.concurrent.atomic.AtomicBoolean; 45 import java.util.function.Consumer; 46 47 /** 48 * Class that represents an App Prediction client. 49 * 50 * <p> 51 * Usage: <pre> {@code 52 * 53 * class MyActivity { 54 * private AppPredictor mClient 55 * 56 * void onCreate() { 57 * mClient = new AppPredictor(...) 58 * mClient.registerPredictionUpdates(...) 59 * } 60 * 61 * void onStart() { 62 * mClient.requestPredictionUpdate() 63 * } 64 * 65 * void onClick(...) { 66 * mClient.notifyAppTargetEvent(...) 67 * } 68 * 69 * void onDestroy() { 70 * mClient.unregisterPredictionUpdates() 71 * mClient.close() 72 * } 73 * 74 * }</pre> 75 * 76 * @hide 77 */ 78 @SystemApi 79 public final class AppPredictor { 80 81 private static final String TAG = AppPredictor.class.getSimpleName(); 82 83 private final IPredictionManager mPredictionManager; 84 private final CloseGuard mCloseGuard = CloseGuard.get(); 85 private final AtomicBoolean mIsClosed = new AtomicBoolean(false); 86 87 private final AppPredictionSessionId mSessionId; 88 @GuardedBy("itself") 89 private final ArrayMap<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>(); 90 91 /** 92 * Creates a new Prediction client. 93 * <p> 94 * The caller should call {@link AppPredictor#destroy()} to dispose the client once it 95 * no longer used. 96 * 97 * @param context The {@link Context} of the user of this {@link AppPredictor}. 98 * @param predictionContext The prediction context. 99 */ AppPredictor(@onNull Context context, @NonNull AppPredictionContext predictionContext)100 AppPredictor(@NonNull Context context, @NonNull AppPredictionContext predictionContext) { 101 IBinder b = ServiceManager.getService(Context.APP_PREDICTION_SERVICE); 102 mPredictionManager = IPredictionManager.Stub.asInterface(b); 103 mSessionId = new AppPredictionSessionId( 104 context.getPackageName() + ":" + UUID.randomUUID(), context.getUserId()); 105 try { 106 mPredictionManager.createPredictionSession(predictionContext, mSessionId, getToken()); 107 } catch (RemoteException e) { 108 Log.e(TAG, "Failed to create predictor", e); 109 e.rethrowAsRuntimeException(); 110 } 111 112 mCloseGuard.open("AppPredictor.close"); 113 } 114 115 /** 116 * Notifies the prediction service of an app target event. 117 * 118 * @param event The {@link AppTargetEvent} that represents the app target event. 119 */ notifyAppTargetEvent(@onNull AppTargetEvent event)120 public void notifyAppTargetEvent(@NonNull AppTargetEvent event) { 121 if (mIsClosed.get()) { 122 throw new IllegalStateException("This client has already been destroyed."); 123 } 124 125 try { 126 mPredictionManager.notifyAppTargetEvent(mSessionId, event); 127 } catch (RemoteException e) { 128 Log.e(TAG, "Failed to notify app target event", e); 129 e.rethrowAsRuntimeException(); 130 } 131 } 132 133 /** 134 * Notifies the prediction service when the targets in a launch location are shown to the user. 135 * 136 * @param launchLocation The launch location where the targets are shown to the user. 137 * @param targetIds List of {@link AppTargetId}s that are shown to the user. 138 */ notifyLaunchLocationShown(@onNull String launchLocation, @NonNull List<AppTargetId> targetIds)139 public void notifyLaunchLocationShown(@NonNull String launchLocation, 140 @NonNull List<AppTargetId> targetIds) { 141 if (mIsClosed.get()) { 142 throw new IllegalStateException("This client has already been destroyed."); 143 } 144 145 try { 146 mPredictionManager.notifyLaunchLocationShown(mSessionId, launchLocation, 147 new ParceledListSlice<>(targetIds)); 148 } catch (RemoteException e) { 149 Log.e(TAG, "Failed to notify location shown event", e); 150 e.rethrowAsRuntimeException(); 151 } 152 } 153 154 /** 155 * Requests the prediction service provide continuous updates of App predictions via the 156 * provided callback, until the given callback is unregistered. 157 * 158 * @see Callback#onTargetsAvailable(List). 159 * 160 * @param callbackExecutor The callback executor to use when calling the callback. 161 * @param callback The Callback to be called when updates of App predictions are available. 162 */ registerPredictionUpdates(@onNull @allbackExecutor Executor callbackExecutor, @NonNull AppPredictor.Callback callback)163 public void registerPredictionUpdates(@NonNull @CallbackExecutor Executor callbackExecutor, 164 @NonNull AppPredictor.Callback callback) { 165 synchronized (mRegisteredCallbacks) { 166 registerPredictionUpdatesLocked(callbackExecutor, callback); 167 } 168 } 169 170 @GuardedBy("mRegisteredCallbacks") registerPredictionUpdatesLocked( @onNull @allbackExecutor Executor callbackExecutor, @NonNull AppPredictor.Callback callback)171 private void registerPredictionUpdatesLocked( 172 @NonNull @CallbackExecutor Executor callbackExecutor, 173 @NonNull AppPredictor.Callback callback) { 174 if (mIsClosed.get()) { 175 throw new IllegalStateException("This client has already been destroyed."); 176 } 177 178 if (mRegisteredCallbacks.containsKey(callback)) { 179 // Skip if this callback is already registered 180 return; 181 } 182 try { 183 final CallbackWrapper callbackWrapper = new CallbackWrapper(callbackExecutor, 184 callback::onTargetsAvailable); 185 mPredictionManager.registerPredictionUpdates(mSessionId, callbackWrapper); 186 mRegisteredCallbacks.put(callback, callbackWrapper); 187 } catch (RemoteException e) { 188 Log.e(TAG, "Failed to register for prediction updates", e); 189 e.rethrowAsRuntimeException(); 190 } 191 } 192 193 /** 194 * Requests the prediction service to stop providing continuous updates to the provided 195 * callback until the callback is re-registered. 196 * 197 * @see {@link AppPredictor#registerPredictionUpdates(Executor, Callback)}. 198 * 199 * @param callback The callback to be unregistered. 200 */ unregisterPredictionUpdates(@onNull AppPredictor.Callback callback)201 public void unregisterPredictionUpdates(@NonNull AppPredictor.Callback callback) { 202 synchronized (mRegisteredCallbacks) { 203 unregisterPredictionUpdatesLocked(callback); 204 } 205 } 206 207 @GuardedBy("mRegisteredCallbacks") unregisterPredictionUpdatesLocked(@onNull AppPredictor.Callback callback)208 private void unregisterPredictionUpdatesLocked(@NonNull AppPredictor.Callback callback) { 209 if (mIsClosed.get()) { 210 throw new IllegalStateException("This client has already been destroyed."); 211 } 212 213 if (!mRegisteredCallbacks.containsKey(callback)) { 214 // Skip if this callback was never registered 215 return; 216 } 217 try { 218 final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(callback); 219 mPredictionManager.unregisterPredictionUpdates(mSessionId, callbackWrapper); 220 } catch (RemoteException e) { 221 Log.e(TAG, "Failed to unregister for prediction updates", e); 222 e.rethrowAsRuntimeException(); 223 } 224 } 225 226 /** 227 * Requests the prediction service to dispatch a new set of App predictions via the provided 228 * callback. 229 * 230 * @see Callback#onTargetsAvailable(List). 231 */ requestPredictionUpdate()232 public void requestPredictionUpdate() { 233 if (mIsClosed.get()) { 234 throw new IllegalStateException("This client has already been destroyed."); 235 } 236 237 try { 238 mPredictionManager.requestPredictionUpdate(mSessionId); 239 } catch (RemoteException e) { 240 Log.e(TAG, "Failed to request prediction update", e); 241 e.rethrowAsRuntimeException(); 242 } 243 } 244 245 /** 246 * Returns a new list of AppTargets sorted based on prediction rank or {@code null} if the 247 * ranker is not available. 248 * 249 * @param targets List of app targets to be sorted. 250 * @param callbackExecutor The callback executor to use when calling the callback. 251 * @param callback The callback to return the sorted list of app targets. 252 */ 253 @Nullable sortTargets(@onNull List<AppTarget> targets, @NonNull Executor callbackExecutor, @NonNull Consumer<List<AppTarget>> callback)254 public void sortTargets(@NonNull List<AppTarget> targets, 255 @NonNull Executor callbackExecutor, @NonNull Consumer<List<AppTarget>> callback) { 256 if (mIsClosed.get()) { 257 throw new IllegalStateException("This client has already been destroyed."); 258 } 259 260 try { 261 mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice<>(targets), 262 new CallbackWrapper(callbackExecutor, callback)); 263 } catch (RemoteException e) { 264 Log.e(TAG, "Failed to sort targets", e); 265 e.rethrowAsRuntimeException(); 266 } 267 } 268 269 /** 270 * Requests a Bundle which includes service features info or {@code null} if the service is not 271 * available. 272 * 273 * @param callbackExecutor The callback executor to use when calling the callback. It cannot be 274 * null. 275 * @param callback The callback to return the Bundle which includes service features info. It 276 * cannot be null. 277 * 278 * @throws IllegalStateException If this AppPredictor has already been destroyed. 279 * @throws RuntimeException If there is a failure communicating with the remote service. 280 */ 281 @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API) requestServiceFeatures(@onNull Executor callbackExecutor, @NonNull Consumer<Bundle> callback)282 public void requestServiceFeatures(@NonNull Executor callbackExecutor, 283 @NonNull Consumer<Bundle> callback) { 284 if (mIsClosed.get()) { 285 throw new IllegalStateException("This client has already been destroyed."); 286 } 287 288 try { 289 mPredictionManager.requestServiceFeatures(mSessionId, 290 new RemoteCallbackWrapper(callbackExecutor, callback)); 291 } catch (RemoteException e) { 292 Log.e(TAG, "Failed to request service feature info", e); 293 e.rethrowAsRuntimeException(); 294 } 295 } 296 297 /** 298 * Destroys the client and unregisters the callback. Any method on this class after this call 299 * with throw {@link IllegalStateException}. 300 */ destroy()301 public void destroy() { 302 if (!mIsClosed.getAndSet(true)) { 303 mCloseGuard.close(); 304 305 synchronized (mRegisteredCallbacks) { 306 destroySessionLocked(); 307 } 308 } else { 309 throw new IllegalStateException("This client has already been destroyed."); 310 } 311 } 312 313 @GuardedBy("mRegisteredCallbacks") destroySessionLocked()314 private void destroySessionLocked() { 315 try { 316 mPredictionManager.onDestroyPredictionSession(mSessionId); 317 } catch (RemoteException e) { 318 Log.e(TAG, "Failed to notify app target event", e); 319 e.rethrowAsRuntimeException(); 320 } 321 mRegisteredCallbacks.clear(); 322 } 323 324 @Override finalize()325 protected void finalize() throws Throwable { 326 try { 327 if (mCloseGuard != null) { 328 mCloseGuard.warnIfOpen(); 329 } 330 if (!mIsClosed.get()) { 331 destroy(); 332 } 333 } finally { 334 super.finalize(); 335 } 336 } 337 338 /** 339 * Returns the id of this prediction session. 340 * 341 * @hide 342 */ 343 @TestApi getSessionId()344 public AppPredictionSessionId getSessionId() { 345 return mSessionId; 346 } 347 348 /** 349 * Callback for receiving prediction updates. 350 */ 351 public interface Callback { 352 353 /** 354 * Called when a new set of predicted app targets are available. 355 * @param targets Sorted list of predicted targets. 356 */ onTargetsAvailable(@onNull List<AppTarget> targets)357 void onTargetsAvailable(@NonNull List<AppTarget> targets); 358 } 359 360 static class CallbackWrapper extends Stub { 361 362 private final Consumer<List<AppTarget>> mCallback; 363 private final Executor mExecutor; 364 CallbackWrapper(@onNull Executor callbackExecutor, @NonNull Consumer<List<AppTarget>> callback)365 CallbackWrapper(@NonNull Executor callbackExecutor, 366 @NonNull Consumer<List<AppTarget>> callback) { 367 mCallback = callback; 368 mExecutor = callbackExecutor; 369 } 370 371 @Override onResult(ParceledListSlice result)372 public void onResult(ParceledListSlice result) { 373 final long identity = Binder.clearCallingIdentity(); 374 try { 375 mExecutor.execute(() -> mCallback.accept(result.getList())); 376 } finally { 377 Binder.restoreCallingIdentity(identity); 378 } 379 } 380 } 381 382 static class RemoteCallbackWrapper extends IRemoteCallback.Stub { 383 384 private final Consumer<Bundle> mCallback; 385 private final Executor mExecutor; 386 RemoteCallbackWrapper(@onNull Executor callbackExecutor, @NonNull Consumer<Bundle> callback)387 RemoteCallbackWrapper(@NonNull Executor callbackExecutor, 388 @NonNull Consumer<Bundle> callback) { 389 mExecutor = callbackExecutor; 390 mCallback = callback; 391 } 392 393 @Override sendResult(Bundle result)394 public void sendResult(Bundle result) { 395 final long identity = Binder.clearCallingIdentity(); 396 try { 397 mExecutor.execute(() -> mCallback.accept(result)); 398 } finally { 399 Binder.restoreCallingIdentity(identity); 400 } 401 } 402 } 403 404 private static class Token { 405 static final IBinder sBinder = new Binder(TAG); 406 } 407 getToken()408 private static IBinder getToken() { 409 return Token.sBinder; 410 } 411 } 412