From f898abc490de6a0b5ac3ca23fb52ff5ffafdaa15 Mon Sep 17 00:00:00 2001 From: Shashwat Razdan Date: Fri, 21 Jan 2022 16:39:46 -0800 Subject: [PATCH] Adding System Server implementation of Cloud Search API Bug: 210528288 Test: RBE Built redfin-eng and tested on a physical device Change-Id: Idac2e83819f9aeee292a5d53f3304d47cfc88af5 --- services/Android.bp | 2 + services/cloudsearch/Android.bp | 22 + .../CloudSearchManagerService.java | 166 ++++++++ ...CloudSearchManagerServiceShellCommand.java | 84 ++++ .../CloudSearchPerUserService.java | 376 ++++++++++++++++++ .../cloudsearch/RemoteCloudSearchService.java | 112 ++++++ .../com/android/server/CircularQueue.java | 99 +++++ .../java/com/android/server/SystemServer.java | 8 + .../com/android/server/CircularQueueTest.java | 62 +++ 9 files changed, 931 insertions(+) create mode 100644 services/cloudsearch/Android.bp create mode 100644 services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchManagerService.java create mode 100644 services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchManagerServiceShellCommand.java create mode 100644 services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java create mode 100644 services/cloudsearch/java/com/android/server/cloudsearch/RemoteCloudSearchService.java create mode 100644 services/core/java/com/android/server/CircularQueue.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java diff --git a/services/Android.bp b/services/Android.bp index 26760aa3765a1..af70692a88e5e 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -79,6 +79,7 @@ filegroup { ":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 @@ java_library { "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 0000000000000..e38e6153016fd --- /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 0000000000000..dafe7a470de0b --- /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 { + + 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 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 0000000000000..51f5fd9668cf7 --- /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 0000000000000..32d66afb33b92 --- /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 + implements RemoteCloudSearchService.RemoteCloudSearchServiceCallbacks { + + private static final String TAG = CloudSearchPerUserService.class.getSimpleName(); + private static final int QUEUE_SIZE = 10; + @GuardedBy("mLock") + private final CircularQueue 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 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 0000000000000..eb16d3bdbd1b1 --- /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 { + + 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 request) { + scheduleAsyncRequest(request); + } + + /** + * Execute async request on remote service immediately instead of sending it to Handler queue. + */ + public void executeOnResolvedService(@NonNull AsyncRequest request) { + executeAsyncRequest(request); + } + + /** + * Failure callback + */ + public interface RemoteCloudSearchServiceCallbacks + extends VultureCallback { + + /** + * 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 0000000000000..aac6752981d7d --- /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 key + * @param value + */ +public class CircularQueue extends LinkedList { + private final int mLimit; + private final ArrayMap 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. + * + *

Note: 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.

+ */ + public Collection values() { + return mArrayMap.values(); + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 1fe71f8e13a14..7fa0f6a47920e 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -376,6 +376,8 @@ public final class SystemServer implements Dumpable { "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 @@ public final class SystemServer implements Dumpable { 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 0000000000000..fac37fd5a481f --- /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 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); + } +}