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