/* * Copyright (C) 2018 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.prediction; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.prediction.IPredictionCallback.Stub; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.IRemoteCallback; import android.os.RemoteException; import android.os.ServiceManager; import android.service.appprediction.flags.Flags; import android.util.ArrayMap; import android.util.Log; import com.android.internal.annotations.GuardedBy; 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; /** * Class that represents an App Prediction client. * *

* Usage:

 {@code
 *
 * class MyActivity {
 *    private AppPredictor mClient
 *
 *    void onCreate() {
 *         mClient = new AppPredictor(...)
 *         mClient.registerPredictionUpdates(...)
 *    }
 *
 *    void onStart() {
 *        mClient.requestPredictionUpdate()
 *    }
 *
 *    void onClick(...) {
 *        mClient.notifyAppTargetEvent(...)
 *    }
 *
 *    void onDestroy() {
 *        mClient.unregisterPredictionUpdates()
 *        mClient.close()
 *    }
 *
 * }
* * @hide */ @SystemApi public final class AppPredictor { private static final String TAG = AppPredictor.class.getSimpleName(); private final IPredictionManager mPredictionManager; private final CloseGuard mCloseGuard = CloseGuard.get(); private final AtomicBoolean mIsClosed = new AtomicBoolean(false); private final AppPredictionSessionId mSessionId; @GuardedBy("itself") private final ArrayMap mRegisteredCallbacks = new ArrayMap<>(); /** * Creates a new Prediction client. *

* The caller should call {@link AppPredictor#destroy()} to dispose the client once it * no longer used. * * @param context The {@link Context} of the user of this {@link AppPredictor}. * @param predictionContext The prediction context. */ AppPredictor(@NonNull Context context, @NonNull AppPredictionContext predictionContext) { IBinder b = ServiceManager.getService(Context.APP_PREDICTION_SERVICE); mPredictionManager = IPredictionManager.Stub.asInterface(b); mSessionId = new AppPredictionSessionId( context.getPackageName() + ":" + UUID.randomUUID(), context.getUserId()); try { mPredictionManager.createPredictionSession(predictionContext, mSessionId, getToken()); } catch (RemoteException e) { Log.e(TAG, "Failed to create predictor", e); e.rethrowAsRuntimeException(); } mCloseGuard.open("AppPredictor.close"); } /** * Notifies the prediction service of an app target event. * * @param event The {@link AppTargetEvent} that represents the app target event. */ public void notifyAppTargetEvent(@NonNull AppTargetEvent event) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } try { mPredictionManager.notifyAppTargetEvent(mSessionId, event); } catch (RemoteException e) { Log.e(TAG, "Failed to notify app target event", e); e.rethrowAsRuntimeException(); } } /** * Notifies the prediction service when the targets in a launch location are shown to the user. * * @param launchLocation The launch location where the targets are shown to the user. * @param targetIds List of {@link AppTargetId}s that are shown to the user. */ public void notifyLaunchLocationShown(@NonNull String launchLocation, @NonNull List targetIds) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } try { mPredictionManager.notifyLaunchLocationShown(mSessionId, launchLocation, new ParceledListSlice<>(targetIds)); } catch (RemoteException e) { Log.e(TAG, "Failed to notify location shown event", e); e.rethrowAsRuntimeException(); } } /** * Requests the prediction service provide continuous updates of App predictions via the * provided callback, until the given callback is unregistered. * * @see Callback#onTargetsAvailable(List). * * @param callbackExecutor The callback executor to use when calling the callback. * @param callback The Callback to be called when updates of App predictions are available. */ public void registerPredictionUpdates(@NonNull @CallbackExecutor Executor callbackExecutor, @NonNull AppPredictor.Callback callback) { synchronized (mRegisteredCallbacks) { registerPredictionUpdatesLocked(callbackExecutor, callback); } } @GuardedBy("mRegisteredCallbacks") private void registerPredictionUpdatesLocked( @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull AppPredictor.Callback callback) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } if (mRegisteredCallbacks.containsKey(callback)) { // Skip if this callback is already registered return; } try { final CallbackWrapper callbackWrapper = new CallbackWrapper(callbackExecutor, callback::onTargetsAvailable); mPredictionManager.registerPredictionUpdates(mSessionId, callbackWrapper); mRegisteredCallbacks.put(callback, callbackWrapper); } catch (RemoteException e) { Log.e(TAG, "Failed to register for prediction updates", e); e.rethrowAsRuntimeException(); } } /** * Requests the prediction service to stop providing continuous updates to the provided * callback until the callback is re-registered. * * @see {@link AppPredictor#registerPredictionUpdates(Executor, Callback)}. * * @param callback The callback to be unregistered. */ public void unregisterPredictionUpdates(@NonNull AppPredictor.Callback callback) { synchronized (mRegisteredCallbacks) { unregisterPredictionUpdatesLocked(callback); } } @GuardedBy("mRegisteredCallbacks") private void unregisterPredictionUpdatesLocked(@NonNull AppPredictor.Callback callback) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } if (!mRegisteredCallbacks.containsKey(callback)) { // Skip if this callback was never registered return; } try { final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(callback); mPredictionManager.unregisterPredictionUpdates(mSessionId, callbackWrapper); } catch (RemoteException e) { Log.e(TAG, "Failed to unregister for prediction updates", e); e.rethrowAsRuntimeException(); } } /** * Requests the prediction service to dispatch a new set of App predictions via the provided * callback. * * @see Callback#onTargetsAvailable(List). */ public void requestPredictionUpdate() { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } try { mPredictionManager.requestPredictionUpdate(mSessionId); } catch (RemoteException e) { Log.e(TAG, "Failed to request prediction update", e); e.rethrowAsRuntimeException(); } } /** * Returns a new list of AppTargets sorted based on prediction rank or {@code null} if the * ranker is not available. * * @param targets List of app targets to be sorted. * @param callbackExecutor The callback executor to use when calling the callback. * @param callback The callback to return the sorted list of app targets. */ @Nullable public void sortTargets(@NonNull List targets, @NonNull Executor callbackExecutor, @NonNull Consumer> callback) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } try { mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice<>(targets), new CallbackWrapper(callbackExecutor, callback)); } catch (RemoteException e) { Log.e(TAG, "Failed to sort targets", e); e.rethrowAsRuntimeException(); } } /** * Requests a Bundle which includes service features info or {@code null} if the service is not * available. * * @param callbackExecutor The callback executor to use when calling the callback. It cannot be * null. * @param callback The callback to return the Bundle which includes service features info. It * cannot be null. * * @throws IllegalStateException If this AppPredictor has already been destroyed. * @throws RuntimeException If there is a failure communicating with the remote service. */ @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API) public void requestServiceFeatures(@NonNull Executor callbackExecutor, @NonNull Consumer callback) { if (mIsClosed.get()) { throw new IllegalStateException("This client has already been destroyed."); } try { mPredictionManager.requestServiceFeatures(mSessionId, new RemoteCallbackWrapper(callbackExecutor, callback)); } catch (RemoteException e) { Log.e(TAG, "Failed to request service feature info", e); e.rethrowAsRuntimeException(); } } /** * Destroys the client and unregisters the callback. Any method on this class after this call * with throw {@link IllegalStateException}. */ public void destroy() { if (!mIsClosed.getAndSet(true)) { mCloseGuard.close(); synchronized (mRegisteredCallbacks) { destroySessionLocked(); } } else { throw new IllegalStateException("This client has already been destroyed."); } } @GuardedBy("mRegisteredCallbacks") private void destroySessionLocked() { try { mPredictionManager.onDestroyPredictionSession(mSessionId); } catch (RemoteException e) { Log.e(TAG, "Failed to notify app target event", e); e.rethrowAsRuntimeException(); } mRegisteredCallbacks.clear(); } @Override protected void finalize() throws Throwable { try { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } if (!mIsClosed.get()) { destroy(); } } finally { super.finalize(); } } /** * Returns the id of this prediction session. * * @hide */ @TestApi public AppPredictionSessionId getSessionId() { return mSessionId; } /** * Callback for receiving prediction updates. */ public interface Callback { /** * Called when a new set of predicted app targets are available. * @param targets Sorted list of predicted 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 { mExecutor.execute(() -> mCallback.accept(result.getList())); } finally { Binder.restoreCallingIdentity(identity); } } } static class RemoteCallbackWrapper extends IRemoteCallback.Stub { private final Consumer mCallback; private final Executor mExecutor; RemoteCallbackWrapper(@NonNull Executor callbackExecutor, @NonNull Consumer callback) { mExecutor = callbackExecutor; mCallback = callback; } @Override public void sendResult(Bundle result) { final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute(() -> mCallback.accept(result)); } finally { Binder.restoreCallingIdentity(identity); } } } private static class Token { static final IBinder sBinder = new Binder(TAG); } private static IBinder getToken() { return Token.sBinder; } }