Update framework from Jetpack.
Changes included:
* 579fbfb: Create a CallerAccess object to encapsulate (package, uid, hasSystemAccess).
* f431798: Send schema change notifications.
Bug: 215624105
Bug: 193494000
Test: Presubmit
Change-Id: I34f35e28ed50936e14b0d5dd572dfe106935b338
diff --git a/service/java/com/android/server/appsearch/AppSearchManagerService.java b/service/java/com/android/server/appsearch/AppSearchManagerService.java
index c16021c..91a4e2e 100644
--- a/service/java/com/android/server/appsearch/AppSearchManagerService.java
+++ b/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -63,6 +63,7 @@
import com.android.server.SystemService;
import com.android.server.appsearch.external.localstorage.stats.CallStats;
import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
+import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
import com.android.server.appsearch.observer.AppSearchObserverProxy;
import com.android.server.appsearch.stats.StatsCollector;
@@ -437,10 +438,12 @@
instance.getAppSearchImpl().getSchema(
packageName,
databaseName,
- callingPackageName,
- callingUid,
- instance.getVisibilityChecker()
- .doesCallerHaveSystemAccess(callingPackageName));
+ new CallerAccess(
+ callingPackageName,
+ callingUid,
+ instance.getVisibilityChecker()
+ .doesCallerHaveSystemAccess(
+ callingPackageName)));
invokeCallbackOnResult(
callback,
AppSearchResult.newSuccessfulResult(response.getBundle()));
@@ -629,10 +632,12 @@
namespace,
id,
typePropertyPaths,
- callingPackageName,
- callingUid,
- instance.getVisibilityChecker()
- .doesCallerHaveSystemAccess(callingPackageName));
+ new CallerAccess(
+ callingPackageName,
+ callingUid,
+ instance.getVisibilityChecker()
+ .doesCallerHaveSystemAccess(
+ callingPackageName)));
} else {
document = instance.getAppSearchImpl().getDocument(
targetPackageName,
@@ -792,9 +797,10 @@
SearchResultPage searchResultPage = instance.getAppSearchImpl().globalQuery(
queryExpression,
new SearchSpec(searchSpecBundle),
- packageName,
- callingUid,
- callerHasSystemAccess,
+ new CallerAccess(
+ packageName,
+ callingUid,
+ callerHasSystemAccess),
instance.getLogger());
++operationSuccessCount;
invokeCallbackOnResult(
@@ -1351,9 +1357,11 @@
AppSearchUserInstance instance =
mAppSearchUserInstanceManager.getUserInstance(targetUser);
instance.getAppSearchImpl().addObserver(
- callingPackage,
- callingUid,
- instance.getVisibilityChecker().doesCallerHaveSystemAccess(callingPackage),
+ new CallerAccess(
+ callingPackage,
+ callingUid,
+ instance.getVisibilityChecker()
+ .doesCallerHaveSystemAccess(callingPackage)),
targetPackageName,
new ObserverSpec(observerSpecBundle),
EXECUTOR,
diff --git a/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java b/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java
index 9e8c1b1..0afce10 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java
@@ -45,6 +45,7 @@
import android.app.appsearch.observer.ObserverSpec;
import android.app.appsearch.util.LogUtil;
import android.os.Bundle;
+import android.os.Process;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -66,6 +67,7 @@
import com.android.server.appsearch.external.localstorage.stats.SearchStats;
import com.android.server.appsearch.external.localstorage.stats.SetSchemaStats;
import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
+import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityUtil;
@@ -472,126 +474,312 @@
mReadWriteLock.writeLock().lock();
try {
throwIfClosedLocked();
-
- SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder();
-
- SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
- for (int i = 0; i < schemas.size(); i++) {
- AppSearchSchema schema = schemas.get(i);
- SchemaTypeConfigProto schemaTypeProto =
- SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version);
- newSchemaBuilder.addTypes(schemaTypeProto);
+ if (mObserverManager.isPackageObserved(packageName)) {
+ return doSetSchemaWithChangeNotificationLocked(
+ packageName,
+ databaseName,
+ schemas,
+ visibilityDocuments,
+ forceOverride,
+ version,
+ setSchemaStatsBuilder);
+ } else {
+ return doSetSchemaNoChangeNotificationLocked(
+ packageName,
+ databaseName,
+ schemas,
+ visibilityDocuments,
+ forceOverride,
+ version,
+ setSchemaStatsBuilder);
}
-
- String prefix = createPrefix(packageName, databaseName);
- // Combine the existing schema (which may have types from other prefixes) with this
- // prefix's new schema. Modifies the existingSchemaBuilder.
- RewrittenSchemaResults rewrittenSchemaResults =
- rewriteSchema(prefix, existingSchemaBuilder, newSchemaBuilder.build());
-
- // Apply schema
- SchemaProto finalSchema = existingSchemaBuilder.build();
- mLogUtil.piiTrace("setSchema, request", finalSchema.getTypesCount(), finalSchema);
- SetSchemaResultProto setSchemaResultProto =
- mIcingSearchEngineLocked.setSchema(finalSchema, forceOverride);
- mLogUtil.piiTrace(
- "setSchema, response", setSchemaResultProto.getStatus(), setSchemaResultProto);
-
- if (setSchemaStatsBuilder != null) {
- setSchemaStatsBuilder.setStatusCode(
- statusProtoToResultCode(setSchemaResultProto.getStatus()));
- AppSearchLoggerHelper.copyNativeStats(setSchemaResultProto, setSchemaStatsBuilder);
- }
-
- // Determine whether it succeeded.
- try {
- checkSuccess(setSchemaResultProto.getStatus());
- } catch (AppSearchException e) {
- // Swallow the exception for the incompatible change case. We will propagate
- // those deleted schemas and incompatible types to the SetSchemaResponse.
- boolean isFailedPrecondition =
- setSchemaResultProto.getStatus().getCode()
- == StatusProto.Code.FAILED_PRECONDITION;
- boolean isIncompatible =
- setSchemaResultProto.getDeletedSchemaTypesCount() > 0
- || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0;
- if (isFailedPrecondition && isIncompatible) {
- return SetSchemaResponseToProtoConverter.toSetSchemaResponse(
- setSchemaResultProto, prefix);
- } else {
- throw e;
- }
- }
-
- // Update derived data structures.
- for (SchemaTypeConfigProto schemaTypeConfigProto :
- rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
- addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
- }
-
- for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
- removeFromMap(mSchemaMapLocked, prefix, schemaType);
- }
- // Since the constructor of VisibilityStore will set schema. Avoid call visibility
- // store before we have already created it.
- if (mVisibilityStoreLocked != null) {
- // Add prefix to all visibility documents.
- List<VisibilityDocument> prefixedVisibilityDocuments =
- new ArrayList<>(visibilityDocuments.size());
- // Find out which Visibility document is deleted or changed to all-default settings.
- // We need to remove them from Visibility Store.
- Set<String> deprecatedVisibilityDocuments =
- new ArraySet<>(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet());
- for (int i = 0; i < visibilityDocuments.size(); i++) {
- VisibilityDocument unPrefixedDocument = visibilityDocuments.get(i);
- // The VisibilityDocument is controlled by the client and it's untrusted but we
- // make it safe by appending a prefix.
- // We must control the package-database prefix. Therefore even if the client
- // fake the id, they can only mess their own app. That's totally allowed and
- // they can do this via the public API too.
- String prefixedSchemaType = prefix + unPrefixedDocument.getId();
- prefixedVisibilityDocuments.add(
- new VisibilityDocument(
- unPrefixedDocument.toBuilder()
- .setId(prefixedSchemaType)
- .build()));
- // This schema has visibility settings. We should keep it from the removal list.
- deprecatedVisibilityDocuments.remove(prefixedSchemaType);
- }
- // Now deprecatedVisibilityDocuments contains those existing schemas that has
- // all-default visibility settings, add deleted schemas. That's all we need to
- // remove.
- deprecatedVisibilityDocuments.addAll(rewrittenSchemaResults.mDeletedPrefixedTypes);
- mVisibilityStoreLocked.removeVisibility(deprecatedVisibilityDocuments);
- mVisibilityStoreLocked.setVisibility(prefixedVisibilityDocuments);
- }
- return SetSchemaResponseToProtoConverter.toSetSchemaResponse(
- setSchemaResultProto, prefix);
} finally {
mReadWriteLock.writeLock().unlock();
}
}
/**
+ * Updates the AppSearch schema for this app, dispatching change notifications.
+ *
+ * @see #setSchema
+ * @see #doSetSchemaNoChangeNotificationLocked
+ */
+ @GuardedBy("mReadWriteLock")
+ @NonNull
+ private SetSchemaResponse doSetSchemaWithChangeNotificationLocked(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull List<AppSearchSchema> schemas,
+ @NonNull List<VisibilityDocument> visibilityDocuments,
+ boolean forceOverride,
+ int version,
+ @Nullable SetSchemaStats.Builder setSchemaStatsBuilder)
+ throws AppSearchException {
+ // First, capture the old state of the system. This includes the old schema as well as
+ // whether each registered observer can access each type. Once VisibilityStore is updated
+ // by the setSchema call, the information of which observers could see which types will be
+ // lost.
+ GetSchemaResponse oldSchema =
+ getSchema(
+ packageName,
+ databaseName,
+ // A CallerAccess object for internal use that has local access to this
+ // database.
+ new CallerAccess(
+ /*callingPackageName=*/ packageName,
+ // The below two settings don't matter since callingPackageName
+ // matches
+ /*callingUid=*/ Process.INVALID_UID,
+ /*callerHasSystemAccess=*/ false));
+
+ // Cache some lookup tables to help us work with the old schema
+ Set<AppSearchSchema> oldSchemaTypes = oldSchema.getSchemas();
+ Map<String, AppSearchSchema> oldSchemaNameToType = new ArrayMap<>(oldSchemaTypes.size());
+ // Maps unprefixed schema name to the set of listening packages that had visibility into
+ // that type under the old schema.
+ Map<String, Set<String>> oldSchemaNameToVisibleListeningPackage =
+ new ArrayMap<>(oldSchemaTypes.size());
+ for (AppSearchSchema oldSchemaType : oldSchemaTypes) {
+ String oldSchemaName = oldSchemaType.getSchemaType();
+ oldSchemaNameToType.put(oldSchemaName, oldSchemaType);
+ oldSchemaNameToVisibleListeningPackage.put(
+ oldSchemaName,
+ mObserverManager.getObserversForSchemaType(
+ packageName,
+ databaseName,
+ oldSchemaName,
+ mVisibilityStoreLocked,
+ mVisibilityCheckerLocked));
+ }
+
+ // Apply the new schema
+ SetSchemaResponse setSchemaResponse =
+ doSetSchemaNoChangeNotificationLocked(
+ packageName,
+ databaseName,
+ schemas,
+ visibilityDocuments,
+ forceOverride,
+ version,
+ setSchemaStatsBuilder);
+
+ // Cache some lookup tables to help us work with the new schema
+ Map<String, AppSearchSchema> newSchemaNameToType = new ArrayMap<>(schemas.size());
+ // Maps unprefixed schema name to the set of listening packages that have visibility into
+ // that type under the new schema.
+ Map<String, Set<String>> newSchemaNameToVisibleListeningPackage =
+ new ArrayMap<>(schemas.size());
+ for (AppSearchSchema newSchemaType : schemas) {
+ String newSchemaName = newSchemaType.getSchemaType();
+ newSchemaNameToType.put(newSchemaName, newSchemaType);
+ newSchemaNameToVisibleListeningPackage.put(
+ newSchemaName,
+ mObserverManager.getObserversForSchemaType(
+ packageName,
+ databaseName,
+ newSchemaName,
+ mVisibilityStoreLocked,
+ mVisibilityCheckerLocked));
+ }
+
+ // Create a unified set of all schema names mentioned in either the old or new schema.
+ Set<String> allSchemaNames = new ArraySet<>(oldSchemaNameToType.keySet());
+ allSchemaNames.addAll(newSchemaNameToType.keySet());
+
+ // Perform the diff between the old and new schema.
+ for (String schemaName : allSchemaNames) {
+ final AppSearchSchema contentBefore = oldSchemaNameToType.get(schemaName);
+ final AppSearchSchema contentAfter = newSchemaNameToType.get(schemaName);
+
+ final boolean existBefore = (contentBefore != null);
+ final boolean existAfter = (contentAfter != null);
+
+ // This should never happen
+ if (!existBefore && !existAfter) {
+ continue;
+ }
+
+ boolean contentsChanged = true;
+ if (existBefore && existAfter && contentBefore.equals(contentAfter)) {
+ contentsChanged = false;
+ }
+
+ Set<String> oldVisibleListeners =
+ oldSchemaNameToVisibleListeningPackage.get(schemaName);
+ Set<String> newVisibleListeners =
+ newSchemaNameToVisibleListeningPackage.get(schemaName);
+ Set<String> allListeningPackages = new ArraySet<>(oldVisibleListeners);
+ if (newVisibleListeners != null) {
+ allListeningPackages.addAll(newVisibleListeners);
+ }
+
+ // Now that we've computed the relationship between the old and new schema, we go
+ // observer by observer and consider the observer's own personal view of the schema.
+ for (String listeningPackageName : allListeningPackages) {
+ // Figure out the visibility
+ final boolean visibleBefore =
+ (existBefore
+ && oldVisibleListeners != null
+ && oldVisibleListeners.contains(listeningPackageName));
+ final boolean visibleAfter =
+ (existAfter
+ && newVisibleListeners != null
+ && newVisibleListeners.contains(listeningPackageName));
+
+ // Now go through the truth table of all the relevant flags.
+ // visibleBefore and visibleAfter take into account existBefore and existAfter, so
+ // we can stop worrying about existBefore and existAfter.
+ boolean sendNotification = false;
+ if (visibleBefore && visibleAfter && contentsChanged) {
+ sendNotification = true; // Type configuration was modified
+ } else if (!visibleBefore && visibleAfter) {
+ sendNotification = true; // Newly granted visibility or type was created
+ } else if (visibleBefore && !visibleAfter) {
+ sendNotification = true; // Revoked visibility or type was deleted
+ } else {
+ // No visibility before and no visibility after. Nothing to dispatch.
+ }
+
+ if (sendNotification) {
+ mObserverManager.onSchemaChange(
+ /*listeningPackageName=*/ listeningPackageName,
+ /*targetPackageName=*/ packageName,
+ /*databaseName=*/ databaseName,
+ /*schemaName=*/ schemaName);
+ }
+ }
+ }
+
+ return setSchemaResponse;
+ }
+
+ /**
+ * Updates the AppSearch schema for this app, without dispatching change notifications.
+ *
+ * <p>This method can be used only when no one is observing {@code packageName}.
+ *
+ * @see #setSchema
+ * @see #doSetSchemaWithChangeNotificationLocked
+ */
+ @GuardedBy("mReadWriteLock")
+ @NonNull
+ private SetSchemaResponse doSetSchemaNoChangeNotificationLocked(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull List<AppSearchSchema> schemas,
+ @NonNull List<VisibilityDocument> visibilityDocuments,
+ boolean forceOverride,
+ int version,
+ @Nullable SetSchemaStats.Builder setSchemaStatsBuilder)
+ throws AppSearchException {
+ SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder();
+
+ SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
+ for (int i = 0; i < schemas.size(); i++) {
+ AppSearchSchema schema = schemas.get(i);
+ SchemaTypeConfigProto schemaTypeProto =
+ SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version);
+ newSchemaBuilder.addTypes(schemaTypeProto);
+ }
+
+ String prefix = createPrefix(packageName, databaseName);
+ // Combine the existing schema (which may have types from other prefixes) with this
+ // prefix's new schema. Modifies the existingSchemaBuilder.
+ RewrittenSchemaResults rewrittenSchemaResults =
+ rewriteSchema(prefix, existingSchemaBuilder, newSchemaBuilder.build());
+
+ // Apply schema
+ SchemaProto finalSchema = existingSchemaBuilder.build();
+ mLogUtil.piiTrace("setSchema, request", finalSchema.getTypesCount(), finalSchema);
+ SetSchemaResultProto setSchemaResultProto =
+ mIcingSearchEngineLocked.setSchema(finalSchema, forceOverride);
+ mLogUtil.piiTrace(
+ "setSchema, response", setSchemaResultProto.getStatus(), setSchemaResultProto);
+
+ if (setSchemaStatsBuilder != null) {
+ setSchemaStatsBuilder.setStatusCode(
+ statusProtoToResultCode(setSchemaResultProto.getStatus()));
+ AppSearchLoggerHelper.copyNativeStats(setSchemaResultProto, setSchemaStatsBuilder);
+ }
+
+ // Determine whether it succeeded.
+ try {
+ checkSuccess(setSchemaResultProto.getStatus());
+ } catch (AppSearchException e) {
+ // Swallow the exception for the incompatible change case. We will propagate
+ // those deleted schemas and incompatible types to the SetSchemaResponse.
+ boolean isFailedPrecondition =
+ setSchemaResultProto.getStatus().getCode()
+ == StatusProto.Code.FAILED_PRECONDITION;
+ boolean isIncompatible =
+ setSchemaResultProto.getDeletedSchemaTypesCount() > 0
+ || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0;
+ if (isFailedPrecondition && isIncompatible) {
+ return SetSchemaResponseToProtoConverter.toSetSchemaResponse(
+ setSchemaResultProto, prefix);
+ } else {
+ throw e;
+ }
+ }
+
+ // Update derived data structures.
+ for (SchemaTypeConfigProto schemaTypeConfigProto :
+ rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
+ addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
+ }
+
+ for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
+ removeFromMap(mSchemaMapLocked, prefix, schemaType);
+ }
+ // Since the constructor of VisibilityStore will set schema. Avoid call visibility
+ // store before we have already created it.
+ if (mVisibilityStoreLocked != null) {
+ // Add prefix to all visibility documents.
+ List<VisibilityDocument> prefixedVisibilityDocuments =
+ new ArrayList<>(visibilityDocuments.size());
+ // Find out which Visibility document is deleted or changed to all-default settings.
+ // We need to remove them from Visibility Store.
+ Set<String> deprecatedVisibilityDocuments =
+ new ArraySet<>(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet());
+ for (int i = 0; i < visibilityDocuments.size(); i++) {
+ VisibilityDocument unPrefixedDocument = visibilityDocuments.get(i);
+ // The VisibilityDocument is controlled by the client and it's untrusted but we
+ // make it safe by appending a prefix.
+ // We must control the package-database prefix. Therefore even if the client
+ // fake the id, they can only mess their own app. That's totally allowed and
+ // they can do this via the public API too.
+ String prefixedSchemaType = prefix + unPrefixedDocument.getId();
+ prefixedVisibilityDocuments.add(
+ new VisibilityDocument(
+ unPrefixedDocument.toBuilder().setId(prefixedSchemaType).build()));
+ // This schema has visibility settings. We should keep it from the removal list.
+ deprecatedVisibilityDocuments.remove(prefixedSchemaType);
+ }
+ // Now deprecatedVisibilityDocuments contains those existing schemas that has
+ // all-default visibility settings, add deleted schemas. That's all we need to
+ // remove.
+ deprecatedVisibilityDocuments.addAll(rewrittenSchemaResults.mDeletedPrefixedTypes);
+ mVisibilityStoreLocked.removeVisibility(deprecatedVisibilityDocuments);
+ mVisibilityStoreLocked.setVisibility(prefixedVisibilityDocuments);
+ }
+ return SetSchemaResponseToProtoConverter.toSetSchemaResponse(setSchemaResultProto, prefix);
+ }
+
+ /**
* Retrieves the AppSearch schema for this package name, database.
*
* <p>This method belongs to query group.
*
- * @param callerPackageName Package name of the calling app
* @param packageName Package that owns the requested {@link AppSearchSchema} instances.
* @param databaseName Database that owns the requested {@link AppSearchSchema} instances.
+ * @param callerAccess Visibility access info of the calling app
* @throws AppSearchException on IcingSearchEngine error.
*/
- // TODO(b/215624105): The combination of (callerPackageName, callerUid, callerHasSystemAccess)
- // occurs together in many places related to visibility. Should these be combined into a struct
- // called something like CallerAccess?
@NonNull
public GetSchemaResponse getSchema(
@NonNull String packageName,
@NonNull String databaseName,
- @NonNull String callerPackageName,
- int callerUid,
- boolean callerHasSystemAccess)
+ @NonNull CallerAccess callerAccess)
throws AppSearchException {
mReadWriteLock.readLock().lock();
try {
@@ -611,9 +799,7 @@
continue;
}
if (!VisibilityUtil.isSchemaSearchableByCaller(
- callerPackageName,
- callerUid,
- callerHasSystemAccess,
+ callerAccess,
packageName,
prefixedSchemaType,
mVisibilityStoreLocked,
@@ -892,22 +1078,18 @@
* @param id The ID of the document to get.
* @param typePropertyPaths A map of schema type to a list of property paths to return in the
* result.
- * @param callerPackageName The package name of the caller application
- * @param callerUid The ID of the caller application
- * @param callerHasSystemAccess A boolean signifying if the caller has system access
+ * @param callerAccess Visibility access info of the calling app
* @return The Document contents
* @throws AppSearchException on IcingSearchEngine error or invalid permissions
*/
- @Nullable
+ @NonNull
public GenericDocument globalGetDocument(
@NonNull String packageName,
@NonNull String databaseName,
@NonNull String namespace,
@NonNull String id,
@NonNull Map<String, List<String>> typePropertyPaths,
- @NonNull String callerPackageName,
- int callerUid,
- boolean callerHasSystemAccess)
+ @NonNull CallerAccess callerAccess)
throws AppSearchException {
mReadWriteLock.readLock().lock();
try {
@@ -921,9 +1103,7 @@
packageName, databaseName, namespace, id, typePropertyPaths);
if (!VisibilityUtil.isSchemaSearchableByCaller(
- callerPackageName,
- callerUid,
- callerHasSystemAccess,
+ callerAccess,
packageName,
documentProto.getSchema(),
mVisibilityStoreLocked,
@@ -969,7 +1149,6 @@
@NonNull String id,
@NonNull Map<String, List<String>> typePropertyPaths)
throws AppSearchException {
-
mReadWriteLock.readLock().lock();
try {
throwIfClosedLocked();
@@ -1123,11 +1302,7 @@
*
* @param queryExpression Query String to search.
* @param searchSpec Spec for setting filters, raw query etc.
- * @param callerPackageName Package name of the caller, should belong to the {@code
- * callerUserHandle}.
- * @param callerUid UID of the client making the globalQuery call.
- * @param callerHasSystemAccess Whether the caller has been positively identified as having
- * access to schemas marked system surfaceable.
+ * @param callerAccess Visibility access info of the calling app
* @param logger logger to collect globalQuery stats
* @return The results of performing this search. It may contain an empty list of results if no
* documents matched the query.
@@ -1137,16 +1312,16 @@
public SearchResultPage globalQuery(
@NonNull String queryExpression,
@NonNull SearchSpec searchSpec,
- @NonNull String callerPackageName,
- int callerUid,
- boolean callerHasSystemAccess,
+ @NonNull CallerAccess callerAccess,
@Nullable AppSearchLogger logger)
throws AppSearchException {
long totalLatencyStartMillis = SystemClock.elapsedRealtime();
SearchStats.Builder sStatsBuilder = null;
if (logger != null) {
sStatsBuilder =
- new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_GLOBAL, callerPackageName);
+ new SearchStats.Builder(
+ SearchStats.VISIBILITY_SCOPE_GLOBAL,
+ callerAccess.getCallingPackageName());
}
mReadWriteLock.readLock().lock();
@@ -1175,11 +1350,7 @@
searchSpec, prefixFilters, mNamespaceMapLocked, mSchemaMapLocked);
// Remove those inaccessible schemas.
searchSpecToProtoConverter.removeInaccessibleSchemaFilter(
- callerPackageName,
- callerUid,
- callerHasSystemAccess,
- mVisibilityStoreLocked,
- mVisibilityCheckerLocked);
+ callerAccess, mVisibilityStoreLocked, mVisibilityCheckerLocked);
if (searchSpecToProtoConverter.isNothingToSearch()) {
// there is nothing to search over given their search filters, so we can return an
// empty SearchResult and skip sending request to Icing.
@@ -1187,7 +1358,8 @@
}
SearchResultPage searchResultPage =
doQueryLocked(queryExpression, searchSpecToProtoConverter, sStatsBuilder);
- addNextPageToken(callerPackageName, searchResultPage.getNextPageToken());
+ addNextPageToken(
+ callerAccess.getCallingPackageName(), searchResultPage.getNextPageToken());
return searchResultPage;
} finally {
mReadWriteLock.readLock().unlock();
@@ -2225,10 +2397,8 @@
* will not queue behind I/O. Therefore it is safe to call from any thread including UI or
* binder threads.
*
- * @param listeningPackageName The package name of the app that wants to receive notifications.
- * @param listeningUid The uid of the app that wants to receive notifications.
- * @param listeningPackageHasSystemAccess Whether the app that wants to receive notifications
- * has access to schema types marked 'visible to system'.
+ * @param listeningPackageAccess Visibility information about the app that wants to receive
+ * notifications.
* @param targetPackageName The package that owns the data the observer wants to be notified
* for.
* @param spec Describes the kind of data changes the observer should trigger for.
@@ -2237,9 +2407,7 @@
* @param observer The callback to trigger on notifications.
*/
public void addObserver(
- @NonNull String listeningPackageName,
- int listeningUid,
- boolean listeningPackageHasSystemAccess,
+ @NonNull CallerAccess listeningPackageAccess,
@NonNull String targetPackageName,
@NonNull ObserverSpec spec,
@NonNull Executor executor,
@@ -2249,13 +2417,7 @@
// being created or removed. If we only registered observer for existing types, it would
// be impossible to ever dispatch a notification of a type being added.
mObserverManager.addObserver(
- listeningPackageName,
- listeningUid,
- listeningPackageHasSystemAccess,
- targetPackageName,
- spec,
- executor,
- observer);
+ listeningPackageAccess, targetPackageName, spec, executor, observer);
}
/**
diff --git a/service/java/com/android/server/appsearch/external/localstorage/ObserverManager.java b/service/java/com/android/server/appsearch/external/localstorage/ObserverManager.java
index ecc7f90..bbd2680 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/ObserverManager.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/ObserverManager.java
@@ -21,17 +21,20 @@
import android.app.appsearch.observer.AppSearchObserverCallback;
import android.app.appsearch.observer.DocumentChangeInfo;
import android.app.appsearch.observer.ObserverSpec;
+import android.app.appsearch.observer.SchemaChangeInfo;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
+import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityUtil;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -86,26 +89,22 @@
private static final class ObserverInfo {
/** The package which registered the observer. */
- final String mListeningPackageName;
+ final CallerAccess mListeningPackageAccess;
- final int mListeningUid;
- final boolean mListeningPackageHasSystemAccess;
final ObserverSpec mObserverSpec;
final Executor mExecutor;
final AppSearchObserverCallback mObserver;
// Values is a set of document IDs
volatile Map<DocumentChangeGroupKey, Set<String>> mDocumentChanges = new ArrayMap<>();
+ // Keys are database prefixes, values are a set of schema names
+ volatile Map<String, Set<String>> mSchemaChanges = new ArrayMap<>();
ObserverInfo(
- @NonNull String listeningPackageName,
- int listeningUid,
- boolean listeningPackageHasSystemAccess,
+ @NonNull CallerAccess listeningPackageAccess,
@NonNull ObserverSpec observerSpec,
@NonNull Executor executor,
@NonNull AppSearchObserverCallback observer) {
- mListeningPackageName = Objects.requireNonNull(listeningPackageName);
- mListeningUid = listeningUid;
- mListeningPackageHasSystemAccess = listeningPackageHasSystemAccess;
+ mListeningPackageAccess = Objects.requireNonNull(listeningPackageAccess);
mObserverSpec = Objects.requireNonNull(observerSpec);
mExecutor = Objects.requireNonNull(executor);
mObserver = Objects.requireNonNull(observer);
@@ -114,7 +113,7 @@
private final Object mLock = new Object();
- /** Maps observed package to observer infos watching something in that package. */
+ /** Maps target packages to ObserverInfos watching something in that package. */
@GuardedBy("mLock")
private final Map<String, List<ObserverInfo>> mObserversLocked = new ArrayMap<>();
@@ -137,10 +136,8 @@
* will not queue behind I/O. Therefore it is safe to call from any thread including UI or
* binder threads.
*
- * @param listeningPackageName The package name of the app that wants to receive notifications.
- * @param listeningUid The uid of the app that wants to receive notifications.
- * @param listeningPackageHasSystemAccess Whether the app that wants to receive notifications
- * has access to schema types marked 'visible to system'.
+ * @param listeningPackageAccess Visibility information about the app that wants to receive
+ * notifications.
* @param targetPackageName The package that owns the data the observer wants to be notified
* for.
* @param spec Describes the kind of data changes the observer should trigger for.
@@ -149,9 +146,7 @@
* @param observer The callback to trigger on notifications.
*/
public void addObserver(
- @NonNull String listeningPackageName,
- int listeningUid,
- boolean listeningPackageHasSystemAccess,
+ @NonNull CallerAccess listeningPackageAccess,
@NonNull String targetPackageName,
@NonNull ObserverSpec spec,
@NonNull Executor executor,
@@ -162,14 +157,7 @@
infos = new ArrayList<>();
mObserversLocked.put(targetPackageName, infos);
}
- infos.add(
- new ObserverInfo(
- listeningPackageName,
- listeningUid,
- listeningPackageHasSystemAccess,
- spec,
- executor,
- observer));
+ infos.add(new ObserverInfo(listeningPackageAccess, spec, executor, observer));
}
}
@@ -220,18 +208,15 @@
return; // No observers for this type
}
// Enqueue changes for later dispatch once the call returns
+ String prefixedSchema = PrefixUtil.createPrefix(packageName, databaseName) + schemaType;
DocumentChangeGroupKey key = null;
for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
if (!matchesSpec(schemaType, observerInfo.mObserverSpec)) {
continue; // Observer doesn't want this notification
}
- String prefixedSchema =
- PrefixUtil.createPrefix(packageName, databaseName) + schemaType;
if (!VisibilityUtil.isSchemaSearchableByCaller(
- /*callerPackageName=*/ observerInfo.mListeningPackageName,
- observerInfo.mListeningUid,
- observerInfo.mListeningPackageHasSystemAccess,
+ /*callerAccess=*/ observerInfo.mListeningPackageAccess,
/*targetPackageName=*/ packageName,
/*prefixedSchema=*/ prefixedSchema,
visibilityStore,
@@ -254,6 +239,60 @@
}
}
+ /**
+ * Enqueues a change to a schema type for a single observer.
+ *
+ * <p>The notification will be queued in memory for later dispatch. You must call {@link
+ * #dispatchAndClearPendingNotifications} to dispatch all such pending notifications.
+ *
+ * <p>Note that unlike {@link #onDocumentChange}, the changes reported here are not dropped for
+ * observers that don't have visibility. This is because the observer might have had visibility
+ * before the schema change, and a final deletion needs to be sent to it. Caller is responsible
+ * for checking visibility of these notifications.
+ *
+ * @param listeningPackageName Name of package that subscribed to notifications and has been
+ * validated by the caller to have the right access to receive this notification.
+ * @param targetPackageName Name of package that owns the changed schema types.
+ * @param databaseName Database in which the changed schema types reside.
+ * @param schemaName Unprefixed name of the changed schema type.
+ */
+ public void onSchemaChange(
+ @NonNull String listeningPackageName,
+ @NonNull String targetPackageName,
+ @NonNull String databaseName,
+ @NonNull String schemaName) {
+ synchronized (mLock) {
+ List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(targetPackageName);
+ if (allObserverInfosForPackage == null || allObserverInfosForPackage.isEmpty()) {
+ return; // No observers for this type
+ }
+ // Enqueue changes for later dispatch once the call returns
+ String prefix = null;
+ for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
+ ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
+ if (!observerInfo
+ .mListeningPackageAccess
+ .getCallingPackageName()
+ .equals(listeningPackageName)) {
+ continue; // Not the observer we've been requested to update right now.
+ }
+ if (!matchesSpec(schemaName, observerInfo.mObserverSpec)) {
+ continue; // Observer doesn't want this notification
+ }
+ if (prefix == null) {
+ prefix = PrefixUtil.createPrefix(targetPackageName, databaseName);
+ }
+ Set<String> changedSchemaNames = observerInfo.mSchemaChanges.get(prefix);
+ if (changedSchemaNames == null) {
+ changedSchemaNames = new ArraySet<>();
+ observerInfo.mSchemaChanges.put(prefix, changedSchemaNames);
+ }
+ changedSchemaNames.add(schemaName);
+ }
+ mHasNotifications = true;
+ }
+ }
+
/** Returns whether there are any observers registered to watch the given package. */
public boolean isPackageObserved(@NonNull String packageName) {
synchronized (mLock) {
@@ -281,6 +320,44 @@
}
}
+ /**
+ * Returns package names of listening packages registered for changes on the given {@code
+ * packageName}, {@code databaseName} and unprefixed {@code schemaType}, only if they have
+ * access to that type according to the provided {@code visibilityChecker}.
+ */
+ @NonNull
+ public Set<String> getObserversForSchemaType(
+ @NonNull String packageName,
+ @NonNull String databaseName,
+ @NonNull String schemaType,
+ @Nullable VisibilityStore visibilityStore,
+ @Nullable VisibilityChecker visibilityChecker) {
+ synchronized (mLock) {
+ List<ObserverInfo> allObserverInfosForPackage = mObserversLocked.get(packageName);
+ if (allObserverInfosForPackage == null) {
+ return Collections.emptySet();
+ }
+ Set<String> result = new ArraySet<>();
+ String prefixedSchema = PrefixUtil.createPrefix(packageName, databaseName) + schemaType;
+ for (int i = 0; i < allObserverInfosForPackage.size(); i++) {
+ ObserverInfo observerInfo = allObserverInfosForPackage.get(i);
+ if (!matchesSpec(schemaType, observerInfo.mObserverSpec)) {
+ continue; // Observer doesn't want this notification
+ }
+ if (!VisibilityUtil.isSchemaSearchableByCaller(
+ /*callerAccess=*/ observerInfo.mListeningPackageAccess,
+ /*targetPackageName=*/ packageName,
+ /*prefixedSchema=*/ prefixedSchema,
+ visibilityStore,
+ visibilityChecker)) {
+ continue; // Observer can't have this notification.
+ }
+ result.add(observerInfo.mListeningPackageAccess.getCallingPackageName());
+ }
+ return result;
+ }
+ }
+
/** Returns whether any notifications have been queued for dispatch. */
public boolean hasNotifications() {
return mHasNotifications;
@@ -308,33 +385,63 @@
@GuardedBy("mLock")
private void dispatchAndClearPendingNotificationsLocked(@NonNull ObserverInfo observerInfo) {
// Get and clear the pending changes
+ Map<String, Set<String>> schemaChanges = observerInfo.mSchemaChanges;
Map<DocumentChangeGroupKey, Set<String>> documentChanges = observerInfo.mDocumentChanges;
- if (documentChanges.isEmpty()) {
+ if (schemaChanges.isEmpty() && documentChanges.isEmpty()) {
return;
}
- observerInfo.mDocumentChanges = new ArrayMap<>();
+ if (!schemaChanges.isEmpty()) {
+ observerInfo.mSchemaChanges = new ArrayMap<>();
+ }
+ if (!documentChanges.isEmpty()) {
+ observerInfo.mDocumentChanges = new ArrayMap<>();
+ }
// Dispatch the pending changes
observerInfo.mExecutor.execute(
() -> {
- for (Map.Entry<DocumentChangeGroupKey, Set<String>> entry :
- documentChanges.entrySet()) {
- DocumentChangeInfo documentChangeInfo =
- new DocumentChangeInfo(
- entry.getKey().mPackageName,
- entry.getKey().mDatabaseName,
- entry.getKey().mNamespace,
- entry.getKey().mSchemaName,
- entry.getValue());
+ // Schema changes
+ if (!schemaChanges.isEmpty()) {
+ for (Map.Entry<String, Set<String>> entry : schemaChanges.entrySet()) {
+ SchemaChangeInfo schemaChangeInfo =
+ new SchemaChangeInfo(
+ /*packageName=*/ PrefixUtil.getPackageName(
+ entry.getKey()),
+ /*databaseName=*/ PrefixUtil.getDatabaseName(
+ entry.getKey()),
+ /*changedSchemaNames=*/ entry.getValue());
- try {
- // TODO(b/193494000): Add code to dispatch SchemaChangeInfo too.
- observerInfo.mObserver.onDocumentChanged(documentChangeInfo);
- } catch (Throwable t) {
- Log.w(
- TAG,
- "AppSearchObserverCallback threw exception during dispatch",
- t);
+ try {
+ observerInfo.mObserver.onSchemaChanged(schemaChangeInfo);
+ } catch (Throwable t) {
+ Log.w(
+ TAG,
+ "AppSearchObserverCallback threw exception during dispatch",
+ t);
+ }
+ }
+ }
+
+ // Document changes
+ if (!documentChanges.isEmpty()) {
+ for (Map.Entry<DocumentChangeGroupKey, Set<String>> entry :
+ documentChanges.entrySet()) {
+ DocumentChangeInfo documentChangeInfo =
+ new DocumentChangeInfo(
+ entry.getKey().mPackageName,
+ entry.getKey().mDatabaseName,
+ entry.getKey().mNamespace,
+ entry.getKey().mSchemaName,
+ entry.getValue());
+
+ try {
+ observerInfo.mObserver.onDocumentChanged(documentChangeInfo);
+ } catch (Throwable t) {
+ Log.w(
+ TAG,
+ "AppSearchObserverCallback threw exception during dispatch",
+ t);
+ }
}
}
});
diff --git a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java
index f396a12..7a68fa1 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java
@@ -28,6 +28,7 @@
import android.util.ArraySet;
import android.util.Log;
+import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityUtil;
@@ -155,18 +156,14 @@
* For each target schema, we will check visibility store is that accessible to the caller. And
* remove this schemas if it is not allowed for caller to query.
*
- * @param callerPackageName The package name of caller
- * @param callerUid The uid of the caller.
- * @param callerHasSystemAccess Whether the caller has system access.
+ * @param callerAccess Visibility access info of the calling app
* @param visibilityStore The {@link VisibilityStore} that store all visibility information.
* @param visibilityChecker Optional visibility checker to check whether the caller could access
* target schemas. Pass {@code null} will reject access for all documents which doesn't
* belong to the calling package.
*/
public void removeInaccessibleSchemaFilter(
- @NonNull String callerPackageName,
- int callerUid,
- boolean callerHasSystemAccess,
+ @NonNull CallerAccess callerAccess,
@Nullable VisibilityStore visibilityStore,
@Nullable VisibilityChecker visibilityChecker) {
Iterator<String> targetPrefixedSchemaFilterIterator =
@@ -176,9 +173,7 @@
String packageName = getPackageName(targetPrefixedSchemaFilter);
if (!VisibilityUtil.isSchemaSearchableByCaller(
- callerPackageName,
- callerUid,
- callerHasSystemAccess,
+ callerAccess,
packageName,
targetPrefixedSchemaFilter,
visibilityStore,
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/CallerAccess.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/CallerAccess.java
new file mode 100644
index 0000000..98f9468
--- /dev/null
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/CallerAccess.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 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.appsearch.external.localstorage.visibilitystore;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Contains attributes of an API caller relevant to its access via visibility store.
+ *
+ * @hide
+ */
+public class CallerAccess {
+ private final String mCallingPackageName;
+ private final int mCallingUid;
+ private final boolean mCallerHasSystemAccess;
+
+ /**
+ * Constructs a new {@link CallerAccess}.
+ *
+ * @param callingPackageName The name of the package which wants to access data.
+ * @param callingUid The uid of the package which wants to access data.
+ * @param callerHasSystemAccess Whether {@code callingPackageName} has access to schema types
+ * marked visible to system via {@link
+ * android.app.appsearch.SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}.
+ */
+ public CallerAccess(
+ @NonNull String callingPackageName, int callingUid, boolean callerHasSystemAccess) {
+ mCallingPackageName = Objects.requireNonNull(callingPackageName);
+ mCallingUid = callingUid;
+ mCallerHasSystemAccess = callerHasSystemAccess;
+ }
+
+ /** Returns the name of the package which wants to access data. */
+ @NonNull
+ public String getCallingPackageName() {
+ return mCallingPackageName;
+ }
+
+ /** Returns the uid of the package which wants to access data. */
+ public int getCallingUid() {
+ return mCallingUid;
+ }
+
+ /**
+ * Returns whether {@code callingPackageName} has access to schema types marked visible to
+ * system via {@link
+ * android.app.appsearch.SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}.
+ */
+ public boolean doesCallerHaveSystemAccess() {
+ return mCallerHasSystemAccess;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CallerAccess)) return false;
+ CallerAccess that = (CallerAccess) o;
+ return mCallingUid == that.mCallingUid
+ && mCallerHasSystemAccess == that.mCallerHasSystemAccess
+ && mCallingPackageName.equals(that.mCallingPackageName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCallingPackageName, mCallingUid, mCallerHasSystemAccess);
+ }
+}
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java
index 22466c8..6bd8300 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java
@@ -79,9 +79,10 @@
mAppSearchImpl.getSchema(
VISIBILITY_PACKAGE_NAME,
VISIBILITY_DATABASE_NAME,
- /*callerPackageName=*/ VISIBILITY_PACKAGE_NAME,
- /*callerUid=*/ Process.myUid(),
- /*callerHasSystemAccess=*/ false);
+ new CallerAccess(
+ /*callingPackageName=*/ VISIBILITY_PACKAGE_NAME,
+ /*callingUid=*/ Process.myUid(),
+ /*callerHasSystemAccess=*/ false));
switch (getSchemaResponse.getVersion()) {
case VisibilityDocument.SCHEMA_VERSION_DOC_PER_PACKAGE:
maybeMigrateToLatest(getSchemaResponse);
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityUtil.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityUtil.java
index 8cc4dbd..4c625e3 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityUtil.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityUtil.java
@@ -35,10 +35,7 @@
* <p>Correctly handles access to own data and the situation that visibilityStore and
* visibilityChecker are not configured.
*
- * @param callerPackageName The package name of the app that wants to access the data.
- * @param callerUid The uid of app that wants to access the data.
- * @param callerHasSystemAccess Whether the app that wants to access the data has access to
- * schema types marked visible to the system.
+ * @param callerAccess Visibility access info of the calling app
* @param targetPackageName The package name of the app that owns the data.
* @param prefixedSchema The prefixed schema type the caller wants to access.
* @param visibilityStore Store for visibility information. If not provided, only access to own
@@ -48,17 +45,16 @@
* @return Whether access by the caller to this prefixed schema should be allowed.
*/
public static boolean isSchemaSearchableByCaller(
- @NonNull String callerPackageName,
- int callerUid,
- boolean callerHasSystemAccess,
+ @NonNull CallerAccess callerAccess,
@NonNull String targetPackageName,
@NonNull String prefixedSchema,
@Nullable VisibilityStore visibilityStore,
@Nullable VisibilityChecker visibilityChecker) {
- Objects.requireNonNull(callerPackageName);
+ Objects.requireNonNull(callerAccess);
Objects.requireNonNull(targetPackageName);
Objects.requireNonNull(prefixedSchema);
- if (callerPackageName.equals(targetPackageName)) {
+
+ if (callerAccess.getCallingPackageName().equals(targetPackageName)) {
return true; // Everyone is always allowed to retrieve their own data.
}
if (visibilityStore == null || visibilityChecker == null) {
@@ -67,8 +63,8 @@
return visibilityChecker.isSchemaSearchableByCaller(
targetPackageName,
prefixedSchema,
- callerUid,
- callerHasSystemAccess,
+ callerAccess.getCallingUid(),
+ callerAccess.doesCallerHaveSystemAccess(),
visibilityStore);
}
}
diff --git a/synced_jetpack_changeid.txt b/synced_jetpack_changeid.txt
index 7dfd853..69917e5 100644
--- a/synced_jetpack_changeid.txt
+++ b/synced_jetpack_changeid.txt
@@ -1 +1 @@
-5df0a880c467b3853603ebd196a4d3682974f1d8
+20277b994473ae37cd2c6773be629c40be59e31c
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java
index a4017e9..04ac957 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java
@@ -38,6 +38,7 @@
import android.app.appsearch.exceptions.AppSearchException;
import android.app.appsearch.observer.DocumentChangeInfo;
import android.app.appsearch.observer.ObserverSpec;
+import android.app.appsearch.observer.SchemaChangeInfo;
import android.app.appsearch.testutil.TestObserverCallback;
import android.content.Context;
import android.os.Process;
@@ -50,6 +51,7 @@
import com.android.server.appsearch.external.localstorage.stats.InitializeStats;
import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
+import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker;
import com.android.server.appsearch.icing.proto.DocumentProto;
import com.android.server.appsearch.icing.proto.GetOptimizeInfoResultProto;
@@ -91,6 +93,17 @@
private File mAppSearchDir;
private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ // Some constants for caller access
+ private final CallerAccess mInvalidCallerAccess =
+ new CallerAccess(
+ mContext.getPackageName(),
+ Process.INVALID_UID,
+ /*callerHasSystemAccess=*/ false);
+ private final CallerAccess mSelfCallerAccess =
+ new CallerAccess(
+ mContext.getPackageName(), Process.myUid(), /*callerHasSystemAccess=*/ false);
+
private AppSearchImpl mAppSearchImpl;
@Before
@@ -518,9 +531,7 @@
mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
new SearchSpec.Builder().addFilterSchemas("Type1").build(),
- mContext.getPackageName(),
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ mInvalidCallerAccess,
/*logger=*/ null);
assertThat(results.getResults()).hasSize(1);
assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
@@ -579,18 +590,14 @@
.getSchema(
/*packageName=*/ mContext.getPackageName(),
/*databaseName=*/ "database1",
- /*callerPackageName=*/ mContext.getPackageName(),
- /*callerUid=*/ Process.myUid(),
- /*callerHasSystemAccess=*/ false)
+ /*callerAccess=*/ mSelfCallerAccess)
.getSchemas())
.isEmpty();
results =
mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
new SearchSpec.Builder().addFilterSchemas("Type1").build(),
- mContext.getPackageName(),
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ mInvalidCallerAccess,
/*logger=*/ null);
assertThat(results.getResults()).isEmpty();
@@ -613,9 +620,7 @@
mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
new SearchSpec.Builder().addFilterSchemas("Type1").build(),
- mContext.getPackageName(),
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ mInvalidCallerAccess,
/*logger=*/ null);
assertThat(results.getResults()).hasSize(1);
assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
@@ -748,16 +753,17 @@
}
@Test
- public void testGlobalQueryEmptyDatabase() throws Exception {
+ public void testGlobalQuery_emptyPackage() throws Exception {
SearchSpec searchSpec =
new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
SearchResultPage searchResultPage =
mAppSearchImpl.globalQuery(
- "",
+ /*queryExpression=*/ "",
searchSpec,
- /*callerPackageName=*/ "",
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ new CallerAccess(
+ /*callingPackageName=*/ "",
+ /*callingUid=*/ Process.INVALID_UID,
+ /*callerHasSystemAccess=*/ false),
/*logger=*/ null);
assertThat(searchResultPage.getResults()).isEmpty();
}
@@ -894,9 +900,8 @@
mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
searchSpec,
- "package1",
- Process.myUid(),
- /*callerHasSystemAccess=*/ false,
+ new CallerAccess(
+ "package1", Process.myUid(), /*callerHasSystemAccess=*/ false),
/*logger=*/ null);
// Document2 will come first because it was inserted last and default return order is
@@ -943,9 +948,8 @@
mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
searchSpec,
- "package1",
- Process.myUid(),
- /*callerHasSystemAccess=*/ false,
+ new CallerAccess(
+ "package1", Process.myUid(), /*callerHasSystemAccess=*/ false),
/*logger=*/ null);
// Document2 will come first because it was inserted last and default return order is
@@ -1155,9 +1159,8 @@
mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
searchSpec,
- "package1",
- Process.myUid(),
- /*callerHasSystemAccess=*/ false,
+ new CallerAccess(
+ "package1", Process.myUid(), /*callerHasSystemAccess=*/ false),
/*logger=*/ null);
// Document2 will come first because it was inserted last and default return order is
@@ -1215,9 +1218,8 @@
mAppSearchImpl.globalQuery(
/*queryExpression=*/ "",
searchSpec,
- "package1",
- Process.myUid(),
- /*callerHasSystemAccess=*/ false,
+ new CallerAccess(
+ "package1", Process.myUid(), /*callerHasSystemAccess=*/ false),
/*logger=*/ null);
// Document2 will come first because it was inserted last and default return order is
@@ -2085,9 +2087,7 @@
mAppSearchImpl.getSchema(
/*packageName=*/ "package",
/*databaseName=*/ "database",
- /*callerPackageName=*/ mContext.getPackageName(),
- /*callerUid=*/ Process.myUid(),
- /*callerHasSystemAccess=*/ false));
+ /*callerAccess=*/ mSelfCallerAccess));
assertThrows(
IllegalStateException.class,
@@ -2120,9 +2120,7 @@
mAppSearchImpl.globalQuery(
"query",
new SearchSpec.Builder().build(),
- "package",
- Process.INVALID_UID,
- /*callerHasSystemAccess=*/ false,
+ mInvalidCallerAccess,
/*logger=*/ null));
assertThrows(
@@ -3161,17 +3159,13 @@
// Register an observer twice, on different packages.
TestObserverCallback observer = new TestObserverCallback();
mAppSearchImpl.addObserver(
- /*listeningPackageName=*/ mContext.getPackageName(),
- /*listeningUid=*/ Process.myUid(),
- /*listeningPackageHasSystemAccess=*/ false,
+ /*listeningPackageAccess=*/ mSelfCallerAccess,
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().build(),
MoreExecutors.directExecutor(),
observer);
mAppSearchImpl.addObserver(
- /*listeningPackageName=*/ mContext.getPackageName(),
- /*listeningUid=*/ Process.myUid(),
- /*listeningPackageHasSystemAccess=*/ false,
+ /*listeningPackageAccess=*/ mSelfCallerAccess,
/*targetPackageName=*/ fakePackage,
new ObserverSpec.Builder().build(),
MoreExecutors.directExecutor(),
@@ -3254,9 +3248,7 @@
"namespace1",
"id1",
/*typePropertyPaths=*/ Collections.emptyMap(),
- /*callerPackageName=*/ mContext.getPackageName(),
- /*callerUid=*/ Process.myUid(),
- /*callerHasSystemAccess=*/ false));
+ /*callerAccess=*/ mSelfCallerAccess));
assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
assertThat(e.getMessage()).isEqualTo("Document (namespace1, id1) not found.");
}
@@ -3302,9 +3294,7 @@
"namespace1",
"id1",
/*typePropertyPaths=*/ Collections.emptyMap(),
- /*callerPackageName=*/ mContext.getPackageName(),
- /*callerUid=*/ Process.myUid(),
- /*callerHasSystemAccess=*/ false);
+ /*callerAccess=*/ mSelfCallerAccess);
assertThat(getResult).isEqualTo(document);
}
@@ -3352,9 +3342,7 @@
"namespace1",
"id2",
/*typePropertyPaths=*/ Collections.emptyMap(),
- /*callerPackageName=*/ mContext.getPackageName(),
- /*callerUid=*/ Process.myUid(),
- /*callerHasSystemAccess=*/ false));
+ /*callerAccess=*/ mSelfCallerAccess));
assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
assertThat(e.getMessage()).isEqualTo("Document (namespace1, id2) not found.");
}
@@ -3402,9 +3390,7 @@
"namespace1",
"id1",
/*typePropertyPaths=*/ Collections.emptyMap(),
- /*callerPackageName=*/ mContext.getPackageName(),
- /*callerUid=*/ Process.myUid(),
- /*callerHasSystemAccess=*/ false));
+ /*callerAccess=*/ mSelfCallerAccess));
mAppSearchImpl.remove(
"package", "database", "namespace1", "id1", /*removeStatsBuilder=*/ null);
@@ -3419,9 +3405,10 @@
"namespace1",
"id1",
/*typePropertyPaths=*/ Collections.emptyMap(),
- /*callerPackageName=*/ "package",
- /*callerUid=*/ Process.myUid(),
- /*callerHasSystemAccess=*/ true));
+ new CallerAccess(
+ "package",
+ Process.myUid(),
+ /*callerHasSystemAccess=*/ true)));
assertThat(noDocException.getResultCode()).isEqualTo(unauthorizedException.getResultCode());
assertThat(noDocException.getMessage()).isEqualTo(unauthorizedException.getMessage());
@@ -3674,9 +3661,10 @@
mAppSearchImpl.getSchema(
"package",
"database",
- /*callerPackageName=*/ "com.android.appsearch.fake.package",
- /*callerUid=*/ 1,
- /*callerHasSystemAccess=*/ false);
+ new CallerAccess(
+ "com.android.appsearch.fake.package",
+ /*callerUid=*/ 1,
+ /*callerHasSystemAccess=*/ false));
assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("Type");
}
@@ -3698,9 +3686,8 @@
mAppSearchImpl.getSchema(
"com.android.appsearch.fake.package",
"database",
- /*callerPackageName=*/ "package",
- /*callerUid=*/ 1,
- /*callerHasSystemAccess=*/ false);
+ new CallerAccess(
+ "package", /*callerUid=*/ 1, /*callerHasSystemAccess=*/ false));
assertThat(getResponse.getSchemas()).isEmpty();
assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
}
@@ -3721,9 +3708,10 @@
mAppSearchImpl.getSchema(
"package",
"database",
- /*callerPackageName=*/ "com.android.fake.package",
- /*callerUid=*/ 1,
- /*callerHasSystemAccess=*/ false);
+ new CallerAccess(
+ "com.android.appsearch.fake.package",
+ /*callerUid=*/ 1,
+ /*callerHasSystemAccess=*/ false));
assertThat(getResponse.getSchemas()).isEmpty();
assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
assertThat(getResponse.getVersion()).isEqualTo(0);
@@ -3734,9 +3722,8 @@
mAppSearchImpl.getSchema(
"package",
"database",
- /*callerPackageName=*/ "package",
- /*callerUid=*/ 1,
- /*callerHasSystemAccess=*/ false);
+ new CallerAccess(
+ "package", /*callerUid=*/ 1, /*callerHasSystemAccess=*/ false));
assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
}
@@ -3780,9 +3767,10 @@
mAppSearchImpl.getSchema(
"package",
"database",
- /*callerPackageName=*/ "com.android.appsearch.fake.package",
- /*callerUid=*/ 1,
- /*callerHasSystemAccess=*/ false);
+ new CallerAccess(
+ "com.android.appsearch.fake.package",
+ /*callerUid=*/ 1,
+ /*callerHasSystemAccess=*/ false));
assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0));
assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("VisibleType");
assertThat(getResponse.getVersion()).isEqualTo(1);
@@ -3802,9 +3790,7 @@
// Register an observer
TestObserverCallback observer = new TestObserverCallback();
mAppSearchImpl.addObserver(
- /*listeningPackageName=*/ mContext.getPackageName(),
- Process.myUid(),
- /*listeningPackageHasSystemAccess=*/ false,
+ /*listeningPackageAccess=*/ mSelfCallerAccess,
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().build(),
MoreExecutors.directExecutor(),
@@ -3861,9 +3847,7 @@
// Register an observer
TestObserverCallback observer = new TestObserverCallback();
mAppSearchImpl.addObserver(
- /*listeningPackageName=*/ mContext.getPackageName(),
- Process.myUid(),
- /*listeningPackageHasSystemAccess=*/ false,
+ /*listeningPackageAccess=*/ mSelfCallerAccess,
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().build(),
MoreExecutors.directExecutor(),
@@ -3907,9 +3891,10 @@
// Register an observer from a simulated different package
TestObserverCallback observer = new TestObserverCallback();
mAppSearchImpl.addObserver(
- /*listeningPackageName=*/ "com.fake.Listening.package",
- Process.myUid(),
- /*listeningPackageHasSystemAccess=*/ false,
+ new CallerAccess(
+ "com.fake.Listening.package",
+ Process.myUid(),
+ /*callerHasSystemAccess=*/ false),
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().build(),
MoreExecutors.directExecutor(),
@@ -3962,9 +3947,8 @@
// Register an observer
TestObserverCallback observer = new TestObserverCallback();
mAppSearchImpl.addObserver(
- fakeListeningPackage,
- fakeListeningUid,
- /*listeningPackageHasSystemAccess=*/ false,
+ new CallerAccess(
+ fakeListeningPackage, fakeListeningUid, /*callerHasSystemAccess=*/ false),
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().build(),
MoreExecutors.directExecutor(),
@@ -4024,9 +4008,8 @@
// Register an observer
TestObserverCallback observer = new TestObserverCallback();
mAppSearchImpl.addObserver(
- fakeListeningPackage,
- fakeListeningUid,
- /*listeningPackageHasSystemAccess=*/ false,
+ new CallerAccess(
+ fakeListeningPackage, fakeListeningUid, /*callerHasSystemAccess=*/ false),
/*targetPackageName=*/ mContext.getPackageName(),
new ObserverSpec.Builder().build(),
MoreExecutors.directExecutor(),
@@ -4048,4 +4031,734 @@
assertThat(observer.getSchemaChanges()).isEmpty();
assertThat(observer.getDocumentChanges()).isEmpty();
}
+
+ @Test
+ public void testAddObserver_schemaChange_added() throws Exception {
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ /*listeningPackageAccess=*/ mSelfCallerAccess,
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Add a schema type
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Dispatch notifications
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type1")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Add two more schema types without touching the existing one
+ observer.clear();
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build(),
+ new AppSearchSchema.Builder("Type3").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Dispatch notifications
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableSet.of("Type2", "Type3")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_removed() throws Exception {
+ // Add a schema type
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ /*listeningPackageAccess=*/ mSelfCallerAccess,
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Remove Type2
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_contents() throws Exception {
+ // Add a schema
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_REQUIRED)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ /*listeningPackageAccess=*/ mSelfCallerAccess,
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Update the schema, but don't make any actual changes
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_REQUIRED)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 1,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Now update the schema again, but this time actually make a change (cardinality of the
+ // property)
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 2,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
+ // Add a schema
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_REQUIRED)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_REQUIRED)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer that only listens for Type2
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ /*listeningPackageAccess=*/ mSelfCallerAccess,
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().addFilterSchemas("Type2").build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Update both types of the schema (changed cardinalities)
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_OPTIONAL)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_visibilityOnly() throws Exception {
+ final String fakeListeningPackage = "com.fake.listening.package";
+ final int fakeListeningUid = 42;
+
+ // Make a fake visibility checker that actually looks at visibility store
+ final VisibilityChecker visibilityChecker =
+ (packageName,
+ prefixedSchema,
+ callerUid,
+ callerHasSystemAccess,
+ visibilityStore) -> {
+ if (callerUid != fakeListeningUid) {
+ return false;
+ }
+ Set<String> allowedPackages =
+ new ArraySet<>(
+ visibilityStore
+ .getVisibility(prefixedSchema)
+ .getPackageNames());
+ return allowedPackages.contains(fakeListeningPackage);
+ };
+ mAppSearchImpl.close();
+ mAppSearchImpl =
+ AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ visibilityChecker);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ new CallerAccess(
+ fakeListeningPackage, fakeListeningUid, /*callerHasSystemAccess=*/ false),
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Add a schema where both types are visible to the fake package.
+ List<AppSearchSchema> schemas =
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build());
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ schemas,
+ /*visibilityDocuments=*/ ImmutableList.of(
+ new VisibilityDocument.Builder("Type1")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build(),
+ new VisibilityDocument.Builder("Type2")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build()),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Notifications of addition should now be dispatched
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableSet.of("Type1", "Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ observer.clear();
+
+ // Update schema, keeping the types identical but denying visibility to type2
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ schemas,
+ /*visibilityDocuments=*/ ImmutableList.of(
+ new VisibilityDocument.Builder("Type1")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build(),
+ new VisibilityDocument.Builder("Type2").build()),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications. This should look like a deletion of Type2.
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ observer.clear();
+
+ // Now update Type2 and make sure no further notification is received.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ ImmutableList.of(
+ new VisibilityDocument.Builder("Type1")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build(),
+ new VisibilityDocument.Builder("Type2").build()),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Grant visibility to Type2 again and make sure it appears
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ ImmutableList.of(
+ new VisibilityDocument.Builder("Type1")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build(),
+ new VisibilityDocument.Builder("Type2")
+ .addVisibleToPackage(
+ new PackageIdentifier(fakeListeningPackage, new byte[0]))
+ .build()),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications. This should look like a creation of Type2.
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_visibilityAndContents() throws Exception {
+ final String fakeListeningPackage = "com.fake.listening.package";
+ final int fakeListeningUid = 42;
+
+ // Make a visibility checker that allows fakeListeningPackage access only to Type2.
+ final VisibilityChecker visibilityChecker =
+ (packageName, prefixedSchema, callerUid, callerHasSystemAccess, visibilityStore) ->
+ callerUid == fakeListeningUid && prefixedSchema.endsWith("Type2");
+ mAppSearchImpl.close();
+ mAppSearchImpl =
+ AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ visibilityChecker);
+
+ // Add a schema.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_REQUIRED)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_REQUIRED)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ new CallerAccess(
+ fakeListeningPackage, fakeListeningUid, /*callerHasSystemAccess=*/ false),
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Update both types of the schema (changed cardinalities)
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_OPTIONAL)
+ .build())
+ .build(),
+ new AppSearchSchema.Builder("Type2")
+ .addProperty(
+ new AppSearchSchema.BooleanPropertyConfig.Builder(
+ "booleanProp")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig
+ .CARDINALITY_OPTIONAL)
+ .build())
+ .build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_partialVisibility_removed() throws Exception {
+ final String fakeListeningPackage = "com.fake.listening.package";
+ final int fakeListeningUid = 42;
+
+ // Make a visibility checker that allows fakeListeningPackage access only to Type2.
+ final VisibilityChecker visibilityChecker =
+ (packageName, prefixedSchema, callerUid, callerHasSystemAccess, visibilityStore) ->
+ callerUid == fakeListeningUid && prefixedSchema.endsWith("Type2");
+ mAppSearchImpl.close();
+ mAppSearchImpl =
+ AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ visibilityChecker);
+
+ // Add a schema.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register an observer
+ TestObserverCallback observer = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ new CallerAccess(
+ fakeListeningPackage, fakeListeningUid, /*callerHasSystemAccess=*/ false),
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observer);
+
+ // Remove Type1
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(new AppSearchSchema.Builder("Type2").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications. Nothing should appear since Type1 is not visible to us.
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+
+ // Now remove Type2. This should cause a notification.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+ assertThat(observer.getSchemaChanges()).isEmpty();
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+ assertThat(observer.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type2")));
+ assertThat(observer.getDocumentChanges()).isEmpty();
+ }
+
+ @Test
+ public void testAddObserver_schemaChange_multipleObservers() throws Exception {
+ // Create two fake packages. One can access Type1, one can access Type2, they both can
+ // access Type3, and no one can access Type4.
+ final String fakePackage1 = "com.fake.listening.package1";
+ final int fakePackage1Uid = 42;
+
+ final String fakePackage2 = "com.fake.listening.package2";
+ final int fakePackage2Uid = 43;
+
+ final VisibilityChecker visibilityChecker =
+ (packageName,
+ prefixedSchema,
+ callerUid,
+ callerHasSystemAccess,
+ visibilityStore) -> {
+ if (prefixedSchema.endsWith("Type1")) {
+ return callerUid == fakePackage1Uid;
+ } else if (prefixedSchema.endsWith("Type2")) {
+ return callerUid == fakePackage2Uid;
+ } else if (prefixedSchema.endsWith("Type3")) {
+ return false;
+ } else if (prefixedSchema.endsWith("Type4")) {
+ return true;
+ } else {
+ throw new IllegalArgumentException(prefixedSchema);
+ }
+ };
+ mAppSearchImpl.close();
+ mAppSearchImpl =
+ AppSearchImpl.create(
+ mAppSearchDir,
+ new UnlimitedLimitConfig(),
+ /*initStatsBuilder=*/ null,
+ ALWAYS_OPTIMIZE,
+ visibilityChecker);
+
+ // Add a schema.
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("Type1").build(),
+ new AppSearchSchema.Builder("Type2").build(),
+ new AppSearchSchema.Builder("Type3").build(),
+ new AppSearchSchema.Builder("Type4").build()),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Register three observers: one in each package, and another in package1 with a filter.
+ TestObserverCallback observerPkg1NoFilter = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ new CallerAccess(fakePackage1, fakePackage1Uid, /*callerHasSystemAccess=*/ false),
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observerPkg1NoFilter);
+
+ TestObserverCallback observerPkg2NoFilter = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ new CallerAccess(fakePackage2, fakePackage2Uid, /*callerHasSystemAccess=*/ false),
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().build(),
+ MoreExecutors.directExecutor(),
+ observerPkg2NoFilter);
+
+ TestObserverCallback observerPkg1FilterType4 = new TestObserverCallback();
+ mAppSearchImpl.addObserver(
+ new CallerAccess(fakePackage1, fakePackage1Uid, /*callerHasSystemAccess=*/ false),
+ /*targetPackageName=*/ mContext.getPackageName(),
+ new ObserverSpec.Builder().addFilterSchemas("Type4").build(),
+ MoreExecutors.directExecutor(),
+ observerPkg1FilterType4);
+
+ // Remove everything
+ mAppSearchImpl.setSchema(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableList.of(),
+ /*visibilityDocuments=*/ Collections.emptyList(),
+ /*forceOverride=*/ false,
+ /*version=*/ 0,
+ /*setSchemaStatsBuilder=*/ null);
+
+ // Dispatch notifications.
+ mAppSearchImpl.dispatchAndClearChangeNotifications();
+
+ // observerPkg1NoFilter should see Type1 and Type4 vanish.
+ // observerPkg2NoFilter should see Type2 and Type4 vanish.
+ // observerPkg2WithFilter should see Type4 vanish.
+ assertThat(observerPkg1NoFilter.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableSet.of("Type1", "Type4")));
+ assertThat(observerPkg1NoFilter.getDocumentChanges()).isEmpty();
+
+ assertThat(observerPkg2NoFilter.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(),
+ "database1",
+ ImmutableSet.of("Type2", "Type4")));
+ assertThat(observerPkg2NoFilter.getDocumentChanges()).isEmpty();
+
+ assertThat(observerPkg1FilterType4.getSchemaChanges())
+ .containsExactly(
+ new SchemaChangeInfo(
+ mContext.getPackageName(), "database1", ImmutableSet.of("Type4")));
+ assertThat(observerPkg1FilterType4.getDocumentChanges()).isEmpty();
+ }
}
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterTest.java
index b9ce1ac..1bd73ac 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -31,6 +31,7 @@
import com.android.server.appsearch.external.localstorage.OptimizeStrategy;
import com.android.server.appsearch.external.localstorage.UnlimitedLimitConfig;
import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
+import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
import com.android.server.appsearch.icing.proto.ResultSpecProto;
import com.android.server.appsearch.icing.proto.SchemaTypeConfigProto;
@@ -497,10 +498,9 @@
"package$database/schema3", schemaTypeConfigProto)));
converter.removeInaccessibleSchemaFilter(
- /*callerPackageName=*/ "otherPackageName",
- /*callerUid=*/ -1,
- /*callerHasSystemAccess=*/ true,
- /*visibilityStore=*/ visibilityStore,
+ new CallerAccess(
+ "otherPackageName", /*callingUid=*/ -1, /*callerHasSystemAccess=*/ true),
+ visibilityStore,
AppSearchTestUtils.createMockVisibilityChecker(
/*visiblePrefixedSchemas=*/ ImmutableSet.of(
prefix + "schema1", prefix + "schema3")));
@@ -552,9 +552,8 @@
// remove all target schema filter, and the query becomes nothing to search.
nonEmptyConverter.removeInaccessibleSchemaFilter(
- /*callerPackageName=*/ "otherPackageName",
- /*callerUid=*/ -1,
- /*callerHasSystemAccess=*/ true,
+ new CallerAccess(
+ "otherPackageName", /*callingUid=*/ -1, /*callerHasSystemAccess=*/ true),
/*visibilityStore=*/ null,
/*visibilityChecker=*/ null);
assertThat(nonEmptyConverter.isNothingToSearch()).isTrue();