/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.service.smartspace; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import android.annotation.CallSuper; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.Service; import android.app.smartspace.ISmartspaceCallback; import android.app.smartspace.SmartspaceConfig; import android.app.smartspace.SmartspaceSessionId; import android.app.smartspace.SmartspaceTarget; import android.app.smartspace.SmartspaceTargetEvent; import android.content.Intent; import android.content.pm.ParceledListSlice; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.service.smartspace.ISmartspaceService.Stub; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; /** * A service used to share the lifecycle of smartspace UI (open, close, interaction) * and also to return smartspace result on a query. * * @hide */ @SystemApi public abstract class SmartspaceService extends Service { /** * The {@link Intent} that must be declared as handled by the service. * *

The service must also require the {@link android.permission#MANAGE_SMARTSPACE} * permission. * * @hide */ public static final String SERVICE_INTERFACE = "android.service.smartspace.SmartspaceService"; private static final boolean DEBUG = false; private static final String TAG = "SmartspaceService"; private final ArrayMap> mSessionCallbacks = new ArrayMap<>(); private Handler mHandler; private final android.service.smartspace.ISmartspaceService mInterface = new Stub() { @Override public void onCreateSmartspaceSession(SmartspaceConfig smartspaceConfig, SmartspaceSessionId sessionId) { mHandler.sendMessage( obtainMessage(SmartspaceService::doCreateSmartspaceSession, SmartspaceService.this, smartspaceConfig, sessionId)); } @Override public void notifySmartspaceEvent(SmartspaceSessionId sessionId, SmartspaceTargetEvent event) { mHandler.sendMessage( obtainMessage(SmartspaceService::notifySmartspaceEvent, SmartspaceService.this, sessionId, event)); } @Override public void requestSmartspaceUpdate(SmartspaceSessionId sessionId) { mHandler.sendMessage( obtainMessage(SmartspaceService::doRequestPredictionUpdate, SmartspaceService.this, sessionId)); } @Override public void registerSmartspaceUpdates(SmartspaceSessionId sessionId, ISmartspaceCallback callback) { mHandler.sendMessage( obtainMessage(SmartspaceService::doRegisterSmartspaceUpdates, SmartspaceService.this, sessionId, callback)); } @Override public void unregisterSmartspaceUpdates(SmartspaceSessionId sessionId, ISmartspaceCallback callback) { mHandler.sendMessage( obtainMessage(SmartspaceService::doUnregisterSmartspaceUpdates, SmartspaceService.this, sessionId, callback)); } @Override public void onDestroySmartspaceSession(SmartspaceSessionId sessionId) { mHandler.sendMessage( obtainMessage(SmartspaceService::doDestroy, SmartspaceService.this, sessionId)); } }; @CallSuper @Override public void onCreate() { super.onCreate(); if (DEBUG) { Log.d(TAG, "onCreate mSessionCallbacks: " + mSessionCallbacks); } mHandler = new Handler(Looper.getMainLooper(), null, true); } @Override @NonNull public final IBinder onBind(@NonNull Intent intent) { if (DEBUG) { Log.d(TAG, "onBind mSessionCallbacks: " + mSessionCallbacks); } if (SERVICE_INTERFACE.equals(intent.getAction())) { return mInterface.asBinder(); } Slog.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent); return null; } private void doCreateSmartspaceSession(@NonNull SmartspaceConfig config, @NonNull SmartspaceSessionId sessionId) { if (DEBUG) { Log.d(TAG, "doCreateSmartspaceSession mSessionCallbacks: " + mSessionCallbacks); } mSessionCallbacks.put(sessionId, new ArrayList<>()); onCreateSmartspaceSession(config, sessionId); } /** * Gets called when the client calls SmartspaceManager#createSmartspaceSession . */ public abstract void onCreateSmartspaceSession(@NonNull SmartspaceConfig config, @NonNull SmartspaceSessionId sessionId); /** * Gets called when the client calls SmartspaceSession#notifySmartspaceEvent . */ @MainThread public abstract void notifySmartspaceEvent(@NonNull SmartspaceSessionId sessionId, @NonNull SmartspaceTargetEvent event); /** * Gets called when the client calls SmartspaceSession#requestSmartspaceUpdate . */ @MainThread public abstract void onRequestSmartspaceUpdate(@NonNull SmartspaceSessionId sessionId); private void doRegisterSmartspaceUpdates(@NonNull SmartspaceSessionId sessionId, @NonNull ISmartspaceCallback callback) { if (DEBUG) { Log.d(TAG, "doRegisterSmartspaceUpdates mSessionCallbacks: " + mSessionCallbacks); } final ArrayList callbacks = mSessionCallbacks.get(sessionId); if (callbacks == null) { Slog.e(TAG, "Failed to register for updates for unknown session: " + sessionId); return; } final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); if (wrapper == null) { callbacks.add(new CallbackWrapper(callback, callbackWrapper -> mHandler.post( () -> removeCallbackWrapper(callbacks, callbackWrapper)))); } } private void doUnregisterSmartspaceUpdates(@NonNull SmartspaceSessionId sessionId, @NonNull ISmartspaceCallback callback) { if (DEBUG) { Log.d(TAG, "doUnregisterSmartspaceUpdates mSessionCallbacks: " + mSessionCallbacks); } final ArrayList callbacks = mSessionCallbacks.get(sessionId); if (callbacks == null) { Slog.e(TAG, "Failed to unregister for updates for unknown session: " + sessionId); return; } final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); removeCallbackWrapper(callbacks, wrapper); } private void doRequestPredictionUpdate(@NonNull SmartspaceSessionId sessionId) { if (DEBUG) { Log.d(TAG, "doRequestPredictionUpdate mSessionCallbacks: " + mSessionCallbacks); } // Just an optimization, if there are no callbacks, then don't bother notifying the service final ArrayList callbacks = mSessionCallbacks.get(sessionId); if (callbacks != null && !callbacks.isEmpty()) { onRequestSmartspaceUpdate(sessionId); } } /** * Finds the callback wrapper for the given callback. */ private CallbackWrapper findCallbackWrapper(ArrayList callbacks, ISmartspaceCallback callback) { for (int i = callbacks.size() - 1; i >= 0; i--) { if (callbacks.get(i).isCallback(callback)) { return callbacks.get(i); } } return null; } private void removeCallbackWrapper(@Nullable ArrayList callbacks, @Nullable CallbackWrapper wrapper) { if (callbacks == null || wrapper == null) { return; } callbacks.remove(wrapper); wrapper.destroy(); } /** * Gets called when the client calls SmartspaceManager#destroy() . */ public abstract void onDestroySmartspaceSession(@NonNull SmartspaceSessionId sessionId); private void doDestroy(@NonNull SmartspaceSessionId sessionId) { if (DEBUG) { Log.d(TAG, "doDestroy mSessionCallbacks: " + mSessionCallbacks); } super.onDestroy(); final ArrayList callbacks = mSessionCallbacks.remove(sessionId); if (callbacks != null) callbacks.forEach(CallbackWrapper::destroy); onDestroySmartspaceSession(sessionId); } /** * Used by the prediction factory to send back results the client app. The can be called * in response to {@link #onRequestSmartspaceUpdate(SmartspaceSessionId)} or proactively as * a result of changes in predictions. */ public final void updateSmartspaceTargets(@NonNull SmartspaceSessionId sessionId, @NonNull List targets) { if (DEBUG) { Log.d(TAG, "updateSmartspaceTargets mSessionCallbacks: " + mSessionCallbacks); } List callbacks = mSessionCallbacks.get(sessionId); if (callbacks != null) { for (CallbackWrapper callback : callbacks) { callback.accept(targets); } } } /** * Destroys a smartspace session. */ @MainThread public abstract void onDestroy(@NonNull SmartspaceSessionId sessionId); private static final class CallbackWrapper implements Consumer>, IBinder.DeathRecipient { private final Consumer mOnBinderDied; private ISmartspaceCallback mCallback; CallbackWrapper(ISmartspaceCallback callback, @Nullable Consumer onBinderDied) { mCallback = callback; mOnBinderDied = onBinderDied; if (mOnBinderDied != null) { try { mCallback.asBinder().linkToDeath(this, 0); } catch (RemoteException e) { Slog.e(TAG, "Failed to link to death: " + e); } } } public boolean isCallback(@NonNull ISmartspaceCallback callback) { if (mCallback == null) { Slog.e(TAG, "Callback is null, likely the binder has died."); return false; } return mCallback.asBinder().equals(callback.asBinder()); } @Override public void accept(List smartspaceTargets) { try { if (mCallback != null) { if (DEBUG) { Slog.d(TAG, "CallbackWrapper.accept smartspaceTargets=" + smartspaceTargets); } mCallback.onResult(new ParceledListSlice(smartspaceTargets)); } } catch (RemoteException e) { Slog.e(TAG, "Error sending result:" + e); } } public void destroy() { if (mCallback != null && mOnBinderDied != null) { mCallback.asBinder().unlinkToDeath(this, 0); } } @Override public void binderDied() { destroy(); mCallback = null; if (mOnBinderDied != null) { mOnBinderDied.accept(this); } } } }