412 lines
14 KiB
Java
412 lines
14 KiB
Java
/*
|
|
* 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.
|
|
*
|
|
* <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
|
|
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<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>();
|
|
|
|
/**
|
|
* 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(), 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<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) {
|
|
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<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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<Bundle> 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<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);
|
|
}
|
|
}
|
|
}
|
|
|
|
static class RemoteCallbackWrapper extends IRemoteCallback.Stub {
|
|
|
|
private final Consumer<Bundle> mCallback;
|
|
private final Executor mExecutor;
|
|
|
|
RemoteCallbackWrapper(@NonNull Executor callbackExecutor,
|
|
@NonNull Consumer<Bundle> 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;
|
|
}
|
|
}
|