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.service.appprediction;
17 
18 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
19 
20 import android.annotation.CallSuper;
21 import android.annotation.MainThread;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.SystemApi;
25 import android.app.Service;
26 import android.app.prediction.AppPredictionContext;
27 import android.app.prediction.AppPredictionSessionId;
28 import android.app.prediction.AppTarget;
29 import android.app.prediction.AppTargetEvent;
30 import android.app.prediction.AppTargetId;
31 import android.app.prediction.IPredictionCallback;
32 import android.content.Intent;
33 import android.content.pm.ParceledListSlice;
34 import android.os.CancellationSignal;
35 import android.os.Handler;
36 import android.os.IBinder;
37 import android.os.Looper;
38 import android.os.RemoteException;
39 import android.service.appprediction.IPredictionService.Stub;
40 import android.util.ArrayMap;
41 import android.util.Log;
42 import android.util.Slog;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.function.Consumer;
47 
48 /**
49  * A service used to predict app and shortcut usage.
50  *
51  * @hide
52  */
53 @SystemApi
54 public abstract class AppPredictionService extends Service {
55 
56     private static final String TAG = "AppPredictionService";
57 
58     /**
59      * The {@link Intent} that must be declared as handled by the service.
60      *
61      * <p>The service must also require the {@link android.permission#MANAGE_APP_PREDICTIONS}
62      * permission.
63      *
64      * @hide
65      */
66     public static final String SERVICE_INTERFACE =
67             "android.service.appprediction.AppPredictionService";
68 
69     private final ArrayMap<AppPredictionSessionId, ArrayList<CallbackWrapper>> mSessionCallbacks =
70             new ArrayMap<>();
71     private Handler mHandler;
72 
73     private final IPredictionService mInterface = new Stub() {
74 
75         @Override
76         public void onCreatePredictionSession(AppPredictionContext context,
77                 AppPredictionSessionId sessionId) {
78             mHandler.sendMessage(
79                     obtainMessage(AppPredictionService::doCreatePredictionSession,
80                             AppPredictionService.this, context, sessionId));
81         }
82 
83         @Override
84         public void notifyAppTargetEvent(AppPredictionSessionId sessionId, AppTargetEvent event) {
85             mHandler.sendMessage(
86                     obtainMessage(AppPredictionService::onAppTargetEvent,
87                             AppPredictionService.this, sessionId, event));
88         }
89 
90         @Override
91         public void notifyLaunchLocationShown(AppPredictionSessionId sessionId,
92                 String launchLocation, ParceledListSlice targetIds) {
93             mHandler.sendMessage(
94                     obtainMessage(AppPredictionService::onLaunchLocationShown,
95                             AppPredictionService.this, sessionId, launchLocation,
96                             targetIds.getList()));
97         }
98 
99         @Override
100         public void sortAppTargets(AppPredictionSessionId sessionId, ParceledListSlice targets,
101                 IPredictionCallback callback) {
102             mHandler.sendMessage(
103                     obtainMessage(AppPredictionService::onSortAppTargets,
104                             AppPredictionService.this, sessionId, targets.getList(), null,
105                             new CallbackWrapper(callback, null)));
106         }
107 
108         @Override
109         public void registerPredictionUpdates(AppPredictionSessionId sessionId,
110                 IPredictionCallback callback) {
111             mHandler.sendMessage(
112                     obtainMessage(AppPredictionService::doRegisterPredictionUpdates,
113                             AppPredictionService.this, sessionId, callback));
114         }
115 
116         @Override
117         public void unregisterPredictionUpdates(AppPredictionSessionId sessionId,
118                 IPredictionCallback callback) {
119             mHandler.sendMessage(
120                     obtainMessage(AppPredictionService::doUnregisterPredictionUpdates,
121                             AppPredictionService.this, sessionId, callback));
122         }
123 
124         @Override
125         public void requestPredictionUpdate(AppPredictionSessionId sessionId) {
126             mHandler.sendMessage(
127                     obtainMessage(AppPredictionService::doRequestPredictionUpdate,
128                             AppPredictionService.this, sessionId));
129         }
130 
131         @Override
132         public void onDestroyPredictionSession(AppPredictionSessionId sessionId) {
133             mHandler.sendMessage(
134                     obtainMessage(AppPredictionService::doDestroyPredictionSession,
135                             AppPredictionService.this, sessionId));
136         }
137     };
138 
139     @CallSuper
140     @Override
onCreate()141     public void onCreate() {
142         super.onCreate();
143         mHandler = new Handler(Looper.getMainLooper(), null, true);
144     }
145 
146     @Override
147     @NonNull
onBind(@onNull Intent intent)148     public final IBinder onBind(@NonNull Intent intent) {
149         if (SERVICE_INTERFACE.equals(intent.getAction())) {
150             return mInterface.asBinder();
151         }
152         Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
153         return null;
154     }
155 
156     /**
157      * Called by a client app to indicate a target launch
158      */
159     @MainThread
onAppTargetEvent(@onNull AppPredictionSessionId sessionId, @NonNull AppTargetEvent event)160     public abstract void onAppTargetEvent(@NonNull AppPredictionSessionId sessionId,
161             @NonNull AppTargetEvent event);
162 
163     /**
164      * Called by a client app to indication a particular location has been shown to the user.
165      */
166     @MainThread
onLaunchLocationShown(@onNull AppPredictionSessionId sessionId, @NonNull String launchLocation, @NonNull List<AppTargetId> targetIds)167     public abstract void onLaunchLocationShown(@NonNull AppPredictionSessionId sessionId,
168             @NonNull String launchLocation, @NonNull List<AppTargetId> targetIds);
169 
doCreatePredictionSession(@onNull AppPredictionContext context, @NonNull AppPredictionSessionId sessionId)170     private void doCreatePredictionSession(@NonNull AppPredictionContext context,
171             @NonNull AppPredictionSessionId sessionId) {
172         mSessionCallbacks.put(sessionId, new ArrayList<>());
173         onCreatePredictionSession(context, sessionId);
174     }
175 
176     /**
177      * Creates a new interaction session.
178      *
179      * @param context interaction context
180      * @param sessionId the session's Id
181      */
onCreatePredictionSession(@onNull AppPredictionContext context, @NonNull AppPredictionSessionId sessionId)182     public void onCreatePredictionSession(@NonNull AppPredictionContext context,
183             @NonNull AppPredictionSessionId sessionId) {}
184 
185     /**
186      * Called by the client app to request sorting of targets based on prediction rank.
187      */
188     @MainThread
onSortAppTargets(@onNull AppPredictionSessionId sessionId, @NonNull List<AppTarget> targets, @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<List<AppTarget>> callback)189     public abstract void onSortAppTargets(@NonNull AppPredictionSessionId sessionId,
190             @NonNull List<AppTarget> targets, @NonNull CancellationSignal cancellationSignal,
191             @NonNull Consumer<List<AppTarget>> callback);
192 
doRegisterPredictionUpdates(@onNull AppPredictionSessionId sessionId, @NonNull IPredictionCallback callback)193     private void doRegisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId,
194             @NonNull IPredictionCallback callback) {
195         final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
196         if (callbacks == null) {
197             Slog.e(TAG, "Failed to register for updates for unknown session: " + sessionId);
198             return;
199         }
200 
201         final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback);
202         if (wrapper == null) {
203             callbacks.add(new CallbackWrapper(callback,
204                     callbackWrapper ->
205                         mHandler.post(() -> removeCallbackWrapper(callbacks, callbackWrapper))));
206             if (callbacks.size() == 1) {
207                 onStartPredictionUpdates();
208             }
209         }
210     }
211 
212     /**
213      * Called when any continuous prediction callback is registered.
214      */
215     @MainThread
onStartPredictionUpdates()216     public void onStartPredictionUpdates() {}
217 
doUnregisterPredictionUpdates(@onNull AppPredictionSessionId sessionId, @NonNull IPredictionCallback callback)218     private void doUnregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId,
219             @NonNull IPredictionCallback callback) {
220         final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
221         if (callbacks == null) {
222             Slog.e(TAG, "Failed to unregister for updates for unknown session: " + sessionId);
223             return;
224         }
225 
226         final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback);
227         removeCallbackWrapper(callbacks, wrapper);
228     }
229 
removeCallbackWrapper(@ullable ArrayList<CallbackWrapper> callbacks, @Nullable CallbackWrapper wrapper)230     private void removeCallbackWrapper(@Nullable ArrayList<CallbackWrapper> callbacks,
231             @Nullable CallbackWrapper wrapper) {
232         if (callbacks == null || wrapper == null) {
233             return;
234         }
235         callbacks.remove(wrapper);
236         wrapper.destroy();
237         if (callbacks.isEmpty()) {
238             onStopPredictionUpdates();
239         }
240     }
241 
242     /**
243      * Called when there are no longer any continuous prediction callbacks registered.
244      */
245     @MainThread
onStopPredictionUpdates()246     public void onStopPredictionUpdates() {}
247 
doRequestPredictionUpdate(@onNull AppPredictionSessionId sessionId)248     private void doRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) {
249         // Just an optimization, if there are no callbacks, then don't bother notifying the service
250         final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
251         if (callbacks != null && !callbacks.isEmpty()) {
252             onRequestPredictionUpdate(sessionId);
253         }
254     }
255 
256     /**
257      * Called by the client app to request target predictions. This method is only called if there
258      * are one or more prediction callbacks registered.
259      *
260      * @see #updatePredictions(AppPredictionSessionId, List)
261      */
262     @MainThread
onRequestPredictionUpdate(@onNull AppPredictionSessionId sessionId)263     public abstract void onRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId);
264 
doDestroyPredictionSession(@onNull AppPredictionSessionId sessionId)265     private void doDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {
266         final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.remove(sessionId);
267         if (callbacks != null) callbacks.forEach(CallbackWrapper::destroy);
268         onDestroyPredictionSession(sessionId);
269     }
270 
271     /**
272      * Destroys the interaction session.
273      *
274      * @param sessionId the id of the session to destroy
275      */
276     @MainThread
onDestroyPredictionSession(@onNull AppPredictionSessionId sessionId)277     public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {}
278 
279     /**
280      * Used by the prediction factory to send back results the client app. The can be called
281      * in response to {@link #onRequestPredictionUpdate(AppPredictionSessionId)} or proactively as
282      * a result of changes in predictions.
283      */
updatePredictions(@onNull AppPredictionSessionId sessionId, @NonNull List<AppTarget> targets)284     public final void updatePredictions(@NonNull AppPredictionSessionId sessionId,
285             @NonNull List<AppTarget> targets) {
286         List<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
287         if (callbacks != null) {
288             for (CallbackWrapper callback : callbacks) {
289                 callback.accept(targets);
290             }
291         }
292     }
293 
294     /**
295      * Finds the callback wrapper for the given callback.
296      */
findCallbackWrapper(ArrayList<CallbackWrapper> callbacks, IPredictionCallback callback)297     private CallbackWrapper findCallbackWrapper(ArrayList<CallbackWrapper> callbacks,
298             IPredictionCallback callback) {
299         for (int i = callbacks.size() - 1; i >= 0; i--) {
300             if (callbacks.get(i).isCallback(callback)) {
301                 return callbacks.get(i);
302             }
303         }
304         return null;
305     }
306 
307     private static final class CallbackWrapper implements Consumer<List<AppTarget>>,
308             IBinder.DeathRecipient {
309 
310         private IPredictionCallback mCallback;
311         private final Consumer<CallbackWrapper> mOnBinderDied;
312 
CallbackWrapper(IPredictionCallback callback, @Nullable Consumer<CallbackWrapper> onBinderDied)313         CallbackWrapper(IPredictionCallback callback,
314                 @Nullable Consumer<CallbackWrapper> onBinderDied) {
315             mCallback = callback;
316             mOnBinderDied = onBinderDied;
317             if (mOnBinderDied != null) {
318                 try {
319                     mCallback.asBinder().linkToDeath(this, 0);
320                 } catch (RemoteException e) {
321                     Slog.e(TAG, "Failed to link to death: " + e);
322                 }
323             }
324         }
325 
isCallback(@onNull IPredictionCallback callback)326         public boolean isCallback(@NonNull IPredictionCallback callback) {
327             if (mCallback == null) {
328                 Slog.e(TAG, "Callback is null, likely the binder has died.");
329                 return false;
330             }
331             return mCallback.asBinder().equals(callback.asBinder());
332         }
333 
destroy()334         public void destroy() {
335             if (mCallback != null && mOnBinderDied != null) {
336                 mCallback.asBinder().unlinkToDeath(this, 0);
337             }
338         }
339 
340         @Override
accept(List<AppTarget> ts)341         public void accept(List<AppTarget> ts) {
342             try {
343                 if (mCallback != null) {
344                     mCallback.onResult(new ParceledListSlice(ts));
345                 }
346             } catch (RemoteException e) {
347                 Slog.e(TAG, "Error sending result:" + e);
348             }
349         }
350 
351         @Override
binderDied()352         public void binderDied() {
353             destroy();
354             mCallback = null;
355             if (mOnBinderDied != null) {
356                 mOnBinderDied.accept(this);
357             }
358         }
359     }
360 }
361