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