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     /**
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(), context.getUserId());
101         try {
102             mPredictionManager.createPredictionSession(predictionContext, mSessionId, getToken());
103         } catch (RemoteException e) {
104             Log.e(TAG, "Failed to create predictor", e);
105             e.rethrowAsRuntimeException();
106         }
107 
108         mCloseGuard.open("AppPredictor.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         synchronized (mRegisteredCallbacks) {
162             registerPredictionUpdatesLocked(callbackExecutor, callback);
163         }
164     }
165 
166     @GuardedBy("mRegisteredCallbacks")
registerPredictionUpdatesLocked( @onNull @allbackExecutor Executor callbackExecutor, @NonNull AppPredictor.Callback callback)167     private void registerPredictionUpdatesLocked(
168             @NonNull @CallbackExecutor Executor callbackExecutor,
169             @NonNull AppPredictor.Callback callback) {
170         if (mIsClosed.get()) {
171             throw new IllegalStateException("This client has already been destroyed.");
172         }
173 
174         if (mRegisteredCallbacks.containsKey(callback)) {
175             // Skip if this callback is already registered
176             return;
177         }
178         try {
179             final CallbackWrapper callbackWrapper = new CallbackWrapper(callbackExecutor,
180                     callback::onTargetsAvailable);
181             mPredictionManager.registerPredictionUpdates(mSessionId, callbackWrapper);
182             mRegisteredCallbacks.put(callback, callbackWrapper);
183         } catch (RemoteException e) {
184             Log.e(TAG, "Failed to register for prediction updates", e);
185             e.rethrowAsRuntimeException();
186         }
187     }
188 
189     /**
190      * Requests the prediction service to stop providing continuous updates to the provided
191      * callback until the callback is re-registered.
192      *
193      * @see {@link AppPredictor#registerPredictionUpdates(Executor, Callback)}.
194      *
195      * @param callback The callback to be unregistered.
196      */
unregisterPredictionUpdates(@onNull AppPredictor.Callback callback)197     public void unregisterPredictionUpdates(@NonNull AppPredictor.Callback callback) {
198         synchronized (mRegisteredCallbacks) {
199             unregisterPredictionUpdatesLocked(callback);
200         }
201     }
202 
203     @GuardedBy("mRegisteredCallbacks")
unregisterPredictionUpdatesLocked(@onNull AppPredictor.Callback callback)204     private void unregisterPredictionUpdatesLocked(@NonNull AppPredictor.Callback callback) {
205         if (mIsClosed.get()) {
206             throw new IllegalStateException("This client has already been destroyed.");
207         }
208 
209         if (!mRegisteredCallbacks.containsKey(callback)) {
210             // Skip if this callback was never registered
211             return;
212         }
213         try {
214             final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(callback);
215             mPredictionManager.unregisterPredictionUpdates(mSessionId, callbackWrapper);
216         } catch (RemoteException e) {
217             Log.e(TAG, "Failed to unregister for prediction updates", e);
218             e.rethrowAsRuntimeException();
219         }
220     }
221 
222     /**
223      * Requests the prediction service to dispatch a new set of App predictions via the provided
224      * callback.
225      *
226      * @see Callback#onTargetsAvailable(List).
227      */
requestPredictionUpdate()228     public void requestPredictionUpdate() {
229         if (mIsClosed.get()) {
230             throw new IllegalStateException("This client has already been destroyed.");
231         }
232 
233         try {
234             mPredictionManager.requestPredictionUpdate(mSessionId);
235         } catch (RemoteException e) {
236             Log.e(TAG, "Failed to request prediction update", e);
237             e.rethrowAsRuntimeException();
238         }
239     }
240 
241     /**
242      * Returns a new list of AppTargets sorted based on prediction rank or {@code null} if the
243      * ranker is not available.
244      *
245      * @param targets List of app targets to be sorted.
246      * @param callbackExecutor The callback executor to use when calling the callback.
247      * @param callback The callback to return the sorted list of app targets.
248      */
249     @Nullable
sortTargets(@onNull List<AppTarget> targets, @NonNull Executor callbackExecutor, @NonNull Consumer<List<AppTarget>> callback)250     public void sortTargets(@NonNull List<AppTarget> targets,
251             @NonNull Executor callbackExecutor, @NonNull Consumer<List<AppTarget>> callback) {
252         if (mIsClosed.get()) {
253             throw new IllegalStateException("This client has already been destroyed.");
254         }
255 
256         try {
257             mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice<>(targets),
258                     new CallbackWrapper(callbackExecutor, callback));
259         } catch (RemoteException e) {
260             Log.e(TAG, "Failed to sort targets", e);
261             e.rethrowAsRuntimeException();
262         }
263     }
264 
265     /**
266      * Destroys the client and unregisters the callback. Any method on this class after this call
267      * with throw {@link IllegalStateException}.
268      */
destroy()269     public void destroy() {
270         if (!mIsClosed.getAndSet(true)) {
271             mCloseGuard.close();
272 
273             synchronized (mRegisteredCallbacks) {
274                 destroySessionLocked();
275             }
276         } else {
277             throw new IllegalStateException("This client has already been destroyed.");
278         }
279     }
280 
281     @GuardedBy("mRegisteredCallbacks")
destroySessionLocked()282     private void destroySessionLocked() {
283         try {
284             mPredictionManager.onDestroyPredictionSession(mSessionId);
285         } catch (RemoteException e) {
286             Log.e(TAG, "Failed to notify app target event", e);
287             e.rethrowAsRuntimeException();
288         }
289         mRegisteredCallbacks.clear();
290     }
291 
292     @Override
finalize()293     protected void finalize() throws Throwable {
294         try {
295             if (mCloseGuard != null) {
296                 mCloseGuard.warnIfOpen();
297             }
298             if (!mIsClosed.get()) {
299                 destroy();
300             }
301         } finally {
302             super.finalize();
303         }
304     }
305 
306     /**
307      * Returns the id of this prediction session.
308      *
309      * @hide
310      */
311     @TestApi
getSessionId()312     public AppPredictionSessionId getSessionId() {
313         return mSessionId;
314     }
315 
316     /**
317      * Callback for receiving prediction updates.
318      */
319     public interface Callback {
320 
321         /**
322          * Called when a new set of predicted app targets are available.
323          * @param targets Sorted list of predicted targets.
324          */
onTargetsAvailable(@onNull List<AppTarget> targets)325         void onTargetsAvailable(@NonNull List<AppTarget> targets);
326     }
327 
328     static class CallbackWrapper extends Stub {
329 
330         private final Consumer<List<AppTarget>> mCallback;
331         private final Executor mExecutor;
332 
CallbackWrapper(@onNull Executor callbackExecutor, @NonNull Consumer<List<AppTarget>> callback)333         CallbackWrapper(@NonNull Executor callbackExecutor,
334                 @NonNull Consumer<List<AppTarget>> callback) {
335             mCallback = callback;
336             mExecutor = callbackExecutor;
337         }
338 
339         @Override
onResult(ParceledListSlice result)340         public void onResult(ParceledListSlice result) {
341             final long identity = Binder.clearCallingIdentity();
342             try {
343                 mExecutor.execute(() -> mCallback.accept(result.getList()));
344             } finally {
345                 Binder.restoreCallingIdentity(identity);
346             }
347         }
348     }
349 
350     private static class Token {
351         static final IBinder sBinder = new Binder(TAG);
352     }
353 
getToken()354     private static IBinder getToken() {
355         return Token.sBinder;
356     }
357 }
358