blob: 68c906948f12550546630c97846cabdabe5dc694 [file] [log] [blame]
/*
* Copyright (C) 2020 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.appsearch;
import static android.app.appsearch.AppSearchResult.throwableToFailedResult;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.IAppSearchBatchResultCallback;
import android.app.appsearch.IAppSearchManager;
import android.app.appsearch.IAppSearchResultCallback;
import android.app.appsearch.PackageIdentifier;
import android.app.appsearch.SearchResultPage;
import android.app.appsearch.SearchSpec;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.os.Binder;
import android.os.Bundle;
import android.os.ParcelableException;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.appsearch.external.localstorage.AppSearchImpl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/** TODO(b/142567528): add comments when implement this class */
public class AppSearchManagerService extends SystemService {
private static final String TAG = "AppSearchManagerService";
private PackageManagerInternal mPackageManagerInternal;
private ImplInstanceManager mImplInstanceManager;
// Cache of unlocked user ids so we don't have to query UserManager service each time. The
// "locked" suffix refers to the fact that access to the field should be locked; unrelated to
// the unlocked status of user ids.
@GuardedBy("mUnlockedUserIdsLocked")
private final Set<Integer> mUnlockedUserIdsLocked = new ArraySet<>();
public AppSearchManagerService(Context context) {
super(context);
}
@Override
public void onStart() {
publishBinderService(Context.APP_SEARCH_SERVICE, new Stub());
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
mImplInstanceManager = ImplInstanceManager.getInstance(getContext());
}
@Override
public void onUserUnlocked(@NonNull TargetUser user) {
synchronized (mUnlockedUserIdsLocked) {
mUnlockedUserIdsLocked.add(user.getUserIdentifier());
}
}
private class Stub extends IAppSearchManager.Stub {
@Override
public void setSchema(
@NonNull String packageName,
@NonNull String databaseName,
@NonNull List<Bundle> schemaBundles,
@NonNull List<String> schemasNotPlatformSurfaceable,
@NonNull Map<String, List<Bundle>> schemasPackageAccessibleBundles,
boolean forceOverride,
@UserIdInt int userId,
@NonNull IAppSearchResultCallback callback) {
Preconditions.checkNotNull(packageName);
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(schemaBundles);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
verifyCallingPackage(callingUid, packageName);
List<AppSearchSchema> schemas = new ArrayList<>(schemaBundles.size());
for (int i = 0; i < schemaBundles.size(); i++) {
schemas.add(new AppSearchSchema(schemaBundles.get(i)));
}
Map<String, List<PackageIdentifier>> schemasPackageAccessible =
new ArrayMap<>(schemasPackageAccessibleBundles.size());
for (Map.Entry<String, List<Bundle>> entry :
schemasPackageAccessibleBundles.entrySet()) {
List<PackageIdentifier> packageIdentifiers =
new ArrayList<>(entry.getValue().size());
for (int i = 0; i < entry.getValue().size(); i++) {
packageIdentifiers.add(new PackageIdentifier(entry.getValue().get(i)));
}
schemasPackageAccessible.put(entry.getKey(), packageIdentifiers);
}
AppSearchImpl impl = mImplInstanceManager.getAppSearchImpl(callingUserId);
impl.setSchema(
packageName,
databaseName,
schemas,
schemasNotPlatformSurfaceable,
schemasPackageAccessible,
forceOverride);
invokeCallbackOnResult(
callback, AppSearchResult.newSuccessfulResult(/*result=*/ null));
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void getSchema(
@NonNull String packageName,
@NonNull String databaseName,
@UserIdInt int userId,
@NonNull IAppSearchResultCallback callback) {
Preconditions.checkNotNull(packageName);
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUidOrThrow();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
verifyCallingPackage(callingUid, packageName);
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
List<AppSearchSchema> schemas = impl.getSchema(packageName, databaseName);
List<Bundle> schemaBundles = new ArrayList<>(schemas.size());
for (int i = 0; i < schemas.size(); i++) {
schemaBundles.add(schemas.get(i).getBundle());
}
invokeCallbackOnResult(
callback, AppSearchResult.newSuccessfulResult(schemaBundles));
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void putDocuments(
@NonNull String packageName,
@NonNull String databaseName,
@NonNull List<Bundle> documentBundles,
@UserIdInt int userId,
@NonNull IAppSearchBatchResultCallback callback) {
Preconditions.checkNotNull(packageName);
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(documentBundles);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
verifyCallingPackage(callingUid, packageName);
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
for (int i = 0; i < documentBundles.size(); i++) {
GenericDocument document = new GenericDocument(documentBundles.get(i));
try {
// TODO(b/173451571): reduce burden of binder thread by enqueue request onto
// a separate thread.
impl.putDocument(packageName, databaseName, document);
resultBuilder.setSuccess(document.getUri(), /*result=*/ null);
} catch (Throwable t) {
resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
}
}
invokeCallbackOnResult(callback, resultBuilder.build());
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void getDocuments(
@NonNull String packageName,
@NonNull String databaseName,
@NonNull String namespace,
@NonNull List<String> uris,
@NonNull Map<String, List<String>> typePropertyPaths,
@UserIdInt int userId,
@NonNull IAppSearchBatchResultCallback callback) {
Preconditions.checkNotNull(packageName);
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(namespace);
Preconditions.checkNotNull(uris);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
verifyCallingPackage(callingUid, packageName);
AppSearchBatchResult.Builder<String, Bundle> resultBuilder =
new AppSearchBatchResult.Builder<>();
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
for (int i = 0; i < uris.size(); i++) {
String uri = uris.get(i);
try {
GenericDocument document =
impl.getDocument(
packageName,
databaseName,
namespace,
uri,
typePropertyPaths);
resultBuilder.setSuccess(uri, document.getBundle());
} catch (Throwable t) {
resultBuilder.setResult(uri, throwableToFailedResult(t));
}
}
invokeCallbackOnResult(callback, resultBuilder.build());
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
// TODO(sidchhabra): Do this in a threadpool.
@Override
public void query(
@NonNull String packageName,
@NonNull String databaseName,
@NonNull String queryExpression,
@NonNull Bundle searchSpecBundle,
@UserIdInt int userId,
@NonNull IAppSearchResultCallback callback) {
Preconditions.checkNotNull(packageName);
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpecBundle);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
verifyCallingPackage(callingUid, packageName);
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
SearchResultPage searchResultPage =
impl.query(
packageName,
databaseName,
queryExpression,
new SearchSpec(searchSpecBundle));
invokeCallbackOnResult(
callback,
AppSearchResult.newSuccessfulResult(searchResultPage.getBundle()));
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void globalQuery(
@NonNull String packageName,
@NonNull String queryExpression,
@NonNull Bundle searchSpecBundle,
@UserIdInt int userId,
@NonNull IAppSearchResultCallback callback) {
Preconditions.checkNotNull(packageName);
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpecBundle);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
verifyCallingPackage(callingUid, packageName);
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
SearchResultPage searchResultPage =
impl.globalQuery(
queryExpression,
new SearchSpec(searchSpecBundle),
packageName,
callingUid);
invokeCallbackOnResult(
callback,
AppSearchResult.newSuccessfulResult(searchResultPage.getBundle()));
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void getNextPage(
long nextPageToken,
@UserIdInt int userId,
@NonNull IAppSearchResultCallback callback) {
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
// TODO(b/162450968) check nextPageToken is being advanced by the same uid as originally
// opened it
try {
verifyUserUnlocked(callingUserId);
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
SearchResultPage searchResultPage = impl.getNextPage(nextPageToken);
invokeCallbackOnResult(
callback,
AppSearchResult.newSuccessfulResult(searchResultPage.getBundle()));
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void invalidateNextPageToken(long nextPageToken, @UserIdInt int userId) {
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
impl.invalidateNextPageToken(nextPageToken);
} catch (Throwable t) {
Log.e(TAG, "Unable to invalidate the query page token", t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void reportUsage(
@NonNull String packageName,
@NonNull String databaseName,
@NonNull String namespace,
@NonNull String uri,
long usageTimeMillis,
@UserIdInt int userId,
@NonNull IAppSearchResultCallback callback) {
Objects.requireNonNull(databaseName);
Objects.requireNonNull(namespace);
Objects.requireNonNull(uri);
Objects.requireNonNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
impl.reportUsage(packageName, databaseName, namespace, uri, usageTimeMillis);
invokeCallbackOnResult(
callback, AppSearchResult.newSuccessfulResult(/*result=*/ null));
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void removeByUri(
@NonNull String packageName,
@NonNull String databaseName,
@NonNull String namespace,
@NonNull List<String> uris,
@UserIdInt int userId,
@NonNull IAppSearchBatchResultCallback callback) {
Preconditions.checkNotNull(packageName);
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(uris);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
verifyCallingPackage(callingUid, packageName);
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
for (int i = 0; i < uris.size(); i++) {
String uri = uris.get(i);
try {
impl.remove(packageName, databaseName, namespace, uri);
resultBuilder.setSuccess(uri, /*result= */ null);
} catch (Throwable t) {
resultBuilder.setResult(uri, throwableToFailedResult(t));
}
}
invokeCallbackOnResult(callback, resultBuilder.build());
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void removeByQuery(
@NonNull String packageName,
@NonNull String databaseName,
@NonNull String queryExpression,
@NonNull Bundle searchSpecBundle,
@UserIdInt int userId,
@NonNull IAppSearchResultCallback callback) {
Preconditions.checkNotNull(packageName);
Preconditions.checkNotNull(databaseName);
Preconditions.checkNotNull(queryExpression);
Preconditions.checkNotNull(searchSpecBundle);
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
verifyCallingPackage(callingUid, packageName);
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
impl.removeByQuery(
packageName,
databaseName,
queryExpression,
new SearchSpec(searchSpecBundle));
invokeCallbackOnResult(callback, AppSearchResult.newSuccessfulResult(null));
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void persistToDisk(@UserIdInt int userId) {
int callingUid = Binder.getCallingUidOrThrow();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
AppSearchImpl impl =
mImplInstanceManager.getAppSearchImpl(callingUserId);
impl.persistToDisk();
} catch (Throwable t) {
Log.e(TAG, "Unable to persist the data to disk", t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
@Override
public void initialize(@UserIdInt int userId, @NonNull IAppSearchResultCallback callback) {
Preconditions.checkNotNull(callback);
int callingUid = Binder.getCallingUid();
int callingUserId = handleIncomingUser(userId, callingUid);
final long callingIdentity = Binder.clearCallingIdentity();
try {
verifyUserUnlocked(callingUserId);
mImplInstanceManager.getOrCreateAppSearchImpl(getContext(), callingUserId);
invokeCallbackOnResult(callback, AppSearchResult.newSuccessfulResult(null));
} catch (Throwable t) {
invokeCallbackOnError(callback, t);
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
}
private void verifyUserUnlocked(int callingUserId) {
synchronized (mUnlockedUserIdsLocked) {
if (!mUnlockedUserIdsLocked.contains(callingUserId)) {
throw new IllegalStateException(
"User " + callingUserId + " is locked or not running.");
}
}
}
private void verifyCallingPackage(int callingUid, @NonNull String callingPackage) {
Preconditions.checkNotNull(callingPackage);
if (mPackageManagerInternal.getPackageUid(
callingPackage, /*flags=*/ 0, UserHandle.getUserId(callingUid))
!= callingUid) {
throw new SecurityException(
"Specified calling package ["
+ callingPackage
+ "] does not match the calling uid "
+ callingUid);
}
}
/** Invokes the {@link IAppSearchResultCallback} with the result. */
private void invokeCallbackOnResult(
IAppSearchResultCallback callback, AppSearchResult<?> result) {
try {
callback.onResult(result);
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
/** Invokes the {@link IAppSearchBatchResultCallback} with the result. */
private void invokeCallbackOnResult(
IAppSearchBatchResultCallback callback, AppSearchBatchResult<?, ?> result) {
try {
callback.onResult(result);
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
/**
* Invokes the {@link IAppSearchResultCallback} with an throwable.
*
* <p>The throwable is convert to a {@link AppSearchResult};
*/
private void invokeCallbackOnError(IAppSearchResultCallback callback, Throwable throwable) {
try {
callback.onResult(throwableToFailedResult(throwable));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
/**
* Invokes the {@link IAppSearchBatchResultCallback} with an unexpected internal throwable.
*
* <p>The throwable is converted to {@link ParcelableException}.
*/
private void invokeCallbackOnError(
IAppSearchBatchResultCallback callback, Throwable throwable) {
try {
//TODO(b/175067650) verify ParcelableException could propagate throwable correctly.
callback.onSystemError(new ParcelableException(throwable));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send error to the callback", e);
}
}
}
// TODO(b/173553485) verifying that the caller has permission to access target user's data
// TODO(b/173553485) Handle ACTION_USER_REMOVED broadcast
// TODO(b/173553485) Implement SystemService.onUserStopping()
private static int handleIncomingUser(@UserIdInt int userId, int callingUid) {
int callingPid = Binder.getCallingPid();
return ActivityManager.handleIncomingUser(
callingPid,
callingUid,
userId,
/*allowAll=*/ false,
/*requireFull=*/ false,
/*name=*/ null,
/*callerPackage=*/ null);
}
}