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);
+    }
+}