diff options
31 files changed, 1694 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp index 687d2d72395f..4c7db0d8b873 100644 --- a/Android.bp +++ b/Android.bp @@ -104,6 +104,9 @@ 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/contentsuggestions/IClassificationsCallback.aidl", + "core/java/android/app/contentsuggestions/IContentSuggestionsManager.aidl", + "core/java/android/app/contentsuggestions/ISelectionsCallback.aidl", "core/java/android/app/prediction/IPredictionCallback.aidl", "core/java/android/app/prediction/IPredictionManager.aidl", "core/java/android/app/role/IOnRoleHoldersChangedListener.aidl", @@ -285,6 +288,7 @@ java_defaults { "core/java/android/service/carrier/ICarrierService.aidl", "core/java/android/service/carrier/ICarrierMessagingCallback.aidl", "core/java/android/service/carrier/ICarrierMessagingService.aidl", + "core/java/android/service/contentsuggestions/IContentSuggestionsService.aidl", "core/java/android/service/euicc/IDeleteSubscriptionCallback.aidl", "core/java/android/service/euicc/IDownloadSubscriptionCallback.aidl", "core/java/android/service/euicc/IEraseSubscriptionsCallback.aidl", diff --git a/api/system-current.txt b/api/system-current.txt index 667a356c08d6..d92a7ed7646f 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -92,6 +92,7 @@ package android { field public static final java.lang.String MANAGE_CARRIER_OEM_UNLOCK_STATE = "android.permission.MANAGE_CARRIER_OEM_UNLOCK_STATE"; field public static final java.lang.String MANAGE_CA_CERTIFICATES = "android.permission.MANAGE_CA_CERTIFICATES"; field public static final java.lang.String MANAGE_CONTENT_CAPTURE = "android.permission.MANAGE_CONTENT_CAPTURE"; + field public static final java.lang.String MANAGE_CONTENT_SUGGESTIONS = "android.permission.MANAGE_CONTENT_SUGGESTIONS"; field public static final java.lang.String MANAGE_DEBUGGING = "android.permission.MANAGE_DEBUGGING"; field public static final java.lang.String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS"; field public static final java.lang.String MANAGE_IPSEC_TUNNELS = "android.permission.MANAGE_IPSEC_TUNNELS"; @@ -849,6 +850,73 @@ package android.app.backup { } +package android.app.contentsuggestions { + + public final class ClassificationsRequest implements android.os.Parcelable { + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public java.util.List<android.app.contentsuggestions.ContentSelection> getSelections(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.contentsuggestions.ClassificationsRequest> CREATOR; + } + + public static final class ClassificationsRequest.Builder { + ctor public ClassificationsRequest.Builder(java.util.List<android.app.contentsuggestions.ContentSelection>); + method public android.app.contentsuggestions.ClassificationsRequest build(); + method public android.app.contentsuggestions.ClassificationsRequest.Builder setExtras(android.os.Bundle); + } + + public final class ContentClassification implements android.os.Parcelable { + ctor public ContentClassification(java.lang.String, android.os.Bundle); + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public java.lang.String getId(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.contentsuggestions.ContentClassification> CREATOR; + } + + public final class ContentSelection implements android.os.Parcelable { + ctor public ContentSelection(java.lang.String, android.os.Bundle); + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public java.lang.String getId(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.contentsuggestions.ContentSelection> CREATOR; + } + + public final class ContentSuggestionsManager { + method public void classifyContentSelections(android.app.contentsuggestions.ClassificationsRequest, java.util.concurrent.Executor, android.app.contentsuggestions.ContentSuggestionsManager.ClassificationsCallback); + method public void notifyInteraction(java.lang.String, android.os.Bundle); + method public void provideContextImage(int, android.os.Bundle); + method public void suggestContentSelections(android.app.contentsuggestions.SelectionsRequest, java.util.concurrent.Executor, android.app.contentsuggestions.ContentSuggestionsManager.SelectionsCallback); + } + + public static abstract interface ContentSuggestionsManager.ClassificationsCallback { + method public abstract void onContentClassificationsAvailable(int, java.util.List<android.app.contentsuggestions.ContentClassification>); + } + + public static abstract interface ContentSuggestionsManager.SelectionsCallback { + method public abstract void onContentSelectionsAvailable(int, java.util.List<android.app.contentsuggestions.ContentSelection>); + } + + public final class SelectionsRequest implements android.os.Parcelable { + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public android.graphics.Point getInterestPoint(); + method public int getTaskId(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.contentsuggestions.SelectionsRequest> CREATOR; + } + + public static final class SelectionsRequest.Builder { + ctor public SelectionsRequest.Builder(int); + method public android.app.contentsuggestions.SelectionsRequest build(); + method public android.app.contentsuggestions.SelectionsRequest.Builder setExtras(android.os.Bundle); + method public android.app.contentsuggestions.SelectionsRequest.Builder setInterestPoint(android.graphics.Point); + } + +} + package android.app.job { public abstract class JobScheduler { @@ -1126,6 +1194,7 @@ package android.content { 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 CONTENT_SUGGESTIONS_SERVICE = "content_suggestions"; field public static final java.lang.String CONTEXTHUB_SERVICE = "contexthub"; field public static final java.lang.String EUICC_CARD_SERVICE = "euicc_card"; field public static final java.lang.String HDMI_CONTROL_SERVICE = "hdmi_control"; @@ -5374,6 +5443,19 @@ package android.service.contentcapture { } +package android.service.contentsuggestions { + + public abstract class ContentSuggestionsService extends android.app.Service { + ctor public ContentSuggestionsService(); + method public abstract void classifyContentSelections(android.app.contentsuggestions.ClassificationsRequest, android.app.contentsuggestions.ContentSuggestionsManager.ClassificationsCallback); + method public abstract void notifyInteraction(java.lang.String, android.os.Bundle); + method public abstract void processContextImage(int, android.graphics.Bitmap, android.os.Bundle); + method public abstract void suggestContentSelections(android.app.contentsuggestions.SelectionsRequest, android.app.contentsuggestions.ContentSuggestionsManager.SelectionsCallback); + field public static final java.lang.String SERVICE_INTERFACE = "android.service.contentsuggestions.ContentSuggestionsService"; + } + +} + package android.service.euicc { public final class DownloadSubscriptionResult implements android.os.Parcelable { diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 6710233d251a..40012613ddb3 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -21,6 +21,8 @@ import android.accounts.IAccountManager; import android.app.ContextImpl.ServiceInitializationState; import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; +import android.app.contentsuggestions.ContentSuggestionsManager; +import android.app.contentsuggestions.IContentSuggestionsManager; import android.app.job.IJobScheduler; import android.app.job.JobScheduler; import android.app.prediction.AppPredictionManager; @@ -1105,6 +1107,20 @@ final class SystemServiceRegistry { } }); + registerService(Context.CONTENT_SUGGESTIONS_SERVICE, + ContentSuggestionsManager.class, + new CachedServiceFetcher<ContentSuggestionsManager>() { + @Override + public ContentSuggestionsManager createService(ContextImpl ctx) { + // No throw as this is an optional service + IBinder b = ServiceManager.getService( + Context.CONTENT_SUGGESTIONS_SERVICE); + IContentSuggestionsManager service = + IContentSuggestionsManager.Stub.asInterface(b); + return new ContentSuggestionsManager(service); + } + }); + registerService(Context.VR_SERVICE, VrManager.class, new CachedServiceFetcher<VrManager>() { @Override public VrManager createService(ContextImpl ctx) throws ServiceNotFoundException { diff --git a/core/java/android/app/contentsuggestions/ClassificationsRequest.aidl b/core/java/android/app/contentsuggestions/ClassificationsRequest.aidl new file mode 100644 index 000000000000..a66413becc29 --- /dev/null +++ b/core/java/android/app/contentsuggestions/ClassificationsRequest.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.contentsuggestions; + +parcelable ClassificationsRequest; diff --git a/core/java/android/app/contentsuggestions/ClassificationsRequest.java b/core/java/android/app/contentsuggestions/ClassificationsRequest.java new file mode 100644 index 000000000000..3f715186abfb --- /dev/null +++ b/core/java/android/app/contentsuggestions/ClassificationsRequest.java @@ -0,0 +1,113 @@ +/* + * 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.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.List; + +/** + * @hide + */ +@SystemApi +public final class ClassificationsRequest implements Parcelable { + @NonNull + private final List<ContentSelection> mSelections; + @Nullable + private final Bundle mExtras; + + private ClassificationsRequest(@NonNull List<ContentSelection> selections, + @Nullable Bundle extras) { + mSelections = selections; + mExtras = extras; + } + + /** + * Return request selections. + */ + public List<ContentSelection> getSelections() { + return mSelections; + } + + /** + * Return the request extras. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedList(mSelections); + dest.writeBundle(mExtras); + } + + public static final Creator<ClassificationsRequest> CREATOR = + new Creator<ClassificationsRequest>() { + @Override + public ClassificationsRequest createFromParcel(Parcel source) { + return new ClassificationsRequest( + source.createTypedArrayList(ContentSelection.CREATOR), + source.readBundle()); + } + + @Override + public ClassificationsRequest[] newArray(int size) { + return new ClassificationsRequest[size]; + } + }; + + /** + * A builder for classifications request events. + * @hide + */ + @SystemApi + public static final class Builder { + + private final List<ContentSelection> mSelections; + private Bundle mExtras; + + public Builder(@NonNull List<ContentSelection> selections) { + mSelections = selections; + } + + /** + * Sets the request extras. + */ + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds a new request instance. + */ + public ClassificationsRequest build() { + return new ClassificationsRequest(mSelections, mExtras); + } + } +} diff --git a/core/java/android/app/contentsuggestions/ContentClassification.aidl b/core/java/android/app/contentsuggestions/ContentClassification.aidl new file mode 100644 index 000000000000..f67b2c8cea36 --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentClassification.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.contentsuggestions; + +parcelable ContentClassification; diff --git a/core/java/android/app/contentsuggestions/ContentClassification.java b/core/java/android/app/contentsuggestions/ContentClassification.java new file mode 100644 index 000000000000..f18520f1053c --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentClassification.java @@ -0,0 +1,79 @@ +/* + * 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.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +@SystemApi +public final class ContentClassification implements Parcelable { + @NonNull + private final String mClassificationId; + @NonNull + private final Bundle mExtras; + + public ContentClassification(@NonNull String classificationId, @NonNull Bundle extras) { + mClassificationId = classificationId; + mExtras = extras; + } + + /** + * Return the classification id. + */ + public String getId() { + return mClassificationId; + } + + /** + * Return the classification extras. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mClassificationId); + dest.writeBundle(mExtras); + } + + public static final Creator<ContentClassification> CREATOR = + new Creator<ContentClassification>() { + @Override + public ContentClassification createFromParcel(Parcel source) { + return new ContentClassification( + source.readString(), + source.readBundle()); + } + + @Override + public ContentClassification[] newArray(int size) { + return new ContentClassification[size]; + } + }; +} diff --git a/core/java/android/app/contentsuggestions/ContentSelection.aidl b/core/java/android/app/contentsuggestions/ContentSelection.aidl new file mode 100644 index 000000000000..e626d69278a4 --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentSelection.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.contentsuggestions; + +parcelable ContentSelection; diff --git a/core/java/android/app/contentsuggestions/ContentSelection.java b/core/java/android/app/contentsuggestions/ContentSelection.java new file mode 100644 index 000000000000..a8917f782e83 --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentSelection.java @@ -0,0 +1,79 @@ +/* + * 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.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +@SystemApi +public final class ContentSelection implements Parcelable { + @NonNull + private final String mSelectionId; + @NonNull + private final Bundle mExtras; + + public ContentSelection(@NonNull String selectionId, @NonNull Bundle extras) { + mSelectionId = selectionId; + mExtras = extras; + } + + /** + * Return the selection id. + */ + public String getId() { + return mSelectionId; + } + + /** + * Return the selection extras. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mSelectionId); + dest.writeBundle(mExtras); + } + + public static final Creator<ContentSelection> CREATOR = + new Creator<ContentSelection>() { + @Override + public ContentSelection createFromParcel(Parcel source) { + return new ContentSelection( + source.readString(), + source.readBundle()); + } + + @Override + public ContentSelection[] newArray(int size) { + return new ContentSelection[size]; + } + }; +} diff --git a/core/java/android/app/contentsuggestions/ContentSuggestionsManager.java b/core/java/android/app/contentsuggestions/ContentSuggestionsManager.java new file mode 100644 index 000000000000..b4d89774cc3b --- /dev/null +++ b/core/java/android/app/contentsuggestions/ContentSuggestionsManager.java @@ -0,0 +1,230 @@ +/* + * 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.contentsuggestions; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Binder; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * When provided with content from an app, can suggest selections and classifications of that + * content. + * + * <p>The content is mainly a snapshot of a running task, the selections will be text and image + * selections with that image content. These mSelections can then be classified to find actions and + * entities on those selections. + * + * <p>Only accessible to blessed components such as Overview. + * + * @hide + */ +@SystemApi +public final class ContentSuggestionsManager { + private static final String TAG = ContentSuggestionsManager.class.getSimpleName(); + + @Nullable + private final IContentSuggestionsManager mService; + + /** @hide */ + public ContentSuggestionsManager(@Nullable IContentSuggestionsManager service) { + mService = service; + } + + /** + * Hints to the system that a new context image for the provided task should be sent to the + * system content suggestions service. + * + * @param taskId of the task to snapshot. + * @param imageContextRequestExtras sent with with request to provide implementation specific + * extra information. + */ + public void provideContextImage(int taskId, @NonNull Bundle imageContextRequestExtras) { + if (mService == null) { + Log.e(TAG, "provideContextImage called, but no ContentSuggestionsManager configured"); + return; + } + + try { + mService.provideContextImage(taskId, imageContextRequestExtras); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Suggest content selections, based on the provided task id and optional + * location on screen provided in the request. Called after provideContextImage(). + * The result can be passed to + * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)} + * to classify actions and entities on these selections. + * + * @param request containing the task and point location. + * @param callbackExecutor to execute the provided callback on. + * @param callback to receive the selections. + */ + public void suggestContentSelections( + @NonNull SelectionsRequest request, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull SelectionsCallback callback) { + if (mService == null) { + Log.e(TAG, + "suggestContentSelections called, but no ContentSuggestionsManager configured"); + return; + } + + try { + mService.suggestContentSelections( + request, new SelectionsCallbackWrapper(callback, callbackExecutor)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Classify actions and entities in content selections, as returned from + * suggestContentSelections. Note these selections may be modified by the + * caller before being passed here. + * + * @param request containing the selections to classify. + * @param callbackExecutor to execute the provided callback on. + * @param callback to receive the classifications. + */ + public void classifyContentSelections( + @NonNull ClassificationsRequest request, + @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull ClassificationsCallback callback) { + if (mService == null) { + Log.e(TAG, "classifyContentSelections called, " + + "but no ContentSuggestionsManager configured"); + return; + } + + try { + mService.classifyContentSelections( + request, new ClassificationsCallbackWrapper(callback, callbackExecutor)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Report telemetry for interaction with suggestions / classifications. + * + * @param requestId the id for the associated interaction + * @param interaction to report back to the system content suggestions service. + */ + public void notifyInteraction(@NonNull String requestId, @NonNull Bundle interaction) { + if (mService == null) { + Log.e(TAG, "notifyInteraction called, but no ContentSuggestionsManager configured"); + return; + } + + try { + mService.notifyInteraction(requestId, interaction); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Callback to receive content selections from + * {@link #suggestContentSelections(SelectionsRequest, Executor, SelectionsCallback)}. + */ + public interface SelectionsCallback { + /** + * Async callback called when the content suggestions service has selections available. + * These can be modified and sent back to the manager for classification. The contents of + * the selection is implementation dependent. + * + * @param statusCode as defined by the implementation of content suggestions service. + * @param selections not {@code null}, but can be size {@code 0}. + */ + void onContentSelectionsAvailable( + int statusCode, @NonNull List<ContentSelection> selections); + } + + /** + * Callback to receive classifications from + * {@link #classifyContentSelections(ClassificationsRequest, Executor, ClassificationsCallback)} + */ + public interface ClassificationsCallback { + /** + * Async callback called when the content suggestions service has classified selections. The + * contents of the classification is implementation dependent. + * + * @param statusCode as defined by the implementation of content suggestions service. + * @param classifications not {@code null}, but can be size {@code 0}. + */ + void onContentClassificationsAvailable(int statusCode, + @NonNull List<ContentClassification> classifications); + } + + private static class SelectionsCallbackWrapper extends ISelectionsCallback.Stub { + private final SelectionsCallback mCallback; + private final Executor mExecutor; + + SelectionsCallbackWrapper( + @NonNull SelectionsCallback callback, @NonNull Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + @Override + public void onContentSelectionsAvailable( + int statusCode, List<ContentSelection> selections) { + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> + mCallback.onContentSelectionsAvailable(statusCode, selections)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + private static final class ClassificationsCallbackWrapper extends + IClassificationsCallback.Stub { + private final ClassificationsCallback mCallback; + private final Executor mExecutor; + + ClassificationsCallbackWrapper(@NonNull ClassificationsCallback callback, + @NonNull Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + @Override + public void onContentClassificationsAvailable( + int statusCode, List<ContentClassification> classifications) { + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> + mCallback.onContentClassificationsAvailable(statusCode, classifications)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } +} diff --git a/core/java/android/app/contentsuggestions/IClassificationsCallback.aidl b/core/java/android/app/contentsuggestions/IClassificationsCallback.aidl new file mode 100644 index 000000000000..69f5d5595986 --- /dev/null +++ b/core/java/android/app/contentsuggestions/IClassificationsCallback.aidl @@ -0,0 +1,25 @@ +/** + * 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.contentsuggestions; + +import android.app.contentsuggestions.ContentClassification; + +/** @hide */ +oneway interface IClassificationsCallback { + void onContentClassificationsAvailable( + int statusCode, in List<ContentClassification> classifications); +} diff --git a/core/java/android/app/contentsuggestions/IContentSuggestionsManager.aidl b/core/java/android/app/contentsuggestions/IContentSuggestionsManager.aidl new file mode 100644 index 000000000000..24f5ad866635 --- /dev/null +++ b/core/java/android/app/contentsuggestions/IContentSuggestionsManager.aidl @@ -0,0 +1,37 @@ +/* + * 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.contentsuggestions; + +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.SelectionsRequest; +import android.os.Bundle; + +/** @hide */ +oneway interface IContentSuggestionsManager { + void provideContextImage( + int taskId, + in Bundle imageContextRequestExtras); + void suggestContentSelections( + in SelectionsRequest request, + in ISelectionsCallback callback); + void classifyContentSelections( + in ClassificationsRequest request, + in IClassificationsCallback callback); + void notifyInteraction(in String requestId, in Bundle interaction); +} diff --git a/core/java/android/app/contentsuggestions/ISelectionsCallback.aidl b/core/java/android/app/contentsuggestions/ISelectionsCallback.aidl new file mode 100644 index 000000000000..19525832a4cd --- /dev/null +++ b/core/java/android/app/contentsuggestions/ISelectionsCallback.aidl @@ -0,0 +1,25 @@ +/** + * 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.contentsuggestions; + +import android.app.contentsuggestions.ContentSelection; +import android.os.Bundle; + +/** @hide */ +oneway interface ISelectionsCallback { + void onContentSelectionsAvailable(int statusCode, in List<ContentSelection> selections); +} diff --git a/core/java/android/app/contentsuggestions/SelectionsRequest.aidl b/core/java/android/app/contentsuggestions/SelectionsRequest.aidl new file mode 100644 index 000000000000..f5ce7c31bd94 --- /dev/null +++ b/core/java/android/app/contentsuggestions/SelectionsRequest.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.contentsuggestions; + +parcelable SelectionsRequest; diff --git a/core/java/android/app/contentsuggestions/SelectionsRequest.java b/core/java/android/app/contentsuggestions/SelectionsRequest.java new file mode 100644 index 000000000000..16f3e6b35aa4 --- /dev/null +++ b/core/java/android/app/contentsuggestions/SelectionsRequest.java @@ -0,0 +1,129 @@ +/* + * 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.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.graphics.Point; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +@SystemApi +public final class SelectionsRequest implements Parcelable { + private final int mTaskId; + @Nullable + private final Point mInterestPoint; + @Nullable + private final Bundle mExtras; + + private SelectionsRequest(int taskId, @Nullable Point interestPoint, @Nullable Bundle extras) { + mTaskId = taskId; + mInterestPoint = interestPoint; + mExtras = extras; + } + + /** + * Return the request task id. + */ + public int getTaskId() { + return mTaskId; + } + + /** + * Return the request point of interest. + */ + public Point getInterestPoint() { + return mInterestPoint; + } + + /** + * Return the request extras. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mTaskId); + dest.writeTypedObject(mInterestPoint, flags); + dest.writeBundle(mExtras); + } + + public static final Creator<SelectionsRequest> CREATOR = + new Creator<SelectionsRequest>() { + @Override + public SelectionsRequest createFromParcel(Parcel source) { + return new SelectionsRequest( + source.readInt(), source.readTypedObject(Point.CREATOR), source.readBundle()); + } + + @Override + public SelectionsRequest[] newArray(int size) { + return new SelectionsRequest[size]; + } + }; + + /** + * A builder for selections requests events. + * @hide + */ + @SystemApi + public static final class Builder { + + private final int mTaskId; + private Point mInterestPoint; + private Bundle mExtras; + + public Builder(int taskId) { + mTaskId = taskId; + } + + /** + * Sets the request extras. + */ + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Sets the request interest point. + */ + public Builder setInterestPoint(@NonNull Point interestPoint) { + mInterestPoint = interestPoint; + return this; + } + + /** + * Builds a new request instance. + */ + public SelectionsRequest build() { + return new SelectionsRequest(mTaskId, mInterestPoint, mExtras); + } + } +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 1626d3028366..fb283071cd55 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"; /** + * Used for getting content selections and classifications for task snapshots. + * + * @hide + * @see #getSystemService(String) + */ + @SystemApi + public static final String CONTENT_SUGGESTIONS_SERVICE = "content_suggestions"; + + /** * Official published name of the app prediction service. * * @hide diff --git a/core/java/android/service/contentsuggestions/ContentSuggestionsService.java b/core/java/android/service/contentsuggestions/ContentSuggestionsService.java new file mode 100644 index 000000000000..0da80397ec75 --- /dev/null +++ b/core/java/android/service/contentsuggestions/ContentSuggestionsService.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.service.contentsuggestions; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.Service; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.ContentSuggestionsManager; +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.SelectionsRequest; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.GraphicBuffer; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; + +/** + * @hide + */ +@SystemApi +public abstract class ContentSuggestionsService extends Service { + + private static final String TAG = ContentSuggestionsService.class.getSimpleName(); + + private Handler mHandler; + + /** + * The action for the intent used to define the content suggestions service. + */ + public static final String SERVICE_INTERFACE = + "android.service.contentsuggestions.ContentSuggestionsService"; + + private final IContentSuggestionsService mInterface = new IContentSuggestionsService.Stub() { + @Override + public void provideContextImage(int taskId, GraphicBuffer contextImage, + Bundle imageContextRequestExtras) { + mHandler.sendMessage( + obtainMessage(ContentSuggestionsService::processContextImage, + ContentSuggestionsService.this, taskId, + Bitmap.createHardwareBitmap(contextImage), + imageContextRequestExtras)); + } + + @Override + public void suggestContentSelections(SelectionsRequest request, + ISelectionsCallback callback) { + mHandler.sendMessage(obtainMessage(ContentSuggestionsService::suggestContentSelections, + ContentSuggestionsService.this, request, wrapSelectionsCallback(callback))); + + } + + @Override + public void classifyContentSelections(ClassificationsRequest request, + IClassificationsCallback callback) { + mHandler.sendMessage(obtainMessage(ContentSuggestionsService::classifyContentSelections, + ContentSuggestionsService.this, request, wrapClassificationCallback(callback))); + } + + @Override + public void notifyInteraction(String requestId, Bundle interaction) { + mHandler.sendMessage( + obtainMessage(ContentSuggestionsService::notifyInteraction, + ContentSuggestionsService.this, requestId, interaction)); + } + }; + + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mHandler = new Handler(Looper.getMainLooper(), null, true); + } + + /** @hide */ + @Override + public final IBinder onBind(Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return mInterface.asBinder(); + } + Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent); + return null; + } + + /** + * Called by the system to provide the snapshot for the task associated with the given + * {@param taskId}. + */ + public abstract void processContextImage( + int taskId, @Nullable Bitmap contextImage, @NonNull Bundle extras); + + /** + * Called by a client app to make a request for content selections. + */ + public abstract void suggestContentSelections(@NonNull SelectionsRequest request, + @NonNull ContentSuggestionsManager.SelectionsCallback callback); + + /** + * Called by a client app to classify the provided content selections. + */ + public abstract void classifyContentSelections(@NonNull ClassificationsRequest request, + @NonNull ContentSuggestionsManager.ClassificationsCallback callback); + + /** + * Called by a client app to report an interaction. + */ + public abstract void notifyInteraction(@NonNull String requestId, @NonNull Bundle interaction); + + private ContentSuggestionsManager.SelectionsCallback wrapSelectionsCallback( + ISelectionsCallback callback) { + return (statusCode, selections) -> { + try { + callback.onContentSelectionsAvailable(statusCode, selections); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + }; + } + + private ContentSuggestionsManager.ClassificationsCallback wrapClassificationCallback( + IClassificationsCallback callback) { + return ((statusCode, classifications) -> { + try { + callback.onContentClassificationsAvailable(statusCode, classifications); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending result: " + e); + } + }); + } +} diff --git a/core/java/android/service/contentsuggestions/IContentSuggestionsService.aidl b/core/java/android/service/contentsuggestions/IContentSuggestionsService.aidl new file mode 100644 index 000000000000..1926478322c8 --- /dev/null +++ b/core/java/android/service/contentsuggestions/IContentSuggestionsService.aidl @@ -0,0 +1,43 @@ +/* + * 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.contentsuggestions; + +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.SelectionsRequest; +import android.graphics.GraphicBuffer; +import android.os.Bundle; + +/** + * Interface from the system to an implementation of a content suggestions service. + * + * @hide + */ +oneway interface IContentSuggestionsService { + void provideContextImage( + int taskId, + in GraphicBuffer contextImage, + in Bundle imageContextRequestExtras); + void suggestContentSelections( + in SelectionsRequest request, + in ISelectionsCallback callback); + void classifyContentSelections( + in ClassificationsRequest request, + in IClassificationsCallback callback); + void notifyInteraction(in String requestId, in Bundle interaction); +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 3fa7cfbc2a26..e73267b541e4 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 content suggestions service. + @hide <p>Not for use by third-party applications.</p> --> + <permission android:name="android.permission.MANAGE_CONTENT_SUGGESTIONS" + 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" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 22bff7d27159..928d73c3675e 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3425,6 +3425,17 @@ --> <string name="config_defaultAppPredictionService" translatable="false"></string> + <!-- The package name for the system's content suggestions service. + Provides suggestions for text and image selection regions in snapshots of apps and should + be able to classify the type of entities in those selections. + + This service must be trusted, as it can be activated without explicit consent of the user. + If no service with the specified name exists on the device, content suggestions wil be + disabled. + Example: "com.android.contentsuggestions/.ContentSuggestionsService" + --> + <string name="config_defaultContentSuggestionsService" 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 d9dbb100f1b6..40891099623a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3276,6 +3276,7 @@ <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="config_defaultContentSuggestionsService" /> <java-symbol type="string" name="notification_channel_foreground_service" /> <java-symbol type="string" name="foreground_service_app_in_background" /> diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 562d44d6acac..fa2289d483fb 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_CONTENT_SUGGESTIONS" /> <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" /> diff --git a/services/Android.bp b/services/Android.bp index 8e96cccd4464..567efac5753c 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -23,6 +23,7 @@ java_library { "services.backup", "services.companion", "services.contentcapture", + "services.contentsuggestions", "services.coverage", "services.devicepolicy", "services.midi", diff --git a/services/contentsuggestions/Android.bp b/services/contentsuggestions/Android.bp new file mode 100644 index 000000000000..fc09d2e5196a --- /dev/null +++ b/services/contentsuggestions/Android.bp @@ -0,0 +1,5 @@ +java_library_static { + name: "services.contentsuggestions", + srcs: ["java/**/*.java"], + libs: ["services.core"], +}
\ No newline at end of file diff --git a/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerService.java b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerService.java new file mode 100644 index 000000000000..58dbea469b9c --- /dev/null +++ b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerService.java @@ -0,0 +1,205 @@ +/* + * 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.contentsuggestions; + +import static android.Manifest.permission.MANAGE_CONTENT_SUGGESTIONS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.IContentSuggestionsManager; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.SelectionsRequest; +import android.content.Context; +import android.os.Binder; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.os.UserHandle; +import android.util.Slog; + +import com.android.server.LocalServices; +import com.android.server.infra.AbstractMasterSystemService; +import com.android.server.infra.FrameworkResourcesServiceNameResolver; +import com.android.server.wm.ActivityTaskManagerInternal; + +import java.io.FileDescriptor; + +/** + * The system service for providing recents / overview with content suggestion selections and + * classifications. + * + * <p>Calls are received here from + * {@link android.app.contentsuggestions.ContentSuggestionsManager} then delegated to + * a per user version of the service. From there they are routed to the remote actual implementation + * that provides the suggestion selections and classifications. + */ +public class ContentSuggestionsManagerService extends + AbstractMasterSystemService< + ContentSuggestionsManagerService, ContentSuggestionsPerUserService> { + + private static final String TAG = ContentSuggestionsManagerService.class.getSimpleName(); + private static final boolean VERBOSE = false; // TODO: make dynamic + + private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes + + private ActivityTaskManagerInternal mActivityTaskManagerInternal; + + public ContentSuggestionsManagerService(Context context) { + super(context, new FrameworkResourcesServiceNameResolver(context, + com.android.internal.R.string.config_defaultContentSuggestionsService), null); + mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); + } + + @Override + protected ContentSuggestionsPerUserService newServiceLocked(int resolvedUserId, + boolean disabled) { + return new ContentSuggestionsPerUserService(this, mLock, resolvedUserId); + } + + @Override + public void onStart() { + publishBinderService( + Context.CONTENT_SUGGESTIONS_SERVICE, new ContentSuggestionsManagerStub()); + } + + @Override + protected void enforceCallingPermissionForManagement() { + getContext().enforceCallingPermission(MANAGE_CONTENT_SUGGESTIONS, TAG); + } + + @Override + protected int getMaximumTemporaryServiceDurationMs() { + return MAX_TEMP_SERVICE_DURATION_MS; + } + + private boolean isCallerRecents(int userId) { + if (mServiceNameResolver.isTemporary(userId)) { + // If a temporary service is set then skip the recents check + return true; + } + return mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid()); + } + + private void enforceCallerIsRecents(int userId, String func) { + if (isCallerRecents(userId)) { + return; + } + + String msg = "Permission Denial: " + func + " from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid() + + " expected caller is recents"; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + + private class ContentSuggestionsManagerStub extends IContentSuggestionsManager.Stub { + @Override + public void provideContextImage(int taskId, @NonNull Bundle imageContextRequestExtras) { + if (imageContextRequestExtras == null) { + throw new IllegalArgumentException("Expected non-null imageContextRequestExtras"); + } + + final int userId = UserHandle.getCallingUserId(); + enforceCallerIsRecents(userId, "provideContextImage"); + + synchronized (mLock) { + final ContentSuggestionsPerUserService service = getServiceForUserLocked(userId); + if (service != null) { + service.provideContextImageLocked(taskId, imageContextRequestExtras); + } else { + if (VERBOSE) { + Slog.v(TAG, "provideContextImageLocked: no service for " + userId); + } + } + } + } + + @Override + public void suggestContentSelections( + @NonNull SelectionsRequest selectionsRequest, + @NonNull ISelectionsCallback selectionsCallback) { + final int userId = UserHandle.getCallingUserId(); + enforceCallerIsRecents(userId, "suggestContentSelections"); + + synchronized (mLock) { + final ContentSuggestionsPerUserService service = getServiceForUserLocked(userId); + if (service != null) { + service.suggestContentSelectionsLocked(selectionsRequest, selectionsCallback); + } else { + if (VERBOSE) { + Slog.v(TAG, "suggestContentSelectionsLocked: no service for " + userId); + } + } + } + } + + @Override + public void classifyContentSelections( + @NonNull ClassificationsRequest classificationsRequest, + @NonNull IClassificationsCallback callback) { + final int userId = UserHandle.getCallingUserId(); + enforceCallerIsRecents(userId, "classifyContentSelections"); + + synchronized (mLock) { + final ContentSuggestionsPerUserService service = getServiceForUserLocked(userId); + if (service != null) { + service.classifyContentSelectionsLocked(classificationsRequest, callback); + } else { + if (VERBOSE) { + Slog.v(TAG, "classifyContentSelectionsLocked: no service for " + userId); + } + } + } + } + + @Override + public void notifyInteraction(@NonNull String requestId, @NonNull Bundle bundle) { + final int userId = UserHandle.getCallingUserId(); + enforceCallerIsRecents(userId, "notifyInteraction"); + + synchronized (mLock) { + final ContentSuggestionsPerUserService service = getServiceForUserLocked(userId); + if (service != null) { + service.notifyInteractionLocked(requestId, bundle); + } else { + if (VERBOSE) { + Slog.v(TAG, "reportInteractionLocked: no service for " + userId); + } + } + } + } + + public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, + @Nullable FileDescriptor err, + @NonNull String[] args, @Nullable ShellCallback callback, + @NonNull ResultReceiver resultReceiver) throws RemoteException { + // Ensure that the caller is the shell process + final int callingUid = Binder.getCallingUid(); + if (callingUid != android.os.Process.SHELL_UID + && callingUid != android.os.Process.ROOT_UID) { + Slog.e(TAG, "Expected shell caller"); + return; + } + new ContentSuggestionsManagerServiceShellCommand(ContentSuggestionsManagerService.this) + .exec(this, in, out, err, args, callback, resultReceiver); + } + } +} diff --git a/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerServiceShellCommand.java b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerServiceShellCommand.java new file mode 100644 index 000000000000..e34f1eadcd02 --- /dev/null +++ b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsManagerServiceShellCommand.java @@ -0,0 +1,84 @@ +/* + * 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.contentsuggestions; + +import android.annotation.NonNull; +import android.os.ShellCommand; + +import java.io.PrintWriter; + +/** + * The shell command implementation for the ContentSuggestionsManagerService. + */ +public class ContentSuggestionsManagerServiceShellCommand extends ShellCommand { + + private static final String TAG = + ContentSuggestionsManagerServiceShellCommand.class.getSimpleName(); + + private final ContentSuggestionsManagerService mService; + + public ContentSuggestionsManagerServiceShellCommand( + @NonNull ContentSuggestionsManagerService 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("ContentSuggestionsService 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("ContentSuggestionsManagerService 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/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsPerUserService.java b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsPerUserService.java new file mode 100644 index 000000000000..385bc6cf3932 --- /dev/null +++ b/services/contentsuggestions/java/com/android/server/contentsuggestions/ContentSuggestionsPerUserService.java @@ -0,0 +1,162 @@ +/* + * 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.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.SelectionsRequest; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.graphics.GraphicBuffer; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.LocalServices; +import com.android.server.infra.AbstractPerUserSystemService; +import com.android.server.wm.ActivityTaskManagerInternal; + +/** + * Per user delegate of {@link ContentSuggestionsManagerService}. + * + * <p>Main job is to forward calls to the remote implementation that can provide suggestion + * selections and classifications. + */ +public final class ContentSuggestionsPerUserService extends + AbstractPerUserSystemService< + ContentSuggestionsPerUserService, ContentSuggestionsManagerService> { + private static final String TAG = ContentSuggestionsPerUserService.class.getSimpleName(); + + @Nullable + @GuardedBy("mLock") + private RemoteContentSuggestionsService mRemoteService; + + @NonNull + private final ActivityTaskManagerInternal mActivityTaskManagerInternal; + + ContentSuggestionsPerUserService( + ContentSuggestionsManagerService master, Object lock, int userId) { + super(master, lock, userId); + mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); + } + + @GuardedBy("mLock") + @Override // from PerUserSystemService + protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) + throws PackageManager.NameNotFoundException { + ServiceInfo si; + try { + si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException e) { + throw new PackageManager.NameNotFoundException( + "Could not get service for " + serviceComponent); + } + 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; + } + + @GuardedBy("mLock") + void provideContextImageLocked(int taskId, @NonNull Bundle imageContextRequestExtras) { + RemoteContentSuggestionsService service = getRemoteServiceLocked(); + if (service != null) { + ActivityManager.TaskSnapshot snapshot = + mActivityTaskManagerInternal.getTaskSnapshot(taskId, false); + GraphicBuffer snapshotBuffer = null; + if (snapshot != null) { + snapshotBuffer = snapshot.getSnapshot(); + } + + service.provideContextImage(taskId, snapshotBuffer, imageContextRequestExtras); + } + } + + @GuardedBy("mLock") + void suggestContentSelectionsLocked( + @NonNull SelectionsRequest selectionsRequest, + @NonNull ISelectionsCallback selectionsCallback) { + RemoteContentSuggestionsService service = getRemoteServiceLocked(); + if (service != null) { + service.suggestContentSelections(selectionsRequest, selectionsCallback); + } + } + + @GuardedBy("mLock") + void classifyContentSelectionsLocked( + @NonNull ClassificationsRequest classificationsRequest, + @NonNull IClassificationsCallback callback) { + RemoteContentSuggestionsService service = getRemoteServiceLocked(); + if (service != null) { + service.classifyContentSelections(classificationsRequest, callback); + } + } + + @GuardedBy("mLock") + void notifyInteractionLocked(@NonNull String requestId, @NonNull Bundle bundle) { + RemoteContentSuggestionsService service = getRemoteServiceLocked(); + if (service != null) { + service.notifyInteraction(requestId, bundle); + } + } + + @GuardedBy("mLock") + @Nullable + private RemoteContentSuggestionsService 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 RemoteContentSuggestionsService(getContext(), + serviceComponent, mUserId, + new RemoteContentSuggestionsService.Callbacks() { + @Override + public void onServiceDied( + @NonNull RemoteContentSuggestionsService service) { + // TODO(b/120865921): properly implement + Slog.w(TAG, "remote content suggestions service died"); + } + }, mMaster.isBindInstantServiceAllowed(), mMaster.verbose); + } + + return mRemoteService; + } +} diff --git a/services/contentsuggestions/java/com/android/server/contentsuggestions/RemoteContentSuggestionsService.java b/services/contentsuggestions/java/com/android/server/contentsuggestions/RemoteContentSuggestionsService.java new file mode 100644 index 000000000000..bf48d7623255 --- /dev/null +++ b/services/contentsuggestions/java/com/android/server/contentsuggestions/RemoteContentSuggestionsService.java @@ -0,0 +1,97 @@ +/* + * 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.contentsuggestions; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.contentsuggestions.ClassificationsRequest; +import android.app.contentsuggestions.IClassificationsCallback; +import android.app.contentsuggestions.ISelectionsCallback; +import android.app.contentsuggestions.SelectionsRequest; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.GraphicBuffer; +import android.os.Bundle; +import android.os.IBinder; +import android.service.contentsuggestions.ContentSuggestionsService; +import android.service.contentsuggestions.IContentSuggestionsService; +import android.text.format.DateUtils; + +import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService; + +/** + * Delegates calls from {@link ContentSuggestionsPerUserService} to the remote actual implementation + * of the suggestion selection and classification service. + */ +public class RemoteContentSuggestionsService extends + AbstractMultiplePendingRequestsRemoteService<RemoteContentSuggestionsService, + IContentSuggestionsService> { + + private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS; + + RemoteContentSuggestionsService(Context context, ComponentName serviceName, + int userId, Callbacks callbacks, + boolean bindInstantServiceAllowed, boolean verbose) { + super(context, ContentSuggestionsService.SERVICE_INTERFACE, serviceName, userId, callbacks, + bindInstantServiceAllowed, verbose, /* initialCapacity= */ 1); + } + + @Override + protected IContentSuggestionsService getServiceInterface(IBinder service) { + return IContentSuggestionsService.Stub.asInterface(service); + } + + @Override + protected long getTimeoutIdleBindMillis() { + return PERMANENT_BOUND_TIMEOUT_MS; + } + + @Override + protected long getRemoteRequestMillis() { + return TIMEOUT_REMOTE_REQUEST_MILLIS; + } + + void provideContextImage(int taskId, @Nullable GraphicBuffer contextImage, + @NonNull Bundle imageContextRequestExtras) { + scheduleAsyncRequest((s) -> s.provideContextImage(taskId, contextImage, + imageContextRequestExtras)); + } + + void suggestContentSelections( + @NonNull SelectionsRequest selectionsRequest, + @NonNull ISelectionsCallback selectionsCallback) { + scheduleAsyncRequest( + (s) -> s.suggestContentSelections(selectionsRequest, selectionsCallback)); + } + + void classifyContentSelections( + @NonNull ClassificationsRequest classificationsRequest, + @NonNull IClassificationsCallback callback) { + scheduleAsyncRequest((s) -> s.classifyContentSelections(classificationsRequest, callback)); + } + + void notifyInteraction(@NonNull String requestId, @NonNull Bundle bundle) { + scheduleAsyncRequest((s) -> s.notifyInteraction(requestId, bundle)); + } + + interface Callbacks + extends VultureCallback<RemoteContentSuggestionsService> { + // NOTE: so far we don't need to notify the callback implementation + // (ContentSuggestionsManager) of the request results (success, timeouts, etc..), so this + // callback interface is empty. + } +} diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 0f286ce30ccd..d8644df3684c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -19,6 +19,7 @@ package com.android.server.wm; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.app.ActivityManager; import android.app.AppProtoEnums; import android.app.IActivityManager; import android.app.IApplicationThread; @@ -479,4 +480,10 @@ public abstract class ActivityTaskManagerInternal { public abstract void setProfilerInfo(ProfilerInfo profilerInfo); public abstract ActivityMetricsLaunchObserverRegistry getLaunchObserverRegistry(); + + /** + * Gets bitmap snapshot of the provided task id. + */ + public abstract ActivityManager.TaskSnapshot getTaskSnapshot(int taskId, + boolean reducedResolution); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 42121ca08696..28515ff809d6 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -7012,5 +7012,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return mStackSupervisor.getActivityMetricsLogger().getLaunchObserverRegistry(); } } + + @Override + public ActivityManager.TaskSnapshot getTaskSnapshot(int taskId, boolean reducedResolution) { + synchronized (mGlobalLock) { + return ActivityTaskManagerService.this.getTaskSnapshot(taskId, reducedResolution); + } + } } } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index ced4261821d9..1fef5b0ba892 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -262,6 +262,8 @@ public final class SystemServer { "com.android.server.adb.AdbService$Lifecycle"; private static final String APP_PREDICTION_MANAGER_SERVICE_CLASS = "com.android.server.appprediction.AppPredictionManagerService"; + private static final String CONTENT_SUGGESTIONS_SERVICE_CLASS = + "com.android.server.contentsuggestions.ContentSuggestionsManagerService"; private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst"; @@ -1160,6 +1162,11 @@ public final class SystemServer { mSystemServiceManager.startService(APP_PREDICTION_MANAGER_SERVICE_CLASS); traceEnd(); + // Content suggestions manager service + traceBeginAndSlog("StartContentSuggestionsService"); + mSystemServiceManager.startService(CONTENT_SUGGESTIONS_SERVICE_CLASS); + traceEnd(); + // NOTE: ClipboardService indirectly depends on IntelligenceService traceBeginAndSlog("StartClipboardService"); mSystemServiceManager.startService(ClipboardService.class); |