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