Adding System Server implementation of Cloud Search API
Bug: 210528288
Test: RBE Built redfin-eng and tested on a physical device
Change-Id: Idac2e83819f9aeee292a5d53f3304d47cfc88af5
diff --git a/services/Android.bp b/services/Android.bp
index 26760aa..af70692 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -79,6 +79,7 @@
":services.backup-sources",
":services.bluetooth-sources", // TODO(b/214988855) : Remove once apex/service-bluetooth jar is ready
":backuplib-sources",
+ ":services.cloudsearch-sources",
":services.companion-sources",
":services.contentcapture-sources",
":services.contentsuggestions-sources",
@@ -133,6 +134,7 @@
"services.appwidget",
"services.autofill",
"services.backup",
+ "services.cloudsearch",
"services.companion",
"services.contentcapture",
"services.contentsuggestions",
diff --git a/services/cloudsearch/Android.bp b/services/cloudsearch/Android.bp
new file mode 100644
index 0000000..e38e615
--- /dev/null
+++ b/services/cloudsearch/Android.bp
@@ -0,0 +1,22 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "services.cloudsearch-sources",
+ srcs: ["java/**/*.java"],
+ path: "java",
+ visibility: ["//frameworks/base/services"],
+}
+
+java_library_static {
+ name: "services.cloudsearch",
+ defaults: ["platform_service_defaults"],
+ srcs: [":services.cloudsearch-sources"],
+ libs: ["services.core"],
+}
diff --git a/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchManagerService.java b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchManagerService.java
new file mode 100644
index 0000000..dafe7a4
--- /dev/null
+++ b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchManagerService.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2022 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.cloudsearch;
+
+import static android.Manifest.permission.MANAGE_CLOUDSEARCH;
+import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
+import static android.content.Context.CLOUDSEARCH_SERVICE;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManagerInternal;
+import android.app.cloudsearch.ICloudSearchManager;
+import android.app.cloudsearch.ICloudSearchManagerCallback;
+import android.app.cloudsearch.SearchRequest;
+import android.app.cloudsearch.SearchResponse;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.util.Slog;
+
+import com.android.internal.R;
+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;
+import java.util.function.Consumer;
+
+/**
+ * A service used to return cloudsearch targets given a query.
+ */
+public class CloudSearchManagerService extends
+ AbstractMasterSystemService<CloudSearchManagerService, CloudSearchPerUserService> {
+
+ private static final String TAG = CloudSearchManagerService.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes
+
+ private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
+
+ public CloudSearchManagerService(Context context) {
+ super(context, new FrameworkResourcesServiceNameResolver(context,
+ R.string.config_defaultCloudSearchService), null,
+ PACKAGE_UPDATE_POLICY_NO_REFRESH | PACKAGE_RESTART_POLICY_NO_REFRESH);
+ mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+ }
+
+ @Override
+ protected CloudSearchPerUserService newServiceLocked(int resolvedUserId, boolean disabled) {
+ return new CloudSearchPerUserService(this, mLock, resolvedUserId);
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(CLOUDSEARCH_SERVICE, new CloudSearchManagerStub());
+ }
+
+ @Override
+ protected void enforceCallingPermissionForManagement() {
+ getContext().enforceCallingPermission(MANAGE_CLOUDSEARCH, TAG);
+ }
+
+ @Override // from AbstractMasterSystemService
+ protected void onServicePackageUpdatedLocked(@UserIdInt int userId) {
+ final CloudSearchPerUserService service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ service.onPackageUpdatedLocked();
+ }
+ }
+
+ @Override // from AbstractMasterSystemService
+ protected void onServicePackageRestartedLocked(@UserIdInt int userId) {
+ final CloudSearchPerUserService service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ service.onPackageRestartedLocked();
+ }
+ }
+
+ @Override
+ protected int getMaximumTemporaryServiceDurationMs() {
+ return MAX_TEMP_SERVICE_DURATION_MS;
+ }
+
+ private class CloudSearchManagerStub extends ICloudSearchManager.Stub {
+
+ @Override
+ public void search(@NonNull SearchRequest searchRequest,
+ @NonNull ICloudSearchManagerCallback callBack) {
+ runForUserLocked("search", searchRequest.getRequestId(), (service) ->
+ service.onSearchLocked(searchRequest, callBack));
+ }
+
+ @Override
+ public void returnResults(IBinder token, String requestId, SearchResponse response) {
+ runForUserLocked("returnResults", requestId, (service) ->
+ service.onReturnResultsLocked(token, requestId, response));
+ }
+
+ public void destroy(@NonNull SearchRequest searchRequest) {
+ runForUserLocked("destroyCloudSearchSession", searchRequest.getRequestId(),
+ (service) -> service.onDestroyLocked(searchRequest.getRequestId()));
+ }
+
+ public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+ @Nullable FileDescriptor err,
+ @NonNull String[] args, @Nullable ShellCallback callback,
+ @NonNull ResultReceiver resultReceiver) {
+ new CloudSearchManagerServiceShellCommand(CloudSearchManagerService.this)
+ .exec(this, in, out, err, args, callback, resultReceiver);
+ }
+
+ private void runForUserLocked(@NonNull final String func,
+ @NonNull final String requestId,
+ @NonNull final Consumer<CloudSearchPerUserService> c) {
+ ActivityManagerInternal am = LocalServices.getService(ActivityManagerInternal.class);
+ final int userId = am.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
+ Binder.getCallingUserHandle().getIdentifier(), false, ALLOW_NON_FULL,
+ null, null);
+
+ if (DEBUG) {
+ Slog.d(TAG, "runForUserLocked:" + func + " from pid=" + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ }
+ Context ctx = getContext();
+ if (!(ctx.checkCallingPermission(MANAGE_CLOUDSEARCH) == PERMISSION_GRANTED
+ || mServiceNameResolver.isTemporary(userId)
+ || mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid()))) {
+
+ String msg = "Permission Denial: Cannot call " + func + " from pid="
+ + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid();
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ final CloudSearchPerUserService service = getServiceForUserLocked(userId);
+ c.accept(service);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+ }
+}
diff --git a/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchManagerServiceShellCommand.java b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchManagerServiceShellCommand.java
new file mode 100644
index 0000000..51f5fd9
--- /dev/null
+++ b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchManagerServiceShellCommand.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 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.cloudsearch;
+
+import android.annotation.NonNull;
+import android.os.ShellCommand;
+
+import java.io.PrintWriter;
+
+/**
+ * The shell command implementation for the CloudSearchManagerService.
+ */
+public class CloudSearchManagerServiceShellCommand extends ShellCommand {
+
+ private static final String TAG =
+ CloudSearchManagerServiceShellCommand.class.getSimpleName();
+
+ private final CloudSearchManagerService mService;
+
+ public CloudSearchManagerServiceShellCommand(@NonNull CloudSearchManagerService 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);
+ pw.println("CloudSearchService temporarily reset. ");
+ return 0;
+ }
+ final int duration = Integer.parseInt(getNextArgRequired());
+ mService.setTemporaryService(userId, serviceName, duration);
+ pw.println("CloudSearchService 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("CloudSearchManagerService 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/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java
new file mode 100644
index 0000000..32d66af
--- /dev/null
+++ b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2022 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.cloudsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppGlobals;
+import android.app.cloudsearch.ICloudSearchManagerCallback;
+import android.app.cloudsearch.SearchRequest;
+import android.app.cloudsearch.SearchResponse;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ServiceInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.service.cloudsearch.CloudSearchService;
+import android.service.cloudsearch.ICloudSearchService;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.infra.AbstractRemoteService;
+import com.android.server.CircularQueue;
+import com.android.server.infra.AbstractPerUserSystemService;
+
+/**
+ * Per-user instance of {@link CloudSearchManagerService}.
+ */
+public class CloudSearchPerUserService extends
+ AbstractPerUserSystemService<CloudSearchPerUserService, CloudSearchManagerService>
+ implements RemoteCloudSearchService.RemoteCloudSearchServiceCallbacks {
+
+ private static final String TAG = CloudSearchPerUserService.class.getSimpleName();
+ private static final int QUEUE_SIZE = 10;
+ @GuardedBy("mLock")
+ private final CircularQueue<String, CloudSearchCallbackInfo> mCallbackQueue =
+ new CircularQueue<>(QUEUE_SIZE);
+ @Nullable
+ @GuardedBy("mLock")
+ private RemoteCloudSearchService mRemoteService;
+ /**
+ * When {@code true}, remote service died but service state is kept so it's restored after
+ * the system re-binds to it.
+ */
+ @GuardedBy("mLock")
+ private boolean mZombie;
+
+ protected CloudSearchPerUserService(CloudSearchManagerService master,
+ Object lock, int userId) {
+ super(master, lock, userId);
+ }
+
+ @Override // from PerUserSystemService
+ protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
+ throws NameNotFoundException {
+
+ ServiceInfo si;
+ try {
+ si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
+ PackageManager.GET_META_DATA, mUserId);
+ } catch (RemoteException e) {
+ throw new NameNotFoundException("Could not get service for " + serviceComponent);
+ }
+ // TODO(b/177858728): must check that either the service is from a system component,
+ // or it matches a service set by shell cmd (so it can be used on CTS tests and when
+ // OEMs are implementing the real service and also verify the proper permissions
+ return si;
+ }
+
+ @GuardedBy("mLock")
+ @Override // from PerUserSystemService
+ protected boolean updateLocked(boolean disabled) {
+ final boolean enabledChanged = super.updateLocked(disabled);
+ if (enabledChanged) {
+ if (isEnabledLocked()) {
+ // Send the pending sessions over to the service
+ resurrectSessionsLocked();
+ } else {
+ // Clear the remote service for the next call
+ updateRemoteServiceLocked();
+ }
+ }
+ return enabledChanged;
+ }
+
+ /**
+ * Notifies the service of a new cloudsearch session.
+ */
+ @GuardedBy("mLock")
+ public void onSearchLocked(@NonNull SearchRequest searchRequest,
+ @NonNull ICloudSearchManagerCallback callback) {
+ String filterList = searchRequest.getSearchConstraints().containsKey(
+ SearchRequest.CONSTRAINT_SEARCH_PROVIDER_FILTER)
+ ? searchRequest.getSearchConstraints().getString(
+ SearchRequest.CONSTRAINT_SEARCH_PROVIDER_FILTER) : "";
+
+ String remoteServicePackageName = getServiceComponentName().getPackageName();
+ // By default, all providers are marked as wanted.
+ boolean wantedProvider = true;
+ if (filterList.length() > 0) {
+ // If providers are specified by the client,
+ wantedProvider = false;
+ String[] providersSpecified = filterList.split(";");
+ for (int i = 0; i < providersSpecified.length; i++) {
+ if (providersSpecified[i].equals(remoteServicePackageName)) {
+ wantedProvider = true;
+ break;
+ }
+ }
+ }
+ // If the provider was not requested by the Client, the request will not be sent to the
+ // provider.
+ if (!wantedProvider) {
+ // TODO(216520546) Send a failure callback to the client.
+ return;
+ }
+ final boolean serviceExists = resolveService(searchRequest,
+ s -> s.onSearch(searchRequest));
+ String requestId = searchRequest.getRequestId();
+ if (serviceExists && !mCallbackQueue.containsKey(requestId)) {
+ final CloudSearchCallbackInfo sessionInfo = new CloudSearchCallbackInfo(
+ requestId, searchRequest, callback, callback.asBinder(), () -> {
+ synchronized (mLock) {
+ onDestroyLocked(requestId);
+ }
+ });
+ if (sessionInfo.linkToDeath()) {
+ mCallbackQueue.put(requestId, sessionInfo);
+ } else {
+ // destroy the session if calling process is already dead
+ onDestroyLocked(requestId);
+ }
+ }
+ }
+
+ /**
+ * Used to return results back to the clients.
+ */
+ public void onReturnResultsLocked(@NonNull IBinder token,
+ @NonNull String requestId,
+ @NonNull SearchResponse response) {
+ if (mCallbackQueue.containsKey(requestId)) {
+ response.setSource(mRemoteService.getComponentName().getPackageName());
+ final CloudSearchCallbackInfo sessionInfo = mCallbackQueue.getElement(requestId);
+ try {
+ if (response.getStatusCode() == SearchResponse.SEARCH_STATUS_OK) {
+ sessionInfo.mCallback.onSearchSucceeded(response);
+ } else {
+ sessionInfo.mCallback.onSearchFailed(response);
+ }
+ } catch (RemoteException e) {
+ onDestroyLocked(requestId);
+ }
+ }
+ }
+
+ /**
+ * Notifies the server about the end of an existing cloudsearch session.
+ */
+ @GuardedBy("mLock")
+ public void onDestroyLocked(@NonNull String requestId) {
+ if (isDebug()) {
+ Slog.d(TAG, "onDestroyLocked(): requestId=" + requestId);
+ }
+ final CloudSearchCallbackInfo sessionInfo = mCallbackQueue.removeElement(requestId);
+ sessionInfo.destroy();
+ }
+
+ @Override
+ public void onFailureOrTimeout(boolean timedOut) {
+ if (isDebug()) {
+ Slog.d(TAG, "onFailureOrTimeout(): timed out=" + timedOut);
+ }
+ // Do nothing, we are just proxying to the cloudsearch service
+ }
+
+ @Override
+ public void onConnectedStateChanged(boolean connected) {
+ if (isDebug()) {
+ Slog.d(TAG, "onConnectedStateChanged(): connected=" + connected);
+ }
+ if (connected) {
+ synchronized (mLock) {
+ if (mZombie) {
+ // Validation check - shouldn't happen
+ if (mRemoteService == null) {
+ Slog.w(TAG, "Cannot resurrect sessions because remote service is null");
+ return;
+ }
+ mZombie = false;
+ resurrectSessionsLocked();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDied(RemoteCloudSearchService service) {
+ if (isDebug()) {
+ Slog.w(TAG, "onServiceDied(): service=" + service);
+ }
+ synchronized (mLock) {
+ mZombie = true;
+ }
+ updateRemoteServiceLocked();
+ }
+
+ @GuardedBy("mLock")
+ private void updateRemoteServiceLocked() {
+ if (mRemoteService != null) {
+ mRemoteService.destroy();
+ mRemoteService = null;
+ }
+ }
+
+ void onPackageUpdatedLocked() {
+ if (isDebug()) {
+ Slog.v(TAG, "onPackageUpdatedLocked()");
+ }
+ destroyAndRebindRemoteService();
+ }
+
+ void onPackageRestartedLocked() {
+ if (isDebug()) {
+ Slog.v(TAG, "onPackageRestartedLocked()");
+ }
+ destroyAndRebindRemoteService();
+ }
+
+ private void destroyAndRebindRemoteService() {
+ if (mRemoteService == null) {
+ return;
+ }
+
+ if (isDebug()) {
+ Slog.d(TAG, "Destroying the old remote service.");
+ }
+ mRemoteService.destroy();
+ mRemoteService = null;
+
+ synchronized (mLock) {
+ mZombie = true;
+ }
+ mRemoteService = getRemoteServiceLocked();
+ if (mRemoteService != null) {
+ if (isDebug()) {
+ Slog.d(TAG, "Rebinding to the new remote service.");
+ }
+ mRemoteService.reconnect();
+ }
+ }
+
+ /**
+ * Called after the remote service connected, it's used to restore state from a 'zombie'
+ * service (i.e., after it died).
+ */
+ private void resurrectSessionsLocked() {
+ final int numCallbacks = mCallbackQueue.size();
+ if (isDebug()) {
+ Slog.d(TAG, "Resurrecting remote service (" + mRemoteService + ") on "
+ + numCallbacks + " requests.");
+ }
+
+ for (CloudSearchCallbackInfo callbackInfo : mCallbackQueue.values()) {
+ callbackInfo.resurrectSessionLocked(this, callbackInfo.mToken);
+ }
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ protected boolean resolveService(
+ @NonNull final SearchRequest requestId,
+ @NonNull final AbstractRemoteService.AsyncRequest<ICloudSearchService> cb) {
+
+ final RemoteCloudSearchService service = getRemoteServiceLocked();
+ if (service != null) {
+ service.executeOnResolvedService(cb);
+ }
+ return service != null;
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private RemoteCloudSearchService 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 RemoteCloudSearchService(getContext(),
+ CloudSearchService.SERVICE_INTERFACE, serviceComponent, mUserId, this,
+ mMaster.isBindInstantServiceAllowed(), mMaster.verbose);
+ }
+
+ return mRemoteService;
+ }
+
+ private static final class CloudSearchCallbackInfo {
+ private static final boolean DEBUG = false; // Do not submit with true
+ @NonNull
+ final IBinder mToken;
+ @NonNull
+ final IBinder.DeathRecipient mDeathRecipient;
+ @NonNull
+ private final String mRequestId;
+ @NonNull
+ private final SearchRequest mSearchRequest;
+ private final ICloudSearchManagerCallback mCallback;
+
+ CloudSearchCallbackInfo(
+ @NonNull final String id,
+ @NonNull final SearchRequest request,
+ @NonNull final ICloudSearchManagerCallback callback,
+ @NonNull final IBinder token,
+ @NonNull final IBinder.DeathRecipient deathRecipient) {
+ if (DEBUG) {
+ Slog.d(TAG, "Creating CloudSearchSessionInfo for session Id=" + id);
+ }
+ mRequestId = id;
+ mSearchRequest = request;
+ mCallback = callback;
+ mToken = token;
+ mDeathRecipient = deathRecipient;
+ }
+
+ boolean linkToDeath() {
+ try {
+ mToken.linkToDeath(mDeathRecipient, 0);
+ } catch (RemoteException e) {
+ if (DEBUG) {
+ Slog.w(TAG, "Caller is dead before session can be started, requestId: "
+ + mRequestId);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ void destroy() {
+ if (DEBUG) {
+ Slog.d(TAG, "Removing callback for Request Id=" + mRequestId);
+ }
+ if (mToken != null) {
+ mToken.unlinkToDeath(mDeathRecipient, 0);
+ }
+ mCallback.asBinder().unlinkToDeath(mDeathRecipient, 0);
+ }
+
+ void resurrectSessionLocked(CloudSearchPerUserService service, IBinder token) {
+ if (DEBUG) {
+ Slog.d(TAG, "Resurrecting remote service (" + service.getRemoteServiceLocked()
+ + ") for request Id=" + mRequestId);
+ }
+ service.onSearchLocked(mSearchRequest, mCallback);
+ }
+ }
+}
diff --git a/services/cloudsearch/java/com/android/server/cloudsearch/RemoteCloudSearchService.java b/services/cloudsearch/java/com/android/server/cloudsearch/RemoteCloudSearchService.java
new file mode 100644
index 0000000..eb16d3b
--- /dev/null
+++ b/services/cloudsearch/java/com/android/server/cloudsearch/RemoteCloudSearchService.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2022 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.cloudsearch;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.IBinder;
+import android.service.cloudsearch.ICloudSearchService;
+import android.text.format.DateUtils;
+
+import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService;
+
+
+/**
+ * Proxy to the {@link android.service.cloudsearch.CloudSearchService} implementation in another
+ * process.
+ */
+public class RemoteCloudSearchService extends
+ AbstractMultiplePendingRequestsRemoteService<RemoteCloudSearchService,
+ ICloudSearchService> {
+
+ private static final String TAG = "RemoteCloudSearchService";
+
+ private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS;
+
+ private final RemoteCloudSearchServiceCallbacks mCallback;
+
+ public RemoteCloudSearchService(Context context, String serviceInterface,
+ ComponentName componentName, int userId,
+ RemoteCloudSearchServiceCallbacks callback, boolean bindInstantServiceAllowed,
+ boolean verbose) {
+ super(context, serviceInterface, componentName, userId, callback,
+ context.getMainThreadHandler(),
+ bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0,
+ verbose, /* initialCapacity= */ 1);
+ mCallback = callback;
+ }
+
+ @Override
+ protected ICloudSearchService getServiceInterface(IBinder service) {
+ return ICloudSearchService.Stub.asInterface(service);
+ }
+
+ @Override
+ protected long getTimeoutIdleBindMillis() {
+ return PERMANENT_BOUND_TIMEOUT_MS;
+ }
+
+ @Override
+ protected long getRemoteRequestMillis() {
+ return TIMEOUT_REMOTE_REQUEST_MILLIS;
+ }
+
+ /**
+ * Schedules a request to bind to the remote service.
+ */
+ public void reconnect() {
+ super.scheduleBind();
+ }
+
+ /**
+ * Schedule async request on remote service.
+ */
+ public void scheduleOnResolvedService(@NonNull AsyncRequest<ICloudSearchService> request) {
+ scheduleAsyncRequest(request);
+ }
+
+ /**
+ * Execute async request on remote service immediately instead of sending it to Handler queue.
+ */
+ public void executeOnResolvedService(@NonNull AsyncRequest<ICloudSearchService> request) {
+ executeAsyncRequest(request);
+ }
+
+ /**
+ * Failure callback
+ */
+ public interface RemoteCloudSearchServiceCallbacks
+ extends VultureCallback<RemoteCloudSearchService> {
+
+ /**
+ * Notifies a the failure or timeout of a remote call.
+ */
+ void onFailureOrTimeout(boolean timedOut);
+
+ /**
+ * Notifies change in connected state of the remote service.
+ */
+ void onConnectedStateChanged(boolean connected);
+ }
+
+ @Override // from AbstractRemoteService
+ protected void handleOnConnectedStateChanged(boolean connected) {
+ if (mCallback != null) {
+ mCallback.onConnectedStateChanged(connected);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/CircularQueue.java b/services/core/java/com/android/server/CircularQueue.java
new file mode 100644
index 0000000..aac6752
--- /dev/null
+++ b/services/core/java/com/android/server/CircularQueue.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 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;
+
+import android.util.ArrayMap;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+/**
+ * CircularQueue of length limit which puts keys in a circular LinkedList and values in an ArrayMap.
+ * @param <K> key
+ * @param <V> value
+ */
+public class CircularQueue<K, V> extends LinkedList<K> {
+ private final int mLimit;
+ private final ArrayMap<K, V> mArrayMap = new ArrayMap<>();
+
+ public CircularQueue(int limit) {
+ this.mLimit = limit;
+ }
+
+ @Override
+ public boolean add(K k) throws IllegalArgumentException {
+ throw new IllegalArgumentException("Call of add(key) prohibited. Please call put(key, "
+ + "value) instead. ");
+ }
+
+ /**
+ * Put a (key|value) pair in the CircularQueue. Only the key will be added to the queue. Value
+ * will be added to the ArrayMap.
+ * @return {@code true} (as specified by {@link Collection#add})
+ */
+ public boolean put(K key, V value) {
+ super.add(key);
+ mArrayMap.put(key, value);
+ while (size() > mLimit) {
+ K removedKey = super.remove();
+ mArrayMap.remove(removedKey);
+ }
+ return true;
+ }
+
+ /**
+ * Removes the element for the provided key from the data structure.
+ * @param key which should be removed
+ * @return the value which was removed
+ */
+ public V removeElement(K key) {
+ super.remove(key);
+ return mArrayMap.remove(key);
+ }
+
+ /**
+ * Retrieve a value from the array.
+ * @param key The key of the value to retrieve.
+ * @return Returns the value associated with the given key,
+ * or null if there is no such key.
+ */
+ public V getElement(K key) {
+ return mArrayMap.get(key);
+ }
+
+ /**
+ * Check whether a key exists in the array.
+ *
+ * @param key The key to search for.
+ * @return Returns true if the key exists, else false.
+ */
+ public boolean containsKey(K key) {
+ return mArrayMap.containsKey(key);
+ }
+
+ /**
+ * Return a {@link java.util.Collection} for iterating over and interacting with all values
+ * in the array map.
+ *
+ * <p><b>Note:</b> this is a fairly inefficient way to access the array contents, it
+ * requires generating a number of temporary objects and allocates additional state
+ * information associated with the container that will remain for the life of the container.</p>
+ */
+ public Collection<V> values() {
+ return mArrayMap.values();
+ }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 1fe71f8..7fa0f6a 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -376,6 +376,8 @@
"com.android.server.searchui.SearchUiManagerService";
private static final String SMARTSPACE_MANAGER_SERVICE_CLASS =
"com.android.server.smartspace.SmartspaceManagerService";
+ private static final String CLOUDSEARCH_MANAGER_SERVICE_CLASS =
+ "com.android.server.cloudsearch.CloudSearchManagerService";
private static final String DEVICE_IDLE_CONTROLLER_CLASS =
"com.android.server.DeviceIdleController";
private static final String BLOB_STORE_MANAGER_SERVICE_CLASS =
@@ -1853,6 +1855,12 @@
mSystemServiceManager.startService(SMARTSPACE_MANAGER_SERVICE_CLASS);
t.traceEnd();
+ // CloudSearch manager service
+ // TODO: add deviceHasConfigString(context, R.string.config_defaultCloudSearchServices)
+ t.traceBegin("StartCloudSearchService");
+ mSystemServiceManager.startService(CLOUDSEARCH_MANAGER_SERVICE_CLASS);
+ t.traceEnd();
+
t.traceBegin("InitConnectivityModuleConnector");
try {
ConnectivityModuleConnector.getInstance().init(context);
diff --git a/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java
new file mode 100644
index 0000000..fac37fd
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+/**
+ * Test {@link CircularQueue}.
+ */
+public class CircularQueueTest {
+
+ private CircularQueue<Integer, String> mQueue;
+ private static final int LIMIT = 2;
+
+ @Test
+ public void testQueueInsertionAndDeletion() {
+ mQueue = new CircularQueue<>(LIMIT);
+ mQueue.put(1, "A");
+ assertEquals(mQueue.getElement(1), "A");
+ mQueue.removeElement(1);
+ assertNull(mQueue.getElement(1));
+ }
+
+ @Test
+ public void testQueueLimit() {
+ mQueue = new CircularQueue<>(LIMIT);
+ mQueue.put(1, "A");
+ mQueue.put(2, "B");
+ mQueue.put(3, "C");
+ assertNull(mQueue.getElement(1));
+ assertEquals(mQueue.getElement(2), "B");
+ assertEquals(mQueue.getElement(3), "C");
+
+ }
+
+ @Test
+ public void testQueueElements() {
+ mQueue = new CircularQueue<>(LIMIT);
+ mQueue.put(1, "A");
+ mQueue.put(2, "B");
+ assertEquals(mQueue.values().size(), 2);
+ mQueue.put(3, "C");
+ assertEquals(mQueue.values().size(), 2);
+ }
+}