Merge "Adding System Server implementation of Cloud Search API"

This commit is contained in:
Shashwat Razdan
2022-01-27 09:09:14 +00:00
committed by Android (Google) Code Review
9 changed files with 931 additions and 0 deletions

View File

@@ -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",

View File

@@ -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"],
}

View File

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

View File

@@ -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("");
}
}
}

View File

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

View File

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

View File

@@ -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();
}
}

View File

@@ -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);

View File

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