1 /*
2  * Copyright (C) 2021 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.smartspace;
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.smartspace.ISmartspaceCallback;
27 import android.app.smartspace.SmartspaceConfig;
28 import android.app.smartspace.SmartspaceSessionId;
29 import android.app.smartspace.SmartspaceTarget;
30 import android.app.smartspace.SmartspaceTargetEvent;
31 import android.content.Intent;
32 import android.content.pm.ParceledListSlice;
33 import android.os.Handler;
34 import android.os.IBinder;
35 import android.os.Looper;
36 import android.os.RemoteException;
37 import android.service.smartspace.ISmartspaceService.Stub;
38 import android.util.ArrayMap;
39 import android.util.Log;
40 import android.util.Slog;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.function.Consumer;
45 
46 /**
47  * A service used to share the lifecycle of smartspace UI (open, close, interaction)
48  * and also to return smartspace result on a query.
49  *
50  * @hide
51  */
52 @SystemApi
53 public abstract class SmartspaceService extends Service {
54 
55     /**
56      * The {@link Intent} that must be declared as handled by the service.
57      *
58      * <p>The service must also require the {@link android.permission#MANAGE_SMARTSPACE}
59      * permission.
60      *
61      * @hide
62      */
63     public static final String SERVICE_INTERFACE =
64             "android.service.smartspace.SmartspaceService";
65     private static final boolean DEBUG = false;
66     private static final String TAG = "SmartspaceService";
67     private final ArrayMap<SmartspaceSessionId, ArrayList<CallbackWrapper>> mSessionCallbacks =
68             new ArrayMap<>();
69     private Handler mHandler;
70 
71     private final android.service.smartspace.ISmartspaceService mInterface = new Stub() {
72 
73         @Override
74         public void onCreateSmartspaceSession(SmartspaceConfig smartspaceConfig,
75                 SmartspaceSessionId sessionId) {
76             mHandler.sendMessage(
77                     obtainMessage(SmartspaceService::doCreateSmartspaceSession,
78                             SmartspaceService.this, smartspaceConfig, sessionId));
79         }
80 
81         @Override
82         public void notifySmartspaceEvent(SmartspaceSessionId sessionId,
83                 SmartspaceTargetEvent event) {
84             mHandler.sendMessage(
85                     obtainMessage(SmartspaceService::notifySmartspaceEvent,
86                             SmartspaceService.this, sessionId, event));
87         }
88 
89         @Override
90         public void requestSmartspaceUpdate(SmartspaceSessionId sessionId) {
91             mHandler.sendMessage(
92                     obtainMessage(SmartspaceService::doRequestPredictionUpdate,
93                             SmartspaceService.this, sessionId));
94         }
95 
96         @Override
97         public void registerSmartspaceUpdates(SmartspaceSessionId sessionId,
98                 ISmartspaceCallback callback) {
99             mHandler.sendMessage(
100                     obtainMessage(SmartspaceService::doRegisterSmartspaceUpdates,
101                             SmartspaceService.this, sessionId, callback));
102         }
103 
104         @Override
105         public void unregisterSmartspaceUpdates(SmartspaceSessionId sessionId,
106                 ISmartspaceCallback callback) {
107             mHandler.sendMessage(
108                     obtainMessage(SmartspaceService::doUnregisterSmartspaceUpdates,
109                             SmartspaceService.this, sessionId, callback));
110         }
111 
112         @Override
113         public void onDestroySmartspaceSession(SmartspaceSessionId sessionId) {
114 
115             mHandler.sendMessage(
116                     obtainMessage(SmartspaceService::doDestroy,
117                             SmartspaceService.this, sessionId));
118         }
119     };
120 
121     @CallSuper
122     @Override
onCreate()123     public void onCreate() {
124         super.onCreate();
125         if (DEBUG) {
126             Log.d(TAG, "onCreate mSessionCallbacks: " + mSessionCallbacks);
127         }
128         mHandler = new Handler(Looper.getMainLooper(), null, true);
129     }
130 
131     @Override
132     @NonNull
onBind(@onNull Intent intent)133     public final IBinder onBind(@NonNull Intent intent) {
134         if (DEBUG) {
135             Log.d(TAG, "onBind mSessionCallbacks: " + mSessionCallbacks);
136         }
137         if (SERVICE_INTERFACE.equals(intent.getAction())) {
138             return mInterface.asBinder();
139         }
140         Slog.w(TAG, "Tried to bind to wrong intent (should be "
141                 + SERVICE_INTERFACE + ": " + intent);
142         return null;
143     }
144 
doCreateSmartspaceSession(@onNull SmartspaceConfig config, @NonNull SmartspaceSessionId sessionId)145     private void doCreateSmartspaceSession(@NonNull SmartspaceConfig config,
146             @NonNull SmartspaceSessionId sessionId) {
147         if (DEBUG) {
148             Log.d(TAG, "doCreateSmartspaceSession mSessionCallbacks: " + mSessionCallbacks);
149         }
150         mSessionCallbacks.put(sessionId, new ArrayList<>());
151         onCreateSmartspaceSession(config, sessionId);
152     }
153 
154     /**
155      * Gets called when the client calls <code> SmartspaceManager#createSmartspaceSession </code>.
156      */
onCreateSmartspaceSession(@onNull SmartspaceConfig config, @NonNull SmartspaceSessionId sessionId)157     public abstract void onCreateSmartspaceSession(@NonNull SmartspaceConfig config,
158             @NonNull SmartspaceSessionId sessionId);
159 
160     /**
161      * Gets called when the client calls <code> SmartspaceSession#notifySmartspaceEvent </code>.
162      */
163     @MainThread
notifySmartspaceEvent(@onNull SmartspaceSessionId sessionId, @NonNull SmartspaceTargetEvent event)164     public abstract void notifySmartspaceEvent(@NonNull SmartspaceSessionId sessionId,
165             @NonNull SmartspaceTargetEvent event);
166 
167     /**
168      * Gets called when the client calls <code> SmartspaceSession#requestSmartspaceUpdate </code>.
169      */
170     @MainThread
onRequestSmartspaceUpdate(@onNull SmartspaceSessionId sessionId)171     public abstract void onRequestSmartspaceUpdate(@NonNull SmartspaceSessionId sessionId);
172 
doRegisterSmartspaceUpdates(@onNull SmartspaceSessionId sessionId, @NonNull ISmartspaceCallback callback)173     private void doRegisterSmartspaceUpdates(@NonNull SmartspaceSessionId sessionId,
174             @NonNull ISmartspaceCallback callback) {
175         if (DEBUG) {
176             Log.d(TAG, "doRegisterSmartspaceUpdates mSessionCallbacks: " + mSessionCallbacks);
177         }
178         final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
179         if (callbacks == null) {
180             Slog.e(TAG, "Failed to register for updates for unknown session: " + sessionId);
181             return;
182         }
183 
184         final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback);
185         if (wrapper == null) {
186             callbacks.add(new CallbackWrapper(callback,
187                     callbackWrapper ->
188                             mHandler.post(
189                                     () -> removeCallbackWrapper(callbacks, callbackWrapper))));
190         }
191     }
192 
doUnregisterSmartspaceUpdates(@onNull SmartspaceSessionId sessionId, @NonNull ISmartspaceCallback callback)193     private void doUnregisterSmartspaceUpdates(@NonNull SmartspaceSessionId sessionId,
194             @NonNull ISmartspaceCallback callback) {
195         if (DEBUG) {
196             Log.d(TAG, "doUnregisterSmartspaceUpdates mSessionCallbacks: " + mSessionCallbacks);
197         }
198         final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
199         if (callbacks == null) {
200             Slog.e(TAG, "Failed to unregister for updates for unknown session: " + sessionId);
201             return;
202         }
203 
204         final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback);
205         removeCallbackWrapper(callbacks, wrapper);
206     }
207 
doRequestPredictionUpdate(@onNull SmartspaceSessionId sessionId)208     private void doRequestPredictionUpdate(@NonNull SmartspaceSessionId sessionId) {
209         if (DEBUG) {
210             Log.d(TAG, "doRequestPredictionUpdate mSessionCallbacks: " + mSessionCallbacks);
211         }
212         // Just an optimization, if there are no callbacks, then don't bother notifying the service
213         final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
214         if (callbacks != null && !callbacks.isEmpty()) {
215             onRequestSmartspaceUpdate(sessionId);
216         }
217     }
218 
219     /**
220      * Finds the callback wrapper for the given callback.
221      */
findCallbackWrapper(ArrayList<CallbackWrapper> callbacks, ISmartspaceCallback callback)222     private CallbackWrapper findCallbackWrapper(ArrayList<CallbackWrapper> callbacks,
223             ISmartspaceCallback callback) {
224         for (int i = callbacks.size() - 1; i >= 0; i--) {
225             if (callbacks.get(i).isCallback(callback)) {
226                 return callbacks.get(i);
227             }
228         }
229         return null;
230     }
231 
removeCallbackWrapper(@ullable ArrayList<CallbackWrapper> callbacks, @Nullable CallbackWrapper wrapper)232     private void removeCallbackWrapper(@Nullable ArrayList<CallbackWrapper> callbacks,
233             @Nullable CallbackWrapper wrapper) {
234         if (callbacks == null || wrapper == null) {
235             return;
236         }
237         callbacks.remove(wrapper);
238         wrapper.destroy();
239     }
240 
241     /**
242      * Gets called when the client calls <code> SmartspaceManager#destroy() </code>.
243      */
onDestroySmartspaceSession(@onNull SmartspaceSessionId sessionId)244     public abstract void onDestroySmartspaceSession(@NonNull SmartspaceSessionId sessionId);
245 
doDestroy(@onNull SmartspaceSessionId sessionId)246     private void doDestroy(@NonNull SmartspaceSessionId sessionId) {
247         if (DEBUG) {
248             Log.d(TAG, "doDestroy mSessionCallbacks: " + mSessionCallbacks);
249         }
250         super.onDestroy();
251 
252         final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.remove(sessionId);
253         if (callbacks != null) callbacks.forEach(CallbackWrapper::destroy);
254         onDestroySmartspaceSession(sessionId);
255     }
256 
257     /**
258      * Used by the prediction factory to send back results the client app. The can be called
259      * in response to {@link #onRequestSmartspaceUpdate(SmartspaceSessionId)} or proactively as
260      * a result of changes in predictions.
261      */
updateSmartspaceTargets(@onNull SmartspaceSessionId sessionId, @NonNull List<SmartspaceTarget> targets)262     public final void updateSmartspaceTargets(@NonNull SmartspaceSessionId sessionId,
263             @NonNull List<SmartspaceTarget> targets) {
264         if (DEBUG) {
265             Log.d(TAG, "updateSmartspaceTargets mSessionCallbacks: " + mSessionCallbacks);
266         }
267         List<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
268         if (callbacks != null) {
269             for (CallbackWrapper callback : callbacks) {
270                 callback.accept(targets);
271             }
272         }
273     }
274 
275     /**
276      * Destroys a smartspace session.
277      */
278     @MainThread
onDestroy(@onNull SmartspaceSessionId sessionId)279     public abstract void onDestroy(@NonNull SmartspaceSessionId sessionId);
280 
281     private static final class CallbackWrapper implements Consumer<List<SmartspaceTarget>>,
282             IBinder.DeathRecipient {
283 
284         private final Consumer<CallbackWrapper> mOnBinderDied;
285         private ISmartspaceCallback mCallback;
286 
CallbackWrapper(ISmartspaceCallback callback, @Nullable Consumer<CallbackWrapper> onBinderDied)287         CallbackWrapper(ISmartspaceCallback callback,
288                 @Nullable Consumer<CallbackWrapper> onBinderDied) {
289             mCallback = callback;
290             mOnBinderDied = onBinderDied;
291             if (mOnBinderDied != null) {
292                 try {
293                     mCallback.asBinder().linkToDeath(this, 0);
294                 } catch (RemoteException e) {
295                     Slog.e(TAG, "Failed to link to death: " + e);
296                 }
297             }
298         }
299 
isCallback(@onNull ISmartspaceCallback callback)300         public boolean isCallback(@NonNull ISmartspaceCallback callback) {
301             if (mCallback == null) {
302                 Slog.e(TAG, "Callback is null, likely the binder has died.");
303                 return false;
304             }
305             return mCallback.asBinder().equals(callback.asBinder());
306         }
307 
308         @Override
accept(List<SmartspaceTarget> smartspaceTargets)309         public void accept(List<SmartspaceTarget> smartspaceTargets) {
310             try {
311                 if (mCallback != null) {
312                     if (DEBUG) {
313                         Slog.d(TAG,
314                                 "CallbackWrapper.accept smartspaceTargets=" + smartspaceTargets);
315                     }
316                     mCallback.onResult(new ParceledListSlice(smartspaceTargets));
317                 }
318             } catch (RemoteException e) {
319                 Slog.e(TAG, "Error sending result:" + e);
320             }
321         }
322 
destroy()323         public void destroy() {
324             if (mCallback != null && mOnBinderDied != null) {
325                 mCallback.asBinder().unlinkToDeath(this, 0);
326             }
327         }
328 
329         @Override
binderDied()330         public void binderDied() {
331             destroy();
332             mCallback = null;
333             if (mOnBinderDied != null) {
334                 mOnBinderDied.accept(this);
335             }
336         }
337     }
338 }
339