diff options
author | 2018-11-14 11:59:02 -0800 | |
---|---|---|
committer | 2019-01-10 15:53:25 -0800 | |
commit | 54e91344e2a0072c40d09405fe5a295467b36c07 (patch) | |
tree | 8e25640c130c1ca8e6975a513c69a21f1961ba02 | |
parent | 00112e6f6f23edf15cc01be5fd099bc0f88cdd4f (diff) |
Adding initial implementation of Prediction client/service API
Test: Build sample app, ensure that app prediction service gets client
requests
Bug: 111701043
Change-Id: I33aceb2de31552b2d740dc333559d68728753e40
Signed-off-by: Winson Chung <winsonc@google.com>
34 files changed, 2369 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp index 15befaef84ae..687d2d72395f 100644 --- a/Android.bp +++ b/Android.bp @@ -104,6 +104,8 @@ java_defaults { "core/java/android/app/backup/IRestoreObserver.aidl", "core/java/android/app/backup/IRestoreSession.aidl", "core/java/android/app/backup/ISelectBackupTransportCallback.aidl", + "core/java/android/app/prediction/IPredictionCallback.aidl", + "core/java/android/app/prediction/IPredictionManager.aidl", "core/java/android/app/role/IOnRoleHoldersChangedListener.aidl", "core/java/android/app/role/IRoleManager.aidl", "core/java/android/app/role/IRoleManagerCallback.aidl", @@ -273,6 +275,7 @@ java_defaults { "core/java/android/rolecontrollerservice/IRoleControllerService.aidl", ":keystore_aidl", "core/java/android/security/keymaster/IKeyAttestationApplicationIdProvider.aidl", + "core/java/android/service/appprediction/IPredictionService.aidl", "core/java/android/service/autofill/augmented/IAugmentedAutofillService.aidl", "core/java/android/service/autofill/augmented/IFillCallback.aidl", "core/java/android/service/autofill/IAutoFillService.aidl", diff --git a/api/system-current.txt b/api/system-current.txt index 891e76480640..667a356c08d6 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -86,6 +86,7 @@ package android { field public static final java.lang.String MANAGE_ACCESSIBILITY = "android.permission.MANAGE_ACCESSIBILITY"; field public static final java.lang.String MANAGE_ACTIVITY_STACKS = "android.permission.MANAGE_ACTIVITY_STACKS"; field public static final java.lang.String MANAGE_APP_OPS_RESTRICTIONS = "android.permission.MANAGE_APP_OPS_RESTRICTIONS"; + field public static final java.lang.String MANAGE_APP_PREDICTIONS = "android.permission.MANAGE_APP_PREDICTIONS"; field public static final java.lang.String MANAGE_APP_TOKENS = "android.permission.MANAGE_APP_TOKENS"; field public static final java.lang.String MANAGE_AUTO_FILL = "android.permission.MANAGE_AUTO_FILL"; field public static final java.lang.String MANAGE_CARRIER_OEM_UNLOCK_STATE = "android.permission.MANAGE_CARRIER_OEM_UNLOCK_STATE"; @@ -856,6 +857,87 @@ package android.app.job { } +package android.app.prediction { + + public final class AppPredictionContext implements android.os.Parcelable { + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public java.lang.String getPackageName(); + method public int getPredictedTargetCount(); + method public java.lang.String getUiSurface(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.prediction.AppPredictionContext> CREATOR; + } + + public static final class AppPredictionContext.Builder { + method public android.app.prediction.AppPredictionContext build(); + method public android.app.prediction.AppPredictionContext.Builder setExtras(android.os.Bundle); + method public android.app.prediction.AppPredictionContext.Builder setPredictedTargetCount(int); + method public android.app.prediction.AppPredictionContext.Builder setUiSurface(java.lang.String); + } + + public final class AppPredictionManager { + method public android.app.prediction.AppPredictor createAppPredictionSession(android.app.prediction.AppPredictionContext); + } + + public final class AppPredictionSessionId implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.prediction.AppPredictionSessionId> CREATOR; + } + + public final class AppPredictor { + method public void destroy(); + method public void notifyAppTargetEvent(android.app.prediction.AppTargetEvent); + method public void notifyLocationShown(java.lang.String, java.util.List<android.app.prediction.AppTargetId>); + method public void registerPredictionUpdates(java.util.concurrent.Executor, android.app.prediction.AppPredictor.Callback); + method public void requestPredictionUpdate(); + method public void sortTargets(java.util.List<android.app.prediction.AppTarget>, java.util.concurrent.Executor, java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>); + method public void unregisterPredictionUpdates(android.app.prediction.AppPredictor.Callback); + } + + public static abstract interface AppPredictor.Callback { + method public abstract void onTargetsAvailable(java.util.List<android.app.prediction.AppTarget>); + } + + public final class AppTarget implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getClassName(); + method public android.app.prediction.AppTargetId getId(); + method public java.lang.String getPackageName(); + method public int getRank(); + method public android.content.pm.ShortcutInfo getShortcutInfo(); + method public android.os.UserHandle getUser(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.prediction.AppTarget> CREATOR; + } + + public final class AppTargetEvent implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public java.lang.String getLaunchLocation(); + method public android.app.prediction.AppTarget getTarget(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int ACTION_DISMISS = 2; // 0x2 + field public static final int ACTION_LAUNCH = 1; // 0x1 + field public static final int ACTION_PIN = 3; // 0x3 + field public static final android.os.Parcelable.Creator<android.app.prediction.AppTargetEvent> CREATOR; + } + + public static final class AppTargetEvent.Builder { + ctor public AppTargetEvent.Builder(android.app.prediction.AppTarget, int); + method public android.app.prediction.AppTargetEvent build(); + method public android.app.prediction.AppTargetEvent.Builder setLaunchLocation(java.lang.String); + } + + public final class AppTargetId implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.prediction.AppTargetId> CREATOR; + } + +} + package android.app.role { public abstract interface OnRoleHoldersChangedListener { @@ -1042,6 +1124,7 @@ package android.content { method public abstract void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle, java.lang.String, android.os.Bundle); method public abstract void sendOrderedBroadcast(android.content.Intent, java.lang.String, android.os.Bundle, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); method public void startActivityAsUser(android.content.Intent, android.os.UserHandle); + field public static final java.lang.String APP_PREDICTION_SERVICE = "app_prediction"; field public static final java.lang.String BACKUP_SERVICE = "backup"; field public static final java.lang.String CONTEXTHUB_SERVICE = "contexthub"; field public static final java.lang.String EUICC_CARD_SERVICE = "euicc_card"; @@ -5146,6 +5229,24 @@ package android.security.keystore.recovery { } +package android.service.appprediction { + + public abstract class AppPredictionService extends android.app.Service { + ctor public AppPredictionService(); + method public abstract void onAppTargetEvent(android.app.prediction.AppPredictionSessionId, android.app.prediction.AppTargetEvent); + method public final android.os.IBinder onBind(android.content.Intent); + method public void onCreatePredictionSession(android.app.prediction.AppPredictionContext, android.app.prediction.AppPredictionSessionId); + method public void onDestroyPredictionSession(android.app.prediction.AppPredictionSessionId); + method public abstract void onLocationShown(android.app.prediction.AppPredictionSessionId, java.lang.String, java.util.List<android.app.prediction.AppTargetId>); + method public abstract void onRequestPredictionUpdate(android.app.prediction.AppPredictionSessionId); + method public abstract void onSortAppTargets(android.app.prediction.AppPredictionSessionId, java.util.List<android.app.prediction.AppTarget>, android.os.CancellationSignal, java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>); + method public void onStartPredictionUpdates(); + method public void onStopPredictionUpdates(); + method public final void updatePredictions(android.app.prediction.AppPredictionSessionId, java.util.List<android.app.prediction.AppTarget>); + } + +} + package android.service.autofill { public abstract class AutofillFieldClassificationService extends android.app.Service { diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 9ddf4bd0870c..6710233d251a 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -23,6 +23,7 @@ import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; import android.app.job.IJobScheduler; import android.app.job.JobScheduler; +import android.app.prediction.AppPredictionManager; import android.app.role.RoleManager; import android.app.slice.SliceManager; import android.app.timedetector.TimeDetector; @@ -1095,6 +1096,15 @@ final class SystemServiceRegistry { return null; }}); + registerService(Context.APP_PREDICTION_SERVICE, AppPredictionManager.class, + new CachedServiceFetcher<AppPredictionManager>() { + @Override + public AppPredictionManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + return new AppPredictionManager(ctx); + } + }); + registerService(Context.VR_SERVICE, VrManager.class, new CachedServiceFetcher<VrManager>() { @Override public VrManager createService(ContextImpl ctx) throws ServiceNotFoundException { diff --git a/core/java/android/app/prediction/AppPredictionContext.aidl b/core/java/android/app/prediction/AppPredictionContext.aidl new file mode 100644 index 000000000000..5767bf4dbc9a --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionContext.aidl @@ -0,0 +1,19 @@ +/** + * 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; + +parcelable AppPredictionContext; diff --git a/core/java/android/app/prediction/AppPredictionContext.java b/core/java/android/app/prediction/AppPredictionContext.java new file mode 100644 index 000000000000..87ccb660e5bd --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionContext.java @@ -0,0 +1,158 @@ +/* + * 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.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * TODO(b/111701043): Add java docs + * + * @hide + */ +@SystemApi +public final class AppPredictionContext implements Parcelable { + + private final int mPredictedTargetCount; + @NonNull + private final String mUiSurface; + @NonNull + private final String mPackageName; + @Nullable + private final Bundle mExtras; + + private AppPredictionContext(@NonNull String uiSurface, int numPredictedTargets, + @NonNull String packageName, @Nullable Bundle extras) { + mUiSurface = uiSurface; + mPredictedTargetCount = numPredictedTargets; + mPackageName = packageName; + mExtras = extras; + } + + private AppPredictionContext(Parcel parcel) { + mUiSurface = parcel.readString(); + mPredictedTargetCount = parcel.readInt(); + mPackageName = parcel.readString(); + mExtras = parcel.readBundle(); + } + + public String getUiSurface() { + return mUiSurface; + } + + public int getPredictedTargetCount() { + return mPredictedTargetCount; + } + + @NonNull + public String getPackageName() { + return mPackageName; + } + + @Nullable + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mUiSurface); + dest.writeInt(mPredictedTargetCount); + dest.writeString(mPackageName); + dest.writeBundle(mExtras); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AppPredictionContext> CREATOR = + new Parcelable.Creator<AppPredictionContext>() { + public AppPredictionContext createFromParcel(Parcel parcel) { + return new AppPredictionContext(parcel); + } + + public AppPredictionContext[] newArray(int size) { + return new AppPredictionContext[size]; + } + }; + + /** + * A builder for app prediction contexts. + * @hide + */ + @SystemApi + public static final class Builder { + + @NonNull + private final String mPackageName; + + private int mPredictedTargetCount; + @Nullable + private String mUiSurface; + @Nullable + private Bundle mExtras; + + /** + * @hide + */ + public Builder(@NonNull Context context) { + mPackageName = context.getPackageName(); + } + + + /** + * Sets the number of prediction targets as a hint. + */ + public Builder setPredictedTargetCount(int predictedTargetCount) { + mPredictedTargetCount = predictedTargetCount; + return this; + } + + /** + * Sets the UI surface. + */ + public Builder setUiSurface(@Nullable String uiSurface) { + mUiSurface = uiSurface; + return this; + } + + /** + * Sets the extras. + */ + public Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds a new context instance. + */ + public AppPredictionContext build() { + return new AppPredictionContext(mUiSurface, mPredictedTargetCount, mPackageName, + mExtras); + } + } +} diff --git a/core/java/android/app/prediction/AppPredictionManager.java b/core/java/android/app/prediction/AppPredictionManager.java new file mode 100644 index 000000000000..f8578d4533ef --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionManager.java @@ -0,0 +1,47 @@ +/* + * 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.NonNull; +import android.annotation.SystemApi; +import android.content.Context; + +import com.android.internal.util.Preconditions; + +/** + * TODO (b/111701043) : Add java doc + * @hide + */ +@SystemApi +public final class AppPredictionManager { + + private final Context mContext; + + /** + * @hide + */ + public AppPredictionManager(Context context) { + mContext = Preconditions.checkNotNull(context); + } + + /** + * Creates a new app prediction session. + */ + public AppPredictor createAppPredictionSession( + @NonNull AppPredictionContext predictionContext) { + return new AppPredictor(mContext, predictionContext); + } +} diff --git a/core/java/android/app/prediction/AppPredictionSessionId.aidl b/core/java/android/app/prediction/AppPredictionSessionId.aidl new file mode 100644 index 000000000000..e8295261ae2a --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionSessionId.aidl @@ -0,0 +1,19 @@ +/** + * 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; + +parcelable AppPredictionSessionId; diff --git a/core/java/android/app/prediction/AppPredictionSessionId.java b/core/java/android/app/prediction/AppPredictionSessionId.java new file mode 100644 index 000000000000..1d7308ec2404 --- /dev/null +++ b/core/java/android/app/prediction/AppPredictionSessionId.java @@ -0,0 +1,86 @@ +/* + * 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.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * TODO (b/111701043) : Add java doc + * + * @hide + */ +@SystemApi +public final class AppPredictionSessionId implements Parcelable { + + private final String mId; + + /** + * @hide + */ + public AppPredictionSessionId(@NonNull String id) { + mId = id; + } + + private AppPredictionSessionId(Parcel p) { + mId = p.readString(); + } + + @Override + public boolean equals(Object o) { + if (!getClass().equals(o != null ? o.getClass() : null)) return false; + + AppPredictionSessionId other = (AppPredictionSessionId) o; + return mId.equals(other.mId); + } + + @Override + public @NonNull String toString() { + return mId; + } + + @Override + public int hashCode() { + // Ensure that the id has a consistent hash + return mId.hashCode(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AppPredictionSessionId> CREATOR = + new Parcelable.Creator<AppPredictionSessionId>() { + public AppPredictionSessionId createFromParcel(Parcel parcel) { + return new AppPredictionSessionId(parcel); + } + + public AppPredictionSessionId[] newArray(int size) { + return new AppPredictionSessionId[size]; + } + }; +} diff --git a/core/java/android/app/prediction/AppPredictor.java b/core/java/android/app/prediction/AppPredictor.java new file mode 100644 index 000000000000..2ddbd08c7477 --- /dev/null +++ b/core/java/android/app/prediction/AppPredictor.java @@ -0,0 +1,257 @@ +/* + * 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.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; + +/** + * TODO (b/111701043) : Add java doc + * + * <p> + * Usage: <pre> {@code + * + * class MyActivity { + * private AppPredictor mClient + * + * void onCreate() { + * mClient = new AppPredictor(...) + * } + * + * void onStart() { + * mClient.requestPredictionUpdate(); + * } + * + * void onDestroy() { + * 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; + 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 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()); + try { + mPredictionManager.createPredictionSession(predictionContext, mSessionId); + } catch (RemoteException e) { + Log.e(TAG, "Failed to create predictor", e); + return; + } + + mCloseGuard.open("close"); + } + + /** + * Notifies the prediction service of an app target event. + */ + public void notifyAppTargetEvent(@NonNull AppTargetEvent event) { + try { + mPredictionManager.notifyAppTargetEvent(mSessionId, event); + } catch (RemoteException e) { + Log.e(TAG, "Failed to notify app target event", e); + } + } + + /** + * Notifies the prediction service when the targets in a launch location are shown to the user. + */ + public void notifyLocationShown(@NonNull String launchLocation, + @NonNull List<AppTargetId> targetIds) { + try { + mPredictionManager.notifyLocationShown(mSessionId, launchLocation, + new ParceledListSlice<>(targetIds)); + } catch (RemoteException e) { + Log.e(TAG, "Failed to notify location shown event", e); + } + } + + /** + * Requests the prediction service provide continuous updates of App predictions via the + * provided callback, until the given callback is unregistered. + * + * @see Callback#onTargetsAvailable(List) + */ + public void registerPredictionUpdates(@NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull AppPredictor.Callback callback) { + 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); + } + } + + /** + * Requests the prediction service to stop providing continuous updates to the provided + * callback until the callback is re-registered. + */ + public void unregisterPredictionUpdates(@NonNull AppPredictor.Callback callback) { + 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); + } + } + + /** + * Requests the prediction service to dispatch a new set of App predictions via the provided + * callback. + * + * @see Callback#onTargetsAvailable(List) + */ + public void requestPredictionUpdate() { + try { + mPredictionManager.requestPredictionUpdate(mSessionId); + } catch (RemoteException e) { + Log.e(TAG, "Failed to request prediction update", e); + } + } + + /** + * Returns a new list of AppTargets sorted based on prediction rank or {@code null} if the + * ranker is not available. + */ + @Nullable + public void sortTargets(@NonNull List<AppTarget> targets, + @NonNull Executor callbackExecutor, @NonNull Consumer<List<AppTarget>> callback) { + try { + mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice(targets), + new CallbackWrapper(callbackExecutor, callback)); + } catch (RemoteException e) { + Log.e(TAG, "Failed to sort targets", e); + } + } + + /** + * Destroys the client and unregisters the callback. Any method on this class after this call + * with throw {@link IllegalStateException}. + * + * TODO(b/111701043): Add state check in other methods. + */ + 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); + } + } + } + + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + destroy(); + } finally { + super.finalize(); + } + } + + /** + * 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); + } + } + } +} diff --git a/core/java/android/app/prediction/AppTarget.aidl b/core/java/android/app/prediction/AppTarget.aidl new file mode 100644 index 000000000000..e4e2bc2aaaf5 --- /dev/null +++ b/core/java/android/app/prediction/AppTarget.aidl @@ -0,0 +1,19 @@ +/** + * 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; + +parcelable AppTarget; diff --git a/core/java/android/app/prediction/AppTarget.java b/core/java/android/app/prediction/AppTarget.java new file mode 100644 index 000000000000..99c1c44866fe --- /dev/null +++ b/core/java/android/app/prediction/AppTarget.java @@ -0,0 +1,166 @@ +/* + * 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.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.pm.ShortcutInfo; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; + +import com.android.internal.util.Preconditions; + +/** + * A representation of a launchable target. + * @hide + */ +@SystemApi +public final class AppTarget implements Parcelable { + + private final AppTargetId mId; + private final String mPackageName; + private final String mClassName; + private final UserHandle mUser; + + private final ShortcutInfo mShortcutInfo; + + private int mRank; + + /** + * @hide + */ + public AppTarget(@NonNull AppTargetId id, @NonNull String packageName, + @Nullable String className, @NonNull UserHandle user) { + mId = id; + mShortcutInfo = null; + + mPackageName = Preconditions.checkNotNull(packageName); + mClassName = className; + mUser = Preconditions.checkNotNull(user); + } + + /** + * @hide + */ + public AppTarget(@NonNull AppTargetId id, @NonNull ShortcutInfo shortcutInfo) { + mId = id; + mShortcutInfo = Preconditions.checkNotNull(shortcutInfo); + + mPackageName = mShortcutInfo.getPackage(); + mUser = mShortcutInfo.getUserHandle(); + mClassName = null; + } + + private AppTarget(Parcel parcel) { + mId = parcel.readTypedObject(AppTargetId.CREATOR); + mShortcutInfo = parcel.readTypedObject(ShortcutInfo.CREATOR); + if (mShortcutInfo == null) { + mPackageName = parcel.readString(); + mClassName = parcel.readString(); + mUser = UserHandle.of(parcel.readInt()); + } else { + mPackageName = mShortcutInfo.getPackage(); + mUser = mShortcutInfo.getUserHandle(); + mClassName = null; + } + mRank = parcel.readInt(); + } + + /** + * Returns the target id. + */ + @NonNull + public AppTargetId getId() { + return mId; + } + + /** + * Returns the class name for the app target. + */ + @Nullable + public String getClassName() { + return mClassName; + } + + /** + * Returns the package name for the app target. + */ + @NonNull + public String getPackageName() { + return mPackageName; + } + + /** + * Returns the user for the app target. + */ + @NonNull + public UserHandle getUser() { + return mUser; + } + + /** + * Returns the shortcut info for the target. + */ + @Nullable + public ShortcutInfo getShortcutInfo() { + return mShortcutInfo; + } + + /** + * Sets the rank of the for the target. + * @hide + */ + public void setRank(int rank) { + mRank = rank; + } + + public int getRank() { + return mRank; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedObject(mId, flags); + dest.writeTypedObject(mShortcutInfo, flags); + if (mShortcutInfo == null) { + dest.writeString(mPackageName); + dest.writeString(mClassName); + dest.writeInt(mUser.getIdentifier()); + } + dest.writeInt(mRank); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AppTarget> CREATOR = + new Parcelable.Creator<AppTarget>() { + public AppTarget createFromParcel(Parcel parcel) { + return new AppTarget(parcel); + } + + public AppTarget[] newArray(int size) { + return new AppTarget[size]; + } + }; +} diff --git a/core/java/android/app/prediction/AppTargetEvent.aidl b/core/java/android/app/prediction/AppTargetEvent.aidl new file mode 100644 index 000000000000..ebed2dadbee6 --- /dev/null +++ b/core/java/android/app/prediction/AppTargetEvent.aidl @@ -0,0 +1,19 @@ +/** + * 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; + +parcelable AppTargetEvent; diff --git a/core/java/android/app/prediction/AppTargetEvent.java b/core/java/android/app/prediction/AppTargetEvent.java new file mode 100644 index 000000000000..18317e1bf99d --- /dev/null +++ b/core/java/android/app/prediction/AppTargetEvent.java @@ -0,0 +1,154 @@ +/* + * 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.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A representation of an app target event. + * @hide + */ +@SystemApi +public final class AppTargetEvent implements Parcelable { + + /** + * @hide + */ + @IntDef({ACTION_LAUNCH, ACTION_DISMISS, ACTION_PIN}) + @Retention(RetentionPolicy.SOURCE) + public @interface ActionType {} + + /** + * Event type constant indicating an app target has been launched. + */ + public static final int ACTION_LAUNCH = 1; + + /** + * Event type constant indicating an app target has been dismissed. + */ + public static final int ACTION_DISMISS = 2; + + /** + * Event type constant indicating an app target has been pinned. + */ + public static final int ACTION_PIN = 3; + + private final AppTarget mTarget; + private final String mLocation; + private final int mAction; + + private AppTargetEvent(@Nullable AppTarget target, @Nullable String location, + @ActionType int actionType) { + mTarget = target; + mLocation = location; + mAction = actionType; + } + + private AppTargetEvent(Parcel parcel) { + mTarget = parcel.readParcelable(null); + mLocation = parcel.readString(); + mAction = parcel.readInt(); + } + + /** + * Returns the app target. + */ + @Nullable + public AppTarget getTarget() { + return mTarget; + } + + /** + * Returns the launch location. + */ + @NonNull + public String getLaunchLocation() { + return mLocation; + } + + /** + * Returns the action type. + */ + @NonNull + public int getAction() { + return mAction; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mTarget, 0); + dest.writeString(mLocation); + dest.writeInt(mAction); + } + + /** + * @see Creator + */ + public static final Creator<AppTargetEvent> CREATOR = + new Creator<AppTargetEvent>() { + public AppTargetEvent createFromParcel(Parcel parcel) { + return new AppTargetEvent(parcel); + } + + public AppTargetEvent[] newArray(int size) { + return new AppTargetEvent[size]; + } + }; + + /** + * A builder for app target events. + * @hide + */ + @SystemApi + public static final class Builder { + private AppTarget mTarget; + private String mLocation; + private @ActionType int mAction; + + public Builder(@Nullable AppTarget target, @ActionType int actionType) { + mTarget = target; + mAction = actionType; + } + + /** + * Sets the launch location. + */ + public Builder setLaunchLocation(String location) { + mLocation = location; + return this; + } + + /** + * Builds a new event instance. + */ + public AppTargetEvent build() { + return new AppTargetEvent(mTarget, mLocation, mAction); + } + } +} diff --git a/core/java/android/app/prediction/AppTargetId.aidl b/core/java/android/app/prediction/AppTargetId.aidl new file mode 100644 index 000000000000..bf69eeaca691 --- /dev/null +++ b/core/java/android/app/prediction/AppTargetId.aidl @@ -0,0 +1,19 @@ +/** + * 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; + +parcelable AppTargetId; diff --git a/core/java/android/app/prediction/AppTargetId.java b/core/java/android/app/prediction/AppTargetId.java new file mode 100644 index 000000000000..0b8fb47377d2 --- /dev/null +++ b/core/java/android/app/prediction/AppTargetId.java @@ -0,0 +1,90 @@ +/* + * 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.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * The id for a prediction target. + * @hide + */ +@SystemApi +public final class AppTargetId implements Parcelable { + + @NonNull + private final String mId; + + /** + * @hide + */ + public AppTargetId(@NonNull String id) { + mId = id; + } + + private AppTargetId(Parcel parcel) { + mId = parcel.readString(); + } + + /** + * Returns the id. + * @hide + */ + @NonNull + public String getId() { + return mId; + } + + @Override + public boolean equals(Object o) { + if (!getClass().equals(o != null ? o.getClass() : null)) return false; + + AppTargetId other = (AppTargetId) o; + return mId.equals(other.mId); + } + + @Override + public int hashCode() { + // Ensure that the id has a consistent hash + return mId.hashCode(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + } + + /** + * @see Creator + */ + public static final Creator<AppTargetId> CREATOR = + new Creator<AppTargetId>() { + public AppTargetId createFromParcel(Parcel parcel) { + return new AppTargetId(parcel); + } + + public AppTargetId[] newArray(int size) { + return new AppTargetId[size]; + } + }; +} diff --git a/core/java/android/app/prediction/IPredictionCallback.aidl b/core/java/android/app/prediction/IPredictionCallback.aidl new file mode 100644 index 000000000000..f6f241e5560d --- /dev/null +++ b/core/java/android/app/prediction/IPredictionCallback.aidl @@ -0,0 +1,27 @@ +/* + * 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.content.pm.ParceledListSlice; + +/** + * @hide + */ +oneway interface IPredictionCallback { + + void onResult(in ParceledListSlice result); +} diff --git a/core/java/android/app/prediction/IPredictionManager.aidl b/core/java/android/app/prediction/IPredictionManager.aidl new file mode 100644 index 000000000000..114a1ffb0eb2 --- /dev/null +++ b/core/java/android/app/prediction/IPredictionManager.aidl @@ -0,0 +1,51 @@ +/* + * 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.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.IPredictionCallback; +import android.content.pm.ParceledListSlice; + +/** + * @hide + */ +interface IPredictionManager { + + void createPredictionSession(in AppPredictionContext context, + in AppPredictionSessionId sessionId); + + void notifyAppTargetEvent(in AppPredictionSessionId sessionId, in AppTargetEvent event); + + void notifyLocationShown(in AppPredictionSessionId sessionId, in String launchLocation, + in ParceledListSlice targetIds); + + void sortAppTargets(in AppPredictionSessionId sessionId, in ParceledListSlice targets, + in IPredictionCallback callback); + + void registerPredictionUpdates(in AppPredictionSessionId sessionId, + in IPredictionCallback callback); + + void unregisterPredictionUpdates(in AppPredictionSessionId sessionId, + in IPredictionCallback callback); + + void requestPredictionUpdate(in AppPredictionSessionId sessionId); + + void onDestroyPredictionSession(in AppPredictionSessionId sessionId); +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index eb7be6f8a6b0..1626d3028366 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3975,6 +3975,15 @@ public abstract class Context { public static final String CONTENT_CAPTURE_MANAGER_SERVICE = "content_capture"; /** + * Official published name of the app prediction service. + * + * @hide + * @see #getSystemService(String) + */ + @SystemApi + public static final String APP_PREDICTION_SERVICE = "app_prediction"; + + /** * Use with {@link #getSystemService(String)} to access the * {@link com.android.server.voiceinteraction.SoundTriggerService}. * diff --git a/core/java/android/service/appprediction/AppPredictionService.java b/core/java/android/service/appprediction/AppPredictionService.java new file mode 100644 index 000000000000..b77405affaec --- /dev/null +++ b/core/java/android/service/appprediction/AppPredictionService.java @@ -0,0 +1,322 @@ +/* + * 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.service.appprediction; + +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.SystemApi; +import android.app.Service; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppTargetId; +import android.app.prediction.IPredictionCallback; +import android.content.Intent; +import android.content.pm.ParceledListSlice; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.service.appprediction.IPredictionService.Stub; +import android.util.ArrayMap; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * TODO(b/111701043): Add java docs + * + * @hide + */ +@SystemApi +public abstract class AppPredictionService extends Service { + + private static final String TAG = "AppPredictionService"; + + /** + * The {@link Intent} that must be declared as handled by the service. + * TODO(b/111701043): Add any docs about permissions the service must hold + * + * @hide + */ + public static final String SERVICE_INTERFACE = + "android.service.appprediction.AppPredictionService"; + + private final ArrayMap<AppPredictionSessionId, ArrayList<CallbackWrapper>> mSessionCallbacks = + new ArrayMap<>(); + private Handler mHandler; + + private final IPredictionService mInterface = new Stub() { + + @Override + public void onCreatePredictionSession(AppPredictionContext context, + AppPredictionSessionId sessionId) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doCreatePredictionSession, + AppPredictionService.this, context, sessionId)); + } + + @Override + public void notifyAppTargetEvent(AppPredictionSessionId sessionId, AppTargetEvent event) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::onAppTargetEvent, + AppPredictionService.this, sessionId, event)); + } + + @Override + public void notifyLocationShown(AppPredictionSessionId sessionId, String launchLocation, + ParceledListSlice targetIds) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::onLocationShown, AppPredictionService.this, + sessionId, launchLocation, targetIds.getList())); + } + + @Override + public void sortAppTargets(AppPredictionSessionId sessionId, ParceledListSlice targets, + IPredictionCallback callback) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::onSortAppTargets, + AppPredictionService.this, sessionId, targets.getList(), null, + new CallbackWrapper(callback))); + } + + @Override + public void registerPredictionUpdates(AppPredictionSessionId sessionId, + IPredictionCallback callback) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doRegisterPredictionUpdates, + AppPredictionService.this, sessionId, callback)); + } + + @Override + public void unregisterPredictionUpdates(AppPredictionSessionId sessionId, + IPredictionCallback callback) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doUnregisterPredictionUpdates, + AppPredictionService.this, sessionId, callback)); + } + + @Override + public void requestPredictionUpdate(AppPredictionSessionId sessionId) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doRequestPredictionUpdate, + AppPredictionService.this, sessionId)); + } + + @Override + public void onDestroyPredictionSession(AppPredictionSessionId sessionId) { + mHandler.sendMessage( + obtainMessage(AppPredictionService::doDestroyPredictionSession, + AppPredictionService.this, sessionId)); + } + }; + + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mHandler = new Handler(Looper.getMainLooper(), null, true); + } + + @Override + public final IBinder onBind(Intent intent) { + return mInterface.asBinder(); + } + + /** + * Called by a client app to indicate a target launch + */ + @MainThread + public abstract void onAppTargetEvent(@NonNull AppPredictionSessionId sessionId, + @NonNull AppTargetEvent event); + + /** + * Called by a client app to indication a particular location has been shown to the user. + */ + @MainThread + public abstract void onLocationShown(@NonNull AppPredictionSessionId sessionId, + @NonNull String launchLocation, @NonNull List<AppTargetId> targetIds); + + private void doCreatePredictionSession(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) { + mSessionCallbacks.put(sessionId, new ArrayList<>()); + onCreatePredictionSession(context, sessionId); + } + + /** + * Creates a new interaction session. + * + * @param context interaction context + * @param sessionId the session's Id + */ + public void onCreatePredictionSession(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) {} + + /** + * Called by the client app to request sorting of targets based on prediction rank. + * TODO(b/111701043): Implement CancellationSignal so caller can cancel a long running request + */ + @MainThread + public abstract void onSortAppTargets(@NonNull AppPredictionSessionId sessionId, + @NonNull List<AppTarget> targets, @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer<List<AppTarget>> callback); + + private void doRegisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + final ArrayList<CallbackWrapper> 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)); + if (callbacks.size() == 1) { + onStartPredictionUpdates(); + } + } + } + + /** + * Called when any continuous prediction callback is registered. + */ + @MainThread + public void onStartPredictionUpdates() {} + + private void doUnregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + final ArrayList<CallbackWrapper> 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); + if (wrapper != null) { + callbacks.remove(wrapper); + if (callbacks.isEmpty()) { + onStopPredictionUpdates(); + } + } + } + + /** + * Called when there are no longer any continuous prediction callbacks registered. + */ + @MainThread + public void onStopPredictionUpdates() {} + + private void doRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) { + final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); + if (callbacks != null && !callbacks.isEmpty()) { + onRequestPredictionUpdate(sessionId); + } + } + + /** + * Called by the client app to request target predictions. This method is only called if there + * are one or more prediction callbacks registered. + * TODO(b/111701043): Add java docs + * + * @see #updatePredictions(AppPredictionSessionId, List) + */ + @MainThread + public abstract void onRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId); + + private void doDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) { + mSessionCallbacks.remove(sessionId); + onDestroyPredictionSession(sessionId); + } + + /** + * Destroys the interaction session. + * + * @param sessionId the id of the session to destroy + */ + @MainThread + public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {} + + /** + * Used by the prediction factory to send back results the client app. The can be called + * in response to {@link #onRequestPredictionUpdate(AppPredictionSessionId)} or proactively as + * a result of changes in predictions. + */ + public final void updatePredictions(@NonNull AppPredictionSessionId sessionId, + @NonNull List<AppTarget> targets) { + List<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId); + if (callbacks != null) { + for (CallbackWrapper callback : callbacks) { + callback.accept(targets); + } + } + } + + /** + * Finds the callback wrapper for the given callback. + */ + private CallbackWrapper findCallbackWrapper(ArrayList<CallbackWrapper> callbacks, + IPredictionCallback callback) { + for (int i = callbacks.size() - 1; i >= 0; i--) { + if (callbacks.get(i).isCallback(callback)) { + return callbacks.get(i); + } + } + return null; + } + + private static final class CallbackWrapper implements Consumer<List<AppTarget>>, + IBinder.DeathRecipient { + + private IPredictionCallback mCallback; + + CallbackWrapper(IPredictionCallback callback) { + mCallback = callback; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death: " + e); + } + } + + public boolean isCallback(@NonNull IPredictionCallback callback) { + return mCallback.equals(callback); + } + + @Override + public void accept(List<AppTarget> ts) { + try { + if (mCallback != null) { + mCallback.onResult(new ParceledListSlice(ts)); + } + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result:" + e); + } + } + + @Override + public void binderDied() { + mCallback = null; + } + } +} diff --git a/core/java/android/service/appprediction/IPredictionService.aidl b/core/java/android/service/appprediction/IPredictionService.aidl new file mode 100644 index 000000000000..3a6d1666f4b9 --- /dev/null +++ b/core/java/android/service/appprediction/IPredictionService.aidl @@ -0,0 +1,53 @@ +/* + * 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.service.appprediction; + +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.IPredictionCallback; +import android.content.pm.ParceledListSlice; + +/** + * Interface from the system to a prediction service. + * + * @hide + */ +oneway interface IPredictionService { + + void onCreatePredictionSession(in AppPredictionContext context, + in AppPredictionSessionId sessionId); + + void notifyAppTargetEvent(in AppPredictionSessionId sessionId, in AppTargetEvent event); + + void notifyLocationShown(in AppPredictionSessionId sessionId, in String launchLocation, + in ParceledListSlice targetIds); + + void sortAppTargets(in AppPredictionSessionId sessionId, in ParceledListSlice targets, + in IPredictionCallback callback); + + void registerPredictionUpdates(in AppPredictionSessionId sessionId, + in IPredictionCallback callback); + + void unregisterPredictionUpdates(in AppPredictionSessionId sessionId, + in IPredictionCallback callback); + + void requestPredictionUpdate(in AppPredictionSessionId sessionId); + + void onDestroyPredictionSession(in AppPredictionSessionId sessionId); +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 449a7b3305ad..3fa7cfbc2a26 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4254,6 +4254,11 @@ <permission android:name="android.permission.MANAGE_CONTENT_CAPTURE" android:protectionLevel="signature" /> + <!-- @SystemApi Allows an application to manage the app predictions service. + @hide <p>Not for use by third-party applications.</p> --> + <permission android:name="android.permission.MANAGE_APP_PREDICTIONS" + android:protectionLevel="signature" /> + <!-- Allows an app to set the theme overlay in /vendor/overlay being used. @hide <p>Not for use by third-party applications.</p> --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 140225e62850..22bff7d27159 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3419,6 +3419,12 @@ --> <string name="config_defaultAugmentedAutofillService" translatable="false"></string> + <!-- The package name for the system's app prediction service. + This service must be trusted, as it can be activated without explicit consent of the user. + Example: "com.android.intelligence/.AppPredictionService" + --> + <string name="config_defaultAppPredictionService" translatable="false"></string> + <!-- Whether the device uses the default focus highlight when focus state isn't specified. --> <bool name="config_useDefaultFocusHighlight">true</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f8c9037d5ab0..d9dbb100f1b6 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3275,6 +3275,7 @@ <java-symbol type="string" name="config_defaultWellbeingPackage" /> <java-symbol type="string" name="config_defaultContentCaptureService" /> <java-symbol type="string" name="config_defaultAugmentedAutofillService" /> + <java-symbol type="string" name="config_defaultAppPredictionService" /> <java-symbol type="string" name="notification_channel_foreground_service" /> <java-symbol type="string" name="foreground_service_app_in_background" /> diff --git a/packages/AppPredictionLib/Android.bp b/packages/AppPredictionLib/Android.bp new file mode 100644 index 000000000000..e0f4dedc9368 --- /dev/null +++ b/packages/AppPredictionLib/Android.bp @@ -0,0 +1,24 @@ +// 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. + +android_library { + name: "app_prediction", + + sdk_version: "system_current", + min_sdk_version: "system_current", + + srcs: [ + "src/**/*.java", + ], +} diff --git a/packages/AppPredictionLib/AndroidManifest.xml b/packages/AppPredictionLib/AndroidManifest.xml new file mode 100644 index 000000000000..b99278881281 --- /dev/null +++ b/packages/AppPredictionLib/AndroidManifest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.app.prediction"> +</manifest> diff --git a/packages/AppPredictionLib/src/com/android/app/prediction/Constants.java b/packages/AppPredictionLib/src/com/android/app/prediction/Constants.java new file mode 100644 index 000000000000..0993c9a13f99 --- /dev/null +++ b/packages/AppPredictionLib/src/com/android/app/prediction/Constants.java @@ -0,0 +1,71 @@ +/* + * 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 com.android.app.prediction; + +/** + * Constants to be used with {@link android.app.prediction.AppPredictor}. + */ +public class Constants { + + /** + * UI surface for predictions displayed on the user's home screen + */ + public static final String UI_SURFACE_HOME = "home"; + + /** + * UI surface for predictions displayed on the recents/task switcher view + */ + public static final String UI_SURFACE_RECENTS = "recents"; + + /** + * UI surface for predictions displayed on the share sheet. + */ + public static final String UI_SURFACE_SHARE = "share"; + + /** + * Location constant when an app target or shortcut is started from the apps list + */ + public static final String LAUNCH_LOCATION_APPS_LIST = "apps_list"; + + /** + * Location constant when an app target or shortcut is started from the user's home screen + */ + public static final String LAUNCH_LOCATION_APPS_HOME = "home"; + + /** + * Location constant when an app target or shortcut is started from task switcher + */ + public static final String LAUNCH_LOCATION_APPS_RECENTS = "recents"; + + /** + * Location constant when an app target or shortcut is started in the share sheet while it is + * in collapsed state (showing a limited set of result). + */ + public static final String LAUNCH_LOCATION_APPS_SHARE_COLLAPSED = "share_collapsed"; + + /** + * Location constant when an app target or shortcut is started in the share sheet while it is + * in expended state and showing all the results. + */ + public static final String LAUNCH_LOCATION_APPS_SHARE_EXPANDED = "shared_expanded"; + + /** + * Location constant when an app target or shortcut is started in the share sheet when the + * target is displayed as a placeholder for an deprecated object. + */ + public static final String LAUNCH_LOCATION_APPS_SHARE_LEGACY = "share_legacy"; +} diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index bff2c844bb48..562d44d6acac 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -134,6 +134,7 @@ <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" /> <uses-permission android:name="android.permission.MANAGE_AUTO_FILL" /> <uses-permission android:name="android.permission.MANAGE_CONTENT_CAPTURE" /> + <uses-permission android:name="android.permission.MANAGE_APP_PREDICTIONS" /> <uses-permission android:name="android.permission.NETWORK_SETTINGS" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.SET_TIME" /> diff --git a/services/Android.bp b/services/Android.bp index 01734f4051d3..8e96cccd4464 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -17,6 +17,7 @@ java_library { static_libs: [ "services.core", "services.accessibility", + "services.appprediction", "services.appwidget", "services.autofill", "services.backup", diff --git a/services/appprediction/Android.bp b/services/appprediction/Android.bp new file mode 100644 index 000000000000..a7be58783aab --- /dev/null +++ b/services/appprediction/Android.bp @@ -0,0 +1,5 @@ +java_library_static { + name: "services.appprediction", + srcs: ["java/**/*.java"], + libs: ["services.core"], +} diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java new file mode 100644 index 000000000000..833eaa06d759 --- /dev/null +++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java @@ -0,0 +1,156 @@ +/* + * 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 com.android.server.appprediction; + +import static android.Manifest.permission.MANAGE_APP_PREDICTIONS; +import static android.content.Context.APP_PREDICTION_SERVICE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.IPredictionCallback; +import android.app.prediction.IPredictionManager; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.Binder; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.os.UserHandle; + +import com.android.server.infra.AbstractMasterSystemService; +import com.android.server.infra.FrameworkResourcesServiceNameResolver; + +import java.io.FileDescriptor; +import java.util.function.Consumer; + +/** + * A service used to predict app and shortcut usage. + * + * <p>The data collected by this service can be analyzed and combined with other sources to provide + * predictions in different areas of the system such as Launcher and Share sheet. + */ +public class AppPredictionManagerService extends + AbstractMasterSystemService<AppPredictionManagerService, AppPredictionPerUserService> { + + private static final String TAG = AppPredictionManagerService.class.getSimpleName(); + + private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes + + public AppPredictionManagerService(Context context) { + super(context, new FrameworkResourcesServiceNameResolver(context, + com.android.internal.R.string.config_defaultAppPredictionService), null); + } + + @Override + protected AppPredictionPerUserService newServiceLocked(int resolvedUserId, boolean disabled) { + return new AppPredictionPerUserService(this, mLock, resolvedUserId); + } + + @Override + public void onStart() { + publishBinderService(APP_PREDICTION_SERVICE, new PredictionManagerServiceStub()); + } + + @Override + protected void enforceCallingPermissionForManagement() { + getContext().enforceCallingPermission(MANAGE_APP_PREDICTIONS, TAG); + } + + @Override + protected int getMaximumTemporaryServiceDurationMs() { + return MAX_TEMP_SERVICE_DURATION_MS; + } + + private class PredictionManagerServiceStub extends IPredictionManager.Stub { + + @Override + public void createPredictionSession(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) { + runForUserLocked((service) -> + service.onCreatePredictionSessionLocked(context, sessionId)); + } + + @Override + public void notifyAppTargetEvent(@NonNull AppPredictionSessionId sessionId, + @NonNull AppTargetEvent event) { + runForUserLocked((service) -> service.notifyAppTargetEventLocked(sessionId, event)); + } + + @Override + public void notifyLocationShown(@NonNull AppPredictionSessionId sessionId, + @NonNull String launchLocation, @NonNull ParceledListSlice targetIds) { + runForUserLocked((service) -> + service.notifyLocationShownLocked(sessionId, launchLocation, targetIds)); + } + + @Override + public void sortAppTargets(@NonNull AppPredictionSessionId sessionId, + @NonNull ParceledListSlice targets, + IPredictionCallback callback) { + runForUserLocked((service) -> + service.sortAppTargetsLocked(sessionId, targets, callback)); + } + + @Override + public void registerPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + runForUserLocked((service) -> + service.registerPredictionUpdatesLocked(sessionId, callback)); + } + + public void unregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + runForUserLocked((service) -> + service.unregisterPredictionUpdatesLocked(sessionId, callback)); + } + + @Override + public void requestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) { + runForUserLocked((service) -> service.requestPredictionUpdateLocked(sessionId)); + } + + @Override + public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) { + runForUserLocked((service) -> service.onDestroyPredictionSessionLocked(sessionId)); + } + + public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, + @Nullable FileDescriptor err, + @NonNull String[] args, @Nullable ShellCallback callback, + @NonNull ResultReceiver resultReceiver) throws RemoteException { + new AppPredictionManagerServiceShellCommand(AppPredictionManagerService.this) + .exec(this, in, out, err, args, callback, resultReceiver); + } + + private void runForUserLocked(@NonNull Consumer<AppPredictionPerUserService> c) { + final int userId = UserHandle.getCallingUserId(); + // TODO(b/111701043): Determine what permission model we want for this + long origId = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + final AppPredictionPerUserService service = getServiceForUserLocked(userId); + c.accept(service); + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + } +} diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerServiceShellCommand.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerServiceShellCommand.java new file mode 100644 index 000000000000..ed7cc67aa46f --- /dev/null +++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerServiceShellCommand.java @@ -0,0 +1,83 @@ +/* + * 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 com.android.server.appprediction; + +import android.annotation.NonNull; +import android.os.ShellCommand; + +import java.io.PrintWriter; + +/** + * The shell command implementation for the AppPredictionManagerService. + */ +public class AppPredictionManagerServiceShellCommand extends ShellCommand { + + private static final String TAG = + AppPredictionManagerServiceShellCommand.class.getSimpleName(); + + private final AppPredictionManagerService mService; + + public AppPredictionManagerServiceShellCommand(@NonNull AppPredictionManagerService service) { + mService = service; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + final PrintWriter pw = getOutPrintWriter(); + switch (cmd) { + case "set": { + final String what = getNextArgRequired(); + switch (what) { + case "temporary-service": { + final int userId = Integer.parseInt(getNextArgRequired()); + String serviceName = getNextArg(); + if (serviceName == null) { + mService.resetTemporaryService(userId); + return 0; + } + final int duration = Integer.parseInt(getNextArgRequired()); + mService.setTemporaryService(userId, serviceName, duration); + pw.println("AppPredictionService temporarily set to " + serviceName + + " for " + duration + "ms"); + break; + } + } + } + break; + default: + return handleDefaultCommands(cmd); + } + return 0; + } + + @Override + public void onHelp() { + try (PrintWriter pw = getOutPrintWriter()) { + pw.println("AppPredictionManagerService commands:"); + pw.println(" help"); + pw.println(" Prints this help text."); + pw.println(""); + pw.println(" set temporary-service USER_ID [COMPONENT_NAME DURATION]"); + pw.println(" Temporarily (for DURATION ms) changes the service implemtation."); + pw.println(" To reset, call with just the USER_ID argument."); + pw.println(""); + } + } +} diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java new file mode 100644 index 000000000000..24d592c38afb --- /dev/null +++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java @@ -0,0 +1,218 @@ +/* + * 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 com.android.server.appprediction; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppGlobals; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.IPredictionCallback; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ParceledListSlice; +import android.content.pm.ServiceInfo; +import android.os.RemoteException; +import android.service.appprediction.AppPredictionService; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.infra.AbstractPerUserSystemService; + +/** + * Per-user instance of {@link AppPredictionManagerService}. + */ +public class AppPredictionPerUserService extends + AbstractPerUserSystemService<AppPredictionPerUserService, AppPredictionManagerService> + implements RemoteAppPredictionService.RemoteAppPredictionServiceCallbacks { + + private static final String TAG = AppPredictionPerUserService.class.getSimpleName(); + + @Nullable + @GuardedBy("mLock") + private RemoteAppPredictionService mRemoteService; + + protected AppPredictionPerUserService(AppPredictionManagerService master, + Object lock, int userId) { + super(master, lock, userId); + } + + @Override // from PerUserSystemService + protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) + throws NameNotFoundException { + + ServiceInfo si; + try { + si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException e) { + throw new NameNotFoundException("Could not get service for " + serviceComponent); + } + // TODO(b/111701043): must check that either the service is from a system component, + // or it matches a service set by shell cmd (so it can be used on CTS tests and when + // OEMs are implementing the real service and also verify the proper permissions + return si; + } + + @GuardedBy("mLock") + @Override // from PerUserSystemService + protected boolean updateLocked(boolean disabled) { + final boolean enabledChanged = super.updateLocked(disabled); + if (enabledChanged) { + if (!isEnabledLocked()) { + // Clear the remote service for the next call + mRemoteService = null; + } + } + return enabledChanged; + } + + /** + * Notifies the service of a new prediction session. + */ + @GuardedBy("mLock") + public void onCreatePredictionSessionLocked(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.onCreatePredictionSession(context, sessionId); + } + } + + /** + * Records an app target event to the service. + */ + @GuardedBy("mLock") + public void notifyAppTargetEventLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull AppTargetEvent event) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.notifyAppTargetEvent(sessionId, event); + } + } + + /** + * Records when a launch location is shown. + */ + @GuardedBy("mLock") + public void notifyLocationShownLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull String launchLocation, @NonNull ParceledListSlice targetIds) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.notifyLocationShown(sessionId, launchLocation, targetIds); + } + } + + /** + * Requests the service to sort a list of apps or shortcuts. + */ + @GuardedBy("mLock") + public void sortAppTargetsLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull ParceledListSlice targets, @NonNull IPredictionCallback callback) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.sortAppTargets(sessionId, targets, callback); + } + } + + /** + * Registers a callback for continuous updates of predicted apps or shortcuts. + */ + @GuardedBy("mLock") + public void registerPredictionUpdatesLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.registerPredictionUpdates(sessionId, callback); + } + } + + /** + * Unregisters a callback for continuous updates of predicted apps or shortcuts. + */ + @GuardedBy("mLock") + public void unregisterPredictionUpdatesLocked(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.unregisterPredictionUpdates(sessionId, callback); + } + } + + /** + * Requests a new set of predicted apps or shortcuts. + */ + @GuardedBy("mLock") + public void requestPredictionUpdateLocked(@NonNull AppPredictionSessionId sessionId) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.requestPredictionUpdate(sessionId); + } + } + + /** + * Notifies the service of the end of an existing prediction session. + */ + @GuardedBy("mLock") + public void onDestroyPredictionSessionLocked(@NonNull AppPredictionSessionId sessionId) { + final RemoteAppPredictionService service = getRemoteServiceLocked(); + if (service != null) { + service.onDestroyPredictionSession(sessionId); + } + } + + @Override + public void onFailureOrTimeout(boolean timedOut) { + if (isDebug()) { + Slog.d(TAG, "onFailureOrTimeout(): timed out=" + timedOut); + } + + // Do nothing, we are just proxying to the prediction service + } + + @Override + public void onServiceDied(RemoteAppPredictionService service) { + if (isDebug()) { + Slog.d(TAG, "onServiceDied():"); + } + + // Do nothing, we are just proxying to the prediction service + } + + @GuardedBy("mLock") + @Nullable + private RemoteAppPredictionService getRemoteServiceLocked() { + if (mRemoteService == null) { + final String serviceName = getComponentNameLocked(); + if (serviceName == null) { + if (mMaster.verbose) { + Slog.v(TAG, "getRemoteServiceLocked(): not set"); + } + return null; + } + ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName); + + mRemoteService = new RemoteAppPredictionService(getContext(), + AppPredictionService.SERVICE_INTERFACE, serviceComponent, mUserId, this, + mMaster.isBindInstantServiceAllowed(), mMaster.verbose); + } + + return mRemoteService; + } +} diff --git a/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java new file mode 100644 index 000000000000..45ea86f9e74e --- /dev/null +++ b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java @@ -0,0 +1,142 @@ +/* + * 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 com.android.server.appprediction; + +import android.annotation.NonNull; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionSessionId; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.IPredictionCallback; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.os.IBinder; +import android.service.appprediction.IPredictionService; +import android.text.format.DateUtils; + +import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService; + + +/** + * Proxy to the {@link android.service.appprediction.AppPredictionService} implemention in another + * process. + */ +public class RemoteAppPredictionService extends + AbstractMultiplePendingRequestsRemoteService<RemoteAppPredictionService, + IPredictionService> { + + private static final String TAG = "RemoteAppPredictionService"; + + private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS; + + public RemoteAppPredictionService(Context context, String serviceInterface, + ComponentName componentName, int userId, + RemoteAppPredictionServiceCallbacks callback, boolean bindInstantServiceAllowed, + boolean verbose) { + super(context, serviceInterface, componentName, userId, callback, bindInstantServiceAllowed, + verbose, /* initialCapacity= */ 1); + } + + @Override + protected IPredictionService getServiceInterface(IBinder service) { + return IPredictionService.Stub.asInterface(service); + } + + @Override + protected long getTimeoutIdleBindMillis() { + return PERMANENT_BOUND_TIMEOUT_MS; + } + + @Override + protected long getRemoteRequestMillis() { + return TIMEOUT_REMOTE_REQUEST_MILLIS; + } + + /** + * Notifies the service of a new prediction session. + */ + public void onCreatePredictionSession(@NonNull AppPredictionContext context, + @NonNull AppPredictionSessionId sessionId) { + scheduleAsyncRequest((s) -> s.onCreatePredictionSession(context, sessionId)); + } + + /** + * Records an app target event to the service. + */ + public void notifyAppTargetEvent(@NonNull AppPredictionSessionId sessionId, + @NonNull AppTargetEvent event) { + scheduleAsyncRequest((s) -> s.notifyAppTargetEvent(sessionId, event)); + } + + /** + * Records when a launch location is shown. + */ + public void notifyLocationShown(@NonNull AppPredictionSessionId sessionId, + @NonNull String launchLocation, @NonNull ParceledListSlice targetIds) { + scheduleAsyncRequest((s) -> s.notifyLocationShown(sessionId, launchLocation, targetIds)); + } + + /** + * Requests the service to sort a list of apps or shortcuts. + */ + public void sortAppTargets(@NonNull AppPredictionSessionId sessionId, + @NonNull ParceledListSlice targets, @NonNull IPredictionCallback callback) { + scheduleAsyncRequest((s) -> s.sortAppTargets(sessionId, targets, callback)); + } + + + /** + * Registers a callback for continuous updates of predicted apps or shortcuts. + */ + public void registerPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + scheduleAsyncRequest((s) -> s.registerPredictionUpdates(sessionId, callback)); + } + + /** + * Unregisters a callback for continuous updates of predicted apps or shortcuts. + */ + public void unregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, + @NonNull IPredictionCallback callback) { + scheduleAsyncRequest((s) -> s.unregisterPredictionUpdates(sessionId, callback)); + } + + /** + * Requests a new set of predicted apps or shortcuts. + */ + public void requestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) { + scheduleAsyncRequest((s) -> s.requestPredictionUpdate(sessionId)); + } + + /** + * Notifies the service of the end of an existing prediction session. + */ + public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) { + scheduleAsyncRequest((s) -> s.onDestroyPredictionSession(sessionId)); + } + + /** + * Failure callback + */ + public interface RemoteAppPredictionServiceCallbacks + extends VultureCallback<RemoteAppPredictionService> { + + /** + * Notifies a the failure or timeout of a remote call. + */ + void onFailureOrTimeout(boolean timedOut); + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 4326c39c43a2..ced4261821d9 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -260,6 +260,8 @@ public final class SystemServer { "com.android.server.accessibility.AccessibilityManagerService$Lifecycle"; private static final String ADB_SERVICE_CLASS = "com.android.server.adb.AdbService$Lifecycle"; + private static final String APP_PREDICTION_MANAGER_SERVICE_CLASS = + "com.android.server.appprediction.AppPredictionManagerService"; private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst"; @@ -1153,6 +1155,11 @@ public final class SystemServer { startContentCaptureService(context); + // App prediction manager service + traceBeginAndSlog("StartAppPredictionService"); + mSystemServiceManager.startService(APP_PREDICTION_MANAGER_SERVICE_CLASS); + traceEnd(); + // NOTE: ClipboardService indirectly depends on IntelligenceService traceBeginAndSlog("StartClipboardService"); mSystemServiceManager.startService(ClipboardService.class); |