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.app.smartspace;
17 
18 import android.annotation.CallbackExecutor;
19 import android.annotation.NonNull;
20 import android.annotation.SystemApi;
21 import android.app.smartspace.ISmartspaceCallback.Stub;
22 import android.content.Context;
23 import android.content.pm.ParceledListSlice;
24 import android.os.Binder;
25 import android.os.IBinder;
26 import android.os.RemoteException;
27 import android.os.ServiceManager;
28 import android.util.ArrayMap;
29 import android.util.Log;
30 
31 import dalvik.system.CloseGuard;
32 
33 import java.util.List;
34 import java.util.UUID;
35 import java.util.concurrent.Executor;
36 import java.util.concurrent.atomic.AtomicBoolean;
37 import java.util.function.Consumer;
38 
39 /**
40  * Client API to share information about the Smartspace UI state and execute query.
41  *
42  * <p>
43  * Usage: <pre> {@code
44  *
45  * class MyActivity {
46  *    private SmartspaceSession mSmartspaceSession;
47  *
48  *    void onCreate() {
49  *         mSmartspaceSession = mSmartspaceManager.createSmartspaceSession(smartspaceConfig)
50  *         mSmartspaceSession.registerSmartspaceUpdates(...)
51  *    }
52  *
53  *    void onStart() {
54  *        mSmartspaceSession.requestSmartspaceUpdate()
55  *    }
56  *
57  *    void onTouch(...) OR
58  *    void onStateTransitionStarted(...) OR
59  *    void onResume(...) OR
60  *    void onStop(...) {
61  *        mSmartspaceSession.notifyEvent(event);
62  *    }
63  *
64  *    void onDestroy() {
65  *        mSmartspaceSession.unregisterPredictionUpdates()
66  *        mSmartspaceSession.close();
67  *    }
68  *
69  * }</pre>
70  *
71  * @hide
72  */
73 @SystemApi
74 public final class SmartspaceSession implements AutoCloseable {
75 
76     private static final String TAG = SmartspaceSession.class.getSimpleName();
77     private static final boolean DEBUG = false;
78 
79     private final android.app.smartspace.ISmartspaceManager mInterface;
80     private final CloseGuard mCloseGuard = CloseGuard.get();
81     private final AtomicBoolean mIsClosed = new AtomicBoolean(false);
82 
83     private final SmartspaceSessionId mSessionId;
84     private final ArrayMap<OnTargetsAvailableListener, CallbackWrapper> mRegisteredCallbacks =
85             new ArrayMap<>();
86 
87     /**
88      * Creates a new Smartspace ui client.
89      * <p>
90      * The caller should call {@link SmartspaceSession#destroy()} to dispose the client once it
91      * no longer used.
92      *
93      * @param context          the {@link Context} of the user of this {@link SmartspaceSession}.
94      * @param smartspaceConfig the Smartspace context.
95      */
96     // b/177858121 Create weak reference child objects to not leak context.
SmartspaceSession(@onNull Context context, @NonNull SmartspaceConfig smartspaceConfig)97     SmartspaceSession(@NonNull Context context, @NonNull SmartspaceConfig smartspaceConfig) {
98         IBinder b = ServiceManager.getService(Context.SMARTSPACE_SERVICE);
99         mInterface = android.app.smartspace.ISmartspaceManager.Stub.asInterface(b);
100         mSessionId = new SmartspaceSessionId(
101                 context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUser());
102         try {
103             mInterface.createSmartspaceSession(smartspaceConfig, mSessionId, getToken());
104         } catch (RemoteException e) {
105             Log.e(TAG, "Failed to create Smartspace session", e);
106             e.rethrowFromSystemServer();
107         }
108 
109         mCloseGuard.open("SmartspaceSession.close");
110     }
111 
112     /**
113      * Notifies the Smartspace service of a Smartspace target event.
114      *
115      * @param event The {@link SmartspaceTargetEvent} that represents the Smartspace target event.
116      */
notifySmartspaceEvent(@onNull SmartspaceTargetEvent event)117     public void notifySmartspaceEvent(@NonNull SmartspaceTargetEvent event) {
118         if (mIsClosed.get()) {
119             throw new IllegalStateException("This client has already been destroyed.");
120         }
121         try {
122             mInterface.notifySmartspaceEvent(mSessionId, event);
123         } catch (RemoteException e) {
124             Log.e(TAG, "Failed to notify event", e);
125             e.rethrowFromSystemServer();
126         }
127     }
128 
129     /**
130      * Requests the smartspace service for an update.
131      */
requestSmartspaceUpdate()132     public void requestSmartspaceUpdate() {
133         if (mIsClosed.get()) {
134             throw new IllegalStateException("This client has already been destroyed.");
135         }
136         try {
137             mInterface.requestSmartspaceUpdate(mSessionId);
138         } catch (RemoteException e) {
139             Log.e(TAG, "Failed to request update.", e);
140             e.rethrowFromSystemServer();
141         }
142     }
143 
144     /**
145      * Requests the smartspace service provide continuous updates of smartspace cards via the
146      * provided callback, until the given callback is unregistered.
147      *
148      * @param listenerExecutor The listener executor to use when firing the listener.
149      * @param listener         The listener to be called when updates of Smartspace targets are
150      *                         available.
151      */
addOnTargetsAvailableListener(@onNull @allbackExecutor Executor listenerExecutor, @NonNull OnTargetsAvailableListener listener)152     public void addOnTargetsAvailableListener(@NonNull @CallbackExecutor Executor listenerExecutor,
153             @NonNull OnTargetsAvailableListener listener) {
154         if (mIsClosed.get()) {
155             throw new IllegalStateException("This client has already been destroyed.");
156         }
157 
158         if (mRegisteredCallbacks.containsKey(listener)) {
159             // Skip if this callback is already registered
160             return;
161         }
162         try {
163             final CallbackWrapper callbackWrapper = new CallbackWrapper(listenerExecutor,
164                     listener::onTargetsAvailable);
165             mRegisteredCallbacks.put(listener, callbackWrapper);
166             mInterface.registerSmartspaceUpdates(mSessionId, callbackWrapper);
167             mInterface.requestSmartspaceUpdate(mSessionId);
168         } catch (RemoteException e) {
169             Log.e(TAG, "Failed to register for smartspace updates", e);
170             e.rethrowAsRuntimeException();
171         }
172     }
173 
174     /**
175      * Requests the smartspace service to stop providing continuous updates to the provided
176      * callback until the callback is re-registered.
177      *
178      * @param listener The callback to be unregistered.
179      * @see {@link SmartspaceSession#addOnTargetsAvailableListener(Executor,
180      * OnTargetsAvailableListener)}.
181      */
removeOnTargetsAvailableListener(@onNull OnTargetsAvailableListener listener)182     public void removeOnTargetsAvailableListener(@NonNull OnTargetsAvailableListener listener) {
183         if (mIsClosed.get()) {
184             throw new IllegalStateException("This client has already been destroyed.");
185         }
186 
187         if (!mRegisteredCallbacks.containsKey(listener)) {
188             // Skip if this callback was never registered
189             return;
190         }
191         try {
192             final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(listener);
193             mInterface.unregisterSmartspaceUpdates(mSessionId, callbackWrapper);
194         } catch (RemoteException e) {
195             Log.e(TAG, "Failed to unregister for smartspace updates", e);
196             e.rethrowAsRuntimeException();
197         }
198     }
199 
200     /**
201      * Destroys the client and unregisters the callback. Any method on this class after this call
202      * will throw {@link IllegalStateException}.
203      */
destroy()204     private void destroy() {
205         if (!mIsClosed.getAndSet(true)) {
206             mCloseGuard.close();
207 
208             // Do destroy;
209             try {
210                 mInterface.destroySmartspaceSession(mSessionId);
211             } catch (RemoteException e) {
212                 Log.e(TAG, "Failed to notify Smartspace target event", e);
213                 e.rethrowFromSystemServer();
214             }
215         } else {
216             throw new IllegalStateException("This client has already been destroyed.");
217         }
218     }
219 
220     @Override
finalize()221     protected void finalize() {
222         try {
223             if (mCloseGuard != null) {
224                 mCloseGuard.warnIfOpen();
225             }
226             if (!mIsClosed.get()) {
227                 destroy();
228             }
229         } finally {
230             try {
231                 super.finalize();
232             } catch (Throwable throwable) {
233                 throwable.printStackTrace();
234             }
235         }
236     }
237 
238     @Override
close()239     public void close() {
240         try {
241             destroy();
242             finalize();
243         } catch (Throwable throwable) {
244             throwable.printStackTrace();
245         }
246     }
247 
248     /**
249      * Listener to receive smartspace targets from the service.
250      */
251     public interface OnTargetsAvailableListener {
252 
253         /**
254          * Called when a new set of smartspace targets are available.
255          *
256          * @param targets Ranked list of smartspace targets.
257          */
onTargetsAvailable(@onNull List<SmartspaceTarget> targets)258         void onTargetsAvailable(@NonNull List<SmartspaceTarget> targets);
259     }
260 
261     static class CallbackWrapper extends Stub {
262 
263         private final Consumer<List<SmartspaceTarget>> mCallback;
264         private final Executor mExecutor;
265 
CallbackWrapper(@onNull Executor callbackExecutor, @NonNull Consumer<List<SmartspaceTarget>> callback)266         CallbackWrapper(@NonNull Executor callbackExecutor,
267                 @NonNull Consumer<List<SmartspaceTarget>> callback) {
268             mCallback = callback;
269             mExecutor = callbackExecutor;
270         }
271 
272         @Override
onResult(ParceledListSlice result)273         public void onResult(ParceledListSlice result) {
274             final long identity = Binder.clearCallingIdentity();
275             try {
276                 if (DEBUG) {
277                     Log.d(TAG, "CallbackWrapper.onResult result=" + result.getList());
278                 }
279                 mExecutor.execute(() -> mCallback.accept(result.getList()));
280             } finally {
281                 Binder.restoreCallingIdentity(identity);
282             }
283         }
284     }
285 
286     private static class Token {
287         static final IBinder sBinder = new Binder(TAG);
288     }
289 
getToken()290     private static IBinder getToken() {
291         return Token.sBinder;
292     }
293 }
294