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