/* * 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.app.smartspace; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.SystemApi; import android.app.smartspace.ISmartspaceCallback.Stub; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.util.ArrayMap; import android.util.Log; import dalvik.system.CloseGuard; import java.util.List; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; /** * Client API to share information about the Smartspace UI state and execute query. * *

* Usage:

 {@code
 *
 * class MyActivity {
 *    private SmartspaceSession mSmartspaceSession;
 *
 *    void onCreate() {
 *         mSmartspaceSession = mSmartspaceManager.createSmartspaceSession(smartspaceConfig)
 *         mSmartspaceSession.registerSmartspaceUpdates(...)
 *    }
 *
 *    void onStart() {
 *        mSmartspaceSession.requestSmartspaceUpdate()
 *    }
 *
 *    void onTouch(...) OR
 *    void onStateTransitionStarted(...) OR
 *    void onResume(...) OR
 *    void onStop(...) {
 *        mSmartspaceSession.notifyEvent(event);
 *    }
 *
 *    void onDestroy() {
 *        mSmartspaceSession.unregisterPredictionUpdates()
 *        mSmartspaceSession.close();
 *    }
 *
 * }
* * @hide */ @SystemApi public final class SmartspaceSession implements AutoCloseable { private static final String TAG = SmartspaceSession.class.getSimpleName(); private static final boolean DEBUG = false; private final android.app.smartspace.ISmartspaceManager mInterface; private final CloseGuard mCloseGuard = CloseGuard.get(); private final AtomicBoolean mIsClosed = new AtomicBoolean(false); private final SmartspaceSessionId mSessionId; private final ArrayMap mRegisteredCallbacks = new ArrayMap<>(); /** * Creates a new Smartspace ui client. *

* The caller should call {@link SmartspaceSession#destroy()} to dispose the client once it * no longer used. * * @param context the {@link Context} of the user of this {@link SmartspaceSession}. * @param smartspaceConfig the Smartspace context. */ // b/177858121 Create weak reference child objects to not leak context. SmartspaceSession(@NonNull Context context, @NonNull SmartspaceConfig smartspaceConfig) { IBinder b = ServiceManager.getService(Context.SMARTSPACE_SERVICE); mInterface = android.app.smartspace.ISmartspaceManager.Stub.asInterface(b); mSessionId = new SmartspaceSessionId( context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUser()); try { mInterface.createSmartspaceSession(smartspaceConfig, mSessionId, getToken()); } catch (RemoteException e) { Log.e(TAG, "Failed to create Smartspace session", e); e.rethrowFromSystemServer(); } mCloseGuard.open("SmartspaceSession.close"); } /** * Notifies the Smartspace service of a Smartspace target event. * * @param event The {@link SmartspaceTargetEvent} that represents the Smartspace target event. */ public void notifySmartspaceEvent(@NonNull SmartspaceTargetEvent event) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } try { mInterface.notifySmartspaceEvent(mSessionId, event); } catch (RemoteException e) { Log.e(TAG, "Failed to notify event", e); e.rethrowFromSystemServer(); } } /** * Requests the smartspace service for an update. */ public void requestSmartspaceUpdate() { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } try { mInterface.requestSmartspaceUpdate(mSessionId); } catch (RemoteException e) { Log.e(TAG, "Failed to request update.", e); e.rethrowFromSystemServer(); } } /** * Requests the smartspace service provide continuous updates of smartspace cards via the * provided callback, until the given callback is unregistered. * * @param listenerExecutor The listener executor to use when firing the listener. * @param listener The listener to be called when updates of Smartspace targets are * available. */ public void addOnTargetsAvailableListener(@NonNull @CallbackExecutor Executor listenerExecutor, @NonNull OnTargetsAvailableListener listener) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } if (mRegisteredCallbacks.containsKey(listener)) { // Skip if this callback is already registered return; } try { final CallbackWrapper callbackWrapper = new CallbackWrapper(listenerExecutor, listener::onTargetsAvailable); mRegisteredCallbacks.put(listener, callbackWrapper); mInterface.registerSmartspaceUpdates(mSessionId, callbackWrapper); mInterface.requestSmartspaceUpdate(mSessionId); } catch (RemoteException e) { Log.e(TAG, "Failed to register for smartspace updates", e); e.rethrowAsRuntimeException(); } } /** * Requests the smartspace service to stop providing continuous updates to the provided * callback until the callback is re-registered. * * @param listener The callback to be unregistered. * @see {@link SmartspaceSession#addOnTargetsAvailableListener(Executor, * OnTargetsAvailableListener)}. */ public void removeOnTargetsAvailableListener(@NonNull OnTargetsAvailableListener listener) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } if (!mRegisteredCallbacks.containsKey(listener)) { // Skip if this callback was never registered return; } try { final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(listener); mInterface.unregisterSmartspaceUpdates(mSessionId, callbackWrapper); } catch (RemoteException e) { Log.e(TAG, "Failed to unregister for smartspace updates", e); e.rethrowAsRuntimeException(); } } /** * Destroys the client and unregisters the callback. Any method on this class after this call * will throw {@link IllegalStateException}. */ private void destroy() { if (!mIsClosed.getAndSet(true)) { mCloseGuard.close(); // Do destroy; try { mInterface.destroySmartspaceSession(mSessionId); } catch (RemoteException e) { Log.e(TAG, "Failed to notify Smartspace target event", e); e.rethrowFromSystemServer(); } } else { throw new IllegalStateException("This client has already been destroyed."); } } @Override protected void finalize() { try { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } if (!mIsClosed.get()) { destroy(); } } finally { try { super.finalize(); } catch (Throwable throwable) { throwable.printStackTrace(); } } } @Override public void close() { try { destroy(); finalize(); } catch (Throwable throwable) { throwable.printStackTrace(); } } /** * Listener to receive smartspace targets from the service. */ public interface OnTargetsAvailableListener { /** * Called when a new set of smartspace targets are available. * * @param targets Ranked list of smartspace targets. */ void onTargetsAvailable(@NonNull List targets); } static class CallbackWrapper extends Stub { private final Consumer> mCallback; private final Executor mExecutor; CallbackWrapper(@NonNull Executor callbackExecutor, @NonNull Consumer> callback) { mCallback = callback; mExecutor = callbackExecutor; } @Override public void onResult(ParceledListSlice result) { final long identity = Binder.clearCallingIdentity(); try { if (DEBUG) { Log.d(TAG, "CallbackWrapper.onResult result=" + result.getList()); } mExecutor.execute(() -> mCallback.accept(result.getList())); } finally { Binder.restoreCallingIdentity(identity); } } } private static class Token { static final IBinder sBinder = new Binder(TAG); } private static IBinder getToken() { return Token.sBinder; } }