| /* |
| * 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.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.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; |
| |
| /** |
| * Class that represents an App Prediction client. |
| * |
| * <p> |
| * Usage: <pre> {@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() |
| * } |
| * |
| * }</pre> |
| * |
| * @hide |
| */ |
| @SystemApi |
| @TestApi |
| 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; |
| private final ArrayMap<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>(); |
| |
| private final IBinder mToken = new Binder(); |
| |
| /** |
| * Creates a new Prediction client. |
| * <p> |
| * 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().toString(), context.getUserId()); |
| try { |
| mPredictionManager.createPredictionSession(predictionContext, mSessionId, mToken); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to create predictor", e); |
| e.rethrowAsRuntimeException(); |
| } |
| |
| mCloseGuard.open("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<AppTargetId> 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) { |
| 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) { |
| 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<AppTarget> targets, |
| @NonNull Executor callbackExecutor, @NonNull Consumer<List<AppTarget>> 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(); |
| } |
| } |
| |
| /** |
| * 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(); |
| |
| // Do destroy; |
| try { |
| mPredictionManager.onDestroyPredictionSession(mSessionId); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to notify app target event", e); |
| e.rethrowAsRuntimeException(); |
| } |
| mRegisteredCallbacks.clear(); |
| } else { |
| throw new IllegalStateException("This client has already been destroyed."); |
| } |
| } |
| |
| @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<AppTarget> targets); |
| } |
| |
| static class CallbackWrapper extends Stub { |
| |
| private final Consumer<List<AppTarget>> mCallback; |
| private final Executor mExecutor; |
| |
| CallbackWrapper(@NonNull Executor callbackExecutor, |
| @NonNull Consumer<List<AppTarget>> 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); |
| } |
| } |
| } |
| } |