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