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