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