diff options
author | Winson Chung <winsonc@google.com> | 2019-01-08 17:41:55 -0800 |
---|---|---|
committer | Winson Chung <winsonc@google.com> | 2019-01-10 16:35:13 -0800 |
commit | 3fb0f258d344ea0d73dc67f941719a2c9d6cdeb6 (patch) | |
tree | 14aa3bfd0e4b244beb8c03b7ccaa645470c93b19 /services/contentsuggestions | |
parent | 54e91344e2a0072c40d09405fe5a295467b36c07 (diff) |
Initial commit of Content Suggestions (overview long press) Manager.
Test: compile
Bug: 120865921
Change-Id: I9f8310112bedc883406f0f50a99041f542036fd9
Diffstat (limited to 'services/contentsuggestions')
5 files changed, 553 insertions, 0 deletions
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. + } +} |