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     private final IBinder mToken = new Binder();
87 
88     /**
89      * Creates a new Smartspace ui client.
90      * <p>
91      * The caller should call {@link SmartspaceSession#destroy()} to dispose the client once it
92      * no longer used.
93      *
94      * @param context          the {@link Context} of the user of this {@link SmartspaceSession}.
95      * @param smartspaceConfig the Smartspace context.
96      */
97     // b/177858121 Create weak reference child objects to not leak context.
SmartspaceSession(@onNull Context context, @NonNull SmartspaceConfig smartspaceConfig)98     SmartspaceSession(@NonNull Context context, @NonNull SmartspaceConfig smartspaceConfig) {
99         IBinder b = ServiceManager.getService(Context.SMARTSPACE_SERVICE);
100         mInterface = android.app.smartspace.ISmartspaceManager.Stub.asInterface(b);
101         mSessionId = new SmartspaceSessionId(
102                 context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUser());
103         try {
104             mInterface.createSmartspaceSession(smartspaceConfig, mSessionId, mToken);
105         } catch (RemoteException e) {
106             Log.e(TAG, "Failed to create Smartspace session", e);
107             e.rethrowFromSystemServer();
108         }
109 
110         mCloseGuard.open("close");
111     }
112 
113     /**
114      * Notifies the Smartspace service of a Smartspace target event.
115      *
116      * @param event The {@link SmartspaceTargetEvent} that represents the Smartspace target event.
117      */
notifySmartspaceEvent(@onNull SmartspaceTargetEvent event)118     public void notifySmartspaceEvent(@NonNull SmartspaceTargetEvent event) {
119         if (mIsClosed.get()) {
120             throw new IllegalStateException("This client has already been destroyed.");
121         }
122         try {
123             mInterface.notifySmartspaceEvent(mSessionId, event);
124         } catch (RemoteException e) {
125             Log.e(TAG, "Failed to notify event", e);
126             e.rethrowFromSystemServer();
127         }
128     }
129 
130     /**
131      * Requests the smartspace service for an update.
132      */
requestSmartspaceUpdate()133     public void requestSmartspaceUpdate() {
134         if (mIsClosed.get()) {
135             throw new IllegalStateException("This client has already been destroyed.");
136         }
137         try {
138             mInterface.requestSmartspaceUpdate(mSessionId);
139         } catch (RemoteException e) {
140             Log.e(TAG, "Failed to request update.", e);
141             e.rethrowFromSystemServer();
142         }
143     }
144 
145     /**
146      * Requests the smartspace service provide continuous updates of smartspace cards via the
147      * provided callback, until the given callback is unregistered.
148      *
149      * @param listenerExecutor The listener executor to use when firing the listener.
150      * @param listener         The listener to be called when updates of Smartspace targets are
151      *                         available.
152      */
addOnTargetsAvailableListener(@onNull @allbackExecutor Executor listenerExecutor, @NonNull OnTargetsAvailableListener listener)153     public void addOnTargetsAvailableListener(@NonNull @CallbackExecutor Executor listenerExecutor,
154             @NonNull OnTargetsAvailableListener listener) {
155         if (mIsClosed.get()) {
156             throw new IllegalStateException("This client has already been destroyed.");
157         }
158 
159         if (mRegisteredCallbacks.containsKey(listener)) {
160             // Skip if this callback is already registered
161             return;
162         }
163         try {
164             final CallbackWrapper callbackWrapper = new CallbackWrapper(listenerExecutor,
165                     listener::onTargetsAvailable);
166             mRegisteredCallbacks.put(listener, callbackWrapper);
167             mInterface.registerSmartspaceUpdates(mSessionId, callbackWrapper);
168             mInterface.requestSmartspaceUpdate(mSessionId);
169         } catch (RemoteException e) {
170             Log.e(TAG, "Failed to register for smartspace updates", e);
171             e.rethrowAsRuntimeException();
172         }
173     }
174 
175     /**
176      * Requests the smartspace service to stop providing continuous updates to the provided
177      * callback until the callback is re-registered.
178      *
179      * @param listener The callback to be unregistered.
180      * @see {@link SmartspaceSession#addOnTargetsAvailableListener(Executor,
181      * OnTargetsAvailableListener)}.
182      */
removeOnTargetsAvailableListener(@onNull OnTargetsAvailableListener listener)183     public void removeOnTargetsAvailableListener(@NonNull OnTargetsAvailableListener listener) {
184         if (mIsClosed.get()) {
185             throw new IllegalStateException("This client has already been destroyed.");
186         }
187 
188         if (!mRegisteredCallbacks.containsKey(listener)) {
189             // Skip if this callback was never registered
190             return;
191         }
192         try {
193             final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(listener);
194             mInterface.unregisterSmartspaceUpdates(mSessionId, callbackWrapper);
195         } catch (RemoteException e) {
196             Log.e(TAG, "Failed to unregister for smartspace updates", e);
197             e.rethrowAsRuntimeException();
198         }
199     }
200 
201     /**
202      * Destroys the client and unregisters the callback. Any method on this class after this call
203      * will throw {@link IllegalStateException}.
204      */
destroy()205     private void destroy() {
206         if (!mIsClosed.getAndSet(true)) {
207             mCloseGuard.close();
208 
209             // Do destroy;
210             try {
211                 mInterface.destroySmartspaceSession(mSessionId);
212             } catch (RemoteException e) {
213                 Log.e(TAG, "Failed to notify Smartspace target event", e);
214                 e.rethrowFromSystemServer();
215             }
216         } else {
217             throw new IllegalStateException("This client has already been destroyed.");
218         }
219     }
220 
221     @Override
finalize()222     protected void finalize() {
223         try {
224             if (mCloseGuard != null) {
225                 mCloseGuard.warnIfOpen();
226             }
227             if (!mIsClosed.get()) {
228                 destroy();
229             }
230         } finally {
231             try {
232                 super.finalize();
233             } catch (Throwable throwable) {
234                 throwable.printStackTrace();
235             }
236         }
237     }
238 
239     @Override
close()240     public void close() {
241         try {
242             destroy();
243             finalize();
244         } catch (Throwable throwable) {
245             throwable.printStackTrace();
246         }
247     }
248 
249     /**
250      * Listener to receive smartspace targets from the service.
251      */
252     public interface OnTargetsAvailableListener {
253 
254         /**
255          * Called when a new set of smartspace targets are available.
256          *
257          * @param targets Ranked list of smartspace targets.
258          */
onTargetsAvailable(@onNull List<SmartspaceTarget> targets)259         void onTargetsAvailable(@NonNull List<SmartspaceTarget> targets);
260     }
261 
262     static class CallbackWrapper extends Stub {
263 
264         private final Consumer<List<SmartspaceTarget>> mCallback;
265         private final Executor mExecutor;
266 
CallbackWrapper(@onNull Executor callbackExecutor, @NonNull Consumer<List<SmartspaceTarget>> callback)267         CallbackWrapper(@NonNull Executor callbackExecutor,
268                 @NonNull Consumer<List<SmartspaceTarget>> callback) {
269             mCallback = callback;
270             mExecutor = callbackExecutor;
271         }
272 
273         @Override
onResult(ParceledListSlice result)274         public void onResult(ParceledListSlice result) {
275             final long identity = Binder.clearCallingIdentity();
276             try {
277                 if (DEBUG) {
278                     Log.d(TAG, "CallbackWrapper.onResult result=" + result.getList());
279                 }
280                 mExecutor.execute(() -> mCallback.accept(result.getList()));
281             } finally {
282                 Binder.restoreCallingIdentity(identity);
283             }
284         }
285     }
286 }
287