• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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