Merge AppSearchImpl Jetpack work from last two quarters.
This switches AppSearch from FakeIcing to the real libicing.
Bug: 162450968
Test: AppSearchManagerTest, AppSearchImplTest
Change-Id: I9ecbe4ce229e4ac9756aa187ad82ba60420a644e
diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java
index 9afa194..7d2b64e 100644
--- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java
+++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchDocument.java
@@ -47,6 +47,10 @@
public class AppSearchDocument {
private static final String TAG = "AppSearchDocument";
+ /** The default empty namespace.*/
+ // TODO(adorokhine): Allow namespace to be specified in the document.
+ public static final String DEFAULT_NAMESPACE = "";
+
/**
* The maximum number of elements in a repeatable field. Will reject the request if exceed
* this limit.
@@ -450,7 +454,7 @@
*/
public Builder(@NonNull String uri, @NonNull String schemaType) {
mBuilderTypeInstance = (BuilderType) this;
- mProtoBuilder.setUri(uri).setSchema(schemaType);
+ mProtoBuilder.setUri(uri).setSchema(schemaType).setNamespace(DEFAULT_NAMESPACE);
// Set current timestamp for creation timestamp by default.
setCreationTimestampMillis(System.currentTimeMillis());
}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchException.java b/apex/appsearch/framework/java/android/app/appsearch/exceptions/AppSearchException.java
similarity index 71%
rename from apex/appsearch/service/java/com/android/server/appsearch/AppSearchException.java
rename to apex/appsearch/framework/java/android/app/appsearch/exceptions/AppSearchException.java
index 9b705ce..00f6e75 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchException.java
+++ b/apex/appsearch/framework/java/android/app/appsearch/exceptions/AppSearchException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright 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.
@@ -14,31 +14,38 @@
* limitations under the License.
*/
-package com.android.server.appsearch;
+package android.app.appsearch.exceptions;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.appsearch.AppSearchResult;
/**
- * An exception thrown by {@link com.android.server.appsearch.AppSearchManagerService} or a
- * subcomponent.
+ * An exception thrown by {@code android.app.appsearch.AppSearchManager} or a subcomponent.
*
- * <p>These exceptions can be converted into a failed {@link android.app.appsearch.AppSearchResult}
+ * <p>These exceptions can be converted into a failed {@link AppSearchResult}
* for propagating to the client.
+ * @hide
*/
+//TODO(b/157082794): Linkify to AppSearchManager once that API is public
public class AppSearchException extends Exception {
private final @AppSearchResult.ResultCode int mResultCode;
- /** Initializes an {@link com.android.server.appsearch.AppSearchException} with no message. */
+ /**
+ * Initializes an {@link AppSearchException} with no message.
+ * @hide
+ */
public AppSearchException(@AppSearchResult.ResultCode int resultCode) {
this(resultCode, /*message=*/ null);
}
+ /** @hide */
public AppSearchException(
@AppSearchResult.ResultCode int resultCode, @Nullable String message) {
this(resultCode, message, /*cause=*/ null);
}
+ /** @hide */
public AppSearchException(
@AppSearchResult.ResultCode int resultCode,
@Nullable String message,
@@ -48,9 +55,9 @@
}
/**
- * Converts this {@link java.lang.Exception} into a failed
- * {@link android.app.appsearch.AppSearchResult}
+ * Converts this {@link java.lang.Exception} into a failed {@link AppSearchResult}
*/
+ @NonNull
public <T> AppSearchResult<T> toAppSearchResult() {
return AppSearchResult.newFailedResult(mResultCode, getMessage());
}
diff --git a/apex/appsearch/service/Android.bp b/apex/appsearch/service/Android.bp
index c125f56..fc1d707 100644
--- a/apex/appsearch/service/Android.bp
+++ b/apex/appsearch/service/Android.bp
@@ -20,7 +20,13 @@
"framework-appsearch",
"services.core",
],
- static_libs: ["icing-java-proto-lite"],
+ static_libs: [
+ "icing-java-proto-lite",
+ "libicing-java",
+ ],
+ required: [
+ "libicing",
+ ],
jarjar_rules: "jarjar-rules.txt",
apex_available: ["com.android.appsearch"],
}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
index 16948b2..75fad82 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -17,8 +17,10 @@
import android.annotation.NonNull;
import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchDocument;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.IAppSearchManager;
+import android.app.appsearch.exceptions.AppSearchException;
import android.content.Context;
import android.os.Binder;
import android.os.UserHandle;
@@ -26,8 +28,7 @@
import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
-import com.android.server.appsearch.impl.AppSearchImpl;
-import com.android.server.appsearch.impl.ImplInstanceManager;
+import com.android.server.appsearch.external.localbackend.AppSearchImpl;
import com.google.android.icing.proto.DocumentProto;
import com.google.android.icing.proto.ResultSpecProto;
@@ -44,6 +45,7 @@
* TODO(b/142567528): add comments when implement this class
*/
public class AppSearchManagerService extends SystemService {
+ private static final String TAG = "AppSearchManagerService";
public AppSearchManagerService(Context context) {
super(context);
@@ -68,7 +70,8 @@
try {
SchemaProto schema = SchemaProto.parseFrom(schemaBytes);
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
- impl.setSchema(callingUid, schema, forceOverride);
+ String databaseName = makeDatabaseName(callingUid);
+ impl.setSchema(databaseName, schema, forceOverride);
callback.complete(AppSearchResult.newSuccessfulResult(/*value=*/ null));
} catch (Throwable t) {
callback.complete(throwableToFailedResult(t));
@@ -88,13 +91,14 @@
long callingIdentity = Binder.clearCallingIdentity();
try {
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ String databaseName = makeDatabaseName(callingUid);
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
for (int i = 0; i < documentsBytes.size(); i++) {
byte[] documentBytes = (byte[]) documentsBytes.get(i);
DocumentProto document = DocumentProto.parseFrom(documentBytes);
try {
- impl.putDocument(callingUid, document);
+ impl.putDocument(databaseName, document);
resultBuilder.setSuccess(document.getUri(), /*value=*/ null);
} catch (Throwable t) {
resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
@@ -118,12 +122,14 @@
long callingIdentity = Binder.clearCallingIdentity();
try {
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ String databaseName = makeDatabaseName(callingUid);
AppSearchBatchResult.Builder<String, byte[]> resultBuilder =
new AppSearchBatchResult.Builder<>();
for (int i = 0; i < uris.size(); i++) {
String uri = uris.get(i);
try {
- DocumentProto document = impl.getDocument(callingUid, uri);
+ DocumentProto document = impl.getDocument(
+ databaseName, AppSearchDocument.DEFAULT_NAMESPACE, uri);
if (document == null) {
resultBuilder.setFailure(
uri, AppSearchResult.RESULT_NOT_FOUND, /*errorMessage=*/ null);
@@ -161,8 +167,9 @@
ResultSpecProto resultSpecProto = ResultSpecProto.parseFrom(resultSpecBytes);
ScoringSpecProto scoringSpecProto = ScoringSpecProto.parseFrom(scoringSpecBytes);
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
- SearchResultProto searchResultProto =
- impl.query(callingUid, searchSpecProto, resultSpecProto, scoringSpecProto);
+ String databaseName = makeDatabaseName(callingUid);
+ SearchResultProto searchResultProto = impl.query(
+ databaseName, searchSpecProto, resultSpecProto, scoringSpecProto);
// TODO(sidchhabra): Translate SearchResultProto errors into error codes. This might
// better be done in AppSearchImpl by throwing an AppSearchException.
if (searchResultProto.getStatus().getCode() != StatusProto.Code.OK) {
@@ -190,17 +197,14 @@
long callingIdentity = Binder.clearCallingIdentity();
try {
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ String databaseName = makeDatabaseName(callingUid);
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
for (int i = 0; i < uris.size(); i++) {
String uri = uris.get(i);
try {
- if (!impl.delete(callingUid, uri)) {
- resultBuilder.setFailure(
- uri, AppSearchResult.RESULT_NOT_FOUND, /*errorMessage=*/ null);
- } else {
- resultBuilder.setSuccess(uri, /*value= */null);
- }
+ impl.remove(databaseName, AppSearchDocument.DEFAULT_NAMESPACE, uri);
+ resultBuilder.setSuccess(uri, /*value= */null);
} catch (Throwable t) {
resultBuilder.setResult(uri, throwableToFailedResult(t));
}
@@ -223,19 +227,14 @@
long callingIdentity = Binder.clearCallingIdentity();
try {
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
+ String databaseName = makeDatabaseName(callingUid);
AppSearchBatchResult.Builder<String, Void> resultBuilder =
new AppSearchBatchResult.Builder<>();
for (int i = 0; i < schemaTypes.size(); i++) {
String schemaType = schemaTypes.get(i);
try {
- if (!impl.deleteByType(callingUid, schemaType)) {
- resultBuilder.setFailure(
- schemaType,
- AppSearchResult.RESULT_NOT_FOUND,
- /*errorMessage=*/ null);
- } else {
- resultBuilder.setSuccess(schemaType, /*value=*/ null);
- }
+ impl.removeByType(databaseName, schemaType);
+ resultBuilder.setSuccess(schemaType, /*value=*/ null);
} catch (Throwable t) {
resultBuilder.setResult(schemaType, throwableToFailedResult(t));
}
@@ -256,7 +255,8 @@
long callingIdentity = Binder.clearCallingIdentity();
try {
AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId);
- impl.deleteAll(callingUid);
+ String databaseName = makeDatabaseName(callingUid);
+ impl.removeAll(databaseName);
callback.complete(AppSearchResult.newSuccessfulResult(null));
} catch (Throwable t) {
callback.complete(throwableToFailedResult(t));
@@ -265,6 +265,25 @@
}
}
+ /**
+ * Returns a unique database name for the given uid.
+ *
+ * <p>The current implementation returns the package name of the app with this uid in a
+ * format like {@code com.example.package} or {@code com.example.sharedname:5678}.
+ */
+ @NonNull
+ private String makeDatabaseName(int callingUid) {
+ // For regular apps, this call will return the package name. If callingUid is an
+ // android:sharedUserId, this value may be another type of name and have a :uid suffix.
+ String callingUidName = getContext().getPackageManager().getNameForUid(callingUid);
+ if (callingUidName == null) {
+ // Not sure how this is possible --- maybe app was uninstalled?
+ throw new IllegalStateException(
+ "Failed to look up package name for uid " + callingUid);
+ }
+ return callingUidName;
+ }
+
private <ValueType> AppSearchResult<ValueType> throwableToFailedResult(
@NonNull Throwable t) {
if (t instanceof AppSearchException) {
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/impl/ImplInstanceManager.java b/apex/appsearch/service/java/com/android/server/appsearch/ImplInstanceManager.java
similarity index 60%
rename from apex/appsearch/service/java/com/android/server/appsearch/impl/ImplInstanceManager.java
rename to apex/appsearch/service/java/com/android/server/appsearch/ImplInstanceManager.java
index 395e30e..c1e6b0f 100644
--- a/apex/appsearch/service/java/com/android/server/appsearch/impl/ImplInstanceManager.java
+++ b/apex/appsearch/service/java/com/android/server/appsearch/ImplInstanceManager.java
@@ -14,21 +14,32 @@
* limitations under the License.
*/
-package com.android.server.appsearch.impl;
+package com.android.server.appsearch;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
+import android.app.appsearch.exceptions.AppSearchException;
import android.content.Context;
+import android.os.Environment;
+import android.os.storage.StorageManager;
import android.util.SparseArray;
+import com.android.server.appsearch.external.localbackend.AppSearchImpl;
+
+import java.io.File;
+
/**
* Manages the lifecycle of instances of {@link AppSearchImpl}.
*
* <p>These instances are managed per unique device-user.
*/
public final class ImplInstanceManager {
+ private static final String APP_SEARCH_DIR = "appSearch";
+
private static final SparseArray<AppSearchImpl> sInstances = new SparseArray<>();
+ private ImplInstanceManager() {}
+
/**
* Gets an instance of AppSearchImpl for the given user.
*
@@ -40,17 +51,33 @@
* @return An initialized {@link AppSearchImpl} for this user
*/
@NonNull
- public static AppSearchImpl getInstance(@NonNull Context context, @UserIdInt int userId) {
+ public static AppSearchImpl getInstance(@NonNull Context context, @UserIdInt int userId)
+ throws AppSearchException {
AppSearchImpl instance = sInstances.get(userId);
if (instance == null) {
synchronized (ImplInstanceManager.class) {
instance = sInstances.get(userId);
if (instance == null) {
- instance = new AppSearchImpl(context, userId);
+ instance = createImpl(context, userId);
sInstances.put(userId, instance);
}
}
}
return instance;
}
+
+ private static AppSearchImpl createImpl(@NonNull Context context, @UserIdInt int userId)
+ throws AppSearchException {
+ File appSearchDir = getAppSearchDir(context, userId);
+ AppSearchImpl appSearchImpl = new AppSearchImpl(appSearchDir);
+ appSearchImpl.initialize();
+ return appSearchImpl;
+ }
+
+ private static File getAppSearchDir(@NonNull Context context, @UserIdInt int userId) {
+ // See com.android.internal.app.ChooserActivity::getPinnedSharedPrefs
+ File userCeDir = Environment.getDataUserCePackageDirectory(
+ StorageManager.UUID_PRIVATE_INTERNAL, userId, context.getPackageName());
+ return new File(userCeDir, APP_SEARCH_DIR);
+ }
}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/AppSearchImpl.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/AppSearchImpl.java
new file mode 100644
index 0000000..462f458
--- /dev/null
+++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localbackend/AppSearchImpl.java
@@ -0,0 +1,865 @@
+/*
+ * Copyright 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.external.localbackend;
+
+import android.util.Log;
+
+import android.annotation.AnyThread;
+import com.android.internal.annotations.GuardedBy;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import android.annotation.WorkerThread;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.exceptions.AppSearchException;
+
+import com.google.android.icing.IcingSearchEngine;
+import com.google.android.icing.proto.DeleteByNamespaceResultProto;
+import com.google.android.icing.proto.DeleteBySchemaTypeResultProto;
+import com.google.android.icing.proto.DeleteResultProto;
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.GetAllNamespacesResultProto;
+import com.google.android.icing.proto.GetOptimizeInfoResultProto;
+import com.google.android.icing.proto.GetResultProto;
+import com.google.android.icing.proto.GetSchemaResultProto;
+import com.google.android.icing.proto.IcingSearchEngineOptions;
+import com.google.android.icing.proto.InitializeResultProto;
+import com.google.android.icing.proto.OptimizeResultProto;
+import com.google.android.icing.proto.PropertyConfigProto;
+import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.PutResultProto;
+import com.google.android.icing.proto.ResetResultProto;
+import com.google.android.icing.proto.ResultSpecProto;
+import com.google.android.icing.proto.SchemaProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.ScoringSpecProto;
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.android.icing.proto.SearchSpecProto;
+import com.google.android.icing.proto.SetSchemaResultProto;
+import com.google.android.icing.proto.StatusProto;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Manages interaction with the native IcingSearchEngine and other components to implement AppSearch
+ * functionality.
+ *
+ * <p>Callers should call {@link #initialize} before using the AppSearchImpl instance. Never create
+ * two instances using the same folder.
+ *
+ * <p>A single instance of {@link AppSearchImpl} can support all databases. Schemas and documents
+ * are physically saved together in {@link IcingSearchEngine}, but logically isolated:
+ * <ul>
+ * <li>Rewrite SchemaType in SchemaProto by adding database name prefix and save into
+ * SchemaTypes set in {@link #setSchema}.
+ * <li>Rewrite namespace and SchemaType in DocumentProto by adding database name prefix and
+ * save to namespaces set in {@link #putDocument}.
+ * <li>Remove database name prefix when retrieve documents in {@link #getDocument} and
+ * {@link #query}.
+ * <li>Rewrite filters in {@link SearchSpecProto} to have all namespaces and schema types of
+ * the queried database when user using empty filters in {@link #query}.
+ * </ul>
+ *
+ * <p>Methods in this class belong to two groups, the query group and the mutate group.
+ * <ul>
+ * <li>All methods are going to modify global parameters and data in Icing are executed under
+ * WRITE lock to keep thread safety.
+ * <li>All methods are going to access global parameters or query data from Icing are executed
+ * under READ lock to improve query performance.
+ * </ul>
+ *
+ * <p>This class is thread safe.
+ * @hide
+ */
+
+@WorkerThread
+public final class AppSearchImpl {
+ private static final String TAG = "AppSearchImpl";
+
+ @VisibleForTesting
+ static final int OPTIMIZE_THRESHOLD_DOC_COUNT = 1000;
+ @VisibleForTesting
+ static final int OPTIMIZE_THRESHOLD_BYTES = 1_000_000; // 1MB
+ @VisibleForTesting
+ static final int CHECK_OPTIMIZE_INTERVAL = 100;
+
+ private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
+ private final CountDownLatch mInitCompleteLatch = new CountDownLatch(1);
+ private final File mIcingDir;
+ private IcingSearchEngine mIcingSearchEngine;
+
+ // The map contains schemaTypes and namespaces for all database. All values in the map have
+ // been already added database name prefix.
+ private final Map<String, Set<String>> mSchemaMap = new HashMap<>();
+ private final Map<String, Set<String>> mNamespaceMap = new HashMap<>();
+
+ /**
+ * The counter to check when to call {@link #checkForOptimize(boolean)}. The interval is
+ * {@link #CHECK_OPTIMIZE_INTERVAL}.
+ */
+ private int mOptimizeIntervalCount = 0;
+
+ /** Creates an instance of {@link AppSearchImpl} which writes data to the given folder. */
+ @AnyThread
+ public AppSearchImpl(@NonNull File icingDir) {
+ mIcingDir = icingDir;
+ }
+
+ /**
+ * Initializes the underlying IcingSearchEngine.
+ *
+ * <p>This method belongs to mutate group.
+ *
+ * @throws AppSearchException on IcingSearchEngine error.
+ */
+ public void initialize() throws AppSearchException {
+ if (isInitialized()) {
+ return;
+ }
+ boolean isReset = false;
+ mReadWriteLock.writeLock().lock();
+ try {
+ // We synchronize here because we don't want to call IcingSearchEngine.initialize() more
+ // than once. It's unnecessary and can be a costly operation.
+ if (isInitialized()) {
+ return;
+ }
+ IcingSearchEngineOptions options = IcingSearchEngineOptions.newBuilder()
+ .setBaseDir(mIcingDir.getAbsolutePath()).build();
+ mIcingSearchEngine = new IcingSearchEngine(options);
+
+ InitializeResultProto initializeResultProto = mIcingSearchEngine.initialize();
+ SchemaProto schemaProto = null;
+ GetAllNamespacesResultProto getAllNamespacesResultProto = null;
+ try {
+ checkSuccess(initializeResultProto.getStatus());
+ schemaProto = getSchemaProto();
+ getAllNamespacesResultProto = mIcingSearchEngine.getAllNamespaces();
+ checkSuccess(getAllNamespacesResultProto.getStatus());
+ } catch (AppSearchException e) {
+ // Some error. Reset and see if it fixes it.
+ reset();
+ isReset = true;
+ }
+ for (SchemaTypeConfigProto schema : schemaProto.getTypesList()) {
+ String qualifiedSchemaType = schema.getSchemaType();
+ addToMap(mSchemaMap, getDatabaseName(qualifiedSchemaType), qualifiedSchemaType);
+ }
+ for (String qualifiedNamespace : getAllNamespacesResultProto.getNamespacesList()) {
+ addToMap(mNamespaceMap, getDatabaseName(qualifiedNamespace), qualifiedNamespace);
+ }
+ mInitCompleteLatch.countDown();
+ if (!isReset) {
+ checkForOptimize(/* force= */ true);
+ }
+ } finally {
+ mReadWriteLock.writeLock().unlock();
+ }
+ }
+
+ /** Checks if the internal state of {@link AppSearchImpl} has been initialized. */
+ @AnyThread
+ public boolean isInitialized() {
+ return mInitCompleteLatch.getCount() == 0;
+ }
+
+ /**
+ * Updates the AppSearch schema for this app.
+ *
+ * <p>This method belongs to mutate group.
+ *
+ * @param databaseName The name of the database where this schema lives.
+ * @param origSchema The schema to set for this app.
+ * @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents
+ * which do not comply with the new schema will be deleted.
+ * @throws AppSearchException on IcingSearchEngine error.
+ * @throws InterruptedException if the current thread was interrupted during execution.
+ */
+ public void setSchema(@NonNull String databaseName, @NonNull SchemaProto origSchema,
+ boolean forceOverride) throws AppSearchException, InterruptedException {
+ awaitInitialized();
+
+ SchemaProto schemaProto = getSchemaProto();
+
+ SchemaProto.Builder existingSchemaBuilder = schemaProto.toBuilder();
+
+ // Combine the existing schema (which may have types from other databases) with this
+ // database's new schema. Modifies the existingSchemaBuilder.
+ Set<String> newTypeNames = rewriteSchema(databaseName, existingSchemaBuilder, origSchema);
+
+ SetSchemaResultProto setSchemaResultProto;
+ mReadWriteLock.writeLock().lock();
+ try {
+ setSchemaResultProto = mIcingSearchEngine.setSchema(existingSchemaBuilder.build(),
+ forceOverride);
+ checkSuccess(setSchemaResultProto.getStatus());
+ mSchemaMap.put(databaseName, newTypeNames);
+ if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
+ || (setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0
+ && forceOverride)) {
+ // Any existing schemas which is not in origSchema will be deleted, and all
+ // documents of these types were also deleted. And so well if we force override
+ // incompatible schemas.
+ checkForOptimize(/* force= */true);
+ }
+ } finally {
+ mReadWriteLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Adds a document to the AppSearch index.
+ *
+ * <p>This method belongs to mutate group.
+ *
+ * @param databaseName The databaseName this document resides in.
+ * @param document The document to index.
+ * @throws AppSearchException on IcingSearchEngine error.
+ * @throws InterruptedException if the current thread was interrupted during execution.
+ */
+ public void putDocument(@NonNull String databaseName, @NonNull DocumentProto document)
+ throws AppSearchException, InterruptedException {
+ awaitInitialized();
+
+ DocumentProto.Builder documentBuilder = document.toBuilder();
+ rewriteDocumentTypes(getDatabasePrefix(databaseName), documentBuilder, /*add=*/ true);
+
+ PutResultProto putResultProto;
+ mReadWriteLock.writeLock().lock();
+ try {
+ putResultProto = mIcingSearchEngine.put(documentBuilder.build());
+ addToMap(mNamespaceMap, databaseName, documentBuilder.getNamespace());
+ // The existing documents with same URI will be deleted, so there maybe some resources
+ // could be released after optimize().
+ checkForOptimize(/* force= */false);
+ } finally {
+ mReadWriteLock.writeLock().unlock();
+ }
+ checkSuccess(putResultProto.getStatus());
+ }
+
+ /**
+ * Retrieves a document from the AppSearch index by URI.
+ *
+ * <p>This method belongs to query group.
+ *
+ * @param databaseName The databaseName this document resides in.
+ * @param namespace The namespace this document resides in.
+ * @param uri The URI of the document to get.
+ * @return The Document contents, or {@code null} if no such URI exists in the system.
+ * @throws AppSearchException on IcingSearchEngine error.
+ * @throws InterruptedException if the current thread was interrupted during execution.
+ */
+ @Nullable
+ public DocumentProto getDocument(@NonNull String databaseName, @NonNull String namespace,
+ @NonNull String uri) throws AppSearchException, InterruptedException {
+ awaitInitialized();
+ GetResultProto getResultProto;
+ mReadWriteLock.readLock().lock();
+ try {
+ getResultProto = mIcingSearchEngine.get(
+ getDatabasePrefix(databaseName) + namespace, uri);
+ } finally {
+ mReadWriteLock.readLock().unlock();
+ }
+ checkSuccess(getResultProto.getStatus());
+
+ DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
+ rewriteDocumentTypes(getDatabasePrefix(databaseName), documentBuilder, /*add=*/ false);
+ return documentBuilder.build();
+ }
+
+ /**
+ * Executes a query against the AppSearch index and returns results.
+ *
+ * <p>This method belongs to query group.
+ *
+ * @param databaseName The databaseName this query for.
+ * @param searchSpec Defines what and how to search
+ * @param resultSpec Defines what results to show
+ * @param scoringSpec Defines how to order results
+ * @return The results of performing this search The proto might have no {@code results} if no
+ * documents matched the query.
+ * @throws AppSearchException on IcingSearchEngine error.
+ * @throws InterruptedException if the current thread was interrupted during execution.
+ */
+ @NonNull
+ public SearchResultProto query(
+ @NonNull String databaseName,
+ @NonNull SearchSpecProto searchSpec,
+ @NonNull ResultSpecProto resultSpec,
+ @NonNull ScoringSpecProto scoringSpec) throws AppSearchException, InterruptedException {
+ awaitInitialized();
+
+ SearchSpecProto.Builder searchSpecBuilder = searchSpec.toBuilder();
+ SearchResultProto searchResultProto;
+ mReadWriteLock.readLock().lock();
+ try {
+ // Only rewrite SearchSpec for non empty database.
+ // rewriteSearchSpecForNonEmptyDatabase will return false for empty database, we
+ // should just return an empty SearchResult and skip sending request to Icing.
+ if (!rewriteSearchSpecForNonEmptyDatabase(databaseName, searchSpecBuilder)) {
+ return SearchResultProto.newBuilder()
+ .setStatus(StatusProto.newBuilder()
+ .setCode(StatusProto.Code.OK)
+ .build())
+ .build();
+ }
+ searchResultProto = mIcingSearchEngine.search(
+ searchSpecBuilder.build(), scoringSpec, resultSpec);
+ } finally {
+ mReadWriteLock.readLock().unlock();
+ }
+ checkSuccess(searchResultProto.getStatus());
+ if (searchResultProto.getResultsCount() == 0) {
+ return searchResultProto;
+ }
+ return rewriteSearchResultProto(databaseName, searchResultProto);
+ }
+
+ /**
+ * Fetches the next page of results of a previously executed query. Results can be empty if
+ * next-page token is invalid or all pages have been returned.
+ *
+ * @param databaseName The databaseName of the previously executed query.
+ * @param nextPageToken The token of pre-loaded results of previously executed query.
+ * @return The next page of results of previously executed query.
+ * @throws AppSearchException on IcingSearchEngine error.
+ * @throws InterruptedException if the current thread was interrupted during execution.
+ */
+ @NonNull
+ public SearchResultProto getNextPage(@NonNull String databaseName, long nextPageToken)
+ throws AppSearchException, InterruptedException {
+ awaitInitialized();
+
+ SearchResultProto searchResultProto = mIcingSearchEngine.getNextPage(nextPageToken);
+ checkSuccess(searchResultProto.getStatus());
+ if (searchResultProto.getResultsCount() == 0) {
+ return searchResultProto;
+ }
+ return rewriteSearchResultProto(databaseName, searchResultProto);
+ }
+
+ /**
+ * Invalidates the next-page token so that no more results of the related query can be returned.
+ * @param nextPageToken The token of pre-loaded results of previously executed query to be
+ * Invalidated.
+ */
+ public void invalidateNextPageToken(long nextPageToken) throws InterruptedException {
+ awaitInitialized();
+ mIcingSearchEngine.invalidateNextPageToken(nextPageToken);
+ }
+
+ /**
+ * Removes the given document by URI.
+ *
+ * <p>This method belongs to mutate group.
+ *
+ * @param databaseName The databaseName the document is in.
+ * @param namespace Namespace of the document to remove.
+ * @param uri URI of the document to remove.
+ * @throws AppSearchException on IcingSearchEngine error.
+ * @throws InterruptedException if the current thread was interrupted during execution.
+ */
+ public void remove(@NonNull String databaseName, @NonNull String namespace,
+ @NonNull String uri) throws AppSearchException, InterruptedException {
+ awaitInitialized();
+
+ String qualifiedNamespace = getDatabasePrefix(databaseName) + namespace;
+ DeleteResultProto deleteResultProto;
+ mReadWriteLock.writeLock().lock();
+ try {
+ deleteResultProto = mIcingSearchEngine.delete(qualifiedNamespace, uri);
+ checkForOptimize(/* force= */false);
+ } finally {
+ mReadWriteLock.writeLock().unlock();
+ }
+ checkSuccess(deleteResultProto.getStatus());
+ }
+
+ /**
+ * Removes all documents having the given {@code schemaType} in given database.
+ *
+ * <p>This method belongs to mutate group.
+ *
+ * @param databaseName The databaseName that contains documents of schemaType.
+ * @param schemaType The schemaType of documents to remove.
+ * @throws AppSearchException on IcingSearchEngine error.
+ * @throws InterruptedException if the current thread was interrupted during execution.
+ */
+ public void removeByType(@NonNull String databaseName, @NonNull String schemaType)
+ throws AppSearchException, InterruptedException {
+ awaitInitialized();
+
+ String qualifiedType = getDatabasePrefix(databaseName) + schemaType;
+ DeleteBySchemaTypeResultProto deleteBySchemaTypeResultProto;
+ mReadWriteLock.writeLock().lock();
+ try {
+ Set<String> existingSchemaTypes = mSchemaMap.get(databaseName);
+ if (existingSchemaTypes == null || !existingSchemaTypes.contains(qualifiedType)) {
+ return;
+ }
+ deleteBySchemaTypeResultProto = mIcingSearchEngine.deleteBySchemaType(qualifiedType);
+ checkForOptimize(/* force= */true);
+ } finally {
+ mReadWriteLock.writeLock().unlock();
+ }
+ checkSuccess(deleteBySchemaTypeResultProto.getStatus());
+ }
+
+ /**
+ * Removes all documents having the given {@code namespace} in given database.
+ *
+ * <p>This method belongs to mutate group.
+ *
+ * @param databaseName The databaseName that contains documents of namespace.
+ * @param namespace The namespace of documents to remove.
+ * @throws AppSearchException on IcingSearchEngine error.
+ * @throws InterruptedException if the current thread was interrupted during execution.
+ */
+ public void removeByNamespace(@NonNull String databaseName, @NonNull String namespace)
+ throws AppSearchException, InterruptedException {
+ awaitInitialized();
+
+ String qualifiedNamespace = getDatabasePrefix(databaseName) + namespace;
+ DeleteByNamespaceResultProto deleteByNamespaceResultProto;
+ mReadWriteLock.writeLock().lock();
+ try {
+ Set<String> existingNamespaces = mNamespaceMap.get(databaseName);
+ if (existingNamespaces == null || !existingNamespaces.contains(qualifiedNamespace)) {
+ return;
+ }
+ deleteByNamespaceResultProto = mIcingSearchEngine.deleteByNamespace(qualifiedNamespace);
+ checkForOptimize(/* force= */true);
+ } finally {
+ mReadWriteLock.writeLock().unlock();
+ }
+ checkSuccess(deleteByNamespaceResultProto.getStatus());
+ }
+
+ /**
+ * Clears the given database by removing all documents and types.
+ *
+ * <p>The schemas will remain. To clear everything including schemas, please call
+ * {@link #setSchema} with an empty schema and {@code forceOverride} set to true.
+ *
+ * <p>This method belongs to mutate group.
+ *
+ * @param databaseName The databaseName to remove all documents from.
+ * @throws AppSearchException on IcingSearchEngine error.
+ * @throws InterruptedException if the current thread was interrupted during execution.
+ */
+ public void removeAll(@NonNull String databaseName)
+ throws AppSearchException, InterruptedException {
+ awaitInitialized();
+ mReadWriteLock.writeLock().lock();
+ try {
+ Set<String> existingNamespaces = mNamespaceMap.get(databaseName);
+ if (existingNamespaces == null) {
+ return;
+ }
+ for (String namespace : existingNamespaces) {
+ DeleteByNamespaceResultProto deleteByNamespaceResultProto =
+ mIcingSearchEngine.deleteByNamespace(namespace);
+ // There's no way for AppSearch to know that all documents in a particular
+ // namespace have been deleted, but if you try to delete an empty namespace, Icing
+ // returns NOT_FOUND. Just ignore that code.
+ checkCodeOneOf(
+ deleteByNamespaceResultProto.getStatus(),
+ StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
+ }
+ mNamespaceMap.remove(databaseName);
+ checkForOptimize(/* force= */true);
+ } finally {
+ mReadWriteLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Clears documents and schema across all databaseNames.
+ *
+ * <p>This method belongs to mutate group.
+ *
+ * @throws AppSearchException on IcingSearchEngine error.
+ */
+ @VisibleForTesting
+ public void reset() throws AppSearchException {
+ ResetResultProto resetResultProto;
+ mReadWriteLock.writeLock().lock();
+ try {
+ resetResultProto = mIcingSearchEngine.reset();
+ mOptimizeIntervalCount = 0;
+ mSchemaMap.clear();
+ mNamespaceMap.clear();
+ } finally {
+ mReadWriteLock.writeLock().unlock();
+ }
+ checkSuccess(resetResultProto.getStatus());
+ }
+
+ /**
+ * Rewrites all types mentioned in the given {@code newSchema} to prepend {@code prefix}.
+ * Rewritten types will be added to the {@code existingSchema}.
+ *
+ * @param databaseName The name of the database where this schema lives.
+ * @param existingSchema A schema that may contain existing types from across all database
+ * instances. Will be mutated to contain the properly rewritten schema
+ * types from {@code newSchema}.
+ * @param newSchema Schema with types to add to the {@code existingSchema}.
+ * @return a Set contains all remaining qualified schema type names in given database.
+ */
+ @VisibleForTesting
+ Set<String> rewriteSchema(@NonNull String databaseName,
+ @NonNull SchemaProto.Builder existingSchema,
+ @NonNull SchemaProto newSchema) throws AppSearchException {
+ String prefix = getDatabasePrefix(databaseName);
+ HashMap<String, SchemaTypeConfigProto> newTypesToProto = new HashMap<>();
+ // Rewrite the schema type to include the typePrefix.
+ for (int typeIdx = 0; typeIdx < newSchema.getTypesCount(); typeIdx++) {
+ SchemaTypeConfigProto.Builder typeConfigBuilder =
+ newSchema.getTypes(typeIdx).toBuilder();
+
+ // Rewrite SchemaProto.types.schema_type
+ String newSchemaType = prefix + typeConfigBuilder.getSchemaType();
+ typeConfigBuilder.setSchemaType(newSchemaType);
+
+ // Rewrite SchemaProto.types.properties.schema_type
+ for (int propertyIdx = 0;
+ propertyIdx < typeConfigBuilder.getPropertiesCount();
+ propertyIdx++) {
+ PropertyConfigProto.Builder propertyConfigBuilder =
+ typeConfigBuilder.getProperties(propertyIdx).toBuilder();
+ if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
+ String newPropertySchemaType =
+ prefix + propertyConfigBuilder.getSchemaType();
+ propertyConfigBuilder.setSchemaType(newPropertySchemaType);
+ typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+ }
+ }
+
+ newTypesToProto.put(newSchemaType, typeConfigBuilder.build());
+ }
+
+ Set<String> newSchemaTypesName = newTypesToProto.keySet();
+
+ // Combine the existing schema (which may have types from other databases) with this
+ // database's new schema. Modifies the existingSchemaBuilder.
+ // Check if we need to replace any old schema types with the new ones.
+ for (int i = 0; i < existingSchema.getTypesCount(); i++) {
+ String schemaType = existingSchema.getTypes(i).getSchemaType();
+ SchemaTypeConfigProto newProto = newTypesToProto.remove(schemaType);
+ if (newProto != null) {
+ // Replacement
+ existingSchema.setTypes(i, newProto);
+ } else if (databaseName.equals(getDatabaseName(schemaType))) {
+ // All types existing before but not in newSchema should be removed.
+ existingSchema.removeTypes(i);
+ --i;
+ }
+ }
+ // We've been removing existing types from newTypesToProto, so everything that remains is
+ // new.
+ existingSchema.addAllTypes(newTypesToProto.values());
+
+ return newSchemaTypesName;
+ }
+
+ /**
+ * Rewrites all types and namespaces mentioned anywhere in {@code documentBuilder} to prepend
+ * or remove {@code prefix}.
+ *
+ * @param prefix The prefix to add or remove
+ * @param documentBuilder The document to mutate
+ * @param add Whether to add prefix to the types and namespaces. If {@code false},
+ * prefix will be removed.
+ * @throws IllegalStateException If {@code add=false} and the document has a type or namespace
+ * that doesn't start with {@code prefix}.
+ */
+ @VisibleForTesting
+ void rewriteDocumentTypes(
+ @NonNull String prefix,
+ @NonNull DocumentProto.Builder documentBuilder,
+ boolean add) {
+ // Rewrite the type name to include/remove the prefix.
+ String newSchema;
+ if (add) {
+ newSchema = prefix + documentBuilder.getSchema();
+ } else {
+ newSchema = removePrefix(prefix, "schemaType", documentBuilder.getSchema());
+ }
+ documentBuilder.setSchema(newSchema);
+
+ // Rewrite the namespace to include/remove the prefix.
+ if (add) {
+ documentBuilder.setNamespace(prefix + documentBuilder.getNamespace());
+ } else {
+ documentBuilder.setNamespace(
+ removePrefix(prefix, "namespace", documentBuilder.getNamespace()));
+ }
+
+ // Recurse into derived documents
+ for (int propertyIdx = 0;
+ propertyIdx < documentBuilder.getPropertiesCount();
+ propertyIdx++) {
+ int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
+ if (documentCount > 0) {
+ PropertyProto.Builder propertyBuilder =
+ documentBuilder.getProperties(propertyIdx).toBuilder();
+ for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
+ DocumentProto.Builder derivedDocumentBuilder =
+ propertyBuilder.getDocumentValues(documentIdx).toBuilder();
+ rewriteDocumentTypes(prefix, derivedDocumentBuilder, add);
+ propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
+ }
+ documentBuilder.setProperties(propertyIdx, propertyBuilder);
+ }
+ }
+ }
+
+ /**
+ * Rewrites searchSpec by adding schemaTypeFilter and namespacesFilter
+ *
+ * <p>If user input empty filter lists, will look up {@link #mSchemaMap} and
+ * {@link #mNamespaceMap} and put all values belong to current database to narrow down Icing
+ * search area.
+ * <p>This method should be only called in query methods and get the READ lock to keep thread
+ * safety.
+ * @return false if the current database is brand new and contains nothing. We should just
+ * return an empty query result to user.
+ */
+ @VisibleForTesting
+ @GuardedBy("mReadWriteLock")
+ boolean rewriteSearchSpecForNonEmptyDatabase(@NonNull String databaseName,
+ @NonNull SearchSpecProto.Builder searchSpecBuilder) {
+ Set<String> existingSchemaTypes = mSchemaMap.get(databaseName);
+ Set<String> existingNamespaces = mNamespaceMap.get(databaseName);
+ if (existingSchemaTypes == null || existingSchemaTypes.isEmpty()
+ || existingNamespaces == null || existingNamespaces.isEmpty()) {
+ return false;
+ }
+ // Rewrite any existing schema types specified in the searchSpec, or add schema types to
+ // limit the search to this database instance.
+ if (searchSpecBuilder.getSchemaTypeFiltersCount() > 0) {
+ for (int i = 0; i < searchSpecBuilder.getSchemaTypeFiltersCount(); i++) {
+ String qualifiedType = getDatabasePrefix(databaseName)
+ + searchSpecBuilder.getSchemaTypeFilters(i);
+ if (existingSchemaTypes.contains(qualifiedType)) {
+ searchSpecBuilder.setSchemaTypeFilters(i, qualifiedType);
+ }
+ }
+ } else {
+ searchSpecBuilder.addAllSchemaTypeFilters(existingSchemaTypes);
+ }
+
+ // Rewrite any existing namespaces specified in the searchSpec, or add namespaces to
+ // limit the search to this database instance.
+ if (searchSpecBuilder.getNamespaceFiltersCount() > 0) {
+ for (int i = 0; i < searchSpecBuilder.getNamespaceFiltersCount(); i++) {
+ String qualifiedNamespace = getDatabasePrefix(databaseName)
+ + searchSpecBuilder.getNamespaceFilters(i);
+ searchSpecBuilder.setNamespaceFilters(i, qualifiedNamespace);
+ }
+ } else {
+ searchSpecBuilder.addAllNamespaceFilters(existingNamespaces);
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ SchemaProto getSchemaProto() throws AppSearchException {
+ GetSchemaResultProto schemaProto = mIcingSearchEngine.getSchema();
+ // TODO(b/161935693) check GetSchemaResultProto is success or not. Call reset() if it's not.
+ // TODO(b/161935693) only allow GetSchemaResultProto NOT_FOUND on first run
+ checkCodeOneOf(schemaProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
+ return schemaProto.getSchema();
+ }
+
+ @NonNull
+ private String getDatabasePrefix(@NonNull String databaseName) {
+ return databaseName + "/";
+ }
+
+ @NonNull
+ private String getDatabaseName(@NonNull String prefixedValue) throws AppSearchException {
+ int delimiterIndex = prefixedValue.indexOf('/');
+ if (delimiterIndex == -1) {
+ throw new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
+ "The databaseName prefixed value doesn't contains a valid database name.");
+ }
+ return prefixedValue.substring(0, delimiterIndex);
+ }
+
+ @NonNull
+ private static String removePrefix(@NonNull String prefix, @NonNull String inputType,
+ @NonNull String input) {
+ if (!input.startsWith(prefix)) {
+ throw new IllegalStateException(
+ "Unexpected " + inputType + " \"" + input
+ + "\" does not start with \"" + prefix + "\"");
+ }
+ return input.substring(prefix.length());
+ }
+
+ @GuardedBy("mReadWriteLock")
+ private void addToMap(Map<String, Set<String>> map, String databaseName, String prefixedValue) {
+ Set<String> values = map.get(databaseName);
+ if (values == null) {
+ values = new HashSet<>();
+ map.put(databaseName, values);
+ }
+ values.add(prefixedValue);
+ }
+
+ /**
+ * Waits for the instance to become initialized.
+ *
+ * @throws InterruptedException if the current thread was interrupted during waiting.
+ */
+ private void awaitInitialized() throws InterruptedException {
+ mInitCompleteLatch.await();
+ }
+
+ /**
+ * Checks the given status code and throws an {@link AppSearchException} if code is an error.
+ *
+ * @throws AppSearchException on error codes.
+ */
+ private void checkSuccess(StatusProto statusProto) throws AppSearchException {
+ checkCodeOneOf(statusProto, StatusProto.Code.OK);
+ }
+
+ /**
+ * Checks the given status code is one of the provided codes, and throws an
+ * {@link AppSearchException} if it is not.
+ */
+ private void checkCodeOneOf(StatusProto statusProto, StatusProto.Code... codes)
+ throws AppSearchException {
+ for (int i = 0; i < codes.length; i++) {
+ if (codes[i] == statusProto.getCode()) {
+ // Everything's good
+ return;
+ }
+ }
+
+ if (statusProto.getCode() == StatusProto.Code.WARNING_DATA_LOSS) {
+ // TODO: May want to propagate WARNING_DATA_LOSS up to AppSearchManager so they can
+ // choose to log the error or potentially pass it on to clients.
+ Log.w(TAG, "Encountered WARNING_DATA_LOSS: " + statusProto.getMessage());
+ return;
+ }
+
+ throw statusProtoToAppSearchException(statusProto);
+ }
+
+ /**
+ * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources.
+ *
+ * <p>This method should be only called in mutate methods and get the WRITE lock to keep thread
+ * safety.
+ * <p>{@link IcingSearchEngine#optimize()} should be called only if
+ * {@link GetOptimizeInfoResultProto} shows there is enough resources could be released.
+ * <p>{@link IcingSearchEngine#getOptimizeInfo()} should be called once per
+ * {@link #CHECK_OPTIMIZE_INTERVAL} of remove executions.
+ *
+ * @param force whether we should directly call {@link IcingSearchEngine#getOptimizeInfo()}.
+ */
+ @GuardedBy("mReadWriteLock")
+ private void checkForOptimize(boolean force) throws AppSearchException {
+ ++mOptimizeIntervalCount;
+ if (force || mOptimizeIntervalCount >= CHECK_OPTIMIZE_INTERVAL) {
+ mOptimizeIntervalCount = 0;
+ GetOptimizeInfoResultProto optimizeInfo = getOptimizeInfoResult();
+ checkSuccess(optimizeInfo.getStatus());
+ // Second threshold, decide when to call optimize().
+ if (optimizeInfo.getOptimizableDocs() >= OPTIMIZE_THRESHOLD_DOC_COUNT
+ || optimizeInfo.getEstimatedOptimizableBytes()
+ >= OPTIMIZE_THRESHOLD_BYTES) {
+ // TODO(b/155939114): call optimize in the same thread will slow down api calls
+ // significantly. Move this call to background.
+ OptimizeResultProto optimizeResultProto = mIcingSearchEngine.optimize();
+ checkSuccess(optimizeResultProto.getStatus());
+ }
+ // TODO(b/147699081): Return OptimizeResultProto & log lost data detail once we add
+ // a field to indicate lost_schema and lost_documents in OptimizeResultProto.
+ // go/icing-library-apis.
+ }
+ }
+
+ /** Remove the rewritten schema types from any result documents.*/
+ private SearchResultProto rewriteSearchResultProto(@NonNull String databaseName,
+ @NonNull SearchResultProto searchResultProto) {
+ SearchResultProto.Builder searchResultsBuilder = searchResultProto.toBuilder();
+ for (int i = 0; i < searchResultsBuilder.getResultsCount(); i++) {
+ if (searchResultProto.getResults(i).hasDocument()) {
+ SearchResultProto.ResultProto.Builder resultBuilder =
+ searchResultsBuilder.getResults(i).toBuilder();
+ DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
+ rewriteDocumentTypes(
+ getDatabasePrefix(databaseName), documentBuilder, /*add=*/false);
+ resultBuilder.setDocument(documentBuilder);
+ searchResultsBuilder.setResults(i, resultBuilder);
+ }
+ }
+ return searchResultsBuilder.build();
+ }
+
+ @VisibleForTesting
+ GetOptimizeInfoResultProto getOptimizeInfoResult() {
+ return mIcingSearchEngine.getOptimizeInfo();
+ }
+
+ /**
+ * Converts an erroneous status code to an AppSearchException. Callers should ensure that
+ * the status code is not OK or WARNING_DATA_LOSS.
+ *
+ * @param statusProto StatusProto with error code and message to translate into
+ * AppSearchException.
+ * @return AppSearchException with the parallel error code.
+ */
+ private AppSearchException statusProtoToAppSearchException(StatusProto statusProto) {
+ switch (statusProto.getCode()) {
+ case INVALID_ARGUMENT:
+ return new AppSearchException(AppSearchResult.RESULT_INVALID_ARGUMENT,
+ statusProto.getMessage());
+ case NOT_FOUND:
+ return new AppSearchException(AppSearchResult.RESULT_NOT_FOUND,
+ statusProto.getMessage());
+ case FAILED_PRECONDITION:
+ // Fallthrough
+ case ABORTED:
+ // Fallthrough
+ case INTERNAL:
+ return new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+ statusProto.getMessage());
+ case OUT_OF_SPACE:
+ return new AppSearchException(AppSearchResult.RESULT_OUT_OF_SPACE,
+ statusProto.getMessage());
+ default:
+ // Some unknown/unsupported error
+ return new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
+ "Unknown IcingSearchEngine status code: " + statusProto.getCode());
+ }
+ }
+}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java b/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java
deleted file mode 100644
index 4358d20..0000000
--- a/apex/appsearch/service/java/com/android/server/appsearch/impl/AppSearchImpl.java
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- * Copyright (C) 2019 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.impl;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.content.Context;
-import android.util.ArraySet;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import com.google.android.icing.proto.DocumentProto;
-import com.google.android.icing.proto.PropertyConfigProto;
-import com.google.android.icing.proto.PropertyProto;
-import com.google.android.icing.proto.ResultSpecProto;
-import com.google.android.icing.proto.SchemaProto;
-import com.google.android.icing.proto.SchemaTypeConfigProto;
-import com.google.android.icing.proto.ScoringSpecProto;
-import com.google.android.icing.proto.SearchResultProto;
-import com.google.android.icing.proto.SearchSpecProto;
-
-import java.util.Set;
-
-/**
- * Manages interaction with {@link FakeIcing} and other components to implement AppSearch
- * functionality.
- */
-public final class AppSearchImpl {
- private final Context mContext;
- private final @UserIdInt int mUserId;
- private final FakeIcing mFakeIcing = new FakeIcing();
-
- AppSearchImpl(@NonNull Context context, @UserIdInt int userId) {
- mContext = context;
- mUserId = userId;
- }
-
- /**
- * Updates the AppSearch schema for this app.
- *
- * @param callingUid The uid of the app calling AppSearch.
- * @param origSchema The schema to set for this app.
- * @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents
- * which do not comply with the new schema will be deleted.
- */
- public void setSchema(int callingUid, @NonNull SchemaProto origSchema, boolean forceOverride) {
- // Rewrite schema type names to include the calling app's package and uid.
- String typePrefix = getTypePrefix(callingUid);
- SchemaProto.Builder schemaBuilder = origSchema.toBuilder();
- rewriteSchemaTypes(typePrefix, schemaBuilder);
-
- // TODO(b/145635424): Save in schema type map
- // TODO(b/145635424): Apply the schema to Icing and report results
- }
-
- /**
- * Rewrites all types mentioned in the given {@code schemaBuilder} to prepend
- * {@code typePrefix}.
- *
- * @param typePrefix The prefix to add
- * @param schemaBuilder The schema to mutate
- */
- @VisibleForTesting
- void rewriteSchemaTypes(
- @NonNull String typePrefix, @NonNull SchemaProto.Builder schemaBuilder) {
- for (int typeIdx = 0; typeIdx < schemaBuilder.getTypesCount(); typeIdx++) {
- SchemaTypeConfigProto.Builder typeConfigBuilder =
- schemaBuilder.getTypes(typeIdx).toBuilder();
-
- // Rewrite SchemaProto.types.schema_type
- String newSchemaType = typePrefix + typeConfigBuilder.getSchemaType();
- typeConfigBuilder.setSchemaType(newSchemaType);
-
- // Rewrite SchemaProto.types.properties.schema_type
- for (int propertyIdx = 0;
- propertyIdx < typeConfigBuilder.getPropertiesCount();
- propertyIdx++) {
- PropertyConfigProto.Builder propertyConfigBuilder =
- typeConfigBuilder.getProperties(propertyIdx).toBuilder();
- if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
- String newPropertySchemaType =
- typePrefix + propertyConfigBuilder.getSchemaType();
- propertyConfigBuilder.setSchemaType(newPropertySchemaType);
- typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
- }
- }
-
- schemaBuilder.setTypes(typeIdx, typeConfigBuilder);
- }
- }
-
- /**
- * Adds a document to the AppSearch index.
- *
- * @param callingUid The uid of the app calling AppSearch.
- * @param origDocument The document to index.
- */
- public void putDocument(int callingUid, @NonNull DocumentProto origDocument) {
- // Rewrite the type names to include the app's prefix
- String typePrefix = getTypePrefix(callingUid);
- DocumentProto.Builder documentBuilder = origDocument.toBuilder();
- rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/ true);
- mFakeIcing.put(documentBuilder.build());
- }
-
- /**
- * Retrieves a document from the AppSearch index by URI.
- *
- * @param callingUid The uid of the app calling AppSearch.
- * @param uri The URI of the document to get.
- * @return The Document contents, or {@code null} if no such URI exists in the system.
- */
- @Nullable
- public DocumentProto getDocument(int callingUid, @NonNull String uri) {
- String typePrefix = getTypePrefix(callingUid);
- DocumentProto document = mFakeIcing.get(uri);
- if (document == null) {
- return null;
- }
-
- // TODO(b/146526096): Since FakeIcing doesn't currently handle namespaces, we perform a
- // post-filter to make sure we don't return documents we shouldn't. This should be removed
- // once the real Icing Lib is implemented.
- if (!document.getNamespace().equals(typePrefix)) {
- return null;
- }
-
- // Rewrite the type names to remove the app's prefix
- DocumentProto.Builder documentBuilder = document.toBuilder();
- rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/ false);
- return documentBuilder.build();
- }
-
- /**
- * Executes a query against the AppSearch index and returns results.
- *
- * @param callingUid The uid of the app calling AppSearch.
- * @param searchSpec Defines what and how to search
- * @param resultSpec Defines what results to show
- * @param scoringSpec Defines how to order results
- * @return The results of performing this search The proto might have no {@code results} if no
- * documents matched the query.
- */
- @NonNull
- public SearchResultProto query(
- int callingUid,
- @NonNull SearchSpecProto searchSpec,
- @NonNull ResultSpecProto resultSpec,
- @NonNull ScoringSpecProto scoringSpec) {
- String typePrefix = getTypePrefix(callingUid);
- SearchResultProto searchResults = mFakeIcing.query(searchSpec.getQuery());
- if (searchResults.getResultsCount() == 0) {
- return searchResults;
- }
- Set<String> qualifiedSearchFilters = null;
- if (searchSpec.getSchemaTypeFiltersCount() > 0) {
- qualifiedSearchFilters = new ArraySet<>(searchSpec.getSchemaTypeFiltersCount());
- for (String schema : searchSpec.getSchemaTypeFiltersList()) {
- String qualifiedSchema = typePrefix + schema;
- qualifiedSearchFilters.add(qualifiedSchema);
- }
- }
- // Rewrite the type names to remove the app's prefix
- SearchResultProto.Builder searchResultsBuilder = searchResults.toBuilder();
- for (int i = 0; i < searchResultsBuilder.getResultsCount(); i++) {
- if (searchResults.getResults(i).hasDocument()) {
- SearchResultProto.ResultProto.Builder resultBuilder =
- searchResultsBuilder.getResults(i).toBuilder();
-
- // TODO(b/145631811): Since FakeIcing doesn't currently handle namespaces, we
- // perform a post-filter to make sure we don't return documents we shouldn't. This
- // should be removed once the real Icing Lib is implemented.
- if (!resultBuilder.getDocument().getNamespace().equals(typePrefix)) {
- searchResultsBuilder.removeResults(i);
- i--;
- continue;
- }
-
- // TODO(b/145631811): Since FakeIcing doesn't currently handle type names, we
- // perform a post-filter to make sure we don't return documents we shouldn't. This
- // should be removed once the real Icing Lib is implemented.
- if (qualifiedSearchFilters != null
- && !qualifiedSearchFilters.contains(
- resultBuilder.getDocument().getSchema())) {
- searchResultsBuilder.removeResults(i);
- i--;
- continue;
- }
-
- DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
- rewriteDocumentTypes(typePrefix, documentBuilder, /*add=*/false);
- resultBuilder.setDocument(documentBuilder);
- searchResultsBuilder.setResults(i, resultBuilder);
- }
- }
- return searchResultsBuilder.build();
- }
-
- /** Deletes the given document by URI */
- public boolean delete(int callingUid, @NonNull String uri) {
- DocumentProto document = mFakeIcing.get(uri);
- if (document == null) {
- return false;
- }
-
- // TODO(b/146526096): Since FakeIcing doesn't currently handle namespaces, we perform a
- // post-filter to make sure we don't delete documents we shouldn't. This should be
- // removed once the real Icing Lib is implemented.
- String typePrefix = getTypePrefix(callingUid);
- if (!typePrefix.equals(document.getNamespace())) {
- throw new SecurityException(
- "Failed to delete document " + uri + "; URI collision in FakeIcing");
- }
-
- return mFakeIcing.delete(uri);
- }
-
- /** Deletes all documents having the given {@code schemaType}. */
- public boolean deleteByType(int callingUid, @NonNull String schemaType) {
- String typePrefix = getTypePrefix(callingUid);
- String qualifiedType = typePrefix + schemaType;
- return mFakeIcing.deleteByType(qualifiedType);
- }
-
- /**
- * Deletes all documents owned by the calling app.
- *
- * @param callingUid The uid of the app calling AppSearch.
- */
- public void deleteAll(int callingUid) {
- String namespace = getTypePrefix(callingUid);
- mFakeIcing.deleteByNamespace(namespace);
- }
-
- /**
- * Rewrites all types mentioned anywhere in {@code documentBuilder} to prepend or remove
- * {@code typePrefix}.
- *
- * @param typePrefix The prefix to add or remove
- * @param documentBuilder The document to mutate
- * @param add Whether to add typePrefix to the types. If {@code false}, typePrefix will be
- * removed from the types.
- * @throws IllegalArgumentException If {@code add=false} and the document has a type that
- * doesn't start with {@code typePrefix}.
- */
- @VisibleForTesting
- void rewriteDocumentTypes(
- @NonNull String typePrefix,
- @NonNull DocumentProto.Builder documentBuilder,
- boolean add) {
- // Rewrite the type name to include/remove the app's prefix
- String newSchema;
- if (add) {
- newSchema = typePrefix + documentBuilder.getSchema();
- } else {
- newSchema = removePrefix(typePrefix, documentBuilder.getSchema());
- }
- documentBuilder.setSchema(newSchema);
-
- // Add/remove namespace. If we ever allow users to set their own namespaces, this will have
- // to change to prepend the prefix instead of setting the whole namespace. We will also have
- // to store the namespaces in a map similar to the type map so we can rewrite queries with
- // empty namespaces.
- if (add) {
- documentBuilder.setNamespace(typePrefix);
- } else if (!documentBuilder.getNamespace().equals(typePrefix)) {
- throw new IllegalStateException(
- "Unexpected namespace \"" + documentBuilder.getNamespace()
- + "\" (expected \"" + typePrefix + "\")");
- } else {
- documentBuilder.clearNamespace();
- }
-
- // Recurse into derived documents
- for (int propertyIdx = 0;
- propertyIdx < documentBuilder.getPropertiesCount();
- propertyIdx++) {
- int documentCount = documentBuilder.getProperties(propertyIdx).getDocumentValuesCount();
- if (documentCount > 0) {
- PropertyProto.Builder propertyBuilder =
- documentBuilder.getProperties(propertyIdx).toBuilder();
- for (int documentIdx = 0; documentIdx < documentCount; documentIdx++) {
- DocumentProto.Builder derivedDocumentBuilder =
- propertyBuilder.getDocumentValues(documentIdx).toBuilder();
- rewriteDocumentTypes(typePrefix, derivedDocumentBuilder, add);
- propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
- }
- documentBuilder.setProperties(propertyIdx, propertyBuilder);
- }
- }
- }
-
- /**
- * Returns a type prefix in a format like {@code com.example.package@1000/} or
- * {@code com.example.sharedname:5678@1000/}.
- */
- @NonNull
- private String getTypePrefix(int callingUid) {
- // For regular apps, this call will return the package name. If callingUid is an
- // android:sharedUserId, this value may be another type of name and have a :uid suffix.
- String callingUidName = mContext.getPackageManager().getNameForUid(callingUid);
- if (callingUidName == null) {
- // Not sure how this is possible --- maybe app was uninstalled?
- throw new IllegalStateException("Failed to look up package name for uid " + callingUid);
- }
- return callingUidName + "@" + mUserId + "/";
- }
-
- @NonNull
- private static String removePrefix(@NonNull String prefix, @NonNull String input) {
- if (!input.startsWith(prefix)) {
- throw new IllegalArgumentException(
- "Input \"" + input + "\" does not start with \"" + prefix + "\"");
- }
- return input.substring(prefix.length());
- }
-}
diff --git a/apex/appsearch/service/java/com/android/server/appsearch/impl/FakeIcing.java b/apex/appsearch/service/java/com/android/server/appsearch/impl/FakeIcing.java
deleted file mode 100644
index da15734..0000000
--- a/apex/appsearch/service/java/com/android/server/appsearch/impl/FakeIcing.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright (C) 2019 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.impl;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.SparseArray;
-
-import com.google.android.icing.proto.DocumentProto;
-import com.google.android.icing.proto.PropertyProto;
-import com.google.android.icing.proto.SearchResultProto;
-import com.google.android.icing.proto.StatusProto;
-
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Fake in-memory implementation of the Icing key-value store and reverse index.
- * <p>
- * Currently, only queries by single exact term are supported. There is no support for persistence,
- * namespaces, i18n tokenization, or schema.
- */
-public class FakeIcing {
- private final AtomicInteger mNextDocId = new AtomicInteger();
- private final Map<String, Integer> mUriToDocIdMap = new ArrayMap<>();
- /** Array of Documents where index into the array is the docId. */
- private final SparseArray<DocumentProto> mDocStore = new SparseArray<>();
- /** Map of term to posting-list (the set of DocIds containing that term). */
- private final Map<String, Set<Integer>> mIndex = new ArrayMap<>();
-
- /**
- * Inserts a document into the index.
- *
- * @param document The document to insert.
- */
- public void put(@NonNull DocumentProto document) {
- String uri = document.getUri();
-
- // Update mDocIdMap
- Integer docId = mUriToDocIdMap.get(uri);
- if (docId != null) {
- // Delete the old doc
- mDocStore.remove(docId);
- }
-
- // Allocate a new docId
- docId = mNextDocId.getAndIncrement();
- mUriToDocIdMap.put(uri, docId);
-
- // Update mDocStore
- mDocStore.put(docId, document);
-
- // Update mIndex
- indexDocument(docId, document);
- }
-
- /**
- * Retrieves a document from the index.
- *
- * @param uri The URI of the document to retrieve.
- * @return The body of the document, or {@code null} if no such document exists.
- */
- @Nullable
- public DocumentProto get(@NonNull String uri) {
- Integer docId = mUriToDocIdMap.get(uri);
- if (docId == null) {
- return null;
- }
- return mDocStore.get(docId);
- }
-
- /**
- * Returns documents containing all words in the given query string.
- *
- * @param queryExpression A set of words to search for. They will be implicitly AND-ed together.
- * No operators are supported.
- * @return A {@link SearchResultProto} containing the matching documents, which may have no
- * results if no documents match.
- */
- @NonNull
- public SearchResultProto query(@NonNull String queryExpression) {
- String[] terms = normalizeString(queryExpression).split("\\s+");
- SearchResultProto.Builder results = SearchResultProto.newBuilder()
- .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK));
- if (terms.length == 0) {
- return results.build();
- }
- Set<Integer> docIds = mIndex.get(terms[0]);
- if (docIds == null || docIds.isEmpty()) {
- return results.build();
- }
- for (int i = 1; i < terms.length; i++) {
- Set<Integer> termDocIds = mIndex.get(terms[i]);
- if (termDocIds == null) {
- return results.build();
- }
- docIds.retainAll(termDocIds);
- if (docIds.isEmpty()) {
- return results.build();
- }
- }
- for (int docId : docIds) {
- DocumentProto document = mDocStore.get(docId);
- if (document != null) {
- results.addResults(
- SearchResultProto.ResultProto.newBuilder().setDocument(document));
- }
- }
- return results.build();
- }
-
- /**
- * Deletes a document by its URI.
- *
- * @param uri The URI of the document to be deleted.
- * @return Whether deletion was successful.
- */
- public boolean delete(@NonNull String uri) {
- // Update mDocIdMap
- Integer docId = mUriToDocIdMap.get(uri);
- if (docId != null) {
- // Delete the old doc
- mDocStore.remove(docId);
- mUriToDocIdMap.remove(uri);
- return true;
- }
- return false;
- }
-
- /** Deletes all documents having the given namespace. */
- public void deleteByNamespace(@NonNull String namespace) {
- for (int i = 0; i < mDocStore.size(); i++) {
- DocumentProto document = mDocStore.valueAt(i);
- if (namespace.equals(document.getNamespace())) {
- mDocStore.removeAt(i);
- mUriToDocIdMap.remove(document.getUri());
- i--;
- }
- }
- }
-
- /**
- * Deletes all documents having the given type.
- *
- * @return true if any documents were deleted.
- */
- public boolean deleteByType(@NonNull String type) {
- boolean deletedAny = false;
- for (int i = 0; i < mDocStore.size(); i++) {
- DocumentProto document = mDocStore.valueAt(i);
- if (type.equals(document.getSchema())) {
- mDocStore.removeAt(i);
- mUriToDocIdMap.remove(document.getUri());
- i--;
- deletedAny = true;
- }
- }
- return deletedAny;
- }
-
- private void indexDocument(int docId, DocumentProto document) {
- for (PropertyProto property : document.getPropertiesList()) {
- for (String stringValue : property.getStringValuesList()) {
- String[] words = normalizeString(stringValue).split("\\s+");
- for (String word : words) {
- indexTerm(docId, word);
- }
- }
- for (Long longValue : property.getInt64ValuesList()) {
- indexTerm(docId, longValue.toString());
- }
- for (Double doubleValue : property.getDoubleValuesList()) {
- indexTerm(docId, doubleValue.toString());
- }
- for (Boolean booleanValue : property.getBooleanValuesList()) {
- indexTerm(docId, booleanValue.toString());
- }
- // Intentionally skipping bytes values
- for (DocumentProto documentValue : property.getDocumentValuesList()) {
- indexDocument(docId, documentValue);
- }
- }
- }
-
- private void indexTerm(int docId, String term) {
- Set<Integer> postingList = mIndex.get(term);
- if (postingList == null) {
- postingList = new ArraySet<>();
- mIndex.put(term, postingList);
- }
- postingList.add(docId);
- }
-
- /** Strips out punctuation and converts to lowercase. */
- private static String normalizeString(String input) {
- return input.replaceAll("\\p{P}", "").toLowerCase(Locale.getDefault());
- }
-}
diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java b/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
index 0d5025a..54a281f2 100644
--- a/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
+++ b/core/tests/coretests/src/android/app/appsearch/AppSearchDocumentTest.java
@@ -238,7 +238,8 @@
.setSchema("schemaType1")
.setCreationTimestampMs(5L)
.setScore(1)
- .setTtlMs(1L);
+ .setTtlMs(1L)
+ .setNamespace("");
HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
propertyProtoMap.put("longKey1",
PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 7fc6bbd7..1f72374 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -78,6 +78,7 @@
"libbinder",
"libc++",
"libcutils",
+ "libicing",
"liblog",
"liblzma",
"libnativehelper",
diff --git a/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/AppSearchImplTest.java b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/AppSearchImplTest.java
new file mode 100644
index 0000000..24f7830
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appsearch/external/localbackend/AppSearchImplTest.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright 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.external.localbackend;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.app.appsearch.exceptions.AppSearchException;
+
+import com.android.server.appsearch.proto.DocumentProto;
+import com.android.server.appsearch.proto.GetOptimizeInfoResultProto;
+import com.android.server.appsearch.proto.IndexingConfig;
+import com.android.server.appsearch.proto.PropertyConfigProto;
+import com.android.server.appsearch.proto.PropertyProto;
+import com.android.server.appsearch.proto.ResultSpecProto;
+import com.android.server.appsearch.proto.SchemaProto;
+import com.android.server.appsearch.proto.SchemaTypeConfigProto;
+import com.android.server.appsearch.proto.ScoringSpecProto;
+import com.android.server.appsearch.proto.SearchResultProto;
+import com.android.server.appsearch.proto.SearchSpecProto;
+import com.android.server.appsearch.proto.StatusProto;
+import com.android.server.appsearch.proto.TermMatchType;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.util.Set;
+
+public class AppSearchImplTest {
+ @Rule
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+ private AppSearchImpl mAppSearchImpl;
+
+ @Before
+ public void setUp() throws Exception {
+ mAppSearchImpl = new AppSearchImpl(mTemporaryFolder.newFolder());
+ mAppSearchImpl.initialize();
+ }
+
+ /**
+ * Ensure that we can rewrite an incoming schema type by adding the database as a prefix. While
+ * also keeping any other existing schema types that may already be part of Icing's persisted
+ * schema.
+ */
+ @Test
+ public void testRewriteSchema() throws Exception {
+ SchemaProto.Builder existingSchemaBuilder = mAppSearchImpl.getSchemaProto().toBuilder();
+
+ SchemaProto newSchema = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("Foo").build())
+ .addTypes(SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("TestType")
+ .addProperties(PropertyConfigProto.newBuilder()
+ .setPropertyName("subject")
+ .setDataType(PropertyConfigProto.DataType.Code.STRING)
+ .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setIndexingConfig(
+ IndexingConfig.newBuilder()
+ .setTokenizerType(
+ IndexingConfig.TokenizerType.Code.PLAIN)
+ .setTermMatchType(TermMatchType.Code.PREFIX)
+ .build()
+ ).build()
+ ).addProperties(PropertyConfigProto.newBuilder()
+ .setPropertyName("link")
+ .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
+ .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setSchemaType("RefType")
+ .build()
+ ).build()
+ ).build();
+
+ Set<String> newTypes = mAppSearchImpl.rewriteSchema("databaseName", existingSchemaBuilder,
+ newSchema);
+ assertThat(newTypes).containsExactly("databaseName/Foo", "databaseName/TestType");
+
+ SchemaProto expectedSchema = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("databaseName/Foo").build())
+ .addTypes(SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("databaseName/TestType")
+ .addProperties(PropertyConfigProto.newBuilder()
+ .setPropertyName("subject")
+ .setDataType(PropertyConfigProto.DataType.Code.STRING)
+ .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setIndexingConfig(
+ IndexingConfig.newBuilder()
+ .setTokenizerType(
+ IndexingConfig.TokenizerType.Code.PLAIN)
+ .setTermMatchType(TermMatchType.Code.PREFIX)
+ .build()
+ ).build()
+ ).addProperties(PropertyConfigProto.newBuilder()
+ .setPropertyName("link")
+ .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
+ .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+ .setSchemaType("databaseName/RefType")
+ .build()
+ ).build())
+ .build();
+ assertThat(existingSchemaBuilder.getTypesList())
+ .containsExactlyElementsIn(expectedSchema.getTypesList());
+ }
+
+ @Test
+ public void testRewriteDocumentProto() {
+ DocumentProto insideDocument = DocumentProto.newBuilder()
+ .setUri("inside-uri")
+ .setSchema("type")
+ .setNamespace("namespace")
+ .build();
+ DocumentProto documentProto = DocumentProto.newBuilder()
+ .setUri("uri")
+ .setSchema("type")
+ .setNamespace("namespace")
+ .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
+ .build();
+
+ DocumentProto expectedInsideDocument = DocumentProto.newBuilder()
+ .setUri("inside-uri")
+ .setSchema("databaseName/type")
+ .setNamespace("databaseName/namespace")
+ .build();
+ DocumentProto expectedDocumentProto = DocumentProto.newBuilder()
+ .setUri("uri")
+ .setSchema("databaseName/type")
+ .setNamespace("databaseName/namespace")
+ .addProperties(PropertyProto.newBuilder().addDocumentValues(expectedInsideDocument))
+ .build();
+
+ DocumentProto.Builder actualDocument = documentProto.toBuilder();
+ mAppSearchImpl.rewriteDocumentTypes("databaseName/", actualDocument, /*add=*/true);
+ assertThat(actualDocument.build()).isEqualTo(expectedDocumentProto);
+ mAppSearchImpl.rewriteDocumentTypes("databaseName/", actualDocument, /*add=*/false);
+ assertThat(actualDocument.build()).isEqualTo(documentProto);
+ }
+
+ @Test
+ public void testOptimize() throws Exception {
+ // Insert schema
+ SchemaProto schema = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("type").build())
+ .build();
+ mAppSearchImpl.setSchema("database", schema, /*forceOverride=*/false);
+
+ // Insert enough documents.
+ for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
+ + AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
+ DocumentProto insideDocument = DocumentProto.newBuilder()
+ .setUri("inside-uri" + i)
+ .setSchema("type")
+ .setNamespace("namespace")
+ .build();
+ mAppSearchImpl.putDocument("database", insideDocument);
+ }
+
+ // Check optimize() will release 0 docs since there is no deletion.
+ GetOptimizeInfoResultProto optimizeInfo = mAppSearchImpl.getOptimizeInfoResult();
+ assertThat(optimizeInfo.getOptimizableDocs()).isEqualTo(0);
+
+ // delete 999 documents , we will reach the threshold to trigger optimize() in next
+ // deletion.
+ for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT - 1; i++) {
+ mAppSearchImpl.remove("database", "namespace", "inside-uri" + i);
+ }
+
+ // optimize() still not be triggered since we are in the interval to call getOptimizeInfo()
+ optimizeInfo = mAppSearchImpl.getOptimizeInfoResult();
+ assertThat(optimizeInfo.getOptimizableDocs())
+ .isEqualTo(AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT - 1);
+
+ // Keep delete docs, will reach the interval this time and trigger optimize().
+ for (int i = AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT;
+ i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
+ + AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
+ mAppSearchImpl.remove("database", "namespace", "inside-uri" + i);
+ }
+
+ // Verify optimize() is triggered
+ optimizeInfo = mAppSearchImpl.getOptimizeInfoResult();
+ assertThat(optimizeInfo.getOptimizableDocs())
+ .isLessThan((long) AppSearchImpl.CHECK_OPTIMIZE_INTERVAL);
+ }
+
+ @Test
+ public void testRewriteSearchSpec() throws Exception {
+ SearchSpecProto.Builder searchSpecProto =
+ SearchSpecProto.newBuilder().setQuery("");
+
+ // Insert schema
+ SchemaProto schema = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder()
+ .setSchemaType("type").build())
+ .build();
+ mAppSearchImpl.setSchema("database", schema, /*forceOverride=*/false);
+ // Insert document
+ DocumentProto insideDocument = DocumentProto.newBuilder()
+ .setUri("inside-uri")
+ .setSchema("type")
+ .setNamespace("namespace")
+ .build();
+ mAppSearchImpl.putDocument("database", insideDocument);
+
+ // Rewrite SearchSpec
+ mAppSearchImpl.rewriteSearchSpecForNonEmptyDatabase(
+ "database", searchSpecProto);
+ assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly("database/type");
+ assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly("database/namespace");
+ }
+
+ @Test
+ public void testQueryEmptyDatabase() throws Exception {
+ SearchResultProto searchResultProto = mAppSearchImpl.query("EmptyDatabase",
+ SearchSpecProto.getDefaultInstance(),
+ ResultSpecProto.getDefaultInstance(), ScoringSpecProto.getDefaultInstance());
+ assertThat(searchResultProto.getResultsCount()).isEqualTo(0);
+ assertThat(searchResultProto.getStatus().getCode()).isEqualTo(StatusProto.Code.OK);
+ }
+
+ @Test
+ public void testRemoveEmptyDatabase_NoExceptionThrown() throws Exception {
+ mAppSearchImpl.removeByType("EmptyDatabase", "FakeType");
+ mAppSearchImpl.removeByNamespace("EmptyDatabase", "FakeNamespace");
+ mAppSearchImpl.removeAll("EmptyDatabase");
+ }
+
+ @Test
+ public void testSetSchema() throws Exception {
+ // Create schemas
+ SchemaProto schemaProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email")).build();
+
+ // Set schema Email to AppSearch database1
+ mAppSearchImpl.setSchema("database1", schemaProto, /*forceOverride=*/false);
+
+ // Create excepted schemaType proto.
+ SchemaProto exceptedProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email"))
+ .build();
+ assertThat(mAppSearchImpl.getSchemaProto().getTypesList())
+ .containsExactlyElementsIn(exceptedProto.getTypesList());
+ }
+
+ @Test
+ public void testRemoveSchema() throws Exception {
+ // Create schemas
+ SchemaProto schemaProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email"))
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Document")).build();
+
+ // Set schema Email and Document to AppSearch database1
+ mAppSearchImpl.setSchema("database1", schemaProto, /*forceOverride=*/false);
+
+ // Create excepted schemaType proto.
+ SchemaProto exceptedProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email"))
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Document"))
+ .build();
+
+ // Check both schema Email and Document saved correctly.
+ assertThat(mAppSearchImpl.getSchemaProto().getTypesList())
+ .containsExactlyElementsIn(exceptedProto.getTypesList());
+
+ // Save only Email this time.
+ schemaProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email")).build();
+
+ // Check the incompatible error has been thrown.
+ SchemaProto finalSchemaProto = schemaProto;
+ AppSearchException e = expectThrows(AppSearchException.class, () ->
+ mAppSearchImpl.setSchema("database1", finalSchemaProto, /*forceOverride=*/false));
+ assertThat(e).hasMessageThat().isEqualTo("Schema is incompatible.");
+
+ // ForceOverride to delete.
+ mAppSearchImpl.setSchema("database1", finalSchemaProto, /*forceOverride=*/true);
+
+ // Check Document schema is removed.
+ exceptedProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email"))
+ .build();
+ assertThat(mAppSearchImpl.getSchemaProto().getTypesList())
+ .containsExactlyElementsIn(exceptedProto.getTypesList());
+ }
+
+ @Test
+ public void testRemoveSchema_differentDataBase() throws Exception {
+ // Create schemas
+ SchemaProto emailAndDocSchemaProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email"))
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Document")).build();
+
+ // Set schema Email and Document to AppSearch database1 and 2
+ mAppSearchImpl.setSchema("database1", emailAndDocSchemaProto, /*forceOverride=*/false);
+ mAppSearchImpl.setSchema("database2", emailAndDocSchemaProto, /*forceOverride=*/false);
+
+ // Create excepted schemaType proto.
+ SchemaProto exceptedProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email"))
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Document"))
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database2/Email"))
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database2/Document"))
+ .build();
+
+ // Check Email and Document is saved in database 1 and 2 correctly.
+ assertThat(mAppSearchImpl.getSchemaProto().getTypesList())
+ .containsExactlyElementsIn(exceptedProto.getTypesList());
+
+ // Save only Email to database1 this time.
+ SchemaProto emailSchemaProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("Email"))
+ .build();
+ mAppSearchImpl.setSchema("database1", emailSchemaProto, /*forceOverride=*/true);
+
+ // Create excepted schemaType list, database 1 should only contain Email but database 2
+ // remains in same.
+ exceptedProto = SchemaProto.newBuilder()
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database1/Email"))
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database2/Email"))
+ .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType("database2/Document"))
+ .build();
+
+ // Check nothing changed in database2.
+ assertThat(mAppSearchImpl.getSchemaProto().getTypesList())
+ .containsExactlyElementsIn(exceptedProto.getTypesList());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/appsearch/impl/AppSearchImplTest.java b/services/tests/servicestests/src/com/android/server/appsearch/impl/AppSearchImplTest.java
deleted file mode 100644
index 8986cba..0000000
--- a/services/tests/servicestests/src/com/android/server/appsearch/impl/AppSearchImplTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2019 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.impl;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.expectThrows;
-
-import android.annotation.UserIdInt;
-import android.content.Context;
-import android.os.UserHandle;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.appsearch.proto.IndexingConfig;
-import com.android.server.appsearch.proto.PropertyConfigProto;
-import com.android.server.appsearch.proto.SchemaProto;
-import com.android.server.appsearch.proto.SchemaTypeConfigProto;
-import com.android.server.appsearch.proto.TermMatchType;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class AppSearchImplTest {
- private final Context mContext = InstrumentationRegistry.getContext();
- private final @UserIdInt int mUserId = UserHandle.getCallingUserId();
-
- @Test
- public void testRewriteSchemaTypes() {
- SchemaProto inSchema = SchemaProto.newBuilder()
- .addTypes(SchemaTypeConfigProto.newBuilder()
- .setSchemaType("TestType")
- .addProperties(PropertyConfigProto.newBuilder()
- .setPropertyName("subject")
- .setDataType(PropertyConfigProto.DataType.Code.STRING)
- .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
- .setIndexingConfig(
- IndexingConfig.newBuilder()
- .setTokenizerType(
- IndexingConfig.TokenizerType.Code.PLAIN)
- .setTermMatchType(TermMatchType.Code.PREFIX)
- .build()
- ).build()
- ).addProperties(PropertyConfigProto.newBuilder()
- .setPropertyName("link")
- .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
- .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
- .setSchemaType("RefType")
- .build()
- ).build()
- ).build();
-
- SchemaProto expectedSchema = SchemaProto.newBuilder()
- .addTypes(SchemaTypeConfigProto.newBuilder()
- .setSchemaType("com.android.server.appsearch.impl@42:TestType")
- .addProperties(PropertyConfigProto.newBuilder()
- .setPropertyName("subject")
- .setDataType(PropertyConfigProto.DataType.Code.STRING)
- .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
- .setIndexingConfig(
- IndexingConfig.newBuilder()
- .setTokenizerType(
- IndexingConfig.TokenizerType.Code.PLAIN)
- .setTermMatchType(TermMatchType.Code.PREFIX)
- .build()
- ).build()
- ).addProperties(PropertyConfigProto.newBuilder()
- .setPropertyName("link")
- .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
- .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
- .setSchemaType("com.android.server.appsearch.impl@42:RefType")
- .build()
- ).build()
- ).build();
-
- AppSearchImpl impl = new AppSearchImpl(mContext, mUserId);
- SchemaProto.Builder actualSchema = inSchema.toBuilder();
- impl.rewriteSchemaTypes("com.android.server.appsearch.impl@42:", actualSchema);
-
- assertThat(actualSchema.build()).isEqualTo(expectedSchema);
- }
-
- @Test
- public void testPackageNotFound() {
- AppSearchImpl impl = new AppSearchImpl(mContext, mUserId);
- IllegalStateException e = expectThrows(
- IllegalStateException.class,
- () -> impl.setSchema(
- /*callingUid=*/Integer.MAX_VALUE,
- SchemaProto.getDefaultInstance(),
- /*forceOverride=*/false));
- assertThat(e).hasMessageThat().contains("Failed to look up package name");
- }
-}
diff --git a/services/tests/servicestests/src/com/android/server/appsearch/impl/FakeIcingTest.java b/services/tests/servicestests/src/com/android/server/appsearch/impl/FakeIcingTest.java
deleted file mode 100644
index 3196fbe..0000000
--- a/services/tests/servicestests/src/com/android/server/appsearch/impl/FakeIcingTest.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2019 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.impl;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.appsearch.proto.DocumentProto;
-import com.android.server.appsearch.proto.PropertyProto;
-import com.android.server.appsearch.proto.SearchResultProto;
-import com.android.server.appsearch.proto.StatusProto;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RunWith(AndroidJUnit4.class)
-public class FakeIcingTest {
- @Test
- public void query() {
- FakeIcing icing = new FakeIcing();
- icing.put(createDoc("uri:cat", "The cat said meow"));
- icing.put(createDoc("uri:dog", "The dog said woof"));
-
- assertThat(queryGetUris(icing, "meow")).containsExactly("uri:cat");
- assertThat(queryGetUris(icing, "said")).containsExactly("uri:cat", "uri:dog");
- assertThat(queryGetUris(icing, "fred")).isEmpty();
- }
-
- @Test
- public void queryNorm() {
- FakeIcing icing = new FakeIcing();
- icing.put(createDoc("uri:cat", "The cat said meow"));
- icing.put(createDoc("uri:dog", "The dog said woof"));
-
- assertThat(queryGetUris(icing, "the")).containsExactly("uri:cat", "uri:dog");
- assertThat(queryGetUris(icing, "The")).containsExactly("uri:cat", "uri:dog");
- assertThat(queryGetUris(icing, "tHe")).containsExactly("uri:cat", "uri:dog");
- }
-
- @Test
- public void get() {
- DocumentProto cat = createDoc("uri:cat", "The cat said meow");
- FakeIcing icing = new FakeIcing();
- icing.put(cat);
- assertThat(icing.get("uri:cat")).isEqualTo(cat);
- }
-
- @Test
- public void replace() {
- DocumentProto cat = createDoc("uri:cat", "The cat said meow");
- DocumentProto dog = createDoc("uri:dog", "The dog said woof");
-
- FakeIcing icing = new FakeIcing();
- icing.put(cat);
- icing.put(dog);
-
- assertThat(queryGetUris(icing, "meow")).containsExactly("uri:cat");
- assertThat(queryGetUris(icing, "said")).containsExactly("uri:cat", "uri:dog");
- assertThat(icing.get("uri:cat")).isEqualTo(cat);
-
- // Replace
- DocumentProto cat2 = createDoc("uri:cat", "The cat said purr");
- DocumentProto bird = createDoc("uri:bird", "The cat said tweet");
- icing.put(cat2);
- icing.put(bird);
-
- assertThat(queryGetUris(icing, "meow")).isEmpty();
- assertThat(queryGetUris(icing, "said")).containsExactly("uri:cat", "uri:dog", "uri:bird");
- assertThat(icing.get("uri:cat")).isEqualTo(cat2);
- }
-
- @Test
- public void delete() {
- DocumentProto cat = createDoc("uri:cat", "The cat said meow");
- DocumentProto dog = createDoc("uri:dog", "The dog said woof");
-
- FakeIcing icing = new FakeIcing();
- icing.put(cat);
- icing.put(dog);
-
- assertThat(queryGetUris(icing, "meow")).containsExactly("uri:cat");
- assertThat(queryGetUris(icing, "said")).containsExactly("uri:cat", "uri:dog");
- assertThat(icing.get("uri:cat")).isEqualTo(cat);
-
- // Delete
- icing.delete("uri:cat");
- icing.delete("uri:notreal");
-
- assertThat(queryGetUris(icing, "meow")).isEmpty();
- assertThat(queryGetUris(icing, "said")).containsExactly("uri:dog");
- assertThat(icing.get("uri:cat")).isNull();
- }
-
- private static DocumentProto createDoc(String uri, String body) {
- return DocumentProto.newBuilder()
- .setUri(uri)
- .addProperties(PropertyProto.newBuilder().addStringValues(body))
- .build();
- }
-
- private static List<String> queryGetUris(FakeIcing icing, String term) {
- List<String> uris = new ArrayList<>();
- SearchResultProto results = icing.query(term);
- assertThat(results.getStatus().getCode()).isEqualTo(StatusProto.Code.OK);
- for (SearchResultProto.ResultProto result : results.getResultsList()) {
- uris.add(result.getDocument().getUri());
- }
- return uris;
- }
-}