Merge "Update ImageAnalysis' target rotation based on motion sensor." into androidx-main am: 4d02ddbe4e

Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/1685185

Change-Id: If403a592c21df79f64bb1a7a9d62d520d4e1de29
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 3a51601..5fd927b 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -1,30 +1,30 @@
 // Signature format: 4.0
 package androidx.appsearch.annotation {
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface AppSearchDocument {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface Document {
     method public abstract String name() default "";
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.CreationTimestampMillis {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.CreationTimestampMillis {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Namespace {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Id {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Property {
-    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Namespace {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Property {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
     method public abstract String name() default "";
     method public abstract boolean required() default false;
-    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Score {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Score {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.TtlMillis {
-  }
-
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Uri {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.TtlMillis {
   }
 
 }
@@ -32,16 +32,27 @@
 package androidx.appsearch.app {
 
   public final class AppSearchBatchResult<KeyType, ValueType> {
+    method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getAll();
     method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getFailures();
     method public java.util.Map<KeyType!,ValueType!> getSuccesses();
     method public boolean isSuccess();
   }
 
+  public static final class AppSearchBatchResult.Builder<KeyType, ValueType> {
+    ctor public AppSearchBatchResult.Builder();
+    method public androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!> build();
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setFailure(KeyType, int, String?);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setResult(KeyType, androidx.appsearch.app.AppSearchResult<ValueType!>);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setSuccess(KeyType, ValueType?);
+  }
+
   public final class AppSearchResult<ValueType> {
     method public String? getErrorMessage();
     method public int getResultCode();
     method public ValueType? getResultValue();
     method public boolean isSuccess();
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newFailedResult(int, String?);
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newSuccessfulResult(ValueType?);
     field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
     field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
     field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -49,6 +60,7 @@
     field public static final int RESULT_NOT_FOUND = 6; // 0x6
     field public static final int RESULT_OK = 0; // 0x0
     field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+    field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
@@ -57,28 +69,71 @@
     method public String getSchemaType();
   }
 
+  public static final class AppSearchSchema.BooleanPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BooleanPropertyConfig.Builder {
+    ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+  }
+
   public static final class AppSearchSchema.Builder {
     ctor public AppSearchSchema.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
   }
 
-  public static final class AppSearchSchema.PropertyConfig {
+  public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BytesPropertyConfig.Builder {
+    ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public String getSchemaType();
+    method public boolean shouldIndexNestedProperties();
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig.Builder {
+    ctor public AppSearchSchema.DocumentPropertyConfig.Builder(String, String);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig.Builder {
+    ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.Int64PropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.Int64PropertyConfig.Builder {
+    ctor public AppSearchSchema.Int64PropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.Int64PropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.Int64PropertyConfig.Builder setCardinality(int);
+  }
+
+  public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
-    method public int getDataType();
-    method public int getIndexingType();
     method public String getName();
-    method public String? getSchemaType();
-    method public int getTokenizerType();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
     field public static final int CARDINALITY_REQUIRED = 3; // 0x3
-    field public static final int DATA_TYPE_BOOLEAN = 4; // 0x4
-    field public static final int DATA_TYPE_BYTES = 5; // 0x5
-    field public static final int DATA_TYPE_DOCUMENT = 6; // 0x6
-    field public static final int DATA_TYPE_DOUBLE = 3; // 0x3
-    field public static final int DATA_TYPE_INT64 = 2; // 0x2
-    field public static final int DATA_TYPE_STRING = 1; // 0x1
+  }
+
+  public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public int getIndexingType();
+    method public int getTokenizerType();
     field public static final int INDEXING_TYPE_EXACT_TERMS = 1; // 0x1
     field public static final int INDEXING_TYPE_NONE = 0; // 0x0
     field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
@@ -86,37 +141,41 @@
     field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
   }
 
-  public static final class AppSearchSchema.PropertyConfig.Builder {
-    ctor public AppSearchSchema.PropertyConfig.Builder(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig build();
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setCardinality(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setDataType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setIndexingType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setSchemaType(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setTokenizerType(int);
+  public static final class AppSearchSchema.StringPropertyConfig.Builder {
+    ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
   }
 
   public interface AppSearchSession extends java.io.Closeable {
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!> getSchema();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeByUri(androidx.appsearch.app.RemoveByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> maybeFlush();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
   }
 
-  public interface DataClassFactory<T> {
+  public interface DocumentClassFactory<T> {
     method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
-    method public String getSchemaType();
+    method public String getSchemaName();
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public class GenericDocument {
     ctor protected GenericDocument(androidx.appsearch.app.GenericDocument);
+    method public static androidx.appsearch.app.GenericDocument fromDocumentClass(Object) throws androidx.appsearch.exceptions.AppSearchException;
     method public long getCreationTimestampMillis();
+    method public String getId();
     method public static int getMaxIndexedProperties();
     method public String getNamespace();
     method public Object? getProperty(String);
@@ -136,16 +195,13 @@
     method public String getSchemaType();
     method public int getScore();
     method public long getTtlMillis();
-    method public String getUri();
-    method public <T> T toDataClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
-    field public static final String DEFAULT_NAMESPACE = "";
+    method public <T> T toDocumentClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
-    ctor public GenericDocument.Builder(String, String);
+    ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
     method public BuilderType setCreationTimestampMillis(long);
-    method public BuilderType setNamespace(String);
     method public BuilderType setPropertyBoolean(String, boolean...);
     method public BuilderType setPropertyBytes(String, byte[]!...);
     method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...);
@@ -156,21 +212,44 @@
     method public BuilderType setTtlMillis(long);
   }
 
-  public final class GetByUriRequest {
+  public final class GetByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
+    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
   }
 
-  public static final class GetByUriRequest.Builder {
-    ctor public GetByUriRequest.Builder();
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.GetByUriRequest build();
-    method public androidx.appsearch.app.GetByUriRequest.Builder setNamespace(String);
+  public static final class GetByDocumentIdRequest.Builder {
+    ctor public GetByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addProjection(String, java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest build();
   }
 
-  public interface GlobalSearchSession {
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
+  public class GetSchemaResponse {
+    method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
+    method @IntRange(from=0) public int getVersion();
+  }
+
+  public static final class GetSchemaResponse.Builder {
+    ctor public GetSchemaResponse.Builder();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
+    method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
+  }
+
+  public interface GlobalSearchSession extends java.io.Closeable {
+    method public void close();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
+  public abstract class Migrator {
+    ctor public Migrator();
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onDowngrade(int, int, androidx.appsearch.app.GenericDocument);
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onUpgrade(int, int, androidx.appsearch.app.GenericDocument);
+    method public abstract boolean shouldMigrate(int, int);
   }
 
   public class PackageIdentifier {
@@ -180,47 +259,92 @@
   }
 
   public final class PutDocumentsRequest {
-    method public java.util.List<androidx.appsearch.app.GenericDocument!> getDocuments();
+    method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
     ctor public PutDocumentsRequest.Builder();
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(androidx.appsearch.app.GenericDocument!...);
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
-  public final class RemoveByUriRequest {
+  public final class RemoveByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
   }
 
-  public static final class RemoveByUriRequest.Builder {
-    ctor public RemoveByUriRequest.Builder();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.RemoveByUriRequest build();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder setNamespace(String);
+  public static final class RemoveByDocumentIdRequest.Builder {
+    ctor public RemoveByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest build();
+  }
+
+  public final class ReportSystemUsageRequest {
+    method public String getDatabaseName();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getPackageName();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportSystemUsageRequest.Builder {
+    ctor public ReportSystemUsageRequest.Builder(String, String, String, String);
+    method public androidx.appsearch.app.ReportSystemUsageRequest build();
+    method public androidx.appsearch.app.ReportSystemUsageRequest.Builder setUsageTimestampMillis(long);
+  }
+
+  public final class ReportUsageRequest {
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportUsageRequest.Builder {
+    ctor public ReportUsageRequest.Builder(String, String);
+    method public androidx.appsearch.app.ReportUsageRequest build();
+    method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
   public final class SearchResult {
-    method public androidx.appsearch.app.GenericDocument getDocument();
+    method public String getDatabaseName();
+    method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.GenericDocument getGenericDocument();
     method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatches();
     method public String getPackageName();
+    method public double getRankingSignal();
+  }
+
+  public static final class SearchResult.Builder {
+    ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addMatch(androidx.appsearch.app.SearchResult.MatchInfo);
+    method public androidx.appsearch.app.SearchResult build();
+    method public androidx.appsearch.app.SearchResult.Builder setDocument(Object) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchResult.Builder setGenericDocument(androidx.appsearch.app.GenericDocument);
+    method public androidx.appsearch.app.SearchResult.Builder setRankingSignal(double);
   }
 
   public static final class SearchResult.MatchInfo {
     method public CharSequence getExactMatch();
-    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchRange();
     method public String getFullText();
     method public String getPropertyPath();
     method public CharSequence getSnippet();
-    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetRange();
+  }
+
+  public static final class SearchResult.MatchInfo.Builder {
+    ctor public SearchResult.MatchInfo.Builder(String);
+    method public androidx.appsearch.app.SearchResult.MatchInfo build();
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setExactMatchRange(androidx.appsearch.app.SearchResult.MatchRange);
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setSnippetRange(androidx.appsearch.app.SearchResult.MatchRange);
   }
 
   public static final class SearchResult.MatchRange {
+    ctor public SearchResult.MatchRange(int, int);
     method public int getEnd();
     method public int getStart();
   }
@@ -231,16 +355,21 @@
   }
 
   public final class SearchSpec {
+    method public java.util.List<java.lang.String!> getFilterNamespaces();
+    method public java.util.List<java.lang.String!> getFilterPackageNames();
+    method public java.util.List<java.lang.String!> getFilterSchemas();
     method public int getMaxSnippetSize();
-    method public java.util.List<java.lang.String!> getNamespaces();
     method public int getOrder();
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
-    method public java.util.List<java.lang.String!> getSchemaTypes();
+    method public int getResultGroupingLimit();
+    method public int getResultGroupingTypeFlags();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
+    field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
     field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
@@ -248,49 +377,102 @@
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
     field public static final int RANKING_STRATEGY_NONE = 0; // 0x0
     field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6; // 0x6
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
+    field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
+    field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
 
   public static final class SearchSpec.Builder {
     ctor public SearchSpec.Builder();
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_SNIPPET_SIZE_LIMIT) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
     method public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_NUM_PER_PAGE) int);
+    method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_SNIPPET_COUNT) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_SNIPPET_PER_PROPERTY_COUNT) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
   }
 
   public final class SetSchemaRequest {
+    method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
-    method public java.util.Set<java.lang.String!> getSchemasNotVisibleToSystemUi();
+    method public java.util.Set<java.lang.String!> getSchemasNotDisplayedBySystem();
     method public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemasVisibleToPackages();
+    method @IntRange(from=1) public int getVersion();
     method public boolean isForceOverride();
   }
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(androidx.appsearch.app.AppSearchSchema!...);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
     method public androidx.appsearch.app.SetSchemaRequest build();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForSystemUi(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForSystemUi(String, boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
+  }
+
+  public class SetSchemaResponse {
+    method public java.util.Set<java.lang.String!> getDeletedTypes();
+    method public java.util.Set<java.lang.String!> getIncompatibleTypes();
+    method public java.util.Set<java.lang.String!> getMigratedTypes();
+    method public java.util.List<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!> getMigrationFailures();
+  }
+
+  public static final class SetSchemaResponse.Builder {
+    ctor public SetSchemaResponse.Builder();
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailure(androidx.appsearch.app.SetSchemaResponse.MigrationFailure);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailures(java.util.Collection<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!>);
+    method public androidx.appsearch.app.SetSchemaResponse build();
+  }
+
+  public static class SetSchemaResponse.MigrationFailure {
+    ctor public SetSchemaResponse.MigrationFailure(String, String, String, androidx.appsearch.app.AppSearchResult<?>);
+    method public androidx.appsearch.app.AppSearchResult<java.lang.Void!> getAppSearchResult();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getSchemaType();
+  }
+
+  public class StorageInfo {
+    method public int getAliveDocumentsCount();
+    method public int getAliveNamespacesCount();
+    method public long getSizeBytes();
+  }
+
+  public static final class StorageInfo.Builder {
+    ctor public StorageInfo.Builder();
+    method public androidx.appsearch.app.StorageInfo build();
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
   }
 
 }
@@ -298,6 +480,9 @@
 package androidx.appsearch.exceptions {
 
   public class AppSearchException extends java.lang.Exception {
+    ctor public AppSearchException(int);
+    ctor public AppSearchException(int, String?);
+    ctor public AppSearchException(int, String?, Throwable?);
     method public int getResultCode();
     method public <T> androidx.appsearch.app.AppSearchResult<T!> toAppSearchResult();
   }
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index 3a51601..5fd927b 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -1,30 +1,30 @@
 // Signature format: 4.0
 package androidx.appsearch.annotation {
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface AppSearchDocument {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface Document {
     method public abstract String name() default "";
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.CreationTimestampMillis {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.CreationTimestampMillis {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Namespace {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Id {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Property {
-    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Namespace {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Property {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
     method public abstract String name() default "";
     method public abstract boolean required() default false;
-    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Score {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Score {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.TtlMillis {
-  }
-
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Uri {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.TtlMillis {
   }
 
 }
@@ -32,16 +32,27 @@
 package androidx.appsearch.app {
 
   public final class AppSearchBatchResult<KeyType, ValueType> {
+    method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getAll();
     method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getFailures();
     method public java.util.Map<KeyType!,ValueType!> getSuccesses();
     method public boolean isSuccess();
   }
 
+  public static final class AppSearchBatchResult.Builder<KeyType, ValueType> {
+    ctor public AppSearchBatchResult.Builder();
+    method public androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!> build();
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setFailure(KeyType, int, String?);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setResult(KeyType, androidx.appsearch.app.AppSearchResult<ValueType!>);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setSuccess(KeyType, ValueType?);
+  }
+
   public final class AppSearchResult<ValueType> {
     method public String? getErrorMessage();
     method public int getResultCode();
     method public ValueType? getResultValue();
     method public boolean isSuccess();
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newFailedResult(int, String?);
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newSuccessfulResult(ValueType?);
     field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
     field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
     field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -49,6 +60,7 @@
     field public static final int RESULT_NOT_FOUND = 6; // 0x6
     field public static final int RESULT_OK = 0; // 0x0
     field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+    field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
@@ -57,28 +69,71 @@
     method public String getSchemaType();
   }
 
+  public static final class AppSearchSchema.BooleanPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BooleanPropertyConfig.Builder {
+    ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+  }
+
   public static final class AppSearchSchema.Builder {
     ctor public AppSearchSchema.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
   }
 
-  public static final class AppSearchSchema.PropertyConfig {
+  public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BytesPropertyConfig.Builder {
+    ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public String getSchemaType();
+    method public boolean shouldIndexNestedProperties();
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig.Builder {
+    ctor public AppSearchSchema.DocumentPropertyConfig.Builder(String, String);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig.Builder {
+    ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.Int64PropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.Int64PropertyConfig.Builder {
+    ctor public AppSearchSchema.Int64PropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.Int64PropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.Int64PropertyConfig.Builder setCardinality(int);
+  }
+
+  public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
-    method public int getDataType();
-    method public int getIndexingType();
     method public String getName();
-    method public String? getSchemaType();
-    method public int getTokenizerType();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
     field public static final int CARDINALITY_REQUIRED = 3; // 0x3
-    field public static final int DATA_TYPE_BOOLEAN = 4; // 0x4
-    field public static final int DATA_TYPE_BYTES = 5; // 0x5
-    field public static final int DATA_TYPE_DOCUMENT = 6; // 0x6
-    field public static final int DATA_TYPE_DOUBLE = 3; // 0x3
-    field public static final int DATA_TYPE_INT64 = 2; // 0x2
-    field public static final int DATA_TYPE_STRING = 1; // 0x1
+  }
+
+  public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public int getIndexingType();
+    method public int getTokenizerType();
     field public static final int INDEXING_TYPE_EXACT_TERMS = 1; // 0x1
     field public static final int INDEXING_TYPE_NONE = 0; // 0x0
     field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
@@ -86,37 +141,41 @@
     field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
   }
 
-  public static final class AppSearchSchema.PropertyConfig.Builder {
-    ctor public AppSearchSchema.PropertyConfig.Builder(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig build();
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setCardinality(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setDataType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setIndexingType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setSchemaType(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setTokenizerType(int);
+  public static final class AppSearchSchema.StringPropertyConfig.Builder {
+    ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
   }
 
   public interface AppSearchSession extends java.io.Closeable {
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!> getSchema();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeByUri(androidx.appsearch.app.RemoveByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> maybeFlush();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
   }
 
-  public interface DataClassFactory<T> {
+  public interface DocumentClassFactory<T> {
     method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
-    method public String getSchemaType();
+    method public String getSchemaName();
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public class GenericDocument {
     ctor protected GenericDocument(androidx.appsearch.app.GenericDocument);
+    method public static androidx.appsearch.app.GenericDocument fromDocumentClass(Object) throws androidx.appsearch.exceptions.AppSearchException;
     method public long getCreationTimestampMillis();
+    method public String getId();
     method public static int getMaxIndexedProperties();
     method public String getNamespace();
     method public Object? getProperty(String);
@@ -136,16 +195,13 @@
     method public String getSchemaType();
     method public int getScore();
     method public long getTtlMillis();
-    method public String getUri();
-    method public <T> T toDataClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
-    field public static final String DEFAULT_NAMESPACE = "";
+    method public <T> T toDocumentClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
-    ctor public GenericDocument.Builder(String, String);
+    ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
     method public BuilderType setCreationTimestampMillis(long);
-    method public BuilderType setNamespace(String);
     method public BuilderType setPropertyBoolean(String, boolean...);
     method public BuilderType setPropertyBytes(String, byte[]!...);
     method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...);
@@ -156,21 +212,44 @@
     method public BuilderType setTtlMillis(long);
   }
 
-  public final class GetByUriRequest {
+  public final class GetByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
+    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
   }
 
-  public static final class GetByUriRequest.Builder {
-    ctor public GetByUriRequest.Builder();
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.GetByUriRequest build();
-    method public androidx.appsearch.app.GetByUriRequest.Builder setNamespace(String);
+  public static final class GetByDocumentIdRequest.Builder {
+    ctor public GetByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addProjection(String, java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest build();
   }
 
-  public interface GlobalSearchSession {
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
+  public class GetSchemaResponse {
+    method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
+    method @IntRange(from=0) public int getVersion();
+  }
+
+  public static final class GetSchemaResponse.Builder {
+    ctor public GetSchemaResponse.Builder();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
+    method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
+  }
+
+  public interface GlobalSearchSession extends java.io.Closeable {
+    method public void close();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
+  public abstract class Migrator {
+    ctor public Migrator();
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onDowngrade(int, int, androidx.appsearch.app.GenericDocument);
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onUpgrade(int, int, androidx.appsearch.app.GenericDocument);
+    method public abstract boolean shouldMigrate(int, int);
   }
 
   public class PackageIdentifier {
@@ -180,47 +259,92 @@
   }
 
   public final class PutDocumentsRequest {
-    method public java.util.List<androidx.appsearch.app.GenericDocument!> getDocuments();
+    method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
     ctor public PutDocumentsRequest.Builder();
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(androidx.appsearch.app.GenericDocument!...);
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
-  public final class RemoveByUriRequest {
+  public final class RemoveByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
   }
 
-  public static final class RemoveByUriRequest.Builder {
-    ctor public RemoveByUriRequest.Builder();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.RemoveByUriRequest build();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder setNamespace(String);
+  public static final class RemoveByDocumentIdRequest.Builder {
+    ctor public RemoveByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest build();
+  }
+
+  public final class ReportSystemUsageRequest {
+    method public String getDatabaseName();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getPackageName();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportSystemUsageRequest.Builder {
+    ctor public ReportSystemUsageRequest.Builder(String, String, String, String);
+    method public androidx.appsearch.app.ReportSystemUsageRequest build();
+    method public androidx.appsearch.app.ReportSystemUsageRequest.Builder setUsageTimestampMillis(long);
+  }
+
+  public final class ReportUsageRequest {
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportUsageRequest.Builder {
+    ctor public ReportUsageRequest.Builder(String, String);
+    method public androidx.appsearch.app.ReportUsageRequest build();
+    method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
   public final class SearchResult {
-    method public androidx.appsearch.app.GenericDocument getDocument();
+    method public String getDatabaseName();
+    method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.GenericDocument getGenericDocument();
     method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatches();
     method public String getPackageName();
+    method public double getRankingSignal();
+  }
+
+  public static final class SearchResult.Builder {
+    ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addMatch(androidx.appsearch.app.SearchResult.MatchInfo);
+    method public androidx.appsearch.app.SearchResult build();
+    method public androidx.appsearch.app.SearchResult.Builder setDocument(Object) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchResult.Builder setGenericDocument(androidx.appsearch.app.GenericDocument);
+    method public androidx.appsearch.app.SearchResult.Builder setRankingSignal(double);
   }
 
   public static final class SearchResult.MatchInfo {
     method public CharSequence getExactMatch();
-    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchRange();
     method public String getFullText();
     method public String getPropertyPath();
     method public CharSequence getSnippet();
-    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetRange();
+  }
+
+  public static final class SearchResult.MatchInfo.Builder {
+    ctor public SearchResult.MatchInfo.Builder(String);
+    method public androidx.appsearch.app.SearchResult.MatchInfo build();
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setExactMatchRange(androidx.appsearch.app.SearchResult.MatchRange);
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setSnippetRange(androidx.appsearch.app.SearchResult.MatchRange);
   }
 
   public static final class SearchResult.MatchRange {
+    ctor public SearchResult.MatchRange(int, int);
     method public int getEnd();
     method public int getStart();
   }
@@ -231,16 +355,21 @@
   }
 
   public final class SearchSpec {
+    method public java.util.List<java.lang.String!> getFilterNamespaces();
+    method public java.util.List<java.lang.String!> getFilterPackageNames();
+    method public java.util.List<java.lang.String!> getFilterSchemas();
     method public int getMaxSnippetSize();
-    method public java.util.List<java.lang.String!> getNamespaces();
     method public int getOrder();
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
-    method public java.util.List<java.lang.String!> getSchemaTypes();
+    method public int getResultGroupingLimit();
+    method public int getResultGroupingTypeFlags();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
+    field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
     field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
@@ -248,49 +377,102 @@
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
     field public static final int RANKING_STRATEGY_NONE = 0; // 0x0
     field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6; // 0x6
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
+    field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
+    field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
 
   public static final class SearchSpec.Builder {
     ctor public SearchSpec.Builder();
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_SNIPPET_SIZE_LIMIT) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
     method public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_NUM_PER_PAGE) int);
+    method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_SNIPPET_COUNT) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_SNIPPET_PER_PROPERTY_COUNT) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
   }
 
   public final class SetSchemaRequest {
+    method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
-    method public java.util.Set<java.lang.String!> getSchemasNotVisibleToSystemUi();
+    method public java.util.Set<java.lang.String!> getSchemasNotDisplayedBySystem();
     method public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemasVisibleToPackages();
+    method @IntRange(from=1) public int getVersion();
     method public boolean isForceOverride();
   }
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(androidx.appsearch.app.AppSearchSchema!...);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
     method public androidx.appsearch.app.SetSchemaRequest build();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForSystemUi(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForSystemUi(String, boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
+  }
+
+  public class SetSchemaResponse {
+    method public java.util.Set<java.lang.String!> getDeletedTypes();
+    method public java.util.Set<java.lang.String!> getIncompatibleTypes();
+    method public java.util.Set<java.lang.String!> getMigratedTypes();
+    method public java.util.List<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!> getMigrationFailures();
+  }
+
+  public static final class SetSchemaResponse.Builder {
+    ctor public SetSchemaResponse.Builder();
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailure(androidx.appsearch.app.SetSchemaResponse.MigrationFailure);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailures(java.util.Collection<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!>);
+    method public androidx.appsearch.app.SetSchemaResponse build();
+  }
+
+  public static class SetSchemaResponse.MigrationFailure {
+    ctor public SetSchemaResponse.MigrationFailure(String, String, String, androidx.appsearch.app.AppSearchResult<?>);
+    method public androidx.appsearch.app.AppSearchResult<java.lang.Void!> getAppSearchResult();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getSchemaType();
+  }
+
+  public class StorageInfo {
+    method public int getAliveDocumentsCount();
+    method public int getAliveNamespacesCount();
+    method public long getSizeBytes();
+  }
+
+  public static final class StorageInfo.Builder {
+    ctor public StorageInfo.Builder();
+    method public androidx.appsearch.app.StorageInfo build();
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
   }
 
 }
@@ -298,6 +480,9 @@
 package androidx.appsearch.exceptions {
 
   public class AppSearchException extends java.lang.Exception {
+    ctor public AppSearchException(int);
+    ctor public AppSearchException(int, String?);
+    ctor public AppSearchException(int, String?, Throwable?);
     method public int getResultCode();
     method public <T> androidx.appsearch.app.AppSearchResult<T!> toAppSearchResult();
   }
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 3a51601..5fd927b 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -1,30 +1,30 @@
 // Signature format: 4.0
 package androidx.appsearch.annotation {
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface AppSearchDocument {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface Document {
     method public abstract String name() default "";
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.CreationTimestampMillis {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.CreationTimestampMillis {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Namespace {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Id {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Property {
-    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Namespace {
+  }
+
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Property {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
     method public abstract String name() default "";
     method public abstract boolean required() default false;
-    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+    method public abstract int tokenizerType() default androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Score {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.Score {
   }
 
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.TtlMillis {
-  }
-
-  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface AppSearchDocument.Uri {
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target(java.lang.annotation.ElementType.FIELD) public static @interface Document.TtlMillis {
   }
 
 }
@@ -32,16 +32,27 @@
 package androidx.appsearch.app {
 
   public final class AppSearchBatchResult<KeyType, ValueType> {
+    method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getAll();
     method public java.util.Map<KeyType!,androidx.appsearch.app.AppSearchResult<ValueType!>!> getFailures();
     method public java.util.Map<KeyType!,ValueType!> getSuccesses();
     method public boolean isSuccess();
   }
 
+  public static final class AppSearchBatchResult.Builder<KeyType, ValueType> {
+    ctor public AppSearchBatchResult.Builder();
+    method public androidx.appsearch.app.AppSearchBatchResult<KeyType!,ValueType!> build();
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setFailure(KeyType, int, String?);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setResult(KeyType, androidx.appsearch.app.AppSearchResult<ValueType!>);
+    method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setSuccess(KeyType, ValueType?);
+  }
+
   public final class AppSearchResult<ValueType> {
     method public String? getErrorMessage();
     method public int getResultCode();
     method public ValueType? getResultValue();
     method public boolean isSuccess();
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newFailedResult(int, String?);
+    method public static <ValueType> androidx.appsearch.app.AppSearchResult<ValueType!> newSuccessfulResult(ValueType?);
     field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
     field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
     field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -49,6 +60,7 @@
     field public static final int RESULT_NOT_FOUND = 6; // 0x6
     field public static final int RESULT_OK = 0; // 0x0
     field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+    field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
@@ -57,28 +69,71 @@
     method public String getSchemaType();
   }
 
+  public static final class AppSearchSchema.BooleanPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BooleanPropertyConfig.Builder {
+    ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+  }
+
   public static final class AppSearchSchema.Builder {
     ctor public AppSearchSchema.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
   }
 
-  public static final class AppSearchSchema.PropertyConfig {
+  public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.BytesPropertyConfig.Builder {
+    ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public String getSchemaType();
+    method public boolean shouldIndexNestedProperties();
+  }
+
+  public static final class AppSearchSchema.DocumentPropertyConfig.Builder {
+    ctor public AppSearchSchema.DocumentPropertyConfig.Builder(String, String);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.DoublePropertyConfig.Builder {
+    ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+  }
+
+  public static final class AppSearchSchema.Int64PropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+  }
+
+  public static final class AppSearchSchema.Int64PropertyConfig.Builder {
+    ctor public AppSearchSchema.Int64PropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.Int64PropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.Int64PropertyConfig.Builder setCardinality(int);
+  }
+
+  public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
-    method public int getDataType();
-    method public int getIndexingType();
     method public String getName();
-    method public String? getSchemaType();
-    method public int getTokenizerType();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
     field public static final int CARDINALITY_REQUIRED = 3; // 0x3
-    field public static final int DATA_TYPE_BOOLEAN = 4; // 0x4
-    field public static final int DATA_TYPE_BYTES = 5; // 0x5
-    field public static final int DATA_TYPE_DOCUMENT = 6; // 0x6
-    field public static final int DATA_TYPE_DOUBLE = 3; // 0x3
-    field public static final int DATA_TYPE_INT64 = 2; // 0x2
-    field public static final int DATA_TYPE_STRING = 1; // 0x1
+  }
+
+  public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
+    method public int getIndexingType();
+    method public int getTokenizerType();
     field public static final int INDEXING_TYPE_EXACT_TERMS = 1; // 0x1
     field public static final int INDEXING_TYPE_NONE = 0; // 0x0
     field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
@@ -86,37 +141,41 @@
     field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
   }
 
-  public static final class AppSearchSchema.PropertyConfig.Builder {
-    ctor public AppSearchSchema.PropertyConfig.Builder(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig build();
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setCardinality(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setDataType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setIndexingType(int);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setSchemaType(String);
-    method public androidx.appsearch.app.AppSearchSchema.PropertyConfig.Builder setTokenizerType(int);
+  public static final class AppSearchSchema.StringPropertyConfig.Builder {
+    ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
+    method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
   }
 
   public interface AppSearchSession extends java.io.Closeable {
     method public void close();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!> getSchema();
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> removeByUri(androidx.appsearch.app.RemoveByUriRequest);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setSchema(androidx.appsearch.app.SetSchemaRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> maybeFlush();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> put(androidx.appsearch.app.PutDocumentsRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> remove(androidx.appsearch.app.RemoveByDocumentIdRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> remove(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportUsage(androidx.appsearch.app.ReportUsageRequest);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
   }
 
-  public interface DataClassFactory<T> {
+  public interface DocumentClassFactory<T> {
     method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
-    method public String getSchemaType();
+    method public String getSchemaName();
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public class GenericDocument {
     ctor protected GenericDocument(androidx.appsearch.app.GenericDocument);
+    method public static androidx.appsearch.app.GenericDocument fromDocumentClass(Object) throws androidx.appsearch.exceptions.AppSearchException;
     method public long getCreationTimestampMillis();
+    method public String getId();
     method public static int getMaxIndexedProperties();
     method public String getNamespace();
     method public Object? getProperty(String);
@@ -136,16 +195,13 @@
     method public String getSchemaType();
     method public int getScore();
     method public long getTtlMillis();
-    method public String getUri();
-    method public <T> T toDataClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
-    field public static final String DEFAULT_NAMESPACE = "";
+    method public <T> T toDocumentClass(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
   public static class GenericDocument.Builder<BuilderType extends androidx.appsearch.app.GenericDocument.Builder> {
-    ctor public GenericDocument.Builder(String, String);
+    ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
     method public BuilderType setCreationTimestampMillis(long);
-    method public BuilderType setNamespace(String);
     method public BuilderType setPropertyBoolean(String, boolean...);
     method public BuilderType setPropertyBytes(String, byte[]!...);
     method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...);
@@ -156,21 +212,44 @@
     method public BuilderType setTtlMillis(long);
   }
 
-  public final class GetByUriRequest {
+  public final class GetByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
+    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
   }
 
-  public static final class GetByUriRequest.Builder {
-    ctor public GetByUriRequest.Builder();
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.GetByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.GetByUriRequest build();
-    method public androidx.appsearch.app.GetByUriRequest.Builder setNamespace(String);
+  public static final class GetByDocumentIdRequest.Builder {
+    ctor public GetByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest.Builder addProjection(String, java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.GetByDocumentIdRequest build();
   }
 
-  public interface GlobalSearchSession {
-    method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
+  public class GetSchemaResponse {
+    method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
+    method @IntRange(from=0) public int getVersion();
+  }
+
+  public static final class GetSchemaResponse.Builder {
+    ctor public GetSchemaResponse.Builder();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
+    method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
+  }
+
+  public interface GlobalSearchSession extends java.io.Closeable {
+    method public void close();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
+  public abstract class Migrator {
+    ctor public Migrator();
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onDowngrade(int, int, androidx.appsearch.app.GenericDocument);
+    method @WorkerThread public abstract androidx.appsearch.app.GenericDocument onUpgrade(int, int, androidx.appsearch.app.GenericDocument);
+    method public abstract boolean shouldMigrate(int, int);
   }
 
   public class PackageIdentifier {
@@ -180,47 +259,92 @@
   }
 
   public final class PutDocumentsRequest {
-    method public java.util.List<androidx.appsearch.app.GenericDocument!> getDocuments();
+    method public java.util.List<androidx.appsearch.app.GenericDocument!> getGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
     ctor public PutDocumentsRequest.Builder();
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDataClass(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(androidx.appsearch.app.GenericDocument!...);
-    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocument(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.lang.Object!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection<?>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection<? extends androidx.appsearch.app.GenericDocument>);
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
-  public final class RemoveByUriRequest {
+  public final class RemoveByDocumentIdRequest {
+    method public java.util.Set<java.lang.String!> getIds();
     method public String getNamespace();
-    method public java.util.Set<java.lang.String!> getUris();
   }
 
-  public static final class RemoveByUriRequest.Builder {
-    ctor public RemoveByUriRequest.Builder();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.lang.String!...);
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder addUri(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.RemoveByUriRequest build();
-    method public androidx.appsearch.app.RemoveByUriRequest.Builder setNamespace(String);
+  public static final class RemoveByDocumentIdRequest.Builder {
+    ctor public RemoveByDocumentIdRequest.Builder(String);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.lang.String!...);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest.Builder addIds(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.RemoveByDocumentIdRequest build();
+  }
+
+  public final class ReportSystemUsageRequest {
+    method public String getDatabaseName();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getPackageName();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportSystemUsageRequest.Builder {
+    ctor public ReportSystemUsageRequest.Builder(String, String, String, String);
+    method public androidx.appsearch.app.ReportSystemUsageRequest build();
+    method public androidx.appsearch.app.ReportSystemUsageRequest.Builder setUsageTimestampMillis(long);
+  }
+
+  public final class ReportUsageRequest {
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public long getUsageTimestampMillis();
+  }
+
+  public static final class ReportUsageRequest.Builder {
+    ctor public ReportUsageRequest.Builder(String, String);
+    method public androidx.appsearch.app.ReportUsageRequest build();
+    method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
   public final class SearchResult {
-    method public androidx.appsearch.app.GenericDocument getDocument();
+    method public String getDatabaseName();
+    method public <T> T getDocument(Class<T!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.GenericDocument getGenericDocument();
     method public java.util.List<androidx.appsearch.app.SearchResult.MatchInfo!> getMatches();
     method public String getPackageName();
+    method public double getRankingSignal();
+  }
+
+  public static final class SearchResult.Builder {
+    ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addMatch(androidx.appsearch.app.SearchResult.MatchInfo);
+    method public androidx.appsearch.app.SearchResult build();
+    method public androidx.appsearch.app.SearchResult.Builder setDocument(Object) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchResult.Builder setGenericDocument(androidx.appsearch.app.GenericDocument);
+    method public androidx.appsearch.app.SearchResult.Builder setRankingSignal(double);
   }
 
   public static final class SearchResult.MatchInfo {
     method public CharSequence getExactMatch();
-    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getExactMatchRange();
     method public String getFullText();
     method public String getPropertyPath();
     method public CharSequence getSnippet();
-    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetPosition();
+    method public androidx.appsearch.app.SearchResult.MatchRange getSnippetRange();
+  }
+
+  public static final class SearchResult.MatchInfo.Builder {
+    ctor public SearchResult.MatchInfo.Builder(String);
+    method public androidx.appsearch.app.SearchResult.MatchInfo build();
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setExactMatchRange(androidx.appsearch.app.SearchResult.MatchRange);
+    method public androidx.appsearch.app.SearchResult.MatchInfo.Builder setSnippetRange(androidx.appsearch.app.SearchResult.MatchRange);
   }
 
   public static final class SearchResult.MatchRange {
+    ctor public SearchResult.MatchRange(int, int);
     method public int getEnd();
     method public int getStart();
   }
@@ -231,16 +355,21 @@
   }
 
   public final class SearchSpec {
+    method public java.util.List<java.lang.String!> getFilterNamespaces();
+    method public java.util.List<java.lang.String!> getFilterPackageNames();
+    method public java.util.List<java.lang.String!> getFilterSchemas();
     method public int getMaxSnippetSize();
-    method public java.util.List<java.lang.String!> getNamespaces();
     method public int getOrder();
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
-    method public java.util.List<java.lang.String!> getSchemaTypes();
+    method public int getResultGroupingLimit();
+    method public int getResultGroupingTypeFlags();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
+    field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
     field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
@@ -248,49 +377,102 @@
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
     field public static final int RANKING_STRATEGY_NONE = 0; // 0x0
     field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6; // 0x6
+    field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
+    field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
+    field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
 
   public static final class SearchSpec.Builder {
     ctor public SearchSpec.Builder();
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
+    method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.lang.String!...);
-    method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_SNIPPET_SIZE_LIMIT) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
     method public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_NUM_PER_PAGE) int);
+    method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_SNIPPET_COUNT) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=androidx.appsearch.app.SearchSpec.MAX_SNIPPET_PER_PROPERTY_COUNT) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
   }
 
   public final class SetSchemaRequest {
+    method public java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!> getMigrators();
     method public java.util.Set<androidx.appsearch.app.AppSearchSchema!> getSchemas();
-    method public java.util.Set<java.lang.String!> getSchemasNotVisibleToSystemUi();
+    method public java.util.Set<java.lang.String!> getSchemasNotDisplayedBySystem();
     method public java.util.Map<java.lang.String!,java.util.Set<androidx.appsearch.app.PackageIdentifier!>!> getSchemasVisibleToPackages();
+    method @IntRange(from=1) public int getVersion();
     method public boolean isForceOverride();
   }
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addDataClass(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(androidx.appsearch.app.AppSearchSchema!...);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchema(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection<? extends java.lang.Class<?>>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection<androidx.appsearch.app.AppSearchSchema!>);
     method public androidx.appsearch.app.SetSchemaRequest build();
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setDataClassVisibilityForSystemUi(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class<?>, boolean) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class<?>, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map<java.lang.String!,androidx.appsearch.app.Migrator!>);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
-    method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForSystemUi(String, boolean);
+    method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
+  }
+
+  public class SetSchemaResponse {
+    method public java.util.Set<java.lang.String!> getDeletedTypes();
+    method public java.util.Set<java.lang.String!> getIncompatibleTypes();
+    method public java.util.Set<java.lang.String!> getMigratedTypes();
+    method public java.util.List<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!> getMigrationFailures();
+  }
+
+  public static final class SetSchemaResponse.Builder {
+    ctor public SetSchemaResponse.Builder();
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addDeletedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addIncompatibleTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedType(String);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigratedTypes(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailure(androidx.appsearch.app.SetSchemaResponse.MigrationFailure);
+    method public androidx.appsearch.app.SetSchemaResponse.Builder addMigrationFailures(java.util.Collection<androidx.appsearch.app.SetSchemaResponse.MigrationFailure!>);
+    method public androidx.appsearch.app.SetSchemaResponse build();
+  }
+
+  public static class SetSchemaResponse.MigrationFailure {
+    ctor public SetSchemaResponse.MigrationFailure(String, String, String, androidx.appsearch.app.AppSearchResult<?>);
+    method public androidx.appsearch.app.AppSearchResult<java.lang.Void!> getAppSearchResult();
+    method public String getDocumentId();
+    method public String getNamespace();
+    method public String getSchemaType();
+  }
+
+  public class StorageInfo {
+    method public int getAliveDocumentsCount();
+    method public int getAliveNamespacesCount();
+    method public long getSizeBytes();
+  }
+
+  public static final class StorageInfo.Builder {
+    ctor public StorageInfo.Builder();
+    method public androidx.appsearch.app.StorageInfo build();
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveDocumentsCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setAliveNamespacesCount(int);
+    method public androidx.appsearch.app.StorageInfo.Builder setSizeBytes(long);
   }
 
 }
@@ -298,6 +480,9 @@
 package androidx.appsearch.exceptions {
 
   public class AppSearchException extends java.lang.Exception {
+    ctor public AppSearchException(int);
+    ctor public AppSearchException(int, String?);
+    ctor public AppSearchException(int, String?, Throwable?);
     method public int getResultCode();
     method public <T> androidx.appsearch.app.AppSearchResult<T!> toAppSearchResult();
   }
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 085f8ad..af7515d 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -30,16 +30,21 @@
         sourceCompatibility = JavaVersion.VERSION_1_8
         targetCompatibility = JavaVersion.VERSION_1_8
     }
+    buildTypes.all {
+        consumerProguardFiles "proguard-rules.pro"
+    }
 }
 
 dependencies {
     api('androidx.annotation:annotation:1.1.0')
+    api(JSR250)
 
     implementation('androidx.concurrent:concurrent-futures:1.0.0')
     implementation('androidx.core:core:1.2.0')
 
     androidTestAnnotationProcessor project(':appsearch:appsearch-compiler')
     androidTestImplementation project(':appsearch:appsearch-local-storage')
+    androidTestImplementation project(':appsearch:appsearch-platform-storage')
     androidTestImplementation(ANDROIDX_TEST_CORE)
     androidTestImplementation(ANDROIDX_TEST_RULES)
     androidTestImplementation(TRUTH)
diff --git a/appsearch/appsearch/proguard-rules.pro b/appsearch/appsearch/proguard-rules.pro
new file mode 100644
index 0000000..a7af3cc
--- /dev/null
+++ b/appsearch/appsearch/proguard-rules.pro
@@ -0,0 +1,16 @@
+#  Copyright (C) 2021 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.
+-keep class ** implements androidx.appsearch.app.DocumentClassFactory { *; }
+
+-keep @androidx.appsearch.annotation.Document class *
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
index 808d90e..b22f8d8 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorLocalTest.java
@@ -29,6 +29,6 @@
     protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
         return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build());
+                new LocalStorage.SearchContext.Builder(context, dbName).build());
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java
new file mode 100644
index 0000000..5dbb8a2
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorPlatformTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class AnnotationProcessorPlatformTest extends AnnotationProcessorTestBase {
+    @Override
+    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+        Context context = ApplicationProvider.getApplicationContext();
+        return PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(context, dbName).build());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
index 58b80b0..699fe17 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -16,16 +16,15 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
 import static androidx.appsearch.app.util.AppSearchTestUtils.checkIsBatchResultSuccess;
 import static androidx.appsearch.app.util.AppSearchTestUtils.convertSearchResultsToDocuments;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.annotation.NonNull;
-import androidx.appsearch.annotation.AppSearchDocument;
-import androidx.appsearch.localstorage.LocalStorage;
+import androidx.appsearch.annotation.Document;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -39,7 +38,7 @@
 
 public abstract class AnnotationProcessorTestBase {
     private AppSearchSession mSession;
-    private static final String DB_NAME_1 = LocalStorage.DEFAULT_DATABASE_NAME;
+    private static final String DB_NAME_1 = "";
 
     protected abstract ListenableFuture<AppSearchSession> createSearchSession(
             @NonNull String dbName);
@@ -63,11 +62,18 @@
         mSession.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
     }
 
-    @AppSearchDocument
+    @Document
     static class Card {
-        @AppSearchDocument.Uri
-        String mUri;
-        @AppSearchDocument.Property
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.CreationTimestampMillis
+        long mCreationTimestampMillis;
+
+        @Document.Property
                 (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
         String mString;        // 3a
 
@@ -80,90 +86,96 @@
                 return false;
             }
             Card otherCard = (Card) other;
-            assertThat(otherCard.mUri).isEqualTo(this.mUri);
+            assertThat(otherCard.mId).isEqualTo(this.mId);
             return true;
         }
     }
 
-    @AppSearchDocument
+    @Document
     static class Gift {
-        @AppSearchDocument.Uri
-        String mUri;
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.CreationTimestampMillis
+        long mCreationTimestampMillis;
 
         // Collections
-        @AppSearchDocument.Property
+        @Document.Property
         Collection<Long> mCollectLong;         // 1a
-        @AppSearchDocument.Property
+        @Document.Property
         Collection<Integer> mCollectInteger;   // 1a
-        @AppSearchDocument.Property
+        @Document.Property
         Collection<Double> mCollectDouble;     // 1a
-        @AppSearchDocument.Property
+        @Document.Property
         Collection<Float> mCollectFloat;       // 1a
-        @AppSearchDocument.Property
+        @Document.Property
         Collection<Boolean> mCollectBoolean;   // 1a
-        @AppSearchDocument.Property
+        @Document.Property
         Collection<byte[]> mCollectByteArr;    // 1a
-        @AppSearchDocument.Property
+        @Document.Property
         Collection<String> mCollectString;     // 1b
-        @AppSearchDocument.Property
+        @Document.Property
         Collection<Card> mCollectCard;         // 1c
 
         // Arrays
-        @AppSearchDocument.Property
+        @Document.Property
         Long[] mArrBoxLong;         // 2a
-        @AppSearchDocument.Property
+        @Document.Property
         long[] mArrUnboxLong;       // 2b
-        @AppSearchDocument.Property
+        @Document.Property
         Integer[] mArrBoxInteger;   // 2a
-        @AppSearchDocument.Property
+        @Document.Property
         int[] mArrUnboxInt;         // 2a
-        @AppSearchDocument.Property
+        @Document.Property
         Double[] mArrBoxDouble;     // 2a
-        @AppSearchDocument.Property
+        @Document.Property
         double[] mArrUnboxDouble;   // 2b
-        @AppSearchDocument.Property
+        @Document.Property
         Float[] mArrBoxFloat;       // 2a
-        @AppSearchDocument.Property
+        @Document.Property
         float[] mArrUnboxFloat;     // 2a
-        @AppSearchDocument.Property
+        @Document.Property
         Boolean[] mArrBoxBoolean;   // 2a
-        @AppSearchDocument.Property
+        @Document.Property
         boolean[] mArrUnboxBoolean; // 2b
-        @AppSearchDocument.Property
+        @Document.Property
         byte[][] mArrUnboxByteArr;  // 2b
-        @AppSearchDocument.Property
+        @Document.Property
         Byte[] mBoxByteArr;         // 2a
-        @AppSearchDocument.Property
+        @Document.Property
         String[] mArrString;        // 2b
-        @AppSearchDocument.Property
+        @Document.Property
         Card[] mArrCard;            // 2c
 
         // Single values
-        @AppSearchDocument.Property
+        @Document.Property
         String mString;        // 3a
-        @AppSearchDocument.Property
+        @Document.Property
         Long mBoxLong;         // 3a
-        @AppSearchDocument.Property
+        @Document.Property
         long mUnboxLong;       // 3b
-        @AppSearchDocument.Property
+        @Document.Property
         Integer mBoxInteger;   // 3a
-        @AppSearchDocument.Property
+        @Document.Property
         int mUnboxInt;         // 3b
-        @AppSearchDocument.Property
+        @Document.Property
         Double mBoxDouble;     // 3a
-        @AppSearchDocument.Property
+        @Document.Property
         double mUnboxDouble;   // 3b
-        @AppSearchDocument.Property
+        @Document.Property
         Float mBoxFloat;       // 3a
-        @AppSearchDocument.Property
+        @Document.Property
         float mUnboxFloat;     // 3b
-        @AppSearchDocument.Property
+        @Document.Property
         Boolean mBoxBoolean;   // 3a
-        @AppSearchDocument.Property
+        @Document.Property
         boolean mUnboxBoolean; // 3b
-        @AppSearchDocument.Property
+        @Document.Property
         byte[] mUnboxByteArr;  // 3a
-        @AppSearchDocument.Property
+        @Document.Property
         Card mCard;            // 3c
 
         @Override
@@ -175,7 +187,8 @@
                 return false;
             }
             Gift otherGift = (Gift) other;
-            assertThat(otherGift.mUri).isEqualTo(this.mUri);
+            assertThat(otherGift.mNamespace).isEqualTo(this.mNamespace);
+            assertThat(otherGift.mId).isEqualTo(this.mId);
             assertThat(otherGift.mArrBoxBoolean).isEqualTo(this.mArrBoxBoolean);
             assertThat(otherGift.mArrBoxDouble).isEqualTo(this.mArrBoxDouble);
             assertThat(otherGift.mArrBoxFloat).isEqualTo(this.mArrBoxFloat);
@@ -224,132 +237,152 @@
             assertThat(second).isNotNull();
             assertThat(first.toArray()).isEqualTo(second.toArray());
         }
+
+        public static Gift createPopulatedGift() {
+            Gift gift = new Gift();
+            gift.mNamespace = "gift.namespace";
+            gift.mId = "gift.id";
+
+            gift.mArrBoxBoolean = new Boolean[]{true, false};
+            gift.mArrBoxDouble = new Double[]{0.0, 1.0};
+            gift.mArrBoxFloat = new Float[]{2.0F, 3.0F};
+            gift.mArrBoxInteger = new Integer[]{4, 5};
+            gift.mArrBoxLong = new Long[]{6L, 7L};
+            gift.mArrString = new String[]{"cat", "dog"};
+            gift.mBoxByteArr = new Byte[]{8, 9};
+            gift.mArrUnboxBoolean = new boolean[]{false, true};
+            gift.mArrUnboxByteArr = new byte[][]{{0, 1}, {2, 3}};
+            gift.mArrUnboxDouble = new double[]{1.0, 0.0};
+            gift.mArrUnboxFloat = new float[]{3.0f, 2.0f};
+            gift.mArrUnboxInt = new int[]{5, 4};
+            gift.mArrUnboxLong = new long[]{7, 6};
+
+            Card card1 = new Card();
+            card1.mNamespace = "card.namespace";
+            card1.mId = "card.id1";
+            Card card2 = new Card();
+            card2.mNamespace = "card.namespace";
+            card2.mId = "card.id2";
+            gift.mArrCard = new Card[]{card2, card2};
+
+            gift.mCollectLong = Arrays.asList(gift.mArrBoxLong);
+            gift.mCollectInteger = Arrays.asList(gift.mArrBoxInteger);
+            gift.mCollectBoolean = Arrays.asList(gift.mArrBoxBoolean);
+            gift.mCollectString = Arrays.asList(gift.mArrString);
+            gift.mCollectDouble = Arrays.asList(gift.mArrBoxDouble);
+            gift.mCollectFloat = Arrays.asList(gift.mArrBoxFloat);
+            gift.mCollectByteArr = Arrays.asList(gift.mArrUnboxByteArr);
+            gift.mCollectCard = Arrays.asList(card2, card2);
+
+            gift.mString = "String";
+            gift.mBoxLong = 1L;
+            gift.mUnboxLong = 2L;
+            gift.mBoxInteger = 3;
+            gift.mUnboxInt = 4;
+            gift.mBoxDouble = 5.0;
+            gift.mUnboxDouble = 6.0;
+            gift.mBoxFloat = 7.0F;
+            gift.mUnboxFloat = 8.0f;
+            gift.mBoxBoolean = true;
+            gift.mUnboxBoolean = false;
+            gift.mUnboxByteArr = new byte[]{1, 2, 3};
+            gift.mCard = card1;
+
+            return gift;
+        }
     }
 
     @Test
     public void testAnnotationProcessor() throws Exception {
         //TODO(b/156296904) add test for int, float, GenericDocument, and class with
-        // @AppSearchDocument annotation
+        // @Document annotation
         mSession.setSchema(
-                new SetSchemaRequest.Builder().addDataClass(Card.class, Gift.class).build()).get();
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class, Gift.class).build())
+                .get();
 
         // Create a Gift object and assign values.
-        Gift inputDataClass = new Gift();
-        inputDataClass.mUri = "gift.uri";
-
-        inputDataClass.mArrBoxBoolean = new Boolean[]{true, false};
-        inputDataClass.mArrBoxDouble = new Double[]{0.0, 1.0};
-        inputDataClass.mArrBoxFloat = new Float[]{2.0F, 3.0F};
-        inputDataClass.mArrBoxInteger = new Integer[]{4, 5};
-        inputDataClass.mArrBoxLong = new Long[]{6L, 7L};
-        inputDataClass.mArrString = new String[]{"cat", "dog"};
-        inputDataClass.mBoxByteArr = new Byte[]{8, 9};
-        inputDataClass.mArrUnboxBoolean = new boolean[]{false, true};
-        inputDataClass.mArrUnboxByteArr = new byte[][]{{0, 1}, {2, 3}};
-        inputDataClass.mArrUnboxDouble = new double[]{1.0, 0.0};
-        inputDataClass.mArrUnboxFloat = new float[]{3.0f, 2.0f};
-        inputDataClass.mArrUnboxInt = new int[]{5, 4};
-        inputDataClass.mArrUnboxLong = new long[]{7, 6};
-
-        Card card1 = new Card();
-        card1.mUri = "card.uri1";
-        Card card2 = new Card();
-        card2.mUri = "card.uri2";
-        inputDataClass.mArrCard = new Card[]{card2, card2};
-
-        inputDataClass.mCollectLong = Arrays.asList(inputDataClass.mArrBoxLong);
-        inputDataClass.mCollectInteger = Arrays.asList(inputDataClass.mArrBoxInteger);
-        inputDataClass.mCollectBoolean = Arrays.asList(inputDataClass.mArrBoxBoolean);
-        inputDataClass.mCollectString = Arrays.asList(inputDataClass.mArrString);
-        inputDataClass.mCollectDouble = Arrays.asList(inputDataClass.mArrBoxDouble);
-        inputDataClass.mCollectFloat = Arrays.asList(inputDataClass.mArrBoxFloat);
-        inputDataClass.mCollectByteArr = Arrays.asList(inputDataClass.mArrUnboxByteArr);
-        inputDataClass.mCollectCard = Arrays.asList(card2, card2);
-
-        inputDataClass.mString = "String";
-        inputDataClass.mBoxLong = 1L;
-        inputDataClass.mUnboxLong = 2L;
-        inputDataClass.mBoxInteger = 3;
-        inputDataClass.mUnboxInt = 4;
-        inputDataClass.mBoxDouble = 5.0;
-        inputDataClass.mUnboxDouble = 6.0;
-        inputDataClass.mBoxFloat = 7.0F;
-        inputDataClass.mUnboxFloat = 8.0f;
-        inputDataClass.mBoxBoolean = true;
-        inputDataClass.mUnboxBoolean = false;
-        inputDataClass.mUnboxByteArr = new byte[]{1, 2, 3};
-        inputDataClass.mCard = card1;
+        Gift inputDocument = Gift.createPopulatedGift();
 
         // Index the Gift document and query it.
-        checkIsBatchResultSuccess(mSession.putDocuments(
-                new PutDocumentsRequest.Builder().addDataClass(inputDataClass).build()));
-        SearchResults searchResults = mSession.query("", new SearchSpec.Builder()
+        checkIsBatchResultSuccess(mSession.put(
+                new PutDocumentsRequest.Builder().addDocuments(inputDocument).build()));
+        SearchResults searchResults = mSession.search("", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(1);
 
-        // Create DataClassFactory for Gift.
-        DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-        DataClassFactory<Gift> factory = registry.getOrCreateFactory(Gift.class);
-
         // Convert GenericDocument to Gift and check values.
-        Gift outputDataClass = factory.fromGenericDocument(documents.get((0)));
-        assertThat(outputDataClass).isEqualTo(inputDataClass);
+        Gift outputDocument = documents.get(0).toDocumentClass(Gift.class);
+        assertThat(outputDocument).isEqualTo(inputDocument);
     }
 
     @Test
     public void testAnnotationProcessor_queryByType() throws Exception {
         mSession.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addDataClass(Card.class, Gift.class)
-                        .addSchema(AppSearchEmail.SCHEMA).build())
+                        .addDocumentClasses(Card.class, Gift.class)
+                        .addSchemas(AppSearchEmail.SCHEMA).build())
                 .get();
 
         // Create documents and index them
-        Gift inputDataClass1 = new Gift();
-        inputDataClass1.mUri = "gift.uri1";
-        Gift inputDataClass2 = new Gift();
-        inputDataClass2.mUri = "gift.uri2";
+        Gift inputDocument1 = new Gift();
+        inputDocument1.mNamespace = "gift.namespace";
+        inputDocument1.mId = "gift.id1";
+        Gift inputDocument2 = new Gift();
+        inputDocument2.mNamespace = "gift.namespace";
+        inputDocument2.mId = "gift.id2";
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri3")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id3")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mSession.putDocuments(
+        checkIsBatchResultSuccess(mSession.put(
                 new PutDocumentsRequest.Builder()
-                        .addDataClass(inputDataClass1, inputDataClass2)
-                        .addGenericDocument(email1).build()));
+                        .addDocuments(inputDocument1, inputDocument2)
+                        .addGenericDocuments(email1).build()));
 
         // Query the documents by it's schema type.
-        SearchResults searchResults = mSession.query("",
+        SearchResults searchResults = mSession.search("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addSchemaType("Gift", AppSearchEmail.SCHEMA_TYPE)
+                        .addFilterSchemas("Gift", AppSearchEmail.SCHEMA_TYPE)
                         .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(3);
 
         // Query the documents by it's class.
-        searchResults = mSession.query("",
+        searchResults = mSession.search("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addSchemaByDataClass(Gift.class)
+                        .addFilterDocumentClasses(Gift.class)
                         .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(2);
 
         // Query the documents by schema type and class mix.
-        searchResults = mSession.query("",
+        searchResults = mSession.search("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addSchemaType(AppSearchEmail.SCHEMA_TYPE)
-                        .addSchemaByDataClass(Gift.class)
+                        .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                        .addFilterDocumentClasses(Gift.class)
                         .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(3);
     }
+
+    @Test
+    public void testGenericDocumentConversion() throws Exception {
+        Gift inGift = Gift.createPopulatedGift();
+        GenericDocument genericDocument1 = GenericDocument.fromDocumentClass(inGift);
+        GenericDocument genericDocument2 = GenericDocument.fromDocumentClass(inGift);
+        Gift outGift = genericDocument2.toDocumentClass(Gift.class);
+
+        assertThat(inGift).isNotSameInstanceAs(outGift);
+        assertThat(inGift).isEqualTo(outGift);
+        assertThat(genericDocument1).isNotSameInstanceAs(genericDocument2);
+        assertThat(genericDocument1).isEqualTo(genericDocument2);
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchEmailTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchEmailTest.java
index ee68c88..5c784e7 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchEmailTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchEmailTest.java
@@ -24,7 +24,7 @@
 
     @Test
     public void testBuildEmailAndGetValue() {
-        AppSearchEmail email = new AppSearchEmail.Builder("uri")
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id")
                 .setFrom("FakeFromAddress")
                 .setCc("CC1", "CC2")
                 // Score and Property are mixed into the middle to make sure DocumentBuilder's
@@ -35,7 +35,8 @@
                 .setBody("EmailBody")
                 .build();
 
-        assertThat(email.getUri()).isEqualTo("uri");
+        assertThat(email.getNamespace()).isEqualTo("namespace");
+        assertThat(email.getId()).isEqualTo("id");
         assertThat(email.getFrom()).isEqualTo("FakeFromAddress");
         assertThat(email.getTo()).isNull();
         assertThat(email.getCc()).asList().containsExactly("CC1", "CC2");
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java
new file mode 100644
index 0000000..125e55f
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchResultTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+public class AppSearchResultTest {
+    @Test
+    public void testMapNullPointerException() {
+        NullPointerException e = assertThrows(NullPointerException.class, () -> {
+            Object o = null;
+            o.toString();
+        });
+        AppSearchResult<?> result = AppSearchResult.throwableToFailedResult(e);
+        assertThat(result.getResultCode()).isEqualTo(AppSearchResult.RESULT_INTERNAL_ERROR);
+        // Makes sure the exception name is included in the string. Some exceptions have terse or
+        // missing strings so it's confusing to read the output without the exception name.
+        assertThat(result.getErrorMessage()).startsWith("NullPointerException");
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java
new file mode 100644
index 0000000..c15f290
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.junit.Test;
+
+public class GenericDocumentTest {
+    @Test
+    public void testRecreateFromParcel() {
+        GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Hello")
+                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
+                .setPropertyDocument(
+                        "propDocument",
+                        new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                                .setPropertyString("propString", "Goodbye")
+                                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                                .build())
+                .build();
+
+        // Serialize the document
+        Parcel inParcel = Parcel.obtain();
+        inParcel.writeBundle(inDoc.getBundle());
+        byte[] data = inParcel.marshall();
+        inParcel.recycle();
+
+        // Deserialize the document
+        Parcel outParcel = Parcel.obtain();
+        outParcel.unmarshall(data, 0, data.length);
+        outParcel.setDataPosition(0);
+        Bundle outBundle = outParcel.readBundle();
+        outParcel.recycle();
+
+        // Compare results
+        GenericDocument outDoc = new GenericDocument(outBundle);
+        assertThat(inDoc).isEqualTo(outDoc);
+        assertThat(outDoc.getPropertyString("propString")).isEqualTo("Hello");
+        assertThat(outDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
+        assertThat(outDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
+                .isEqualTo("Goodbye");
+        assertThat(outDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+                .isEqualTo(new byte[][]{{3, 4}});
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/PutDocumentsRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/PutDocumentsRequestTest.java
index fdcf932..ed028c2 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/PutDocumentsRequestTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/PutDocumentsRequestTest.java
@@ -16,13 +16,13 @@
 
 package androidx.appsearch.app;
 
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
 
-import androidx.appsearch.annotation.AppSearchDocument;
+import androidx.appsearch.annotation.Document;
 import androidx.appsearch.localstorage.LocalStorage;
 import androidx.test.core.app.ApplicationProvider;
 
@@ -36,47 +36,51 @@
 
     @Test
     public void addGenericDocument_byCollection() {
-        Set<AppSearchEmail> emails = ImmutableSet.of(new AppSearchEmail.Builder("test1").build(),
-                new AppSearchEmail.Builder("test2").build());
-        PutDocumentsRequest request = new PutDocumentsRequest.Builder().addGenericDocument(emails)
+        Set<AppSearchEmail> emails =
+                ImmutableSet.of(new AppSearchEmail.Builder("namespace", "test1").build(),
+                        new AppSearchEmail.Builder("namespace", "test2").build());
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder().addGenericDocuments(emails)
                 .build();
 
-        assertThat(request.getDocuments().get(0).getUri()).isEqualTo("test1");
-        assertThat(request.getDocuments().get(1).getUri()).isEqualTo("test2");
+        assertThat(request.getGenericDocuments().get(0).getId()).isEqualTo("test1");
+        assertThat(request.getGenericDocuments().get(1).getId()).isEqualTo("test2");
     }
 
 // @exportToFramework:startStrip()
-    @AppSearchDocument
+    @Document
     static class Card {
-        @AppSearchDocument.Uri
-        String mUri;
+        @Document.Namespace
+        String mNamespace;
 
-        @AppSearchDocument.Property(indexingType = INDEXING_TYPE_PREFIXES)
+        @Document.Id
+        String mId;
+
+        @Document.Property(indexingType = INDEXING_TYPE_PREFIXES)
         String mString;
 
-        Card(String mUri, String mString) {
-            this.mUri = mUri;
+        Card(String mNamespace, String mId, String mString) {
+            this.mId = mId;
+            this.mNamespace = mNamespace;
             this.mString = mString;
         }
     }
 
     @Test
-    public void addDataClass_byCollection() throws Exception {
+    public void addDocumentClasses_byCollection() throws Exception {
         // A schema with Card must be set in order to be able to add a Card instance to
         // PutDocumentsRequest.
         Context context = ApplicationProvider.getApplicationContext();
         AppSearchSession session = LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context)
-                        .setDatabaseName(LocalStorage.DEFAULT_DATABASE_NAME)
+                new LocalStorage.SearchContext.Builder(context, /*databaseName=*/ "")
                         .build()
         ).get();
-        session.setSchema(new SetSchemaRequest.Builder().addDataClass(Card.class).build());
+        session.setSchema(new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build());
 
-        Set<Card> cards = ImmutableSet.of(new Card("cardUri", "cardProperty"));
-        PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDataClass(cards)
+        Set<Card> cards = ImmutableSet.of(new Card("cardNamespace", "cardId", "cardProperty"));
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDocuments(cards)
                 .build();
 
-        assertThat(request.getDocuments().get(0).getUri()).isEqualTo("cardUri");
+        assertThat(request.getGenericDocuments().get(0).getId()).isEqualTo("cardId");
     }
 // @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
index 5d98ead..42beb2f 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
@@ -16,15 +16,16 @@
 
 package androidx.appsearch.app;
 
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.os.Bundle;
 
-import androidx.appsearch.annotation.AppSearchDocument;
+import androidx.appsearch.annotation.Document;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
 import org.junit.Test;
@@ -39,8 +40,9 @@
     public void testGetBundle() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addNamespace("namespace1", "namespace2")
-                .addSchemaType("schemaTypes1", "schemaTypes2")
+                .addFilterNamespaces("namespace1", "namespace2")
+                .addFilterSchemas("schemaTypes1", "schemaTypes2")
+                .addFilterPackageNames("package1", "package2")
                 .setSnippetCount(5)
                 .setSnippetCountPerProperty(10)
                 .setMaxSnippetSize(15)
@@ -54,8 +56,10 @@
                 .isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
         assertThat(bundle.getStringArrayList(SearchSpec.NAMESPACE_FIELD)).containsExactly(
                 "namespace1", "namespace2");
-        assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_TYPE_FIELD)).containsExactly(
+        assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_FIELD)).containsExactly(
                 "schemaTypes1", "schemaTypes2");
+        assertThat(bundle.getStringArrayList(SearchSpec.PACKAGE_NAME_FIELD)).containsExactly(
+                "package1", "package2");
         assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_FIELD)).isEqualTo(5);
         assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_PER_PROPERTY_FIELD)).isEqualTo(10);
         assertThat(bundle.getInt(SearchSpec.MAX_SNIPPET_FIELD)).isEqualTo(15);
@@ -69,9 +73,9 @@
     public void testGetProjectionTypePropertyMasks() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addProjection("TypeA", "field1", "field2.subfield2")
-                .addProjection("TypeB", "field7")
-                .addProjection("TypeC")
+                .addProjection("TypeA", ImmutableList.of("field1", "field2.subfield2"))
+                .addProjection("TypeB", ImmutableList.of("field7"))
+                .addProjection("TypeC", ImmutableList.of())
                 .build();
 
         Map<String, List<String>> typePropertyPathMap = searchSpec.getProjections();
@@ -93,12 +97,15 @@
     }
 
 // @exportToFramework:startStrip()
-    @AppSearchDocument
+    @Document
     static class King extends Card {
-        @AppSearchDocument.Uri
-        String mUri;
+        @Document.Namespace
+        String mNamespace;
 
-        @AppSearchDocument.Property
+        @Document.Id
+        String mId;
+
+        @Document.Property
                 (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
         String mString;
     }
@@ -106,15 +113,15 @@
     static class Card {}
 
     @Test
-    public void testAddSchemaByDataClass_byCollection() throws Exception {
+    public void testFilterDocumentClasses_byCollection() throws Exception {
         Set<Class<King>> cardClassSet = ImmutableSet.of(King.class);
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addSchemaByDataClass(cardClassSet)
+                .addFilterDocumentClasses(cardClassSet)
                 .build();
 
         Bundle bundle = searchSpec.getBundle();
-        assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_TYPE_FIELD)).containsExactly(
+        assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_FIELD)).containsExactly(
                 "King");
     }
 // @exportToFramework:endStrip()
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
index 4b8b21d..ae31035 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
@@ -16,14 +16,14 @@
 
 package androidx.appsearch.app;
 
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
-import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
 
-import androidx.appsearch.annotation.AppSearchDocument;
+import androidx.appsearch.annotation.Document;
 import androidx.collection.ArrayMap;
 
 import com.google.common.collect.ImmutableSet;
@@ -38,12 +38,15 @@
 
 public class SetSchemaRequestTest {
 // @exportToFramework:startStrip()
-    @AppSearchDocument
+    @Document
     static class Card {
-        @AppSearchDocument.Uri
-        String mUri;
+        @Document.Namespace
+        String mNamespace;
 
-        @AppSearchDocument.Property
+        @Document.Id
+        String mId;
+
+        @Document.Property
                 (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
         String mString;
 
@@ -56,29 +59,36 @@
                 return false;
             }
             AnnotationProcessorTestBase.Card otherCard = (AnnotationProcessorTestBase.Card) other;
-            assertThat(otherCard.mUri).isEqualTo(this.mUri);
+            assertThat(otherCard.mNamespace).isEqualTo(this.mNamespace);
+            assertThat(otherCard.mId).isEqualTo(this.mId);
             return true;
         }
     }
 
     static class Spade {}
 
-    @AppSearchDocument
+    @Document
     static class King extends Spade {
-        @AppSearchDocument.Uri
-        String mUri;
+        @Document.Id
+        String mId;
 
-        @AppSearchDocument.Property
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Property
                 (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
         String mString;
     }
 
-    @AppSearchDocument
+    @Document
     static class Queen extends Spade {
-        @AppSearchDocument.Uri
-        String mUri;
+        @Document.Namespace
+        String mNamespace;
 
-        @AppSearchDocument.Property
+        @Document.Id
+        String mId;
+
+        @Document.Property
                 (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
         String mString;
     }
@@ -93,9 +103,9 @@
     }
 
     @Test
-    public void testInvalidSchemaReferences_fromSystemUiVisibility() {
+    public void testInvalidSchemaReferences_fromDisplayedBySystem() {
         IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
-                () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForSystemUi(
+                () -> new SetSchemaRequest.Builder().setSchemaTypeDisplayedBySystem(
                         "InvalidSchema", false).build());
         assertThat(expected).hasMessageThat().contains("referenced, but were not added");
     }
@@ -110,51 +120,49 @@
     }
 
     @Test
-    public void testSchemaTypeVisibilityForSystemUi_visible() {
+    public void testSetSchemaTypeDisplayedBySystem_displayed() {
         AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
 
-        // By default, the schema is visible.
+        // By default, the schema is displayed.
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addSchema(schema).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).isEmpty();
+                new SetSchemaRequest.Builder().addSchemas(schema).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).isEmpty();
 
-        request =
-                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
-                        "Schema", true).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).isEmpty();
+        request = new SetSchemaRequest.Builder()
+                .addSchemas(schema).setSchemaTypeDisplayedBySystem("Schema", true).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).isEmpty();
     }
 
     @Test
-    public void testSchemaTypeVisibilityForSystemUi_notVisible() {
+    public void testSetSchemaTypeDisplayedBySystem_notDisplayed() {
         AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
-        SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
-                        "Schema", false).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).containsExactly("Schema");
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema).setSchemaTypeDisplayedBySystem("Schema", false).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).containsExactly("Schema");
     }
 
 // @exportToFramework:startStrip()
     @Test
-    public void testDataClassVisibilityForSystemUi_visible() throws Exception {
-        // By default, the schema is visible.
+    public void testSetDocumentClassDisplayedBySystem_displayed() throws Exception {
+        // By default, the schema is displayed.
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).isEmpty();
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
+        assertThat(request.getSchemasNotDisplayedBySystem()).isEmpty();
 
         request =
-                new SetSchemaRequest.Builder().addDataClass(
-                        Card.class).setDataClassVisibilityForSystemUi(
+                new SetSchemaRequest.Builder().addDocumentClasses(
+                        Card.class).setDocumentClassDisplayedBySystem(
                         Card.class, true).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).isEmpty();
+        assertThat(request.getSchemasNotDisplayedBySystem()).isEmpty();
     }
 
     @Test
-    public void testDataClassVisibilityForSystemUi_notVisible() throws Exception {
+    public void testSetDocumentClassDisplayedBySystem_notDisplayed() throws Exception {
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(
-                        Card.class).setDataClassVisibilityForSystemUi(
+                new SetSchemaRequest.Builder().addDocumentClasses(
+                        Card.class).setDocumentClassDisplayedBySystem(
                         Card.class, false).build();
-        assertThat(request.getSchemasNotVisibleToSystemUi()).containsExactly("Card");
+        assertThat(request.getSchemasNotDisplayedBySystem()).containsExactly("Card");
     }
 // @exportToFramework:endStrip()
 
@@ -164,7 +172,7 @@
 
         // By default, the schema is not visible.
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addSchema(schema).build();
+                new SetSchemaRequest.Builder().addSchemas(schema).build();
         assertThat(request.getSchemasVisibleToPackages()).isEmpty();
 
         PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
@@ -173,7 +181,7 @@
         expectedVisibleToPackagesMap.put("Schema", Collections.singleton(packageIdentifier));
 
         request =
-                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForPackage(
+                new SetSchemaRequest.Builder().addSchemas(schema).setSchemaTypeVisibilityForPackage(
                         "Schema", /*visible=*/ true, packageIdentifier).build();
         assertThat(request.getSchemasVisibleToPackages()).containsExactlyEntriesIn(
                 expectedVisibleToPackagesMap);
@@ -184,7 +192,7 @@
         AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
 
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForPackage(
+                new SetSchemaRequest.Builder().addSchemas(schema).setSchemaTypeVisibilityForPackage(
                         "Schema", /*visible=*/ false, new PackageIdentifier("com.package.foo",
                                 /*sha256Certificate=*/ new byte[]{})).build();
         assertThat(request.getSchemasVisibleToPackages()).isEmpty();
@@ -201,7 +209,7 @@
 
         SetSchemaRequest request =
                 new SetSchemaRequest.Builder()
-                        .addSchema(schema)
+                        .addSchemas(schema)
                         // Set it visible for "Schema"
                         .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
                                 true, packageIdentifier)
@@ -219,7 +227,7 @@
 
         SetSchemaRequest request =
                 new SetSchemaRequest.Builder()
-                        .addSchema(schema)
+                        .addSchemas(schema)
                         // First set it as visible
                         .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
                                 true, new PackageIdentifier("com.package.foo",
@@ -236,10 +244,10 @@
 
 // @exportToFramework:startStrip()
     @Test
-    public void testDataClassVisibilityForPackage_visible() throws Exception {
+    public void testSetDocumentClassVisibilityForPackage_visible() throws Exception {
         // By default, the schema is not visible.
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
         assertThat(request.getSchemasVisibleToPackages()).isEmpty();
 
         PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
@@ -248,18 +256,18 @@
         expectedVisibleToPackagesMap.put("Card", Collections.singleton(packageIdentifier));
 
         request =
-                new SetSchemaRequest.Builder().addDataClass(
-                        Card.class).setDataClassVisibilityForPackage(
+                new SetSchemaRequest.Builder().addDocumentClasses(
+                        Card.class).setDocumentClassVisibilityForPackage(
                         Card.class, /*visible=*/ true, packageIdentifier).build();
         assertThat(request.getSchemasVisibleToPackages()).containsExactlyEntriesIn(
                 expectedVisibleToPackagesMap);
     }
 
     @Test
-    public void testDataClassVisibilityForPackage_notVisible() throws Exception {
+    public void testSetDocumentClassVisibilityForPackage_notVisible() throws Exception {
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(
-                        Card.class).setDataClassVisibilityForPackage(
+                new SetSchemaRequest.Builder().addDocumentClasses(
+                        Card.class).setDocumentClassVisibilityForPackage(
                         Card.class, /*visible=*/ false,
                         new PackageIdentifier("com.package.foo", /*sha256Certificate=*/
                                 new byte[]{})).build();
@@ -267,10 +275,10 @@
     }
 
     @Test
-    public void testDataClassVisibilityForPackage_deduped() throws Exception {
+    public void testSetDocumentClassVisibilityForPackage_deduped() throws Exception {
         // By default, the schema is not visible.
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
         assertThat(request.getSchemasVisibleToPackages()).isEmpty();
 
         PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
@@ -280,10 +288,10 @@
 
         request =
                 new SetSchemaRequest.Builder()
-                        .addDataClass(Card.class)
-                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+                        .addDocumentClasses(Card.class)
+                        .setDocumentClassVisibilityForPackage(Card.class, /*visible=*/
                                 true, packageIdentifier)
-                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+                        .setDocumentClassVisibilityForPackage(Card.class, /*visible=*/
                                 true, packageIdentifier)
                         .build();
         assertThat(request.getSchemasVisibleToPackages()).containsExactlyEntriesIn(
@@ -291,21 +299,21 @@
     }
 
     @Test
-    public void testDataClassVisibilityForPackage_removed() throws Exception {
+    public void testSetDocumentClassVisibilityForPackage_removed() throws Exception {
         // By default, the schema is not visible.
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+                new SetSchemaRequest.Builder().addDocumentClasses(Card.class).build();
         assertThat(request.getSchemasVisibleToPackages()).isEmpty();
 
         request =
                 new SetSchemaRequest.Builder()
-                        .addDataClass(Card.class)
+                        .addDocumentClasses(Card.class)
                         // First set it as visible
-                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+                        .setDocumentClassVisibilityForPackage(Card.class, /*visible=*/
                                 true, new PackageIdentifier("com.package.foo",
                                         /*sha256Certificate=*/ new byte[]{100}))
                         // Then make it not visible
-                        .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+                        .setDocumentClassVisibilityForPackage(Card.class, /*visible=*/
                                 false, new PackageIdentifier("com.package.foo",
                                         /*sha256Certificate=*/ new byte[]{100}))
                         .build();
@@ -315,23 +323,35 @@
     }
 
     @Test
-    public void testAddDataClass_byCollection() throws Exception {
+    public void testAddDocumentClasses_byCollection() throws Exception {
         Set<Class<? extends Spade>> cardClasses = ImmutableSet.of(Queen.class, King.class);
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(cardClasses)
+                new SetSchemaRequest.Builder().addDocumentClasses(cardClasses)
                         .build();
         assertThat(getSchemaTypesFromSetSchemaRequest(request)).containsExactly("Queen",
                 "King");
     }
 
     @Test
-    public void testAddDataClass_byCollectionWithSeparateCalls() throws
+    public void testAddDocumentClasses_byCollectionWithSeparateCalls() throws
             Exception {
         SetSchemaRequest request =
-                new SetSchemaRequest.Builder().addDataClass(ImmutableSet.of(Queen.class))
-                        .addDataClass(ImmutableSet.of(King.class)).build();
+                new SetSchemaRequest.Builder().addDocumentClasses(ImmutableSet.of(Queen.class))
+                        .addDocumentClasses(ImmutableSet.of(King.class)).build();
         assertThat(getSchemaTypesFromSetSchemaRequest(request)).containsExactly("Queen",
                 "King");
     }
+
+    @Test
+    public void testSetVersion() throws
+            Exception {
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder()
+                .addDocumentClasses(ImmutableSet.of(Queen.class)).setVersion(0).build());
+        assertThat(exception).hasMessageThat().contains("Version must be a positive number");
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                        .addDocumentClasses(ImmutableSet.of(Queen.class)).setVersion(1).build();
+        assertThat(request.getVersion()).isEqualTo(1);
+    }
 // @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchBatchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchBatchResultCtsTest.java
new file mode 100644
index 0000000..2a72215
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchBatchResultCtsTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchResult;
+
+import org.junit.Test;
+
+public class AppSearchBatchResultCtsTest {
+    @Test
+    public void testIsSuccess_true() {
+        AppSearchBatchResult<String, Integer> result =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setSuccess("keySuccess2", 2)
+                        .setResult("keySuccess3", AppSearchResult.newSuccessfulResult(3))
+                        .build();
+        assertThat(result.isSuccess()).isTrue();
+        result.checkSuccess();
+    }
+
+    @Test
+    public void testIsSuccess_false() {
+        AppSearchBatchResult<String, Integer> result1 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setSuccess("keySuccess2", 2)
+                        .setFailure(
+                                "keyFailure1", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .build();
+
+        AppSearchBatchResult<String, Integer> result2 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setResult(
+                                "keyFailure3",
+                                AppSearchResult.newFailedResult(
+                                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"))
+                        .build();
+
+        assertThat(result1.isSuccess()).isFalse();
+        assertThat(result2.isSuccess()).isFalse();
+        assertThrows(IllegalStateException.class, result1::checkSuccess);
+        assertThrows(IllegalStateException.class, result2::checkSuccess);
+    }
+
+    @Test
+    public void testIsSuccess_replace() {
+        AppSearchBatchResult<String, Integer> result1 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("key", 1)
+                        .setFailure("key", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .build();
+
+        AppSearchBatchResult<String, Integer> result2 =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setFailure("key", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .setSuccess("key", 1)
+                        .build();
+
+        assertThat(result1.isSuccess()).isFalse();
+        assertThrows(IllegalStateException.class, result1::checkSuccess);
+        assertThat(result2.isSuccess()).isTrue();
+        result2.checkSuccess();
+    }
+
+    @Test
+    public void testGetters() {
+        AppSearchBatchResult<String, Integer> result =
+                new AppSearchBatchResult.Builder<String, Integer>()
+                        .setSuccess("keySuccess1", 1)
+                        .setSuccess("keySuccess2", 2)
+                        .setFailure(
+                                "keyFailure1", AppSearchResult.RESULT_UNKNOWN_ERROR, "message1")
+                        .setFailure(
+                                "keyFailure2", AppSearchResult.RESULT_INTERNAL_ERROR, "message2")
+                        .setResult("keySuccess3", AppSearchResult.newSuccessfulResult(3))
+                        .setResult(
+                                "keyFailure3",
+                                AppSearchResult.newFailedResult(
+                                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"))
+                        .build();
+
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getSuccesses()).containsExactly(
+                "keySuccess1", 1, "keySuccess2", 2, "keySuccess3", 3);
+        assertThat(result.getFailures()).containsExactly(
+                "keyFailure1",
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_UNKNOWN_ERROR, "message1"),
+                "keyFailure2",
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, "message2"),
+                "keyFailure3",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"));
+        assertThat(result.getAll()).containsExactly(
+                "keySuccess1", AppSearchResult.newSuccessfulResult(1),
+                "keySuccess2", AppSearchResult.newSuccessfulResult(2),
+                "keySuccess3", AppSearchResult.newSuccessfulResult(3),
+                "keyFailure1",
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_UNKNOWN_ERROR, "message1"),
+                "keyFailure2",
+                AppSearchResult.newFailedResult(AppSearchResult.RESULT_INTERNAL_ERROR, "message2"),
+                "keyFailure3",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INVALID_ARGUMENT, "message3"));
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchMigratorTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchMigratorTest.java
new file mode 100644
index 0000000..218a631
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchMigratorTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.Migrator;
+
+import org.junit.Test;
+
+public class AppSearchMigratorTest {
+
+    @Test
+    public void testOnUpgrade() {
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                        document.getSchemaType())
+                        .setCreationTimestampMillis(document.getCreationTimestampMillis())
+                        .setScore(document.getScore())
+                        .setTtlMillis(document.getTtlMillis())
+                        .setPropertyString("migration",
+                                "Upgrade the document from version " + currentVersion
+                                        + " to version " + finalVersion)
+                        .build();
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        GenericDocument input = new GenericDocument.Builder<>("namespace", "id",
+                "schemaType")
+                .setCreationTimestampMillis(12345L)
+                .setScore(100)
+                .setTtlMillis(54321L).build();
+
+        GenericDocument expected = new GenericDocument.Builder<>("namespace", "id",
+                "schemaType")
+                .setCreationTimestampMillis(12345L)
+                .setScore(100)
+                .setTtlMillis(54321L)
+                .setPropertyString("migration",
+                        "Upgrade the document from version 3 to version 5")
+                .build();
+
+        GenericDocument output = migrator.onUpgrade(/*currentVersion=*/3,
+                /*finalVersion=*/5, input);
+        assertThat(output).isEqualTo(expected);
+    }
+
+    @Test
+    public void testOnDowngrade() {
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                        document.getSchemaType())
+                        .setCreationTimestampMillis(document.getCreationTimestampMillis())
+                        .setScore(document.getScore())
+                        .setTtlMillis(document.getTtlMillis())
+                        .setPropertyString("migration",
+                                "Downgrade the document from version " + currentVersion
+                                        + " to version " + finalVersion)
+                        .build();
+            }
+        };
+
+        GenericDocument input = new GenericDocument.Builder<>("namespace", "id",
+                "schemaType")
+                .setCreationTimestampMillis(12345L)
+                .setScore(100)
+                .setTtlMillis(54321L).build();
+
+        GenericDocument expected = new GenericDocument.Builder<>("namespace", "id",
+                "schemaType")
+                .setCreationTimestampMillis(12345L)
+                .setScore(100)
+                .setTtlMillis(54321L)
+                .setPropertyString("migration",
+                        "Downgrade the document from version 6 to version 4")
+                .build();
+
+        GenericDocument output = migrator.onDowngrade(/*currentVersion=*/6,
+                /*finalVersion=*/4, input);
+        assertThat(output).isEqualTo(expected);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
index 7a646b0..e0815a5 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
@@ -23,6 +23,7 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 import androidx.appsearch.exceptions.IllegalSchemaException;
 
 import org.junit.Test;
@@ -30,45 +31,32 @@
 public class AppSearchSchemaCtsTest {
     @Test
     public void testInvalidEnums() {
-        PropertyConfig.Builder builder = new PropertyConfig.Builder("test");
-        assertThrows(IllegalArgumentException.class, () -> builder.setDataType(99));
+        StringPropertyConfig.Builder builder = new StringPropertyConfig.Builder("test");
         assertThrows(IllegalArgumentException.class, () -> builder.setCardinality(99));
     }
 
     @Test
-    public void testMissingFields() {
-        PropertyConfig.Builder builder = new PropertyConfig.Builder("test");
-        IllegalSchemaException e = assertThrows(IllegalSchemaException.class, builder::build);
-        assertThat(e).hasMessageThat().contains("Missing field: dataType");
-
-        builder.setDataType(PropertyConfig.DATA_TYPE_DOCUMENT);
-        e = assertThrows(IllegalSchemaException.class, builder::build);
-        assertThat(e).hasMessageThat().contains("Missing field: schemaType");
-
-        builder.setSchemaType("TestType");
-        e = assertThrows(IllegalSchemaException.class, builder::build);
-        assertThat(e).hasMessageThat().contains("Missing field: cardinality");
-
-        builder.setCardinality(PropertyConfig.CARDINALITY_REPEATED);
-        builder.build();
+    public void testDefaultValues() {
+        StringPropertyConfig builder = new StringPropertyConfig.Builder("test").build();
+        assertThat(builder.getIndexingType()).isEqualTo(StringPropertyConfig.INDEXING_TYPE_NONE);
+        assertThat(builder.getTokenizerType()).isEqualTo(StringPropertyConfig.TOKENIZER_TYPE_NONE);
+        assertThat(builder.getCardinality()).isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
     }
 
     @Test
     public void testDuplicateProperties() {
         AppSearchSchema.Builder builder = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 );
         IllegalSchemaException e = assertThrows(IllegalSchemaException.class,
-                () -> builder.addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                () -> builder.addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()));
         assertThat(e).hasMessageThat().contains("Property defined more than once: subject");
     }
@@ -76,19 +64,17 @@
     @Test
     public void testEquals_identical() {
         AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
         AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
         assertThat(schema1).isEqualTo(schema2);
@@ -98,18 +84,16 @@
     @Test
     public void testEquals_differentOrder() {
         AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
         AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                         .build()
                 ).build();
@@ -118,21 +102,19 @@
     }
 
     @Test
-    public void testEquals_failure() {
+    public void testEquals_failure_differentProperty() {
         AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
         AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Diff
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
         assertThat(schema1).isNotEqualTo(schema2);
@@ -142,32 +124,28 @@
     @Test
     public void testEquals_failure_differentOrder() {
         AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
-                ).addProperty(new PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                ).addProperty(new StringPropertyConfig.Builder("body")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
         // Order of 'body' and 'subject' has been switched
         AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
-                .addProperty(new PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("body")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
-                ).addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                ).addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
         assertThat(schema1).isNotEqualTo(schema2);
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaMigrationCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaMigrationCtsTestBase.java
new file mode 100644
index 0000000..5e71a96
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaMigrationCtsTestBase.java
@@ -0,0 +1,1394 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app.cts;
+
+import static androidx.appsearch.app.util.AppSearchTestUtils.checkIsBatchResultSuccess;
+import static androidx.appsearch.app.util.AppSearchTestUtils.convertSearchResultsToDocuments;
+import static androidx.appsearch.app.util.AppSearchTestUtils.doGet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/*
+ * For schema migration, we have 4 factors
+ * A. is ForceOverride set to true?
+ * B. is the schema change backwards compatible?
+ * C. is shouldTrigger return true?
+ * D. is there a migration triggered for each incompatible type and no deleted types?
+ * If B is true then D could never be false, so that will give us 12 combinations.
+ *
+ *                                Trigger       Delete      First            Second
+ * A      B       C       D       Migration     Types       SetSchema        SetSchema
+ * TRUE   TRUE    TRUE    TRUE    Yes                       succeeds         succeeds(noop)
+ * TRUE   TRUE    FALSE   TRUE                              succeeds         succeeds(noop)
+ * TRUE   FALSE   TRUE    TRUE    Yes                       fail             succeeds
+ * TRUE   FALSE   TRUE    FALSE   Yes           Yes         fail             succeeds
+ * TRUE   FALSE   FALSE   TRUE                  Yes         fail             succeeds
+ * TRUE   FALSE   FALSE   FALSE                 Yes         fail             succeeds
+ * FALSE  TRUE    TRUE    TRUE    Yes                       succeeds         succeeds(noop)
+ * FALSE  TRUE    FALSE   TRUE                              succeeds         succeeds(noop)
+ * FALSE  FALSE   TRUE    TRUE    Yes                       fail             succeeds
+ * FALSE  FALSE   TRUE    FALSE   Yes                       fail             throw error
+ * FALSE  FALSE   FALSE   TRUE    Impossible case, migrators are inactivity
+ * FALSE  FALSE   FALSE   FALSE                             fail             throw error
+ */
+//TODO(b/178060626) add a platform version of this test
+public abstract class AppSearchSchemaMigrationCtsTestBase {
+
+    private static final String DB_NAME = "";
+    private static final long DOCUMENT_CREATION_TIME = 12345L;
+    private static final Migrator ACTIVE_NOOP_MIGRATOR = new Migrator() {
+        @Override
+        public boolean shouldMigrate(int currentVersion, int finalVersion) {
+            return true;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+    };
+    private static final Migrator INACTIVE_MIGRATOR = new Migrator() {
+        @Override
+        public boolean shouldMigrate(int currentVersion, int finalVersion) {
+            return false;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+    };
+
+    private AppSearchSession mDb;
+
+    protected abstract ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull String dbName);
+
+    @Before
+    public void setUp() throws Exception {
+        mDb = createSearchSession(DB_NAME).get();
+
+        // Cleanup whatever documents may still exist in these databases. This is needed in
+        // addition to tearDown in case a test exited without completing properly.
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).build()).get();
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id0", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id0", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Cleanup whatever documents may still exist in these databases.
+        mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_B_C_D() throws Exception {
+        // create a backwards compatible schema and update the version
+        AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_B_NC_D() throws Exception {
+        // create a backwards compatible schema but don't update the version
+        AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_C_D() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_C_ND() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                        .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
+                        .setForceOverride(true)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_NC_D() throws Exception {
+        // create a backwards incompatible schema but don't update the version
+        AppSearchSchema NB_NC_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(NB_NC_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_A_NB_NC_ND() throws Exception {
+        // create a backwards incompatible schema but don't update the version
+        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+                        .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
+                        .setForceOverride(true)
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_B_C_D() throws Exception {
+        // create a backwards compatible schema and update the version
+        AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_B_NC_D() throws Exception {
+        // create a backwards compatible schema but don't update the version
+        AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setForceOverride(true)
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_NB_C_D() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        mDb.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                        .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+    }
+
+    @Test
+    public void testSchemaMigration_NA_NB_C_ND() throws Exception {
+        // create a backwards incompatible schema and update the version
+        AppSearchSchema $B_C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(
+                        new SetSchemaRequest.Builder().addSchemas($B_C_Schema)
+                                .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
+                                .setVersion(2)     // upgrade version
+                                .build()).get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+    }
+
+    @Test
+    public void testSchemaMigration_NA_NB_NC_ND() throws Exception {
+        // create a backwards incompatible schema but don't update the version
+        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+                .build();
+
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(
+                        new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+                                .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
+                                .build()).get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+    }
+
+    @Test
+    public void testSchemaMigration() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("To")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setPropertyString("To", "testTo example1")
+                .build();
+        GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "testSchema")
+                .setPropertyString("subject", "testPut example2")
+                .setPropertyString("To", "testTo example2")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null, "id2", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type and upgrade the version number
+        AppSearchSchema newSchema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // set the new schema to AppSearch, the first document will be migrated successfully but the
+        // second one will be failed.
+
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return currentVersion != finalVersion;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                if (document.getId().equals("id2")) {
+                    return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                            document.getSchemaType())
+                            .setPropertyString("subject", "testPut example2")
+                            .setPropertyString("to",
+                                    "Expect to fail, property not in the schema")
+                            .build();
+                }
+                return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                        document.getSchemaType())
+                        .setPropertyString("subject", "testPut example1 migrated")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Downgrade should not be triggered for this test");
+            }
+        };
+
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(2)     // upgrade version
+                        .build()).get();
+
+        // Check the schema has been saved
+        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes())
+                .containsExactly("testSchema");
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("testSchema");
+
+        // Check migrate the first document is success
+        GenericDocument expected = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1 migrated")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected);
+
+        // Check migrate the second document is fail.
+        assertThat(setSchemaResponse.getMigrationFailures()).hasSize(1);
+        SetSchemaResponse.MigrationFailure migrationFailure =
+                setSchemaResponse.getMigrationFailures().get(0);
+        assertThat(migrationFailure.getNamespace()).isEqualTo("namespace");
+        assertThat(migrationFailure.getSchemaType()).isEqualTo("testSchema");
+        assertThat(migrationFailure.getDocumentId()).isEqualTo("id2");
+    }
+
+    @Test
+    public void testSchemaMigration_downgrade() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("To")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).setVersion(3).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setPropertyString("To", "testTo example1")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type and upgrade the version number
+        AppSearchSchema newSchema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // set the new schema to AppSearch
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return currentVersion != finalVersion;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Upgrade should not be triggered for this test");
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>(document.getNamespace(), document.getId(),
+                        document.getSchemaType())
+                        .setPropertyString("subject", "testPut example1 migrated")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+            }
+        };
+
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(1)     // downgrade version
+                        .build()).get();
+
+        // Check the schema has been saved
+        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes())
+                .containsExactly("testSchema");
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("testSchema");
+
+        // Check migrate is success
+        GenericDocument expected = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1 migrated")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_sameVersion() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("To")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).setVersion(3).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setPropertyString("To", "testTo example1")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type with the same version number
+        AppSearchSchema newSchema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // set the new schema to AppSearch
+        Migrator migrator = new Migrator() {
+
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return currentVersion != finalVersion;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Upgrade should not be triggered for this test");
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Downgrade should not be triggered for this test");
+            }
+        };
+
+        // SetSchema with forceOverride=false
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(3)     // same version
+                        .build()).get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+
+        // SetSchema with forceOverride=true
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(3)     // same version
+                        .setForceOverride(true).build()).get();
+
+        assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
+
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes())
+                .containsExactly("testSchema");
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+
+    }
+
+    @Test
+    public void testSchemaMigration_noMigration() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("To")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).setVersion(2).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyString("subject", "testPut example1")
+                .setPropertyString("To", "testTo example1")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc1).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create new schema type and upgrade the version number
+        AppSearchSchema newSchema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+
+        // Set start version to be 3 means we won't trigger migration for 2.
+        Migrator migrator = new Migrator() {
+
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return currentVersion > 2 && currentVersion != finalVersion;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Upgrade should not be triggered for this test");
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                throw new IllegalStateException("Downgrade should not be triggered for this test");
+            }
+        };
+
+        // SetSchema with forceOverride=false
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(newSchema)
+                        .setMigrator("testSchema", migrator)
+                        .setVersion(4)     // upgrade version
+                        .build()).get());
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+    }
+
+
+    @Test
+    public void testSchemaMigration_sourceToNowhere() throws Exception {
+        // set the source schema to AppSearch
+        AppSearchSchema schema = new AppSearchSchema.Builder("sourceSchema")
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(schema).setForceOverride(true).build()).get();
+
+        // save a doc to the source type
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id1", "sourceSchema")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        Migrator migrator_sourceToNowhere = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>(
+                        "zombieNamespace", "zombieId", "nonExistSchema")
+                        .build();
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // SetSchema with forceOverride=false
+        // Source type exist, destination type doesn't exist.
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(new SetSchemaRequest.Builder()
+                        .setMigrator("sourceSchema", migrator_sourceToNowhere)
+                        .setVersion(2).build())   // upgrade version
+                        .get());
+        assertThat(exception).hasMessageThat().contains(
+                "Receive a migrated document with schema type: nonExistSchema. "
+                        + "But the schema types doesn't exist in the request");
+
+        // SetSchema with forceOverride=true
+        // Source type exist, destination type doesn't exist.
+        exception = assertThrows(ExecutionException.class,
+                () -> mDb.setSchema(new SetSchemaRequest.Builder()
+                        .setMigrator("sourceSchema", migrator_sourceToNowhere)
+                        .setForceOverride(true)
+                        .setVersion(2).build())   // upgrade version
+                        .get());
+        assertThat(exception).hasMessageThat().contains(
+                "Receive a migrated document with schema type: nonExistSchema. "
+                        + "But the schema types doesn't exist in the request");
+    }
+
+    @Test
+    public void testSchemaMigration_nowhereToDestination() throws Exception {
+        // set the destination schema to AppSearch
+        AppSearchSchema destinationSchema =
+                new AppSearchSchema.Builder("destinationSchema").build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(destinationSchema).setForceOverride(true).build()).get();
+
+        Migrator migrator_nowhereToDestination = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+
+        // Source type doesn't exist, destination type exist. Since source type doesn't exist,
+        // no matter force override or not, the migrator won't be invoked
+        // SetSchema with forceOverride=false
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
+                        .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+                        .setVersion(2) //  upgrade version
+                        .build()).get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+
+        // SetSchema with forceOverride=true
+        setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
+                        .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+                        .setVersion(2) //  upgrade version
+                        .setForceOverride(true).build()).get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaMigration_nowhereToNowhere() throws Exception {
+        // set empty schema
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .setForceOverride(true).build()).get();
+        Migrator migrator_nowhereToNowhere = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+
+        // Source type doesn't exist, destination type exist. Since source type doesn't exist,
+        // no matter force override or not, the migrator won't be invoked
+        // SetSchema with forceOverride=false
+        SetSchemaResponse setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder()
+                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                        .setVersion(2)  //  upgrade version
+                        .build()).get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+
+        // SetSchema with forceOverride=true
+        setSchemaResponse =
+                mDb.setSchema(new SetSchemaRequest.Builder()
+                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                        .setVersion(2) //  upgrade version
+                        .setForceOverride(true).build()).get();
+        assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaMigration_toAnotherType() throws Exception {
+        // set the source schema to AppSearch
+        AppSearchSchema sourceSchema = new AppSearchSchema.Builder("sourceSchema")
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(sourceSchema).setForceOverride(true).build()).get();
+
+        // save a doc to the source type
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id1", "sourceSchema").build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create the destination type and migrator
+        AppSearchSchema destinationSchema = new AppSearchSchema.Builder("destinationSchema")
+                .build();
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return new GenericDocument.Builder<>("namespace",
+                        document.getId(),
+                        "destinationSchema")
+                        .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                        .build();
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // SetSchema with forceOverride=false and increase overall version
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(destinationSchema)
+                .setMigrator("sourceSchema", migrator)
+                .setForceOverride(false)
+                .setVersion(2) //  upgrade version
+                .build()).get();
+        assertThat(setSchemaResponse.getDeletedTypes())
+                .containsExactly("sourceSchema");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("sourceSchema");
+
+        // Check successfully migrate the doc to the destination type
+        GenericDocument expected = new GenericDocument.Builder<>(
+                "namespace", "id1", "destinationSchema")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_toMultipleDestinationType() throws Exception {
+        // set the source schema to AppSearch
+        AppSearchSchema sourceSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("Age")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(sourceSchema).setForceOverride(true).build()).get();
+
+        // save a child and an adult to the Person type
+        GenericDocument childDoc = new GenericDocument.Builder<>(
+                "namespace", "Person1", "Person")
+                .setPropertyLong("Age", 6).build();
+        GenericDocument adultDoc = new GenericDocument.Builder<>(
+                "namespace", "Person2", "Person")
+                .setPropertyLong("Age", 36).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(childDoc, adultDoc).build()));
+        assertThat(result.getSuccesses()).containsExactly("Person1", null, "Person2", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // create the migrator
+        Migrator migrator = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                if (document.getPropertyLong("Age") < 21) {
+                    return new GenericDocument.Builder<>(
+                            "namespace", "child-id", "Child")
+                            .setPropertyLong("Age", document.getPropertyLong("Age"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                } else {
+                    return new GenericDocument.Builder<>(
+                            "namespace", "adult-id", "Adult")
+                            .setPropertyLong("Age", document.getPropertyLong("Age"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                }
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // create adult and child schema
+        AppSearchSchema adultSchema = new AppSearchSchema.Builder("Adult")
+                .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("Age")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        AppSearchSchema childSchema = new AppSearchSchema.Builder("Child")
+                .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("Age")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+
+        // SetSchema with forceOverride=false and increase overall version
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(adultSchema, childSchema)
+                .setMigrator("Person", migrator)
+                .setForceOverride(false)
+                .setVersion(2) //  upgrade version
+                .build()).get();
+        assertThat(setSchemaResponse.getDeletedTypes())
+                .containsExactly("Person");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("Person");
+
+        // Check successfully migrate the child doc
+        GenericDocument expectedInChild = new GenericDocument.Builder<>(
+                "namespace", "child-id", "Child")
+                .setPropertyLong("Age", 6)
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "child-id"))
+                .containsExactly(expectedInChild);
+
+        // Check successfully migrate the adult doc
+        GenericDocument expectedInAdult = new GenericDocument.Builder<>(
+                "namespace", "adult-id", "Adult")
+                .setPropertyLong("Age", 36)
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "adult-id"))
+                .containsExactly(expectedInAdult);
+    }
+
+    @Test
+    public void testSchemaMigration_loadTest() throws Exception {
+        // set the two source type A & B to AppSearch
+        AppSearchSchema sourceSchemaA = new AppSearchSchema.Builder("schemaA")
+                .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("num")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        AppSearchSchema sourceSchemaB = new AppSearchSchema.Builder("schemaB")
+                .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("num")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(sourceSchemaA, sourceSchemaB).setForceOverride(true).build()).get();
+
+        // save 100 docs to each type
+        PutDocumentsRequest.Builder putRequestBuilder = new PutDocumentsRequest.Builder();
+        for (int i = 0; i < 100; i++) {
+            GenericDocument docInA = new GenericDocument.Builder<>(
+                    "namespace", "idA-" + i, "schemaA")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            GenericDocument docInB = new GenericDocument.Builder<>(
+                    "namespace", "idB-" + i, "schemaB")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            putRequestBuilder.addGenericDocuments(docInA, docInB);
+        }
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                putRequestBuilder.build()));
+        assertThat(result.getFailures()).isEmpty();
+
+        // create three destination types B, C & D
+        AppSearchSchema destinationSchemaB = new AppSearchSchema.Builder("schemaB")
+                .addProperty(
+                        new AppSearchSchema.Int64PropertyConfig.Builder("numNewProperty")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        AppSearchSchema destinationSchemaC = new AppSearchSchema.Builder("schemaC")
+                .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("num")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+        AppSearchSchema destinationSchemaD = new AppSearchSchema.Builder("schemaD")
+                .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("num")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .build())
+                .build();
+
+        // Create an active migrator for type A which will migrate first 50 docs to C and second
+        // 50 docs to D
+        Migrator migratorA = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                if (document.getPropertyLong("num") < 50) {
+                    return new GenericDocument.Builder<>("namespace",
+                            document.getId() + "-destC", "schemaC")
+                            .setPropertyLong("num", document.getPropertyLong("num"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                } else {
+                    return new GenericDocument.Builder<>("namespace",
+                            document.getId() + "-destD", "schemaD")
+                            .setPropertyLong("num", document.getPropertyLong("num"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                }
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // Create an active migrator for type B which will migrate first 50 docs to B and second
+        // 50 docs to D
+        Migrator migratorB = new Migrator() {
+            @Override
+            public boolean shouldMigrate(int currentVersion, int finalVersion) {
+                return true;
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                if (document.getPropertyLong("num") < 50) {
+                    return new GenericDocument.Builder<>("namespace",
+                            document.getId() + "-destB", "schemaB")
+                            .setPropertyLong("numNewProperty",
+                                    document.getPropertyLong("num"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                } else {
+                    return new GenericDocument.Builder<>("namespace",
+                            document.getId() + "-destD", "schemaD")
+                            .setPropertyLong("num", document.getPropertyLong("num"))
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                            .build();
+                }
+            }
+
+            @NonNull
+            @Override
+            public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                    @NonNull GenericDocument document) {
+                return document;
+            }
+        };
+
+        // SetSchema with forceOverride=false and increase overall version
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(destinationSchemaB, destinationSchemaC, destinationSchemaD)
+                .setMigrator("schemaA", migratorA)
+                .setMigrator("schemaB", migratorB)
+                .setForceOverride(false)
+                .setVersion(2)    // upgrade version
+                .build()).get();
+        assertThat(setSchemaResponse.getDeletedTypes())
+                .containsExactly("schemaA");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("schemaB");
+        assertThat(setSchemaResponse.getMigratedTypes())
+                .containsExactly("schemaA", "schemaB");
+
+        // generate expected documents
+        List<GenericDocument> expectedDocs = new ArrayList<>();
+        for (int i = 0; i < 50; i++) {
+            GenericDocument docAToC = new GenericDocument.Builder<>(
+                    "namespace", "idA-" + i + "-destC", "schemaC")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            GenericDocument docBToB = new GenericDocument.Builder<>(
+                    "namespace", "idB-" + i + "-destB", "schemaB")
+                    .setPropertyLong("numNewProperty", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            expectedDocs.add(docAToC);
+            expectedDocs.add(docBToB);
+        }
+
+        for (int i = 50; i < 100; i++) {
+            GenericDocument docAToD = new GenericDocument.Builder<>(
+                    "namespace", "idA-" + i + "-destD", "schemaD")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            GenericDocument docBToD = new GenericDocument.Builder<>(
+                    "namespace", "idB-" + i + "-destD", "schemaD")
+                    .setPropertyLong("num", i)
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+            expectedDocs.add(docAToD);
+            expectedDocs.add(docBToD);
+        }
+        //query all documents and compare
+        SearchResults searchResults = mDb.search("", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactlyElementsIn(expectedDocs);
+    }
+
+    //*************************** Multi-step migration tests   ******************************
+    // Version structure and how version bumps:
+    // Version 1: Start - typeA docs contains "subject" property.
+    // Version 2: typeA docs get new "body" property, contains "subject" and "body" now.
+    // Version 3: typeA docs is migrated to typeB, typeA docs got removed, typeB doc contains
+    //            "subject" and "body" property.
+    // Version 4: typeB docs remove "subject" property, contains only "body" now.
+
+    // Create a multi-step migrator for A, which could migrate version 1-3 to 4.
+    private static final Migrator MULTI_STEP_MIGRATOR_A = new Migrator() {
+        @Override
+        public boolean shouldMigrate(int currentVersion, int finalVersion) {
+            return currentVersion < 3;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            GenericDocument.Builder docBuilder =
+                    new GenericDocument.Builder<>("namespace", "id", "TypeB")
+                            .setCreationTimestampMillis(DOCUMENT_CREATION_TIME);
+            if (currentVersion == 2) {
+                docBuilder.setPropertyString("body", document.getPropertyString("body"));
+            } else {
+                docBuilder.setPropertyString("body",
+                        "new content for the newly added 'body' property");
+            }
+            return docBuilder.build();
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+    };
+
+    // create a multi-step migrator for B, which could migrate version 1-3 to 4.
+    private static final Migrator MULTI_STEP_MIGRATOR_B = new Migrator() {
+        @Override
+        public boolean shouldMigrate(int currentVersion, int finalVersion) {
+            return currentVersion == 3;
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onUpgrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return new GenericDocument.Builder<>("namespace", "id", "TypeB")
+                    .setPropertyString("body", document.getPropertyString("body"))
+                    .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                    .build();
+        }
+
+        @NonNull
+        @Override
+        public GenericDocument onDowngrade(int currentVersion, int finalVersion,
+                @NonNull GenericDocument document) {
+            return document;
+        }
+    };
+
+    // create a setSchemaRequest, which could migrate version 1-3 to 4.
+    private static final SetSchemaRequest MULTI_STEP_REQUEST = new SetSchemaRequest.Builder()
+            .addSchemas(new AppSearchSchema.Builder("TypeB")
+                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
+                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                            .setIndexingType(
+                                    AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                            .setTokenizerType(
+                                    AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                            .build())
+                    .build())
+            .setMigrator("TypeA", MULTI_STEP_MIGRATOR_A)
+            .setMigrator("TypeB", MULTI_STEP_MIGRATOR_B)
+            .setVersion(4)
+            .build();
+
+    @Test
+    public void testSchemaMigration_multiStep1To4() throws Exception {
+        // set version 1 to the database, only contain TypeA
+        AppSearchSchema typeA = new AppSearchSchema.Builder("TypeA")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(typeA).setForceOverride(true).setVersion(1).build()).get();
+
+        // save a doc to version 1.
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeA")
+                .setPropertyString("subject", "subject")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // update to version 4.
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
+
+        // Create expected doc. Since we started at version 1 and migrated to version 4:
+        // 1: A 'body' property should have been added with "new content for the newly added 'body'
+        //    property"
+        // 2: The type should have been changed from 'TypeA' to 'TypeB'
+        // 3: The 'subject' property should have been removed
+        GenericDocument expected = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeB")
+                .setPropertyString("body", "new content for the newly added 'body' property")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_multiStep2To4() throws Exception {
+        // set version 2 to the database, only contain TypeA with a new property
+        AppSearchSchema typeA = new AppSearchSchema.Builder("TypeA")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(typeA).setForceOverride(true).setVersion(2).build()).get();
+
+        // save a doc to version 2.
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeA")
+                .setPropertyString("subject", "subject")
+                .setPropertyString("body", "bodyFromA")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // update to version 4.
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
+
+        // create expected doc, body exists in type A of version 2
+        GenericDocument expected = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeB")
+                .setPropertyString("body", "bodyFromA")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected);
+    }
+
+    @Test
+    public void testSchemaMigration_multiStep3To4() throws Exception {
+        // set version 3 to the database, only contain TypeB
+        AppSearchSchema typeA = new AppSearchSchema.Builder("TypeB")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .build();
+        mDb.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(typeA).setForceOverride(true).setVersion(3).build()).get();
+
+        // save a doc to version 2.
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeB")
+                .setPropertyString("subject", "subject")
+                .setPropertyString("body", "bodyFromB")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME).build();
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+        assertThat(result.getSuccesses()).containsExactly("id", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // update to version 4.
+        SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
+        assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
+        assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("TypeB");
+        assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeB");
+
+        // create expected doc, body exists in type A of version 3
+        GenericDocument expected = new GenericDocument.Builder<>(
+                "namespace", "id", "TypeB")
+                .setPropertyString("body", "bodyFromB")
+                .setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
+                .build();
+        assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaMigrationLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaMigrationLocalCtsTest.java
new file mode 100644
index 0000000..c2ab67f
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaMigrationLocalCtsTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app.cts;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.localstorage.LocalStorage;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+public class AppSearchSchemaMigrationLocalCtsTest extends AppSearchSchemaMigrationCtsTestBase{
+    @Override
+    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+        Context context = ApplicationProvider.getApplicationContext();
+        return LocalStorage.createSearchSession(
+                new LocalStorage.SearchContext.Builder(context, dbName).build());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
index be85f72..a15e8f15 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
@@ -16,9 +16,11 @@
 
 package androidx.appsearch.app.cts;
 
+import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_SCHEMA;
 import static androidx.appsearch.app.util.AppSearchTestUtils.checkIsBatchResultSuccess;
 import static androidx.appsearch.app.util.AppSearchTestUtils.convertSearchResultsToDocuments;
 import static androidx.appsearch.app.util.AppSearchTestUtils.doGet;
+import static androidx.appsearch.app.util.AppSearchTestUtils.retrieveAllSearchResults;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -32,20 +34,24 @@
 import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.app.GetByUriRequest;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.PutDocumentsRequest;
-import androidx.appsearch.app.RemoveByUriRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.app.cts.customer.EmailDataClass;
+import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.app.cts.customer.EmailDocument;
 import androidx.appsearch.exceptions.AppSearchException;
-import androidx.appsearch.localstorage.LocalStorage;
 import androidx.test.core.app.ApplicationProvider;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 
@@ -62,11 +68,12 @@
 import java.util.concurrent.ExecutorService;
 
 public abstract class AppSearchSessionCtsTestBase {
-    private AppSearchSession mDb1;
-    private static final String DB_NAME_1 = LocalStorage.DEFAULT_DATABASE_NAME;
-    private AppSearchSession mDb2;
+    private static final String DB_NAME_1 = "";
     private static final String DB_NAME_2 = "testDb2";
 
+    private AppSearchSession mDb1;
+    private AppSearchSession mDb2;
+
     protected abstract ListenableFuture<AppSearchSession> createSearchSession(
             @NonNull String dbName);
 
@@ -92,35 +99,124 @@
     }
 
     private void cleanup() throws Exception {
-        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
     }
 
     @Test
     public void testSetSchema() throws Exception {
         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                ).addProperty(new StringPropertyConfig.Builder("body")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+    }
+
+    @Test
+    public void testSetSchema_Failure() throws Exception {
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        AppSearchSchema emailSchema1 = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
+                .build();
+
+        Throwable throwable = assertThrows(ExecutionException.class,
+                () -> mDb1.setSchema(new SetSchemaRequest.Builder()
+                        .addSchemas(emailSchema1).build()).get()).getCause();
+        assertThat(throwable).isInstanceOf(AppSearchException.class);
+        AppSearchException exception = (AppSearchException) throwable;
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+        assertThat(exception).hasMessageThat().contains("Incompatible types: {builtin:Email}");
+
+        throwable = assertThrows(ExecutionException.class,
+                () -> mDb1.setSchema(new SetSchemaRequest.Builder().build()).get()).getCause();
+
+        assertThat(throwable).isInstanceOf(AppSearchException.class);
+        exception = (AppSearchException) throwable;
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+        assertThat(exception).hasMessageThat().contains("Deleted types: {builtin:Email}");
+    }
+
+    @Test
+    public void testSetSchema_updateVersion() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+                .setVersion(1).build()).get();
+
+        Set<AppSearchSchema> actualSchemaTypes = mDb1.getSchema().get().getSchemas();
+        assertThat(actualSchemaTypes).containsExactly(schema);
+
+        // increase version number
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+                .setVersion(2).build()).get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(2);
+    }
+
+    @Test
+    public void testSetSchema_checkVersion() throws Exception {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // set different version number to different database.
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+                .setVersion(135).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(schema)
+                .setVersion(246).build()).get();
+
+
+        // check the version has been set correctly.
+        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(135);
+
+        getSchemaResponse = mDb2.getSchema().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema);
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(246);
     }
 
 // @exportToFramework:startStrip()
 
     @Test
-    public void testSetSchema_dataClass() throws Exception {
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addDataClass(EmailDataClass.class).build()).get();
+    public void testSetSchema_addDocumentClasses() throws Exception {
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addDocumentClasses(EmailDocument.class).build()).get();
     }
 // @exportToFramework:endStrip()
 
@@ -129,87 +225,163 @@
     @Test
     public void testGetSchema() throws Exception {
         AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                ).addProperty(new StringPropertyConfig.Builder("body")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
         AppSearchSchema emailSchema2 = new AppSearchSchema.Builder("Email2")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Diff
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                ).addProperty(new StringPropertyConfig.Builder("body")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Diff
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
 
         SetSchemaRequest request1 = new SetSchemaRequest.Builder()
-                .addSchema(emailSchema1).addDataClass(EmailDataClass.class).build();
+                .addSchemas(emailSchema1).addDocumentClasses(EmailDocument.class).build();
         SetSchemaRequest request2 = new SetSchemaRequest.Builder()
-                .addSchema(emailSchema2).addDataClass(EmailDataClass.class).build();
+                .addSchemas(emailSchema2).addDocumentClasses(EmailDocument.class).build();
 
         mDb1.setSchema(request1).get();
         mDb2.setSchema(request2).get();
 
-        Set<AppSearchSchema> actual1 = mDb1.getSchema().get();
-        Set<AppSearchSchema> actual2 = mDb2.getSchema().get();
-
+        Set<AppSearchSchema> actual1 = mDb1.getSchema().get().getSchemas();
+        assertThat(actual1).hasSize(2);
         assertThat(actual1).isEqualTo(request1.getSchemas());
+        Set<AppSearchSchema> actual2 = mDb2.getSchema().get().getSchemas();
+        assertThat(actual2).hasSize(2);
         assertThat(actual2).isEqualTo(request2.getSchemas());
     }
 // @exportToFramework:endStrip()
 
     @Test
+    public void testGetNamespaces() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        assertThat(mDb1.getNamespaces().get()).isEmpty();
+
+        // Index a document
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(new AppSearchEmail.Builder("namespace1", "id1").build())
+                .build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1");
+
+        // Index additional data
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(
+                        new AppSearchEmail.Builder("namespace2", "id1").build(),
+                        new AppSearchEmail.Builder("namespace2", "id2").build(),
+                        new AppSearchEmail.Builder("namespace3", "id1").build())
+                .build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly(
+                "namespace1", "namespace2", "namespace3");
+
+        // Remove namespace2/id2 -- namespace2 should still exist because of namespace2/id1
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
+                        "id2").build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly(
+                "namespace1", "namespace2", "namespace3");
+
+        // Remove namespace2/id1 -- namespace2 should now be gone
+        checkIsBatchResultSuccess(
+                mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace2").addIds(
+                        "id1").build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1", "namespace3");
+
+        // Make sure the list of namespaces is preserved after restart
+        mDb1.close();
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1", "namespace3");
+    }
+
+    @Test
+    public void testGetNamespaces_dbIsolation() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        assertThat(mDb1.getNamespaces().get()).isEmpty();
+        assertThat(mDb2.getNamespaces().get()).isEmpty();
+
+        // Index documents
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(new AppSearchEmail.Builder("namespace1_db1", "id1").build())
+                .build()));
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(new AppSearchEmail.Builder("namespace2_db1", "id1").build())
+                .build()));
+        checkIsBatchResultSuccess(mDb2.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(new AppSearchEmail.Builder("namespace_db2", "id1").build())
+                .build()));
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1_db1", "namespace2_db1");
+        assertThat(mDb2.getNamespaces().get()).containsExactly("namespace_db2");
+
+        // Make sure the list of namespaces is preserved after restart
+        mDb1.close();
+        mDb1 = createSearchSession(DB_NAME_1).get();
+        assertThat(mDb1.getNamespaces().get()).containsExactly("namespace1_db1", "namespace2_db1");
+        assertThat(mDb2.getNamespaces().get()).containsExactly("namespace_db2");
+    }
+
+    @Test
+    public void testGetSchema_emptyDB() throws Exception {
+        GetSchemaResponse getSchemaResponse = mDb1.getSchema().get();
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(0);
+    }
+
+    @Test
     public void testPutDocuments() throws Exception {
         // Schema registration
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
-        AppSearchEmail email = new AppSearchEmail.Builder("uri1")
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
                 .setFrom("[email protected]")
                 .setTo("[email protected]", "[email protected]")
                 .setSubject("testPut example")
                 .setBody("This is the body of the testPut email")
                 .build();
 
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email).build()));
-        assertThat(result.getSuccesses()).containsExactly("uri1", null);
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
     }
 
 // @exportToFramework:startStrip()
 
     @Test
-    public void testPutDocuments_dataClass() throws Exception {
+    public void testPut_addDocumentClasses() throws Exception {
         // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addDataClass(EmailDataClass.class).build()).get();
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addDocumentClasses(EmailDocument.class).build()).get();
 
         // Index a document
-        EmailDataClass email = new EmailDataClass();
-        email.uri = "uri1";
+        EmailDocument email = new EmailDocument();
+        email.namespace = "namespace";
+        email.id = "id1";
         email.subject = "testPut example";
         email.body = "This is the body of the testPut email";
 
-        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addDataClass(email).build()));
-        assertThat(result.getSuccesses()).containsExactly("uri1", null);
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addDocuments(email).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
     }
 // @exportToFramework:endStrip()
@@ -218,88 +390,83 @@
     public void testUpdateSchema() throws Exception {
         // Schema registration
         AppSearchSchema oldEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
         AppSearchSchema newEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("body")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
         AppSearchSchema giftSchema = new AppSearchSchema.Builder("Gift")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-                        .setDataType(PropertyConfig.DATA_TYPE_INT64)
+                .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("price")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_NONE)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_NONE)
                         .build())
                 .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(oldEmailSchema).build()).get();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(oldEmailSchema).build()).get();
 
         // Try to index a gift. This should fail as it's not in the schema.
         GenericDocument gift =
-                new GenericDocument.Builder<>("gift1", "Gift").setPropertyLong("price", 5).build();
+                new GenericDocument.Builder<>("namespace", "gift1", "Gift").setPropertyLong("price",
+                        5).build();
         AppSearchBatchResult<String, Void> result =
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(gift).build()).get();
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()).get();
         assertThat(result.isSuccess()).isFalse();
         assertThat(result.getFailures().get("gift1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
         // Update the schema to include the gift and update email with a new field
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(newEmailSchema, giftSchema).build()).get();
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(newEmailSchema, giftSchema).build()).get();
 
         // Try to index the document again, which should now work
         checkIsBatchResultSuccess(
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(gift).build()));
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(gift).build()));
 
         // Indexing an email with a body should also work
-        AppSearchEmail email = new AppSearchEmail.Builder("email1")
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "email1")
                 .setSubject("testPut example")
                 .setBody("This is the body of the testPut email")
                 .build();
         checkIsBatchResultSuccess(
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email).build()));
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
     }
 
     @Test
     public void testRemoveSchema() throws Exception {
         // Schema registration
         AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
 
         // Index an email and check it present.
-        AppSearchEmail email = new AppSearchEmail.Builder("email1")
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "email1")
                 .setSubject("testPut example")
                 .build();
         checkIsBatchResultSuccess(
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email).build()));
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
         List<GenericDocument> outDocuments =
-                doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "email1");
+                doGet(mDb1, "namespace", "email1");
         assertThat(outDocuments).hasSize(1);
         AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
         assertThat(outEmail).isEqualTo(email);
@@ -311,28 +478,27 @@
         assertThat(failResult1).isInstanceOf(AppSearchException.class);
         assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
         assertThat(failResult1).hasMessageThat().contains(
-                "Deleted types: [androidx.appsearch.test$" + DB_NAME_1 + "/builtin:Email]");
+                "Deleted types: {builtin:Email}");
 
         // Try to remove the email schema again, which should now work as we set forceOverride to
         // be true.
         mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
 
         // Make sure the indexed email is gone.
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder()
-                        .setNamespace(GenericDocument.DEFAULT_NAMESPACE)
-                        .addUri("email1")
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace")
+                        .addIds("email1")
                         .build()).get();
         assertThat(getResult.isSuccess()).isFalse();
         assertThat(getResult.getFailures().get("email1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
         // Try to index an email again. This should fail as the schema has been removed.
-        AppSearchEmail email2 = new AppSearchEmail.Builder("email2")
+        AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2")
                 .setSubject("testPut example")
                 .build();
-        AppSearchBatchResult<String, Void> failResult2 = mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email2).build()).get();
+        AppSearchBatchResult<String, Void> failResult2 = mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()).get();
         assertThat(failResult2.isSuccess()).isFalse();
         assertThat(failResult2.getFailures().get("email2").getErrorMessage())
                 .isEqualTo("Schema type config 'androidx.appsearch.test$" + DB_NAME_1
@@ -343,37 +509,36 @@
     public void testRemoveSchema_twoDatabases() throws Exception {
         // Schema registration in mDb1 and mDb2
         AppSearchSchema emailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build())
                 .build();
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().addSchema(emailSchema).build()).get();
+        mDb1.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder().addSchemas(emailSchema).build()).get();
 
         // Index an email and check it present in database1.
-        AppSearchEmail email1 = new AppSearchEmail.Builder("email1")
+        AppSearchEmail email1 = new AppSearchEmail.Builder("namespace", "email1")
                 .setSubject("testPut example")
                 .build();
         checkIsBatchResultSuccess(
-                mDb1.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
+                mDb1.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
         List<GenericDocument> outDocuments =
-                doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "email1");
+                doGet(mDb1, "namespace", "email1");
         assertThat(outDocuments).hasSize(1);
         AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
         assertThat(outEmail).isEqualTo(email1);
 
         // Index an email and check it present in database2.
-        AppSearchEmail email2 = new AppSearchEmail.Builder("email2")
+        AppSearchEmail email2 = new AppSearchEmail.Builder("namespace", "email2")
                 .setSubject("testPut example")
                 .build();
         checkIsBatchResultSuccess(
-                mDb2.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
-        outDocuments = doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "email2");
+                mDb2.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
+        outDocuments = doGet(mDb2, "namespace", "email2");
         assertThat(outDocuments).hasSize(1);
         outEmail = new AppSearchEmail(outDocuments.get(0));
         assertThat(outEmail).isEqualTo(email2);
@@ -386,119 +551,436 @@
         assertThat(failResult1).isInstanceOf(AppSearchException.class);
         assertThat(failResult1).hasMessageThat().contains("Schema is incompatible");
         assertThat(failResult1).hasMessageThat().contains(
-                "Deleted types: [androidx.appsearch.test$" + DB_NAME_1 + "/builtin:Email]");
+                "Deleted types: {builtin:Email}");
 
         // Try to remove the email schema again, which should now work as we set forceOverride to
         // be true.
         mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
 
         // Make sure the indexed email is gone in database 1.
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().setNamespace(GenericDocument.DEFAULT_NAMESPACE)
-                        .addUri("email1").build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace")
+                        .addIds("email1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
         assertThat(getResult.getFailures().get("email1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
         // Try to index an email again. This should fail as the schema has been removed.
-        AppSearchEmail email3 = new AppSearchEmail.Builder("email3")
+        AppSearchEmail email3 = new AppSearchEmail.Builder("namespace", "email3")
                 .setSubject("testPut example")
                 .build();
-        AppSearchBatchResult<String, Void> failResult2 = mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email3).build()).get();
+        AppSearchBatchResult<String, Void> failResult2 = mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email3).build()).get();
         assertThat(failResult2.isSuccess()).isFalse();
         assertThat(failResult2.getFailures().get("email3").getErrorMessage())
                 .isEqualTo("Schema type config 'androidx.appsearch.test$" + DB_NAME_1
                         + "/builtin:Email' not found");
 
         // Make sure email in database 2 still present.
-        outDocuments = doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "email2");
+        outDocuments = doGet(mDb2, "namespace", "email2");
         assertThat(outDocuments).hasSize(1);
         outEmail = new AppSearchEmail(outDocuments.get(0));
         assertThat(outEmail).isEqualTo(email2);
 
         // Make sure email could still be indexed in database 2.
         checkIsBatchResultSuccess(
-                mDb2.putDocuments(
-                        new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
+                mDb2.put(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
     }
 
     @Test
     public void testGetDocuments() throws Exception {
         // Schema registration
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
         AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Get the document
-        List<GenericDocument> outDocuments = doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1");
+        List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "id1");
         assertThat(outDocuments).hasSize(1);
         AppSearchEmail outEmail = new AppSearchEmail(outDocuments.get(0));
         assertThat(outEmail).isEqualTo(inEmail);
 
         // Can't get the document in the other instance.
-        AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
+        AppSearchBatchResult<String, GenericDocument> failResult = mDb2.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds(
+                        "id1").build()).get();
         assertThat(failResult.isSuccess()).isFalse();
-        assertThat(failResult.getFailures().get("uri1").getResultCode())
+        assertThat(failResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
     }
 
 // @exportToFramework:startStrip()
 
     @Test
-    public void testGetDocuments_dataClass() throws Exception {
+    public void testGet_addDocumentClasses() throws Exception {
         // Schema registration
-        mDb1.setSchema(
-                new SetSchemaRequest.Builder().addDataClass(EmailDataClass.class).build()).get();
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addDocumentClasses(EmailDocument.class).build()).get();
 
         // Index a document
-        EmailDataClass inEmail = new EmailDataClass();
-        inEmail.uri = "uri1";
+        EmailDocument inEmail = new EmailDocument();
+        inEmail.namespace = "namespace";
+        inEmail.id = "id1";
         inEmail.subject = "testPut example";
         inEmail.body = "This is the body of the testPut inEmail";
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addDataClass(inEmail).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addDocuments(inEmail).build()));
 
         // Get the document
-        List<GenericDocument> outDocuments = doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1");
+        List<GenericDocument> outDocuments = doGet(mDb1, "namespace", "id1");
         assertThat(outDocuments).hasSize(1);
-        EmailDataClass outEmail = outDocuments.get(0).toDataClass(EmailDataClass.class);
-        assertThat(inEmail.uri).isEqualTo(outEmail.uri);
+        EmailDocument outEmail = outDocuments.get(0).toDocumentClass(EmailDocument.class);
+        assertThat(inEmail.id).isEqualTo(outEmail.id);
         assertThat(inEmail.subject).isEqualTo(outEmail.subject);
         assertThat(inEmail.body).isEqualTo(outEmail.body);
     }
 // @exportToFramework:endStrip()
 
+
     @Test
-    public void testQuery() throws Exception {
+    public void testGetDocuments_projection() throws Exception {
         // Schema registration
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
 
-        // Index a document
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1", "id2")
+                .addProjection(
+                        AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_projectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace").addIds(
+                "id1",
+                "id2").addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList()).build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_projectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1", "id2")
+                .addProjection("NonExistentType", Collections.emptyList())
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_wildcardProjection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1", "id2")
+                .addProjection(
+                        GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "to"))
+                .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_wildcardProjectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace").addIds(
+                "id1",
+                "id2").addProjection(GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                Collections.emptyList()).build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGetDocuments_wildcardProjectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Get with type property paths {"Email", ["subject", "to"]}
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1", "id2")
+                .addProjection("NonExistentType", Collections.emptyList())
+                .addProjection(
+                        GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "to"))
+                .build();
+        List<GenericDocument> outDocuments = doGet(mDb1, request);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(outDocuments).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testQuery() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Query for the document
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
@@ -506,7 +988,7 @@
         assertThat(documents.get(0)).isEqualTo(inEmail);
 
         // Multi-term query
-        searchResults = mDb1.query("body email", new SearchSpec.Builder()
+        searchResults = mDb1.search("body email", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
@@ -518,25 +1000,25 @@
     public void testQuery_getNextPage() throws Exception {
         // Schema registration
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
         Set<AppSearchEmail> emailSet = new HashSet<>();
         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
         // Index 31 documents
         for (int i = 0; i < 31; i++) {
             AppSearchEmail inEmail =
-                    new AppSearchEmail.Builder("uri" + i)
+                    new AppSearchEmail.Builder("namespace", "id" + i)
                             .setFrom("[email protected]")
                             .setTo("[email protected]", "[email protected]")
                             .setSubject("testPut example")
                             .setBody("This is the body of the testPut email")
                             .build();
             emailSet.add(inEmail);
-            putDocumentsRequestBuilder.addGenericDocument(inEmail);
+            putDocumentsRequestBuilder.addGenericDocuments(inEmail);
         }
-        checkIsBatchResultSuccess(mDb1.putDocuments(putDocumentsRequestBuilder.build()));
+        checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
 
         // Set number of results per page is 7.
-        SearchResults searchResults = mDb1.query("body",
+        SearchResults searchResults = mDb1.search("body",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .setResultCountPerPage(7)
@@ -551,7 +1033,7 @@
             results = searchResults.getNextPage().get();
             ++pageNumber;
             for (SearchResult result : results) {
-                documents.add(result.getDocument());
+                documents.add(result.getGenericDocument());
             }
         } while (results.size() > 0);
 
@@ -565,13 +1047,12 @@
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
+                        .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
 
         // Index two documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
@@ -579,73 +1060,81 @@
                         .setBody("A little lamb, little lamb")
                         .build();
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("I'm a little teapot")
                         .setBody("short and stout. Here is my handle, here is my spout.")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email1, email2).build()));
+                        .addGenericDocuments(email1, email2).build()));
 
         // Query for "little". It should match both emails.
-        SearchResults searchResults = mDb1.query("little", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("little", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
                 .build());
-        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
 
         // The email1 should be ranked higher because 'little' appears three times in email1 and
         // only once in email2.
-        assertThat(documents).containsExactly(email1, email2).inOrder();
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(0).getRankingSignal()).isGreaterThan(
+                results.get(1).getRankingSignal());
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
 
         // Query for "little OR stout". It should match both emails.
-        searchResults = mDb1.query("little OR stout", new SearchSpec.Builder()
+        searchResults = mDb1.search("little OR stout", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
                 .build());
-        documents = convertSearchResultsToDocuments(searchResults);
+        results = retrieveAllSearchResults(searchResults);
 
         // The email2 should be ranked higher because 'little' appears once and "stout", which is a
         // rarer term, appears once. email1 only has the three 'little' appearances.
-        assertThat(documents).containsExactly(email2, email1).inOrder();
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(0).getRankingSignal()).isGreaterThan(
+                results.get(1).getRankingSignal());
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(1).getRankingSignal()).isGreaterThan(0);
     }
 
     @Test
     public void testQuery_typeFilter() throws Exception {
         // Schema registration
         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
-                .addProperty(new PropertyConfig.Builder("foo")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("foo")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .build()
                 ).build();
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(genericSchema)
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(genericSchema)
                         .build()).get();
 
         // Index a document
         AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        GenericDocument inDoc = new GenericDocument.Builder<>("uri2", "Generic")
+        GenericDocument inDoc = new GenericDocument.Builder<>("namespace", "id2", "Generic")
                 .setPropertyString("foo", "body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail, inDoc).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inDoc).build()));
 
         // Query for the documents
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
@@ -653,8 +1142,8 @@
         assertThat(documents).containsExactly(inEmail, inDoc);
 
         // Query only for Document
-        searchResults = mDb1.query("body", new SearchSpec.Builder()
-                .addSchemaType("Generic", "Generic") // duplicate type in filter won't matter.
+        searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .addFilterSchemas("Generic", "Generic") // duplicate type in filter won't matter.
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
@@ -663,33 +1152,66 @@
     }
 
     @Test
+    public void testQuery_packageFilter() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("foo")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+
+        // Query for the document within our package
+        SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterPackageNames(ApplicationProvider.getApplicationContext().getPackageName())
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(email);
+
+        // Query for the document in some other package, which won't exist
+        searchResults = mDb1.search("foo", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterPackageNames("some.other.package")
+                .build());
+        List<SearchResult> results = searchResults.getNextPage().get();
+        assertThat(results).isEmpty();
+    }
+
+    @Test
     public void testQuery_namespaceFilter() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build());
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index two documents
         AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("expectedNamespace")
+                new AppSearchEmail.Builder("expectedNamespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
         AppSearchEmail unexpectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("unexpectedNamespace")
+                new AppSearchEmail.Builder("unexpectedNamespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(expectedEmail, unexpectedEmail).build()));
+                        .addGenericDocuments(expectedEmail, unexpectedEmail).build()));
 
         // Query for all namespaces
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
@@ -697,9 +1219,9 @@
         assertThat(documents).containsExactly(expectedEmail, unexpectedEmail);
 
         // Query only for expectedNamespace
-        searchResults = mDb1.query("body",
+        searchResults = mDb1.search("body",
                 new SearchSpec.Builder()
-                        .addNamespace("expectedNamespace")
+                        .addFilterNamespaces("expectedNamespace")
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .build());
         documents = convertSearchResultsToDocuments(searchResults);
@@ -711,21 +1233,21 @@
     public void testQuery_getPackageName() throws Exception {
         // Schema registration
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
         AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Query for the document
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
 
@@ -735,10 +1257,70 @@
         do {
             results = searchResults.getNextPage().get();
             for (SearchResult result : results) {
-                assertThat(result.getDocument()).isEqualTo(inEmail);
+                assertThat(result.getGenericDocument()).isEqualTo(inEmail);
                 assertThat(result.getPackageName()).isEqualTo(
                         ApplicationProvider.getApplicationContext().getPackageName());
-                documents.add(result.getDocument());
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+        assertThat(documents).hasSize(1);
+    }
+
+    @Test
+    public void testQuery_getDatabaseName() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        List<SearchResult> results;
+        List<GenericDocument> documents = new ArrayList<>();
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            for (SearchResult result : results) {
+                assertThat(result.getGenericDocument()).isEqualTo(inEmail);
+                assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_1);
+                documents.add(result.getGenericDocument());
+            }
+        } while (results.size() > 0);
+        assertThat(documents).hasSize(1);
+
+        // Schema registration for another database
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
+
+        // Query for the document
+        searchResults = mDb2.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        documents = new ArrayList<>();
+        // keep loading next page until it's empty.
+        do {
+            results = searchResults.getNextPage().get();
+            for (SearchResult result : results) {
+                assertThat(result.getGenericDocument()).isEqualTo(inEmail);
+                assertThat(result.getDatabaseName()).isEqualTo(DB_NAME_2);
+                documents.add(result.getGenericDocument());
             }
         } while (results.size() > 0);
         assertThat(documents).hasSize(1);
@@ -749,26 +1331,28 @@
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
 
         // Index two documents
         AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
@@ -776,34 +1360,31 @@
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
+                        .addGenericDocuments(email, note).build()));
 
         // Query with type property paths {"Email", ["body", "to"]}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection(AppSearchEmail.SCHEMA_TYPE, "body", "to")
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
 
         // The email document should have been returned with only the "body" and "to"
         // properties. The note document should have been returned with all of its properties.
         AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setTo("[email protected]", "[email protected]")
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
@@ -815,26 +1396,28 @@
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
 
         // Index two documents
         AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
@@ -842,17 +1425,16 @@
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
+                        .addGenericDocuments(email, note).build()));
 
         // Query with type property paths {"Email", []}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
                 .build());
@@ -861,13 +1443,11 @@
         // The email document should have been returned without any properties. The note document
         // should have been returned with all of its properties.
         AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .build();
         GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
@@ -879,26 +1459,28 @@
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
 
         // Index two documents
         AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
@@ -906,35 +1488,32 @@
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
+                        .addGenericDocuments(email, note).build()));
 
         // Query with type property paths {"NonExistentType", []}, {"Email", ["body", "to"]}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addProjection("NonExistentType", Collections.emptyList())
-                .addProjection(AppSearchEmail.SCHEMA_TYPE, "body", "to")
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body", "to"))
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
 
         // The email document should have been returned with only the "body" and "to" properties.
         // The note document should have been returned with all of its properties.
         AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setTo("[email protected]", "[email protected]")
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
@@ -946,26 +1525,27 @@
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
 
         // Index two documents
         AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
@@ -973,34 +1553,32 @@
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
+                        .addGenericDocuments(email, note).build()));
 
         // Query with type property paths {"*", ["body", "to"]}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, "body", "to")
+                .addProjection(
+                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
 
         // The email document should have been returned with only the "body" and "to"
         // properties. The note document should have been returned with only the "body" property.
         AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setTo("[email protected]", "[email protected]")
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("body", "Note body").build();
         assertThat(documents).containsExactly(expectedNote, expectedEmail);
@@ -1011,26 +1589,25 @@
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN).build())
                                 .build()).build()).get();
 
         // Index two documents
         AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
@@ -1038,17 +1615,16 @@
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
+                        .addGenericDocuments(email, note).build()));
 
         // Query with type property paths {"*", []}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList())
                 .build());
@@ -1056,13 +1632,11 @@
 
         // The email and note documents should have been returned without any properties.
         AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .build();
         GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000).build();
         assertThat(documents).containsExactly(expectedNote, expectedEmail);
     }
@@ -1072,26 +1646,28 @@
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
-                        .addSchema(new AppSearchSchema.Builder("Note")
-                                .addProperty(new PropertyConfig.Builder("title")
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .addProperty(new PropertyConfig.Builder("body")
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
                                         .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
-                                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
                                         .setTokenizerType(
-                                                PropertyConfig.TOKENIZER_TYPE_PLAIN).build())
-                                .build()).build()).get();
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
 
         // Index two documents
         AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
@@ -1099,35 +1675,33 @@
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument note =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("title", "Note title")
                         .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email, note).build()));
+                        .addGenericDocuments(email, note).build()));
 
         // Query with type property paths {"NonExistentType", []}, {"*", ["body", "to"]}
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addProjection("NonExistentType", Collections.emptyList())
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, "body", "to")
+                .addProjection(
+                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
 
         // The email document should have been returned with only the "body" and "to"
         // properties. The note document should have been returned with only the "body" property.
         AppSearchEmail expectedEmail =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setTo("[email protected]", "[email protected]")
                         .setBody("This is the body of the testPut email")
                         .build();
         GenericDocument expectedNote =
-                new GenericDocument.Builder<>("uri2", "Note")
-                        .setNamespace("namespace")
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
                         .setCreationTimestampMillis(1000)
                         .setPropertyString("body", "Note body").build();
         assertThat(documents).containsExactly(expectedNote, expectedEmail);
@@ -1137,34 +1711,34 @@
     public void testQuery_twoInstances() throws Exception {
         // Schema registration
         mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
         mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document to instance 1.
         AppSearchEmail inEmail1 =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail1).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
 
         // Index a document to instance 2.
         AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("uri2")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail2).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
 
         // Query for instance 1.
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
@@ -1172,7 +1746,7 @@
         assertThat(documents).containsExactly(inEmail1);
 
         // Query for instance 2.
-        searchResults = mDb2.query("body", new SearchSpec.Builder()
+        searchResults = mDb2.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
@@ -1185,30 +1759,28 @@
         // Schema registration
         // TODO(tytytyww) add property for long and  double.
         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
-                .addProperty(new PropertyConfig.Builder("subject")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("subject")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .build()
                 ).build();
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(genericSchema).build()).get();
+                new SetSchemaRequest.Builder().addSchemas(genericSchema).build()).get();
 
         // Index a document
         GenericDocument document =
-                new GenericDocument.Builder<>("uri", "Generic")
-                        .setNamespace("document")
+                new GenericDocument.Builder<>("namespace", "id", "Generic")
                         .setPropertyString("subject", "A commonly used fake word is foo. "
                                 + "Another nonsense word that’s used a lot is bar")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(document).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
 
         // Query for the document
-        SearchResults searchResults = mDb1.query("foo",
+        SearchResults searchResults = mDb1.search("foo",
                 new SearchSpec.Builder()
-                        .addSchemaType("Generic")
+                        .addFilterSchemas("Generic")
                         .setSnippetCount(1)
                         .setSnippetCountPerProperty(1)
                         .setMaxSnippetSize(10)
@@ -1223,10 +1795,10 @@
         SearchResult.MatchInfo matchInfo = matchInfos.get(0);
         assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. "
                 + "Another nonsense word that’s used a lot is bar");
-        assertThat(matchInfo.getExactMatchPosition()).isEqualTo(
+        assertThat(matchInfo.getExactMatchRange()).isEqualTo(
                 new SearchResult.MatchRange(/*lower=*/29,  /*upper=*/32));
         assertThat(matchInfo.getExactMatch()).isEqualTo("foo");
-        assertThat(matchInfo.getSnippetPosition()).isEqualTo(
+        assertThat(matchInfo.getSnippetRange()).isEqualTo(
                 new SearchResult.MatchRange(/*lower=*/26,  /*upper=*/33));
         assertThat(matchInfo.getSnippet()).isEqualTo("is foo.");
     }
@@ -1235,48 +1807,51 @@
     public void testRemove() throws Exception {
         // Schema registration
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
 
         // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri2")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
 
         // Delete the document
-        checkIsBatchResultSuccess(mDb1.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()));
+        checkIsBatchResultSuccess(mDb1.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds(
+                        "id1").build()));
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1", "uri2").build())
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1",
+                        "id2").build())
                 .get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
 
-        // Test if we delete a nonexistent URI.
-        AppSearchBatchResult<String, Void> deleteResult = mDb1.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()).get();
+        // Test if we delete a nonexistent id.
+        AppSearchBatchResult<String, Void> deleteResult = mDb1.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds(
+                        "id1").build()).get();
 
-        assertThat(deleteResult.getFailures().get("uri1").getResultCode()).isEqualTo(
+        assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
                 AppSearchResult.RESULT_NOT_FOUND);
     }
 
@@ -1284,49 +1859,89 @@
     public void testRemoveByQuery() throws Exception {
         // Schema registration
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("foo")
                         .setBody("This is the body of the testPut email")
                         .build();
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("bar")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
 
         // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri2")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id2")).hasSize(1);
 
         // Delete the email 1 by query "foo"
-        mDb1.removeByQuery("foo",
+        mDb1.remove("foo",
                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1", "uri2").build())
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build())
                 .get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
 
         // Delete the email 2 by query "bar"
-        mDb1.removeByQuery("bar",
+        mDb1.remove("bar",
                 new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build()).get();
-        getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri2").build())
+        getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build())
                 .get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri2").getResultCode())
+        assertThat(getResult.getFailures().get("id2").getResultCode())
+                .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void testRemoveByQuery_packageFilter() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("foo")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+
+        // Check the presence of the documents
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+
+        // Try to delete email with query "foo", but restricted to a different package name.
+        // Won't work and email will still exist.
+        mDb1.remove("foo",
+                new SearchSpec.Builder().setTermMatch(
+                        SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames(
+                        "some.other.package").build()).get();
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+
+        // Delete the email by query "foo", restricted to the correct package this time.
+        mDb1.remove("foo", new SearchSpec.Builder().setTermMatch(
+                SearchSpec.TERM_MATCH_PREFIX).addFilterPackageNames(
+                ApplicationProvider.getApplicationContext().getPackageName()).build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2").build())
+                .get();
+        assertThat(getResult.isSuccess()).isFalse();
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
     }
 
@@ -1334,44 +1949,44 @@
     public void testRemove_twoInstances() throws Exception {
         // Schema registration
         mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
 
         // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
 
         // Can't delete in the other instance.
-        AppSearchBatchResult<String, Void> deleteResult = mDb2.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(deleteResult.getFailures().get("uri1").getResultCode()).isEqualTo(
+        AppSearchBatchResult<String, Void> deleteResult = mDb2.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
+        assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
                 AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
 
         // Delete the document
-        checkIsBatchResultSuccess(mDb1.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()));
+        checkIsBatchResultSuccess(mDb1.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()));
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
-        // Test if we delete a nonexistent URI.
-        deleteResult = mDb1.removeByUri(
-                new RemoveByUriRequest.Builder().addUri("uri1").build()).get();
-        assertThat(deleteResult.getFailures().get("uri1").getResultCode()).isEqualTo(
+        // Test if we delete a nonexistent id.
+        deleteResult = mDb1.remove(
+                new RemoveByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
+        assertThat(deleteResult.getFailures().get("id1").getResultCode()).isEqualTo(
                 AppSearchResult.RESULT_NOT_FOUND);
     }
 
@@ -1380,360 +1995,353 @@
         // Schema registration
         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic").build();
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).addSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
                         genericSchema).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
         GenericDocument document1 =
-                new GenericDocument.Builder<>("uri3", "Generic").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2, document1)
+                new GenericDocument.Builder<>("namespace", "id3", "Generic").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
                         .build()));
 
         // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1", "uri2",
-                "uri3")).hasSize(3);
+        assertThat(doGet(mDb1, "namespace", "id1", "id2", "id3")).hasSize(3);
 
         // Delete the email type
-        mDb1.removeByQuery("",
+        mDb1.remove("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .addSchemaType(AppSearchEmail.SCHEMA_TYPE)
+                        .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
                         .build())
                 .get();
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1", "uri2", "uri3").build())
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1", "id2", "id3").build())
                 .get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getFailures().get("uri2").getResultCode())
+        assertThat(getResult.getFailures().get("id2").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getSuccesses().get("uri3")).isEqualTo(document1);
+        assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1);
     }
 
     @Test
     public void testRemoveByTypes_twoInstances() throws Exception {
         // Schema registration
         mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
         mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
 
         // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-        assertThat(doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "uri2")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+        assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
 
         // Delete the email type in instance 1
-        mDb1.removeByQuery("",
+        mDb1.remove("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .addSchemaType(AppSearchEmail.SCHEMA_TYPE)
+                        .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
                         .build())
                 .get();
 
         // Make sure it's really gone in instance 1
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
         // Make sure it's still in instance 2.
-        getResult = mDb2.getByUri(
-                new GetByUriRequest.Builder().addUri("uri2").build()).get();
+        getResult = mDb2.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
         assertThat(getResult.isSuccess()).isTrue();
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
     }
 
     @Test
     public void testRemoveByNamespace() throws Exception {
         // Schema registration
         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
-                .addProperty(new PropertyConfig.Builder("foo")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new StringPropertyConfig.Builder("foo")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .build()
                 ).build();
         mDb1.setSchema(
-                new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).addSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).addSchemas(
                         genericSchema).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("email")
+                new AppSearchEmail.Builder("email", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("email")
+                new AppSearchEmail.Builder("email", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
         GenericDocument document1 =
-                new GenericDocument.Builder<>("uri3", "Generic")
-                        .setNamespace("document")
+                new GenericDocument.Builder<>("document", "id3", "Generic")
                         .setPropertyString("foo", "bar").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2, document1)
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2, document1)
                         .build()));
 
         // Check the presence of the documents
-        assertThat(doGet(mDb1, /*namespace=*/"email", "uri1", "uri2")).hasSize(2);
-        assertThat(doGet(mDb1, /*namespace=*/"document", "uri3")).hasSize(1);
+        assertThat(doGet(mDb1, /*namespace=*/"email", "id1", "id2")).hasSize(2);
+        assertThat(doGet(mDb1, /*namespace=*/"document", "id3")).hasSize(1);
 
         // Delete the email namespace
-        mDb1.removeByQuery("",
+        mDb1.remove("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .addNamespace("email")
+                        .addFilterNamespaces("email")
                         .build())
                 .get();
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().setNamespace("email")
-                        .addUri("uri1", "uri2").build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("email")
+                        .addIds("id1", "id2").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        assertThat(getResult.getFailures().get("uri2").getResultCode())
+        assertThat(getResult.getFailures().get("id2").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
-        getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().setNamespace("document")
-                        .addUri("uri3").build()).get();
+        getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("document")
+                        .addIds("id3").build()).get();
         assertThat(getResult.isSuccess()).isTrue();
-        assertThat(getResult.getSuccesses().get("uri3")).isEqualTo(document1);
+        assertThat(getResult.getSuccesses().get("id3")).isEqualTo(document1);
     }
 
     @Test
     public void testRemoveByNamespaces_twoInstances() throws Exception {
         // Schema registration
         mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
         mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("email")
+                new AppSearchEmail.Builder("email", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("email")
+                new AppSearchEmail.Builder("email", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
 
         // Check the presence of the documents
-        assertThat(doGet(mDb1, /*namespace=*/"email", "uri1")).hasSize(1);
-        assertThat(doGet(mDb2, /*namespace=*/"email", "uri2")).hasSize(1);
+        assertThat(doGet(mDb1, /*namespace=*/"email", "id1")).hasSize(1);
+        assertThat(doGet(mDb2, /*namespace=*/"email", "id2")).hasSize(1);
 
         // Delete the email namespace in instance 1
-        mDb1.removeByQuery("",
+        mDb1.remove("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                        .addNamespace("email")
+                        .addFilterNamespaces("email")
                         .build())
                 .get();
 
         // Make sure it's really gone in instance 1
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().setNamespace("email")
-                        .addUri("uri1").build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("email")
+                        .addIds("id1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
         // Make sure it's still in instance 2.
-        getResult = mDb2.getByUri(
-                new GetByUriRequest.Builder().setNamespace("email")
-                        .addUri("uri2").build()).get();
+        getResult = mDb2.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("email")
+                        .addIds("id2").build()).get();
         assertThat(getResult.isSuccess()).isTrue();
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
     }
 
     @Test
     public void testRemoveAll_twoInstances() throws Exception {
         // Schema registration
         mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
         mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email2).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email2).build()));
 
         // Check the presence of the documents
-        assertThat(doGet(mDb1, GenericDocument.DEFAULT_NAMESPACE, "uri1")).hasSize(1);
-        assertThat(doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "uri2")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
+        assertThat(doGet(mDb2, "namespace", "id2")).hasSize(1);
 
         // Delete the all document in instance 1
-        mDb1.removeByQuery("",
+        mDb1.remove("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .build())
                 .get();
 
         // Make sure it's really gone in instance 1
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
         // Make sure it's still in instance 2.
-        getResult = mDb2.getByUri(
-                new GetByUriRequest.Builder().addUri("uri2").build()).get();
+        getResult = mDb2.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id2").build()).get();
         assertThat(getResult.isSuccess()).isTrue();
-        assertThat(getResult.getSuccesses().get("uri2")).isEqualTo(email2);
+        assertThat(getResult.getSuccesses().get("id2")).isEqualTo(email2);
     }
 
     @Test
     public void testRemoveAll_termMatchType() throws Exception {
         // Schema registration
         mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
         mDb2.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example 2")
                         .setBody("This is the body of the testPut second email")
                         .build();
         AppSearchEmail email3 =
-                new AppSearchEmail.Builder("uri3")
+                new AppSearchEmail.Builder("namespace", "id3")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example 3")
                         .setBody("This is the body of the testPut second email")
                         .build();
         AppSearchEmail email4 =
-                new AppSearchEmail.Builder("uri4")
+                new AppSearchEmail.Builder("namespace", "id4")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example 4")
                         .setBody("This is the body of the testPut second email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1, email2).build()));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email3, email4).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email3, email4).build()));
 
         // Check the presence of the documents
-        SearchResults searchResults = mDb1.query("", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(2);
-        searchResults = mDb2.query("", new SearchSpec.Builder()
+        searchResults = mDb2.search("", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).hasSize(2);
 
         // Delete the all document in instance 1 with TERM_MATCH_PREFIX
-        mDb1.removeByQuery("",
+        mDb1.remove("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                         .build())
                 .get();
-        searchResults = mDb1.query("", new SearchSpec.Builder()
+        searchResults = mDb1.search("", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).isEmpty();
 
         // Delete the all document in instance 2 with TERM_MATCH_EXACT_ONLY
-        mDb2.removeByQuery("",
+        mDb2.remove("",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .build())
                 .get();
-        searchResults = mDb2.query("", new SearchSpec.Builder()
+        searchResults = mDb2.search("", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
@@ -1744,70 +2352,70 @@
     public void testRemoveAllAfterEmpty() throws Exception {
         // Schema registration
         mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(AppSearchEmail.SCHEMA).build()).get();
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index documents
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email1).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
 
         // Check the presence of the documents
-        assertThat(doGet(mDb1, "namespace", "uri1")).hasSize(1);
+        assertThat(doGet(mDb1, "namespace", "id1")).hasSize(1);
 
         // Remove the document
         checkIsBatchResultSuccess(
-                mDb1.removeByUri(new RemoveByUriRequest.Builder()
-                        .setNamespace("namespace").addUri("uri1").build()));
+                mDb1.remove(new RemoveByDocumentIdRequest.Builder("namespace").addIds(
+                        "id1").build()));
 
         // Make sure it's really gone
-        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
+        AppSearchBatchResult<String, GenericDocument> getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
         // Delete the all documents
-        mDb1.removeByQuery(
+        mDb1.remove(
                 "", new SearchSpec.Builder().setTermMatch(SearchSpec.TERM_MATCH_PREFIX).build())
                 .get();
 
         // Make sure it's still gone
-        getResult = mDb1.getByUri(
-                new GetByUriRequest.Builder().addUri("uri1").build()).get();
+        getResult = mDb1.getByDocumentId(
+                new GetByDocumentIdRequest.Builder("namespace").addIds("id1").build()).get();
         assertThat(getResult.isSuccess()).isFalse();
-        assertThat(getResult.getFailures().get("uri1").getResultCode())
+        assertThat(getResult.getFailures().get("id1").getResultCode())
                 .isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
     }
 
     @Test
     public void testCloseAndReopen() throws Exception {
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build());
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
         AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // close and re-open the appSearchSession
         mDb1.close();
         mDb1 = createSearchSession(DB_NAME_1).get();
 
         // Query for the document
-        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .build());
         List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
@@ -1826,7 +2434,7 @@
         try {
             // Schema registration -- just mutate something
             sameThreadDb.setSchema(
-                    new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build()).get();
+                    new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
             // Close the database. No further call will be allowed.
             sameThreadDb.close();
@@ -1834,10 +2442,10 @@
             // Try to query the closed database
             // We are using the same-thread db here to make sure it has been closed.
             IllegalStateException e = assertThrows(IllegalStateException.class, () ->
-                    sameThreadDb.query("query", new SearchSpec.Builder()
+                    sameThreadDb.search("query", new SearchSpec.Builder()
                             .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                             .build()));
-            assertThat(e).hasMessageThat().contains("AppSearchSession has already been closed");
+            assertThat(e).hasMessageThat().contains("SearchSession has already been closed");
         } finally {
             // To clean the data that has been added in the test, need to re-open the session and
             // set an empty schema.
@@ -1846,4 +2454,228 @@
             reopen.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
         }
     }
+
+    @Test
+    public void testReportUsage() throws Exception {
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index two documents.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1").build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2).build()));
+
+        // Email 1 has more usages, but email 2 has more recent usages.
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+                .setUsageTimestampMillis(1000).build()).get();
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+                .setUsageTimestampMillis(2000).build()).get();
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id1")
+                .setUsageTimestampMillis(3000).build()).get();
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id2")
+                .setUsageTimestampMillis(10000).build()).get();
+        mDb1.reportUsage(new ReportUsageRequest.Builder("namespace", "id2")
+                .setUsageTimestampMillis(20000).build()).get();
+
+        // Query by number of usages
+        List<SearchResult> results = retrieveAllSearchResults(
+                mDb1.search("", new SearchSpec.Builder()
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .build()));
+        // Email 1 has three usages and email 2 has two usages.
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(0).getRankingSignal()).isEqualTo(3);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
+        assertThat(results.get(1).getRankingSignal()).isEqualTo(2);
+
+        // Query by most recent usag.
+        List<GenericDocument> documents = convertSearchResultsToDocuments(
+                mDb1.search("", new SearchSpec.Builder()
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .build()));
+        // TODO(b/182958600) Check the score for usage timestamp once b/182958600 is fixed.
+        assertThat(documents).containsExactly(email2, email1).inOrder();
+    }
+
+    @Test
+    public void testGetStorageInfo() throws Exception {
+        StorageInfo storageInfo = mDb1.getStorageInfo().get();
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Still no storage space attributed with just a schema
+        storageInfo = mDb1.getStorageInfo().get();
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+
+        // Index two documents.
+        AppSearchEmail email1 = new AppSearchEmail.Builder("namespace1", "id1").build();
+        AppSearchEmail email2 = new AppSearchEmail.Builder("namespace1", "id2").build();
+        AppSearchEmail email3 = new AppSearchEmail.Builder("namespace2", "id1").build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email1, email2,
+                        email3).build()));
+
+        // Non-zero size now
+        storageInfo = mDb1.getStorageInfo().get();
+        assertThat(storageInfo.getSizeBytes()).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(3);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void testFlush() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+
+        AppSearchBatchResult<String, Void> result = checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        // The future returned from maybeFlush will be set as a void or an Exception on error.
+        mDb1.maybeFlush().get();
+    }
+
+    @Test
+    public void testQuery_ResultGroupingLimits() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index four documents.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace1", "id2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace2", "id3")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "id4")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+
+        // Query with per package result grouping. Only the last document 'email4' should be
+        // returned.
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail4);
+
+        // Query with per namespace result grouping. Only the last document in each namespace should
+        // be returned ('email4' and 'email2').
+        searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail4, inEmail2);
+
+        // Query with per package and per namespace result grouping. Only the last document in each
+        // namespace should be returned ('email4' and 'email2').
+        searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inEmail4, inEmail2);
+    }
+
+    @Test
+    public void testIndexNestedDocuments() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA)
+                .addSchemas(new AppSearchSchema.Builder("YesNestedIndex")
+                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                "prop", AppSearchEmail.SCHEMA_TYPE)
+                                .setShouldIndexNestedProperties(true)
+                                .build())
+                        .build())
+                .addSchemas(new AppSearchSchema.Builder("NoNestedIndex")
+                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                "prop", AppSearchEmail.SCHEMA_TYPE)
+                                .setShouldIndexNestedProperties(false)
+                                .build())
+                        .build())
+                .build())
+                .get();
+
+        // Index the documents.
+        AppSearchEmail email = new AppSearchEmail.Builder("", "")
+                .setSubject("This is the body")
+                .build();
+        GenericDocument yesNestedIndex =
+                new GenericDocument.Builder<>("namespace", "yesNestedIndex", "YesNestedIndex")
+                        .setPropertyDocument("prop", email)
+                        .build();
+        GenericDocument noNestedIndex =
+                new GenericDocument.Builder<>("namespace", "noNestedIndex", "NoNestedIndex")
+                        .setPropertyDocument("prop", email)
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(yesNestedIndex, noNestedIndex).build()));
+
+        // Query.
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setSnippetCount(10)
+                .setSnippetCountPerProperty(10)
+                .build());
+        List<SearchResult> page = searchResults.getNextPage().get();
+        assertThat(page).hasSize(1);
+        assertThat(page.get(0).getGenericDocument()).isEqualTo(yesNestedIndex);
+        List<SearchResult.MatchInfo> matches = page.get(0).getMatches();
+        assertThat(matches).hasSize(1);
+        assertThat(matches.get(0).getPropertyPath()).isEqualTo("prop.subject");
+        assertThat(matches.get(0).getFullText()).isEqualTo("This is the body");
+        assertThat(matches.get(0).getExactMatch()).isEqualTo("body");
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java
index 541cd5a..0d5b693 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionLocalCtsTest.java
@@ -32,7 +32,7 @@
     protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
         return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build());
+                new LocalStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
@@ -40,7 +40,7 @@
             @NonNull String dbName, @NonNull ExecutorService executor) {
         Context context = ApplicationProvider.getApplicationContext();
         return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build(),
-                executor);
+                new LocalStorage.SearchContext.Builder(context, dbName)
+                        .setWorkerExecutor(executor).build());
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionPlatformCtsTest.java
new file mode 100644
index 0000000..87ce00d
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionPlatformCtsTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app.cts;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.concurrent.ExecutorService;
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class AppSearchSessionPlatformCtsTest extends AppSearchSessionCtsTestBase {
+    @Override
+    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+        Context context = ApplicationProvider.getApplicationContext();
+        return PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(context, dbName).build());
+    }
+
+    @Override
+    protected ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull String dbName, @NonNull ExecutorService executor) {
+        Context context = ApplicationProvider.getApplicationContext();
+        return PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(context, dbName)
+                        .setWorkerExecutor(executor).build());
+    }
+
+    @Override
+    @Test
+    @Ignore("TODO(b/177266929)")
+    public void testSetSchema_updateVersion() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    @Test
+    @Ignore("TODO(b/177266929)")
+    public void testRemoveSchema() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    @Test
+    @Ignore("TODO(b/177266929)")
+    public void testRemoveSchema_twoDatabases() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GenericDocumentCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GenericDocumentCtsTest.java
index 164adad..b79c182 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GenericDocumentCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GenericDocumentCtsTest.java
@@ -28,17 +28,18 @@
     private static final byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
     private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
     private static final GenericDocument sDocumentProperties1 = new GenericDocument
-            .Builder<>("sDocumentProperties1", "sDocumentPropertiesSchemaType1")
+            .Builder<>("namespace", "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
             .setCreationTimestampMillis(12345L)
             .build();
     private static final GenericDocument sDocumentProperties2 = new GenericDocument
-            .Builder<>("sDocumentProperties2", "sDocumentPropertiesSchemaType2")
+            .Builder<>("namespace", "sDocumentProperties2", "sDocumentPropertiesSchemaType2")
             .setCreationTimestampMillis(6789L)
             .build();
 
     @Test
     public void testDocumentEquals_identical() {
-        GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setTtlMillis(1L)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
@@ -48,7 +49,8 @@
                 .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
                 .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
                 .build();
-        GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setTtlMillis(1L)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
@@ -64,7 +66,8 @@
 
     @Test
     public void testDocumentEquals_differentOrder() {
-        GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
                 .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
@@ -75,7 +78,8 @@
                 .build();
 
         // Create second document with same parameter but different order.
-        GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setPropertyBoolean("booleanKey1", true, false, true)
                 .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
@@ -90,13 +94,15 @@
 
     @Test
     public void testDocumentEquals_failure() {
-        GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
                 .build();
 
         // Create second document with same order but different value.
-        GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setPropertyLong("longKey1", 1L, 2L, 4L) // Different
                 .build();
@@ -106,13 +112,15 @@
 
     @Test
     public void testDocumentEquals_repeatedFieldOrder_failure() {
-        GenericDocument document1 = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setPropertyBoolean("booleanKey1", true, false, true)
                 .build();
 
         // Create second document with same order but different value.
-        GenericDocument document2 = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setPropertyBoolean("booleanKey1", true, true, false) // Different
                 .build();
@@ -122,7 +130,7 @@
 
     @Test
     public void testDocumentGetSingleValue() {
-        GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setScore(1)
                 .setTtlMillis(1L)
@@ -133,7 +141,7 @@
                 .setPropertyBytes("byteKey1", sByteArray1)
                 .setPropertyDocument("documentKey1", sDocumentProperties1)
                 .build();
-        assertThat(document.getUri()).isEqualTo("uri1");
+        assertThat(document.getId()).isEqualTo("id1");
         assertThat(document.getTtlMillis()).isEqualTo(1L);
         assertThat(document.getSchemaType()).isEqualTo("schemaType1");
         assertThat(document.getCreationTimestampMillis()).isEqualTo(5);
@@ -143,13 +151,13 @@
         assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
         assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
         assertThat(document.getPropertyBytes("byteKey1"))
-                .asList().containsExactly((byte) 1, (byte) 2, (byte) 3);
+                .asList().containsExactly((byte) 1, (byte) 2, (byte) 3).inOrder();
         assertThat(document.getPropertyDocument("documentKey1")).isEqualTo(sDocumentProperties1);
     }
 
     @Test
     public void testDocumentGetArrayValues() {
-        GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
                 .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
@@ -159,24 +167,25 @@
                 .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
                 .build();
 
-        assertThat(document.getUri()).isEqualTo("uri1");
+        assertThat(document.getId()).isEqualTo("id1");
         assertThat(document.getSchemaType()).isEqualTo("schemaType1");
-        assertThat(document.getPropertyLongArray("longKey1")).asList().containsExactly(1L, 2L, 3L);
+        assertThat(document.getPropertyLongArray("longKey1")).asList()
+                .containsExactly(1L, 2L, 3L).inOrder();
         assertThat(document.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
-                .containsExactly(1.0, 2.0, 3.0);
+                .containsExactly(1.0, 2.0, 3.0).inOrder();
         assertThat(document.getPropertyBooleanArray("booleanKey1")).asList()
-                .containsExactly(true, false, true);
+                .containsExactly(true, false, true).inOrder();
         assertThat(document.getPropertyStringArray("stringKey1")).asList()
-                .containsExactly("test-value1", "test-value2", "test-value3");
+                .containsExactly("test-value1", "test-value2", "test-value3").inOrder();
         assertThat(document.getPropertyBytesArray("byteKey1")).asList()
-                .containsExactly(sByteArray1, sByteArray2);
+                .containsExactly(sByteArray1, sByteArray2).inOrder();
         assertThat(document.getPropertyDocumentArray("documentKey1")).asList()
-                .containsExactly(sDocumentProperties1, sDocumentProperties2);
+                .containsExactly(sDocumentProperties1, sDocumentProperties2).inOrder();
     }
 
     @Test
     public void testDocument_toString() {
-        GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document = new GenericDocument.Builder<>("", "id1", "schemaType1")
                 .setCreationTimestampMillis(5L)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
                 .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
@@ -185,41 +194,41 @@
                 .setPropertyBytes("byteKey1", sByteArray1, sByteArray2)
                 .setPropertyDocument("documentKey1", sDocumentProperties1, sDocumentProperties2)
                 .build();
-        String exceptedString = "{ key: 'creationTimestampMillis' value: 5 } "
-                + "{ key: 'namespace' value:  } "
-                + "{ key: 'properties' value: "
-                +       "{ key: 'booleanKey1' value: [ 'true' 'false' 'true' ] } "
-                +       "{ key: 'byteKey1' value: "
-                +             "{ key: 'byteArray' value: [ '1' '2' '3' ] } "
-                +             "{ key: 'byteArray' value: [ '4' '5' '6' '7' ] }  } "
-                +       "{ key: 'documentKey1' value: [ '"
-                +             "{ key: 'creationTimestampMillis' value: 12345 } "
-                +             "{ key: 'namespace' value:  } "
-                +             "{ key: 'properties' value:  } "
-                +             "{ key: 'schemaType' value: sDocumentPropertiesSchemaType1 } "
-                +             "{ key: 'score' value: 0 } "
-                +             "{ key: 'ttlMillis' value: 0 } "
-                +             "{ key: 'uri' value: sDocumentProperties1 } ' '"
-                +             "{ key: 'creationTimestampMillis' value: 6789 } "
-                +             "{ key: 'namespace' value:  } "
-                +             "{ key: 'properties' value:  } "
-                +             "{ key: 'schemaType' value: sDocumentPropertiesSchemaType2 } "
-                +             "{ key: 'score' value: 0 } "
-                +             "{ key: 'ttlMillis' value: 0 } "
-                +             "{ key: 'uri' value: sDocumentProperties2 } ' ] } "
-                +       "{ key: 'doubleKey1' value: [ '1.0' '2.0' '3.0' ] } "
-                +       "{ key: 'longKey1' value: [ '1' '2' '3' ] } "
-                +       "{ key: 'stringKey1' value: [ 'String1' 'String2' 'String3' ] }  } "
-                + "{ key: 'schemaType' value: schemaType1 } "
-                + "{ key: 'score' value: 0 } "
-                + "{ key: 'ttlMillis' value: 0 } "
-                + "{ key: 'uri' value: uri1 } ";
+        String exceptedString = "{ name: 'creationTimestampMillis' value: 5 } "
+                + "{ name: 'id' value: id1 } "
+                + "{ name: 'namespace' value:  } "
+                + "{ name: 'properties' value: "
+                + "{ name: 'booleanKey1' value: [ 'true' 'false' 'true' ] } "
+                + "{ name: 'byteKey1' value: "
+                + "{ name: 'byteArray' value: [ '1' '2' '3' ] } "
+                + "{ name: 'byteArray' value: [ '4' '5' '6' '7' ] }  } "
+                + "{ name: 'documentKey1' value: [ '"
+                + "{ name: 'creationTimestampMillis' value: 12345 } "
+                + "{ name: 'id' value: sDocumentProperties1 } "
+                + "{ name: 'namespace' value: namespace } "
+                + "{ name: 'properties' value:  } "
+                + "{ name: 'schemaType' value: sDocumentPropertiesSchemaType1 } "
+                + "{ name: 'score' value: 0 } "
+                + "{ name: 'ttlMillis' value: 0 } ' '"
+                + "{ name: 'creationTimestampMillis' value: 6789 } "
+                + "{ name: 'id' value: sDocumentProperties2 } "
+                + "{ name: 'namespace' value: namespace } "
+                + "{ name: 'properties' value:  } "
+                + "{ name: 'schemaType' value: sDocumentPropertiesSchemaType2 } "
+                + "{ name: 'score' value: 0 } "
+                + "{ name: 'ttlMillis' value: 0 } ' ] } "
+                + "{ name: 'doubleKey1' value: [ '1.0' '2.0' '3.0' ] } "
+                + "{ name: 'longKey1' value: [ '1' '2' '3' ] } "
+                + "{ name: 'stringKey1' value: [ 'String1' 'String2' 'String3' ] }  } "
+                + "{ name: 'schemaType' value: schemaType1 } "
+                + "{ name: 'score' value: 0 } "
+                + "{ name: 'ttlMillis' value: 0 } ";
         assertThat(document.toString()).isEqualTo(exceptedString);
     }
 
     @Test
     public void testDocumentGetValues_differentTypes() {
-        GenericDocument document = new GenericDocument.Builder<>("uri1", "schemaType1")
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
                 .setScore(1)
                 .setPropertyLong("longKey1", 1L)
                 .setPropertyBoolean("booleanKey1", true, false, true)
@@ -237,7 +246,7 @@
         // Get a value with multiple elements as an array and as a single value
         assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
         assertThat(document.getPropertyStringArray("stringKey1")).asList()
-                .containsExactly("test-value1", "test-value2", "test-value3");
+                .containsExactly("test-value1", "test-value2", "test-value3").inOrder();
 
         // Get a value of the wrong type
         assertThat(document.getPropertyDouble("longKey1")).isEqualTo(0.0);
@@ -245,10 +254,471 @@
     }
 
     @Test
+    public void testDocument_setEmptyValues() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setPropertyBoolean("testKey")
+                .build();
+        assertThat(document.getPropertyBooleanArray("testKey")).isEmpty();
+    }
+
+    @Test
     public void testDocumentInvalid() {
-        GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("uri1", "schemaType1");
+        GenericDocument.Builder<?> builder = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1");
+        String nullString = null;
+
         assertThrows(
                 IllegalArgumentException.class,
-                () -> builder.setPropertyBoolean("test", new boolean[]{}));
+                () -> builder.setPropertyString("testKey", "string1", nullString));
+    }
+
+    @Test
+    public void testRetrieveTopLevelProperties() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+
+        // Top-level repeated properties should be retrievable
+        assertThat(doc.getPropertyStringArray("propString")).asList()
+                .containsExactly("Goodbye", "Hello").inOrder();
+        assertThat(doc.getPropertyLongArray("propInts")).asList()
+                .containsExactly(3L, 1L, 4L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDoubles")).usingTolerance(0.0001)
+                .containsExactly(3.14, 0.42).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propBools")).asList().containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{3, 4}});
+
+        // Top-level repeated properties should retrieve the first element
+        assertThat(doc.getPropertyString("propString")).isEqualTo("Goodbye");
+        assertThat(doc.getPropertyLong("propInts")).isEqualTo(3);
+        assertThat(doc.getPropertyDouble("propDoubles")).isWithin(0.0001)
+                .of(3.14);
+        assertThat(doc.getPropertyBoolean("propBools")).isFalse();
+        assertThat(doc.getPropertyBytes("propBytes")).isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveNestedProperties() {
+        GenericDocument innerDoc = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc)
+                .build();
+
+        // Document should be retrievable via both array and single getters
+        assertThat(doc.getPropertyDocument("propDocument")).isEqualTo(innerDoc);
+        assertThat(doc.getPropertyDocumentArray("propDocument")).asList()
+                .containsExactly(innerDoc);
+        assertThat((GenericDocument[]) doc.getProperty("propDocument")).asList()
+                .containsExactly(innerDoc);
+
+        // Nested repeated properties should be retrievable
+        assertThat(doc.getPropertyStringArray("propDocument.propString")).asList()
+                .containsExactly("Goodbye", "Hello").inOrder();
+        assertThat(doc.getPropertyLongArray("propDocument.propInts")).asList()
+                .containsExactly(3L, 1L, 4L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDocument.propDoubles")).usingTolerance(0.0001)
+                .containsExactly(3.14, 0.42).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propDocument.propBools")).asList()
+                .containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propDocument.propBytes")).isEqualTo(
+                new byte[][]{{3, 4}});
+        assertThat(doc.getProperty("propDocument.propBytes")).isEqualTo(
+                new byte[][]{{3, 4}});
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument.propString"))
+                .isEqualTo("Goodbye");
+        assertThat(doc.getPropertyLong("propDocument.propInts")).isEqualTo(3);
+        assertThat(doc.getPropertyDouble("propDocument.propDoubles")).isWithin(0.0001)
+                .of(3.14);
+        assertThat(doc.getPropertyBoolean("propDocument.propBools")).isFalse();
+        assertThat(doc.getPropertyBytes("propDocument.propBytes")).isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveNestedPropertiesMultipleNestedDocuments() {
+        GenericDocument innerDoc0 = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyString("propStringTwo", "Fee", "Fi")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument innerDoc1 = new GenericDocument.Builder<>("namespace", "id3", "schema2")
+                .setPropertyString("propString", "Aloha")
+                .setPropertyLong("propInts", 7, 5, 6)
+                .setPropertyLong("propIntsTwo", 8, 6)
+                .setPropertyDouble("propDoubles", 7.14, 0.356)
+                .setPropertyBoolean("propBools", true)
+                .setPropertyBytes("propBytes", new byte[][]{{8, 9}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc0, innerDoc1)
+                .build();
+
+        // Documents should be retrievable via both array and single getters
+        assertThat(doc.getPropertyDocument("propDocument")).isEqualTo(innerDoc0);
+        assertThat(doc.getPropertyDocumentArray("propDocument")).asList()
+                .containsExactly(innerDoc0, innerDoc1).inOrder();
+        assertThat((GenericDocument[]) doc.getProperty("propDocument")).asList()
+                .containsExactly(innerDoc0, innerDoc1).inOrder();
+
+        // Nested repeated properties should be retrievable and should merge the arrays from the
+        // inner documents.
+        assertThat(doc.getPropertyStringArray("propDocument.propString")).asList()
+                .containsExactly("Goodbye", "Hello", "Aloha").inOrder();
+        assertThat(doc.getPropertyLongArray("propDocument.propInts")).asList()
+                .containsExactly(3L, 1L, 4L, 7L, 5L, 6L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDocument.propDoubles")).usingTolerance(0.0001)
+                .containsExactly(3.14, 0.42, 7.14, 0.356).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propDocument.propBools")).asList()
+                .containsExactly(false, true).inOrder();
+        assertThat(doc.getPropertyBytesArray("propDocument.propBytes")).isEqualTo(
+                new byte[][]{{3, 4}, {8, 9}});
+        assertThat(doc.getProperty("propDocument.propBytes")).isEqualTo(
+                new byte[][]{{3, 4}, {8, 9}});
+
+        // Nested repeated properties should properly handle properties appearing in only one inner
+        // document, but not the other.
+        assertThat(
+                doc.getPropertyStringArray("propDocument.propStringTwo")).asList()
+                .containsExactly("Fee", "Fi").inOrder();
+        assertThat(doc.getPropertyLongArray("propDocument.propIntsTwo")).asList()
+                .containsExactly(8L, 6L).inOrder();
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument.propString"))
+                .isEqualTo("Goodbye");
+        assertThat(doc.getPropertyString("propDocument.propStringTwo"))
+                .isEqualTo("Fee");
+        assertThat(doc.getPropertyLong("propDocument.propInts")).isEqualTo(3);
+        assertThat(doc.getPropertyLong("propDocument.propIntsTwo")).isEqualTo(8L);
+        assertThat(doc.getPropertyDouble("propDocument.propDoubles")).isWithin(0.0001)
+                .of(3.14);
+        assertThat(doc.getPropertyBoolean("propDocument.propBools")).isFalse();
+        assertThat(doc.getPropertyBytes("propDocument.propBytes")).isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveTopLevelPropertiesIndex() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+
+        // Top-level repeated properties should be retrievable
+        assertThat(doc.getPropertyStringArray("propString[1]")).asList()
+                .containsExactly("Hello");
+        assertThat(doc.getPropertyLongArray("propInts[2]")).asList()
+                .containsExactly(4L);
+        assertThat(doc.getPropertyDoubleArray("propDoubles[0]")).usingTolerance(0.0001)
+                .containsExactly(3.14);
+        assertThat(doc.getPropertyBooleanArray("propBools[0]")).asList().containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propBytes[0]")).isEqualTo(new byte[][]{{3, 4}});
+        assertThat(doc.getProperty("propBytes[0]")).isEqualTo(new byte[][]{{3, 4}});
+
+        // Top-level repeated properties should retrieve the first element
+        assertThat(doc.getPropertyString("propString[1]")).isEqualTo("Hello");
+        assertThat(doc.getPropertyLong("propInts[2]")).isEqualTo(4L);
+        assertThat(doc.getPropertyDouble("propDoubles[0]")).isWithin(0.0001)
+                .of(3.14);
+        assertThat(doc.getPropertyBoolean("propBools[0]")).isFalse();
+        assertThat(doc.getPropertyBytes("propBytes[0]")).isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveTopLevelPropertiesIndexOutOfRange() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+
+        // Array getters should return null when given a bad index.
+        assertThat(doc.getPropertyStringArray("propString[5]")).isNull();
+
+        // Single getters should return default when given a bad index.
+        assertThat(doc.getPropertyDouble("propDoubles[7]")).isEqualTo(0.0);
+    }
+
+    @Test
+    public void testNestedProperties_unusualPaths() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setPropertyString("propString", "Hello", "Goodbye")
+                .setPropertyDocument("propDocs1", new GenericDocument.Builder<>("", "", "schema1")
+                        .setPropertyString("", "Cat", "Dog")
+                        .build())
+                .setPropertyDocument("propDocs2", new GenericDocument.Builder<>("", "", "schema1")
+                        .setPropertyDocument("", new GenericDocument.Builder<>("", "", "schema1")
+                                .setPropertyString("", "Red", "Blue")
+                                .setPropertyString("propString", "Bat", "Hawk")
+                                .build())
+                        .build())
+                .setPropertyDocument("", new GenericDocument.Builder<>("", "", "schema1")
+                        .setPropertyDocument("", new GenericDocument.Builder<>("", "", "schema1")
+                                .setPropertyString("", "Orange", "Green")
+                                .setPropertyString("propString", "Toad", "Bird")
+                                .build())
+                        .build())
+                .build();
+        assertThat(doc.getPropertyString("propString")).isEqualTo("Hello");
+        assertThat(doc.getPropertyString("propString[1]")).isEqualTo("Goodbye");
+        assertThat(doc.getPropertyString("propDocs1.")).isEqualTo("Cat");
+        assertThat(doc.getPropertyString("propDocs1.[1]")).isEqualTo("Dog");
+        assertThat(doc.getPropertyStringArray("propDocs1[0].")).asList()
+                .containsExactly("Cat", "Dog").inOrder();
+        assertThat(doc.getPropertyString("propDocs2..propString")).isEqualTo("Bat");
+        assertThat(doc.getPropertyString("propDocs2..propString[1]")).isEqualTo("Hawk");
+        assertThat(doc.getPropertyString("propDocs2..")).isEqualTo("Red");
+        assertThat(doc.getPropertyString("propDocs2..[1]")).isEqualTo("Blue");
+        assertThat(doc.getPropertyString("[0]..propString[1]")).isEqualTo("Bird");
+        assertThat(doc.getPropertyString("[0]..[1]")).isEqualTo("Green");
+    }
+
+    @Test
+    public void testNestedProperties_invalidPaths() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .setPropertyDocument("propDocs", new GenericDocument.Builder<>("", "", "schema1")
+                        .setPropertyString("", "Cat")
+                        .build())
+                .build();
+
+        // Some paths are invalid because they don't apply to the given document --- these should
+        // return null. It's not the querier's fault.
+        assertThat(doc.getPropertyStringArray("propString.propInts")).isNull();
+        assertThat(doc.getPropertyStringArray("propDocs.propFoo")).isNull();
+        assertThat(doc.getPropertyStringArray("propDocs.propNestedString.propFoo")).isNull();
+
+        // Some paths are invalid because they are malformed. These throw an exception --- the
+        // querier shouldn't provide such paths.
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propDocs.[0]propInts"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propString[0"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propString[0.]"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propString[banana]"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propString[-1]"));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> doc.getPropertyStringArray("propDocs[0]cat"));
+    }
+
+    @Test
+    public void testRetrieveNestedPropertiesIntermediateIndex() {
+        GenericDocument innerDoc0 = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyString("propStringTwo", "Fee", "Fi")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument innerDoc1 = new GenericDocument.Builder<>("namespace", "id3", "schema2")
+                .setPropertyString("propString", "Aloha")
+                .setPropertyLong("propInts", 7, 5, 6)
+                .setPropertyLong("propIntsTwo", 8, 6)
+                .setPropertyDouble("propDoubles", 7.14, 0.356)
+                .setPropertyBoolean("propBools", true)
+                .setPropertyBytes("propBytes", new byte[][]{{8, 9}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc0, innerDoc1)
+                .build();
+
+        // Documents should be retrievable via both array and single getters
+        assertThat(doc.getPropertyDocument("propDocument[1]")).isEqualTo(innerDoc1);
+        assertThat(doc.getPropertyDocumentArray("propDocument[1]")).asList()
+                .containsExactly(innerDoc1);
+        assertThat((GenericDocument[]) doc.getProperty("propDocument[1]")).asList()
+                .containsExactly(innerDoc1);
+
+        // Nested repeated properties should be retrievable and should merge the arrays from the
+        // inner documents.
+        assertThat(doc.getPropertyStringArray("propDocument[1].propString")).asList()
+                .containsExactly("Aloha");
+        assertThat(doc.getPropertyLongArray("propDocument[0].propInts")).asList()
+                .containsExactly(3L, 1L, 4L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDocument[1].propDoubles")).usingTolerance(0.0001)
+                .containsExactly(7.14, 0.356).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propDocument[0].propBools")).asList()
+                .containsExactly(false);
+        assertThat((boolean[]) doc.getProperty("propDocument[0].propBools")).asList()
+                .containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propDocument[1].propBytes")).isEqualTo(
+                new byte[][]{{8, 9}});
+
+        // Nested repeated properties should properly handle properties appearing in only one inner
+        // document, but not the other.
+        assertThat(doc.getPropertyStringArray("propDocument[0].propStringTwo")).asList()
+                .containsExactly("Fee", "Fi").inOrder();
+        assertThat(doc.getPropertyStringArray("propDocument[1].propStringTwo")).isNull();
+        assertThat(doc.getPropertyLongArray("propDocument[0].propIntsTwo")).isNull();
+        assertThat(doc.getPropertyLongArray("propDocument[1].propIntsTwo")).asList()
+                .containsExactly(8L, 6L).inOrder();
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument[1].propString"))
+                .isEqualTo("Aloha");
+        assertThat(doc.getPropertyString("propDocument[0].propStringTwo"))
+                .isEqualTo("Fee");
+        assertThat(doc.getPropertyLong("propDocument[1].propInts")).isEqualTo(7L);
+        assertThat(doc.getPropertyLong("propDocument[1].propIntsTwo")).isEqualTo(8L);
+        assertThat(doc.getPropertyDouble("propDocument[0].propDoubles"))
+                .isWithin(0.0001).of(3.14);
+        assertThat(doc.getPropertyBoolean("propDocument[1].propBools")).isTrue();
+        assertThat(doc.getPropertyBytes("propDocument[0].propBytes"))
+                .isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveNestedPropertiesLeafIndex() {
+        GenericDocument innerDoc0 = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyString("propStringTwo", "Fee", "Fi")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument innerDoc1 = new GenericDocument.Builder<>("namespace", "id3", "schema2")
+                .setPropertyString("propString", "Aloha")
+                .setPropertyLong("propInts", 7, 5, 6)
+                .setPropertyLong("propIntsTwo", 8, 6)
+                .setPropertyDouble("propDoubles", 7.14, 0.356)
+                .setPropertyBoolean("propBools", true)
+                .setPropertyBytes("propBytes", new byte[][]{{8, 9}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc0, innerDoc1)
+                .build();
+
+        // Nested repeated properties should be retrievable and should merge the arrays from the
+        // inner documents.
+        assertThat(doc.getPropertyStringArray("propDocument.propString[0]")).asList()
+                .containsExactly("Goodbye", "Aloha").inOrder();
+        assertThat(doc.getPropertyLongArray("propDocument.propInts[2]")).asList()
+                .containsExactly(4L, 6L).inOrder();
+        assertThat(doc.getPropertyDoubleArray("propDocument.propDoubles[1]"))
+                .usingTolerance(0.0001).containsExactly(0.42, 0.356).inOrder();
+        assertThat((double[]) doc.getProperty("propDocument.propDoubles[1]"))
+                .usingTolerance(0.0001).containsExactly(0.42, 0.356).inOrder();
+        assertThat(doc.getPropertyBooleanArray("propDocument.propBools[0]")).asList()
+                .containsExactly(false, true).inOrder();
+        assertThat(doc.getPropertyBytesArray("propDocument.propBytes[0]"))
+                .isEqualTo(new byte[][]{{3, 4}, {8, 9}});
+
+        // Nested repeated properties should properly handle properties appearing in only one inner
+        // document, but not the other.
+        assertThat(doc.getPropertyStringArray("propDocument.propStringTwo[0]")).asList()
+                .containsExactly("Fee");
+        assertThat((String[]) doc.getProperty("propDocument.propStringTwo[0]")).asList()
+                .containsExactly("Fee");
+        assertThat(doc.getPropertyLongArray("propDocument.propIntsTwo[1]")).asList()
+                .containsExactly(6L);
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument.propString[1]"))
+                .isEqualTo("Hello");
+        assertThat(doc.getPropertyString("propDocument.propStringTwo[1]"))
+                .isEqualTo("Fi");
+        assertThat(doc.getPropertyLong("propDocument.propInts[1]"))
+                .isEqualTo(1L);
+        assertThat(doc.getPropertyLong("propDocument.propIntsTwo[1]")).isEqualTo(6L);
+        assertThat(doc.getPropertyDouble("propDocument.propDoubles[1]"))
+                .isWithin(0.0001).of(0.42);
+        assertThat(doc.getPropertyBoolean("propDocument.propBools[0]")).isFalse();
+        assertThat(doc.getPropertyBytes("propDocument.propBytes[0]"))
+                .isEqualTo(new byte[]{3, 4});
+    }
+
+    @Test
+    public void testRetrieveNestedPropertiesIntermediateAndLeafIndices() {
+        GenericDocument innerDoc0 = new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                .setPropertyString("propString", "Goodbye", "Hello")
+                .setPropertyString("propStringTwo", "Fee", "Fi")
+                .setPropertyLong("propInts", 3, 1, 4)
+                .setPropertyDouble("propDoubles", 3.14, 0.42)
+                .setPropertyBoolean("propBools", false)
+                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                .build();
+        GenericDocument innerDoc1 = new GenericDocument.Builder<>("namespace", "id3", "schema2")
+                .setPropertyString("propString", "Aloha")
+                .setPropertyLong("propInts", 7, 5, 6)
+                .setPropertyLong("propIntsTwo", 8, 6)
+                .setPropertyDouble("propDoubles", 7.14, 0.356)
+                .setPropertyBoolean("propBools", true)
+                .setPropertyBytes("propBytes", new byte[][]{{8, 9}})
+                .build();
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyDocument("propDocument", innerDoc0, innerDoc1)
+                .build();
+
+        // Nested repeated properties should be retrievable and should merge the arrays from the
+        // inner documents.
+        assertThat(doc.getPropertyStringArray("propDocument[1].propString[0]")).asList()
+                .containsExactly("Aloha");
+        assertThat(doc.getPropertyLongArray("propDocument[0].propInts[2]")).asList()
+                .containsExactly(4L);
+        assertThat((long[]) doc.getProperty("propDocument[0].propInts[2]")).asList()
+                .containsExactly(4L);
+        assertThat(doc.getPropertyDoubleArray("propDocument[1].propDoubles[1]"))
+                .usingTolerance(0.0001).containsExactly(0.356);
+        assertThat(doc.getPropertyBooleanArray("propDocument[0].propBools[0]")).asList()
+                .containsExactly(false);
+        assertThat(doc.getPropertyBytesArray("propDocument[1].propBytes[0]"))
+                .isEqualTo(new byte[][]{{8, 9}});
+
+        // Nested properties should retrieve the first element
+        assertThat(doc.getPropertyString("propDocument[0].propString[1]"))
+                .isEqualTo("Hello");
+        assertThat(doc.getPropertyString("propDocument[0].propStringTwo[1]"))
+                .isEqualTo("Fi");
+        assertThat(doc.getPropertyLong("propDocument[1].propInts[1]"))
+                .isEqualTo(5L);
+        assertThat(doc.getPropertyLong("propDocument[1].propIntsTwo[1]"))
+                .isEqualTo(6L);
+        assertThat(doc.getPropertyDouble("propDocument[0].propDoubles[1]"))
+                .isWithin(0.0001).of(0.42);
+        assertThat(doc.getPropertyBoolean("propDocument[1].propBools[0]")).isTrue();
+        assertThat(doc.getPropertyBytes("propDocument[0].propBytes[0]"))
+                .isEqualTo(new byte[]{3, 4});
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java
index 0120153..90f58c0 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java
@@ -21,21 +21,25 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import android.content.Context;
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchEmail;
+import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.ReportSystemUsageRequest;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.localstorage.LocalStorage;
+import androidx.appsearch.exceptions.AppSearchException;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableList;
@@ -48,10 +52,11 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 
 public abstract class GlobalSearchSessionCtsTestBase {
     private AppSearchSession mDb1;
-    private static final String DB_NAME_1 = LocalStorage.DEFAULT_DATABASE_NAME;
+    private static final String DB_NAME_1 = "";
     private AppSearchSession mDb2;
     private static final String DB_NAME_2 = "testDb2";
 
@@ -59,6 +64,7 @@
 
     protected abstract ListenableFuture<AppSearchSession> createSearchSession(
             @NonNull String dbName);
+
     protected abstract ListenableFuture<GlobalSearchSession> createGlobalSearchSession();
 
     @Before
@@ -82,13 +88,15 @@
     }
 
     private void cleanup() throws Exception {
-        mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
     }
 
     private List<GenericDocument> snapshotResults(String queryExpression, SearchSpec spec)
             throws Exception {
-        SearchResults searchResults = mGlobalAppSearchManager.query(queryExpression, spec);
+        SearchResults searchResults = mGlobalAppSearchManager.search(queryExpression, spec);
         return convertSearchResultsToDocuments(searchResults);
     }
 
@@ -119,19 +127,19 @@
                 exactSearchSpec);
 
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document
         AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail).build()));
 
         // Query for the document
         List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
@@ -154,32 +162,32 @@
         List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
 
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a document to instance 1.
         AppSearchEmail inEmail1 =
-                new AppSearchEmail.Builder("uri1")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail1).build()));
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
 
         // Index a document to instance 2.
         AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("uri2")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(inEmail2).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
 
         // Query across all instances
         List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
@@ -196,28 +204,28 @@
         List<GenericDocument> beforeBodyDocuments = snapshotResults("body", exactSearchSpec);
 
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
         List<AppSearchEmail> emailList = new ArrayList<>();
         PutDocumentsRequest.Builder putDocumentsRequestBuilder = new PutDocumentsRequest.Builder();
 
         // Index 31 documents
         for (int i = 0; i < 31; i++) {
             AppSearchEmail inEmail =
-                    new AppSearchEmail.Builder("uri" + i)
+                    new AppSearchEmail.Builder("namespace", "id" + i)
                             .setFrom("[email protected]")
                             .setTo("[email protected]", "[email protected]")
                             .setSubject("testPut example")
                             .setBody("This is the body of the testPut email")
                             .build();
             emailList.add(inEmail);
-            putDocumentsRequestBuilder.addGenericDocument(inEmail);
+            putDocumentsRequestBuilder.addGenericDocuments(inEmail);
         }
-        checkIsBatchResultSuccess(mDb1.putDocuments(putDocumentsRequestBuilder.build()));
+        checkIsBatchResultSuccess(mDb1.put(putDocumentsRequestBuilder.build()));
 
         // Set number of results per page is 7.
         int pageSize = 7;
-        SearchResults searchResults = mGlobalAppSearchManager.query("body",
+        SearchResults searchResults = mGlobalAppSearchManager.search("body",
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .setResultCountPerPage(pageSize)
@@ -232,7 +240,7 @@
             results = searchResults.getNextPage().get();
             ++pageNumber;
             for (SearchResult result : results) {
-                documents.add(result.getDocument());
+                documents.add(result.getGenericDocument());
             }
         } while (results.size() > 0);
 
@@ -257,39 +265,41 @@
         SearchSpec exactEmailSearchSpec =
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addSchemaType(AppSearchEmail.SCHEMA_TYPE)
+                        .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
                         .build();
         List<GenericDocument> beforeBodyEmailDocuments = snapshotResults("body",
                 exactEmailSearchSpec);
 
         // Schema registration
         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
-                .addProperty(new PropertyConfig.Builder("foo")
-                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("foo")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                         .build()
                 ).build();
 
         // db1 has both "Generic" and "builtin:Email"
-        mDb1.setSchema(new SetSchemaRequest.Builder()
-                .addSchema(genericSchema).addSchema(AppSearchEmail.SCHEMA).build()).get();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(genericSchema).addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // db2 only has "builtin:Email"
-        mDb2.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index a generic document into db1
-        GenericDocument genericDocument = new GenericDocument.Builder<>("uri2", "Generic")
+        GenericDocument genericDocument = new GenericDocument.Builder<>("namespace", "id2",
+                "Generic")
                 .setPropertyString("foo", "body").build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(genericDocument).build()));
+                        .addGenericDocuments(genericDocument).build()));
 
         AppSearchEmail email =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
@@ -297,10 +307,10 @@
                         .build();
 
         // Put the email in both databases
-        checkIsBatchResultSuccess((mDb1.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email).build())));
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(email).build()));
+        checkIsBatchResultSuccess((mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build())));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
 
         // Query for all documents across types
         List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
@@ -325,40 +335,38 @@
         SearchSpec exactNamespace1SearchSpec =
                 new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addNamespace("namespace1")
+                        .addFilterNamespaces("namespace1")
                         .build();
         List<GenericDocument> beforeBodyNamespace1Documents = snapshotResults("body",
                 exactNamespace1SearchSpec);
 
         // Schema registration
-        mDb1.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
-        mDb2.setSchema(new SetSchemaRequest.Builder().addSchema(AppSearchEmail.SCHEMA).build())
-                .get();
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
 
         // Index two documents
         AppSearchEmail document1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace1")
+                new AppSearchEmail.Builder("namespace1", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(document1).build()));
+                        .addGenericDocuments(document1).build()));
 
         AppSearchEmail document2 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace2")
+                new AppSearchEmail.Builder("namespace2", "id1")
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
-                new PutDocumentsRequest.Builder().addGenericDocument(document2).build()));
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
 
         // Query for all namespaces
         List<GenericDocument> afterBodyDocuments = snapshotResults("body", exactSearchSpec);
@@ -372,66 +380,122 @@
                 ImmutableList.of(document1));
     }
 
+    @Test
+    public void testGlobalQuery_packageFilter() throws Exception {
+        // Snapshot what documents may already exist on the device.
+        SearchSpec otherPackageSearchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterPackageNames("some.other.package")
+                .build();
+        List<GenericDocument> beforeOtherPackageDocuments = snapshotResults("body",
+                otherPackageSearchSpec);
+
+        SearchSpec testPackageSearchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addFilterPackageNames(
+                                ApplicationProvider.getApplicationContext().getPackageName())
+                        .build();
+        List<GenericDocument> beforeTestPackageDocuments = snapshotResults("body",
+                testPackageSearchSpec);
+
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index two documents
+        AppSearchEmail document1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(document1).build()));
+
+        AppSearchEmail document2 =
+                new AppSearchEmail.Builder("namespace2", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document2).build()));
+
+        // Query in some other package
+        List<GenericDocument> afterOtherPackageDocuments = snapshotResults("body",
+                otherPackageSearchSpec);
+        assertAddedBetweenSnapshots(beforeOtherPackageDocuments, afterOtherPackageDocuments,
+                Collections.emptyList());
+
+        // Query within our package
+        List<GenericDocument> afterTestPackageDocuments = snapshotResults("body",
+                testPackageSearchSpec);
+        assertAddedBetweenSnapshots(beforeTestPackageDocuments, afterTestPackageDocuments,
+                ImmutableList.of(document1, document2));
+    }
+
     // TODO(b/175039682) Add test cases for wildcard projection once go/oag/1534646 is submitted.
     @Test
     public void testGlobalQuery_projectionTwoInstances() throws Exception {
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
+                        .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
         mDb2.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
+                        .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
 
         // Index one document in each database.
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email1).build()));
+                        .addGenericDocuments(email1).build()));
 
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
+        checkIsBatchResultSuccess(mDb2.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email2).build()));
+                        .addGenericDocuments(email2).build()));
 
         // Query with type property paths {"Email", ["subject", "to"]}
         List<GenericDocument> documents =
                 snapshotResults("body", new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .addProjection(AppSearchEmail.SCHEMA_TYPE,
-                                "subject", "to")
+                        .addProjection(
+                                AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
                         .build());
 
         // The two email documents should have been returned with only the "subject" and "to"
         // properties.
         AppSearchEmail expected1 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setCreationTimestampMillis(1000)
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .build();
         AppSearchEmail expected2 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
@@ -444,39 +508,37 @@
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
+                        .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
         mDb2.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
+                        .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
 
         // Index one document in each database.
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email1).build()));
+                        .addGenericDocuments(email1).build()));
 
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
+        checkIsBatchResultSuccess(mDb2.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email2).build()));
+                        .addGenericDocuments(email2).build()));
 
         // Query with type property paths {"Email", []}
         List<GenericDocument> documents =
@@ -488,13 +550,11 @@
 
         // The two email documents should have been returned without any properties.
         AppSearchEmail expected1 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setCreationTimestampMillis(1000)
                         .build();
         AppSearchEmail expected2 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .build();
         assertThat(documents).containsExactly(expected1, expected2);
@@ -505,64 +565,183 @@
         // Schema registration
         mDb1.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
+                        .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
         mDb2.setSchema(
                 new SetSchemaRequest.Builder()
-                        .addSchema(AppSearchEmail.SCHEMA)
+                        .addSchemas(AppSearchEmail.SCHEMA)
                         .build()).get();
 
         // Index one document in each database.
         AppSearchEmail email1 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb1.putDocuments(
+        checkIsBatchResultSuccess(mDb1.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email1).build()));
+                        .addGenericDocuments(email1).build()));
 
         AppSearchEmail email2 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setCreationTimestampMillis(1000)
                         .setFrom("[email protected]")
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .setBody("This is the body of the testPut email")
                         .build();
-        checkIsBatchResultSuccess(mDb2.putDocuments(
+        checkIsBatchResultSuccess(mDb2.put(
                 new PutDocumentsRequest.Builder()
-                        .addGenericDocument(email2).build()));
+                        .addGenericDocuments(email2).build()));
 
         // Query with type property paths {"NonExistentType", []}, {"Email", ["subject", "to"]}
         List<GenericDocument> documents =
                 snapshotResults("body", new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .addProjection("NonExistentType", Collections.emptyList())
-                        .addProjection(AppSearchEmail.SCHEMA_TYPE, "subject", "to")
+                        .addProjection(
+                                AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
                         .build());
 
         // The two email documents should have been returned with only the "subject" and "to"
         // properties.
         AppSearchEmail expected1 =
-                new AppSearchEmail.Builder("uri2")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id2")
                         .setCreationTimestampMillis(1000)
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .build();
         AppSearchEmail expected2 =
-                new AppSearchEmail.Builder("uri1")
-                        .setNamespace("namespace")
+                new AppSearchEmail.Builder("namespace", "id1")
                         .setCreationTimestampMillis(1000)
                         .setTo("[email protected]", "[email protected]")
                         .setSubject("testPut example")
                         .build();
         assertThat(documents).containsExactly(expected1, expected2);
     }
+
+    @Test
+    public void testQuery_ResultGroupingLimits() throws Exception {
+        // Schema registration
+        mDb1.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Index one document in 'namespace1' and one document in 'namespace2' into db1.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace2", "id2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+
+        // Index one document in 'namespace1' and one document in 'namespace2' into db2.
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace1", "id3")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "id4")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.put(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+
+        // Query with per package result grouping. Only the last document 'email4' should be
+        // returned.
+        List<GenericDocument> documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setResultGrouping(
+                                SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                        .build());
+        assertThat(documents).containsExactly(inEmail4);
+
+        // Query with per namespace result grouping. Only the last document in each namespace should
+        // be returned ('email4' and 'email3').
+        documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setResultGrouping(
+                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                        .build());
+        assertThat(documents).containsExactly(inEmail4, inEmail3);
+
+        // Query with per package and per namespace result grouping. Only the last document in each
+        // namespace should be returned ('email4' and 'email3').
+        documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setResultGrouping(
+                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                        .build());
+        assertThat(documents).containsExactly(inEmail4, inEmail3);
+    }
+
+    @Test
+    public void testReportSystemUsage_ForbiddenFromNonSystem() throws Exception {
+        // Index a document
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.put(new PutDocumentsRequest.Builder().addGenericDocuments(email1).build()));
+
+        // Query
+        List<SearchResult> page;
+        try (SearchResults results = mGlobalAppSearchManager.search("", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                .build())) {
+            page = results.getNextPage().get();
+        }
+        assertThat(page).isNotEmpty();
+        SearchResult firstResult = page.get(0);
+
+        ExecutionException exception = assertThrows(
+                ExecutionException.class, () -> mGlobalAppSearchManager.reportSystemUsage(
+                        new ReportSystemUsageRequest.Builder(
+                                firstResult.getPackageName(),
+                                firstResult.getDatabaseName(),
+                                firstResult.getGenericDocument().getNamespace(),
+                                firstResult.getGenericDocument().getId())
+                                .build()).get());
+        assertThat(exception).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException ase = (AppSearchException) exception.getCause();
+        assertThat(ase.getResultCode()).isEqualTo(AppSearchResult.RESULT_SECURITY_ERROR);
+        assertThat(ase).hasMessageThat().contains(
+                "androidx.appsearch.test does not have access to report system usage");
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java
index 4586ca1..cbf44ba 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionLocalCtsTest.java
@@ -26,14 +26,12 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-// TODO(b/175801531): Support this test for the platform backend once the global search API is
-//  public.
 public class GlobalSearchSessionLocalCtsTest extends GlobalSearchSessionCtsTestBase {
     @Override
     protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
         return LocalStorage.createSearchSession(
-                new LocalStorage.SearchContext.Builder(context).setDatabaseName(dbName).build());
+                new LocalStorage.SearchContext.Builder(context, dbName).build());
     }
 
     @Override
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionPlatformCtsTest.java
new file mode 100644
index 0000000..8089c89
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionPlatformCtsTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app.cts;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class GlobalSearchSessionPlatformCtsTest extends GlobalSearchSessionCtsTestBase {
+    @Override
+    protected ListenableFuture<AppSearchSession> createSearchSession(@NonNull String dbName) {
+        Context context = ApplicationProvider.getApplicationContext();
+        return PlatformStorage.createSearchSession(
+                new PlatformStorage.SearchContext.Builder(context, dbName).build());
+    }
+
+    @Override
+    protected ListenableFuture<GlobalSearchSession> createGlobalSearchSession() {
+        Context context = ApplicationProvider.getApplicationContext();
+        return PlatformStorage.createGlobalSearchSession(
+                new PlatformStorage.GlobalSearchContext.Builder(context).build());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SearchSpecCtsTest.java
index ad3c9a5..a196bc6 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SearchSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SearchSpecCtsTest.java
@@ -18,40 +18,41 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
-
 import androidx.appsearch.app.SearchSpec;
 
 import org.junit.Test;
 
 public class SearchSpecCtsTest {
     @Test
-    public void buildSearchSpecWithoutTermMatchType() {
-        RuntimeException e = assertThrows(RuntimeException.class, () -> new SearchSpec.Builder()
-                .addSchemaType("testSchemaType")
-                .build());
-        assertThat(e).hasMessageThat().contains("Missing termMatchType field");
+    public void testBuildSearchSpecWithoutTermMatch() {
+        SearchSpec searchSpec = new SearchSpec.Builder().addFilterSchemas("testSchemaType").build();
+        assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
     }
 
     @Test
     public void testBuildSearchSpec() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addNamespace("namespace1", "namespace2")
-                .addSchemaType("schemaTypes1", "schemaTypes2")
+                .addFilterNamespaces("namespace1", "namespace2")
+                .addFilterSchemas("schemaTypes1", "schemaTypes2")
+                .addFilterPackageNames("package1", "package2")
                 .setSnippetCount(5)
                 .setSnippetCountPerProperty(10)
                 .setMaxSnippetSize(15)
                 .setResultCountPerPage(42)
                 .setOrder(SearchSpec.ORDER_ASCENDING)
                 .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 37)
                 .build();
 
         assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
-        assertThat(searchSpec.getNamespaces())
+        assertThat(searchSpec.getFilterNamespaces())
                 .containsExactly("namespace1", "namespace2").inOrder();
-        assertThat(searchSpec.getSchemaTypes())
+        assertThat(searchSpec.getFilterSchemas())
                 .containsExactly("schemaTypes1", "schemaTypes2").inOrder();
+        assertThat(searchSpec.getFilterPackageNames())
+                .containsExactly("package1", "package2").inOrder();
         assertThat(searchSpec.getSnippetCount()).isEqualTo(5);
         assertThat(searchSpec.getSnippetCountPerProperty()).isEqualTo(10);
         assertThat(searchSpec.getMaxSnippetSize()).isEqualTo(15);
@@ -59,5 +60,9 @@
         assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
         assertThat(searchSpec.getRankingStrategy())
                 .isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE);
+        assertThat(searchSpec.getResultGroupingTypeFlags())
+                .isEqualTo(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE);
+        assertThat(searchSpec.getResultGroupingLimit()).isEqualTo(37);
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SetSchemaResponseCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SetSchemaResponseCtsTest.java
new file mode 100644
index 0000000..3ba175a
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/SetSchemaResponseCtsTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.SetSchemaResponse;
+
+import org.junit.Test;
+
+public class SetSchemaResponseCtsTest {
+    @Test
+    public void testRebuild() {
+        SetSchemaResponse.MigrationFailure failure1 = new SetSchemaResponse.MigrationFailure(
+                "namespace",
+                "failure1",
+                "schemaType",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR, "errorMessage"));
+        SetSchemaResponse.MigrationFailure failure2 = new SetSchemaResponse.MigrationFailure(
+                "namespace",
+                "failure2",
+                "schemaType",
+                AppSearchResult.newFailedResult(
+                        AppSearchResult.RESULT_INTERNAL_ERROR,  "errorMessage"));
+
+        SetSchemaResponse original = new SetSchemaResponse.Builder()
+                .addDeletedType("delete1")
+                .addIncompatibleType("incompatible1")
+                .addMigratedType("migrated1")
+                .addMigrationFailure(failure1)
+                .build();
+        assertThat(original.getDeletedTypes()).containsExactly("delete1");
+        assertThat(original.getIncompatibleTypes()).containsExactly("incompatible1");
+        assertThat(original.getMigratedTypes()).containsExactly("migrated1");
+        assertThat(original.getMigrationFailures()).containsExactly(failure1);
+
+        SetSchemaResponse rebuild = original.toBuilder()
+                        .addDeletedType("delete2")
+                        .addIncompatibleType("incompatible2")
+                        .addMigratedType("migrated2")
+                        .addMigrationFailure(failure2)
+                        .build();
+
+        // rebuild won't effect the original object
+        assertThat(original.getDeletedTypes()).containsExactly("delete1");
+        assertThat(original.getIncompatibleTypes()).containsExactly("incompatible1");
+        assertThat(original.getMigratedTypes()).containsExactly("migrated1");
+        assertThat(original.getMigrationFailures()).containsExactly(failure1);
+
+        assertThat(rebuild.getDeletedTypes()).containsExactly("delete1", "delete2");
+        assertThat(rebuild.getIncompatibleTypes()).containsExactly("incompatible1",
+                "incompatible2");
+        assertThat(rebuild.getMigratedTypes()).containsExactly("migrated1", "migrated2");
+        assertThat(rebuild.getMigrationFailures()).containsExactly(failure1, failure2);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/CustomerDocumentTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/CustomerDocumentTest.java
index 07297fb..642eafd 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/CustomerDocumentTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/CustomerDocumentTest.java
@@ -35,15 +35,15 @@
     private static final byte[] BYTE_ARRAY1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
     private static final byte[] BYTE_ARRAY2 = new byte[]{(byte) 4, (byte) 5, (byte) 6};
     private static final GenericDocument DOCUMENT_PROPERTIES1 = new GenericDocument
-            .Builder<>("sDocumentProperties1", "sDocumentPropertiesSchemaType1")
+            .Builder<>("namespace", "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
             .build();
     private static final GenericDocument DOCUMENT_PROPERTIES2 = new GenericDocument
-            .Builder<>("sDocumentProperties2", "sDocumentPropertiesSchemaType2")
+            .Builder<>("namespace", "sDocumentProperties2", "sDocumentPropertiesSchemaType2")
             .build();
 
     @Test
     public void testBuildCustomerDocument() {
-        CustomerDocument customerDocument = new CustomerDocument.Builder("uri1")
+        CustomerDocument customerDocument = new CustomerDocument.Builder("namespace", "id1")
                 .setScore(1)
                 .setCreationTimestampMillis(0)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
@@ -54,7 +54,8 @@
                 .setPropertyDocument("documentKey1", DOCUMENT_PROPERTIES1, DOCUMENT_PROPERTIES2)
                 .build();
 
-        assertThat(customerDocument.getUri()).isEqualTo("uri1");
+        assertThat(customerDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(customerDocument.getId()).isEqualTo("id1");
         assertThat(customerDocument.getSchemaType()).isEqualTo("customerDocument");
         assertThat(customerDocument.getScore()).isEqualTo(1);
         assertThat(customerDocument.getCreationTimestampMillis()).isEqualTo(0L);
@@ -83,8 +84,8 @@
         }
 
         public static class Builder extends GenericDocument.Builder<CustomerDocument.Builder> {
-            private Builder(@NonNull String uri) {
-                super(uri, "customerDocument");
+            private Builder(@NonNull String namespace, @NonNull String id) {
+                super(namespace, id, "customerDocument");
             }
 
             @Override
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/EmailDataClass.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/EmailDocument.java
similarity index 64%
rename from appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/EmailDataClass.java
rename to appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/EmailDocument.java
index 1f8136d..251c722 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/EmailDataClass.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/customer/EmailDocument.java
@@ -16,17 +16,20 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.app.cts.customer;
 
-import androidx.appsearch.annotation.AppSearchDocument;
-import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 
-@AppSearchDocument
-public final class EmailDataClass {
-    @AppSearchDocument.Uri
-    public String uri;
+@Document
+public final class EmailDocument {
+    @Document.Namespace
+    public String namespace;
 
-    @AppSearchDocument.Property(indexingType = PropertyConfig.INDEXING_TYPE_PREFIXES)
+    @Document.Id
+    public String id;
+
+    @Document.Property(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
     public String subject;
 
-    @AppSearchDocument.Property(indexingType = PropertyConfig.INDEXING_TYPE_PREFIXES)
+    @Document.Property(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
     public String body;
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
index e9cade2..6e27675 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
@@ -22,12 +22,13 @@
 import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.app.GetByUriRequest;
+import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Future;
 
 public class AppSearchTestUtils {
@@ -41,30 +42,51 @@
     }
 
     public static List<GenericDocument> doGet(
-            AppSearchSession session, String namespace, String... uris) throws Exception {
+            AppSearchSession session, String namespace, String... ids) throws Exception {
         AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
-                session.getByUri(
-                        new GetByUriRequest.Builder()
-                                .setNamespace(namespace).addUri(uris).build()));
-        assertThat(result.getSuccesses()).hasSize(uris.length);
+                session.getByDocumentId(
+                        new GetByDocumentIdRequest.Builder(namespace).addIds(ids).build()));
+        assertThat(result.getSuccesses()).hasSize(ids.length);
         assertThat(result.getFailures()).isEmpty();
-        List<GenericDocument> list = new ArrayList<>(uris.length);
-        for (String uri : uris) {
-            list.add(result.getSuccesses().get(uri));
+        List<GenericDocument> list = new ArrayList<>(ids.length);
+        for (String id : ids) {
+            list.add(result.getSuccesses().get(id));
+        }
+        return list;
+    }
+
+    public static List<GenericDocument> doGet(
+            AppSearchSession session, GetByDocumentIdRequest request) throws Exception {
+        AppSearchBatchResult<String, GenericDocument> result = checkIsBatchResultSuccess(
+                session.getByDocumentId(request));
+        Set<String> ids = request.getIds();
+        assertThat(result.getSuccesses()).hasSize(ids.size());
+        assertThat(result.getFailures()).isEmpty();
+        List<GenericDocument> list = new ArrayList<>(ids.size());
+        for (String id : ids) {
+            list.add(result.getSuccesses().get(id));
         }
         return list;
     }
 
     public static List<GenericDocument> convertSearchResultsToDocuments(SearchResults searchResults)
             throws Exception {
-        List<SearchResult> results = searchResults.getNextPage().get();
-        List<GenericDocument> documents = new ArrayList<>();
-        while (results.size() > 0) {
-            for (SearchResult result : results) {
-                documents.add(result.getDocument());
-            }
-            results = searchResults.getNextPage().get();
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+        List<GenericDocument> documents = new ArrayList<>(results.size());
+        for (SearchResult result : results) {
+            documents.add(result.getGenericDocument());
         }
         return documents;
     }
+
+    public static List<SearchResult> retrieveAllSearchResults(SearchResults searchResults)
+            throws Exception {
+        List<SearchResult> page = searchResults.getNextPage().get();
+        List<SearchResult> results = new ArrayList<>();
+        while (!page.isEmpty()) {
+            results.addAll(page);
+            page = searchResults.getNextPage().get();
+        }
+        return results;
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
index 389c3ee..0a30002 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
@@ -201,6 +201,18 @@
         assertThat(BundleUtil.deepHashCode(b1)).isNotEqualTo(BundleUtil.deepHashCode(b2));
     }
 
+    @Test
+    public void testDeepHashCode_differentKeys() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            Bundle b = new Bundle();
+            b.putString("key" + i, "value");
+            inputs[i] = b;
+        }
+        assertThat(BundleUtil.deepHashCode(inputs[0]))
+                .isNotEqualTo(BundleUtil.deepHashCode(inputs[1]));
+    }
+
     private static Bundle createThoroughBundle() {
         Bundle toy1 = new Bundle();
         toy1.putString("a", "a");
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
similarity index 80%
rename from appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
rename to appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
index a4243eb..4fa85e6 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
@@ -25,10 +25,10 @@
 import java.lang.annotation.Target;
 
 /**
- * Marks a class as a data class known to AppSearch.
+ * Marks a class as an entity known to AppSearch containing a data record.
  *
  * <p>Each field annotated with {@link Property @Property} will become an AppSearch searchable
- * property. Fields annotated with other annotations included here (like {@link Uri @Uri}) will have
+ * property. Fields annotated with other annotations included here (like {@link Id @Id}) will have
  * the special behaviour described in that annotation. All other members (those which do not have
  * any of these annotations) will be ignored by AppSearch and will not be persisted or set.
  *
@@ -52,43 +52,39 @@
  *     used to populate those fields instead of methods 1 and 2.
  * </ol>
  *
- * <p>The class must also have exactly one member annotated with {@link Uri @Uri}.
+ * <p>The class must also have exactly one member annotated with {@link Id @Id}.
  */
 @Documented
 @Retention(RetentionPolicy.CLASS)
 @Target(ElementType.TYPE)
-public @interface AppSearchDocument {
+public @interface Document {
     /**
-     * Marks a member field of a document as the document's URI.
+     * Marks a member field of a document as the document's unique identifier (ID).
      *
-     * <p>Indexing a document with a particular {@link java.net.URI} replaces any existing
-     * documents with the same URI in that namespace.
+     * <p>Indexing a document with a particular ID replaces any existing documents with the same
+     * ID in that namespace.
      *
-     * <p>A document must have exactly one such field, and it must be of type {@link String} or
-     * {@link android.net.Uri}.
+     * <p>A document must have exactly one such field, and it must be of type {@link String}.
      *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. it
+     * <p>See the class description of {@link Document} for other requirements (i.e. it
      * must be visible, or have a visible getter and setter, or be exposed through a visible
      * constructor).
      */
     @Documented
     @Retention(RetentionPolicy.CLASS)
     @Target(ElementType.FIELD)
-    @interface Uri {}
+    @interface Id {}
 
     /**
      * Marks a member field of a document as the document's namespace.
      *
      * <p>The namespace is an arbitrary user-provided string that can be used to group documents
-     * during querying or deletion. Indexing a document with a particular {@link java.net.URI}
-     * replaces any existing documents with the same URI in that namespace.
+     * during querying or deletion. Indexing a document with a particular ID replaces any existing
+     * documents with the same ID in that namespace.
      *
-     * <p>This field is not required. If not present or not set, the document will be assigned to
-     * the default namespace, {@link androidx.appsearch.app.GenericDocument#DEFAULT_NAMESPACE}.
+     * <p>A document must have exactly one such field, and it must be of type {@link String}.
      *
-     * <p>If present, the field must be of type {@code String}.
-     *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. if
+     * <p>See the class description of {@link Document} for other requirements (i.e. if
      * present it must be visible, or have a visible getter and setter, or be exposed through a
      * visible constructor).
      */
@@ -108,7 +104,7 @@
      *
      * <p>If present, the field must be of type {@code long} or {@link Long}.
      *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. if
+     * <p>See the class description of {@link Document} for other requirements (i.e. if
      * present it must be visible, or have a visible getter and setter, or be exposed through a
      * visible constructor).
      */
@@ -127,7 +123,7 @@
      *
      * <p>If present, the field must be of type {@code long} or {@link Long}.
      *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. if
+     * <p>See the class description of {@link Document} for other requirements (i.e. if
      * present it must be visible, or have a visible getter and setter, or be exposed through a
      * visible constructor).
      */
@@ -147,7 +143,7 @@
      *
      * <p>If present, the field must be of type {@code int} or {@link Integer}.
      *
-     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. if
+     * <p>See the class description of {@link Document} for other requirements (i.e. if
      * present it must be visible, or have a visible getter and setter, or be exposed through a
      * visible constructor).
      */
@@ -179,22 +175,22 @@
          * Configures how tokens should be extracted from this property.
          *
          * <p>If not specified, defaults to {@link
-         * AppSearchSchema.PropertyConfig#TOKENIZER_TYPE_PLAIN} (the field will be tokenized
+         * AppSearchSchema.StringPropertyConfig#TOKENIZER_TYPE_PLAIN} (the field will be tokenized
          * along word boundaries as plain text).
          */
-        @AppSearchSchema.PropertyConfig.TokenizerType int tokenizerType()
-                default AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+        @AppSearchSchema.StringPropertyConfig.TokenizerType int tokenizerType()
+                default AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN;
 
         /**
          * Configures how a property should be indexed so that it can be retrieved by queries.
          *
          * <p>If not specified, defaults to {@link
-         * AppSearchSchema.PropertyConfig#INDEXING_TYPE_NONE} (the field will not be indexed and
-         * cannot be queried).
+         * AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE} (the field will not be indexed
+         * and cannot be queried).
          * TODO(b/171857731) renamed to TermMatchType when using String-specific indexing config.
          */
-        @AppSearchSchema.PropertyConfig.IndexingType int indexingType()
-                default AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+        @AppSearchSchema.StringPropertyConfig.IndexingType int indexingType()
+                default AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
 
         /**
          * Configures whether this property must be specified for the document to be valid.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
index 302545b..98d3c92 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
@@ -24,21 +24,32 @@
 import java.util.Map;
 
 /**
- * Provides access to multiple {@link AppSearchResult}s from a batch operation accepting multiple
- * inputs.
+ * Provides results for AppSearch batch operations which encompass multiple documents.
  *
- * @param <KeyType> The type of the keys for {@link #getSuccesses} and {@link #getFailures}.
- * @param <ValueType> The type of result objects associated with the keys.
+ * <p>Individual results of a batch operation are separated into two maps: one for successes and
+ * one for failures. For successes, {@link #getSuccesses()} will return a map of keys to
+ * instances of the value type. For failures, {@link #getFailures()} will return a map of keys to
+ * {@link AppSearchResult} objects.
+ *
+ * <p>Alternatively, {@link #getAll()} returns a map of keys to {@link AppSearchResult} objects for
+ * both successes and failures.
+ *
+ * @see AppSearchSession#put
+ * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#remove
  */
 public final class AppSearchBatchResult<KeyType, ValueType> {
     @NonNull private final Map<KeyType, ValueType> mSuccesses;
     @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mFailures;
+    @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mAll;
 
     AppSearchBatchResult(
             @NonNull Map<KeyType, ValueType> successes,
-            @NonNull Map<KeyType, AppSearchResult<ValueType>> failures) {
+            @NonNull Map<KeyType, AppSearchResult<ValueType>> failures,
+            @NonNull Map<KeyType, AppSearchResult<ValueType>> all) {
         mSuccesses = successes;
         mFailures = failures;
+        mAll = all;
     }
 
     /** Returns {@code true} if this {@link AppSearchBatchResult} has no failures. */
@@ -47,8 +58,12 @@
     }
 
     /**
-     * Returns a {@link Map} of all successful keys mapped to the successful
-     * {@link AppSearchResult}s they produced.
+     * Returns a {@link Map} of keys mapped to instances of the value type for all successful
+     * individual results.
+     *
+     * <p>Example: {@link AppSearchSession#getByDocumentId} returns an {@link AppSearchBatchResult}.
+     * Each key (the document ID, of {@code String} type) will map to a {@link GenericDocument}
+     * object.
      *
      * <p>The values of the {@link Map} will not be {@code null}.
      */
@@ -58,8 +73,8 @@
     }
 
     /**
-     * Returns a {@link Map} of all failed keys mapped to the failed {@link AppSearchResult}s they
-     * produced.
+     * Returns a {@link Map} of keys mapped to instances of {@link AppSearchResult} for all
+     * failed individual results.
      *
      * <p>The values of the {@link Map} will not be {@code null}.
      */
@@ -69,6 +84,17 @@
     }
 
     /**
+     * Returns a {@link Map} of keys mapped to instances of {@link AppSearchResult} for all
+     * individual results.
+     *
+     * <p>The values of the {@link Map} will not be {@code null}.
+     */
+    @NonNull
+    public Map<KeyType, AppSearchResult<ValueType>> getAll() {
+        return mAll;
+    }
+
+    /**
      * Asserts that this {@link AppSearchBatchResult} has no failures.
      * @hide
      */
@@ -87,20 +113,22 @@
     /**
      * Builder for {@link AppSearchBatchResult} objects.
      *
-     * @param <KeyType> The type of keys.
-     * @param <ValueType> The type of result objects associated with the keys.
-     * @hide
+     * <p>Once {@link #build} is called, the instance can no longer be used.
      */
     public static final class Builder<KeyType, ValueType> {
         private final Map<KeyType, ValueType> mSuccesses = new ArrayMap<>();
         private final Map<KeyType, AppSearchResult<ValueType>> mFailures = new ArrayMap<>();
+        private final Map<KeyType, AppSearchResult<ValueType>> mAll = new ArrayMap<>();
         private boolean mBuilt = false;
 
         /**
-         * Associates the {@code key} with the given successful return value.
+         * Associates the {@code key} with the provided successful return value.
          *
          * <p>Any previous mapping for a key, whether success or failure, is deleted.
+         *
+         * @throws IllegalStateException if the builder has already been used.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")  // See getSuccesses
         @NonNull
         public Builder<KeyType, ValueType> setSuccess(
                 @NonNull KeyType key, @Nullable ValueType result) {
@@ -110,10 +138,13 @@
         }
 
         /**
-         * Associates the {@code key} with the given failure code and error message.
+         * Associates the {@code key} with the provided failure code and error message.
          *
          * <p>Any previous mapping for a key, whether success or failure, is deleted.
+         *
+         * @throws IllegalStateException if the builder has already been used.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")  // See getFailures
         @NonNull
         public Builder<KeyType, ValueType> setFailure(
                 @NonNull KeyType key,
@@ -125,10 +156,13 @@
         }
 
         /**
-         * Associates the {@code key} with the given {@code result}.
+         * Associates the {@code key} with the provided {@code result}.
          *
          * <p>Any previous mapping for a key, whether success or failure, is deleted.
+         *
+         * @throws IllegalStateException if the builder has already been used.
          */
+        @SuppressWarnings("MissingGetterMatchingBuilder")  // See getAll
         @NonNull
         public Builder<KeyType, ValueType> setResult(
                 @NonNull KeyType key, @NonNull AppSearchResult<ValueType> result) {
@@ -142,15 +176,20 @@
                 mFailures.put(key, result);
                 mSuccesses.remove(key);
             }
+            mAll.put(key, result);
             return this;
         }
 
-        /** Builds an {@link AppSearchBatchResult} from the contents of this {@link Builder}. */
+        /**
+         * Builds an {@link AppSearchBatchResult} object from the contents of this {@link Builder}.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
         @NonNull
         public AppSearchBatchResult<KeyType, ValueType> build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             mBuilt = true;
-            return new AppSearchBatchResult<>(mSuccesses, mFailures);
+            return new AppSearchBatchResult<>(mSuccesses, mFailures, mAll);
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEmail.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEmail.java
index 9f7aa0e..eb2f8a1 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEmail.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEmail.java
@@ -21,6 +21,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 
 /**
  * Encapsulates a {@link GenericDocument} that represent an email.
@@ -42,46 +43,40 @@
     private static final String KEY_BODY = "body";
 
     public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
-            .addProperty(new PropertyConfig.Builder(KEY_FROM)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            .addProperty(new StringPropertyConfig.Builder(KEY_FROM)
                     .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_TO)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_TO)
                     .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_CC)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_CC)
                     .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_BCC)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_BCC)
                     .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_SUBJECT)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_SUBJECT)
                     .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
-            ).addProperty(new PropertyConfig.Builder(KEY_BODY)
-                    .setDataType(PropertyConfig.DATA_TYPE_STRING)
+            ).addProperty(new StringPropertyConfig.Builder(KEY_BODY)
                     .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                    .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
                     .build()
 
             ).build();
@@ -161,14 +156,14 @@
      * The builder class for {@link AppSearchEmail}.
      */
     public static class Builder extends GenericDocument.Builder<AppSearchEmail.Builder> {
-
         /**
          * Creates a new {@link AppSearchEmail.Builder}
          *
-         * @param uri The Uri of the Email.
+         * @param namespace The namespace of the Email.
+         * @param id The ID of the Email.
          */
-        public Builder(@NonNull String uri) {
-            super(uri, SCHEMA_TYPE);
+        public Builder(@NonNull String namespace, @NonNull String id) {
+            super(namespace, id, SCHEMA_TYPE);
         }
 
         /**
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
index 75a1053..b018ed0 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
@@ -16,12 +16,15 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
+import android.util.Log;
+
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.core.util.ObjectsCompat;
+import androidx.core.util.Preconditions;
 
 import java.io.IOException;
 import java.lang.annotation.Retention;
@@ -33,6 +36,8 @@
  * @param <ValueType> The type of result object for successful calls.
  */
 public final class AppSearchResult<ValueType> {
+    private static final String TAG = "AppSearchResult";
+
     /**
      * Result codes from {@link AppSearchSession} methods.
      * @hide
@@ -46,6 +51,7 @@
             RESULT_OUT_OF_SPACE,
             RESULT_NOT_FOUND,
             RESULT_INVALID_SCHEMA,
+            RESULT_SECURITY_ERROR,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ResultCode {}
@@ -86,6 +92,9 @@
     /** The caller supplied a schema which is invalid or incompatible with the previous schema. */
     public static final int RESULT_INVALID_SCHEMA = 7;
 
+    /** The caller requested an operation it does not have privileges for. */
+    public static final int RESULT_SECURITY_ERROR = 8;
+
     private final @ResultCode int mResultCode;
     @Nullable private final ValueType mResultValue;
     @Nullable private final String mErrorMessage;
@@ -168,7 +177,6 @@
 
     /**
      * Creates a new successful {@link AppSearchResult}.
-     * @hide
      */
     @NonNull
     public static <ValueType> AppSearchResult<ValueType> newSuccessfulResult(
@@ -178,7 +186,6 @@
 
     /**
      * Creates a new failed {@link AppSearchResult}.
-     * @hide
      */
     @NonNull
     public static <ValueType> AppSearchResult<ValueType> newFailedResult(
@@ -186,17 +193,42 @@
         return new AppSearchResult<>(resultCode, /*resultValue=*/ null, errorMessage);
     }
 
+    /**
+     * Creates a new failed {@link AppSearchResult} by a AppSearchResult in another type.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static <ValueType> AppSearchResult<ValueType> newFailedResult(
+            @NonNull AppSearchResult<?> otherFailedResult) {
+        Preconditions.checkState(!otherFailedResult.isSuccess(),
+                "Cannot convert a success result to a failed result");
+        return AppSearchResult.newFailedResult(
+                otherFailedResult.getResultCode(), otherFailedResult.getErrorMessage());
+    }
+
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
     public static <ValueType> AppSearchResult<ValueType> throwableToFailedResult(
             @NonNull Throwable t) {
+        // Log for traceability. NOT_FOUND is logged at VERBOSE because this error can occur during
+        // the regular operation of the system (b/183550974). Everything else is logged at DEBUG.
+        if (t instanceof AppSearchException
+                && ((AppSearchException) t).getResultCode() == RESULT_NOT_FOUND) {
+            Log.v(TAG, "Converting throwable to failed result: " + t);
+        } else {
+            Log.d(TAG, "Converting throwable to failed result.", t);
+        }
+
         if (t instanceof AppSearchException) {
             return ((AppSearchException) t).toAppSearchResult();
         }
 
+        String exceptionClass = t.getClass().getSimpleName();
         @AppSearchResult.ResultCode int resultCode;
-        if (t instanceof IllegalStateException) {
+        if (t instanceof IllegalStateException || t instanceof NullPointerException) {
             resultCode = AppSearchResult.RESULT_INTERNAL_ERROR;
         } else if (t instanceof IllegalArgumentException) {
             resultCode = AppSearchResult.RESULT_INVALID_ARGUMENT;
@@ -205,6 +237,6 @@
         } else {
             resultCode = AppSearchResult.RESULT_UNKNOWN_ERROR;
         }
-        return AppSearchResult.newFailedResult(resultCode, t.toString());
+        return AppSearchResult.newFailedResult(resultCode, exceptionClass + ": " + t.getMessage());
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index aad7c19..3c13dcc 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -16,7 +16,6 @@
 
 package androidx.appsearch.app;
 
-import android.annotation.SuppressLint;
 import android.os.Bundle;
 
 import androidx.annotation.IntDef;
@@ -94,7 +93,7 @@
         }
         List<PropertyConfig> ret = new ArrayList<>(propertyBundles.size());
         for (int i = 0; i < propertyBundles.size(); i++) {
-            ret.add(new PropertyConfig(propertyBundles.get(i)));
+            ret.add(PropertyConfig.fromBundle(propertyBundles.get(i)));
         }
         return ret;
     }
@@ -133,10 +132,6 @@
         }
 
         /** Adds a property to the given type. */
-        // TODO(b/171360120): MissingGetterMatchingBuilder expects a method called getPropertys, but
-        //  we provide the (correct) method getProperties. Once the bug referenced in this TODO is
-        //  fixed, remove this SuppressLint.
-        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
         public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
@@ -166,18 +161,15 @@
     }
 
     /**
-     * Configuration for a single property (field) of a document type.
+     * Common configuration for a single property (field) in a Document.
      *
      * <p>For example, an {@code EmailMessage} would be a type and the {@code subject} would be
      * a property.
      */
-    public static final class PropertyConfig {
-        private static final String NAME_FIELD = "name";
-        private static final String DATA_TYPE_FIELD = "dataType";
-        private static final String SCHEMA_TYPE_FIELD = "schemaType";
-        private static final String CARDINALITY_FIELD = "cardinality";
-        private static final String INDEXING_TYPE_FIELD = "indexingType";
-        private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";
+    public abstract static class PropertyConfig {
+        static final String NAME_FIELD = "name";
+        static final String DATA_TYPE_FIELD = "dataType";
+        static final String CARDINALITY_FIELD = "cardinality";
 
         /**
          * Physical data-types of the contents of the property.
@@ -196,18 +188,29 @@
         @Retention(RetentionPolicy.SOURCE)
         public @interface DataType {}
 
+        /** @hide */
         public static final int DATA_TYPE_STRING = 1;
+
+        /** @hide */
         public static final int DATA_TYPE_INT64 = 2;
+
+        /** @hide */
         public static final int DATA_TYPE_DOUBLE = 3;
+
+        /** @hide */
         public static final int DATA_TYPE_BOOLEAN = 4;
 
-        /** Unstructured BLOB. */
+        /**
+         * Unstructured BLOB.
+         * @hide
+         */
         public static final int DATA_TYPE_BYTES = 5;
 
         /**
          * Indicates that the property is itself a {@link GenericDocument}, making it part of a
          * hierarchical schema. Any property using this DataType MUST have a valid
          * {@link PropertyConfig#getSchemaType}.
+         * @hide
          */
         public static final int DATA_TYPE_DOCUMENT = 6;
 
@@ -234,6 +237,102 @@
         /** Exactly one value [1]. */
         public static final int CARDINALITY_REQUIRED = 3;
 
+        final Bundle mBundle;
+
+        @Nullable
+        private Integer mHashCode;
+
+        PropertyConfig(@NonNull Bundle bundle) {
+            mBundle = Preconditions.checkNotNull(bundle);
+        }
+
+        @Override
+        public String toString() {
+            return mBundle.toString();
+        }
+
+        /** Returns the name of this property. */
+        @NonNull
+        public String getName() {
+            return mBundle.getString(NAME_FIELD, "");
+        }
+
+        /**
+         * Returns the type of data the property contains (e.g. string, int, bytes, etc).
+         *
+         * @hide
+         */
+        public @DataType int getDataType() {
+            return mBundle.getInt(DATA_TYPE_FIELD, -1);
+        }
+
+        /**
+         * Returns the cardinality of the property (whether it is optional, required or repeated).
+         */
+        public @Cardinality int getCardinality() {
+            return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof PropertyConfig)) {
+                return false;
+            }
+            PropertyConfig otherProperty = (PropertyConfig) other;
+            return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
+        }
+
+        @Override
+        public int hashCode() {
+            if (mHashCode == null) {
+                mHashCode = BundleUtil.deepHashCode(mBundle);
+            }
+            return mHashCode;
+        }
+
+        /**
+         * Converts a {@link Bundle} into a {@link PropertyConfig} depending on its internal data
+         * type.
+         *
+         * <p>The bundle is not cloned.
+         *
+         * @throws IllegalArgumentException if the bundle does no contain a recognized
+         * value in its {@code DATA_TYPE_FIELD}.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) {
+            switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) {
+                case PropertyConfig.DATA_TYPE_STRING:
+                    return new StringPropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_INT64:
+                    return new Int64PropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_DOUBLE:
+                    return new DoublePropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_BOOLEAN:
+                    return new BooleanPropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_BYTES:
+                    return new BytesPropertyConfig(propertyBundle);
+                case PropertyConfig.DATA_TYPE_DOCUMENT:
+                    return new DocumentPropertyConfig(propertyBundle);
+                default:
+                    throw new IllegalArgumentException(
+                            "Unsupported property bundle of type "
+                                    + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)
+                                    + "; contents: " + propertyBundle);
+            }
+        }
+    }
+
+    /** Configuration for a property of type String in a Document. */
+    public static final class StringPropertyConfig extends PropertyConfig {
+        private static final String INDEXING_TYPE_FIELD = "indexingType";
+        private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";
+
         /**
          * Encapsulates the configurations on how AppSearch should query/index these terms.
          * @hide
@@ -246,14 +345,7 @@
         @Retention(RetentionPolicy.SOURCE)
         public @interface IndexingType {}
 
-        /**
-         * Content in this property will not be tokenized or indexed.
-         *
-         * <p>Useful if the data type is not made up of terms (e.g.
-         * {@link PropertyConfig#DATA_TYPE_DOCUMENT} or {@link PropertyConfig#DATA_TYPE_BYTES}
-         * type). None of the properties inside the nested property will be indexed regardless of
-         * the value of {@code indexingType} for the nested properties.
-         */
+        /** Content in this property will not be tokenized or indexed. */
         public static final int INDEXING_TYPE_NONE = 0;
 
         /**
@@ -286,55 +378,16 @@
         public @interface TokenizerType {}
 
         /**
-         * It is only valid for tokenizer_type to be 'NONE' if the data type is
-         * {@link PropertyConfig#DATA_TYPE_DOCUMENT}.
+         * It is only valid for tokenizer_type to be 'NONE' if {@link #getIndexingType} is
+         * {@link #INDEXING_TYPE_NONE}.
          */
         public static final int TOKENIZER_TYPE_NONE = 0;
 
         /** Tokenization for plain text. */
         public static final int TOKENIZER_TYPE_PLAIN = 1;
 
-        final Bundle mBundle;
-
-        @Nullable
-        private Integer mHashCode;
-
-        PropertyConfig(@NonNull Bundle bundle) {
-            mBundle = Preconditions.checkNotNull(bundle);
-        }
-
-        @Override
-        public String toString() {
-            return mBundle.toString();
-        }
-
-        /** Returns the name of this property. */
-        @NonNull
-        public String getName() {
-            return mBundle.getString(NAME_FIELD, "");
-        }
-
-        /** Returns the type of data the property contains (e.g. string, int, bytes, etc). */
-        public @DataType int getDataType() {
-            return mBundle.getInt(DATA_TYPE_FIELD, -1);
-        }
-
-        /**
-         * Returns the logical schema-type of the contents of this property.
-         *
-         * <p>Only set when {@link #getDataType} is set to {@link #DATA_TYPE_DOCUMENT}.
-         * Otherwise, it is {@code null}.
-         */
-        @Nullable
-        public String getSchemaType() {
-            return mBundle.getString(SCHEMA_TYPE_FIELD);
-        }
-
-        /**
-         * Returns the cardinality of the property (whether it is optional, required or repeated).
-         */
-        public @Cardinality int getCardinality() {
-            return mBundle.getInt(CARDINALITY_FIELD, -1);
+        StringPropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
         }
 
         /** Returns how the property is indexed. */
@@ -347,83 +400,29 @@
             return mBundle.getInt(TOKENIZER_TYPE_FIELD);
         }
 
-        @Override
-        public boolean equals(@Nullable Object other) {
-            if (this == other) {
-                return true;
-            }
-            if (!(other instanceof PropertyConfig)) {
-                return false;
-            }
-            PropertyConfig otherProperty = (PropertyConfig) other;
-            return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
-        }
-
-        @Override
-        public int hashCode() {
-            if (mHashCode == null) {
-                mHashCode = BundleUtil.deepHashCode(mBundle);
-            }
-            return mHashCode;
-        }
-
-        /**
-         * Builder for {@link PropertyConfig}.
-         *
-         * <p>The following properties must be set, or {@link PropertyConfig} construction will
-         * fail:
-         * <ul>
-         *     <li>dataType
-         *     <li>cardinality
-         * </ul>
-         *
-         * <p>In addition, if {@code schemaType} is {@link #DATA_TYPE_DOCUMENT}, {@code schemaType}
-         * is also required.
-         */
+        /** Builder for {@link StringPropertyConfig}. */
         public static final class Builder {
             private final Bundle mBundle = new Bundle();
             private boolean mBuilt = false;
 
-            /** Creates a new {@link PropertyConfig.Builder}. */
+            /** Creates a new {@link StringPropertyConfig.Builder}. */
             public Builder(@NonNull String propertyName) {
                 mBundle.putString(NAME_FIELD, propertyName);
-            }
-
-            /**
-             * Type of data the property contains (e.g. string, int, bytes, etc).
-             *
-             * <p>This property must be set.
-             */
-            @NonNull
-            public PropertyConfig.Builder setDataType(@DataType int dataType) {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
-                Preconditions.checkArgumentInRange(
-                        dataType, DATA_TYPE_STRING, DATA_TYPE_DOCUMENT, "dataType");
-                mBundle.putInt(DATA_TYPE_FIELD, dataType);
-                return this;
-            }
-
-            /**
-             * The logical schema-type of the contents of this property.
-             *
-             * <p>Only required when {@link #setDataType} is set to
-             * {@link #DATA_TYPE_DOCUMENT}. Otherwise, it is ignored.
-             */
-            @NonNull
-            public PropertyConfig.Builder setSchemaType(@NonNull String schemaType) {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
-                Preconditions.checkNotNull(schemaType);
-                mBundle.putString(SCHEMA_TYPE_FIELD, schemaType);
-                return this;
+                mBundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING);
+                mBundle.putInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+                mBundle.putInt(INDEXING_TYPE_FIELD, INDEXING_TYPE_NONE);
+                mBundle.putInt(TOKENIZER_TYPE_FIELD, TOKENIZER_TYPE_NONE);
             }
 
             /**
              * The cardinality of the property (whether it is optional, required or repeated).
              *
-             * <p>This property must be set.
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
              */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
             @NonNull
-            public PropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+            public StringPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
                 Preconditions.checkState(!mBuilt, "Builder has already been used");
                 Preconditions.checkArgumentInRange(
                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
@@ -433,9 +432,13 @@
 
             /**
              * Configures how a property should be indexed so that it can be retrieved by queries.
+             *
+             * <p>If this method is not called, the default indexing type is
+             * {@link StringPropertyConfig#INDEXING_TYPE_NONE}, so that it cannot be matched by
+             * queries.
              */
             @NonNull
-            public PropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+            public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
                 Preconditions.checkState(!mBuilt, "Builder has already been used");
                 Preconditions.checkArgumentInRange(
                         indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType");
@@ -443,9 +446,19 @@
                 return this;
             }
 
-            /** Configures how this property should be tokenized (split into words). */
+            /**
+             * Configures how this property should be tokenized (split into words).
+             *
+             * <p>If this method is not called, the default indexing type is
+             * {@link StringPropertyConfig#TOKENIZER_TYPE_NONE}, so that it is not tokenized.
+             *
+             * <p>This method must be called with a value other than
+             * {@link StringPropertyConfig#TOKENIZER_TYPE_NONE} if the property is indexed (i.e.
+             * if {@link #setIndexingType} has been called with a value other than
+             * {@link StringPropertyConfig#INDEXING_TYPE_NONE}).
+             */
             @NonNull
-            public PropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
+            public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
                 Preconditions.checkState(!mBuilt, "Builder has already been used");
                 Preconditions.checkArgumentInRange(
                         tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType");
@@ -454,32 +467,323 @@
             }
 
             /**
+             * Constructs a new {@link StringPropertyConfig} from the contents of this builder.
+             *
+             * <p>After calling this method, the builder must no longer be used.
+             *
+             * @throws IllegalStateException if the builder has already been used
+             */
+            @NonNull
+            public StringPropertyConfig build() {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                mBuilt = true;
+                return new StringPropertyConfig(mBundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing a 64-bit integer. */
+    public static final class Int64PropertyConfig extends PropertyConfig {
+        Int64PropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Builder for {@link Int64PropertyConfig}. */
+        public static final class Builder {
+            private final Bundle mBundle = new Bundle();
+            private boolean mBuilt = false;
+
+            /** Creates a new {@link Int64PropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mBundle.putString(NAME_FIELD, propertyName);
+                mBundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_INT64);
+                mBundle.putInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public Int64PropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mBundle.putInt(CARDINALITY_FIELD, cardinality);
+                return this;
+            }
+
+            /**
+             * Constructs a new {@link Int64PropertyConfig} from the contents of this builder.
+             *
+             * <p>After calling this method, the builder must no longer be used.
+             *
+             * @throws IllegalStateException if the builder has already been used
+             */
+            @NonNull
+            public Int64PropertyConfig build() {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                mBuilt = true;
+                return new Int64PropertyConfig(mBundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing a double-precision decimal number. */
+    public static final class DoublePropertyConfig extends PropertyConfig {
+        DoublePropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Builder for {@link DoublePropertyConfig}. */
+        public static final class Builder {
+            private final Bundle mBundle = new Bundle();
+            private boolean mBuilt = false;
+
+            /** Creates a new {@link DoublePropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mBundle.putString(NAME_FIELD, propertyName);
+                mBundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE);
+                mBundle.putInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public DoublePropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mBundle.putInt(CARDINALITY_FIELD, cardinality);
+                return this;
+            }
+
+            /**
+             * Constructs a new {@link DoublePropertyConfig} from the contents of this builder.
+             *
+             * <p>After calling this method, the builder must no longer be used.
+             *
+             * @throws IllegalStateException if the builder has already been used
+             */
+            @NonNull
+            public DoublePropertyConfig build() {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                mBuilt = true;
+                return new DoublePropertyConfig(mBundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing a boolean. */
+    public static final class BooleanPropertyConfig extends PropertyConfig {
+        BooleanPropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Builder for {@link BooleanPropertyConfig}. */
+        public static final class Builder {
+            private final Bundle mBundle = new Bundle();
+            private boolean mBuilt = false;
+
+            /** Creates a new {@link BooleanPropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mBundle.putString(NAME_FIELD, propertyName);
+                mBundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN);
+                mBundle.putInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public BooleanPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mBundle.putInt(CARDINALITY_FIELD, cardinality);
+                return this;
+            }
+
+            /**
+             * Constructs a new {@link BooleanPropertyConfig} from the contents of this builder.
+             *
+             * <p>After calling this method, the builder must no longer be used.
+             *
+             * @throws IllegalStateException if the builder has already been used
+             */
+            @NonNull
+            public BooleanPropertyConfig build() {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                mBuilt = true;
+                return new BooleanPropertyConfig(mBundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing a byte array. */
+    public static final class BytesPropertyConfig extends PropertyConfig {
+        BytesPropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Builder for {@link BytesPropertyConfig}. */
+        public static final class Builder {
+            private final Bundle mBundle = new Bundle();
+            private boolean mBuilt = false;
+
+            /** Creates a new {@link BytesPropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mBundle.putString(NAME_FIELD, propertyName);
+                mBundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES);
+                mBundle.putInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public BytesPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mBundle.putInt(CARDINALITY_FIELD, cardinality);
+                return this;
+            }
+
+            /**
+             * Constructs a new {@link BytesPropertyConfig} from the contents of this builder.
+             *
+             * <p>After calling this method, the builder must no longer be used.
+             *
+             * @throws IllegalStateException if the builder has already been used
+             */
+            @NonNull
+            public BytesPropertyConfig build() {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                mBuilt = true;
+                return new BytesPropertyConfig(mBundle);
+            }
+        }
+    }
+
+    /** Configuration for a property containing another Document. */
+    public static final class DocumentPropertyConfig extends PropertyConfig {
+        private static final String SCHEMA_TYPE_FIELD = "schemaType";
+        private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties";
+
+        DocumentPropertyConfig(@NonNull Bundle bundle) {
+            super(bundle);
+        }
+
+        /** Returns the logical schema-type of the contents of this document property. */
+        @NonNull
+        public String getSchemaType() {
+            return Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
+        }
+
+        /**
+         * Returns whether fields in the nested document should be indexed according to that
+         * document's schema.
+         *
+         * <p>If false, the nested document's properties are not indexed regardless of its own
+         * schema.
+         */
+        public boolean shouldIndexNestedProperties() {
+            return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD);
+        }
+
+        /**
+         * Builder for {@link DocumentPropertyConfig}.
+         *
+         * <p>The following properties must be set, or {@link DocumentPropertyConfig} construction
+         * will fail:
+         * <ul>
+         *     <li>cardinality
+         *     <li>schemaType
+         * </ul>
+         */
+        public static final class Builder {
+            private final Bundle mBundle = new Bundle();
+            private boolean mBuilt = false;
+
+            /**
+             * Creates a new {@link DocumentPropertyConfig.Builder}.
+             *
+             * @param propertyName The logical name of the property in the schema, which will be
+             *                     used as the key for this property in
+             *                     {@link GenericDocument.Builder#setPropertyDocument}.
+             * @param schemaType The type of documents which will be stored in this property.
+             *                   Documents of different types cannot be mixed into a single
+             *                   property.
+             */
+            public Builder(@NonNull String propertyName, @NonNull String schemaType) {
+                mBundle.putString(NAME_FIELD, propertyName);
+                mBundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT);
+                mBundle.putInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
+                mBundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, false);
+                mBundle.putString(SCHEMA_TYPE_FIELD, schemaType);
+            }
+
+            /**
+             * The cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is
+             * {@link PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @SuppressWarnings("MissingGetterMatchingBuilder")  // getter defined in superclass
+            @NonNull
+            public DocumentPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mBundle.putInt(CARDINALITY_FIELD, cardinality);
+                return this;
+            }
+
+            /**
+             * Configures whether fields in the nested document should be indexed according to that
+             * document's schema.
+             *
+             * <p>If false, the nested document's properties are not indexed regardless of its own
+             * schema.
+             */
+            @NonNull
+            public DocumentPropertyConfig.Builder setShouldIndexNestedProperties(
+                    boolean indexNestedProperties) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                mBundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, indexNestedProperties);
+                return this;
+            }
+
+            /**
              * Constructs a new {@link PropertyConfig} from the contents of this builder.
              *
              * <p>After calling this method, the builder must no longer be used.
              *
-             * @throws IllegalSchemaException If the property is not correctly populated (e.g.
+             * @throws IllegalStateException if the builder has already been used (e.g.
              *     missing {@code dataType}).
              */
             @NonNull
-            public PropertyConfig build() {
+            public DocumentPropertyConfig build() {
                 Preconditions.checkState(!mBuilt, "Builder has already been used");
-                // TODO(b/147692920): Send the schema to Icing Lib for official validation, instead
-                //     of partially reimplementing some of the validation Icing does here.
-                if (!mBundle.containsKey(DATA_TYPE_FIELD)) {
-                    throw new IllegalSchemaException("Missing field: dataType");
-                }
-                if (mBundle.getString(SCHEMA_TYPE_FIELD, "").isEmpty()
-                        && mBundle.getInt(DATA_TYPE_FIELD) == DATA_TYPE_DOCUMENT) {
-                    throw new IllegalSchemaException(
-                            "Missing field: schemaType (required for configs with "
-                                    + "dataType = DOCUMENT)");
-                }
-                if (!mBundle.containsKey(CARDINALITY_FIELD)) {
-                    throw new IllegalSchemaException("Missing field: cardinality");
-                }
                 mBuilt = true;
-                return new PropertyConfig(mBundle);
+                return new DocumentPropertyConfig(mBundle);
             }
         }
     }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index 76ce163..13bd787 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -26,176 +26,200 @@
 import java.util.Set;
 
 /**
- * Represents a connection to an AppSearch storage system where {@link GenericDocument}s can be
- * placed and queried.
+ * Provides a connection to a single AppSearch database.
  *
- * All implementations of this interface must be thread safe.
+ * <p>An {@link AppSearchSession} instance provides access to database operations such as setting
+ * a schema, adding documents, and searching.
+ *
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @see GlobalSearchSession
  */
 public interface AppSearchSession extends Closeable {
 
     /**
-     * Sets the schema that will be used by documents provided to the {@link #putDocuments} method.
+     * Sets the schema that represents the organizational structure of data within the AppSearch
+     * database.
      *
-     * <p>The schema provided here is compared to the stored copy of the schema previously supplied
-     * to {@link #setSchema}, if any, to determine how to treat existing documents. The following
-     * types of schema modifications are always safe and are made without deleting any existing
-     * documents:
-     * <ul>
-     *     <li>Addition of new types
-     *     <li>Addition of new
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL OPTIONAL} or
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_REPEATED REPEATED} properties to a
-     *         type
-     *     <li>Changing the cardinality of a data type to be less restrictive (e.g. changing an
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL OPTIONAL} property into a
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_REPEATED REPEATED} property.
-     * </ul>
+     * <p>Upon creating an {@link AppSearchSession}, {@link #setSchema} should be called. If the
+     * schema needs to be updated, or it has not been previously set, then the provided schema
+     * will be saved and persisted to disk. Otherwise, {@link #setSchema} is handled efficiently
+     * as a no-op call.
      *
-     * <p>The following types of schema changes are not backwards-compatible:
-     * <ul>
-     *     <li>Removal of an existing type
-     *     <li>Removal of a property from a type
-     *     <li>Changing the data type ({@code boolean}, {@code long}, etc.) of an existing property
-     *     <li>For properties of {@code Document} type, changing the schema type of
-     *         {@code Document}s of that property
-     *     <li>Changing the cardinality of a data type to be more restrictive (e.g. changing an
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_OPTIONAL OPTIONAL} property into a
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_REQUIRED REQUIRED} property).
-     *     <li>Adding a
-     *         {@link AppSearchSchema.PropertyConfig#CARDINALITY_REQUIRED REQUIRED} property.
-     * </ul>
-     * <p>Supplying a schema with such changes will, by default, result in this call completing its
-     * future with an {@link androidx.appsearch.exceptions.AppSearchException} with a code of
-     * {@link AppSearchResult#RESULT_INVALID_SCHEMA} and a message describing the incompatibility.
-     * In this case the previously set schema will remain active.
-     *
-     * <p>If you need to make non-backwards-compatible changes as described above, you can set the
-     * {@link SetSchemaRequest.Builder#setForceOverride} method to {@code true}. In this case,
-     * instead of completing its future with an
-     * {@link androidx.appsearch.exceptions.AppSearchException} with the
-     * {@link AppSearchResult#RESULT_INVALID_SCHEMA} error code, all documents which are not
-     * compatible with the new schema will be deleted and the incompatible schema will be applied.
-     *
-     * <p>It is a no-op to set the same schema as has been previously set; this is handled
-     * efficiently.
-     *
-     * <p>By default, documents are visible on platform surfaces. To opt out, call {@code
-     * SetSchemaRequest.Builder#setPlatformSurfaceable} with {@code surfaceable} as false. Any
-     * visibility settings apply only to the schemas that are included in the {@code request}.
-     * Visibility settings for a schema type do not apply or persist across
-     * {@link SetSchemaRequest}s.
-     *
-     * @param request The schema update request.
-     * @return The pending result of performing this operation.
+     * @param  request the schema to set or update the AppSearch database to.
+     * @return a {@link ListenableFuture} which resolves to a {@link SetSchemaResponse} object.
      */
     // TODO(b/169883602): Change @code references to @link when setPlatformSurfaceable APIs are
     //  exposed.
     @NonNull
-    ListenableFuture<Void> setSchema(@NonNull SetSchemaRequest request);
+    ListenableFuture<SetSchemaResponse> setSchema(
+            @NonNull SetSchemaRequest request);
 
     /**
      * Retrieves the schema most recently successfully provided to {@link #setSchema}.
      *
-     * @return The pending result of performing this operation.
+     * @return The pending {@link GetSchemaResponse} of performing this operation.
      */
     // This call hits disk; async API prevents us from treating these calls as properties.
     @SuppressLint("KotlinPropertyAccess")
     @NonNull
-    ListenableFuture<Set<AppSearchSchema>> getSchema();
+    ListenableFuture<GetSchemaResponse> getSchema();
 
     /**
-     * Indexes documents into AppSearch.
+     * Retrieves the set of all namespaces in the current database with at least one document.
      *
-     * <p>Each {@link GenericDocument}'s {@code schemaType} field must be set to the name of a
-     * schema type previously registered via the {@link #setSchema} method.
-     *
-     * @param request {@link PutDocumentsRequest} containing documents to be indexed
-     * @return The pending result of performing this operation. The keys of the returned
-     * {@link AppSearchBatchResult} are the URIs of the input documents. The values are
-     * {@code null} if they were successfully indexed, or a failed {@link AppSearchResult}
-     * otherwise.
+     * @return The pending result of performing this operation.
      */
     @NonNull
-    ListenableFuture<AppSearchBatchResult<String, Void>> putDocuments(
-            @NonNull PutDocumentsRequest request);
+    ListenableFuture<Set<String>> getNamespaces();
 
     /**
-     * Retrieves {@link GenericDocument}s by URI.
+     * Indexes documents into the {@link AppSearchSession} database.
      *
-     * @param request {@link GetByUriRequest} containing URIs to be retrieved.
-     * @return The pending result of performing this operation. The keys of the returned
-     * {@link AppSearchBatchResult} are the input URIs. The values are the returned
-     * {@link GenericDocument}s on success, or a failed {@link AppSearchResult} otherwise.
-     * URIs that are not found will return a failed {@link AppSearchResult} with a result code
-     * of {@link AppSearchResult#RESULT_NOT_FOUND}.
+     * <p>Each {@link GenericDocument} object must have a {@code schemaType} field set to an
+     * {@link AppSearchSchema} type that has been previously registered by calling the
+     * {@link #setSchema} method.
+     *
+     * @param request containing documents to be indexed.
+     * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
+     * The keys of the returned {@link AppSearchBatchResult} are the IDs of the input documents.
+     * The values are either {@code null} if the corresponding document was successfully indexed,
+     * or a failed {@link AppSearchResult} otherwise.
      */
     @NonNull
-    ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByUri(
-            @NonNull GetByUriRequest request);
+    ListenableFuture<AppSearchBatchResult<String, Void>> put(@NonNull PutDocumentsRequest request);
 
     /**
-     * Searches a document based on a given query string.
+     * Gets {@link GenericDocument} objects by document IDs in a namespace from the
+     * {@link AppSearchSession} database.
      *
-     * <p>Currently we support following features in the raw query format:
-     * <ul>
-     *     <li>AND
-     *     <p>AND joins (e.g. “match documents that have both the terms ‘dog’ and
-     *     ‘cat’”).
-     *     Example: hello world matches documents that have both ‘hello’ and ‘world’
-     *     <li>OR
-     *     <p>OR joins (e.g. “match documents that have either the term ‘dog’ or
-     *     ‘cat’”).
-     *     Example: dog OR puppy
-     *     <li>Exclusion
-     *     <p>Exclude a term (e.g. “match documents that do
-     *     not have the term ‘dog’”).
-     *     Example: -dog excludes the term ‘dog’
-     *     <li>Grouping terms
-     *     <p>Allow for conceptual grouping of subqueries to enable hierarchical structures (e.g.
-     *     “match documents that have either ‘dog’ or ‘puppy’, and either ‘cat’ or ‘kitten’”).
-     *     Example: (dog puppy) (cat kitten) two one group containing two terms.
-     *     <li>Property restricts
-     *     <p> Specifies which properties of a document to specifically match terms in (e.g.
-     *     “match documents where the ‘subject’ property contains ‘important’”).
-     *     Example: subject:important matches documents with the term ‘important’ in the
-     *     ‘subject’ property
-     *     <li>Schema type restricts
-     *     <p>This is similar to property restricts, but allows for restricts on top-level document
-     *     fields, such as schema_type. Clients should be able to limit their query to documents of
-     *     a certain schema_type (e.g. “match documents that are of the ‘Email’ schema_type”).
-     *     Example: { schema_type_filters: “Email”, “Video”,query: “dog” } will match documents
-     *     that contain the query term ‘dog’ and are of either the ‘Email’ schema type or the
-     *     ‘Video’ schema type.
-     * </ul>
-     *
-     * <p> This method is lightweight. The heavy work will be done in
-     * {@link SearchResults#getNextPage()}.
-     *
-     * @param queryExpression Query String to search.
-     * @param searchSpec      Spec for setting filters, raw query etc.
-     * @return The search result of performing this operation.
-     */
-    @NonNull
-    SearchResults query(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
-
-    /**
-     * Removes {@link GenericDocument}s from the index by URI.
-     *
-     * @param request Request containing URIs to be removed.
-     * @return The pending result of performing this operation. The keys of the returned
-     * {@link AppSearchBatchResult} are the input URIs. The values are {@code null} on success,
-     * or a failed {@link AppSearchResult} otherwise. URIs that are not found will return a
-     * failed {@link AppSearchResult} with a result code of
+     * @param request a request containing a namespace and IDs to get documents for.
+     * @return A {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
+     * The keys of the {@link AppSearchBatchResult} represent the input document IDs from the
+     * {@link GetByDocumentIdRequest} object. The values are either the corresponding
+     * {@link GenericDocument} object for the ID on success, or an {@link AppSearchResult}
+     * object on failure. For example, if an ID is not found, the value for that ID will be set
+     * to an {@link AppSearchResult} object with result code:
      * {@link AppSearchResult#RESULT_NOT_FOUND}.
      */
     @NonNull
-    ListenableFuture<AppSearchBatchResult<String, Void>> removeByUri(
-            @NonNull RemoveByUriRequest request);
+    ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+            @NonNull GetByDocumentIdRequest request);
+
+    /**
+     * Retrieves documents from the open {@link AppSearchSession} that match a given query string
+     * and type of search provided.
+     *
+     * <p>Query strings can be empty, contain one term with no operators, or contain multiple
+     * terms and operators.
+     *
+     * <p>For query strings that are empty, all documents that match the {@link SearchSpec} will be
+     * returned.
+     *
+     * <p>For query strings with a single term and no operators, documents that match the
+     * provided query string and {@link SearchSpec} will be returned.
+     *
+     * <p>The following operators are supported:
+     *
+     * <ul>
+     *     <li>AND (implicit)
+     *     <p>AND is an operator that matches documents that contain <i>all</i>
+     *     provided terms.
+     *     <p><b>NOTE:</b> A space between terms is treated as an "AND" operator. Explicitly
+     *     including "AND" in a query string will treat "AND" as a term, returning documents that
+     *     also contain "AND".
+     *     <p>Example: "apple AND banana" matches documents that contain the
+     *     terms "apple", "and", "banana".
+     *     <p>Example: "apple banana" matches documents that contain both "apple" and
+     *     "banana".
+     *     <p>Example: "apple banana cherry" matches documents that contain "apple", "banana", and
+     *     "cherry".
+     *
+     *     <li>OR
+     *     <p>OR is an operator that matches documents that contain <i>any</i> provided term.
+     *     <p>Example: "apple OR banana" matches documents that contain either "apple" or "banana".
+     *     <p>Example: "apple OR banana OR cherry" matches documents that contain any of
+     *     "apple", "banana", or "cherry".
+     *
+     *     <li>Exclusion (-)
+     *     <p>Exclusion (-) is an operator that matches documents that <i>do not</i> contain the
+     *     provided term.
+     *     <p>Example: "-apple" matches documents that do not contain "apple".
+     *
+     *     <li>Grouped Terms
+     *     <p>For queries that require multiple operators and terms, terms can be grouped into
+     *     subqueries. Subqueries are contained within an open "(" and close ")" parenthesis.
+     *     <p>Example: "(donut OR bagel) (coffee OR tea)" matches documents that contain
+     *     either "donut" or "bagel" and either "coffee" or "tea".
+     *
+     *     <li>Property Restricts
+     *     <p>For queries that require a term to match a specific {@link AppSearchSchema}
+     *     property of a document, a ":" must be included between the property name and the term.
+     *     <p>Example: "subject:important" matches documents that contain the term "important" in
+     *     the "subject" property.
+     * </ul>
+     *
+     * <p>Additional search specifications, such as filtering by {@link AppSearchSchema} type or
+     * adding projection, can be set by calling the corresponding {@link SearchSpec.Builder} setter.
+     *
+     * <p>This method is lightweight. The heavy work will be done in
+     * {@link SearchResults#getNextPage}.
+     *
+     * @param queryExpression query string to search.
+     * @param searchSpec      spec for setting document filters, adding projection, setting term
+     *                        match type, etc.
+     * @return a {@link SearchResults} object for retrieved matched documents.
+     */
+    @NonNull
+    SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+    /**
+     * Reports usage of a particular document by namespace and ID.
+     *
+     * <p>A usage report represents an event in which a user interacted with or viewed a document.
+     *
+     * <p>For each call to {@link #reportUsage}, AppSearch updates usage count and usage recency
+     * metrics for that particular document. These metrics are used for ordering {@link #search}
+     * results by the {@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} and
+     * {@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} ranking strategies.
+     *
+     * <p>Reporting usage of a document is optional.
+     *
+     * @param request The usage reporting request.
+     * @return The pending result of performing this operation which resolves to {@code null} on
+     *     success.
+     */
+    @NonNull
+    ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request);
+
+    /**
+     * Removes {@link GenericDocument} objects by document IDs in a namespace from the
+     * {@link AppSearchSession} database.
+     *
+     * <p>Removed documents will no longer be surfaced by {@link #search} or
+     * {@link #getByDocumentId}
+     * calls.
+     *
+     * <p>Once the database crosses the document count or byte usage threshold, removed documents
+     * will be deleted from disk.
+     *
+     * @param request {@link RemoveByDocumentIdRequest} with IDs in a namespace to remove from the
+     *                index.
+     * @return a {@link ListenableFuture} which resolves to an {@link AppSearchBatchResult}.
+     * The keys of the {@link AppSearchBatchResult} represent the input IDs from the
+     * {@link RemoveByDocumentIdRequest} object. The values are either {@code null} on success,
+     * or a failed {@link AppSearchResult} otherwise. IDs that are not found will return a failed
+     * {@link AppSearchResult} with a result code of {@link AppSearchResult#RESULT_NOT_FOUND}.
+     */
+    @NonNull
+    ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest request);
 
     /**
      * Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
      * match the {@code queryExpression} in given namespaces and schemaTypes which is set via
-     * {@link SearchSpec.Builder#addNamespace} and {@link SearchSpec.Builder#addSchemaType}.
+     * {@link SearchSpec.Builder#addFilterNamespaces} and
+     * {@link SearchSpec.Builder#addFilterSchemas}.
      *
      * <p> An empty {@code queryExpression} matches all documents.
      *
@@ -209,8 +233,29 @@
      * @return The pending result of performing this operation.
      */
     @NonNull
-    ListenableFuture<Void> removeByQuery(
-            @NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+    ListenableFuture<Void> remove(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+    /**
+     * Gets the storage info for this {@link AppSearchSession} database.
+     *
+     * <p>This may take time proportional to the number of documents and may be inefficient to
+     * call repeatedly.
+     *
+     * @return a {@link ListenableFuture} which resolves to a {@link StorageInfo} object.
+     */
+    @NonNull
+    ListenableFuture<StorageInfo> getStorageInfo();
+
+    /**
+     * Flush all schema and document updates, additions, and deletes to disk if possible.
+     *
+     * @return The pending result of performing this operation.
+     * {@link androidx.appsearch.exceptions.AppSearchException} with
+     * {@link AppSearchResult#RESULT_INTERNAL_ERROR} will be set to the future if we hit error when
+     * save to disk.
+     */
+    @NonNull
+    ListenableFuture<Void> maybeFlush();
 
     /**
      * Closes the {@link AppSearchSession} to persist all schema and document updates, additions,
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactoryRegistry.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactoryRegistry.java
deleted file mode 100644
index 3be56c6..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactoryRegistry.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * 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.
- */
-// @exportToFramework:skipFile()
-package androidx.appsearch.app;
-
-import androidx.annotation.AnyThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.appsearch.exceptions.AppSearchException;
-import androidx.core.util.Preconditions;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A registry which maintains instances of {@link androidx.appsearch.app.DataClassFactory}.
- * @hide
- */
-@AnyThread
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public final class DataClassFactoryRegistry {
-    private static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
-
-    private static volatile DataClassFactoryRegistry sInstance = null;
-
-    private final Map<Class<?>, DataClassFactory<?>> mFactories = new HashMap<>();
-
-    private DataClassFactoryRegistry() {}
-
-    /** Returns the singleton instance of {@link DataClassFactoryRegistry}. */
-    @NonNull
-    public static DataClassFactoryRegistry getInstance() {
-        if (sInstance == null) {
-            synchronized (DataClassFactoryRegistry.class) {
-                if (sInstance == null) {
-                    sInstance = new DataClassFactoryRegistry();
-                }
-            }
-        }
-        return sInstance;
-    }
-
-    /**
-     * Gets the {@link DataClassFactory} instance that can convert to and from objects of type
-     * {@code T}.
-     *
-     * @throws AppSearchException if no factory for this data class could be found on the classpath
-     */
-    @NonNull
-    @SuppressWarnings("unchecked")
-    public <T> DataClassFactory<T> getOrCreateFactory(@NonNull Class<T> dataClass)
-            throws AppSearchException {
-        Preconditions.checkNotNull(dataClass);
-        DataClassFactory<?> factory;
-        synchronized (this) {
-            factory = mFactories.get(dataClass);
-        }
-        if (factory == null) {
-            factory = loadFactoryByReflection(dataClass);
-            synchronized (this) {
-                DataClassFactory<?> racingFactory = mFactories.get(dataClass);
-                if (racingFactory == null) {
-                    mFactories.put(dataClass, factory);
-                } else {
-                    // Another thread beat us to it
-                    factory = racingFactory;
-                }
-            }
-        }
-        return (DataClassFactory<T>) factory;
-    }
-
-    /**
-     * Gets the {@link DataClassFactory} instance that can convert to and from objects of type
-     * {@code T}.
-     *
-     * @throws AppSearchException if no factory for this data class could be found on the classpath
-     */
-    @NonNull
-    @SuppressWarnings("unchecked")
-    public <T> DataClassFactory<T> getOrCreateFactory(@NonNull T dataClass)
-            throws AppSearchException {
-        Preconditions.checkNotNull(dataClass);
-        Class<?> clazz = dataClass.getClass();
-        DataClassFactory<?> factory = getOrCreateFactory(clazz);
-        return (DataClassFactory<T>) factory;
-    }
-
-    private DataClassFactory<?> loadFactoryByReflection(@NonNull Class<?> dataClass)
-            throws AppSearchException {
-        Package pkg = dataClass.getPackage();
-        String simpleName = dataClass.getCanonicalName();
-        if (simpleName == null) {
-            throw new AppSearchException(
-                    AppSearchResult.RESULT_INTERNAL_ERROR,
-                    "Failed to find simple name for data class \"" + dataClass
-                            + "\". Perhaps it is anonymous?");
-        }
-
-        // Creates factory class name under the package.
-        // For a class Foo annotated with @AppSearchDocument, we will generated a
-        // $$__AppSearch__Foo.class under the package.
-        // For an inner class Foo.Bar annotated with @AppSearchDocument, we will generated a
-        // $$__AppSearch__Foo$$__Bar.class under the package.
-        String packageName = "";
-        if (pkg != null) {
-            packageName = pkg.getName() + ".";
-            simpleName = simpleName.substring(packageName.length()).replace(".", "$$__");
-        }
-        String factoryClassName = packageName + GEN_CLASS_PREFIX + simpleName;
-
-        Class<?> factoryClass;
-        try {
-            factoryClass = Class.forName(factoryClassName);
-        } catch (ClassNotFoundException e) {
-            throw new AppSearchException(
-                    AppSearchResult.RESULT_INTERNAL_ERROR,
-                    "Failed to find data class converter \"" + factoryClassName
-                            + "\". Perhaps the annotation processor was not run or the class was "
-                            + "proguarded out?",
-                    e);
-        }
-        Object instance;
-        try {
-            instance = factoryClass.getDeclaredConstructor().newInstance();
-        } catch (Exception e) {
-            throw new AppSearchException(
-                    AppSearchResult.RESULT_INTERNAL_ERROR,
-                    "Failed to construct data class converter \"" + factoryClassName + "\"",
-                    e);
-        }
-        return (DataClassFactory<?>) instance;
-    }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactory.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
similarity index 63%
rename from appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactory.java
rename to appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
index 014b23b..bd03221 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DataClassFactory.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactory.java
@@ -20,31 +20,35 @@
 import androidx.appsearch.exceptions.AppSearchException;
 
 /**
- * An interface for factories which can convert between data classes and {@link GenericDocument}.
+ * An interface for factories which can convert between instances of classes annotated with
+ * \@{@link androidx.appsearch.annotation.Document} and instances of {@link GenericDocument}.
  *
- * @param <T> The type of data class this factory converts to and from {@link GenericDocument}.
+ * @param <T> The document class type this factory converts to and from {@link GenericDocument}.
  */
-public interface DataClassFactory<T> {
+public interface DocumentClassFactory<T> {
     /**
      * Returns the name of this schema type, e.g. {@code Email}.
      *
      * <p>This is the name used in queries for type restricts.
      */
     @NonNull
-    String getSchemaType();
+    String getSchemaName();
 
-    /** Returns the schema for this data class. */
+    /** Returns the schema for this document class. */
     @NonNull
     AppSearchSchema getSchema() throws AppSearchException;
 
     /**
-     * Converts an instance of the data class into a {@link androidx.appsearch.app.GenericDocument}.
+     * Converts an instance of the class annotated with
+     * \@{@link androidx.appsearch.annotation.Document} into a
+     * {@link androidx.appsearch.app.GenericDocument}.
      */
     @NonNull
-    GenericDocument toGenericDocument(@NonNull T dataClass) throws AppSearchException;
+    GenericDocument toGenericDocument(@NonNull T document) throws AppSearchException;
 
     /**
-     * Converts a {@link androidx.appsearch.app.GenericDocument} into an instance of the data class.
+     * Converts a {@link androidx.appsearch.app.GenericDocument} into an instance of the document
+     * class.
      */
     @NonNull
     T fromGenericDocument(@NonNull GenericDocument genericDoc) throws AppSearchException;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactoryRegistry.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactoryRegistry.java
new file mode 100644
index 0000000..6ce9a9b6
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/DocumentClassFactoryRegistry.java
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.core.util.Preconditions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A registry which maintains instances of {@link DocumentClassFactory}.
+ * @hide
+ */
+@AnyThread
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class DocumentClassFactoryRegistry {
+    private static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
+
+    private static volatile DocumentClassFactoryRegistry sInstance = null;
+
+    private final Map<Class<?>, DocumentClassFactory<?>> mFactories = new HashMap<>();
+
+    private DocumentClassFactoryRegistry() {}
+
+    /** Returns the singleton instance of {@link DocumentClassFactoryRegistry}. */
+    @NonNull
+    public static DocumentClassFactoryRegistry getInstance() {
+        if (sInstance == null) {
+            synchronized (DocumentClassFactoryRegistry.class) {
+                if (sInstance == null) {
+                    sInstance = new DocumentClassFactoryRegistry();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    /**
+     * Gets the {@link DocumentClassFactory} instance that can convert to and from objects of type
+     * {@code T}.
+     *
+     * @throws AppSearchException if no factory for this document class could be found on the
+     * classpath
+     */
+    @NonNull
+    @SuppressWarnings("unchecked")
+    public <T> DocumentClassFactory<T> getOrCreateFactory(@NonNull Class<T> documentClass)
+            throws AppSearchException {
+        Preconditions.checkNotNull(documentClass);
+        DocumentClassFactory<?> factory;
+        synchronized (this) {
+            factory = mFactories.get(documentClass);
+        }
+        if (factory == null) {
+            factory = loadFactoryByReflection(documentClass);
+            synchronized (this) {
+                DocumentClassFactory<?> racingFactory = mFactories.get(documentClass);
+                if (racingFactory == null) {
+                    mFactories.put(documentClass, factory);
+                } else {
+                    // Another thread beat us to it
+                    factory = racingFactory;
+                }
+            }
+        }
+        return (DocumentClassFactory<T>) factory;
+    }
+
+    /**
+     * Gets the {@link DocumentClassFactory} instance that can convert to and from objects of type
+     * {@code T}.
+     *
+     * @throws AppSearchException if no factory for this document class could be found on the
+     * classpath
+     */
+    @NonNull
+    @SuppressWarnings("unchecked")
+    public <T> DocumentClassFactory<T> getOrCreateFactory(@NonNull T documentClass)
+            throws AppSearchException {
+        Preconditions.checkNotNull(documentClass);
+        Class<?> clazz = documentClass.getClass();
+        DocumentClassFactory<?> factory = getOrCreateFactory(clazz);
+        return (DocumentClassFactory<T>) factory;
+    }
+
+    private DocumentClassFactory<?> loadFactoryByReflection(@NonNull Class<?> documentClass)
+            throws AppSearchException {
+        Package pkg = documentClass.getPackage();
+        String simpleName = documentClass.getCanonicalName();
+        if (simpleName == null) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_INTERNAL_ERROR,
+                    "Failed to find simple name for document class \"" + documentClass
+                            + "\". Perhaps it is anonymous?");
+        }
+
+        // Creates factory class name under the package.
+        // For a class Foo annotated with @Document, we will generated a
+        // $$__AppSearch__Foo.class under the package.
+        // For an inner class Foo.Bar annotated with @Document, we will generated a
+        // $$__AppSearch__Foo$$__Bar.class under the package.
+        String packageName = "";
+        if (pkg != null) {
+            packageName = pkg.getName() + ".";
+            simpleName = simpleName.substring(packageName.length()).replace(".", "$$__");
+        }
+        String factoryClassName = packageName + GEN_CLASS_PREFIX + simpleName;
+
+        Class<?> factoryClass;
+        try {
+            factoryClass = Class.forName(factoryClassName);
+        } catch (ClassNotFoundException e) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_INTERNAL_ERROR,
+                    "Failed to find document class converter \"" + factoryClassName
+                            + "\". Perhaps the annotation processor was not run or the class was "
+                            + "proguarded out?",
+                    e);
+        }
+        Object instance;
+        try {
+            instance = factoryClass.getDeclaredConstructor().newInstance();
+        } catch (Exception e) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_INTERNAL_ERROR,
+                    "Failed to construct document class converter \"" + factoryClassName + "\"",
+                    e);
+        }
+        return (DocumentClassFactory<?>) instance;
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 77d3918..11e11c7 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -18,12 +18,14 @@
 
 import android.annotation.SuppressLint;
 import android.os.Bundle;
+import android.os.Parcelable;
 import android.util.Log;
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.Document;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.util.BundleUtil;
 import androidx.core.util.Preconditions;
@@ -32,33 +34,29 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 /**
  * Represents a document unit.
  *
- * <p>Documents are constructed via {@link GenericDocument.Builder}.
+ * <p>Documents contain structured data conforming to their {@link AppSearchSchema} type.
+ * Each document is uniquely identified by a namespace and a String ID within that namespace.
  *
- * @see AppSearchSession#putDocuments
- * @see AppSearchSession#getByUri
- * @see AppSearchSession#query
+ * <p>Documents are constructed either by using the {@link GenericDocument.Builder} or providing
+ * an annotated {@link Document} data class.
+ *
+ * @see AppSearchSession#put
+ * @see AppSearchSession#getByDocumentId
+ * @see AppSearchSession#search
  */
 public class GenericDocument {
     private static final String TAG = "AppSearchGenericDocumen";
 
-    /** The default empty namespace. */
-    public static final String DEFAULT_NAMESPACE = "";
-
-    /**
-     * The maximum number of elements in a repeatable field. Will reject the request if exceed
-     * this limit.
-     */
+    /** The maximum number of elements in a repeatable field. */
     private static final int MAX_REPEATED_PROPERTY_LENGTH = 100;
 
-    /**
-     * The maximum {@link String#length} of a {@link String} field. Will reject the request if
-     * {@link String}s longer than this.
-     */
+    /** The maximum {@link String#length} of a {@link String} field. */
     private static final int MAX_STRING_LENGTH = 20_000;
 
     /** The maximum number of indexed properties a document can have. */
@@ -73,7 +71,7 @@
     private static final String PROPERTIES_FIELD = "properties";
     private static final String BYTE_ARRAY_FIELD = "byteArray";
     private static final String SCHEMA_TYPE_FIELD = "schemaType";
-    private static final String URI_FIELD = "uri";
+    private static final String ID_FIELD = "id";
     private static final String SCORE_FIELD = "score";
     private static final String TTL_MILLIS_FIELD = "ttlMillis";
     private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
@@ -82,24 +80,51 @@
     /**
      * The maximum number of indexed properties a document can have.
      *
-     * <p>Indexed properties are properties where the
-     * {@link AppSearchSchema.PropertyConfig#getIndexingType()} constant is anything other than
-     * {@link AppSearchSchema.PropertyConfig.IndexingType#INDEXING_TYPE_NONE}.
+     * <p>Indexed properties are properties which are strings where the
+     * {@link AppSearchSchema.StringPropertyConfig#getIndexingType} value is anything other
+     * than {@link AppSearchSchema.StringPropertyConfig.IndexingType#INDEXING_TYPE_NONE}.
      */
     public static int getMaxIndexedProperties() {
         return MAX_INDEXED_PROPERTIES;
     }
 
-    /** Contains {@link GenericDocument} basic information (uri, schemaType etc). */
+// @exportToFramework:startStrip()
+
+    /**
+     * Converts an instance of a class annotated with \@{@link Document} into an instance of
+     * {@link GenericDocument}.
+     *
+     * @param document An instance of a class annotated with \@{@link Document}.
+     * @return an instance of {@link GenericDocument} produced by converting {@code document}.
+     * @throws AppSearchException if no generated conversion class exists on the classpath for the
+     *                            given document class or an unexpected error occurs during
+     *                            conversion.
+     * @see GenericDocument#toDocumentClass
+     */
+    @NonNull
+    public static GenericDocument fromDocumentClass(@NonNull Object document)
+            throws AppSearchException {
+        Preconditions.checkNotNull(document);
+        DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+        DocumentClassFactory<Object> factory = registry.getOrCreateFactory(document);
+        return factory.toGenericDocument(document);
+    }
+// @exportToFramework:endStrip()
+
+    /**
+     * Contains all {@link GenericDocument} information in a packaged format.
+     *
+     * <p>Keys are the {@code *_FIELD} constants in this class.
+     */
     @NonNull
     final Bundle mBundle;
 
-    /** Contains all properties in {@link GenericDocument} to support getting properties via keys */
+    /** Contains all properties in {@link GenericDocument} to support getting properties via name */
     @NonNull
     private final Bundle mProperties;
 
     @NonNull
-    private final String mUri;
+    private final String mId;
     @NonNull
     private final String mSchemaType;
     private final long mCreationTimestampMillis;
@@ -107,11 +132,10 @@
     private Integer mHashCode;
 
     /**
-     * Rebuilds a {@link GenericDocument} by the a bundle.
+     * Rebuilds a {@link GenericDocument} from a bundle.
      *
-     * @param bundle Contains {@link GenericDocument} basic information (uri, schemaType etc) and
-     *               a properties bundle contains all properties in {@link GenericDocument} to
-     *               support getting properties via keys.
+     * @param bundle Packaged {@link GenericDocument} data, such as the result of
+     *               {@link #getBundle}.
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -119,7 +143,7 @@
         Preconditions.checkNotNull(bundle);
         mBundle = bundle;
         mProperties = Preconditions.checkNotNull(bundle.getParcelable(PROPERTIES_FIELD));
-        mUri = Preconditions.checkNotNull(mBundle.getString(URI_FIELD));
+        mId = Preconditions.checkNotNull(mBundle.getString(ID_FIELD));
         mSchemaType = Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
         mCreationTimestampMillis = mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD,
                 System.currentTimeMillis());
@@ -145,19 +169,19 @@
         return mBundle;
     }
 
-    /** Returns the URI of the {@link GenericDocument}. */
+    /** Returns the unique identifier of the {@link GenericDocument}. */
     @NonNull
-    public String getUri() {
-        return mUri;
+    public String getId() {
+        return mId;
     }
 
     /** Returns the namespace of the {@link GenericDocument}. */
     @NonNull
     public String getNamespace() {
-        return mBundle.getString(NAMESPACE_FIELD, DEFAULT_NAMESPACE);
+        return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/ "");
     }
 
-    /** Returns the schema type of the {@link GenericDocument}. */
+    /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
     @NonNull
     public String getSchemaType() {
         return mSchemaType;
@@ -168,19 +192,20 @@
      *
      * <p>The value is in the {@link System#currentTimeMillis} time base.
      */
+    /*@exportToFramework:CurrentTimeMillisLong*/
     public long getCreationTimestampMillis() {
         return mCreationTimestampMillis;
     }
 
     /**
-     * Returns the TTL (Time To Live) of the {@link GenericDocument}, in milliseconds.
+     * Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
      *
      * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
      * {@code creationTimestampMillis + ttlMillis}, measured in the {@link System#currentTimeMillis}
      * time base, the document will be auto-deleted.
      *
      * <p>The default value is 0, which means the document is permanent and won't be auto-deleted
-     * until the app is uninstalled.
+     * until the app is uninstalled or {@link AppSearchSession#remove} is called.
      */
     public long getTtlMillis() {
         return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
@@ -190,12 +215,12 @@
      * Returns the score of the {@link GenericDocument}.
      *
      * <p>The score is a query-independent measure of the document's quality, relative to
-     * other {@link GenericDocument}s of the same type.
+     * other {@link GenericDocument} objects of the same {@link AppSearchSchema} type.
      *
      * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}.
      * Documents with higher scores are considered better than documents with lower scores.
      *
-     * <p>Any nonnegative integer can be used a score.
+     * <p>Any non-negative integer can be used a score.
      */
     public int getScore() {
         return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE);
@@ -208,133 +233,506 @@
     }
 
     /**
-     * Retrieves the property value with the given key as {@link Object}.
+     * Retrieves the property value with the given path as {@link Object}.
      *
-     * @param key The key to look for.
-     * @return The entry with the given key as an object or {@code null} if there is no such key.
+     * <p>A path can be a simple property name, such as those returned by {@link #getPropertyNames}.
+     * It may also be a dot-delimited path through the nested document hierarchy, with nested
+     * {@link GenericDocument} properties accessed via {@code '.'} and repeated properties
+     * optionally indexed into via {@code [n]}.
+     *
+     * <p>For example, given the following {@link GenericDocument}:
+     * <pre>
+     *     (Message) {
+     *         from: "[email protected]"
+     *         to: [{
+     *             name: "Albert Einstein"
+     *             email: "[email protected]"
+     *           }, {
+     *             name: "Marie Curie"
+     *             email: "[email protected]"
+     *           }]
+     *         tags: ["important", "inbox"]
+     *         subject: "Hello"
+     *     }
+     * </pre>
+     *
+     * <p>Here are some example paths and their results:
+     * <ul>
+     *     <li>{@code "from"} returns {@code "[email protected]"} as a {@link String} array with
+     *     one element
+     *     <li>{@code "to"} returns the two nested documents containing contact information as a
+     *     {@link GenericDocument} array with two elements
+     *     <li>{@code "to[1]"} returns the second nested document containing Marie Curie's
+     *     contact information as a {@link GenericDocument} array with one element
+     *     <li>{@code "to[1].email"} returns {@code "[email protected]"}
+     *     <li>{@code "to[100].email"} returns {@code null} as this particular document does not
+     *     have that many elements in its {@code "to"} array.
+     *     <li>{@code "to.email"} aggregates emails across all nested documents that have them,
+     *     returning {@code ["[email protected]", "[email protected]"]} as a {@link String}
+     *     array with two elements.
+     * </ul>
+     *
+     * <p>If you know the expected type of the property you are retrieving, it is recommended to use
+     * one of the typed versions of this method instead, such as {@link #getPropertyString} or
+     * {@link #getPropertyStringArray}.
+     *
+     * @param path The path to look for.
+     * @return The entry with the given path as an object or {@code null} if there is no such path.
+     *   The returned object will be one of the following types: {@code String[]}, {@code long[]},
+     *   {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}.
      */
     @Nullable
-    public Object getProperty(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        Object property = mProperties.get(key);
-        if (property instanceof ArrayList) {
-            return getPropertyBytesArray(key);
-        } else if (property instanceof Bundle[]) {
-            return getPropertyDocumentArray(key);
+    public Object getProperty(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object rawValue = getRawPropertyFromRawDocument(path, mBundle);
+
+        // Unpack the raw value into the types the user expects, if required.
+        if (rawValue instanceof Bundle) {
+            // getRawPropertyFromRawDocument may return a document as a bare Bundle as a performance
+            // optimization for lookups.
+            GenericDocument document = new GenericDocument((Bundle) rawValue);
+            return new GenericDocument[]{document};
         }
-        return property;
+
+        if (rawValue instanceof List) {
+            // byte[][] fields are packed into List<Bundle> where each Bundle contains just a single
+            // entry: BYTE_ARRAY_FIELD -> byte[].
+            @SuppressWarnings("unchecked")
+            List<Bundle> bundles = (List<Bundle>) rawValue;
+            if (bundles.size() == 0) {
+                return null;
+            }
+            byte[][] bytes = new byte[bundles.size()][];
+            for (int i = 0; i < bundles.size(); i++) {
+                Bundle bundle = bundles.get(i);
+                if (bundle == null) {
+                    Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);
+                    continue;
+                }
+                byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD);
+                if (innerBytes == null) {
+                    Log.e(TAG, "The bundle at " + i + " contains a null byte[].");
+                    continue;
+                }
+                bytes[i] = innerBytes;
+            }
+            return bytes;
+        }
+
+        if (rawValue instanceof Parcelable[]) {
+            // The underlying Bundle of nested GenericDocuments is packed into a Parcelable array.
+            // We must unpack it into GenericDocument instances.
+            Parcelable[] bundles = (Parcelable[]) rawValue;
+            if (bundles.length == 0) {
+                return null;
+            }
+            GenericDocument[] documents = new GenericDocument[bundles.length];
+            for (int i = 0; i < bundles.length; i++) {
+                if (bundles[i] == null) {
+                    Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path);
+                    continue;
+                }
+                if (!(bundles[i] instanceof Bundle)) {
+                    Log.e(TAG, "The inner element at " + i + " is a " + bundles[i].getClass()
+                            + ", not a Bundle for path: " + path);
+                    continue;
+                }
+                documents[i] = new GenericDocument((Bundle) bundles[i]);
+            }
+            return documents;
+        }
+
+        // Otherwise the raw property is the same as the final property and needs no transformation.
+        return rawValue;
     }
 
     /**
-     * Retrieves a {@link String} value by key.
+     * Looks up a property path within the given document bundle.
      *
-     * @param key The key to look for.
-     * @return The first {@link String} associated with the given key or {@code null} if there is
-     * no such key or the value is of a different type.
+     * <p>The return value may be any of GenericDocument's internal repeated storage types
+     * (String[], long[], double[], boolean[], ArrayList&lt;Bundle&gt;, Parcelable[]).
      */
     @Nullable
-    public String getPropertyString(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        String[] propertyArray = getPropertyStringArray(key);
+    private static Object getRawPropertyFromRawDocument(
+            @NonNull String path, @NonNull Bundle documentBundle) {
+        Preconditions.checkNotNull(path);
+        Preconditions.checkNotNull(documentBundle);
+        Bundle properties = Preconditions.checkNotNull(documentBundle.getBundle(PROPERTIES_FIELD));
+
+        // Determine whether the path is just a raw property name with no control characters
+        int controlIdx = -1;
+        boolean controlIsIndex = false;
+        for (int i = 0; i < path.length(); i++) {
+            char c = path.charAt(i);
+            if (c == '[' || c == '.') {
+                controlIdx = i;
+                controlIsIndex = c == '[';
+                break;
+            }
+        }
+
+        // Look up the value of the first path element
+        Object firstElementValue;
+        if (controlIdx == -1) {
+            firstElementValue = properties.get(path);
+        } else {
+            String name = path.substring(0, controlIdx);
+            firstElementValue = properties.get(name);
+        }
+
+        // If the path has no further elements, we're done.
+        if (firstElementValue == null || controlIdx == -1) {
+            return firstElementValue;
+        }
+
+        // At this point, for a path like "recipients[0]", firstElementValue contains the value of
+        // "recipients". If the first element of the path is an indexed value, we now update
+        // firstElementValue to contain "recipients[0]" instead.
+        String remainingPath;
+        if (!controlIsIndex) {
+            // Remaining path is everything after the .
+            remainingPath = path.substring(controlIdx + 1);
+        } else {
+            int endBracketIdx = path.indexOf(']', controlIdx);
+            if (endBracketIdx == -1) {
+                throw new IllegalArgumentException("Malformed path (no ending ']'): " + path);
+            }
+            if (endBracketIdx + 1 < path.length() && path.charAt(endBracketIdx + 1) != '.') {
+                throw new IllegalArgumentException(
+                        "Malformed path (']' not followed by '.'): " + path);
+            }
+            String indexStr = path.substring(controlIdx + 1, endBracketIdx);
+            int index = Integer.parseInt(indexStr);
+            if (index < 0) {
+                throw new IllegalArgumentException("Path index less than 0: " + path);
+            }
+
+            // Remaining path is everything after the [n]
+            if (endBracketIdx + 1 < path.length()) {
+                // More path remains, and we've already checked that charAt(endBracketIdx+1) == .
+                remainingPath = path.substring(endBracketIdx + 2);
+            } else {
+                // No more path remains.
+                remainingPath = null;
+            }
+
+            // Extract the right array element
+            Object extractedValue = null;
+            if (firstElementValue instanceof String[]) {
+                String[] stringValues = (String[]) firstElementValue;
+                if (index < stringValues.length) {
+                    extractedValue = Arrays.copyOfRange(stringValues, index, index + 1);
+                }
+            } else if (firstElementValue instanceof long[]) {
+                long[] longValues = (long[]) firstElementValue;
+                if (index < longValues.length) {
+                    extractedValue = Arrays.copyOfRange(longValues, index, index + 1);
+                }
+            } else if (firstElementValue instanceof double[]) {
+                double[] doubleValues = (double[]) firstElementValue;
+                if (index < doubleValues.length) {
+                    extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1);
+                }
+            } else if (firstElementValue instanceof boolean[]) {
+                boolean[] booleanValues = (boolean[]) firstElementValue;
+                if (index < booleanValues.length) {
+                    extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1);
+                }
+            } else if (firstElementValue instanceof List) {
+                @SuppressWarnings("unchecked")
+                List<Bundle> bundles = (List<Bundle>) firstElementValue;
+                if (index < bundles.size()) {
+                    extractedValue = bundles.subList(index, index + 1);
+                }
+            } else if (firstElementValue instanceof Parcelable[]) {
+                // Special optimization: to avoid creating new singleton arrays for traversing paths
+                // we return the bare document Bundle in this particular case.
+                Parcelable[] bundles = (Parcelable[]) firstElementValue;
+                if (index < bundles.length) {
+                    extractedValue = (Bundle) bundles[index];
+                }
+            } else {
+                throw new IllegalStateException("Unsupported value type: " + firstElementValue);
+            }
+            firstElementValue = extractedValue;
+        }
+
+        // If we are at the end of the path or there are no deeper elements in this document, we
+        // have nothing to recurse into.
+        if (firstElementValue == null || remainingPath == null) {
+            return firstElementValue;
+        }
+
+        // More of the path remains; recursively evaluate it
+        if (firstElementValue instanceof Bundle) {
+            return getRawPropertyFromRawDocument(remainingPath, (Bundle) firstElementValue);
+        } else if (firstElementValue instanceof Parcelable[]) {
+            Parcelable[] parcelables = (Parcelable[]) firstElementValue;
+            if (parcelables.length == 1) {
+                return getRawPropertyFromRawDocument(remainingPath, (Bundle) parcelables[0]);
+            }
+
+            // Slowest path: we're collecting values across repeated nested docs. (Example: given a
+            // path like recipient.name, where recipient is a repeated field, we return a string
+            // array where each recipient's name is an array element).
+            //
+            // Performance note: Suppose that we have a property path "a.b.c" where the "a"
+            // property has N document values and each containing a "b" property with M document
+            // values and each of those containing a "c" property with an int array.
+            //
+            // We'll allocate a new ArrayList for each of the "b" properties, add the M int arrays
+            // from the "c" properties to it and then we'll allocate an int array in
+            // flattenAccumulator before returning that (1 + M allocation per "b" property).
+            //
+            // When we're on the "a" properties, we'll allocate an ArrayList and add the N
+            // flattened int arrays returned from the "b" properties to the list. Then we'll
+            // allocate an int array in flattenAccumulator (1 + N ("b" allocs) allocations per "a").
+            // So this implementation could incur 1 + N + NM allocs.
+            //
+            // However, we expect the vast majority of getProperty calls to be either for direct
+            // property names (not paths) or else property paths returned from snippetting, which
+            // always refer to exactly one property value and don't aggregate across repeated
+            // values. The implementation is optimized for these two cases, requiring no additional
+            // allocations. So we've decided that the above performance characteristics are OK for
+            // the less used path.
+            List<Object> accumulator = new ArrayList<>(parcelables.length);
+            for (int i = 0; i < parcelables.length; i++) {
+                Object value =
+                        getRawPropertyFromRawDocument(remainingPath, (Bundle) parcelables[i]);
+                if (value != null) {
+                    accumulator.add(value);
+                }
+            }
+            return flattenAccumulator(accumulator);
+        } else {
+            Log.e(TAG, "Failed to apply path to document; no nested value found: " + path);
+            return null;
+        }
+    }
+
+    /**
+     * Combines accumulated repeated properties from multiple documents into a single array.
+     *
+     * @param accumulator List containing objects of the following types: {@code String[]},
+     *                    {@code long[]}, {@code double[]}, {@code boolean[]}, {@code List<Bundle>},
+     *                    or {@code Parcelable[]}.
+     * @return The result of concatenating each individual list element into a larger array/list of
+     *         the same type.
+     */
+    @Nullable
+    private static Object flattenAccumulator(@NonNull List<Object> accumulator) {
+        if (accumulator.isEmpty()) {
+            return null;
+        }
+        Object first = accumulator.get(0);
+        if (first instanceof String[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((String[]) accumulator.get(i)).length;
+            }
+            String[] result = new String[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                String[] castValue = (String[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        if (first instanceof long[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((long[]) accumulator.get(i)).length;
+            }
+            long[] result = new long[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                long[] castValue = (long[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        if (first instanceof double[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((double[]) accumulator.get(i)).length;
+            }
+            double[] result = new double[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                double[] castValue = (double[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        if (first instanceof boolean[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((boolean[]) accumulator.get(i)).length;
+            }
+            boolean[] result = new boolean[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                boolean[] castValue = (boolean[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        if (first instanceof List) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((List<?>) accumulator.get(i)).size();
+            }
+            List<Bundle> result = new ArrayList<>(length);
+            for (int i = 0; i < accumulator.size(); i++) {
+                @SuppressWarnings("unchecked")
+                List<Bundle> castValue = (List<Bundle>) accumulator.get(i);
+                result.addAll(castValue);
+            }
+            return result;
+        }
+        if (first instanceof Parcelable[]) {
+            int length = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                length += ((Parcelable[]) accumulator.get(i)).length;
+            }
+            Parcelable[] result = new Parcelable[length];
+            int total = 0;
+            for (int i = 0; i < accumulator.size(); i++) {
+                Parcelable[] castValue = (Parcelable[]) accumulator.get(i);
+                System.arraycopy(castValue, 0, result, total, castValue.length);
+                total += castValue.length;
+            }
+            return result;
+        }
+        throw new IllegalStateException("Unexpected property type: " + first);
+    }
+
+    /**
+     * Retrieves a {@link String} property by path.
+     *
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@link String} associated with the given path or {@code null} if there is
+     * no such value or the value is of a different type.
+     */
+    @Nullable
+    public String getPropertyString(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        String[] propertyArray = getPropertyStringArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return null;
         }
-        warnIfSinglePropertyTooLong("String", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("String", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@code long} value by key.
+     * Retrieves a {@code long} property by path.
      *
-     * @param key The key to look for.
-     * @return The first {@code long} associated with the given key or default value {@code 0} if
-     * there is no such key or the value is of a different type.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code long} associated with the given path or default value {@code 0} if
+     * there is no such value or the value is of a different type.
      */
-    public long getPropertyLong(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        long[] propertyArray = getPropertyLongArray(key);
+    public long getPropertyLong(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        long[] propertyArray = getPropertyLongArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return 0;
         }
-        warnIfSinglePropertyTooLong("Long", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("Long", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@code double} value by key.
+     * Retrieves a {@code double} property by path.
      *
-     * @param key The key to look for.
-     * @return The first {@code double} associated with the given key or default value {@code 0.0}
-     * if there is no such key or the value is of a different type.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code double} associated with the given path or default value {@code 0.0}
+     * if there is no such value or the value is of a different type.
      */
-    public double getPropertyDouble(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        double[] propertyArray = getPropertyDoubleArray(key);
+    public double getPropertyDouble(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        double[] propertyArray = getPropertyDoubleArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return 0.0;
         }
-        warnIfSinglePropertyTooLong("Double", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("Double", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@code boolean} value by key.
+     * Retrieves a {@code boolean} property by path.
      *
-     * @param key The key to look for.
-     * @return The first {@code boolean} associated with the given key or default value
-     * {@code false} if there is no such key or the value is of a different type.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code boolean} associated with the given path or default value
+     * {@code false} if there is no such value or the value is of a different type.
      */
-    public boolean getPropertyBoolean(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        boolean[] propertyArray = getPropertyBooleanArray(key);
+    public boolean getPropertyBoolean(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        boolean[] propertyArray = getPropertyBooleanArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return false;
         }
-        warnIfSinglePropertyTooLong("Boolean", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("Boolean", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@code byte[]} value by key.
+     * Retrieves a {@code byte[]} property by path.
      *
-     * @param key The key to look for.
-     * @return The first {@code byte[]} associated with the given key or {@code null} if there is
-     * no such key or the value is of a different type.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code byte[]} associated with the given path or {@code null} if there is
+     * no such value or the value is of a different type.
      */
     @Nullable
-    public byte[] getPropertyBytes(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        byte[][] propertyArray = getPropertyBytesArray(key);
+    public byte[] getPropertyBytes(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        byte[][] propertyArray = getPropertyBytesArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return null;
         }
-        warnIfSinglePropertyTooLong("ByteArray", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("ByteArray", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /**
-     * Retrieves a {@link GenericDocument} value by key.
+     * Retrieves a {@link GenericDocument} property by path.
      *
-     * @param key The key to look for.
-     * @return The first {@link GenericDocument} associated with the given key or {@code null} if
-     * there is no such key or the value is of a different type.
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@link GenericDocument} associated with the given path or {@code null} if
+     * there is no such value or the value is of a different type.
      */
     @Nullable
-    public GenericDocument getPropertyDocument(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        GenericDocument[] propertyArray = getPropertyDocumentArray(key);
+    public GenericDocument getPropertyDocument(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        GenericDocument[] propertyArray = getPropertyDocumentArray(path);
         if (propertyArray == null || propertyArray.length == 0) {
             return null;
         }
-        warnIfSinglePropertyTooLong("Document", key, propertyArray.length);
+        warnIfSinglePropertyTooLong("Document", path, propertyArray.length);
         return propertyArray[0];
     }
 
     /** Prints a warning to logcat if the given propertyLength is greater than 1. */
     private static void warnIfSinglePropertyTooLong(
-            @NonNull String propertyType, @NonNull String key, int propertyLength) {
+            @NonNull String propertyType, @NonNull String path, int propertyLength) {
         if (propertyLength > 1) {
-            Log.w(TAG, "The value for \"" + key + "\" contains " + propertyLength
+            Log.w(TAG, "The value for \"" + path + "\" contains " + propertyLength
                     + " elements. Only the first one will be returned from "
                     + "getProperty" + propertyType + "(). Try getProperty" + propertyType
                     + "Array().");
@@ -342,154 +740,150 @@
     }
 
     /**
-     * Retrieves a repeated {@code String} property by key.
+     * Retrieves a repeated {@code String} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code String[]} associated with the given key, or {@code null} if no value is
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code String[]} associated with the given path, or {@code null} if no value is
      * set or the value is of a different type.
      */
     @Nullable
-    public String[] getPropertyStringArray(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        return getAndCastPropertyArray(key, String[].class);
+    public String[] getPropertyStringArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, String[].class);
     }
 
     /**
-     * Retrieves a repeated {@link String} property by key.
+     * Retrieves a repeated {@code long[]} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code long[]} associated with the given key, or {@code null} if no value is
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code long[]} associated with the given path, or {@code null} if no value is
      * set or the value is of a different type.
      */
     @Nullable
-    public long[] getPropertyLongArray(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        return getAndCastPropertyArray(key, long[].class);
+    public long[] getPropertyLongArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, long[].class);
     }
 
     /**
-     * Retrieves a repeated {@code double} property by key.
+     * Retrieves a repeated {@code double} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code double[]} associated with the given key, or {@code null} if no value is
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code double[]} associated with the given path, or {@code null} if no value is
      * set or the value is of a different type.
      */
     @Nullable
-    public double[] getPropertyDoubleArray(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        return getAndCastPropertyArray(key, double[].class);
+    public double[] getPropertyDoubleArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, double[].class);
     }
 
     /**
-     * Retrieves a repeated {@code boolean} property by key.
+     * Retrieves a repeated {@code boolean} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code boolean[]} associated with the given key, or {@code null} if no value
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code boolean[]} associated with the given path, or {@code null} if no value
      * is set or the value is of a different type.
      */
     @Nullable
-    public boolean[] getPropertyBooleanArray(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        return getAndCastPropertyArray(key, boolean[].class);
+    public boolean[] getPropertyBooleanArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, boolean[].class);
     }
 
     /**
-     * Retrieves a {@code byte[][]} property by key.
+     * Retrieves a {@code byte[][]} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@code byte[][]} associated with the given key, or {@code null} if no value is
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@code byte[][]} associated with the given path, or {@code null} if no value is
      * set or the value is of a different type.
      */
     @SuppressLint("ArrayReturn")
     @Nullable
-    @SuppressWarnings("unchecked")
-    public byte[][] getPropertyBytesArray(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        ArrayList<Bundle> bundles = getAndCastPropertyArray(key, ArrayList.class);
-        if (bundles == null || bundles.size() == 0) {
-            return null;
-        }
-        byte[][] bytes = new byte[bundles.size()][];
-        for (int i = 0; i < bundles.size(); i++) {
-            Bundle bundle = bundles.get(i);
-            if (bundle == null) {
-                Log.e(TAG, "The inner bundle is null at " + i + ", for key: " + key);
-                continue;
-            }
-            byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD);
-            if (innerBytes == null) {
-                Log.e(TAG, "The bundle at " + i + " contains a null byte[].");
-                continue;
-            }
-            bytes[i] = innerBytes;
-        }
-        return bytes;
+    public byte[][] getPropertyBytesArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, byte[][].class);
     }
 
     /**
-     * Retrieves a repeated {@link GenericDocument} property by key.
+     * Retrieves a repeated {@link GenericDocument} property by path.
      *
-     * @param key The key to look for.
-     * @return The {@link GenericDocument}[] associated with the given key, or {@code null} if no
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The {@link GenericDocument}[] associated with the given path, or {@code null} if no
      * value is set or the value is of a different type.
      */
     @SuppressLint("ArrayReturn")
     @Nullable
-    public GenericDocument[] getPropertyDocumentArray(@NonNull String key) {
-        Preconditions.checkNotNull(key);
-        Bundle[] bundles = getAndCastPropertyArray(key, Bundle[].class);
-        if (bundles == null || bundles.length == 0) {
-            return null;
-        }
-        GenericDocument[] documents = new GenericDocument[bundles.length];
-        for (int i = 0; i < bundles.length; i++) {
-            if (bundles[i] == null) {
-                Log.e(TAG, "The inner bundle is null at " + i + ", for key: " + key);
-                continue;
-            }
-            documents[i] = new GenericDocument(bundles[i]);
-        }
-        return documents;
+    public GenericDocument[] getPropertyDocumentArray(@NonNull String path) {
+        Preconditions.checkNotNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, GenericDocument[].class);
     }
 
     /**
-     * Gets a repeated property of the given key, and casts it to the given class type, which
-     * must be an array class type.
+     * Casts a repeated property to the provided type, logging an error and returning {@code null}
+     * if the cast fails.
+     *
+     * @param path Path to the property within the document. Used for logging.
+     * @param value Value of the property
+     * @param tClass Class to cast the value into
      */
     @Nullable
-    private <T> T getAndCastPropertyArray(@NonNull String key, @NonNull Class<T> tClass) {
-        Object value = mProperties.get(key);
+    private static <T> T safeCastProperty(
+            @NonNull String path, @Nullable Object value, @NonNull Class<T> tClass) {
         if (value == null) {
             return null;
         }
         try {
             return tClass.cast(value);
         } catch (ClassCastException e) {
-            Log.w(TAG, "Error casting to requested type for key \"" + key + "\"", e);
+            Log.w(TAG, "Error casting to requested type for path \"" + path + "\"", e);
             return null;
         }
     }
 
 // @exportToFramework:startStrip()
+
     /**
-     * Converts this GenericDocument into an instance of the provided data class.
+     * Converts this GenericDocument into an instance of the provided document class.
      *
-     * <p>It is the developer's responsibility to ensure the right kind of data class is being
+     * <p>It is the developer's responsibility to ensure the right kind of document class is being
      * supplied here, either by structuring the application code to ensure the document type is
      * known, or by checking the return value of {@link #getSchemaType}.
      *
-     * <p>Document properties are identified by String keys and any that are found are assigned into
-     * fields of the given data class, so the most likely outcome of supplying the wrong data class
-     * would be an empty or partially populated result.
+     * <p>Document properties are identified by {@code String} names. Any that are found are
+     * assigned into fields of the given document class. As such, the most likely outcome of
+     * supplying the wrong document class would be an empty or partially populated result.
      *
-     * @param dataClass a class annotated with
-     *                  {@link androidx.appsearch.annotation.AppSearchDocument}.
+     * @param documentClass a class annotated with {@link Document}
+     * @return an instance of the document class after being converted from a
+     * {@link GenericDocument}
+     * @throws AppSearchException if no factory for this document class could be found on the
+     *                            classpath.
+     * @see GenericDocument#fromDocumentClass
      */
     @NonNull
-    public <T> T toDataClass(@NonNull Class<T> dataClass) throws AppSearchException {
-        Preconditions.checkNotNull(dataClass);
-        DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-        DataClassFactory<T> factory = registry.getOrCreateFactory(dataClass);
+    public <T> T toDocumentClass(@NonNull Class<T> documentClass) throws AppSearchException {
+        Preconditions.checkNotNull(documentClass);
+        DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+        DocumentClassFactory<T> factory = registry.getOrCreateFactory(documentClass);
         return factory.fromGenericDocument(this);
     }
 // @exportToFramework:endStrip()
@@ -520,17 +914,15 @@
         return bundleToString(mBundle).toString();
     }
 
-    @SuppressWarnings("unchecked")
     private static StringBuilder bundleToString(Bundle bundle) {
         StringBuilder stringBuilder = new StringBuilder();
         try {
-            final Set<String> keySet = bundle.keySet();
-            String[] keys = keySet.toArray(new String[0]);
-            // Sort keys to make output deterministic. We need a custom comparator to handle
+            String[] names = bundle.keySet().toArray(new String[0]);
+            // Sort names to make output deterministic. We need a custom comparator to handle
             // nulls (arbitrarily putting them first, similar to Comparator.nullsFirst, which is
             // only available since N).
             Arrays.sort(
-                    keys,
+                    names,
                     (@Nullable String s1, @Nullable String s2) -> {
                         if (s1 == null) {
                             return s2 == null ? 0 : -1;
@@ -540,9 +932,9 @@
                             return s1.compareTo(s2);
                         }
                     });
-            for (String key : keys) {
-                stringBuilder.append("{ key: '").append(key).append("' value: ");
-                Object valueObject = bundle.get(key);
+            for (String name : names) {
+                stringBuilder.append("{ name: '").append(name).append("' value: ");
+                Object valueObject = bundle.get(name);
                 if (valueObject == null) {
                     stringBuilder.append("<null>");
                 } else if (valueObject instanceof Bundle) {
@@ -560,9 +952,11 @@
                         stringBuilder.append("' ");
                     }
                     stringBuilder.append("]");
-                } else if (valueObject instanceof ArrayList) {
-                    for (Bundle innerBundle : (ArrayList<Bundle>) valueObject) {
-                        stringBuilder.append(bundleToString(innerBundle));
+                } else if (valueObject instanceof List) {
+                    @SuppressWarnings("unchecked")
+                    List<Bundle> bundles = (List<Bundle>) valueObject;
+                    for (int i = 0; i < bundles.size(); i++) {
+                        stringBuilder.append(bundleToString(bundles.get(i)));
                     }
                 } else {
                     stringBuilder.append(valueObject.toString());
@@ -593,25 +987,35 @@
         private boolean mBuilt = false;
 
         /**
-         * Create a new {@link GenericDocument.Builder}.
+         * Creates a new {@link GenericDocument.Builder}.
          *
-         * @param uri        The uri of {@link GenericDocument}.
-         * @param schemaType The schema type of the {@link GenericDocument}. The passed-in
-         *                   {@code schemaType} must be defined using
+         * <p>Once {@link #build} is called, the instance can no longer be used.
+         *
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         *
+         * @param namespace  the namespace to set for the {@link GenericDocument}.
+         * @param id         the unique identifier for the {@link GenericDocument} in its namespace.
+         * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
+         *                   provided {@code schemaType} must be defined using
          *                   {@link AppSearchSession#setSchema} prior
          *                   to inserting a document of this {@code schemaType} into the
          *                   AppSearch index using
-         *                   {@link AppSearchSession#putDocuments}. Otherwise, the document will be
-         *                   rejected by {@link AppSearchSession#putDocuments}.
+         *                   {@link AppSearchSession#put}.
+         *                   Otherwise, the document will be rejected by
+         *                   {@link AppSearchSession#put} with result code
+         *                   {@link AppSearchResult#RESULT_NOT_FOUND}.
          */
         @SuppressWarnings("unchecked")
-        public Builder(@NonNull String uri, @NonNull String schemaType) {
-            Preconditions.checkNotNull(uri);
+        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+            Preconditions.checkNotNull(namespace);
+            Preconditions.checkNotNull(id);
             Preconditions.checkNotNull(schemaType);
             mBuilderTypeInstance = (BuilderType) this;
-            mBundle.putString(GenericDocument.URI_FIELD, uri);
+            mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
+            mBundle.putString(GenericDocument.ID_FIELD, id);
             mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
-            mBundle.putString(GenericDocument.NAMESPACE_FIELD, DEFAULT_NAMESPACE);
             // Set current timestamp for creation timestamp by default.
             mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
                     System.currentTimeMillis());
@@ -621,31 +1025,18 @@
         }
 
         /**
-         * Sets the app-defined namespace this Document resides in. No special values are
-         * reserved or understood by the infrastructure.
-         *
-         * <p>URIs are unique within a namespace.
-         *
-         * <p>The number of namespaces per app should be kept small for efficiency reasons.
-         */
-        @NonNull
-        public BuilderType setNamespace(@NonNull String namespace) {
-            mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
-            return mBuilderTypeInstance;
-        }
-
-        /**
          * Sets the score of the {@link GenericDocument}.
          *
          * <p>The score is a query-independent measure of the document's quality, relative to
-         * other {@link GenericDocument}s of the same type.
+         * other {@link GenericDocument} objects of the same {@link AppSearchSchema} type.
          *
          * <p>Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}.
          * Documents with higher scores are considered better than documents with lower scores.
          *
-         * <p>Any nonnegative integer can be used a score.
+         * <p>Any non-negative integer can be used a score. By default, scores are set to 0.
          *
-         * @throws IllegalArgumentException If the provided value is negative.
+         * @param score any non-negative {@code int} representing the document's score.
+         * @throws IllegalStateException if the builder has already been used.
          */
         @NonNull
         public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
@@ -660,11 +1051,15 @@
         /**
          * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds.
          *
-         * <p>Should be set using a value obtained from the {@link System#currentTimeMillis} time
-         * base.
+         * <p>This should be set using a value obtained from the {@link System#currentTimeMillis}
+         * time base.
+         *
+         * @param creationTimestampMillis a creation timestamp in milliseconds.
+         * @throws IllegalStateException if the builder has already been used.
          */
         @NonNull
-        public BuilderType setCreationTimestampMillis(long creationTimestampMillis) {
+        public BuilderType setCreationTimestampMillis(
+                /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
                     creationTimestampMillis);
@@ -672,17 +1067,18 @@
         }
 
         /**
-         * Sets the TTL (Time To Live) of the {@link GenericDocument}, in milliseconds.
+         * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
          *
          * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
          * {@code creationTimestampMillis + ttlMillis}, measured in the
          * {@link System#currentTimeMillis} time base, the document will be auto-deleted.
          *
          * <p>The default value is 0, which means the document is permanent and won't be
-         * auto-deleted until the app is uninstalled.
+         * auto-deleted until the app is uninstalled or {@link AppSearchSession#remove} is
+         * called.
          *
-         * @param ttlMillis A non-negative duration in milliseconds.
-         * @throws IllegalArgumentException If the provided value is negative.
+         * @param ttlMillis a non-negative duration in milliseconds.
+         * @throws IllegalStateException if the builder has already been used.
          */
         @NonNull
         public BuilderType setTtlMillis(long ttlMillis) {
@@ -698,15 +1094,21 @@
          * Sets one or multiple {@code String} values for a property, replacing its previous
          * values.
          *
-         * @param key    The key associated with the {@code values}.
-         * @param values The {@code String} values of the property.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code String} values of the property.
+         * @throws IllegalArgumentException if no values are provided, if provided values exceed
+         *                                  maximum repeated property length, or if a passed in
+         *                                  {@code String} is {@code null}.
+         * @throws IllegalStateException    if the builder has already been used.
          */
         @NonNull
-        public BuilderType setPropertyString(@NonNull String key, @NonNull String... values) {
+        public BuilderType setPropertyString(@NonNull String name, @NonNull String... values) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -714,15 +1116,19 @@
          * Sets one or multiple {@code boolean} values for a property, replacing its previous
          * values.
          *
-         * @param key    The key associated with the {@code values}.
-         * @param values The {@code boolean} values of the property.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code boolean} values of the property.
+         * @throws IllegalArgumentException if values exceed maximum repeated property length.
+         * @throws IllegalStateException    if the builder has already been used.
          */
         @NonNull
-        public BuilderType setPropertyBoolean(@NonNull String key, @NonNull boolean... values) {
+        public BuilderType setPropertyBoolean(@NonNull String name, @NonNull boolean... values) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -730,15 +1136,19 @@
          * Sets one or multiple {@code long} values for a property, replacing its previous
          * values.
          *
-         * @param key    The key associated with the {@code values}.
-         * @param values The {@code long} values of the property.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code long} values of the property.
+         * @throws IllegalArgumentException if values exceed maximum repeated property length.
+         * @throws IllegalStateException    if the builder has already been used.
          */
         @NonNull
-        public BuilderType setPropertyLong(@NonNull String key, @NonNull long... values) {
+        public BuilderType setPropertyLong(@NonNull String name, @NonNull long... values) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -746,30 +1156,41 @@
          * Sets one or multiple {@code double} values for a property, replacing its previous
          * values.
          *
-         * @param key    The key associated with the {@code values}.
-         * @param values The {@code double} values of the property.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code double} values of the property.
+         * @throws IllegalArgumentException if values exceed maximum repeated property length.
+         * @throws IllegalStateException    if the builder has already been used.
          */
         @NonNull
-        public BuilderType setPropertyDouble(@NonNull String key, @NonNull double... values) {
+        public BuilderType setPropertyDouble(@NonNull String name, @NonNull double... values) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
         /**
          * Sets one or multiple {@code byte[]} for a property, replacing its previous values.
          *
-         * @param key    The key associated with the {@code values}.
-         * @param values The {@code byte[]} of the property.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code byte[]} of the property.
+         * @throws IllegalArgumentException if no values are provided, if provided values exceed
+         *                                  maximum repeated property length, or if a passed in
+         *                                  {@code byte[]} is
+         *                                  {@code null}.
+         * @throws IllegalStateException    if the builder has already been used.
          */
         @NonNull
-        public BuilderType setPropertyBytes(@NonNull String key, @NonNull byte[]... values) {
+        public BuilderType setPropertyBytes(@NonNull String name, @NonNull byte[]... values) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
@@ -777,22 +1198,29 @@
          * Sets one or multiple {@link GenericDocument} values for a property, replacing its
          * previous values.
          *
-         * @param key    The key associated with the {@code values}.
-         * @param values The {@link GenericDocument} values of the property.
+         * @param name    the name associated with the {@code values}. Must match the name
+         *                for this property as given in
+         *                {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@link GenericDocument} values of the property.
+         * @throws IllegalArgumentException if no values are provided, if provided values exceed
+         *                                  if provided values exceed maximum repeated property
+         *                                  length, or if a passed in
+         *                                  {@link GenericDocument} is {@code null}.
+         * @throws IllegalStateException    if the builder has already been used.
          */
         @NonNull
         public BuilderType setPropertyDocument(
-                @NonNull String key, @NonNull GenericDocument... values) {
+                @NonNull String name, @NonNull GenericDocument... values) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(name);
             Preconditions.checkNotNull(values);
-            putInPropertyBundle(key, values);
+            putInPropertyBundle(name, values);
             return mBuilderTypeInstance;
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull String[] values)
+        private void putInPropertyBundle(@NonNull String name, @NonNull String[] values)
                 throws IllegalArgumentException {
-            validateRepeatedPropertyLength(key, values.length);
+            validateRepeatedPropertyLength(name, values.length);
             for (int i = 0; i < values.length; i++) {
                 if (values[i] == null) {
                     throw new IllegalArgumentException("The String at " + i + " is null.");
@@ -802,22 +1230,22 @@
                             + MAX_STRING_LENGTH + ".");
                 }
             }
-            mProperties.putStringArray(key, values);
+            mProperties.putStringArray(name, values);
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull boolean[] values) {
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.putBooleanArray(key, values);
+        private void putInPropertyBundle(@NonNull String name, @NonNull boolean[] values) {
+            validateRepeatedPropertyLength(name, values.length);
+            mProperties.putBooleanArray(name, values);
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull double[] values) {
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.putDoubleArray(key, values);
+        private void putInPropertyBundle(@NonNull String name, @NonNull double[] values) {
+            validateRepeatedPropertyLength(name, values.length);
+            mProperties.putDoubleArray(name, values);
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull long[] values) {
-            validateRepeatedPropertyLength(key, values.length);
-            mProperties.putLongArray(key, values);
+        private void putInPropertyBundle(@NonNull String name, @NonNull long[] values) {
+            validateRepeatedPropertyLength(name, values.length);
+            mProperties.putLongArray(name, values);
         }
 
         /**
@@ -826,8 +1254,8 @@
          * <p>Bundle doesn't support for two dimension array byte[][], we are converting byte[][]
          * into ArrayList<Bundle>, and each elements will contain a one dimension byte[].
          */
-        private void putInPropertyBundle(@NonNull String key, @NonNull byte[][] values) {
-            validateRepeatedPropertyLength(key, values.length);
+        private void putInPropertyBundle(@NonNull String name, @NonNull byte[][] values) {
+            validateRepeatedPropertyLength(name, values.length);
             ArrayList<Bundle> bundles = new ArrayList<>(values.length);
             for (int i = 0; i < values.length; i++) {
                 if (values[i] == null) {
@@ -837,33 +1265,35 @@
                 bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]);
                 bundles.add(bundle);
             }
-            mProperties.putParcelableArrayList(key, bundles);
+            mProperties.putParcelableArrayList(name, bundles);
         }
 
-        private void putInPropertyBundle(@NonNull String key, @NonNull GenericDocument[] values) {
-            validateRepeatedPropertyLength(key, values.length);
-            Bundle[] documentBundles = new Bundle[values.length];
+        private void putInPropertyBundle(@NonNull String name, @NonNull GenericDocument[] values) {
+            validateRepeatedPropertyLength(name, values.length);
+            Parcelable[] documentBundles = new Parcelable[values.length];
             for (int i = 0; i < values.length; i++) {
                 if (values[i] == null) {
                     throw new IllegalArgumentException("The document at " + i + " is null.");
                 }
                 documentBundles[i] = values[i].mBundle;
             }
-            mProperties.putParcelableArray(key, documentBundles);
+            mProperties.putParcelableArray(name, documentBundles);
         }
 
-        private static void validateRepeatedPropertyLength(@NonNull String key, int length) {
-            if (length == 0) {
-                throw new IllegalArgumentException("The input array is empty.");
-            } else if (length > MAX_REPEATED_PROPERTY_LENGTH) {
+        private static void validateRepeatedPropertyLength(@NonNull String name, int length) {
+            if (length > MAX_REPEATED_PROPERTY_LENGTH) {
                 throw new IllegalArgumentException(
-                        "Repeated property \"" + key + "\" has length " + length
+                        "Repeated property \"" + name + "\" has length " + length
                                 + ", which exceeds the limit of "
                                 + MAX_REPEATED_PROPERTY_LENGTH);
             }
         }
 
-        /** Builds the {@link GenericDocument} object. */
+        /**
+         * Builds the {@link GenericDocument} object.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
         @NonNull
         public GenericDocument build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
new file mode 100644
index 0000000..2e4ece1
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
@@ -0,0 +1,188 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Encapsulates a request to retrieve documents by namespace and IDs from the
+ * {@link AppSearchSession} database.
+ *
+ * @see AppSearchSession#getByDocumentId
+ */
+public final class GetByDocumentIdRequest {
+    /**
+     * Schema type to be used in
+     * {@link GetByDocumentIdRequest.Builder#addProjection}
+     * to apply property paths to all results, excepting any types that have had their own, specific
+     * property paths set.
+     */
+    public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+    private final String mNamespace;
+    private final Set<String> mIds;
+    private final Map<String, List<String>> mTypePropertyPathsMap;
+
+    GetByDocumentIdRequest(@NonNull String namespace, @NonNull Set<String> ids, @NonNull Map<String,
+            List<String>> typePropertyPathsMap) {
+        mNamespace = Preconditions.checkNotNull(namespace);
+        mIds = Preconditions.checkNotNull(ids);
+        mTypePropertyPathsMap = Preconditions.checkNotNull(typePropertyPathsMap);
+    }
+
+    /** Returns the namespace attached to the request. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the set of document IDs attached to the request. */
+    @NonNull
+    public Set<String> getIds() {
+        return Collections.unmodifiableSet(mIds);
+    }
+
+    /**
+     * Returns a map from schema type to property paths to be used for projection.
+     *
+     * <p>If the map is empty, then all properties will be retrieved for all results.
+     *
+     * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
+     * by this function, rather than calling it multiple times.
+     */
+    @NonNull
+    public Map<String, List<String>> getProjections() {
+        Map<String, List<String>> copy = new ArrayMap<>();
+        for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
+            copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+        }
+        return copy;
+    }
+
+    /**
+     * Returns a map from schema type to property paths to be used for projection.
+     *
+     * <p>If the map is empty, then all properties will be retrieved for all results.
+     *
+     * <p>A more efficient version of {@link #getProjections}, but it returns a modifiable map.
+     * This is not meant to be unhidden and should only be used by internal classes.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public Map<String, List<String>> getProjectionsInternal() {
+        return mTypePropertyPathsMap;
+    }
+
+    /**
+     * Builder for {@link GetByDocumentIdRequest} objects.
+     *
+     * <p>Once {@link #build} is called, the instance can no longer be used.
+     */
+    public static final class Builder {
+        private final String mNamespace;
+        private final Set<String> mIds = new ArraySet<>();
+        private final Map<String, List<String>> mProjectionTypePropertyPaths = new ArrayMap<>();
+        private boolean mBuilt = false;
+
+        /** Creates a {@link GetByDocumentIdRequest.Builder} instance. */
+        public Builder(@NonNull String namespace) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+        }
+
+        /**
+         * Adds one or more document IDs to the request.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public Builder addIds(@NonNull String... ids) {
+            Preconditions.checkNotNull(ids);
+            return addIds(Arrays.asList(ids));
+        }
+
+        /**
+         * Adds a collection of IDs to the request.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public Builder addIds(@NonNull Collection<String> ids) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(ids);
+            mIds.addAll(ids);
+            return this;
+        }
+
+        /**
+         * Adds property paths for the specified type to be used for projection. If property
+         * paths are added for a type, then only the properties referred to will be retrieved for
+         * results of that type. If a property path that is specified isn't present in a result,
+         * it will be ignored for that result. Property paths cannot be null.
+         *
+         * <p>If no property paths are added for a particular type, then all properties of
+         * results of that type will be retrieved.
+         *
+         * <p>If property path is added for the
+         * {@link GetByDocumentIdRequest#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths
+         * will apply to all results, excepting any types that have their own, specific property
+         * paths set.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         *
+         * @see SearchSpec.Builder#addProjection
+         */
+        @NonNull
+        public Builder addProjection(
+                @NonNull String schemaType, @NonNull Collection<String> propertyPaths) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(propertyPaths);
+            List<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
+            for (String propertyPath : propertyPaths) {
+                Preconditions.checkNotNull(propertyPath);
+                propertyPathsList.add(propertyPath);
+            }
+            mProjectionTypePropertyPaths.put(schemaType, propertyPathsList);
+            return this;
+        }
+
+        /**
+         * Builds a new {@link GetByDocumentIdRequest}.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public GetByDocumentIdRequest build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBuilt = true;
+            return new GetByDocumentIdRequest(mNamespace, mIds, mProjectionTypePropertyPaths);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByUriRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByUriRequest.java
deleted file mode 100644
index 9461790..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByUriRequest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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 androidx.appsearch.app;
-
-import androidx.annotation.NonNull;
-import androidx.collection.ArraySet;
-import androidx.core.util.Preconditions;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-
-/**
- * Encapsulates a request to retrieve documents by namespace and URI.
- *
- * @see AppSearchSession#getByUri
- */
-public final class GetByUriRequest {
-    private final String mNamespace;
-    private final Set<String> mUris;
-
-    GetByUriRequest(@NonNull String namespace, @NonNull Set<String> uris) {
-        mNamespace = namespace;
-        mUris = uris;
-    }
-
-    /** Returns the namespace to get documents from. */
-    @NonNull
-    public String getNamespace() {
-        return mNamespace;
-    }
-
-    /** Returns the URIs to get from the namespace. */
-    @NonNull
-    public Set<String> getUris() {
-        return Collections.unmodifiableSet(mUris);
-    }
-
-    /** Builder for {@link GetByUriRequest} objects. */
-    public static final class Builder {
-        private String mNamespace = GenericDocument.DEFAULT_NAMESPACE;
-        private final Set<String> mUris = new ArraySet<>();
-        private boolean mBuilt = false;
-
-        /**
-         * Sets which namespace these documents will be retrieved from.
-         *
-         * <p>If this is not set, it defaults to {@link GenericDocument#DEFAULT_NAMESPACE}.
-         */
-        @NonNull
-        public Builder setNamespace(@NonNull String namespace) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(namespace);
-            mNamespace = namespace;
-            return this;
-        }
-
-        /** Adds one or more URIs to the request. */
-        @NonNull
-        public Builder addUri(@NonNull String... uris) {
-            Preconditions.checkNotNull(uris);
-            return addUri(Arrays.asList(uris));
-        }
-
-        /** Adds one or more URIs to the request. */
-        @NonNull
-        public Builder addUri(@NonNull Collection<String> uris) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(uris);
-            mUris.addAll(uris);
-            return this;
-        }
-
-        /** Builds a new {@link GetByUriRequest}. */
-        @NonNull
-        public GetByUriRequest build() {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            mBuilt = true;
-            return new GetByUriRequest(mNamespace, mUris);
-        }
-    }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
new file mode 100644
index 0000000..dab249a
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import android.os.Bundle;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+/** The response class of {@link AppSearchSession#getSchema} */
+public class GetSchemaResponse {
+    private static final String VERSION_FIELD = "version";
+    private static final String SCHEMAS_FIELD = "schemas";
+
+    private final Bundle mBundle;
+
+    GetSchemaResponse(@NonNull Bundle bundle) {
+        mBundle = Preconditions.checkNotNull(bundle);
+    }
+
+    /**
+     * Returns the {@link Bundle} populated by this builder.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public Bundle getBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Returns the overall database schema version.
+     *
+     * <p>If the database is empty, 0 will be returned.
+     */
+    @IntRange(from = 0)
+    public int getVersion() {
+        return mBundle.getInt(VERSION_FIELD);
+    }
+
+    /**
+     * Return the schemas most recently successfully provided to
+     * {@link AppSearchSession#setSchema}.
+     *
+     * <p>It is inefficient to call this method repeatedly.
+     */
+    @NonNull
+    public Set<AppSearchSchema> getSchemas() {
+        ArrayList<Bundle> schemaBundles = mBundle.getParcelableArrayList(SCHEMAS_FIELD);
+        Set<AppSearchSchema> schemas = new ArraySet<>(schemaBundles.size());
+        for (int i = 0; i < schemaBundles.size(); i++) {
+            schemas.add(new AppSearchSchema(schemaBundles.get(i)));
+        }
+        return schemas;
+    }
+
+    /**
+     * Builder for {@link GetSchemaResponse} objects.
+     */
+    public static final class Builder {
+        private int mVersion = 0;
+        private boolean mBuilt = false;
+        private final ArrayList<Bundle> mSchemaBundles = new ArrayList<>();
+
+        /**
+         * Sets the database overall schema version.
+         *
+         * <p>Default version is 0
+         */
+        @NonNull
+        public Builder setVersion(@IntRange(from = 0) int version) {
+            mVersion = version;
+            return this;
+        }
+
+        /**  Adds one {@link AppSearchSchema} to the schema list.  */
+        @NonNull
+        public Builder addSchema(@NonNull AppSearchSchema schema) {
+            mSchemaBundles.add(schema.getBundle());
+            return this;
+        }
+
+        /** Builds a {@link GetSchemaResponse} object. */
+        @NonNull
+        public GetSchemaResponse build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Bundle bundle = new Bundle();
+            bundle.putInt(VERSION_FIELD, mVersion);
+            bundle.putParcelableArrayList(SCHEMAS_FIELD, mSchemaBundles);
+            mBuilt = true;
+            return new GetSchemaResponse(bundle);
+        }
+    }
+
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
index fc35552..8b7dbe9 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
@@ -18,54 +18,66 @@
 
 import androidx.annotation.NonNull;
 
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.Closeable;
+
 /**
- * This class provides global access to the centralized AppSearch index maintained by the system.
+ * Provides a connection to all AppSearch databases the querying application has been
+ * granted access to.
  *
- * <p>Apps can retrieve indexed documents through the query API.
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @see AppSearchSession
  */
-public interface GlobalSearchSession {
+public interface GlobalSearchSession extends Closeable {
     /**
-     * Searches across all documents in the storage based on a given query string.
+     * Retrieves documents from all AppSearch databases that the querying application has access to.
      *
-     * <p>Currently we support following features in the raw query format:
-     * <ul>
-     *     <li>AND
-     *     <p>AND joins (e.g. “match documents that have both the terms ‘dog’ and
-     *     ‘cat’”).
-     *     Example: hello world matches documents that have both ‘hello’ and ‘world’
-     *     <li>OR
-     *     <p>OR joins (e.g. “match documents that have either the term ‘dog’ or
-     *     ‘cat’”).
-     *     Example: dog OR puppy
-     *     <li>Exclusion
-     *     <p>Exclude a term (e.g. “match documents that do
-     *     not have the term ‘dog’”).
-     *     Example: -dog excludes the term ‘dog’
-     *     <li>Grouping terms
-     *     <p>Allow for conceptual grouping of subqueries to enable hierarchical structures (e.g.
-     *     “match documents that have either ‘dog’ or ‘puppy’, and either ‘cat’ or ‘kitten’”).
-     *     Example: (dog puppy) (cat kitten) two one group containing two terms.
-     *     <li>Property restricts
-     *     <p> Specifies which properties of a document to specifically match terms in (e.g.
-     *     “match documents where the ‘subject’ property contains ‘important’”).
-     *     Example: subject:important matches documents with the term ‘important’ in the
-     *     ‘subject’ property
-     *     <li>Schema type restricts
-     *     <p>This is similar to property restricts, but allows for restricts on top-level document
-     *     fields, such as schema_type. Clients should be able to limit their query to documents of
-     *     a certain schema_type (e.g. “match documents that are of the ‘Email’ schema_type”).
-     *     Example: { schema_type_filters: “Email”, “Video”,query: “dog” } will match documents
-     *     that contain the query term ‘dog’ and are of either the ‘Email’ schema type or the
-     *     ‘Video’ schema type.
-     * </ul>
+     * <p>Applications can be granted access to documents by specifying
+     * {@link SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}, or
+     * {@link SetSchemaRequest.Builder#setDocumentClassVisibilityForPackage} when building a schema.
      *
-     * <p> This method is lightweight. The heavy work will be done in
-     * {@link SearchResults#getNextPage()}.
+     * <p>Document access can also be granted to system UIs by specifying
+     * {@link SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}, or
+     * {@link SetSchemaRequest.Builder#setDocumentClassDisplayedBySystem}
+     * when building a schema.
      *
-     * @param queryExpression Query String to search.
-     * @param searchSpec      Spec for setting filters, raw query etc.
-     * @return The search result of performing this operation.
+     * <p>See {@link AppSearchSession#search} for a detailed explanation on
+     * forming a query string.
+     *
+     * <p>This method is lightweight. The heavy work will be done in
+     * {@link SearchResults#getNextPage}.
+     *
+     * @param queryExpression query string to search.
+     * @param searchSpec      spec for setting document filters, adding projection, setting term
+     *                        match type, etc.
+     * @return a {@link SearchResults} object for retrieved matched documents.
      */
     @NonNull
-    SearchResults query(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+    SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);
+
+    /**
+     * Reports that a particular document has been used from a system surface.
+     *
+     * <p>See {@link AppSearchSession#reportUsage} for a general description of document usage, as
+     * well as an API that can be used by the app itself.
+     *
+     * <p>Usage reported via this method is accounted separately from usage reported via
+     * {@link AppSearchSession#reportUsage} and may be accessed using the constants
+     * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_COUNT} and
+     * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP}.
+     *
+     * @return The pending result of performing this operation which resolves to {@code null} on
+     *     success. The pending result will be completed with an
+     *     {@link androidx.appsearch.exceptions.AppSearchException} with a code of
+     *     {@link AppSearchResult#RESULT_SECURITY_ERROR} if this API is invoked by an app which
+     *     is not part of the system.
+     */
+    @NonNull
+    ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request);
+
+    /** Closes the {@link GlobalSearchSession}. */
+    @Override
+    void close();
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
new file mode 100644
index 0000000..b47735b
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Migrator.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+
+/**
+ * A migrator class to translate {@link GenericDocument} from different version of
+ * {@link AppSearchSchema}
+ *
+ * <p>Make non-backwards-compatible changes will delete all stored documents in old schema. You
+ * can save your documents by setting {@link Migrator} via the
+ * {@link SetSchemaRequest.Builder#setMigrator} for each type and target version you want to save.
+ *
+ * <p>{@link #onDowngrade} or {@link #onUpgrade} will be triggered if the version number of the
+ * schema stored in AppSearch is different with the version in the request.
+ *
+ * <p>If any error or Exception occurred in the {@link #onDowngrade} or {@link #onUpgrade}, all the
+ * setSchema request will be rejected unless the schema changes are backwards-compatible, and stored
+ * documents won't have any observable changes.
+ */
+public abstract class Migrator {
+    /**
+     * Returns {@code true} if this migrator's source type needs to be migrated to update from
+     * currentVersion to finalVersion.
+     *
+     * <p>Migration won't be triggered if currentVersion is equal to finalVersion even if
+     * {@link #shouldMigrate} return true;
+     */
+    public abstract boolean shouldMigrate(int currentVersion, int finalVersion);
+
+    /**
+     * Migrates {@link GenericDocument} to a newer version of {@link AppSearchSchema}.
+     *
+     * <p>This method will be invoked only if the {@link SetSchemaRequest} is setting a
+     * higher version number than the current {@link AppSearchSchema} saved in AppSearch.
+     *
+     * <p>If this {@link Migrator} is provided to cover a compatible schema change via
+     * {@link AppSearchSession#setSchema}, documents under the old version won't be removed
+     * unless you use the same document ID.
+     *
+     * <p>This method will be invoked on the background worker thread provided via
+     * {@link AppSearchSession#setSchema}.
+     *
+     * @param currentVersion The current version of the document's schema.
+     * @param finalVersion  The final version that documents need to be migrated to.
+     * @param document       The {@link GenericDocument} need to be translated to new version.
+     * @return               A {@link GenericDocument} in new version.
+     */
+    @WorkerThread
+    @NonNull
+    public abstract GenericDocument onUpgrade(int currentVersion, int finalVersion,
+            @NonNull GenericDocument document);
+
+    /**
+     * Migrates {@link GenericDocument} to an older version of {@link AppSearchSchema}.
+     *
+     * <p>This method will be invoked only if the {@link SetSchemaRequest} is setting a
+     * lower version number than the current {@link AppSearchSchema} saved in AppSearch.
+     *
+     * <p>If this {@link Migrator} is provided to cover a compatible schema change via
+     * {@link AppSearchSession#setSchema}, documents under the old version won't be removed
+     * unless you use the same document ID.
+     *
+     * <p>This method will be invoked on the background worker thread.
+     *
+     * @param currentVersion The current version of the document's schema.
+     * @param finalVersion  The final version that documents need to be migrated to.
+     * @param document       The {@link GenericDocument} need to be translated to new version.
+     * @return               A {@link GenericDocument} in new version.
+     */
+    @WorkerThread
+    @NonNull
+    public abstract GenericDocument onDowngrade(int currentVersion, int finalVersion,
+            @NonNull GenericDocument document);
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
index 17d6fae..5d54f23 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
@@ -16,16 +16,19 @@
 
 package androidx.appsearch.app;
 
-import androidx.annotation.NonNull;
-import androidx.core.util.ObjectsCompat;
-import androidx.core.util.Preconditions;
+import android.os.Bundle;
 
-import java.util.Arrays;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.util.BundleUtil;
+import androidx.core.util.Preconditions;
 
 /** This class represents a uniquely identifiable package. */
 public class PackageIdentifier {
-    private final String mPackageName;
-    private final byte[] mSha256Certificate;
+    private static final String PACKAGE_NAME_FIELD = "packageName";
+    private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";
+
+    private final Bundle mBundle;
 
     /**
      * Creates a unique identifier for a package.
@@ -34,18 +37,32 @@
      * @param sha256Certificate SHA256 certificate digest of the package.
      */
     public PackageIdentifier(@NonNull String packageName, @NonNull byte[] sha256Certificate) {
-        mPackageName = Preconditions.checkNotNull(packageName);
-        mSha256Certificate = Preconditions.checkNotNull(sha256Certificate);
+        mBundle = new Bundle();
+        mBundle.putString(PACKAGE_NAME_FIELD, packageName);
+        mBundle.putByteArray(SHA256_CERTIFICATE_FIELD, sha256Certificate);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public PackageIdentifier(@NonNull Bundle bundle) {
+        mBundle = Preconditions.checkNotNull(bundle);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public Bundle getBundle() {
+        return mBundle;
     }
 
     @NonNull
     public String getPackageName() {
-        return mPackageName;
+        return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD));
     }
 
     @NonNull
     public byte[] getSha256Certificate() {
-        return mSha256Certificate;
+        return Preconditions.checkNotNull(mBundle.getByteArray(SHA256_CERTIFICATE_FIELD));
     }
 
     @Override
@@ -57,12 +74,11 @@
             return false;
         }
         final PackageIdentifier other = (PackageIdentifier) obj;
-        return this.mPackageName.equals(other.mPackageName)
-                && Arrays.equals(this.mSha256Certificate, other.mSha256Certificate);
+        return BundleUtil.deepEquals(mBundle, other.mBundle);
     }
 
     @Override
     public int hashCode() {
-        return ObjectsCompat.hash(mPackageName, Arrays.hashCode(mSha256Certificate));
+        return BundleUtil.deepHashCode(mBundle);
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
index 17d424e..c4a0c6d 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
@@ -29,9 +29,13 @@
 import java.util.List;
 
 /**
- * Encapsulates a request to index a document into an {@link AppSearchSession} database.
+ * Encapsulates a request to index documents into an {@link AppSearchSession} database.
  *
- * <p>@see AppSearchSession#putDocuments
+ * <p>Documents added to the request can be instances of classes annotated with
+ * {@link androidx.appsearch.annotation.Document} or instances of
+ * {@link GenericDocument}.
+ *
+ * @see AppSearchSession#put
  */
 public final class PutDocumentsRequest {
     private final List<GenericDocument> mDocuments;
@@ -40,9 +44,9 @@
         mDocuments = documents;
     }
 
-    /** Returns the documents that are part of this request. */
+    /** Returns a list of {@link GenericDocument} objects that are part of this request. */
     @NonNull
-    public List<GenericDocument> getDocuments() {
+    public List<GenericDocument> getGenericDocuments() {
         return Collections.unmodifiableList(mDocuments);
     }
 
@@ -55,18 +59,24 @@
         private final List<GenericDocument> mDocuments = new ArrayList<>();
         private boolean mBuilt = false;
 
-        /** Adds one or more {@link GenericDocument} objects to the request. */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getDocuments()
+        /**
+         * Adds one or more {@link GenericDocument} objects to the request.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
         @NonNull
-        public Builder addGenericDocument(@NonNull GenericDocument... documents) {
+        public Builder addGenericDocuments(@NonNull GenericDocument... documents) {
             Preconditions.checkNotNull(documents);
-            return addGenericDocument(Arrays.asList(documents));
+            return addGenericDocuments(Arrays.asList(documents));
         }
 
-        /** Adds a collection of {@link GenericDocument} objects to the request. */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getDocuments()
+        /**
+         * Adds a collection of {@link GenericDocument} objects to the request.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
         @NonNull
-        public Builder addGenericDocument(
+        public Builder addGenericDocuments(
                 @NonNull Collection<? extends GenericDocument> documents) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkNotNull(documents);
@@ -76,54 +86,53 @@
 
 // @exportToFramework:startStrip()
         /**
-         * Adds one or more annotated {@link androidx.appsearch.annotation.AppSearchDocument}
+         * Adds one or more annotated {@link androidx.appsearch.annotation.Document}
          * documents to the request.
          *
-         * @param dataClasses annotated
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument} documents.
-         * @throws AppSearchException if an error occurs converting a data class into a
+         * @param documents annotated
+         *                    {@link androidx.appsearch.annotation.Document} documents.
+         * @throws AppSearchException if an error occurs converting a document class into a
          *                            {@link GenericDocument}.
+         * @throws IllegalStateException if the builder has already been used.
          */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getDocuments()
+        // Merged list available from getGenericDocuments()
+        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder addDataClass(@NonNull Object... dataClasses) throws AppSearchException {
-            Preconditions.checkNotNull(dataClasses);
-            return addDataClass(Arrays.asList(dataClasses));
+        public Builder addDocuments(@NonNull Object... documents) throws AppSearchException {
+            Preconditions.checkNotNull(documents);
+            return addDocuments(Arrays.asList(documents));
         }
 
         /**
          * Adds a collection of annotated
-         * {@link androidx.appsearch.annotation.AppSearchDocument} documents to the request.
+         * {@link androidx.appsearch.annotation.Document} documents to the request.
          *
-         * @param dataClasses annotated
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument} documents.
-         * @throws AppSearchException if an error occurs converting a data class into a
+         * @param documents annotated
+         *                    {@link androidx.appsearch.annotation.Document} documents.
+         * @throws AppSearchException if an error occurs converting a document into a
          *                            {@link GenericDocument}.
+         * @throws IllegalStateException if the builder has already been used.
          */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getDocuments()
+        // Merged list available from getGenericDocuments()
+        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder addDataClass(@NonNull Collection<?> dataClasses)
-                throws AppSearchException {
+        public Builder addDocuments(@NonNull Collection<?> documents) throws AppSearchException {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(dataClasses);
-            List<GenericDocument> genericDocuments = new ArrayList<>(dataClasses.size());
-            for (Object dataClass : dataClasses) {
-                GenericDocument genericDocument = toGenericDocument(dataClass);
+            Preconditions.checkNotNull(documents);
+            List<GenericDocument> genericDocuments = new ArrayList<>(documents.size());
+            for (Object document : documents) {
+                GenericDocument genericDocument = GenericDocument.fromDocumentClass(document);
                 genericDocuments.add(genericDocument);
             }
-            return addGenericDocument(genericDocuments);
-        }
-
-        @NonNull
-        private static <T> GenericDocument toGenericDocument(@NonNull T dataClass)
-                throws AppSearchException {
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            DataClassFactory<T> factory = registry.getOrCreateFactory(dataClass);
-            return factory.toGenericDocument(dataClass);
+            return addGenericDocuments(genericDocuments);
         }
 // @exportToFramework:endStrip()
 
-        /** Creates a new {@link PutDocumentsRequest} object. */
+        /**
+         * Creates a new {@link PutDocumentsRequest} object.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
         @NonNull
         public PutDocumentsRequest build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
new file mode 100644
index 0000000..3425aa0
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
@@ -0,0 +1,106 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Encapsulates a request to remove documents by namespace and IDs from the
+ * {@link AppSearchSession} database.
+ *
+ * @see AppSearchSession#remove
+ */
+public final class RemoveByDocumentIdRequest {
+    private final String mNamespace;
+    private final Set<String> mIds;
+
+    RemoveByDocumentIdRequest(String namespace, Set<String> ids) {
+        mNamespace = namespace;
+        mIds = ids;
+    }
+
+    /** Returns the namespace to remove documents from. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the set of document IDs attached to the request. */
+    @NonNull
+    public Set<String> getIds() {
+        return Collections.unmodifiableSet(mIds);
+    }
+
+    /**
+     * Builder for {@link RemoveByDocumentIdRequest} objects.
+     *
+     * <p>Once {@link #build} is called, the instance can no longer be used.
+     */
+    public static final class Builder {
+        private final String mNamespace;
+        private final Set<String> mIds = new ArraySet<>();
+        private boolean mBuilt = false;
+
+        /** Creates a {@link RemoveByDocumentIdRequest.Builder} instance. */
+        public Builder(@NonNull String namespace) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+        }
+
+        /**
+         * Adds one or more document IDs to the request.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public Builder addIds(@NonNull String... ids) {
+            Preconditions.checkNotNull(ids);
+            return addIds(Arrays.asList(ids));
+        }
+
+        /**
+         * Adds a collection of IDs to the request.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public Builder addIds(@NonNull Collection<String> ids) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(ids);
+            mIds.addAll(ids);
+            return this;
+        }
+
+        /**
+         * Builds a new {@link RemoveByDocumentIdRequest}.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public RemoveByDocumentIdRequest build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBuilt = true;
+            return new RemoveByDocumentIdRequest(mNamespace, mIds);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
deleted file mode 100644
index ed7cad9..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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 androidx.appsearch.app;
-
-import androidx.annotation.NonNull;
-import androidx.collection.ArraySet;
-import androidx.core.util.Preconditions;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-
-/**
- * Encapsulates a request to remove documents by namespace and URI.
- *
- * @see AppSearchSession#removeByUri
- */
-public final class RemoveByUriRequest {
-    private final String mNamespace;
-    private final Set<String> mUris;
-
-    RemoveByUriRequest(String namespace, Set<String> uris) {
-        mNamespace = namespace;
-        mUris = uris;
-    }
-
-    /** Returns the namespace to remove documents from. */
-    @NonNull
-    public String getNamespace() {
-        return mNamespace;
-    }
-
-    /** Returns the URIs of documents to remove from the namespace. */
-    @NonNull
-    public Set<String> getUris() {
-        return Collections.unmodifiableSet(mUris);
-    }
-
-    /** Builder for {@link RemoveByUriRequest} objects. */
-    public static final class Builder {
-        private String mNamespace = GenericDocument.DEFAULT_NAMESPACE;
-        private final Set<String> mUris = new ArraySet<>();
-        private boolean mBuilt = false;
-
-        /**
-         * Sets which namespace these documents will be removed from.
-         *
-         * <p>If this is not set, it defaults to {@link GenericDocument#DEFAULT_NAMESPACE}.
-         */
-        @NonNull
-        public Builder setNamespace(@NonNull String namespace) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(namespace);
-            mNamespace = namespace;
-            return this;
-        }
-
-        /** Adds one or more URIs to the request. */
-        @NonNull
-        public Builder addUri(@NonNull String... uris) {
-            Preconditions.checkNotNull(uris);
-            return addUri(Arrays.asList(uris));
-        }
-
-        /** Adds one or more URIs to the request. */
-        @NonNull
-        public Builder addUri(@NonNull Collection<String> uris) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(uris);
-            mUris.addAll(uris);
-            return this;
-        }
-
-        /** Builds a new {@link RemoveByUriRequest}. */
-        @NonNull
-        public RemoveByUriRequest build() {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            mBuilt = true;
-            return new RemoveByUriRequest(mNamespace, mUris);
-        }
-    }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
new file mode 100644
index 0000000..b483d63
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportSystemUsageRequest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+/**
+ * A request to report usage of a document owned by another app from a system UI surface.
+ *
+ * <p>Usage reported in this way is measured separately from usage reported via
+ * {@link AppSearchSession#reportUsage}.
+ *
+ * <p>See {@link GlobalSearchSession#reportSystemUsage} for a detailed description of usage
+ * reporting.
+ */
+public final class ReportSystemUsageRequest {
+    private final String mPackageName;
+    private final String mDatabase;
+    private final String mNamespace;
+    private final String mDocumentId;
+    private final long mUsageTimestampMillis;
+
+    ReportSystemUsageRequest(
+            @NonNull String packageName,
+            @NonNull String database,
+            @NonNull String namespace,
+            @NonNull String documentId,
+            long usageTimestampMillis) {
+        mPackageName = Preconditions.checkNotNull(packageName);
+        mDatabase = Preconditions.checkNotNull(database);
+        mNamespace = Preconditions.checkNotNull(namespace);
+        mDocumentId = Preconditions.checkNotNull(documentId);
+        mUsageTimestampMillis = usageTimestampMillis;
+    }
+
+    /** Returns the package name of the app which owns the document that was used. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns the database in which the document that was used resides. */
+    @NonNull
+    public String getDatabaseName() {
+        return mDatabase;
+    }
+
+    /** Returns the namespace of the document that was used. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the ID of document that was used. */
+    @NonNull
+    public String getDocumentId() {
+        return mDocumentId;
+    }
+
+    /**
+     * Returns the timestamp in milliseconds of the usage report (the time at which the document
+     * was used).
+     *
+     * <p>The value is in the {@link System#currentTimeMillis} time base.
+     */
+    /*@exportToFramework:CurrentTimeMillisLong*/
+    public long getUsageTimestampMillis() {
+        return mUsageTimestampMillis;
+    }
+
+    /** Builder for {@link ReportSystemUsageRequest} objects. */
+    public static final class Builder {
+        private final String mPackageName;
+        private final String mDatabase;
+        private final String mNamespace;
+        private final String mDocumentId;
+        private Long mUsageTimestampMillis;
+        private boolean mBuilt = false;
+
+        /** Creates a {@link ReportSystemUsageRequest.Builder} instance. */
+        public Builder(
+                @NonNull String packageName,
+                @NonNull String database,
+                @NonNull String namespace,
+                @NonNull String documentId) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+            mDatabase = Preconditions.checkNotNull(database);
+            mNamespace = Preconditions.checkNotNull(namespace);
+            mDocumentId = Preconditions.checkNotNull(documentId);
+        }
+
+        /**
+         * Sets the timestamp in milliseconds of the usage report (the time at which the document
+         * was used).
+         *
+         * <p>The value is in the {@link System#currentTimeMillis} time base.
+         *
+         * <p>If unset, this defaults to the current timestamp at the time that the
+         * {@link ReportSystemUsageRequest} is constructed.
+         *
+         * @throws IllegalStateException if the builder has already been used
+         */
+        @NonNull
+        public ReportSystemUsageRequest.Builder setUsageTimestampMillis(
+                /*@exportToFramework:CurrentTimeMillisLong*/ long usageTimestampMillis) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mUsageTimestampMillis = usageTimestampMillis;
+            return this;
+        }
+
+        /**
+         * Builds a new {@link ReportSystemUsageRequest}.
+         *
+         * @throws IllegalStateException if the builder has already been used
+         */
+        @NonNull
+        public ReportSystemUsageRequest build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            if (mUsageTimestampMillis == null) {
+                mUsageTimestampMillis = System.currentTimeMillis();
+            }
+            mBuilt = true;
+            return new ReportSystemUsageRequest(
+                    mPackageName, mDatabase, mNamespace, mDocumentId, mUsageTimestampMillis);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
new file mode 100644
index 0000000..584874d
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+/**
+ * A request to report usage of a document.
+ *
+ * <p>See {@link AppSearchSession#reportUsage} for a detailed description of usage reporting.
+ *
+ * @see AppSearchSession#reportUsage
+ */
+public final class ReportUsageRequest {
+    private final String mNamespace;
+    private final String mDocumentId;
+    private final long mUsageTimestampMillis;
+
+    ReportUsageRequest(
+            @NonNull String namespace, @NonNull String documentId, long usageTimestampMillis) {
+        mNamespace = Preconditions.checkNotNull(namespace);
+        mDocumentId = Preconditions.checkNotNull(documentId);
+        mUsageTimestampMillis = usageTimestampMillis;
+    }
+
+    /** Returns the namespace of the document that was used. */
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    /** Returns the ID of document that was used. */
+    @NonNull
+    public String getDocumentId() {
+        return mDocumentId;
+    }
+
+    /**
+     * Returns the timestamp in milliseconds of the usage report (the time at which the document
+     * was used).
+     *
+     * <p>The value is in the {@link System#currentTimeMillis} time base.
+     */
+    /*@exportToFramework:CurrentTimeMillisLong*/
+    public long getUsageTimestampMillis() {
+        return mUsageTimestampMillis;
+    }
+
+    /** Builder for {@link ReportUsageRequest} objects. */
+    public static final class Builder {
+        private final String mNamespace;
+        private final String mDocumentId;
+        private Long mUsageTimestampMillis;
+        private boolean mBuilt = false;
+
+        /** Creates a {@link ReportUsageRequest.Builder} instance. */
+        public Builder(@NonNull String namespace, @NonNull String documentId) {
+            mNamespace = Preconditions.checkNotNull(namespace);
+            mDocumentId = Preconditions.checkNotNull(documentId);
+        }
+
+        /**
+         * Sets the timestamp in milliseconds of the usage report (the time at which the document
+         * was used).
+         *
+         * <p>The value is in the {@link System#currentTimeMillis} time base.
+         *
+         * <p>If unset, this defaults to the current timestamp at the time that the
+         * {@link ReportUsageRequest} is constructed.
+         *
+         * @throws IllegalStateException if the builder has already been used
+         */
+        @NonNull
+        public ReportUsageRequest.Builder setUsageTimestampMillis(
+                /*@exportToFramework:CurrentTimeMillisLong*/ long usageTimestampMillis) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mUsageTimestampMillis = usageTimestampMillis;
+            return this;
+        }
+
+        /**
+         * Builds a new {@link ReportUsageRequest}.
+         *
+         * @throws IllegalStateException if the builder has already been used
+         */
+        @NonNull
+        public ReportUsageRequest build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            if (mUsageTimestampMillis == null) {
+                mUsageTimestampMillis = System.currentTimeMillis();
+            }
+            mBuilt = true;
+            return new ReportUsageRequest(mNamespace, mDocumentId, mUsageTimestampMillis);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
index dcddb5e..fcbad30 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -21,6 +21,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.exceptions.AppSearchException;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
@@ -32,7 +33,7 @@
  *
  * <p>This allows clients to obtain:
  * <ul>
- *   <li>The document which matched, using {@link #getDocument}
+ *   <li>The document which matched, using {@link #getGenericDocument}
  *   <li>Information about which properties in the document matched, and "snippet" information
  *       containing textual summaries of the document's matches, using {@link #getMatches}
  *  </ul>
@@ -43,17 +44,11 @@
  * @see SearchResults
  */
 public final class SearchResult {
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public static final String DOCUMENT_FIELD = "document";
-
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public static final String MATCHES_FIELD = "matches";
-
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public static final String PACKAGE_NAME_FIELD = "packageName";
+    static final String DOCUMENT_FIELD = "document";
+    static final String MATCHES_FIELD = "matches";
+    static final String PACKAGE_NAME_FIELD = "packageName";
+    static final String DATABASE_NAME_FIELD = "databaseName";
+    static final String RANKING_SIGNAL_FIELD = "rankingSignal";
 
     @NonNull
     private final Bundle mBundle;
@@ -79,13 +74,30 @@
         return mBundle;
     }
 
+// @exportToFramework:startStrip()
+    /**
+     * Contains the matching document, converted to the given document class.
+     *
+     * <p>This is equivalent to calling {@code getGenericDocument().toDocumentClass(T.class)}.
+     *
+     * @return Document object which matched the query.
+     * @throws AppSearchException if no factory for this document class could be found on the
+     *       classpath.
+     */
+    @NonNull
+    public <T> T getDocument(@NonNull Class<T> documentClass) throws AppSearchException {
+        Preconditions.checkNotNull(documentClass);
+        return getGenericDocument().toDocumentClass(documentClass);
+    }
+// @exportToFramework:endStrip()
+
     /**
      * Contains the matching {@link GenericDocument}.
      *
      * @return Document object which matched the query.
      */
     @NonNull
-    public GenericDocument getDocument() {
+    public GenericDocument getGenericDocument() {
         if (mDocument == null) {
             mDocument = new GenericDocument(
                     Preconditions.checkNotNull(mBundle.getBundle(DOCUMENT_FIELD)));
@@ -108,7 +120,7 @@
                     Preconditions.checkNotNull(mBundle.getParcelableArrayList(MATCHES_FIELD));
             mMatches = new ArrayList<>(matchBundles.size());
             for (int i = 0; i < matchBundles.size(); i++) {
-                MatchInfo matchInfo = new MatchInfo(getDocument(), matchBundles.get(i));
+                MatchInfo matchInfo = new MatchInfo(matchBundles.get(i), getGenericDocument());
                 mMatches.add(matchInfo);
             }
         }
@@ -126,6 +138,128 @@
     }
 
     /**
+     * Contains the database name that stored the {@link GenericDocument}.
+     *
+     * @return Name of the database within which the document is stored
+     */
+    @NonNull
+    public String getDatabaseName() {
+        return Preconditions.checkNotNull(mBundle.getString(DATABASE_NAME_FIELD));
+    }
+
+    /**
+     * Returns the ranking signal of the {@link GenericDocument}, according to the
+     * ranking strategy set in {@link SearchSpec.Builder#setRankingStrategy(int)}.
+     *
+     * The meaning of the ranking signal and its value is determined by the selected ranking
+     * strategy:
+     * <ul>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_NONE} - this value will be 0</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_DOCUMENT_SCORE} - the value returned by calling
+     * {@link GenericDocument#getScore()} on the document returned by
+     * {@link #getGenericDocument()}</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_CREATION_TIMESTAMP} - the value returned by calling
+     * {@link GenericDocument#getCreationTimestampMillis()} on the document returned by
+     * {@link #getGenericDocument()}</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_RELEVANCE_SCORE} - an arbitrary double value where
+     * a higher value means more relevant</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_COUNT} - the number of times usage has been
+     * reported for the document returned by {@link #getGenericDocument()}</li>
+     * <li>{@link SearchSpec#RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP} - the timestamp of the
+     * most recent usage that has been reported for the document returned by
+     * {@link #getGenericDocument()}</li>
+     * </ul>
+     *
+     * @return Ranking signal of the document
+     */
+    public double getRankingSignal() {
+        return mBundle.getDouble(RANKING_SIGNAL_FIELD);
+    }
+
+    /** Builder for {@link SearchResult} objects. */
+    public static final class Builder {
+        private final Bundle mBundle = new Bundle();
+        private final ArrayList<Bundle> mMatchInfos = new ArrayList<>();
+
+        private boolean mBuilt;
+
+        /**
+         * Constructs a new builder for {@link SearchResult} objects.
+         *
+         * @param packageName the package name the matched document belongs to
+         * @param databaseName the database name the matched document belongs to.
+         */
+        public Builder(@NonNull String packageName, @NonNull String databaseName) {
+            mBundle.putString(PACKAGE_NAME_FIELD, Preconditions.checkNotNull(packageName));
+            mBundle.putString(DATABASE_NAME_FIELD, Preconditions.checkNotNull(databaseName));
+        }
+
+// @exportToFramework:startStrip()
+        /**
+         * Sets the document which matched.
+         *
+         * @param document An instance of a class annotated with
+         * {@link androidx.appsearch.annotation.Document}.
+         *
+         * @throws AppSearchException if an error occurs converting a document class into a
+         *                            {@link GenericDocument}.
+         * @throws IllegalStateException if the builder has already been used
+         */
+        @NonNull
+        public Builder setDocument(@NonNull Object document) throws AppSearchException {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(document);
+            return setGenericDocument(GenericDocument.fromDocumentClass(document));
+        }
+// @exportToFramework:endStrip()
+
+        /**
+         * Sets the document which matched.
+         *
+         * @throws IllegalStateException if the builder has already been used
+         */
+        @NonNull
+        public Builder setGenericDocument(@NonNull GenericDocument document) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBundle.putBundle(DOCUMENT_FIELD, document.getBundle());
+            return this;
+        }
+
+        /** Adds another match to this SearchResult. */
+        @NonNull
+        public Builder addMatch(@NonNull MatchInfo matchInfo) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkState(
+                    matchInfo.mDocument == null,
+                    "This MatchInfo is already associated with a SearchResult and can't be "
+                            + "reassigned");
+            mMatchInfos.add(matchInfo.mBundle);
+            return this;
+        }
+
+        /** Sets the ranking signal of the matched document in this SearchResult. */
+        @NonNull
+        public Builder setRankingSignal(double rankingSignal) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBundle.putDouble(RANKING_SIGNAL_FIELD, rankingSignal);
+            return this;
+        }
+
+        /**
+         * Constructs a new {@link SearchResult}.
+         *
+         * @throws IllegalStateException if the builder has already been used
+         */
+        @NonNull
+        public SearchResult build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBundle.putParcelableArrayList(MATCHES_FIELD, mMatchInfos);
+            mBuilt = true;
+            return new SearchResult(mBundle);
+        }
+    }
+
+    /**
      * This class represents a match objects for any Snippets that might be present in
      * {@link SearchResults} from query. Using this class
      * user can get the full text, exact matches and Snippets of document content for a given match.
@@ -139,9 +273,9 @@
      * <p>{@link MatchInfo#getPropertyPath()} returns "subject"
      * <p>{@link MatchInfo#getFullText()} returns "A commonly used fake word is foo. Another
      * nonsense word that’s used a lot is bar."
-     * <p>{@link MatchInfo#getExactMatchPosition()} returns [29, 32]
+     * <p>{@link MatchInfo#getExactMatchRange()} returns [29, 32]
      * <p>{@link MatchInfo#getExactMatch()} returns "foo"
-     * <p>{@link MatchInfo#getSnippetPosition()} returns [26, 33]
+     * <p>{@link MatchInfo#getSnippetRange()} returns [26, 33]
      * <p>{@link MatchInfo#getSnippet()} returns "is foo."
      * <p>
      * <p>Class Example 2:
@@ -155,70 +289,62 @@
      * <p> Match-1
      * <p>{@link MatchInfo#getPropertyPath()} returns "sender.name"
      * <p>{@link MatchInfo#getFullText()} returns "Test Name Jr."
-     * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 4]
+     * <p>{@link MatchInfo#getExactMatchRange()} returns [0, 4]
      * <p>{@link MatchInfo#getExactMatch()} returns "Test"
-     * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 9]
+     * <p>{@link MatchInfo#getSnippetRange()} returns [0, 9]
      * <p>{@link MatchInfo#getSnippet()} returns "Test Name"
      * <p> Match-2
      * <p>{@link MatchInfo#getPropertyPath()} returns "sender.email"
      * <p>{@link MatchInfo#getFullText()} returns "[email protected]"
-     * <p>{@link MatchInfo#getExactMatchPosition()} returns [0, 20]
+     * <p>{@link MatchInfo#getExactMatchRange()} returns [0, 20]
      * <p>{@link MatchInfo#getExactMatch()} returns "[email protected]"
-     * <p>{@link MatchInfo#getSnippetPosition()} returns [0, 20]
+     * <p>{@link MatchInfo#getSnippetRange()} returns [0, 20]
      * <p>{@link MatchInfo#getSnippet()} returns "[email protected]"
      */
     public static final class MatchInfo {
-        /**
-         * The path of the matching snippet property.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String PROPERTY_PATH_FIELD = "propertyPath";
+        /** The path of the matching snippet property. */
+        private static final String PROPERTY_PATH_FIELD = "propertyPath";
+        private static final String EXACT_MATCH_RANGE_LOWER_FIELD = "exactMatchRangeLower";
+        private static final String EXACT_MATCH_RANGE_UPPER_FIELD = "exactMatchRangeUpper";
+        private static final String SNIPPET_RANGE_LOWER_FIELD = "snippetRangeLower";
+        private static final String SNIPPET_RANGE_UPPER_FIELD = "snippetRangeUpper";
 
-        /**
-         * The index of matching value in its property. A property may have multiple values. This
-         * index indicates which value is the match.
-         *
-         * @hide
-         */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String VALUES_INDEX_FIELD = "valuesIndex";
-
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String EXACT_MATCH_POSITION_LOWER_FIELD = "exactMatchPositionLower";
-
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String EXACT_MATCH_POSITION_UPPER_FIELD = "exactMatchPositionUpper";
-
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String WINDOW_POSITION_LOWER_FIELD = "windowPositionLower";
-
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public static final String WINDOW_POSITION_UPPER_FIELD = "windowPositionUpper";
-
-        private final String mFullText;
         private final String mPropertyPath;
-        private final Bundle mBundle;
+        final Bundle mBundle;
+
+        /**
+         * Document which the match comes from.
+         *
+         * <p>If this is {@code null}, methods which require access to the document, like
+         * {@link #getExactMatch}, will throw {@link NullPointerException}.
+         */
+        @Nullable
+        final GenericDocument mDocument;
+
+        /** Full text of the matched property. Populated on first use. */
+        @Nullable
+        private String mFullText;
+
+        /** Range of property that exactly matched the query. Populated on first use. */
+        @Nullable
         private MatchRange mExactMatchRange;
+
+        /** Range of some reasonable amount of context around the query. Populated on first use. */
+        @Nullable
         private MatchRange mWindowRange;
 
-        MatchInfo(@NonNull GenericDocument document, @NonNull Bundle bundle) {
+        MatchInfo(@NonNull Bundle bundle, @Nullable GenericDocument document) {
             mBundle = Preconditions.checkNotNull(bundle);
-            Preconditions.checkNotNull(document);
+            mDocument = document;
             mPropertyPath = Preconditions.checkNotNull(bundle.getString(PROPERTY_PATH_FIELD));
-            mFullText = getPropertyValues(
-                    document, mPropertyPath, mBundle.getInt(VALUES_INDEX_FIELD));
         }
 
         /**
          * Gets the property path corresponding to the given entry.
-         * <p>Property Path: '.' - delimited sequence of property names indicating which property in
-         * the Document these snippets correspond to.
+         *
+         * <p>A property path is a '.' - delimited sequence of property names indicating which
+         * property in the document these snippets correspond to.
+         *
          * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
          * For class example 1 this returns "subject"
          */
@@ -234,6 +360,12 @@
          */
         @NonNull
         public String getFullText() {
+            if (mFullText == null) {
+                Preconditions.checkState(
+                        mDocument != null,
+                        "Document has not been populated; this MatchInfo cannot be used yet");
+                mFullText = getPropertyValues(mDocument, mPropertyPath);
+            }
             return mFullText;
         }
 
@@ -242,11 +374,11 @@
          * <p>For class example 1 this returns [29, 32]
          */
         @NonNull
-        public MatchRange getExactMatchPosition() {
+        public MatchRange getExactMatchRange() {
             if (mExactMatchRange == null) {
                 mExactMatchRange = new MatchRange(
-                        mBundle.getInt(EXACT_MATCH_POSITION_LOWER_FIELD),
-                        mBundle.getInt(EXACT_MATCH_POSITION_UPPER_FIELD));
+                        mBundle.getInt(EXACT_MATCH_RANGE_LOWER_FIELD),
+                        mBundle.getInt(EXACT_MATCH_RANGE_UPPER_FIELD));
             }
             return mExactMatchRange;
         }
@@ -257,7 +389,7 @@
          */
         @NonNull
         public CharSequence getExactMatch() {
-            return getSubstring(getExactMatchPosition());
+            return getSubstring(getExactMatchRange());
         }
 
         /**
@@ -267,11 +399,11 @@
          * <p>For class example 1 this returns [29, 41].
          */
         @NonNull
-        public MatchRange getSnippetPosition() {
+        public MatchRange getSnippetRange() {
             if (mWindowRange == null) {
                 mWindowRange = new MatchRange(
-                        mBundle.getInt(WINDOW_POSITION_LOWER_FIELD),
-                        mBundle.getInt(WINDOW_POSITION_UPPER_FIELD));
+                        mBundle.getInt(SNIPPET_RANGE_LOWER_FIELD),
+                        mBundle.getInt(SNIPPET_RANGE_UPPER_FIELD));
             }
             return mWindowRange;
         }
@@ -286,7 +418,7 @@
          */
         @NonNull
         public CharSequence getSnippet() {
-            return getSubstring(getSnippetPosition());
+            return getSubstring(getSnippetRange());
         }
 
         private CharSequence getSubstring(MatchRange range) {
@@ -294,18 +426,81 @@
         }
 
         /** Extracts the matching string from the document. */
-        private static String getPropertyValues(
-                GenericDocument document, String propertyName, int valueIndex) {
+        private static String getPropertyValues(GenericDocument document, String propertyName) {
             // In IcingLib snippeting is available for only 3 data types i.e String, double and
             // long, so we need to check which of these three are requested.
-            // TODO (tytytyww): getPropertyStringArray takes property name, handle for property
-            //  path.
             // TODO (tytytyww): support double[] and long[].
-            String[] values = document.getPropertyStringArray(propertyName);
-            if (values == null) {
-                throw new IllegalStateException("No content found for requested property path!");
+            String result = document.getPropertyString(propertyName);
+            if (result == null) {
+                throw new IllegalStateException(
+                        "No content found for requested property path: " + propertyName);
             }
-            return values[valueIndex];
+            return result;
+        }
+
+        /** Builder for {@link MatchInfo} objects. */
+        public static final class Builder {
+            private final Bundle mBundle = new Bundle();
+            private boolean mBuilt = false;
+
+            /**
+             * Creates a new {@link MatchInfo.Builder} reporting a match with the given property
+             * path.
+             *
+             * <p>A property path is a dot-delimited sequence of property names indicating which
+             * property in the document these snippets correspond to.
+             *
+             * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc.
+             * For class example 1 this returns "subject".
+             *
+             * @param propertyPath A {@code dot-delimited sequence of property names indicating
+             *                     which property in the document these snippets correspond to.
+             */
+            public Builder(@NonNull String propertyPath) {
+                mBundle.putString(
+                        SearchResult.MatchInfo.PROPERTY_PATH_FIELD,
+                        Preconditions.checkNotNull(propertyPath));
+            }
+
+            /**
+             * Sets the exact {@link MatchRange} corresponding to the given entry.
+             *
+             * @throws IllegalStateException if the builder has already been used
+             */
+            @NonNull
+            public Builder setExactMatchRange(@NonNull MatchRange matchRange) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                Preconditions.checkNotNull(matchRange);
+                mBundle.putInt(MatchInfo.EXACT_MATCH_RANGE_LOWER_FIELD, matchRange.getStart());
+                mBundle.putInt(MatchInfo.EXACT_MATCH_RANGE_UPPER_FIELD, matchRange.getEnd());
+                return this;
+            }
+
+            /**
+             * Sets the snippet {@link MatchRange} corresponding to the given entry.
+             *
+             * @throws IllegalStateException if the builder has already been used
+             */
+            @NonNull
+            public Builder setSnippetRange(@NonNull MatchRange matchRange) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                Preconditions.checkNotNull(matchRange);
+                mBundle.putInt(MatchInfo.SNIPPET_RANGE_LOWER_FIELD, matchRange.getStart());
+                mBundle.putInt(MatchInfo.SNIPPET_RANGE_UPPER_FIELD, matchRange.getEnd());
+                return this;
+            }
+
+            /**
+             * Constructs a new {@link MatchInfo}.
+             *
+             * @throws IllegalStateException if the builder has already been used
+             */
+            @NonNull
+            public MatchInfo build() {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                mBuilt = true;
+                return new MatchInfo(mBundle, /*document=*/ null);
+            }
         }
     }
 
@@ -328,9 +523,7 @@
          *
          * @param start The start point (inclusive)
          * @param end   The end point (exclusive)
-         * @hide
          */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public MatchRange(int start, int end) {
             if (start > end) {
                 throw new IllegalArgumentException("Start point must be less than or equal to "
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
index cdd3f3e..6bf301f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResults.java
@@ -24,25 +24,30 @@
 import java.util.List;
 
 /**
- * SearchResults are a returned object from a query API.
+ * Encapsulates results of a search operation.
  *
- * <p>Each {@link SearchResult} contains a document and may contain other fields like snippets
- * based on request.
+ * <p>Each {@link AppSearchSession#search} operation returns a list of {@link SearchResult}
+ * objects, referred to as a "page", limited by the size configured by
+ * {@link SearchSpec.Builder#setResultCountPerPage}.
  *
- * <p>Should close this object after finish fetching results.
+ * <p>To fetch a page of results, call {@link #getNextPage()}.
+ *
+ * <p>All instances of {@link SearchResults} must call {@link SearchResults#close()} after the
+ * results are fetched.
  *
  * <p>This class is not thread safe.
  */
 public interface SearchResults extends Closeable {
     /**
-     * Gets a whole page of {@link SearchResult}s.
+     * Retrieves the next page of {@link SearchResult} objects.
      *
-     * <p>Re-call this method to get next page of {@link SearchResult}, until it returns an
-     * empty list.
+     * <p>The page size is configured by {@link SearchSpec.Builder#setResultCountPerPage}.
      *
-     * <p>The page size is set by {@link SearchSpec.Builder#setResultCountPerPage}.
+     * <p>Continue calling this method to access results until it returns an empty list,
+     * signifying there are no more results.
      *
-     * @return The pending result of performing this operation.
+     * @return a {@link ListenableFuture} which resolves to a list of {@link SearchResult}
+     * objects.
      */
     @NonNull
     ListenableFuture<List<SearchResult>> getNextPage();
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index 5c939dd..67f495c 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -23,8 +23,8 @@
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.Document;
 import androidx.appsearch.exceptions.AppSearchException;
-import androidx.appsearch.exceptions.IllegalSearchSpecException;
 import androidx.collection.ArrayMap;
 import androidx.core.util.Preconditions;
 
@@ -52,8 +52,9 @@
     public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
 
     static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
-    static final String SCHEMA_TYPE_FIELD = "schemaType";
+    static final String SCHEMA_FIELD = "schema";
     static final String NAMESPACE_FIELD = "namespace";
+    static final String PACKAGE_NAME_FIELD = "packageName";
     static final String NUM_PER_PAGE_FIELD = "numPerPage";
     static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
     static final String ORDER_FIELD = "order";
@@ -61,6 +62,8 @@
     static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty";
     static final String MAX_SNIPPET_FIELD = "maxSnippet";
     static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";
+    static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";
+    static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";
 
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -75,6 +78,7 @@
 
     /**
      * Term Match Type for the query.
+     *
      * @hide
      */
     // NOTE: The integer values of these constants must match the proto enum constants in
@@ -84,7 +88,8 @@
             TERM_MATCH_PREFIX
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface TermMatch {}
+    public @interface TermMatch {
+    }
 
     /**
      * Query terms will only match exact tokens in the index.
@@ -99,6 +104,7 @@
 
     /**
      * Ranking Strategy for query result.
+     *
      * @hide
      */
     // NOTE: The integer values of these constants must match the proto enum constants in
@@ -107,12 +113,17 @@
             RANKING_STRATEGY_NONE,
             RANKING_STRATEGY_DOCUMENT_SCORE,
             RANKING_STRATEGY_CREATION_TIMESTAMP,
-            RANKING_STRATEGY_RELEVANCE_SCORE
+            RANKING_STRATEGY_RELEVANCE_SCORE,
+            RANKING_STRATEGY_USAGE_COUNT,
+            RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP,
+            RANKING_STRATEGY_SYSTEM_USAGE_COUNT,
+            RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP,
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface RankingStrategy {}
+    public @interface RankingStrategy {
+    }
 
-    /** No Ranking, results are returned in arbitrary order.*/
+    /** No Ranking, results are returned in arbitrary order. */
     public static final int RANKING_STRATEGY_NONE = 0;
     /** Ranked by app-provided document scores. */
     public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1;
@@ -120,9 +131,18 @@
     public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2;
     /** Ranked by document relevance score. */
     public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3;
+    /** Ranked by number of usages, as reported by the app. */
+    public static final int RANKING_STRATEGY_USAGE_COUNT = 4;
+    /** Ranked by timestamp of last usage, as reported by the app. */
+    public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5;
+    /** Ranked by number of usages from a system UI surface. */
+    public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6;
+    /** Ranked by timestamp of last usage from a system UI surface. */
+    public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7;
 
     /**
      * Order for query result.
+     *
      * @hide
      */
     // NOTE: The integer values of these constants must match the proto enum constants in
@@ -132,13 +152,39 @@
             ORDER_ASCENDING
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface Order {}
+    public @interface Order {
+    }
 
     /** Search results will be returned in a descending order. */
     public static final int ORDER_DESCENDING = 0;
     /** Search results will be returned in an ascending order. */
     public static final int ORDER_ASCENDING = 1;
 
+    /**
+     * Grouping type for result limits.
+     *
+     * @hide
+     */
+    @IntDef(flag = true, value = {
+            GROUPING_TYPE_PER_PACKAGE,
+            GROUPING_TYPE_PER_NAMESPACE
+    })
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface GroupingType {
+    }
+
+    /**
+     * Results should be grouped together by package for the purpose of enforcing a limit on the
+     * number of results returned per package.
+     */
+    public static final int GROUPING_TYPE_PER_PACKAGE = 0b01;
+    /**
+     * Results should be grouped together by namespace for the purpose of enforcing a limit on the
+     * number of results returned per namespace.
+     */
+    public static final int GROUPING_TYPE_PER_NAMESPACE = 0b10;
+
     private final Bundle mBundle;
 
     /** @hide */
@@ -150,6 +196,7 @@
 
     /**
      * Returns the {@link Bundle} populated by this builder.
+     *
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -169,21 +216,21 @@
      * <p>If empty, the query will search over all schema types.
      */
     @NonNull
-    public List<String> getSchemaTypes() {
-        List<String> schemaTypes = mBundle.getStringArrayList(SCHEMA_TYPE_FIELD);
-        if (schemaTypes == null) {
+    public List<String> getFilterSchemas() {
+        List<String> schemas = mBundle.getStringArrayList(SCHEMA_FIELD);
+        if (schemas == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(schemaTypes);
+        return Collections.unmodifiableList(schemas);
     }
 
     /**
-     * Returns the list of namespaces to search for.
+     * Returns the list of namespaces to search over.
      *
      * <p>If empty, the query will search over all namespaces.
      */
     @NonNull
-    public List<String> getNamespaces() {
+    public List<String> getFilterNamespaces() {
         List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
         if (namespaces == null) {
             return Collections.emptyList();
@@ -191,6 +238,22 @@
         return Collections.unmodifiableList(namespaces);
     }
 
+    /**
+     * Returns the list of package name filters to search over.
+     *
+     * <p>If empty, the query will search over all packages that the caller has access to. If
+     * package names are specified which caller doesn't have access to, then those package names
+     * will be ignored.
+     */
+    @NonNull
+    public List<String> getFilterPackageNames() {
+        List<String> packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD);
+        if (packageNames == null) {
+            return Collections.emptyList();
+        }
+        return Collections.unmodifiableList(packageNames);
+    }
+
     /** Returns the number of results per page in the result set. */
     public int getResultCountPerPage() {
         return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
@@ -233,23 +296,40 @@
      */
     @NonNull
     public Map<String, List<String>> getProjections() {
-        Bundle typePropertyPathsBundle =
-                mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
-        Set<String> schemaTypes = typePropertyPathsBundle.keySet();
-        Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemaTypes.size());
-        for (String schemaType : schemaTypes) {
-            typePropertyPathsMap.put(schemaType,
-                    typePropertyPathsBundle.getStringArrayList(schemaType));
+        Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
+        Set<String> schemas = typePropertyPathsBundle.keySet();
+        Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+        for (String schema : schemas) {
+            typePropertyPathsMap.put(schema, typePropertyPathsBundle.getStringArrayList(schema));
         }
         return typePropertyPathsMap;
     }
 
+    /**
+     * Get the type of grouping limit to apply, or 0 if {@link Builder#setResultGrouping} was not
+     * called.
+     */
+    public @GroupingType int getResultGroupingTypeFlags() {
+        return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS);
+    }
+
+    /**
+     * Get the maximum number of results to return for each group.
+     *
+     * @return the maximum number of results to return for each group or Integer.MAX_VALUE if
+     * {@link Builder#setResultGrouping(int, int)} was not called.
+     */
+    public int getResultGroupingLimit() {
+        return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE);
+    }
+
     /** Builder for {@link SearchSpec objects}. */
     public static final class Builder {
 
         private final Bundle mBundle;
-        private final ArrayList<String> mSchemaTypes = new ArrayList<>();
+        private final ArrayList<String> mSchemas = new ArrayList<>();
         private final ArrayList<String> mNamespaces = new ArrayList<>();
+        private final ArrayList<String> mPackageNames = new ArrayList<>();
         private final Bundle mProjectionTypePropertyMasks = new Bundle();
         private boolean mBuilt = false;
 
@@ -257,10 +337,14 @@
         public Builder() {
             mBundle = new Bundle();
             mBundle.putInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE);
+            mBundle.putInt(TERM_MATCH_TYPE_FIELD, TERM_MATCH_PREFIX);
         }
 
         /**
          * Indicates how the query terms should match {@code TermMatchCode} in the index.
+         *
+         * <p>If this method is not called, the default term match type is
+         * {@link SearchSpec#TERM_MATCH_PREFIX}.
          */
         @NonNull
         public Builder setTermMatch(@TermMatch int termMatchTypeCode) {
@@ -278,10 +362,10 @@
          * <p>If unset, the query will search over all schema types.
          */
         @NonNull
-        public Builder addSchemaType(@NonNull String... schemaTypes) {
-            Preconditions.checkNotNull(schemaTypes);
+        public Builder addFilterSchemas(@NonNull String... schemas) {
+            Preconditions.checkNotNull(schemas);
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            return addSchemaType(Arrays.asList(schemaTypes));
+            return addFilterSchemas(Arrays.asList(schemas));
         }
 
         /**
@@ -291,56 +375,58 @@
          * <p>If unset, the query will search over all schema types.
          */
         @NonNull
-        public Builder addSchemaType(@NonNull Collection<String> schemaTypes) {
-            Preconditions.checkNotNull(schemaTypes);
+        public Builder addFilterSchemas(@NonNull Collection<String> schemas) {
+            Preconditions.checkNotNull(schemas);
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            mSchemaTypes.addAll(schemaTypes);
+            mSchemas.addAll(schemas);
             return this;
         }
 
 // @exportToFramework:startStrip()
+
         /**
-         * Adds the Schema type of given data classes to the Schema type filter of
+         * Adds the Schema names of given document classes to the Schema type filter of
          * {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
          *
          * <p>If unset, the query will search over all schema types.
          *
-         * @param dataClasses classes annotated with
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
+         * @param documentClasses classes annotated with {@link Document}.
          */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getSchemaTypes
+        // Merged list available from getFilterSchemas
+        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder addSchemaByDataClass(@NonNull Collection<? extends Class<?>> dataClasses)
-                throws AppSearchException {
-            Preconditions.checkNotNull(dataClasses);
+        public Builder addFilterDocumentClasses(
+                @NonNull Collection<? extends Class<?>> documentClasses) throws AppSearchException {
+            Preconditions.checkNotNull(documentClasses);
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            List<String> schemaTypes = new ArrayList<>(dataClasses.size());
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            for (Class<?> dataClass : dataClasses) {
-                DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
-                schemaTypes.add(factory.getSchemaType());
+            List<String> schemas = new ArrayList<>(documentClasses.size());
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            for (Class<?> documentClass : documentClasses) {
+                DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+                schemas.add(factory.getSchemaName());
             }
-            addSchemaType(schemaTypes);
+            addFilterSchemas(schemas);
             return this;
         }
 // @exportToFramework:endStrip()
 
 // @exportToFramework:startStrip()
+
         /**
-         * Adds the Schema type of given data classes to the Schema type filter of
+         * Adds the Schema names of given document classes to the Schema type filter of
          * {@link SearchSpec} Entry. Only search for documents that have the specified schema types.
          *
          * <p>If unset, the query will search over all schema types.
          *
-         * @param dataClasses classes annotated with
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
+         * @param documentClasses classes annotated with {@link Document}.
          */
-        @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getSchemas()
+        // Merged list available from getFilterSchemas()
+        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder addSchemaByDataClass(@NonNull Class<?>... dataClasses)
+        public Builder addFilterDocumentClasses(@NonNull Class<?>... documentClasses)
                 throws AppSearchException {
-            Preconditions.checkNotNull(dataClasses);
-            return addSchemaByDataClass(Arrays.asList(dataClasses));
+            Preconditions.checkNotNull(documentClasses);
+            return addFilterDocumentClasses(Arrays.asList(documentClasses));
         }
 // @exportToFramework:endStrip()
 
@@ -350,10 +436,10 @@
          * <p>If unset, the query will search over all namespaces.
          */
         @NonNull
-        public Builder addNamespace(@NonNull String... namespaces) {
+        public Builder addFilterNamespaces(@NonNull String... namespaces) {
             Preconditions.checkNotNull(namespaces);
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            return addNamespace(Arrays.asList(namespaces));
+            return addFilterNamespaces(Arrays.asList(namespaces));
         }
 
         /**
@@ -362,7 +448,7 @@
          * <p>If unset, the query will search over all namespaces.
          */
         @NonNull
-        public Builder addNamespace(@NonNull Collection<String> namespaces) {
+        public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) {
             Preconditions.checkNotNull(namespaces);
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             mNamespaces.addAll(namespaces);
@@ -370,6 +456,37 @@
         }
 
         /**
+         * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
+         * were indexed from the specified packages.
+         *
+         * <p>If unset, the query will search over all packages that the caller has access to.
+         * If package names are specified which caller doesn't have access to, then those package
+         * names will be ignored.
+         */
+        @NonNull
+        public Builder addFilterPackageNames(@NonNull String... packageNames) {
+            Preconditions.checkNotNull(packageNames);
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            return addFilterPackageNames(Arrays.asList(packageNames));
+        }
+
+        /**
+         * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that
+         * were indexed from the specified packages.
+         *
+         * <p>If unset, the query will search over all packages that the caller has access to.
+         * If package names are specified which caller doesn't have access to, then those package
+         * names will be ignored.
+         */
+        @NonNull
+        public Builder addFilterPackageNames(@NonNull Collection<String> packageNames) {
+            Preconditions.checkNotNull(packageNames);
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mPackageNames.addAll(packageNames);
+            return this;
+        }
+
+        /**
          * Sets the number of results per page in the returned object.
          *
          * <p>The default number of results per page is 10.
@@ -383,12 +500,12 @@
             return this;
         }
 
-        /** Sets ranking strategy for AppSearch results.*/
+        /** Sets ranking strategy for AppSearch results. */
         @NonNull
         public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkArgumentInRange(rankingStrategy, RANKING_STRATEGY_NONE,
-                    RANKING_STRATEGY_RELEVANCE_SCORE, "Result ranking strategy");
+                    RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP, "Result ranking strategy");
             mBundle.putInt(RANKING_STRATEGY_FIELD, rankingStrategy);
             return this;
         }
@@ -464,7 +581,7 @@
             return this;
         }
 
-       /**
+        /**
          * Adds property paths for the specified type to be used for projection. If property
          * paths are added for a type, then only the properties referred to will be retrieved for
          * results of that type. If a property path that is specified isn't present in a result,
@@ -503,7 +620,7 @@
          * <p>Then, suppose that a query for "important" is issued with the following projection
          * type property paths:
          * <pre>{@code
-         * {schemaType: "Email", ["subject", "sender.name", "recipients.name"]}
+         * {schema: "Email", ["subject", "sender.name", "recipients.name"]}
          * }</pre>
          *
          * <p>The above document will be returned as:
@@ -526,39 +643,44 @@
          */
         @NonNull
         public SearchSpec.Builder addProjection(
-                @NonNull String schemaType, @NonNull String... propertyPaths) {
-            Preconditions.checkNotNull(propertyPaths);
-            return addProjection(schemaType, Arrays.asList(propertyPaths));
-        }
-
-        /**
-         * Adds property paths for the specified type to be used for projection. If property
-         * paths are added for a type, then only the properties referred to will be retrieved for
-         * results of that type. If a property path that is specified isn't present in a result,
-         * it will be ignored for that result. Property paths cannot be null.
-         *
-         * <p>If no property paths are added for a particular type, then all properties of
-         * results of that type will be retrieved.
-         *
-         * <p>If property path is added for the
-         * {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths will
-         * apply to all results, excepting any types that have their own, specific property paths
-         * set.
-         *
-         * {@see SearchSpec.Builder#addProjection(String, String...)}
-         */
-        @NonNull
-        public SearchSpec.Builder addProjection(
-                @NonNull String schemaType, @NonNull Collection<String> propertyPaths) {
+                @NonNull String schema, @NonNull Collection<String> propertyPaths) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(schema);
             Preconditions.checkNotNull(propertyPaths);
             ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
             for (String propertyPath : propertyPaths) {
                 Preconditions.checkNotNull(propertyPath);
                 propertyPathsArrayList.add(propertyPath);
             }
-            mProjectionTypePropertyMasks.putStringArrayList(schemaType, propertyPathsArrayList);
+            mProjectionTypePropertyMasks.putStringArrayList(schema, propertyPathsArrayList);
+            return this;
+        }
+
+        /**
+         * Set the maximum number of results to return for each group, where groups are defined
+         * by grouping type.
+         *
+         * <p>Calling this method will override any previous calls. So calling
+         * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 7) and then calling
+         * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 2) will result in only the latter, a
+         * limit of two results per package, being applied. Or calling setResultGrouping
+         * (GROUPING_TYPE_PER_PACKAGE, 1) and then calling setResultGrouping
+         * (GROUPING_TYPE_PER_PACKAGE | GROUPING_PER_NAMESPACE, 5) will result in five results
+         * per package per namespace.
+         *
+         * @param groupingTypeFlags One or more combination of grouping types.
+         * @param limit             Number of results to return per {@code groupingTypeFlags}.
+         * @throws IllegalArgumentException if groupingTypeFlags is zero.
+         */
+        // Individual parameters available from getResultGroupingTypeFlags and
+        // getResultGroupingLimit
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setResultGrouping(@GroupingType int groupingTypeFlags, int limit) {
+            Preconditions.checkState(groupingTypeFlags != 0,
+                    "Result grouping type cannot be zero.");
+            mBundle.putInt(RESULT_GROUPING_TYPE_FLAGS, groupingTypeFlags);
+            mBundle.putInt(RESULT_GROUPING_LIMIT, limit);
             return this;
         }
 
@@ -570,11 +692,9 @@
         @NonNull
         public SearchSpec build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            if (!mBundle.containsKey(TERM_MATCH_TYPE_FIELD)) {
-                throw new IllegalSearchSpecException("Missing termMatchType field.");
-            }
             mBundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
-            mBundle.putStringArrayList(SCHEMA_TYPE_FIELD, mSchemaTypes);
+            mBundle.putStringArrayList(SCHEMA_FIELD, mSchemas);
+            mBundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames);
             mBundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks);
             mBuilt = true;
             return new SearchSpec(mBundle);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index 07df364..64c3ad3 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -18,6 +18,7 @@
 
 import android.annotation.SuppressLint;
 
+import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -36,44 +37,90 @@
 /**
  * Encapsulates a request to update the schema of an {@link AppSearchSession} database.
  *
+ * <p>The schema is composed of a collection of {@link AppSearchSchema} objects, each of which
+ * defines a unique type of data.
+ *
+ * <p>The first call to SetSchemaRequest will set the provided schema and store it within the
+ * {@link AppSearchSession} database.
+ *
+ * <p>Subsequent calls will compare the provided schema to the previously saved schema, to
+ * determine how to treat existing documents.
+ *
+ * <p>The following types of schema modifications are always safe and are made without deleting any
+ * existing documents:
+ * <ul>
+ *     <li>Addition of new {@link AppSearchSchema} types
+ *     <li>Addition of new properties to an existing {@link AppSearchSchema} type
+ *     <li>Changing the cardinality of a property to be less restrictive
+ * </ul>
+ *
+ * <p>The following types of schema changes are not backwards compatible:
+ * <ul>
+ *     <li>Removal of an existing {@link AppSearchSchema} type
+ *     <li>Removal of a property from an existing {@link AppSearchSchema} type
+ *     <li>Changing the data type of an existing property
+ *     <li>Changing the cardinality of a property to be more restrictive
+ * </ul>
+ *
+ * <p>Providing a schema with incompatible changes, will throw an
+ * {@link androidx.appsearch.exceptions.AppSearchException}, with a message describing the
+ * incompatibility. As a result, the previously set schema will remain unchanged.
+ *
+ * <p>Backward incompatible changes can be made by :
+ * <ul>
+ *     <li>setting {@link SetSchemaRequest.Builder#setForceOverride} method to {@code true}.
+ *         This deletes all  documents that are incompatible with the new schema. The new schema is
+ *         then saved and persisted to disk.
+ *     <li>Add a {@link Migrator} for each incompatible type and make no deletion. The migrator
+ *         will migrate documents from it's old schema version to the new version. Migrated types
+ *         will be set into both {@link SetSchemaResponse#getIncompatibleTypes()} and
+ *         {@link SetSchemaResponse#getMigratedTypes()}. See the migration section below.
+ * </ul>
  * @see AppSearchSession#setSchema
+ * @see Migrator
  */
 public final class SetSchemaRequest {
     private final Set<AppSearchSchema> mSchemas;
-    private final Set<String> mSchemasNotVisibleToSystemUi;
+    private final Set<String> mSchemasNotDisplayedBySystem;
     private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
+    private final Map<String, Migrator> mMigrators;
     private final boolean mForceOverride;
+    private final int mVersion;
 
     SetSchemaRequest(@NonNull Set<AppSearchSchema> schemas,
-            @NonNull Set<String> schemasNotVisibleToSystemUi,
+            @NonNull Set<String> schemasNotDisplayedBySystem,
             @NonNull Map<String, Set<PackageIdentifier>> schemasVisibleToPackages,
-            boolean forceOverride) {
+            @NonNull Map<String, Migrator> migrators,
+            boolean forceOverride,
+            int version) {
         mSchemas = Preconditions.checkNotNull(schemas);
-        mSchemasNotVisibleToSystemUi = Preconditions.checkNotNull(schemasNotVisibleToSystemUi);
+        mSchemasNotDisplayedBySystem = Preconditions.checkNotNull(schemasNotDisplayedBySystem);
         mSchemasVisibleToPackages = Preconditions.checkNotNull(schemasVisibleToPackages);
+        mMigrators = Preconditions.checkNotNull(migrators);
         mForceOverride = forceOverride;
+        mVersion = version;
     }
 
-    /** Returns the schemas that are part of this request. */
+    /** Returns the {@link AppSearchSchema} types that are part of this request. */
     @NonNull
     public Set<AppSearchSchema> getSchemas() {
         return Collections.unmodifiableSet(mSchemas);
     }
 
     /**
-     * Returns the set of schema types that have opted out of being visible on system UI surfaces.
+     * Returns all the schema types that are opted out of being displayed and visible on any
+     * system UI surface.
      */
     @NonNull
-    public Set<String> getSchemasNotVisibleToSystemUi() {
-        return Collections.unmodifiableSet(mSchemasNotVisibleToSystemUi);
+    public Set<String> getSchemasNotDisplayedBySystem() {
+        return Collections.unmodifiableSet(mSchemasNotDisplayedBySystem);
     }
 
     /**
      * Returns a mapping of schema types to the set of packages that have access
-     * to that schema type. Each package is represented by a {@link PackageIdentifier}.
-     * name and byte[] certificate.
+     * to that schema type.
      *
-     * This method is inefficient to call repeatedly.
+     * <p>It’s inefficient to call this method repeatedly.
      */
     @NonNull
     public Map<String, Set<PackageIdentifier>> getSchemasVisibleToPackages() {
@@ -85,11 +132,19 @@
     }
 
     /**
-     * Returns a mapping of schema types to the set of packages that have access
-     * to that schema type. Each package is represented by a {@link PackageIdentifier}.
-     * name and byte[] certificate.
+     * Returns the map of {@link Migrator}, the key will be the schema type of the
+     * {@link Migrator} associated with.
+     */
+    @NonNull
+    public Map<String, Migrator> getMigrators() {
+        return Collections.unmodifiableMap(mMigrators);
+    }
+
+    /**
+     * Returns a mapping of {@link AppSearchSchema} types to the set of packages that have access
+     * to that schema type.
      *
-     * A more efficient version of {@link #getSchemasVisibleToPackages}, but it returns a
+     * <p>A more efficient version of {@link #getSchemasVisibleToPackages}, but it returns a
      * modifiable map. This is not meant to be unhidden and should only be used by internal
      * classes.
      *
@@ -106,33 +161,51 @@
         return mForceOverride;
     }
 
-    /** Builder for {@link SetSchemaRequest} objects. */
+    /** Returns the database overall schema version. */
+    @IntRange(from = 1)
+    public int getVersion() {
+        return mVersion;
+    }
+
+    /**
+     * Builder for {@link SetSchemaRequest} objects.
+     *
+     * <p>Once {@link #build} is called, the instance can no longer be used.
+     */
     public static final class Builder {
         private final Set<AppSearchSchema> mSchemas = new ArraySet<>();
-        private final Set<String> mSchemasNotVisibleToSystemUi = new ArraySet<>();
+        private final Set<String> mSchemasNotDisplayedBySystem = new ArraySet<>();
         private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages =
                 new ArrayMap<>();
+        private final Map<String, Migrator> mMigrators = new ArrayMap<>();
         private boolean mForceOverride = false;
+        private int mVersion = 1;
         private boolean mBuilt = false;
 
         /**
-         * Adds one or more types to the schema.
+         * Adds one or more {@link AppSearchSchema} types to the schema.
          *
-         * <p>Any documents of these types will be visible on system UI surfaces by default.
+         * <p>An {@link AppSearchSchema} object represents one type of structured data.
+         *
+         * <p>Any documents of these types will be displayed on system UI surfaces by default.
+         *
+         * @throws IllegalStateException if the builder has already been used.
          */
         @NonNull
-        public Builder addSchema(@NonNull AppSearchSchema... schemas) {
+        public Builder addSchemas(@NonNull AppSearchSchema... schemas) {
             Preconditions.checkNotNull(schemas);
-            return addSchema(Arrays.asList(schemas));
+            return addSchemas(Arrays.asList(schemas));
         }
 
         /**
-         * Adds one or more types to the schema.
+         * Adds a collection of {@link AppSearchSchema} objects to the schema.
          *
-         * <p>Any documents of these types will be visible on system UI surfaces by default.
+         * <p>An {@link AppSearchSchema} object represents one type of structured data.
+         *
+         * @throws IllegalStateException if the builder has already been used.
          */
         @NonNull
-        public Builder addSchema(@NonNull Collection<AppSearchSchema> schemas) {
+        public Builder addSchemas(@NonNull Collection<AppSearchSchema> schemas) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkNotNull(schemas);
             mSchemas.addAll(schemas);
@@ -141,77 +214,100 @@
 
 // @exportToFramework:startStrip()
         /**
-         * Adds one or more types to the schema.
+         * Adds one or more {@link androidx.appsearch.annotation.Document} annotated classes to the
+         * schema.
          *
-         * <p>Any documents of these types will be visible on system UI surfaces by default.
-         *
-         * @param dataClasses classes annotated with
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
+         * @param documentClasses classes annotated with
+         *                        {@link androidx.appsearch.annotation.Document}.
          * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
+         *                            has not generated a schema for the given document classes.
+         * @throws IllegalStateException if the builder has already been used.
          */
         @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getSchemas()
         @NonNull
-        public Builder addDataClass(@NonNull Class<?>... dataClasses)
+        public Builder addDocumentClasses(@NonNull Class<?>... documentClasses)
                 throws AppSearchException {
-            Preconditions.checkNotNull(dataClasses);
-            return addDataClass(Arrays.asList(dataClasses));
+            Preconditions.checkNotNull(documentClasses);
+            return addDocumentClasses(Arrays.asList(documentClasses));
         }
 
         /**
-         * Adds one or more types to the schema.
+         * Adds a collection of {@link androidx.appsearch.annotation.Document} annotated classes to
+         * the schema.
          *
-         * <p>Any documents of these types will be visible on system UI surfaces by default.
-         *
-         * @param dataClasses classes annotated with
-         *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
+         * @param documentClasses classes annotated with
+         *                        {@link androidx.appsearch.annotation.Document}.
          * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
+         *                            has not generated a schema for the given document classes.
+         * @throws IllegalStateException if the builder has already been used.
          */
         @SuppressLint("MissingGetterMatchingBuilder")  // Merged list available from getSchemas()
         @NonNull
-        public Builder addDataClass(@NonNull Collection<? extends Class<?>> dataClasses)
+        public Builder addDocumentClasses(@NonNull Collection<? extends Class<?>> documentClasses)
                 throws AppSearchException {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(dataClasses);
-            List<AppSearchSchema> schemas = new ArrayList<>(dataClasses.size());
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            for (Class<?> dataClass : dataClasses) {
-                DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
+            Preconditions.checkNotNull(documentClasses);
+            List<AppSearchSchema> schemas = new ArrayList<>(documentClasses.size());
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            for (Class<?> documentClass : documentClasses) {
+                DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
                 schemas.add(factory.getSchema());
             }
-            return addSchema(schemas);
+            return addSchemas(schemas);
         }
 // @exportToFramework:endStrip()
 
         /**
-         * Sets visibility on system UI surfaces for the given {@code schemaType}.
+         * Sets whether or not documents from the provided {@code schemaType} will be displayed
+         * and visible on any system UI surface.
          *
-         * @param schemaType The schema type to set visibility on.
-         * @param visible    Whether the {@code schemaType} will be visible or not.
+         * <p>This setting applies to the provided {@code schemaType} only, and does not persist
+         * across {@link AppSearchSession#setSchema} calls.
+         *
+         * <p>The default behavior, if this method is not called, is to allow types to be
+         * displayed on system UI surfaces.
+         *
+         * @param schemaType The name of an {@link AppSearchSchema} within the same
+         *                   {@link SetSchemaRequest}, which will be configured.
+         * @param displayed  Whether documents of this type will be displayed on system UI surfaces.
+         * @throws IllegalStateException if the builder has already been used.
          */
-        // Merged list available from getSchemasNotVisibleToSystemUi
+        // Merged list available from getSchemasNotDisplayedBySystem
         @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setSchemaTypeVisibilityForSystemUi(@NonNull String schemaType,
-                boolean visible) {
+        public Builder setSchemaTypeDisplayedBySystem(
+                @NonNull String schemaType, boolean displayed) {
             Preconditions.checkNotNull(schemaType);
             Preconditions.checkState(!mBuilt, "Builder has already been used");
 
-            if (visible) {
-                mSchemasNotVisibleToSystemUi.remove(schemaType);
+            if (displayed) {
+                mSchemasNotDisplayedBySystem.remove(schemaType);
             } else {
-                mSchemasNotVisibleToSystemUi.add(schemaType);
+                mSchemasNotDisplayedBySystem.add(schemaType);
             }
             return this;
         }
 
         /**
-         * Sets visibility for a package for the given {@code schemaType}.
+         * Sets whether or not documents from the provided {@code schemaType} can be read by the
+         * specified package.
+         *
+         * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
+         * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+         *
+         * <p>To opt into one-way data sharing with another application, the developer will need to
+         * explicitly grant the other application’s package name and certificate Read access to its
+         * data.
+         *
+         * <p>For two-way data sharing, both applications need to explicitly grant Read access to
+         * one another.
+         *
+         * <p>By default, data sharing between applications is disabled.
          *
          * @param schemaType        The schema type to set visibility on.
          * @param visible           Whether the {@code schemaType} will be visible or not.
          * @param packageIdentifier Represents the package that will be granted visibility.
+         * @throws IllegalStateException if the builder has already been used.
          */
         // Merged list available from getSchemasVisibleToPackages
         @SuppressLint("MissingGetterMatchingBuilder")
@@ -245,61 +341,150 @@
             return this;
         }
 
-// @exportToFramework:startStrip()
         /**
-         * Sets visibility on system UI surfaces for the given {@code dataClass}.
+         * Sets the {@link Migrator} associated with the given SchemaType.
          *
-         * @param dataClass The schema to set visibility on.
-         * @param visible   Whether the {@code schemaType} will be visible or not.
-         * @return {@link SetSchemaRequest.Builder}
-         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
+         * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
+         * from the current version number stored in AppSearch to the final version set via
+         * {@link #setVersion}.
+         *
+         * <p>A {@link Migrator} will be invoked if the current version number stored in
+         * AppSearch is different from the final version set via {@link #setVersion} and
+         * {@link Migrator#shouldMigrate} returns {@code true}.
+         *
+         * <p>The target schema type of the output {@link GenericDocument} of
+         * {@link Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this
+         * {@link SetSchemaRequest}.
+         *
+         * @param schemaType The schema type to set migrator on.
+         * @param migrator   The migrator translates a document from its current version to the
+         *                   final version set via {@link #setVersion}.
+         *
+         * @see SetSchemaRequest.Builder#setVersion
+         * @see SetSchemaRequest.Builder#addSchemas
+         * @see AppSearchSession#setSchema
          */
-        // Merged list available from getSchemasNotVisibleToSystemUi
-        @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setDataClassVisibilityForSystemUi(@NonNull Class<?> dataClass,
-                boolean visible) throws AppSearchException {
-            Preconditions.checkNotNull(dataClass);
-
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
-            return setSchemaTypeVisibilityForSystemUi(factory.getSchemaType(), visible);
+        @SuppressLint("MissingGetterMatchingBuilder")        // Getter return plural objects.
+        public Builder setMigrator(@NonNull String schemaType, @NonNull Migrator migrator) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(migrator);
+            mMigrators.put(schemaType, migrator);
+            return this;
         }
 
         /**
-         * Sets visibility for a package for the given {@code dataClass}.
+         * Sets a Map of {@link Migrator}s.
          *
-         * @param dataClass         The schema to set visibility on.
-         * @param visible           Whether the {@code schemaType} will be visible or not.
-         * @param packageIdentifier Represents the package that will be granted visibility
-         * @return {@link SetSchemaRequest.Builder}
+         * <p>The {@link Migrator} migrates all {@link GenericDocument}s under given schema type
+         * from the current version number stored in AppSearch to the final version set via
+         * {@link #setVersion}.
+         *
+         * <p>A {@link Migrator} will be invoked if the current version number stored in
+         * AppSearch is different from the final version set via {@link #setVersion} and
+         * {@link Migrator#shouldMigrate} returns {@code true}.
+         *
+         * <p>The target schema type of the output {@link GenericDocument} of
+         * {@link Migrator#onUpgrade} or {@link Migrator#onDowngrade} must exist in this
+         * {@link SetSchemaRequest}.
+         *
+         * @param migrators  A {@link Map} of migrators that translate a document from it's current
+         *                   version to the final version set via {@link #setVersion}.
+         *
+         * @see SetSchemaRequest.Builder#setVersion
+         * @see SetSchemaRequest.Builder#addSchemas
+         * @see AppSearchSession#setSchema
+         */
+        @NonNull
+        public Builder setMigrators(@NonNull Map<String, Migrator> migrators) {
+            Preconditions.checkNotNull(migrators);
+            mMigrators.putAll(migrators);
+            return this;
+        }
+
+// @exportToFramework:startStrip()
+
+        /**
+         * Sets whether or not documents from the provided
+         * {@link androidx.appsearch.annotation.Document} annotated class will be displayed and
+         * visible on any system UI surface.
+         *
+         * <p>This setting applies to the provided {@link androidx.appsearch.annotation.Document}
+         * annotated class only, and does not persist across {@link AppSearchSession#setSchema}
+         * calls.
+         *
+         * <p>The default behavior, if this method is not called, is to allow types to be
+         * displayed on system UI surfaces.
+         *
+         * @param documentClass A class annotated with
+         *                      {@link androidx.appsearch.annotation.Document}, the visibility of
+         *                      which will be configured
+         * @param displayed     Whether documents of this type will be displayed on system UI
+         *                      surfaces.
          * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
-         *                            has not generated a schema for the given data classes.
+         *                            has not generated a schema for the given document class.
+         */
+        // Merged list available from getSchemasNotDisplayedBySystem
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setDocumentClassDisplayedBySystem(@NonNull Class<?> documentClass,
+                boolean displayed) throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return setSchemaTypeDisplayedBySystem(factory.getSchemaName(), displayed);
+        }
+
+        /**
+         * Sets whether or not documents from the provided
+         * {@link androidx.appsearch.annotation.Document} annotated class can be read by the
+         * specified package.
+         *
+         * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
+         * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
+         *
+         * <p>To opt into one-way data sharing with another application, the developer will need to
+         * explicitly grant the other application’s package name and certificate Read access to its
+         * data.
+         *
+         * <p>For two-way data sharing, both applications need to explicitly grant Read access to
+         * one another.
+         *
+         * <p>By default, app data sharing between applications is disabled.
+         *
+         * @param documentClass     The {@link androidx.appsearch.annotation.Document} class to set
+         *                          visibility on.
+         * @param visible           Whether the {@code documentClass} will be visible or not.
+         * @param packageIdentifier Represents the package that will be granted visibility.
+         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+         *                            has not generated a schema for the given document class.
          */
         // Merged list available from getSchemasVisibleToPackages
         @SuppressLint("MissingGetterMatchingBuilder")
         @NonNull
-        public Builder setDataClassVisibilityForPackage(@NonNull Class<?> dataClass,
+        public Builder setDocumentClassVisibilityForPackage(@NonNull Class<?> documentClass,
                 boolean visible, @NonNull PackageIdentifier packageIdentifier)
                 throws AppSearchException {
-            Preconditions.checkNotNull(dataClass);
+            Preconditions.checkNotNull(documentClass);
 
-            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
-            DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
-            return setSchemaTypeVisibilityForPackage(factory.getSchemaType(), visible,
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return setSchemaTypeVisibilityForPackage(factory.getSchemaName(), visible,
                     packageIdentifier);
         }
 // @exportToFramework:endStrip()
 
         /**
-         * Configures the {@link SetSchemaRequest} to delete any existing documents that don't
-         * follow the new schema.
+         * Sets whether or not to override the current schema in the {@link AppSearchSession}
+         * database.
          *
-         * <p>By default, this is {@code false} and schema incompatibility causes the
-         * {@link AppSearchSession#setSchema} call to fail.
+         * <p>Call this method whenever backward incompatible changes need to be made by setting
+         * {@code forceOverride} to {@code true}. As a result, during execution of the setSchema
+         * operation, all documents that are incompatible with the new schema will be deleted and
+         * the new schema will be saved and persisted.
          *
-         * @see AppSearchSession#setSchema
+         * <p>By default, this is {@code false}.
          */
         @NonNull
         public Builder setForceOverride(boolean forceOverride) {
@@ -308,20 +493,54 @@
         }
 
         /**
-         * Builds a new {@link SetSchemaRequest}.
+         * Sets the version number of the overall {@link AppSearchSchema} in the database.
          *
-         * @throws IllegalArgumentException If schema types were referenced, but the
-         *                                  corresponding {@link AppSearchSchema} was never added.
+         * <p>The {@link AppSearchSession} database can only ever hold documents for one version
+         * at a time.
+         *
+         * <p>Setting a version number that is different from the version number  currently stored
+         * in AppSearch will result in AppSearch calling the {@link Migrator}s provided to
+         * {@link AppSearchSession#setSchema} to migrate the documents already in AppSearch from
+         * the previous version to the one set in this request. The version number can be
+         * updated without any other changes to the set of schemas.
+         *
+         * <p>The version number can stay the same, increase, or decrease relative to the current
+         * version number that is already stored in the {@link AppSearchSession} database.
+         *
+         * @param version A positive integer representing the version of the entire set of
+         *                schemas represents the version of the whole schema in the
+         *                {@link AppSearchSession} database, default version is 1.
+         *
+         * @throws IllegalStateException if the version is negative or the builder has already been
+         *                               used.
+         *
+         * @see AppSearchSession#setSchema
+         * @see Migrator
+         * @see SetSchemaRequest.Builder#setMigrator
+         */
+        @NonNull
+        public Builder setVersion(@IntRange(from = 1) int version) {
+            Preconditions.checkArgument(version >= 1, "Version must be a positive number.");
+            mVersion = version;
+            return this;
+        }
+
+        /**
+         * Builds a new {@link SetSchemaRequest} object.
+         *
+         * @throws IllegalArgumentException if schema types were referenced, but the
+         *                                  corresponding {@link AppSearchSchema} type was never
+         *                                  added.
+         * @throws IllegalStateException if the builder has already been used.
          */
         @NonNull
         public SetSchemaRequest build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
-            mBuilt = true;
 
-            // Verify that any schema types with visibility settings refer to a real schema.
+            // Verify that any schema types with display or visibility settings refer to a real
+            // schema.
             // Create a copy because we're going to remove from the set for verification purposes.
-            Set<String> referencedSchemas = new ArraySet<>(
-                    mSchemasNotVisibleToSystemUi);
+            Set<String> referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem);
             referencedSchemas.addAll(mSchemasVisibleToPackages.keySet());
 
             for (AppSearchSchema schema : mSchemas) {
@@ -331,13 +550,12 @@
                 // We still have schema types that weren't seen in our mSchemas set. This means
                 // there wasn't a corresponding AppSearchSchema.
                 throw new IllegalArgumentException(
-                        "Schema types " + referencedSchemas
-                                + " referenced, but were not added.");
+                        "Schema types " + referencedSchemas + " referenced, but were not added.");
             }
 
-            return new SetSchemaRequest(mSchemas, mSchemasNotVisibleToSystemUi,
-                    mSchemasVisibleToPackages,
-                    mForceOverride);
+            mBuilt = true;
+            return new SetSchemaRequest(mSchemas, mSchemasNotDisplayedBySystem,
+                    mSchemasVisibleToPackages, mMigrators, mForceOverride, mVersion);
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
new file mode 100644
index 0000000..127a905
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/** The response class of {@link AppSearchSession#setSchema} */
+public class SetSchemaResponse {
+
+    private static final String DELETED_TYPES_FIELD = "deletedTypes";
+    private static final String INCOMPATIBLE_TYPES_FIELD = "incompatibleTypes";
+    private static final String MIGRATED_TYPES_FIELD = "migratedTypes";
+
+    private final Bundle mBundle;
+    /**
+     * The migrationFailures won't be saved in the bundle. Since:
+     * <ul>
+     *     <li>{@link MigrationFailure} is generated in {@link AppSearchSession} which will be
+     *         the SDK side in platform. We don't need to pass it from service side via binder.
+     *     <li>Translate multiple {@link MigrationFailure}s to bundles in {@link Builder} and then
+     *         back in constructor will be a huge waste.
+     * </ul>
+     */
+    private final List<MigrationFailure> mMigrationFailures;
+
+    /** Cache of the inflated deleted schema types. Comes from inflating mBundles at first use. */
+    @Nullable
+    private Set<String> mDeletedTypes;
+
+    /** Cache of the inflated migrated schema types. Comes from inflating mBundles at first use. */
+    @Nullable
+    private Set<String> mMigratedTypes;
+
+    /**
+     * Cache of the inflated incompatible schema types. Comes from inflating mBundles at first use.
+     */
+    @Nullable
+    private Set<String> mIncompatibleTypes;
+
+    SetSchemaResponse(@NonNull Bundle bundle, @NonNull List<MigrationFailure> migrationFailures) {
+        mBundle = Preconditions.checkNotNull(bundle);
+        mMigrationFailures = Preconditions.checkNotNull(migrationFailures);
+    }
+
+    SetSchemaResponse(@NonNull Bundle bundle) {
+        this(bundle, /*migrationFailures=*/ Collections.emptyList());
+    }
+
+    /**
+     * Returns the {@link Bundle} populated by this builder.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public Bundle getBundle() {
+        return mBundle;
+    }
+
+    /**
+     * Returns a {@link List} of all failed {@link MigrationFailure}.
+     *
+     * <p>A {@link MigrationFailure} will be generated if the system trying to save a post-migrated
+     * {@link GenericDocument} but fail.
+     *
+     * <p>{@link MigrationFailure} contains the namespace, id and schemaType of the post-migrated
+     * {@link GenericDocument} and the error reason. Mostly it will be mismatch the schema it
+     * migrated to.
+     */
+    @NonNull
+    public List<MigrationFailure> getMigrationFailures() {
+        return Collections.unmodifiableList(mMigrationFailures);
+    }
+
+    /**
+     * Returns a {@link Set} of schema type that were deleted by the
+     * {@link AppSearchSession#setSchema} call.
+     */
+    @NonNull
+    public Set<String> getDeletedTypes() {
+        if (mDeletedTypes == null) {
+            mDeletedTypes = new ArraySet<>(
+                    Preconditions.checkNotNull(mBundle.getStringArrayList(DELETED_TYPES_FIELD)));
+        }
+        return Collections.unmodifiableSet(mDeletedTypes);
+    }
+
+    /**
+     * Returns a {@link Set} of schema type that were migrated by the
+     * {@link AppSearchSession#setSchema} call.
+     */
+    @NonNull
+    public Set<String> getMigratedTypes() {
+        if (mMigratedTypes == null) {
+            mMigratedTypes = new ArraySet<>(
+                    Preconditions.checkNotNull(mBundle.getStringArrayList(MIGRATED_TYPES_FIELD)));
+        }
+        return Collections.unmodifiableSet(mMigratedTypes);
+    }
+
+    /**
+     * Returns a {@link Set} of schema type whose new definitions set in the
+     * {@link AppSearchSession#setSchema} call were incompatible with the pre-existing schema.
+     *
+     * <p>If a {@link Migrator} is provided for this type and the migration is success triggered.
+     * The type will also appear in {@link #getMigratedTypes()}.
+     *
+     * @see AppSearchSession#setSchema
+     * @see SetSchemaRequest.Builder#setForceOverride
+     */
+    @NonNull
+    public Set<String> getIncompatibleTypes() {
+        if (mIncompatibleTypes == null) {
+            mIncompatibleTypes = new ArraySet<>(
+                    Preconditions.checkNotNull(
+                            mBundle.getStringArrayList(INCOMPATIBLE_TYPES_FIELD)));
+        }
+        return Collections.unmodifiableSet(mIncompatibleTypes);
+    }
+
+    /**
+     * Translates the {@link SetSchemaResponse}'s bundle to {@link Builder}.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    // TODO(b/179302942) change to Builder(mBundle) powered by mBundle.deepCopy
+    public Builder toBuilder() {
+        return new Builder()
+                .addDeletedTypes(getDeletedTypes())
+                .addIncompatibleTypes(getIncompatibleTypes())
+                .addMigratedTypes(getMigratedTypes())
+                .addMigrationFailures(mMigrationFailures);
+    }
+
+    /** Builder for {@link SetSchemaResponse} objects. */
+    public static final class Builder {
+        private final ArrayList<MigrationFailure> mMigrationFailures = new ArrayList<>();
+        private final ArrayList<String> mDeletedTypes = new ArrayList<>();
+        private final ArrayList<String> mMigratedTypes = new ArrayList<>();
+        private final ArrayList<String> mIncompatibleTypes = new ArrayList<>();
+        private boolean mBuilt = false;
+
+        /**  Adds {@link MigrationFailure}s to the list of migration failures. */
+        @NonNull
+        public Builder addMigrationFailures(
+                @NonNull Collection<MigrationFailure> migrationFailures) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mMigrationFailures.addAll(Preconditions.checkNotNull(migrationFailures));
+            return this;
+        }
+
+        /**  Adds a {@link MigrationFailure} to the list of migration failures. */
+        @NonNull
+        public Builder addMigrationFailure(@NonNull MigrationFailure migrationFailure) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mMigrationFailures.add(Preconditions.checkNotNull(migrationFailure));
+            return this;
+        }
+
+        /**  Adds deletedTypes to the list of deleted schema types. */
+        @NonNull
+        public Builder addDeletedTypes(@NonNull Collection<String> deletedTypes) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mDeletedTypes.addAll(Preconditions.checkNotNull(deletedTypes));
+            return this;
+        }
+
+        /**  Adds one deletedType to the list of deleted schema types. */
+        @NonNull
+        public Builder addDeletedType(@NonNull String deletedType) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mDeletedTypes.add(Preconditions.checkNotNull(deletedType));
+            return this;
+        }
+
+        /**  Adds incompatibleTypes to the list of incompatible schema types. */
+        @NonNull
+        public Builder addIncompatibleTypes(@NonNull Collection<String> incompatibleTypes) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mIncompatibleTypes.addAll(Preconditions.checkNotNull(incompatibleTypes));
+            return this;
+        }
+
+        /**  Adds one incompatibleType to the list of incompatible schema types. */
+        @NonNull
+        public Builder addIncompatibleType(@NonNull String incompatibleType) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mIncompatibleTypes.add(Preconditions.checkNotNull(incompatibleType));
+            return this;
+        }
+
+        /**  Adds migratedTypes to the list of migrated schema types. */
+        @NonNull
+        public Builder addMigratedTypes(@NonNull Collection<String> migratedTypes) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mMigratedTypes.addAll(Preconditions.checkNotNull(migratedTypes));
+            return this;
+        }
+
+        /**  Adds one migratedType to the list of migrated schema types. */
+        @NonNull
+        public Builder addMigratedType(@NonNull String migratedType) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mMigratedTypes.add(Preconditions.checkNotNull(migratedType));
+            return this;
+        }
+
+        /** Builds a {@link SetSchemaResponse} object. */
+        @NonNull
+        public SetSchemaResponse build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Bundle bundle = new Bundle();
+            bundle.putStringArrayList(INCOMPATIBLE_TYPES_FIELD, mIncompatibleTypes);
+            bundle.putStringArrayList(DELETED_TYPES_FIELD, mDeletedTypes);
+            bundle.putStringArrayList(MIGRATED_TYPES_FIELD, mMigratedTypes);
+            mBuilt = true;
+            // Avoid converting the potential thousands of MigrationFailures to Pracelable and
+            // back just for put in bundle. In platform, we should set MigrationFailures in
+            // AppSearchSession after we pass SetSchemaResponse via binder.
+            return new SetSchemaResponse(bundle, mMigrationFailures);
+        }
+    }
+
+    /**
+     * The class represents a post-migrated {@link GenericDocument} that failed to be saved by
+     * {@link AppSearchSession#setSchema}.
+     */
+    public static class MigrationFailure {
+        private static final String SCHEMA_TYPE_FIELD = "schemaType";
+        private static final String NAMESPACE_FIELD = "namespace";
+        private static final String DOCUMENT_ID_FIELD = "id";
+        private static final String ERROR_MESSAGE_FIELD = "errorMessage";
+        private static final String RESULT_CODE_FIELD = "resultCode";
+
+        private final Bundle mBundle;
+
+        /**
+         * Constructs a new {@link MigrationFailure}.
+         *
+         * @param namespace    The namespace of the document which failed to be migrated.
+         * @param documentId   The id of the document which failed to be migrated.
+         * @param schemaType   The type of the document which failed to be migrated.
+         * @param failedResult The reason why the document failed to be indexed.
+         * @throws IllegalArgumentException if the provided {@code failedResult} was not a failure.
+         */
+        public MigrationFailure(
+                @NonNull String namespace,
+                @NonNull String documentId,
+                @NonNull String schemaType,
+                @NonNull AppSearchResult<?> failedResult) {
+            mBundle = new Bundle();
+            mBundle.putString(NAMESPACE_FIELD, Preconditions.checkNotNull(namespace));
+            mBundle.putString(DOCUMENT_ID_FIELD, Preconditions.checkNotNull(documentId));
+            mBundle.putString(SCHEMA_TYPE_FIELD, Preconditions.checkNotNull(schemaType));
+
+            Preconditions.checkNotNull(failedResult);
+            Preconditions.checkArgument(
+                    !failedResult.isSuccess(), "failedResult was actually successful");
+            mBundle.putString(ERROR_MESSAGE_FIELD, failedResult.getErrorMessage());
+            mBundle.putInt(RESULT_CODE_FIELD, failedResult.getResultCode());
+        }
+
+        MigrationFailure(@NonNull Bundle bundle) {
+            mBundle = Preconditions.checkNotNull(bundle);
+        }
+
+        /**
+         * Returns the Bundle of the {@link MigrationFailure}.
+         *
+         * @hide
+         */
+        @NonNull
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Bundle getBundle() {
+            return mBundle;
+        }
+
+        /** Returns the namespace of the {@link GenericDocument} that failed to be migrated. */
+        @NonNull
+        public String getNamespace() {
+            return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/"");
+        }
+
+        /** Returns the id of the {@link GenericDocument} that failed to be migrated. */
+        @NonNull
+        public String getDocumentId() {
+            return mBundle.getString(DOCUMENT_ID_FIELD, /*defaultValue=*/"");
+        }
+
+        /** Returns the schema type of the {@link GenericDocument} that failed to be migrated. */
+        @NonNull
+        public String getSchemaType() {
+            return mBundle.getString(SCHEMA_TYPE_FIELD, /*defaultValue=*/"");
+        }
+
+        /**
+         * Returns the {@link AppSearchResult} that indicates why the
+         * post-migration {@link GenericDocument} failed to be indexed.
+         */
+        @NonNull
+        public AppSearchResult<Void> getAppSearchResult() {
+            return AppSearchResult.newFailedResult(mBundle.getInt(RESULT_CODE_FIELD),
+                    mBundle.getString(ERROR_MESSAGE_FIELD, /*defaultValue=*/""));
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
new file mode 100644
index 0000000..812a6f6
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.app;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/** The response class of {@code AppSearchSession#getStorageInfo}. */
+public class StorageInfo {
+
+    private static final String SIZE_BYTES_FIELD = "sizeBytes";
+    private static final String ALIVE_DOCUMENTS_COUNT = "aliveDocumentsCount";
+    private static final String ALIVE_NAMESPACES_COUNT = "aliveNamespacesCount";
+
+    private final Bundle mBundle;
+
+    StorageInfo(@NonNull Bundle bundle) {
+        mBundle = Preconditions.checkNotNull(bundle);
+    }
+
+    /**
+     * Returns the {@link Bundle} populated by this builder.
+     * @hide
+     */
+    @NonNull
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public Bundle getBundle() {
+        return mBundle;
+    }
+
+    /** Returns the estimated size of the session's database in bytes. */
+    public long getSizeBytes() {
+        return mBundle.getLong(SIZE_BYTES_FIELD);
+    }
+
+    /**
+     * Returns the number of alive documents in the current session.
+     *
+     * <p>Alive documents are documents that haven't been deleted and haven't exceeded the ttl as
+     * set in {@link GenericDocument.Builder#setTtlMillis}.
+     */
+    public int getAliveDocumentsCount() {
+        return mBundle.getInt(ALIVE_DOCUMENTS_COUNT);
+    }
+
+    /**
+     * Returns the number of namespaces that have at least one alive document in the current
+     * session's database.
+     *
+     * <p>Alive documents are documents that haven't been deleted and haven't exceeded the ttl as
+     * set in {@link GenericDocument.Builder#setTtlMillis}.
+     */
+    public int getAliveNamespacesCount() {
+        return mBundle.getInt(ALIVE_NAMESPACES_COUNT);
+    }
+
+    /** Builder for {@link StorageInfo} objects. */
+    public static final class Builder {
+        private final Bundle mBundle = new Bundle();
+        private boolean mBuilt = false;
+
+        /** Sets the size in bytes. */
+        @NonNull
+        public StorageInfo.Builder setSizeBytes(long sizeBytes) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBundle.putLong(SIZE_BYTES_FIELD, sizeBytes);
+            return this;
+        }
+
+        /** Sets the number of alive documents. */
+        @NonNull
+        public StorageInfo.Builder setAliveDocumentsCount(int numAliveDocuments) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBundle.putInt(ALIVE_DOCUMENTS_COUNT, numAliveDocuments);
+            return this;
+        }
+
+        /** Sets the number of alive namespaces. */
+        @NonNull
+        public StorageInfo.Builder setAliveNamespacesCount(int numAliveNamespaces) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBundle.putInt(ALIVE_NAMESPACES_COUNT, numAliveNamespaces);
+            return this;
+        }
+
+        /** Builds a {@link StorageInfo} object. */
+        @NonNull
+        public StorageInfo build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBuilt = true;
+            return new StorageInfo(mBundle);
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
index 262c97e..98689f5 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
@@ -31,19 +31,35 @@
 
     /**
      * Initializes an {@link AppSearchException} with no message.
-     * @hide
+     *
+     * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
      */
     public AppSearchException(@AppSearchResult.ResultCode int resultCode) {
         this(resultCode, /*message=*/ null);
     }
 
-    /** @hide */
+    /**
+     * Initializes an {@link AppSearchException} with a result code and message.
+     *
+     * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
+     * @param message    The detail message (which is saved for later retrieval by the
+     *                   {@link #getMessage()} method).
+     */
     public AppSearchException(
             @AppSearchResult.ResultCode int resultCode, @Nullable String message) {
         this(resultCode, message, /*cause=*/ null);
     }
 
-    /** @hide */
+    /**
+     * Initializes an {@link AppSearchException} with a result code, message and cause.
+     *
+     * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
+     * @param message    The detail message (which is saved for later retrieval by the
+     *                   {@link #getMessage()} method).
+     * @param cause      The cause (which is saved for later retrieval by the {@link #getCause()}
+     *                   method). (A null value is permitted, and indicates that the cause is
+     *                   nonexistent or unknown.)
+     */
     public AppSearchException(
             @AppSearchResult.ResultCode int resultCode,
             @Nullable String message,
@@ -52,14 +68,16 @@
         mResultCode = resultCode;
     }
 
-    /** Returns the result code this exception was constructed with. */
+    /**
+     * Returns the result code this exception was constructed with.
+     *
+     * @return One of the constants documented in {@link AppSearchResult#getResultCode}.
+     */
     public @AppSearchResult.ResultCode int getResultCode() {
         return mResultCode;
     }
 
-    /**
-     * Converts this {@link java.lang.Exception} into a failed {@link 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/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/IllegalSearchSpecException.java b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/IllegalSearchSpecException.java
deleted file mode 100644
index 3e06f81..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/IllegalSearchSpecException.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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 androidx.appsearch.exceptions;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-/**
- * Indicates that a {@link androidx.appsearch.app.SearchResult} has logical inconsistencies such
- * as unpopulated mandatory fields or illegal combinations of parameters.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class IllegalSearchSpecException extends IllegalArgumentException {
-    /**
-     * Constructs a new {@link IllegalSearchSpecException}.
-     *
-     * @param message A developer-readable description of the issue with the bundle.
-     */
-    public IllegalSearchSpecException(@NonNull String message) {
-        super(message);
-    }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
index 86de233..c8f5946 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
@@ -148,35 +148,41 @@
         if (bundle == null) {
             return 0;
         }
-        int[] hashCodes = new int[bundle.size()];
-        int i = 0;
+        int[] hashCodes = new int[bundle.size() + 1];
+        int hashCodeIdx = 0;
         // Bundle inherit its hashCode() from Object.java, which only relative to their memory
         // address. Bundle doesn't have an order, so we should iterate all keys and combine
         // their value's hashcode into an array. And use the hashcode of the array to be
         // the hashcode of the bundle.
-        for (String key : bundle.keySet()) {
-            Object value = bundle.get(key);
+        // Because bundle.keySet() doesn't guarantee any particular order, we need to sort the keys
+        // in case the iteration order varies from run to run.
+        String[] keys = bundle.keySet().toArray(new String[0]);
+        Arrays.sort(keys);
+        // Hash the keys so we can detect key-only differences
+        hashCodes[hashCodeIdx++] = Arrays.hashCode(keys);
+        for (int keyIdx = 0; keyIdx < keys.length; keyIdx++) {
+            Object value = bundle.get(keys[keyIdx]);
             if (value instanceof Bundle) {
-                hashCodes[i++] = deepHashCode((Bundle) value);
+                hashCodes[hashCodeIdx++] = deepHashCode((Bundle) value);
             } else if (value instanceof int[]) {
-                hashCodes[i++] = Arrays.hashCode((int[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((int[]) value);
             } else if (value instanceof byte[]) {
-                hashCodes[i++] = Arrays.hashCode((byte[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((byte[]) value);
             } else if (value instanceof char[]) {
-                hashCodes[i++] = Arrays.hashCode((char[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((char[]) value);
             } else if (value instanceof long[]) {
-                hashCodes[i++] = Arrays.hashCode((long[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((long[]) value);
             } else if (value instanceof float[]) {
-                hashCodes[i++] = Arrays.hashCode((float[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((float[]) value);
             } else if (value instanceof short[]) {
-                hashCodes[i++] = Arrays.hashCode((short[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((short[]) value);
             } else if (value instanceof double[]) {
-                hashCodes[i++] = Arrays.hashCode((double[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((double[]) value);
             } else if (value instanceof boolean[]) {
-                hashCodes[i++] = Arrays.hashCode((boolean[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((boolean[]) value);
             } else if (value instanceof String[]) {
                 // Optimization to avoid Object[] handler creating an inner array for common cases
-                hashCodes[i++] = Arrays.hashCode((String[]) value);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode((String[]) value);
             } else if (value instanceof Object[]) {
                 Object[] array = (Object[]) value;
                 int[] innerHashCodes = new int[array.length];
@@ -187,7 +193,7 @@
                         innerHashCodes[j] = array[j].hashCode();
                     }
                 }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
             } else if (value instanceof ArrayList) {
                 ArrayList<?> list = (ArrayList<?>) value;
                 int[] innerHashCodes = new int[list.size()];
@@ -199,7 +205,7 @@
                         innerHashCodes[j] = item.hashCode();
                     }
                 }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
             } else if (value instanceof SparseArray) {
                 SparseArray<?> array = (SparseArray<?>) value;
                 int[] innerHashCodes = new int[array.size() * 2];
@@ -212,9 +218,9 @@
                         innerHashCodes[j * 2 + 1] = item.hashCode();
                     }
                 }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+                hashCodes[hashCodeIdx++] = Arrays.hashCode(innerHashCodes);
             } else {
-                hashCodes[i++] = value.hashCode();
+                hashCodes[hashCodeIdx++] = value.hashCode();
             }
         }
         return Arrays.hashCode(hashCodes);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/SchemaMigrationUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/SchemaMigrationUtil.java
new file mode 100644
index 0000000..799b9de
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/SchemaMigrationUtil.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utilities for schema migration.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SchemaMigrationUtil {
+    private SchemaMigrationUtil() {}
+
+    /**
+     * Returns all active {@link Migrator}s that need to be triggered in this migration.
+     *
+     * <p>{@link Migrator#shouldMigrate} returns {@code true} will make the {@link Migrator} active.
+     */
+    @NonNull
+    public static Map<String, Migrator> getActiveMigrators(
+            @NonNull Set<AppSearchSchema> existingSchemas,
+            @NonNull Map<String, Migrator> migrators,
+            int currentVersion,
+            int finalVersion) {
+        if (currentVersion == finalVersion) {
+            return Collections.emptyMap();
+        }
+        Set<String> existingTypes = new ArraySet<>(existingSchemas.size());
+        for (AppSearchSchema schema : existingSchemas) {
+            existingTypes.add(schema.getSchemaType());
+        }
+
+        Map<String, Migrator> activeMigrators = new ArrayMap<>();
+        for (Map.Entry<String, Migrator> entry : migrators.entrySet()) {
+            // The device contains the source type, and we should trigger migration for the type.
+            String schemaType = entry.getKey();
+            Migrator migrator = entry.getValue();
+            if (existingTypes.contains(schemaType)
+                    && migrator.shouldMigrate(currentVersion, finalVersion)) {
+                activeMigrators.put(schemaType, migrator);
+            }
+        }
+        return activeMigrators;
+    }
+
+    /**
+     * Checks the setSchema() call won't delete any types or has incompatible types after
+     * all {@link Migrator} has been triggered..
+     */
+    public static void checkDeletedAndIncompatibleAfterMigration(
+            @NonNull SetSchemaResponse setSchemaResponse,
+            @NonNull Set<String> activeMigrators) throws AppSearchException {
+        Set<String> unmigratedIncompatibleTypes =
+                new ArraySet<>(setSchemaResponse.getIncompatibleTypes());
+        unmigratedIncompatibleTypes.removeAll(activeMigrators);
+
+        Set<String> unmigratedDeletedTypes =
+                new ArraySet<>(setSchemaResponse.getDeletedTypes());
+        unmigratedDeletedTypes.removeAll(activeMigrators);
+
+        // check if there are any unmigrated incompatible types or deleted types. If there
+        // are, we will getActiveMigratorsthrow an exception. That's the only case we
+        // swallowed in the AppSearchImpl#setSchema().
+        // Since the force override is false, the schema will not have been set if there are
+        // any incompatible or deleted types.
+        checkDeletedAndIncompatible(unmigratedDeletedTypes,
+                unmigratedIncompatibleTypes);
+    }
+
+    /**  Checks the setSchema() call won't delete any types or has incompatible types. */
+    public static void checkDeletedAndIncompatible(@NonNull Set<String> deletedTypes,
+            @NonNull Set<String> incompatibleTypes) throws AppSearchException {
+        if (deletedTypes.size() > 0
+                || incompatibleTypes.size() > 0) {
+            String newMessage = "Schema is incompatible."
+                    + "\n  Deleted types: " + deletedTypes
+                    + "\n  Incompatible types: " + incompatibleTypes;
+            throw new AppSearchException(AppSearchResult.RESULT_INVALID_SCHEMA, newMessage);
+        }
+    }
+}
diff --git a/appsearch/compiler/build.gradle b/appsearch/compiler/build.gradle
index 7d67114..2421047 100644
--- a/appsearch/compiler/build.gradle
+++ b/appsearch/compiler/build.gradle
@@ -26,6 +26,8 @@
 
 dependencies {
     api('androidx.annotation:annotation:1.1.0')
+    api(JSR250)
+    implementation(AUTO_COMMON)
     implementation(JAVAPOET)
 
     // For testing, add in the compiled classes from appsearch to get access to annotations.
@@ -43,6 +45,6 @@
     type = LibraryType.COMPILER_PLUGIN
     mavenGroup = LibraryGroups.APPSEARCH
     inceptionYear = '2019'
-    description = 'Compiler for AndroidX AppSearch data classes'
+    description = 'Compiler for classes annotated with @androidx.appsearch.annotation.Document'
     failOnDeprecationWarnings = false
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
index 63f6e379..09fc40d 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
@@ -15,17 +15,22 @@
  */
 package androidx.appsearch.compiler;
 
+import static javax.lang.model.util.ElementFilter.typesIn;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.google.auto.common.BasicAnnotationProcessor;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.Set;
 
-import javax.annotation.processing.AbstractProcessor;
 import javax.annotation.processing.Messager;
 import javax.annotation.processing.ProcessingEnvironment;
-import javax.annotation.processing.RoundEnvironment;
 import javax.annotation.processing.SupportedAnnotationTypes;
 import javax.annotation.processing.SupportedOptions;
 import javax.annotation.processing.SupportedSourceVersion;
@@ -33,13 +38,13 @@
 import javax.lang.model.element.Element;
 import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.TypeElement;
-import javax.tools.Diagnostic;
+import javax.tools.Diagnostic.Kind;
 
-/** Processes AppSearchDocument annotations. */
-@SupportedAnnotationTypes({IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS})
+/** Processes @Document annotations. */
+@SupportedAnnotationTypes({IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS})
 @SupportedSourceVersion(SourceVersion.RELEASE_8)
 @SupportedOptions({AppSearchCompiler.OUTPUT_DIR_OPTION})
-public class AppSearchCompiler extends AbstractProcessor {
+public class AppSearchCompiler extends BasicAnnotationProcessor {
     /**
      * This property causes us to write output to a different folder instead of the usual filer
      * location. It should only be used for testing.
@@ -47,8 +52,6 @@
     @VisibleForTesting
     static final String OUTPUT_DIR_OPTION = "AppSearchCompiler.OutputDir";
 
-    private Messager mMessager;
-
     @Override
     @NonNull
     public SourceVersion getSupportedSourceVersion() {
@@ -56,73 +59,67 @@
     }
 
     @Override
-    public synchronized void init(@NonNull ProcessingEnvironment processingEnvironment) {
-        super.init(processingEnvironment);
-        mMessager = processingEnvironment.getMessager();
+    protected Iterable<? extends Step> steps() {
+        return ImmutableList.of(new AppSearchCompileStep(processingEnv));
     }
 
-    @Override
-    public boolean process(
-            @NonNull Set<? extends TypeElement> set,
-            @NonNull RoundEnvironment roundEnvironment) {
-        try {
-            tryProcess(set, roundEnvironment);
-        } catch (ProcessingException e) {
-            e.printDiagnostic(mMessager);
+    private static final class AppSearchCompileStep implements Step {
+        private final ProcessingEnvironment mProcessingEnv;
+        private final Messager mMessager;
+
+        AppSearchCompileStep(ProcessingEnvironment processingEnv) {
+            mProcessingEnv = processingEnv;
+            mMessager = processingEnv.getMessager();
         }
-        // True means we claimed the annotations. This is true regardless of whether they were
-        // used correctly.
-        return true;
-    }
 
-    private void tryProcess(
-            @NonNull Set<? extends TypeElement> set,
-            @NonNull RoundEnvironment roundEnvironment) throws ProcessingException {
-        if (set.isEmpty()) return;
+        @Override
+        public ImmutableSet<String> annotations() {
+            return ImmutableSet.of(IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
+        }
 
-        // Find the TypeElement corresponding to the @AppSearchDocument annotation. We can't use the
-        // annotation class directly because the appsearch project compiles only on Android, but
-        // this annotation processor runs on the host.
-        TypeElement appSearchDocument =
-                findAnnotation(set, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+        @Override
+        public ImmutableSet<Element> process(
+                ImmutableSetMultimap<String, Element> elementsByAnnotation) {
+            Set<TypeElement> documentElements =
+                    typesIn(elementsByAnnotation.get(
+                            IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS));
+            for (TypeElement document : documentElements) {
+                try {
+                    processDocument(document);
+                } catch (ProcessingException e) {
+                    // Prints error message.
+                    e.printDiagnostic(mMessager);
+                }
+            }
+            // No elements will be passed to next round of processing.
+            return ImmutableSet.of();
+        }
 
-        for (Element element : roundEnvironment.getElementsAnnotatedWith(appSearchDocument)) {
+        private void processDocument(@NonNull TypeElement element) throws ProcessingException {
             if (element.getKind() != ElementKind.CLASS) {
                 throw new ProcessingException(
-                        "@AppSearchDocument annotation on something other than a class", element);
+                        "@Document annotation on something other than a class", element);
             }
-            processAppSearchDocument((TypeElement) element);
-        }
-    }
 
-    private void processAppSearchDocument(@NonNull TypeElement element) throws ProcessingException {
-        AppSearchDocumentModel model = AppSearchDocumentModel.create(processingEnv, element);
-        CodeGenerator generator = CodeGenerator.generate(processingEnv, model);
-        String outputDir = processingEnv.getOptions().get(OUTPUT_DIR_OPTION);
-        try {
-            if (outputDir == null || outputDir.isEmpty()) {
-                generator.writeToFiler();
-            } else {
-                mMessager.printMessage(
-                        Diagnostic.Kind.NOTE,
-                        "Writing output to \"" + outputDir
-                                + "\" due to the presence of -A" + OUTPUT_DIR_OPTION);
-                generator.writeToFolder(new File(outputDir));
-            }
-        } catch (IOException e) {
-            ProcessingException pe =
-                    new ProcessingException("Failed to write output", model.getClassElement());
-            pe.initCause(e);
-            throw pe;
-        }
-    }
-
-    private TypeElement findAnnotation(Set<? extends TypeElement> set, String name) {
-        for (TypeElement typeElement : set) {
-            if (typeElement.getQualifiedName().contentEquals(name)) {
-                return typeElement;
+            DocumentModel model = DocumentModel.create(mProcessingEnv, element);
+            CodeGenerator generator = CodeGenerator.generate(mProcessingEnv, model);
+            String outputDir = mProcessingEnv.getOptions().get(OUTPUT_DIR_OPTION);
+            try {
+                if (outputDir == null || outputDir.isEmpty()) {
+                    generator.writeToFiler();
+                } else {
+                    mMessager.printMessage(
+                            Kind.NOTE,
+                            "Writing output to \"" + outputDir
+                                    + "\" due to the presence of -A" + OUTPUT_DIR_OPTION);
+                    generator.writeToFolder(new File(outputDir));
+                }
+            } catch (IOException e) {
+                ProcessingException pe =
+                        new ProcessingException("Failed to write output", model.getClassElement());
+                pe.initCause(e);
+                throw pe;
             }
         }
-        return null;
     }
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java
index 9ee924a..4b8aa80 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java
@@ -17,8 +17,9 @@
 package androidx.appsearch.compiler;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
 
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
 import com.squareup.javapoet.JavaFile;
 import com.squareup.javapoet.ParameterizedTypeName;
 import com.squareup.javapoet.TypeName;
@@ -27,32 +28,30 @@
 import java.io.File;
 import java.io.IOException;
 
+import javax.annotation.Generated;
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.lang.model.element.Modifier;
 
 /**
  * Generates java code for an {@link androidx.appsearch.app.AppSearchSchema} and a translator
- * between the data class and a {@link androidx.appsearch.app.GenericDocument}.
+ * between the document class and a {@link androidx.appsearch.app.GenericDocument}.
  */
 class CodeGenerator {
-    @VisibleForTesting
-    static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
-
     private final ProcessingEnvironment mEnv;
     private final IntrospectionHelper mHelper;
-    private final AppSearchDocumentModel mModel;
+    private final DocumentModel mModel;
 
     private final String mOutputPackage;
     private final TypeSpec mOutputClass;
 
     public static CodeGenerator generate(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model)
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model)
             throws ProcessingException {
         return new CodeGenerator(env, model);
     }
 
     private CodeGenerator(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model)
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model)
             throws ProcessingException {
         // Prepare constants needed for processing
         mEnv = env;
@@ -74,27 +73,23 @@
 
     /**
      * Creates factory class for any class annotated with
-     * {@link androidx.appsearch.annotation.AppSearchDocument}
+     * {@link androidx.appsearch.annotation.Document}
      * <p>Class Example 1:
-     *   For a class Foo annotated with @AppSearchDocument, we will generated a
+     *   For a class Foo annotated with @Document, we will generated a
      *   $$__AppSearch__Foo.class under the output package.
      * <p>Class Example 2:
-     *   For an inner class Foo.Bar annotated with @AppSearchDocument, we will generated a
+     *   For an inner class Foo.Bar annotated with @Document, we will generated a
      *   $$__AppSearch__Foo$$__Bar.class under the output package.
      */
     private TypeSpec createClass() throws ProcessingException {
         // Gets the full name of target class.
         String qualifiedName = mModel.getClassElement().getQualifiedName().toString();
-        String packageName = mOutputPackage + ".";
-
-        // Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
-        // for inner class Foo.Bar.
-        String genClassName = GEN_CLASS_PREFIX
-                + qualifiedName.substring(packageName.length()).replace(".", "$$__");
+        String className = qualifiedName.substring(mOutputPackage.length() + 1);
+        ClassName genClassName = mHelper.getDocumentClassFactoryForClass(mOutputPackage, className);
 
         TypeName genClassType = TypeName.get(mModel.getClassElement().asType());
         TypeName factoryType = ParameterizedTypeName.get(
-                mHelper.getAppSearchClass("DataClassFactory"),
+                mHelper.getAppSearchClass("DocumentClassFactory"),
                 genClassType);
 
         TypeSpec.Builder genClass = TypeSpec
@@ -103,6 +98,12 @@
                 .addModifiers(Modifier.PUBLIC)
                 .addSuperinterface(factoryType);
 
+        // Add the @Generated annotation to avoid static analysis running on these files
+        genClass.addAnnotation(
+                AnnotationSpec.builder(Generated.class)
+                        .addMember("value", "$S", AppSearchCompiler.class.getCanonicalName())
+                        .build());
+
         SchemaCodeGenerator.generate(mEnv, mModel, genClass);
         ToGenericDocumentCodeGenerator.generate(mEnv, mModel, genClass);
         FromGenericDocumentCodeGenerator.generate(mEnv, mModel, genClass);
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
similarity index 91%
rename from appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java
rename to appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
index 76578db..d05cb82 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentModel.java
@@ -40,14 +40,14 @@
 import javax.lang.model.element.VariableElement;
 
 /**
- * Processes AppSearchDocument annotations.
+ * Processes @Document annotations.
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class AppSearchDocumentModel {
+class DocumentModel {
 
     /** Enumeration of fields that must be handled specially (i.e. are not properties) */
-    enum SpecialField { URI, NAMESPACE, CREATION_TIMESTAMP_MILLIS, TTL_MILLIS, SCORE }
+    enum SpecialField { ID, NAMESPACE, CREATION_TIMESTAMP_MILLIS, TTL_MILLIS, SCORE }
     /** Determines how the annotation processor has decided to read the value of a field. */
     enum ReadKind { FIELD, GETTER }
     /** Determines how the annotation processor has decided to write the value of a field. */
@@ -55,7 +55,7 @@
 
     private final IntrospectionHelper mIntrospectionHelper;
     private final TypeElement mClass;
-    private final AnnotationMirror mAppSearchDocumentAnnotation;
+    private final AnnotationMirror mDocumentAnnotation;
     private final Set<ExecutableElement> mConstructors = new LinkedHashSet<>();
     private final Set<ExecutableElement> mMethods = new LinkedHashSet<>();
     private final Map<String, VariableElement> mAllAppSearchFields = new LinkedHashMap<>();
@@ -66,18 +66,18 @@
     private final Map<VariableElement, ProcessingException> mWriteWhyConstructor = new HashMap<>();
     private List<String> mChosenConstructorParams = null;
 
-    private AppSearchDocumentModel(
+    private DocumentModel(
             @NonNull ProcessingEnvironment env,
             @NonNull TypeElement clazz)
             throws ProcessingException {
         mIntrospectionHelper = new IntrospectionHelper(env);
         mClass = clazz;
         if (mClass.getModifiers().contains(Modifier.PRIVATE)) {
-            throw new ProcessingException("@AppSearchDocument annotated class is private", mClass);
+            throw new ProcessingException("@Document annotated class is private", mClass);
         }
 
-        mAppSearchDocumentAnnotation = mIntrospectionHelper.getAnnotation(
-                mClass, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+        mDocumentAnnotation = mIntrospectionHelper.getAnnotation(
+                mClass, IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
 
         // Scan methods and constructors. AppSearch doesn't define any annotations that apply to
         // these, but we will need this info when processing fields to make sure the fields can
@@ -102,7 +102,7 @@
     @NonNull
     public String getSchemaName() {
         Map<String, Object> params =
-                mIntrospectionHelper.getAnnotationParams(mAppSearchDocumentAnnotation);
+                mIntrospectionHelper.getAnnotationParams(mDocumentAnnotation);
         String name = params.get("name").toString();
         if (name.isEmpty()) {
             return mClass.getSimpleName().toString();
@@ -161,8 +161,8 @@
     }
 
     private void scanFields() throws ProcessingException {
-        Element uriField = null;
         Element namespaceField = null;
+        Element idField = null;
         Element creationTimestampField = null;
         Element ttlField = null;
         Element scoreField = null;
@@ -173,13 +173,13 @@
             for (AnnotationMirror annotation : child.getAnnotationMirrors()) {
                 String annotationFq = annotation.getAnnotationType().toString();
                 boolean isAppSearchField = true;
-                if (IntrospectionHelper.URI_CLASS.equals(annotationFq)) {
-                    if (uriField != null) {
+                if (IntrospectionHelper.ID_CLASS.equals(annotationFq)) {
+                    if (idField != null) {
                         throw new ProcessingException(
-                                "Class contains multiple fields annotated @Uri", child);
+                                "Class contains multiple fields annotated @Id", child);
                     }
-                    uriField = child;
-                    mSpecialFieldNames.put(SpecialField.URI, fieldName);
+                    idField = child;
+                    mSpecialFieldNames.put(SpecialField.ID, fieldName);
 
                 } else if (IntrospectionHelper.NAMESPACE_CLASS.equals(annotationFq)) {
                     if (namespaceField != null) {
@@ -228,11 +228,18 @@
             }
         }
 
-        // Every document must always have a URI
-        if (uriField == null) {
+        // Every document must always have a namespace
+        if (namespaceField == null) {
             throw new ProcessingException(
-                    "All @AppSearchDocument classes must have exactly one field annotated with "
-                            + "@Uri", mClass);
+                    "All @Document classes must have exactly one field annotated with @Namespace",
+                    mClass);
+        }
+
+        // Every document must always have an ID
+        if (idField == null) {
+            throw new ProcessingException(
+                    "All @Document classes must have exactly one field annotated with @Id",
+                    mClass);
         }
 
         for (VariableElement appSearchField : mAllAppSearchFields.values()) {
@@ -430,13 +437,13 @@
     }
 
     /**
-     * Tries to create an {@link AppSearchDocumentModel} from the given {@link Element}.
+     * Tries to create an {@link DocumentModel} from the given {@link Element}.
      *
-     * @throws ProcessingException if the @{@code AppSearchDocument}-annotated class is invalid.
+     * @throws ProcessingException if the @{@code Document}-annotated class is invalid.
      */
-    public static AppSearchDocumentModel create(
+    public static DocumentModel create(
             @NonNull ProcessingEnvironment env, @NonNull TypeElement clazz)
             throws ProcessingException {
-        return new AppSearchDocumentModel(env, clazz);
+        return new DocumentModel(env, clazz);
     }
 }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
index 7ccdc53..273b47b 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
@@ -43,22 +43,22 @@
 
 /**
  * Generates java code for a translator from a {@link androidx.appsearch.app.GenericDocument} to
- * a data class.
+ * an instance of a class annotated with {@link androidx.appsearch.annotation.Document}.
  */
 class FromGenericDocumentCodeGenerator {
     private final ProcessingEnvironment mEnv;
     private final IntrospectionHelper mHelper;
-    private final AppSearchDocumentModel mModel;
+    private final DocumentModel mModel;
 
     public static void generate(
             @NonNull ProcessingEnvironment env,
-            @NonNull AppSearchDocumentModel model,
+            @NonNull DocumentModel model,
             @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
         new FromGenericDocumentCodeGenerator(env, model).generate(classBuilder);
     }
 
     private FromGenericDocumentCodeGenerator(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model) {
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
         mEnv = env;
         mHelper = new IntrospectionHelper(env);
         mModel = model;
@@ -80,14 +80,14 @@
 
         unpackSpecialFields(methodBuilder);
 
-        // Unpack properties from the GenericDocument into the format desired by the data class
+        // Unpack properties from the GenericDocument into the format desired by the document class
         for (Map.Entry<String, VariableElement> entry : mModel.getPropertyFields().entrySet()) {
             fieldFromGenericDoc(methodBuilder, entry.getKey(), entry.getValue());
         }
 
-        // Create an instance of the data class via the chosen constructor
+        // Create an instance of the document class via the chosen constructor
         methodBuilder.addStatement(
-                "$T dataClass = new $T($L)", classType, classType, getConstructorParams());
+                "$T document = new $T($L)", classType, classType, getConstructorParams());
 
         // Assign all fields which weren't set in the constructor
         for (String field : mModel.getAllFields().keySet()) {
@@ -97,13 +97,13 @@
             }
         }
 
-        methodBuilder.addStatement("return dataClass");
+        methodBuilder.addStatement("return document");
         return methodBuilder.build();
     }
 
     /**
      * Converts a field from a {@link androidx.appsearch.app.GenericDocument} into a format suitable
-     * for the data class.
+     * for the document class.
      */
     private void fieldFromGenericDoc(
             @NonNull MethodSpec.Builder builder,
@@ -121,7 +121,7 @@
         //       conversion of the collection elements is needed. We can use Arrays#asList for this.
         //
         //   1c: ListForLoopCallFromGenericDocument
-        //       List contains a class which is annotated with @AppSearchDocument.
+        //       List contains a class which is annotated with @Document.
         //       We have to convert this from an array of GenericDocument[], by reading each element
         //       one-by-one and converting it through the standard conversion machinery.
         //
@@ -143,7 +143,7 @@
         //       We can directly use this field with no conversion.
         //
         //   2c: ArrayForLoopCallFromGenericDocument
-        //       Array is of a class which is annotated with @AppSearchDocument.
+        //       Array is of a class which is annotated with @Document.
         //       We have to convert this from an array of GenericDocument[], by reading each element
         //       one-by-one and converting it through the standard conversion machinery.
         //
@@ -167,7 +167,7 @@
         //       needed
         //
         //   3c: FieldCallFromGenericDocument
-        //       Field is of a class which is annotated with @AppSearchDocument.
+        //       Field is of a class which is annotated with @Document.
         //       We have to convert this from a GenericDocument through the standard conversion
         //       machinery.
         //
@@ -322,7 +322,7 @@
     }
 
     //   1c: ListForLoopCallFromGenericDocument
-    //       List contains a class which is annotated with @AppSearchDocument.
+    //       List contains a class which is annotated with @Document.
     //       We have to convert this from an array of GenericDocument[], by reading each element
     //       one-by-one and converting it through the standard conversion machinery.
     private boolean tryListForLoopCallFromGenericDocument(
@@ -340,9 +340,9 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            mHelper.getAnnotation(element, IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 1c
+            // The propertyType doesn't have @Document annotation, this is not a type 1c
             // list.
             return false;
         }
@@ -356,17 +356,15 @@
 
         // If not null, iterate and assign
         body.add("if ($NCopy != null) {\n", fieldName).indent();
-        body.addStatement("$T factory = $T.getInstance().getOrCreateFactory($T.class)",
-                ParameterizedTypeName.get(mHelper.getAppSearchClass("DataClassFactory"),
-                        TypeName.get(propertyType)),
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
-        body.addStatement("$NConv = new $T<>($NCopy.length)", fieldName, ArrayList.class,
-                fieldName);
+        body.addStatement(
+                "$NConv = new $T<>($NCopy.length)", fieldName, ArrayList.class, fieldName);
 
-        body.add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent();
-        body.addStatement("$NConv.add(factory.fromGenericDocument($NCopy[i]))", fieldName,
-                fieldName);
-        body.unindent().add("}\n");
+        body
+                .add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent()
+                .addStatement(
+                        "$NConv.add($NCopy[i].toDocumentClass($T.class))",
+                        fieldName, fieldName, propertyType)
+                .unindent().add("}\n");
 
         body.unindent().add("}\n");  //  if ($NCopy != null) {
         method.add(body.build());
@@ -525,7 +523,7 @@
     }
 
     //   2c: ArrayForLoopCallFromGenericDocument
-    //       Array is of a class which is annotated with @AppSearchDocument.
+    //       Array is of a class which is annotated with @Document.
     //       We have to convert this from an array of GenericDocument[], by reading each element
     //       one-by-one and converting it through the standard conversion machinery.
     private boolean tryArrayForLoopCallFromGenericDocument(
@@ -542,9 +540,9 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            mHelper.getAnnotation(element, IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 2c
+            // The propertyType doesn't have @Document annotation, this is not a type 2c
             // array.
             return false;
         }
@@ -562,15 +560,13 @@
         // If not null, iterate and assign
         body.add("if ($NCopy != null) {\n", fieldName).indent();
         body.addStatement("$NConv = new $T[$NCopy.length]", fieldName, propertyType, fieldName);
-        body.addStatement("$T factory = $T.getInstance().getOrCreateFactory($T.class)",
-                ParameterizedTypeName.get(mHelper.getAppSearchClass("DataClassFactory"),
-                        TypeName.get(propertyType)),
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
 
-        body.add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent();
-        body.addStatement("$NConv[i] = factory.fromGenericDocument($NCopy[i])", fieldName,
-                fieldName);
-        body.unindent().add("}\n");
+        body
+                .add("for (int i = 0; i < $NCopy.length; i++) {\n", fieldName).indent()
+                .addStatement(
+                        "$NConv[i] = $NCopy[i].toDocumentClass($T.class)",
+                        fieldName, fieldName, propertyType)
+                .unindent().add("}\n");
 
         body.unindent().add("}\n");  //  if ($NCopy != null) {
         method.add(body.build());
@@ -716,7 +712,7 @@
     }
 
     //   3c: FieldCallFromGenericDocument
-    //       Field is of a class which is annotated with @AppSearchDocument.
+    //       Field is of a class which is annotated with @Document.
     //       We have to convert this from a GenericDocument through the standard conversion
     //       machinery.
     private boolean tryFieldCallFromGenericDocument(
@@ -733,9 +729,9 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            mHelper.getAnnotation(element, IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 3c
+            // The propertyType doesn't have @Document annotation, this is not a type 3c
             // field.
             return false;
         }
@@ -745,14 +741,12 @@
 
         body.addStatement("$T $NConv = null", propertyType, fieldName);
         // If not null, assign
-        body.add("if ($NCopy != null) {\n", fieldName).indent();
-
-        body.addStatement("$NConv = $T.getInstance().getOrCreateFactory($T.class)"
-                        + ".fromGenericDocument($NCopy)", fieldName,
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType,
-                fieldName);
-
-        body.unindent().add("}\n");
+        body
+                .add("if ($NCopy != null) {\n", fieldName).indent()
+                .addStatement(
+                        "$NConv = $NCopy.toDocumentClass($T.class)",
+                        fieldName, fieldName, propertyType)
+                .unindent().add("}\n");
 
         method.add(body.build());
 
@@ -772,15 +766,15 @@
     }
 
     private void unpackSpecialFields(@NonNull MethodSpec.Builder method) {
-        for (AppSearchDocumentModel.SpecialField specialField :
-                AppSearchDocumentModel.SpecialField.values()) {
+        for (DocumentModel.SpecialField specialField :
+                DocumentModel.SpecialField.values()) {
             String fieldName = mModel.getSpecialFieldName(specialField);
             if (fieldName == null) {
-                continue;  // The data class doesn't have this field, so no need to unpack it.
+                continue;  // The document class doesn't have this field, so no need to unpack it.
             }
             switch (specialField) {
-                case URI:
-                    method.addStatement("String $NConv = genericDoc.getUri()", fieldName);
+                case ID:
+                    method.addStatement("String $NConv = genericDoc.getId()", fieldName);
                     break;
                 case NAMESPACE:
                     method.addStatement("String $NConv = genericDoc.getNamespace()", fieldName);
@@ -803,10 +797,10 @@
     private CodeBlock createAppSearchFieldWrite(@NonNull String fieldName) {
         switch (mModel.getFieldWriteKind(fieldName)) {
             case FIELD:
-                return CodeBlock.of("dataClass.$N = $NConv", fieldName, fieldName);
+                return CodeBlock.of("document.$N = $NConv", fieldName, fieldName);
             case SETTER:
                 String setter = mModel.getAccessorName(fieldName, /*get=*/ false);
-                return CodeBlock.of("dataClass.$N($NConv)", setter, fieldName);
+                return CodeBlock.of("document.$N($NConv)", setter, fieldName);
             default:
                 return null;  // Constructor params should already have been set
         }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
index 0488796..3be3a0d 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
@@ -17,6 +17,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 
 import com.squareup.javapoet.ClassName;
 
@@ -41,23 +42,20 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class IntrospectionHelper {
+    @VisibleForTesting
+    static final String GEN_CLASS_PREFIX = "$$__AppSearch__";
+
     static final String APPSEARCH_PKG = "androidx.appsearch.app";
     static final String APPSEARCH_EXCEPTION_PKG = "androidx.appsearch.exceptions";
     static final String APPSEARCH_EXCEPTION_SIMPLE_NAME = "AppSearchException";
-    static final String APP_SEARCH_DOCUMENT_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument";
-    static final String URI_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.Uri";
-    static final String NAMESPACE_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.Namespace";
+    static final String DOCUMENT_ANNOTATION_CLASS = "androidx.appsearch.annotation.Document";
+    static final String ID_CLASS = "androidx.appsearch.annotation.Document.Id";
+    static final String NAMESPACE_CLASS = "androidx.appsearch.annotation.Document.Namespace";
     static final String CREATION_TIMESTAMP_MILLIS_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.CreationTimestampMillis";
-    static final String TTL_MILLIS_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.TtlMillis";
-    static final String SCORE_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.Score";
-    static final String PROPERTY_CLASS =
-            "androidx.appsearch.annotation.AppSearchDocument.Property";
+            "androidx.appsearch.annotation.Document.CreationTimestampMillis";
+    static final String TTL_MILLIS_CLASS = "androidx.appsearch.annotation.Document.TtlMillis";
+    static final String SCORE_CLASS = "androidx.appsearch.annotation.Document.Score";
+    static final String PROPERTY_CLASS = "androidx.appsearch.annotation.Document.Property";
 
     final TypeMirror mCollectionType;
     final TypeMirror mListType;
@@ -124,6 +122,24 @@
         return ret;
     }
 
+    /**
+     * Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
+     * for inner class Foo.Bar.
+     */
+    public ClassName getDocumentClassFactoryForClass(String pkg, String className) {
+        String genClassName = GEN_CLASS_PREFIX + className.replace(".", "$$__");
+        return ClassName.get(pkg, genClassName);
+    }
+
+    /**
+     * Creates the name of output class. $$__AppSearch__Foo for Foo, $$__AppSearch__Foo$$__Bar
+     * for inner class Foo.Bar.
+     */
+    public ClassName getDocumentClassFactoryForClass(ClassName clazz) {
+        String className = clazz.canonicalName().substring(clazz.packageName().length() + 1);
+        return getDocumentClassFactoryForClass(clazz.packageName(), className);
+    }
+
     public ClassName getAppSearchClass(String clazz, String... nested) {
         return ClassName.get(APPSEARCH_PKG, clazz, nested);
     }
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
index eca9c5a..d3e3d1b 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
@@ -42,17 +42,17 @@
 class SchemaCodeGenerator {
     private final ProcessingEnvironment mEnv;
     private final IntrospectionHelper mHelper;
-    private final AppSearchDocumentModel mModel;
+    private final DocumentModel mModel;
 
     public static void generate(
             @NonNull ProcessingEnvironment env,
-            @NonNull AppSearchDocumentModel model,
+            @NonNull DocumentModel model,
             @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
         new SchemaCodeGenerator(env, model).generate(classBuilder);
     }
 
     private SchemaCodeGenerator(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model) {
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
         mEnv = env;
         mHelper = new IntrospectionHelper(env);
         mModel = model;
@@ -60,17 +60,17 @@
 
     private void generate(@NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
         classBuilder.addField(
-                FieldSpec.builder(String.class, "SCHEMA_TYPE")
-                        .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
+                FieldSpec.builder(String.class, "SCHEMA_NAME")
+                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                         .initializer("$S", mModel.getSchemaName())
                         .build());
 
         classBuilder.addMethod(
-                MethodSpec.methodBuilder("getSchemaType")
+                MethodSpec.methodBuilder("getSchemaName")
                         .addModifiers(Modifier.PUBLIC)
                         .returns(TypeName.get(mHelper.mStringType))
                         .addAnnotation(Override.class)
-                        .addStatement("return SCHEMA_TYPE")
+                        .addStatement("return SCHEMA_NAME")
                         .build());
 
         classBuilder.addMethod(
@@ -85,7 +85,7 @@
 
     private CodeBlock createSchemaInitializer() throws ProcessingException {
         CodeBlock.Builder codeBlock = CodeBlock.builder()
-                .add("new $T(SCHEMA_TYPE)", mHelper.getAppSearchClass("AppSearchSchema", "Builder"))
+                .add("new $T(SCHEMA_NAME)", mHelper.getAppSearchClass("AppSearchSchema", "Builder"))
                 .indent();
         for (VariableElement property : mModel.getPropertyFields().values()) {
             codeBlock.add("\n.addProperty($L)", createPropertySchema(property));
@@ -100,18 +100,11 @@
                 mHelper.getAnnotation(property, IntrospectionHelper.PROPERTY_CLASS);
         Map<String, Object> params = mHelper.getAnnotationParams(annotation);
 
-        // Start the builder for that property
-        String propertyName = mModel.getPropertyName(property);
-        CodeBlock.Builder codeBlock = CodeBlock.builder()
-                .add("new $T($S)",
-                        mHelper.getAppSearchClass("AppSearchSchema", "PropertyConfig", "Builder"),
-                        propertyName)
-                .indent();
-
         // Find the property type
         Types typeUtil = mEnv.getTypeUtils();
         TypeMirror propertyType;
         boolean repeated = false;
+        boolean isPropertyString = false;
         boolean isPropertyDocument = false;
         if (property.asType().getKind() == TypeKind.ERROR) {
             throw new ProcessingException("Property type unknown to java compiler", property);
@@ -136,42 +129,47 @@
         } else {
             propertyType = property.asType();
         }
-        ClassName propertyTypeEnum;
+        ClassName propertyClass;
         if (typeUtil.isSameType(propertyType, mHelper.mStringType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_STRING");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "StringPropertyConfig");
+            isPropertyString = true;
         } else if (typeUtil.isSameType(propertyType, mHelper.mIntegerBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mIntPrimitiveType)
                 || typeUtil.isSameType(propertyType, mHelper.mLongBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mLongPrimitiveType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_INT64");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "Int64PropertyConfig");
         } else if (typeUtil.isSameType(propertyType, mHelper.mFloatBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mFloatPrimitiveType)
                 || typeUtil.isSameType(propertyType, mHelper.mDoubleBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mDoublePrimitiveType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_DOUBLE");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "DoublePropertyConfig");
         } else if (typeUtil.isSameType(propertyType, mHelper.mBooleanBoxType)
                 || typeUtil.isSameType(propertyType, mHelper.mBooleanPrimitiveType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_BOOLEAN");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "BooleanPropertyConfig");
         } else if (typeUtil.isSameType(propertyType, mHelper.mBytePrimitiveArrayType)
                 || typeUtil.isSameType(propertyType, mHelper.mByteBoxArrayType)) {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_BYTES");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "BytesPropertyConfig");
         } else {
-            propertyTypeEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "DATA_TYPE_DOCUMENT");
+            propertyClass = mHelper.getAppSearchClass("AppSearchSchema", "DocumentPropertyConfig");
             isPropertyDocument = true;
         }
-        codeBlock.add("\n.setDataType($T)", propertyTypeEnum);
 
+        // Start the builder for the property
+        String propertyName = mModel.getPropertyName(property);
+        CodeBlock.Builder codeBlock = CodeBlock.builder();
         if (isPropertyDocument) {
-            codeBlock.add("\n.setSchemaType($T.getInstance()"
-                    + ".getOrCreateFactory($T.class).getSchemaType())",
-                    mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
+            ClassName documentClass = (ClassName) ClassName.get(propertyType);
+            ClassName documentFactoryClass = mHelper.getDocumentClassFactoryForClass(documentClass);
+            codeBlock.add(
+                    "new $T($S, $T.SCHEMA_NAME)",
+                    propertyClass.nestedClass("Builder"),
+                    propertyName,
+                    documentFactoryClass);
+        } else {
+            codeBlock.add("new $T($S)", propertyClass.nestedClass("Builder"), propertyName);
         }
+        codeBlock.indent();
+
         // Find property cardinality
         ClassName cardinalityEnum;
         if (repeated) {
@@ -186,42 +184,45 @@
         }
         codeBlock.add("\n.setCardinality($T)", cardinalityEnum);
 
-        // Find tokenizer type
-        int tokenizerType = Integer.parseInt(params.get("tokenizerType").toString());
-        if (Integer.parseInt(params.get("indexingType").toString()) == 0) {
-            //TODO(b/171857731) remove this hack after apply to Icing lib's change.
-            tokenizerType = 0;
-        }
-        ClassName tokenizerEnum;
-        if (tokenizerType == 0 || isPropertyDocument) {  // TOKENIZER_TYPE_NONE
-            //It is only valid for tokenizer_type to be 'NONE' if the data type is
-            // {@link PropertyConfig#DATA_TYPE_DOCUMENT}.
-            tokenizerEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "TOKENIZER_TYPE_NONE");
-        } else if (tokenizerType == 1) {  // TOKENIZER_TYPE_PLAIN
-            tokenizerEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "TOKENIZER_TYPE_PLAIN");
-        } else {
-            throw new ProcessingException("Unknown tokenizer type " + tokenizerType, property);
-        }
-        codeBlock.add("\n.setTokenizerType($T)", tokenizerEnum);
+        if (isPropertyString) {
+            // Find tokenizer type
+            int tokenizerType = Integer.parseInt(params.get("tokenizerType").toString());
+            if (Integer.parseInt(params.get("indexingType").toString()) == 0) {
+                //TODO(b/171857731) remove this hack after apply to Icing lib's change.
+                tokenizerType = 0;
+            }
+            ClassName tokenizerEnum;
+            if (tokenizerType == 0) {  // TOKENIZER_TYPE_NONE
+                tokenizerEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_NONE");
+            } else if (tokenizerType == 1) {  // TOKENIZER_TYPE_PLAIN
+                tokenizerEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_PLAIN");
+            } else {
+                throw new ProcessingException("Unknown tokenizer type " + tokenizerType, property);
+            }
+            codeBlock.add("\n.setTokenizerType($T)", tokenizerEnum);
 
-        // Find indexing type
-        int indexingType = Integer.parseInt(params.get("indexingType").toString());
-        ClassName indexingEnum;
-        if (indexingType == 0) {  // INDEXING_TYPE_NONE
-            indexingEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "INDEXING_TYPE_NONE");
-        } else if (indexingType == 1) {  // INDEXING_TYPE_EXACT_TERMS
-            indexingEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "INDEXING_TYPE_EXACT_TERMS");
-        } else if (indexingType == 2) {  // INDEXING_TYPE_PREFIXES
-            indexingEnum = mHelper.getAppSearchClass(
-                    "AppSearchSchema", "PropertyConfig", "INDEXING_TYPE_PREFIXES");
-        } else {
-            throw new ProcessingException("Unknown indexing type " + indexingType, property);
+            // Find indexing type
+            int indexingType = Integer.parseInt(params.get("indexingType").toString());
+            ClassName indexingEnum;
+            if (indexingType == 0) {  // INDEXING_TYPE_NONE
+                indexingEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "INDEXING_TYPE_NONE");
+            } else if (indexingType == 1) {  // INDEXING_TYPE_EXACT_TERMS
+                indexingEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "INDEXING_TYPE_EXACT_TERMS");
+            } else if (indexingType == 2) {  // INDEXING_TYPE_PREFIXES
+                indexingEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "INDEXING_TYPE_PREFIXES");
+            } else {
+                throw new ProcessingException("Unknown indexing type " + indexingType, property);
+            }
+            codeBlock.add("\n.setIndexingType($T)", indexingEnum);
+
+        } else if (isPropertyDocument) {
+            // TODO(b/177572431): Apply setIndexNestedProperties here
         }
-        codeBlock.add("\n.setIndexingType($T)", indexingEnum);
 
         // Done!
         codeBlock.add("\n.build()");
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
index b30d907..c7e5c83 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
@@ -39,23 +39,24 @@
 import javax.lang.model.util.Types;
 
 /**
- * Generates java code for a translator from a data class to a
+ * Generates java code for a translator from an instance of a class annotated with
+ * {@link androidx.appsearch.annotation.Document} into a
  * {@link androidx.appsearch.app.GenericDocument}.
  */
 class ToGenericDocumentCodeGenerator {
     private final ProcessingEnvironment mEnv;
     private final IntrospectionHelper mHelper;
-    private final AppSearchDocumentModel mModel;
+    private final DocumentModel mModel;
 
     public static void generate(
             @NonNull ProcessingEnvironment env,
-            @NonNull AppSearchDocumentModel model,
+            @NonNull DocumentModel model,
             @NonNull TypeSpec.Builder classBuilder) throws ProcessingException {
         new ToGenericDocumentCodeGenerator(env, model).generate(classBuilder);
     }
 
     private ToGenericDocumentCodeGenerator(
-            @NonNull ProcessingEnvironment env, @NonNull AppSearchDocumentModel model) {
+            @NonNull ProcessingEnvironment env, @NonNull DocumentModel model) {
         mEnv = env;
         mHelper = new IntrospectionHelper(env);
         mModel = model;
@@ -72,17 +73,19 @@
                 .addModifiers(Modifier.PUBLIC)
                 .returns(mHelper.getAppSearchClass("GenericDocument"))
                 .addAnnotation(Override.class)
-                .addParameter(classType, "dataClass")
+                .addParameter(classType, "document")
                 .addException(mHelper.getAppSearchExceptionClass());
 
-        // Construct a new GenericDocument.Builder with the schema type and URI
-        methodBuilder.addStatement("$T builder =\nnew $T<>($L, SCHEMA_TYPE)",
+        // Construct a new GenericDocument.Builder with the namespace, id, and schema type
+        methodBuilder.addStatement("$T builder =\nnew $T<>($L, $L, SCHEMA_NAME)",
                 ParameterizedTypeName.get(
                         mHelper.getAppSearchClass("GenericDocument", "Builder"),
                         WildcardTypeName.subtypeOf(Object.class)),
                 mHelper.getAppSearchClass("GenericDocument", "Builder"),
                 createAppSearchFieldRead(
-                        mModel.getSpecialFieldName(AppSearchDocumentModel.SpecialField.URI)));
+                        mModel.getSpecialFieldName(DocumentModel.SpecialField.NAMESPACE)),
+                createAppSearchFieldRead(
+                        mModel.getSpecialFieldName(DocumentModel.SpecialField.ID)));
 
         setSpecialFields(methodBuilder);
 
@@ -96,7 +99,7 @@
     }
 
     /**
-     * Converts a field from a data class into a format suitable for one of the
+     * Converts a field from a document class into a format suitable for one of the
      * {@link androidx.appsearch.app.GenericDocument.Builder#setProperty} methods.
      */
     private void fieldToGenericDoc(
@@ -117,7 +120,7 @@
         //       this.
         //
         //   1c: CollectionForLoopCallToGenericDocument
-        //       Collection contains a class which is annotated with @AppSearchDocument.
+        //       Collection contains a class which is annotated with @Document.
         //       We have to convert this into an array of GenericDocument[], by reading each element
         //       one-by-one and converting it through the standard conversion machinery.
         //
@@ -141,7 +144,7 @@
         //       We can directly use this field with no conversion.
         //
         //   2c: ArrayForLoopCallToGenericDocument
-        //       Array is of a class which is annotated with @AppSearchDocument.
+        //       Array is of a class which is annotated with @Document.
         //       We have to convert this into an array of GenericDocument[], by reading each element
         //       one-by-one and converting it through the standard conversion machinery.
         //
@@ -164,7 +167,7 @@
         //       We can use this field directly without testing for null.
         //
         //   3c: FieldCallToGenericDocument
-        //       Field is of a class which is annotated with @AppSearchDocument.
+        //       Field is of a class which is annotated with @Document.
         //       We have to convert this into a GenericDocument through the standard conversion
         //       machinery.
         //
@@ -209,7 +212,7 @@
         if (!tryCollectionForLoopAssign(body, fieldName, propertyName, propertyType)           // 1a
                 && !tryCollectionCallToArray(body, fieldName, propertyName, propertyType)      // 1b
                 && !tryCollectionForLoopCallToGenericDocument(
-                        body, fieldName, propertyName, propertyType)) {                        // 1c
+                body, fieldName, propertyName, propertyType)) {                        // 1c
             // Scenario 1x
             throw new ProcessingException(
                     "Unhandled out property type (1x): " + property.asType().toString(), property);
@@ -304,7 +307,7 @@
     }
 
     //   1c: CollectionForLoopCallToGenericDocument
-    //       Collection contains a class which is annotated with @AppSearchDocument.
+    //       Collection contains a class which is annotated with @Document.
     //       We have to convert this into an array of GenericDocument[], by reading each element
     //       one-by-one and converting it through the standard conversion machinery.
     private boolean tryCollectionForLoopCallToGenericDocument(
@@ -322,28 +325,28 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            mHelper.getAnnotation(element, IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 1c
+            // The propertyType doesn't have @Document annotation, this is not a type 1c
             // list.
             return false;
         }
 
-        body.addStatement("GenericDocument[] $NConv = new GenericDocument[$NCopy.size()]",
+        body.addStatement(
+                "GenericDocument[] $NConv = new GenericDocument[$NCopy.size()]",
                 fieldName, fieldName);
-        body.addStatement("$T factory = $T.getInstance().getOrCreateFactory($T.class)",
-                ParameterizedTypeName.get(mHelper.getAppSearchClass("DataClassFactory"),
-                        TypeName.get(propertyType)),
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
-
         body.addStatement("int i = 0");
-        body.add("for ($T item : $NCopy) {\n", propertyType, fieldName).indent();
-        body.addStatement("$NConv[i++] = factory.toGenericDocument(item)", fieldName);
+        body
+                .add("for ($T item : $NCopy) {\n", propertyType, fieldName).indent()
+                .addStatement(
+                        "$NConv[i++] = $T.fromDocumentClass(item)",
+                        fieldName, mHelper.getAppSearchClass("GenericDocument"))
+                .unindent().add("}\n");
 
-        body.unindent().add("}\n");
-
-        body.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
-                .unindent().add("}\n");   //  if ($NCopy != null) {
+        body
+                .addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
+                .unindent()
+                .add("}\n");   //  if ($NCopy != null) {
 
         method.add(body.build());
         return true;
@@ -379,7 +382,7 @@
         if (!tryArrayForLoopAssign(body, fieldName, propertyName, propertyType)                // 2a
                 && !tryArrayUseDirectly(body, fieldName, propertyName, propertyType)           // 2b
                 && !tryArrayForLoopCallToGenericDocument(
-                        body, fieldName, propertyName, propertyType)) {                        // 2c
+                body, fieldName, propertyName, propertyType)) {                        // 2c
             // Scenario 2x
             throw new ProcessingException(
                     "Unhandled out property type (2x): " + property.asType().toString(), property);
@@ -483,7 +486,7 @@
     }
 
     //   2c: ArrayForLoopCallToGenericDocument
-    //       Array is of a class which is annotated with @AppSearchDocument.
+    //       Array is of a class which is annotated with @Document.
     //       We have to convert this into an array of GenericDocument[], by reading each element
     //       one-by-one and converting it through the standard conversion machinery.
     private boolean tryArrayForLoopCallToGenericDocument(
@@ -501,23 +504,22 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            mHelper.getAnnotation(element, IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 1c
+            // The propertyType doesn't have @Document annotation, this is not a type 1c
             // list.
             return false;
         }
 
-        body.addStatement("GenericDocument[] $NConv = new GenericDocument[$NCopy.length]",
+        body.addStatement(
+                "GenericDocument[] $NConv = new GenericDocument[$NCopy.length]",
                 fieldName, fieldName);
-        body.addStatement("$T factory = $T.getInstance().getOrCreateFactory($T.class)",
-                ParameterizedTypeName.get(mHelper.getAppSearchClass("DataClassFactory"),
-                        TypeName.get(propertyType)),
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType);
-        body.add("for (int i = 0; i < $NConv.length; i++) {\n", fieldName).indent();
-        body.addStatement("$NConv[i] = factory.toGenericDocument($NCopy[i])",
-                fieldName, fieldName);
-        body.unindent().add("}\n");
+        body
+                .add("for (int i = 0; i < $NConv.length; i++) {\n", fieldName).indent()
+                .addStatement(
+                        "$NConv[i] = $T.fromDocumentClass($NCopy[i])",
+                        fieldName, mHelper.getAppSearchClass("GenericDocument"), fieldName)
+                .unindent().add("}\n");
 
         body.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName)
                 .unindent().add("}\n");    //  if ($NCopy != null) {
@@ -540,9 +542,9 @@
         if (!tryFieldUseDirectlyWithNullCheck(
                 body, fieldName, propertyName, property.asType())  // 3a
                 && !tryFieldUseDirectlyWithoutNullCheck(
-                        body, fieldName, propertyName, property.asType())  // 3b
+                body, fieldName, propertyName, property.asType())  // 3b
                 && !tryFieldCallToGenericDocument(
-                        body, fieldName, propertyName, property.asType())) {  // 3c
+                body, fieldName, propertyName, property.asType())) {  // 3c
             // Scenario 3x
             throw new ProcessingException(
                     "Unhandled out property type (3x): " + property.asType().toString(), property);
@@ -627,7 +629,7 @@
     }
 
     //   3c: FieldCallToGenericDocument
-    //       Field is of a class which is annotated with @AppSearchDocument.
+    //       Field is of a class which is annotated with @Document.
     //       We have to convert this into a GenericDocument through the standard conversion
     //       machinery.
     private boolean tryFieldCallToGenericDocument(
@@ -643,47 +645,39 @@
             return false;
         }
         try {
-            mHelper.getAnnotation(element, IntrospectionHelper.APP_SEARCH_DOCUMENT_CLASS);
+            mHelper.getAnnotation(element, IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS);
         } catch (ProcessingException e) {
-            // The propertyType doesn't have @AppSearchDocument annotation, this is not a type 3c
+            // The propertyType doesn't have @Document annotation, this is not a type 3c
             // field.
             return false;
         }
-        method.addStatement("$T $NCopy = $L", propertyType, propertyName,
-                createAppSearchFieldRead(fieldName));
+        method.addStatement(
+                "$T $NCopy = $L", propertyType, fieldName, createAppSearchFieldRead(fieldName));
 
-        method.add("if ($NCopy != null) {\n", propertyName).indent();
+        method.add("if ($NCopy != null) {\n", fieldName).indent();
 
-        method.addStatement("GenericDocument $NConv = $T.getInstance().getOrCreateFactory($T.class)"
-                        + ".toGenericDocument($NCopy)", fieldName,
-                mHelper.getAppSearchClass("DataClassFactoryRegistry"), propertyType,
-                propertyName);
-        method.addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName);
+        method
+                .addStatement(
+                        "GenericDocument $NConv = $T.fromDocumentClass($NCopy)",
+                        fieldName, mHelper.getAppSearchClass("GenericDocument"), fieldName)
+                .addStatement("builder.setPropertyDocument($S, $NConv)", propertyName, fieldName);
 
         method.unindent().add("}\n");
         return true;
     }
 
     private void setSpecialFields(MethodSpec.Builder method) {
-        for (AppSearchDocumentModel.SpecialField specialField :
-                AppSearchDocumentModel.SpecialField.values()) {
+        for (DocumentModel.SpecialField specialField :
+                DocumentModel.SpecialField.values()) {
             String fieldName = mModel.getSpecialFieldName(specialField);
             if (fieldName == null) {
-                continue;  // The data class doesn't have this field, so no need to set it.
+                continue;  // The document class doesn't have this field, so no need to set it.
             }
             switch (specialField) {
-                case URI:
+                case ID:
                     break;  // Always provided to builder constructor; cannot be set separately.
                 case NAMESPACE:
-                    method.addCode(CodeBlock.builder()
-                            .addStatement(
-                                    "String $NCopy = $L",
-                                    fieldName, createAppSearchFieldRead(fieldName))
-                            .add("if ($NCopy != null) {\n", fieldName).indent()
-                            .addStatement("builder.setNamespace($NCopy)", fieldName)
-                            .unindent().add("}\n")
-                            .build());
-                    break;
+                    break;  // Always provided to builder constructor; cannot be set separately.
                 case CREATION_TIMESTAMP_MILLIS:
                     method.addStatement(
                             "builder.setCreationTimestampMillis($L)",
@@ -704,10 +698,10 @@
     private CodeBlock createAppSearchFieldRead(@NonNull String fieldName) {
         switch (mModel.getFieldReadKind(fieldName)) {
             case FIELD:
-                return CodeBlock.of("dataClass.$N", fieldName);
+                return CodeBlock.of("document.$N", fieldName);
             case GETTER:
                 String getter = mModel.getAccessorName(fieldName, /*get=*/ true);
-                return CodeBlock.of("dataClass.$N()", getter);
+                return CodeBlock.of("document.$N()", getter);
         }
         return null;
     }
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index 1693cd6..ddbe7b6a 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -57,7 +57,7 @@
     @Test
     public void testNonClass() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public interface Gift {}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "annotation on something other than a class");
@@ -68,7 +68,7 @@
         Compilation compilation = compile(
                 "Wrapper",
                 "public class Wrapper {\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "private class Gift {}\n"
                         + "}  // Wrapper\n"
         );
@@ -77,47 +77,62 @@
     }
 
     @Test
-    public void testNoUri() {
+    public void testNoId() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
-                        + "public class Gift {}\n");
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "must have exactly one field annotated with @Uri");
+                "must have exactly one field annotated with @Id");
     }
 
     @Test
-    public void testManyUri() {
+    public void testManyIds() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri1;\n"
-                        + "  @AppSearchDocument.Uri String uri2;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id1;\n"
+                        + "  @Document.Id String id2;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
-                "contains multiple fields annotated @Uri");
+                "contains multiple fields annotated @Id");
     }
 
     @Test
     public void testManyCreationTimestamp() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.CreationTimestampMillis long ts1;\n"
-                        + "  @AppSearchDocument.CreationTimestampMillis long ts2;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.CreationTimestampMillis long ts1;\n"
+                        + "  @Document.CreationTimestampMillis long ts2;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "contains multiple fields annotated @CreationTimestampMillis");
     }
 
     @Test
+    public void testNoNamespace() {
+        Compilation compilation = compile(
+                "@Document\n"
+                        + "public class Gift {\n"
+                        + "  @Document.Id String id;\n"
+                        + "}\n");
+        CompilationSubject.assertThat(compilation).hadErrorContaining(
+                "must have exactly one field annotated with @Namespace");
+    }
+
+    @Test
     public void testManyNamespace() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Namespace String ns1;\n"
-                        + "  @AppSearchDocument.Namespace String ns2;\n"
+                        + "  @Document.Namespace String ns1;\n"
+                        + "  @Document.Namespace String ns2;\n"
+                        + "  @Document.Id String id;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "contains multiple fields annotated @Namespace");
@@ -126,11 +141,12 @@
     @Test
     public void testManyTtlMillis() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.TtlMillis long ts1;\n"
-                        + "  @AppSearchDocument.TtlMillis long ts2;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.TtlMillis long ts1;\n"
+                        + "  @Document.TtlMillis long ts2;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "contains multiple fields annotated @TtlMillis");
@@ -139,11 +155,12 @@
     @Test
     public void testManyScore() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Score int score1;\n"
-                        + "  @AppSearchDocument.Score int score2;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Score int score1;\n"
+                        + "  @Document.Score int score2;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "contains multiple fields annotated @Score");
@@ -152,10 +169,11 @@
     @Test
     public void testPropertyOnField() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int getPrice() { return 0; }\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property private int getPrice() { return 0; }\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "annotation type not applicable to this kind of declaration");
@@ -164,10 +182,11 @@
     @Test
     public void testCantRead_noGetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property private int price;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "Field cannot be read: it is private and we failed to find a suitable getter "
@@ -177,10 +196,11 @@
     @Test
     public void testCantRead_privateGetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property private int price;\n"
                         + "  private int getPrice() { return 0; }\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
@@ -193,10 +213,11 @@
     @Test
     public void testCantRead_wrongParamGetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property private int price;\n"
                         + "  int getPrice(int n) { return 0; }\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
@@ -209,10 +230,11 @@
     @Test
     public void testRead_MultipleGetters() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property private int price;\n"
                         + "  int getPrice(int n) { return 0; }\n"
                         + "  int getPrice() { return 0; }\n"
                         + "  void setPrice(int n) {}\n"
@@ -224,10 +246,11 @@
     @Test
     public void testCantWrite_noSetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property private int price;\n"
                         + "  int getPrice() { return price; }\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
@@ -242,10 +265,11 @@
     @Test
     public void testCantWrite_privateSetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property private int price;\n"
                         + "  int getPrice() { return price; }\n"
                         + "  private void setPrice(int n) {}\n"
                         + "}\n");
@@ -263,10 +287,11 @@
     @Test
     public void testCantWrite_wrongParamSetter() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property private int price;\n"
                         + "  int getPrice() { return price; }\n"
                         + "  void setPrice() {}\n"
                         + "}\n");
@@ -284,10 +309,11 @@
     @Test
     public void testWrite_multipleSetters() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property private int price;\n"
                         + "  int getPrice() { return price; }\n"
                         + "  void setPrice() {}\n"
                         + "  void setPrice(int n) {}\n"
@@ -299,11 +325,12 @@
     @Test
     public void testWrite_privateConstructor() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
                         + "  private Gift() {}\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property int price;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "Failed to find any suitable constructors to build this class");
@@ -314,29 +341,32 @@
     @Test
     public void testWrite_constructorMissingParams() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
                         + "  Gift(int price) {}\n"
-                        + "  @AppSearchDocument.Uri final String uri;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id final String id;\n"
+                        + "  @Document.Property int price;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "Failed to find any suitable constructors to build this class");
         CompilationSubject.assertThat(compilation).hadWarningContaining(
-                "doesn't have parameters for the following fields: [uri]");
+                "doesn't have parameters for the following fields: [id]");
     }
 
     @Test
     public void testWrite_constructorExtraParams() {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  Gift(int price, String uri, int unknownParam) {\n"
-                        + "    this.uri = uri;\n"
+                        + "  Gift(int price, String id, String namespace, int unknownParam) {\n"
+                        + "    this.id = id;\n"
+                        + "    this.namespace = namespace;\n"
                         + "    this.price = price;\n"
                         + "  }\n"
-                        + "  @AppSearchDocument.Uri final String uri;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id final String id;\n"
+                        + "  @Document.Property int price;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
                 "Failed to find any suitable constructors to build this class");
@@ -348,17 +378,19 @@
     @Test
     public void testSuccessSimple() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  Gift(boolean dog, String uri) {\n"
-                        + "    this.uri = uri;\n"
+                        + "  Gift(boolean dog, String id, String namespace) {\n"
+                        + "    this.id = id;\n"
+                        + "    this.namespace = namespace;\n"
                         + "    this.dog = dog;\n"
                         + "  }\n"
-                        + "  @AppSearchDocument.Uri final String uri;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
-                        + "  @AppSearchDocument.Property boolean cat = false;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id final String id;\n"
+                        + "  @Document.Property int price;\n"
+                        + "  @Document.Property boolean cat = false;\n"
                         + "  public void setCat(boolean cat) {}\n"
-                        + "  @AppSearchDocument.Property private final boolean dog;\n"
+                        + "  @Document.Property private final boolean dog;\n"
                         + "  public boolean getDog() { return dog; }\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
@@ -368,9 +400,10 @@
     @Test
     public void testDifferentTypeName() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument(name=\"DifferentType\")\n"
+                "@Document(name=\"DifferentType\")\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
@@ -380,13 +413,14 @@
     public void testRepeatedFields() throws Exception {
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property List<String> listOfString;\n"
-                        + "  @AppSearchDocument.Property Collection<Integer> setOfInt;\n"
-                        + "  @AppSearchDocument.Property byte[][] repeatedByteArray;\n"
-                        + "  @AppSearchDocument.Property byte[] byteArray;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property List<String> listOfString;\n"
+                        + "  @Document.Property Collection<Integer> setOfInt;\n"
+                        + "  @Document.Property byte[][] repeatedByteArray;\n"
+                        + "  @Document.Property byte[] byteArray;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
@@ -396,13 +430,14 @@
     public void testCardinality() throws Exception {
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(required=true) List<String> repeatReq;\n"
-                        + " @AppSearchDocument.Property(required=false) List<String> repeatNoReq;\n"
-                        + "  @AppSearchDocument.Property(required=true) Float req;\n"
-                        + "  @AppSearchDocument.Property(required=false) Float noReq;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property(required=true) List<String> repeatReq;\n"
+                        + "  @Document.Property(required=false) List<String> repeatNoReq;\n"
+                        + "  @Document.Property(required=true) Float req;\n"
+                        + "  @Document.Property(required=false) Float noReq;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
@@ -413,17 +448,18 @@
         // TODO(b/156296904): Uncomment Gift in this test when it's supported
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property String stringProp;\n"
-                        + "  @AppSearchDocument.Property Integer integerProp;\n"
-                        + "  @AppSearchDocument.Property Long longProp;\n"
-                        + "  @AppSearchDocument.Property Float floatProp;\n"
-                        + "  @AppSearchDocument.Property Double doubleProp;\n"
-                        + "  @AppSearchDocument.Property Boolean booleanProp;\n"
-                        + "  @AppSearchDocument.Property byte[] bytesProp;\n"
-                        //+ "  @AppSearchDocument.Property Gift documentProp;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property String stringProp;\n"
+                        + "  @Document.Property Integer integerProp;\n"
+                        + "  @Document.Property Long longProp;\n"
+                        + "  @Document.Property Float floatProp;\n"
+                        + "  @Document.Property Double doubleProp;\n"
+                        + "  @Document.Property Boolean booleanProp;\n"
+                        + "  @Document.Property byte[] bytesProp;\n"
+                        //+ "  @Document.Property Gift documentProp;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
@@ -435,11 +471,12 @@
         // by using the integer constants directly.
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(tokenizerType=0) String tokNone;\n"
-                        + "  @AppSearchDocument.Property(tokenizerType=1) String tokPlain;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property(tokenizerType=0) String tokNone;\n"
+                        + "  @Document.Property(tokenizerType=1) String tokPlain;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
@@ -451,10 +488,11 @@
         // by using the integer constants directly.
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(indexingType=1, tokenizerType=100)\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property(indexingType=1, tokenizerType=100)\n"
                         + "  String str;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining("Unknown tokenizer type 100");
@@ -466,12 +504,13 @@
         // by using the integer constants directly.
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(indexingType=0) String indexNone;\n"
-                        + "  @AppSearchDocument.Property(indexingType=1) String indexExact;\n"
-                        + "  @AppSearchDocument.Property(indexingType=2) String indexPrefix;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property(indexingType=0) String indexNone;\n"
+                        + "  @Document.Property(indexingType=1) String indexExact;\n"
+                        + "  @Document.Property(indexingType=2) String indexPrefix;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
@@ -483,10 +522,11 @@
         // by using the integer constants directly.
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(indexingType=100, tokenizerType=1)\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property(indexingType=100, tokenizerType=1)\n"
                         + "  String str;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining("Unknown indexing type 100");
@@ -496,10 +536,11 @@
     public void testPropertyName() throws Exception {
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Property(name=\"newName\") String oldName;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.Property(name=\"newName\") String oldName;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
@@ -511,9 +552,10 @@
         Compilation compilation = compile(
                 "import java.util.*;\n"
                         + "import androidx.appsearch.app.GenericDocument;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
                         + "\n"
                         + "  // Collections\n"
                         + "  @Property Collection<Long> collectLong;\n"         // 1a
@@ -564,9 +606,10 @@
     public void testToGenericDocument_invalidTypes() {
         Compilation compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
                         + "  @Property Collection<Byte[]> collectBoxByteArr;\n" // 1x
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
@@ -574,9 +617,10 @@
 
         compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
                         + "  @Property Collection<Byte> collectByte;\n" // 1x
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
@@ -584,9 +628,10 @@
 
         compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
                         + "  @Property Collection<Object> collectObject;\n" // 1x
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
@@ -594,9 +639,10 @@
 
         compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
                         + "  @Property Byte[][] arrBoxByteArr;\n" // 2x
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
@@ -604,9 +650,10 @@
 
         compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
                         + "  @Property Object[] arrObject;\n" // 2x
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
@@ -614,9 +661,10 @@
 
         compilation = compile(
                 "import java.util.*;\n"
-                        + "@AppSearchDocument\n"
+                        + "@Document\n"
                         + "public class Gift {\n"
-                        + "  @Uri String uri;\n"
+                        + "  @Namespace String namespace;\n"
+                        + "  @Id String id;\n"
                         + "  @Property Object object;\n" // 3x
                         + "}\n");
         CompilationSubject.assertThat(compilation).hadErrorContaining(
@@ -626,14 +674,14 @@
     @Test
     public void testAllSpecialFields_field() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Namespace String namespace;\n"
-                        + "  @AppSearchDocument.CreationTimestampMillis long creationTs;\n"
-                        + "  @AppSearchDocument.TtlMillis int ttlMs;\n"
-                        + "  @AppSearchDocument.Property int price;\n"
-                        + "  @AppSearchDocument.Score int score;\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id String id;\n"
+                        + "  @Document.CreationTimestampMillis long creationTs;\n"
+                        + "  @Document.TtlMillis int ttlMs;\n"
+                        + "  @Document.Property int price;\n"
+                        + "  @Document.Score int score;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden("Gift.java");
@@ -642,15 +690,20 @@
     @Test
     public void testAllSpecialFields_getter() throws Exception {
         Compilation compilation = compile(
-                "@AppSearchDocument\n"
+                "@Document\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument.Uri private String uri;\n"
-                        + "  @AppSearchDocument.Score private int score;\n"
-                        + "  @AppSearchDocument.CreationTimestampMillis private long creationTs;\n"
-                        + "  @AppSearchDocument.TtlMillis private int ttlMs;\n"
-                        + "  @AppSearchDocument.Property private int price;\n"
-                        + "  public String getUri() { return uri; }\n"
-                        + "  public void setUri(String uri) { this.uri = uri; }\n"
+                        + "  @Document.Namespace String namespace;\n"
+                        + "  @Document.Id private String id;\n"
+                        + "  @Document.Score private int score;\n"
+                        + "  @Document.CreationTimestampMillis private long creationTs;\n"
+                        + "  @Document.TtlMillis private int ttlMs;\n"
+                        + "  @Document.Property private int price;\n"
+                        + "  public String getId() { return id; }\n"
+                        + "  public void setId(String id) { this.id = id; }\n"
+                        + "  public String getNamespace() { return namespace; }\n"
+                        + "  public void setNamespace(String namespace) {\n"
+                        + "    this.namespace = namespace;\n"
+                        + "  }\n"
                         + "  public int getScore() { return score; }\n"
                         + "  public void setScore(int score) { this.score = score; }\n"
                         + "  public long getCreationTs() { return creationTs; }\n"
@@ -672,9 +725,10 @@
                 "import java.util.*;\n"
                         + "import androidx.appsearch.app.GenericDocument;\n"
                         + "public class Gift {\n"
-                        + "  @AppSearchDocument\n"
+                        + "  @Document\n"
                         + "  public static class InnerGift{\n"
-                        + "    @AppSearchDocument.Uri String uri;\n"
+                        + "    @Document.Namespace String namespace;\n"
+                        + "    @Document.Id String id;\n"
                         + "    @Property String[] arrString;\n"        // 2b
                         + "  }\n"
                         + "}\n");
@@ -688,8 +742,8 @@
 
     private Compilation compile(String classSimpleName, String classBody) {
         String src = "package com.example.appsearch;\n"
-                + "import androidx.appsearch.annotation.AppSearchDocument;\n"
-                + "import androidx.appsearch.annotation.AppSearchDocument.*;\n"
+                + "import androidx.appsearch.annotation.Document;\n"
+                + "import androidx.appsearch.annotation.Document.*;\n"
                 + classBody;
         JavaFileObject jfo = JavaFileObjects.forSourceString(
                 "com.example.appsearch." + classSimpleName,
@@ -722,7 +776,8 @@
 
         // Get the actual file contents
         File actualPackageDir = new File(mGenFilesDir, "com/example/appsearch");
-        File actualPath = new File(actualPackageDir, CodeGenerator.GEN_CLASS_PREFIX + className);
+        File actualPath =
+                new File(actualPackageDir, IntrospectionHelper.GEN_CLASS_PREFIX + className);
         Truth.assertWithMessage("Path " + actualPath + " is not a file")
                 .that(actualPath.isFile()).isTrue();
         String actual = Files.asCharSource(actualPath, StandardCharsets.UTF_8).read();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
index b256fa5..27e36c1 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
@@ -1,7 +1,7 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Boolean;
@@ -11,92 +11,75 @@
 import java.lang.Long;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("stringProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("stringProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("integerProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("integerProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("longProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("longProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("floatProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("floatProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("doubleProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("doubleProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("booleanProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("booleanProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("bytesProp")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytesProp")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String stringPropCopy = dataClass.stringProp;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String stringPropCopy = document.stringProp;
     if (stringPropCopy != null) {
       builder.setPropertyString("stringProp", stringPropCopy);
     }
-    Integer integerPropCopy = dataClass.integerProp;
+    Integer integerPropCopy = document.integerProp;
     if (integerPropCopy != null) {
       builder.setPropertyLong("integerProp", integerPropCopy);
     }
-    Long longPropCopy = dataClass.longProp;
+    Long longPropCopy = document.longProp;
     if (longPropCopy != null) {
       builder.setPropertyLong("longProp", longPropCopy);
     }
-    Float floatPropCopy = dataClass.floatProp;
+    Float floatPropCopy = document.floatProp;
     if (floatPropCopy != null) {
       builder.setPropertyDouble("floatProp", floatPropCopy);
     }
-    Double doublePropCopy = dataClass.doubleProp;
+    Double doublePropCopy = document.doubleProp;
     if (doublePropCopy != null) {
       builder.setPropertyDouble("doubleProp", doublePropCopy);
     }
-    Boolean booleanPropCopy = dataClass.booleanProp;
+    Boolean booleanPropCopy = document.booleanProp;
     if (booleanPropCopy != null) {
       builder.setPropertyBoolean("booleanProp", booleanPropCopy);
     }
-    byte[] bytesPropCopy = dataClass.bytesProp;
+    byte[] bytesPropCopy = document.bytesProp;
     if (bytesPropCopy != null) {
       builder.setPropertyBytes("bytesProp", bytesPropCopy);
     }
@@ -105,7 +88,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] stringPropCopy = genericDoc.getPropertyStringArray("stringProp");
     String stringPropConv = null;
     if (stringPropCopy != null && stringPropCopy.length != 0) {
@@ -141,15 +125,16 @@
     if (bytesPropCopy != null && bytesPropCopy.length != 0) {
       bytesPropConv = bytesPropCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.stringProp = stringPropConv;
-    dataClass.integerProp = integerPropConv;
-    dataClass.longProp = longPropConv;
-    dataClass.floatProp = floatPropConv;
-    dataClass.doubleProp = doublePropConv;
-    dataClass.booleanProp = booleanPropConv;
-    dataClass.bytesProp = bytesPropConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.stringProp = stringPropConv;
+    document.integerProp = integerPropConv;
+    document.longProp = longPropConv;
+    document.floatProp = floatPropConv;
+    document.doubleProp = doublePropConv;
+    document.booleanProp = booleanPropConv;
+    document.bytesProp = bytesPropConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
index e315345..5e45dd0 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_field.JAVA
@@ -1,62 +1,57 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String namespaceCopy = dataClass.namespace;
-    if (namespaceCopy != null) {
-      builder.setNamespace(namespaceCopy);
-    }
-    builder.setCreationTimestampMillis(dataClass.creationTs);
-    builder.setTtlMillis(dataClass.ttlMs);
-    builder.setScore(dataClass.score);
-    builder.setPropertyLong("price", dataClass.price);
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setCreationTimestampMillis(document.creationTs);
+    builder.setTtlMillis(document.ttlMs);
+    builder.setScore(document.score);
+    builder.setPropertyLong("price", document.price);
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
     String namespaceConv = genericDoc.getNamespace();
     long creationTsConv = genericDoc.getCreationTimestampMillis();
     long ttlMsConv = genericDoc.getTtlMillis();
     int scoreConv = genericDoc.getScore();
     int priceConv = (int) genericDoc.getPropertyLong("price");
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.namespace = namespaceConv;
-    dataClass.creationTs = creationTsConv;
-    dataClass.ttlMs = ttlMsConv;
-    dataClass.price = priceConv;
-    dataClass.score = scoreConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.creationTs = creationTsConv;
+    document.ttlMs = ttlMsConv;
+    document.price = priceConv;
+    document.score = scoreConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
index 1cf9253..4956ecd9 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_getter.JAVA
@@ -1,56 +1,57 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.getUri(), SCHEMA_TYPE);
-    builder.setCreationTimestampMillis(dataClass.getCreationTs());
-    builder.setTtlMillis(dataClass.getTtlMs());
-    builder.setScore(dataClass.getScore());
-    builder.setPropertyLong("price", dataClass.getPrice());
+        new GenericDocument.Builder<>(document.namespace, document.getId(), SCHEMA_NAME);
+    builder.setCreationTimestampMillis(document.getCreationTs());
+    builder.setTtlMillis(document.getTtlMs());
+    builder.setScore(document.getScore());
+    builder.setPropertyLong("price", document.getPrice());
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     long creationTsConv = genericDoc.getCreationTimestampMillis();
     long ttlMsConv = genericDoc.getTtlMillis();
     int scoreConv = genericDoc.getScore();
     int priceConv = (int) genericDoc.getPropertyLong("price");
-    Gift dataClass = new Gift();
-    dataClass.setUri(uriConv);
-    dataClass.setScore(scoreConv);
-    dataClass.setCreationTs(creationTsConv);
-    dataClass.setTtlMs(ttlMsConv);
-    dataClass.setPrice(priceConv);
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.setId(idConv);
+    document.setScore(scoreConv);
+    document.setCreationTs(creationTsConv);
+    document.setTtlMs(ttlMsConv);
+    document.setPrice(priceConv);
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
index 13f4f18..0fa2028 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testCardinality.JAVA
@@ -1,7 +1,7 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Float;
@@ -9,64 +9,58 @@
 import java.lang.String;
 import java.util.Arrays;
 import java.util.List;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("repeatReq")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("repeatReq")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("repeatNoReq")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("repeatNoReq")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("req")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("req")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("noReq")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("noReq")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    List<String> repeatReqCopy = dataClass.repeatReq;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    List<String> repeatReqCopy = document.repeatReq;
     if (repeatReqCopy != null) {
       String[] repeatReqConv = repeatReqCopy.toArray(new String[0]);
       builder.setPropertyString("repeatReq", repeatReqConv);
     }
-    List<String> repeatNoReqCopy = dataClass.repeatNoReq;
+    List<String> repeatNoReqCopy = document.repeatNoReq;
     if (repeatNoReqCopy != null) {
       String[] repeatNoReqConv = repeatNoReqCopy.toArray(new String[0]);
       builder.setPropertyString("repeatNoReq", repeatNoReqConv);
     }
-    Float reqCopy = dataClass.req;
+    Float reqCopy = document.req;
     if (reqCopy != null) {
       builder.setPropertyDouble("req", reqCopy);
     }
-    Float noReqCopy = dataClass.noReq;
+    Float noReqCopy = document.noReq;
     if (noReqCopy != null) {
       builder.setPropertyDouble("noReq", noReqCopy);
     }
@@ -75,7 +69,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] repeatReqCopy = genericDoc.getPropertyStringArray("repeatReq");
     List<String> repeatReqConv = null;
     if (repeatReqCopy != null) {
@@ -96,12 +91,13 @@
     if (noReqCopy != null && noReqCopy.length != 0) {
       noReqConv = (float) noReqCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.repeatReq = repeatReqConv;
-    dataClass.repeatNoReq = repeatNoReqConv;
-    dataClass.req = reqConv;
-    dataClass.noReq = noReqConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.repeatReq = repeatReqConv;
+    document.repeatNoReq = repeatNoReqConv;
+    document.req = reqConv;
+    document.noReq = noReqConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
index 2c55500..27247d7 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDifferentTypeName.JAVA
@@ -1,38 +1,42 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "DifferentType";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "DifferentType";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    return dataClass;
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
index 87283d4..0e4a18f 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testIndexingType.JAVA
@@ -1,57 +1,56 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("indexNone")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("indexNone")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("indexExact")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("indexExact")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("indexPrefix")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("indexPrefix")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String indexNoneCopy = dataClass.indexNone;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String indexNoneCopy = document.indexNone;
     if (indexNoneCopy != null) {
       builder.setPropertyString("indexNone", indexNoneCopy);
     }
-    String indexExactCopy = dataClass.indexExact;
+    String indexExactCopy = document.indexExact;
     if (indexExactCopy != null) {
       builder.setPropertyString("indexExact", indexExactCopy);
     }
-    String indexPrefixCopy = dataClass.indexPrefix;
+    String indexPrefixCopy = document.indexPrefix;
     if (indexPrefixCopy != null) {
       builder.setPropertyString("indexPrefix", indexPrefixCopy);
     }
@@ -60,7 +59,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] indexNoneCopy = genericDoc.getPropertyStringArray("indexNone");
     String indexNoneConv = null;
     if (indexNoneCopy != null && indexNoneCopy.length != 0) {
@@ -76,11 +76,12 @@
     if (indexPrefixCopy != null && indexPrefixCopy.length != 0) {
       indexPrefixConv = indexPrefixCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.indexNone = indexNoneConv;
-    dataClass.indexExact = indexExactConv;
-    dataClass.indexPrefix = indexPrefixConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.indexNone = indexNoneConv;
+    document.indexExact = indexExactConv;
+    document.indexPrefix = indexPrefixConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
index 8900092..37ada00 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInnerClass.JAVA
@@ -1,37 +1,38 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift$$__InnerGift implements DataClassFactory<Gift.InnerGift> {
-  private static final String SCHEMA_TYPE = "InnerGift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift$$__InnerGift implements DocumentClassFactory<Gift.InnerGift> {
+  public static final String SCHEMA_NAME = "InnerGift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrString")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("arrString")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift.InnerGift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift.InnerGift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String[] arrStringCopy = dataClass.arrString;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String[] arrStringCopy = document.arrString;
     if (arrStringCopy != null) {
       builder.setPropertyString("arrString", arrStringCopy);
     }
@@ -40,11 +41,13 @@
 
   @Override
   public Gift.InnerGift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] arrStringConv = genericDoc.getPropertyStringArray("arrString");
-    Gift.InnerGift dataClass = new Gift.InnerGift();
-    dataClass.uri = uriConv;
-    dataClass.arrString = arrStringConv;
-    return dataClass;
+    Gift.InnerGift document = new Gift.InnerGift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.arrString = arrStringConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
index d843153..5aaf27b 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPropertyName.JAVA
@@ -1,37 +1,38 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("newName")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("newName")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String oldNameCopy = dataClass.oldName;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String oldNameCopy = document.oldName;
     if (oldNameCopy != null) {
       builder.setPropertyString("newName", oldNameCopy);
     }
@@ -40,15 +41,17 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] oldNameCopy = genericDoc.getPropertyStringArray("newName");
     String oldNameConv = null;
     if (oldNameCopy != null && oldNameCopy.length != 0) {
       oldNameConv = oldNameCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.oldName = oldNameConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.oldName = oldNameConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
index 890d43d..72c381d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRead_MultipleGetters.JAVA
@@ -1,47 +1,48 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    builder.setPropertyLong("price", dataClass.getPrice());
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setPropertyLong("price", document.getPrice());
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     int priceConv = (int) genericDoc.getPropertyLong("price");
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.setPrice(priceConv);
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.setPrice(priceConv);
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
index be28a52..fc8438a 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testRepeatedFields.JAVA
@@ -1,7 +1,7 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Integer;
@@ -11,55 +11,47 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("listOfString")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("listOfString")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("setOfInt")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("setOfInt")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("repeatedByteArray")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("repeatedByteArray")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("byteArray")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("byteArray")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    List<String> listOfStringCopy = dataClass.listOfString;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    List<String> listOfStringCopy = document.listOfString;
     if (listOfStringCopy != null) {
       String[] listOfStringConv = listOfStringCopy.toArray(new String[0]);
       builder.setPropertyString("listOfString", listOfStringConv);
     }
-    Collection<Integer> setOfIntCopy = dataClass.setOfInt;
+    Collection<Integer> setOfIntCopy = document.setOfInt;
     if (setOfIntCopy != null) {
       long[] setOfIntConv = new long[setOfIntCopy.size()];
       int i = 0;
@@ -68,11 +60,11 @@
       }
       builder.setPropertyLong("setOfInt", setOfIntConv);
     }
-    byte[][] repeatedByteArrayCopy = dataClass.repeatedByteArray;
+    byte[][] repeatedByteArrayCopy = document.repeatedByteArray;
     if (repeatedByteArrayCopy != null) {
       builder.setPropertyBytes("repeatedByteArray", repeatedByteArrayCopy);
     }
-    byte[] byteArrayCopy = dataClass.byteArray;
+    byte[] byteArrayCopy = document.byteArray;
     if (byteArrayCopy != null) {
       builder.setPropertyBytes("byteArray", byteArrayCopy);
     }
@@ -81,7 +73,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] listOfStringCopy = genericDoc.getPropertyStringArray("listOfString");
     List<String> listOfStringConv = null;
     if (listOfStringCopy != null) {
@@ -101,12 +94,13 @@
     if (byteArrayCopy != null && byteArrayCopy.length != 0) {
       byteArrayConv = byteArrayCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.listOfString = listOfStringConv;
-    dataClass.setOfInt = setOfIntConv;
-    dataClass.repeatedByteArray = repeatedByteArrayConv;
-    dataClass.byteArray = byteArrayConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.listOfString = listOfStringConv;
+    document.setOfInt = setOfIntConv;
+    document.repeatedByteArray = repeatedByteArrayConv;
+    document.byteArray = byteArrayConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
index 8499df6..bcc152c 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testSuccessSimple.JAVA
@@ -1,63 +1,58 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("cat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("cat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("dog")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("dog")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    builder.setPropertyLong("price", dataClass.price);
-    builder.setPropertyBoolean("cat", dataClass.cat);
-    builder.setPropertyBoolean("dog", dataClass.getDog());
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setPropertyLong("price", document.price);
+    builder.setPropertyBoolean("cat", document.cat);
+    builder.setPropertyBoolean("dog", document.getDog());
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     int priceConv = (int) genericDoc.getPropertyLong("price");
     boolean catConv = genericDoc.getPropertyBoolean("cat");
     boolean dogConv = genericDoc.getPropertyBoolean("dog");
-    Gift dataClass = new Gift(dogConv, uriConv);
-    dataClass.price = priceConv;
-    dataClass.cat = catConv;
-    return dataClass;
+    Gift document = new Gift(dogConv, idConv, namespaceConv);
+    document.namespace = namespaceConv;
+    document.price = priceConv;
+    document.cat = catConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
index d3ee29b..3469a810 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
@@ -1,8 +1,7 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
-import androidx.appsearch.app.DataClassFactoryRegistry;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Boolean;
@@ -17,239 +16,139 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("collectLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectInteger")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("collectInteger")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("collectDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("collectFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("collectBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectByteArr")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("collectByteArr")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectString")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("collectString")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("collectGift")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT)
-            .setSchemaType(DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).getSchemaType())
+          .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder("collectGift", $$__AppSearch__Gift.SCHEMA_NAME)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("arrBoxLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("arrUnboxLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxInteger")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("arrBoxInteger")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxInt")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("arrUnboxInt")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("arrBoxDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("arrUnboxDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("arrBoxFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("arrUnboxFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrBoxBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("arrBoxBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("arrUnboxBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrUnboxByteArr")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("arrUnboxByteArr")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxByteArr")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("boxByteArr")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrString")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("arrString")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("arrGift")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT)
-            .setSchemaType(DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).getSchemaType())
+          .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder("arrGift", $$__AppSearch__Gift.SCHEMA_NAME)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("string")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("string")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("boxLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxLong")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("unboxLong")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxInteger")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("boxInteger")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxInt")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("unboxInt")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("boxDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxDouble")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("unboxDouble")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("boxFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxFloat")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE)
+          .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("unboxFloat")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("boxBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boxBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxBoolean")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN)
+          .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("unboxBoolean")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("unboxByteArr")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES)
+          .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("unboxByteArr")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("gift")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT)
-            .setSchemaType(DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).getSchemaType())
+          .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder("gift", $$__AppSearch__Gift.SCHEMA_NAME)
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    Collection<Long> collectLongCopy = dataClass.collectLong;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    Collection<Long> collectLongCopy = document.collectLong;
     if (collectLongCopy != null) {
       long[] collectLongConv = new long[collectLongCopy.size()];
       int i = 0;
@@ -258,7 +157,7 @@
       }
       builder.setPropertyLong("collectLong", collectLongConv);
     }
-    Collection<Integer> collectIntegerCopy = dataClass.collectInteger;
+    Collection<Integer> collectIntegerCopy = document.collectInteger;
     if (collectIntegerCopy != null) {
       long[] collectIntegerConv = new long[collectIntegerCopy.size()];
       int i = 0;
@@ -267,7 +166,7 @@
       }
       builder.setPropertyLong("collectInteger", collectIntegerConv);
     }
-    Collection<Double> collectDoubleCopy = dataClass.collectDouble;
+    Collection<Double> collectDoubleCopy = document.collectDouble;
     if (collectDoubleCopy != null) {
       double[] collectDoubleConv = new double[collectDoubleCopy.size()];
       int i = 0;
@@ -276,7 +175,7 @@
       }
       builder.setPropertyDouble("collectDouble", collectDoubleConv);
     }
-    Collection<Float> collectFloatCopy = dataClass.collectFloat;
+    Collection<Float> collectFloatCopy = document.collectFloat;
     if (collectFloatCopy != null) {
       double[] collectFloatConv = new double[collectFloatCopy.size()];
       int i = 0;
@@ -285,7 +184,7 @@
       }
       builder.setPropertyDouble("collectFloat", collectFloatConv);
     }
-    Collection<Boolean> collectBooleanCopy = dataClass.collectBoolean;
+    Collection<Boolean> collectBooleanCopy = document.collectBoolean;
     if (collectBooleanCopy != null) {
       boolean[] collectBooleanConv = new boolean[collectBooleanCopy.size()];
       int i = 0;
@@ -294,7 +193,7 @@
       }
       builder.setPropertyBoolean("collectBoolean", collectBooleanConv);
     }
-    Collection<byte[]> collectByteArrCopy = dataClass.collectByteArr;
+    Collection<byte[]> collectByteArrCopy = document.collectByteArr;
     if (collectByteArrCopy != null) {
       byte[][] collectByteArrConv = new byte[collectByteArrCopy.size()][];
       int i = 0;
@@ -303,22 +202,21 @@
       }
       builder.setPropertyBytes("collectByteArr", collectByteArrConv);
     }
-    Collection<String> collectStringCopy = dataClass.collectString;
+    Collection<String> collectStringCopy = document.collectString;
     if (collectStringCopy != null) {
       String[] collectStringConv = collectStringCopy.toArray(new String[0]);
       builder.setPropertyString("collectString", collectStringConv);
     }
-    Collection<Gift> collectGiftCopy = dataClass.collectGift;
+    Collection<Gift> collectGiftCopy = document.collectGift;
     if (collectGiftCopy != null) {
       GenericDocument[] collectGiftConv = new GenericDocument[collectGiftCopy.size()];
-      DataClassFactory<Gift> factory = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class);
       int i = 0;
       for (Gift item : collectGiftCopy) {
-        collectGiftConv[i++] = factory.toGenericDocument(item);
+        collectGiftConv[i++] = GenericDocument.fromDocumentClass(item);
       }
       builder.setPropertyDocument("collectGift", collectGiftConv);
     }
-    Long[] arrBoxLongCopy = dataClass.arrBoxLong;
+    Long[] arrBoxLongCopy = document.arrBoxLong;
     if (arrBoxLongCopy != null) {
       long[] arrBoxLongConv = new long[arrBoxLongCopy.length];
       for (int i = 0 ; i < arrBoxLongCopy.length ; i++) {
@@ -326,11 +224,11 @@
       }
       builder.setPropertyLong("arrBoxLong", arrBoxLongConv);
     }
-    long[] arrUnboxLongCopy = dataClass.arrUnboxLong;
+    long[] arrUnboxLongCopy = document.arrUnboxLong;
     if (arrUnboxLongCopy != null) {
       builder.setPropertyLong("arrUnboxLong", arrUnboxLongCopy);
     }
-    Integer[] arrBoxIntegerCopy = dataClass.arrBoxInteger;
+    Integer[] arrBoxIntegerCopy = document.arrBoxInteger;
     if (arrBoxIntegerCopy != null) {
       long[] arrBoxIntegerConv = new long[arrBoxIntegerCopy.length];
       for (int i = 0 ; i < arrBoxIntegerCopy.length ; i++) {
@@ -338,7 +236,7 @@
       }
       builder.setPropertyLong("arrBoxInteger", arrBoxIntegerConv);
     }
-    int[] arrUnboxIntCopy = dataClass.arrUnboxInt;
+    int[] arrUnboxIntCopy = document.arrUnboxInt;
     if (arrUnboxIntCopy != null) {
       long[] arrUnboxIntConv = new long[arrUnboxIntCopy.length];
       for (int i = 0 ; i < arrUnboxIntCopy.length ; i++) {
@@ -346,7 +244,7 @@
       }
       builder.setPropertyLong("arrUnboxInt", arrUnboxIntConv);
     }
-    Double[] arrBoxDoubleCopy = dataClass.arrBoxDouble;
+    Double[] arrBoxDoubleCopy = document.arrBoxDouble;
     if (arrBoxDoubleCopy != null) {
       double[] arrBoxDoubleConv = new double[arrBoxDoubleCopy.length];
       for (int i = 0 ; i < arrBoxDoubleCopy.length ; i++) {
@@ -354,11 +252,11 @@
       }
       builder.setPropertyDouble("arrBoxDouble", arrBoxDoubleConv);
     }
-    double[] arrUnboxDoubleCopy = dataClass.arrUnboxDouble;
+    double[] arrUnboxDoubleCopy = document.arrUnboxDouble;
     if (arrUnboxDoubleCopy != null) {
       builder.setPropertyDouble("arrUnboxDouble", arrUnboxDoubleCopy);
     }
-    Float[] arrBoxFloatCopy = dataClass.arrBoxFloat;
+    Float[] arrBoxFloatCopy = document.arrBoxFloat;
     if (arrBoxFloatCopy != null) {
       double[] arrBoxFloatConv = new double[arrBoxFloatCopy.length];
       for (int i = 0 ; i < arrBoxFloatCopy.length ; i++) {
@@ -366,7 +264,7 @@
       }
       builder.setPropertyDouble("arrBoxFloat", arrBoxFloatConv);
     }
-    float[] arrUnboxFloatCopy = dataClass.arrUnboxFloat;
+    float[] arrUnboxFloatCopy = document.arrUnboxFloat;
     if (arrUnboxFloatCopy != null) {
       double[] arrUnboxFloatConv = new double[arrUnboxFloatCopy.length];
       for (int i = 0 ; i < arrUnboxFloatCopy.length ; i++) {
@@ -374,7 +272,7 @@
       }
       builder.setPropertyDouble("arrUnboxFloat", arrUnboxFloatConv);
     }
-    Boolean[] arrBoxBooleanCopy = dataClass.arrBoxBoolean;
+    Boolean[] arrBoxBooleanCopy = document.arrBoxBoolean;
     if (arrBoxBooleanCopy != null) {
       boolean[] arrBoxBooleanConv = new boolean[arrBoxBooleanCopy.length];
       for (int i = 0 ; i < arrBoxBooleanCopy.length ; i++) {
@@ -382,15 +280,15 @@
       }
       builder.setPropertyBoolean("arrBoxBoolean", arrBoxBooleanConv);
     }
-    boolean[] arrUnboxBooleanCopy = dataClass.arrUnboxBoolean;
+    boolean[] arrUnboxBooleanCopy = document.arrUnboxBoolean;
     if (arrUnboxBooleanCopy != null) {
       builder.setPropertyBoolean("arrUnboxBoolean", arrUnboxBooleanCopy);
     }
-    byte[][] arrUnboxByteArrCopy = dataClass.arrUnboxByteArr;
+    byte[][] arrUnboxByteArrCopy = document.arrUnboxByteArr;
     if (arrUnboxByteArrCopy != null) {
       builder.setPropertyBytes("arrUnboxByteArr", arrUnboxByteArrCopy);
     }
-    Byte[] boxByteArrCopy = dataClass.boxByteArr;
+    Byte[] boxByteArrCopy = document.boxByteArr;
     if (boxByteArrCopy != null) {
       byte[] boxByteArrConv = new byte[boxByteArrCopy.length];
       for (int i = 0 ; i < boxByteArrCopy.length ; i++) {
@@ -398,55 +296,54 @@
       }
       builder.setPropertyBytes("boxByteArr", boxByteArrConv);
     }
-    String[] arrStringCopy = dataClass.arrString;
+    String[] arrStringCopy = document.arrString;
     if (arrStringCopy != null) {
       builder.setPropertyString("arrString", arrStringCopy);
     }
-    Gift[] arrGiftCopy = dataClass.arrGift;
+    Gift[] arrGiftCopy = document.arrGift;
     if (arrGiftCopy != null) {
       GenericDocument[] arrGiftConv = new GenericDocument[arrGiftCopy.length];
-      DataClassFactory<Gift> factory = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class);
       for (int i = 0; i < arrGiftConv.length; i++) {
-        arrGiftConv[i] = factory.toGenericDocument(arrGiftCopy[i]);
+        arrGiftConv[i] = GenericDocument.fromDocumentClass(arrGiftCopy[i]);
       }
       builder.setPropertyDocument("arrGift", arrGiftConv);
     }
-    String stringCopy = dataClass.string;
+    String stringCopy = document.string;
     if (stringCopy != null) {
       builder.setPropertyString("string", stringCopy);
     }
-    Long boxLongCopy = dataClass.boxLong;
+    Long boxLongCopy = document.boxLong;
     if (boxLongCopy != null) {
       builder.setPropertyLong("boxLong", boxLongCopy);
     }
-    builder.setPropertyLong("unboxLong", dataClass.unboxLong);
-    Integer boxIntegerCopy = dataClass.boxInteger;
+    builder.setPropertyLong("unboxLong", document.unboxLong);
+    Integer boxIntegerCopy = document.boxInteger;
     if (boxIntegerCopy != null) {
       builder.setPropertyLong("boxInteger", boxIntegerCopy);
     }
-    builder.setPropertyLong("unboxInt", dataClass.unboxInt);
-    Double boxDoubleCopy = dataClass.boxDouble;
+    builder.setPropertyLong("unboxInt", document.unboxInt);
+    Double boxDoubleCopy = document.boxDouble;
     if (boxDoubleCopy != null) {
       builder.setPropertyDouble("boxDouble", boxDoubleCopy);
     }
-    builder.setPropertyDouble("unboxDouble", dataClass.unboxDouble);
-    Float boxFloatCopy = dataClass.boxFloat;
+    builder.setPropertyDouble("unboxDouble", document.unboxDouble);
+    Float boxFloatCopy = document.boxFloat;
     if (boxFloatCopy != null) {
       builder.setPropertyDouble("boxFloat", boxFloatCopy);
     }
-    builder.setPropertyDouble("unboxFloat", dataClass.unboxFloat);
-    Boolean boxBooleanCopy = dataClass.boxBoolean;
+    builder.setPropertyDouble("unboxFloat", document.unboxFloat);
+    Boolean boxBooleanCopy = document.boxBoolean;
     if (boxBooleanCopy != null) {
       builder.setPropertyBoolean("boxBoolean", boxBooleanCopy);
     }
-    builder.setPropertyBoolean("unboxBoolean", dataClass.unboxBoolean);
-    byte[] unboxByteArrCopy = dataClass.unboxByteArr;
+    builder.setPropertyBoolean("unboxBoolean", document.unboxBoolean);
+    byte[] unboxByteArrCopy = document.unboxByteArr;
     if (unboxByteArrCopy != null) {
       builder.setPropertyBytes("unboxByteArr", unboxByteArrCopy);
     }
-    Gift giftCopy = dataClass.gift;
+    Gift giftCopy = document.gift;
     if (giftCopy != null) {
-      GenericDocument giftConv = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).toGenericDocument(giftCopy);
+      GenericDocument giftConv = GenericDocument.fromDocumentClass(giftCopy);
       builder.setPropertyDocument("gift", giftConv);
     }
     return builder.build();
@@ -454,7 +351,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     long[] collectLongCopy = genericDoc.getPropertyLongArray("collectLong");
     List<Long> collectLongConv = null;
     if (collectLongCopy != null) {
@@ -511,10 +409,9 @@
     GenericDocument[] collectGiftCopy = genericDoc.getPropertyDocumentArray("collectGift");
     List<Gift> collectGiftConv = null;
     if (collectGiftCopy != null) {
-      DataClassFactory<Gift> factory = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class);
       collectGiftConv = new ArrayList<>(collectGiftCopy.length);
       for (int i = 0; i < collectGiftCopy.length; i++) {
-        collectGiftConv.add(factory.fromGenericDocument(collectGiftCopy[i]));
+        collectGiftConv.add(collectGiftCopy[i].toDocumentClass(Gift.class));
       }
     }
     long[] arrBoxLongCopy = genericDoc.getPropertyLongArray("arrBoxLong");
@@ -590,9 +487,8 @@
     Gift[] arrGiftConv = null;
     if (arrGiftCopy != null) {
       arrGiftConv = new Gift[arrGiftCopy.length];
-      DataClassFactory<Gift> factory = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class);
       for (int i = 0; i < arrGiftCopy.length; i++) {
-        arrGiftConv[i] = factory.fromGenericDocument(arrGiftCopy[i]);
+        arrGiftConv[i] = arrGiftCopy[i].toDocumentClass(Gift.class);
       }
     }
     String[] stringCopy = genericDoc.getPropertyStringArray("string");
@@ -638,45 +534,46 @@
     GenericDocument giftCopy = genericDoc.getPropertyDocument("gift");
     Gift giftConv = null;
     if (giftCopy != null) {
-      giftConv = DataClassFactoryRegistry.getInstance().getOrCreateFactory(Gift.class).fromGenericDocument(giftCopy);
+      giftConv = giftCopy.toDocumentClass(Gift.class);
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.collectLong = collectLongConv;
-    dataClass.collectInteger = collectIntegerConv;
-    dataClass.collectDouble = collectDoubleConv;
-    dataClass.collectFloat = collectFloatConv;
-    dataClass.collectBoolean = collectBooleanConv;
-    dataClass.collectByteArr = collectByteArrConv;
-    dataClass.collectString = collectStringConv;
-    dataClass.collectGift = collectGiftConv;
-    dataClass.arrBoxLong = arrBoxLongConv;
-    dataClass.arrUnboxLong = arrUnboxLongConv;
-    dataClass.arrBoxInteger = arrBoxIntegerConv;
-    dataClass.arrUnboxInt = arrUnboxIntConv;
-    dataClass.arrBoxDouble = arrBoxDoubleConv;
-    dataClass.arrUnboxDouble = arrUnboxDoubleConv;
-    dataClass.arrBoxFloat = arrBoxFloatConv;
-    dataClass.arrUnboxFloat = arrUnboxFloatConv;
-    dataClass.arrBoxBoolean = arrBoxBooleanConv;
-    dataClass.arrUnboxBoolean = arrUnboxBooleanConv;
-    dataClass.arrUnboxByteArr = arrUnboxByteArrConv;
-    dataClass.boxByteArr = boxByteArrConv;
-    dataClass.arrString = arrStringConv;
-    dataClass.arrGift = arrGiftConv;
-    dataClass.string = stringConv;
-    dataClass.boxLong = boxLongConv;
-    dataClass.unboxLong = unboxLongConv;
-    dataClass.boxInteger = boxIntegerConv;
-    dataClass.unboxInt = unboxIntConv;
-    dataClass.boxDouble = boxDoubleConv;
-    dataClass.unboxDouble = unboxDoubleConv;
-    dataClass.boxFloat = boxFloatConv;
-    dataClass.unboxFloat = unboxFloatConv;
-    dataClass.boxBoolean = boxBooleanConv;
-    dataClass.unboxBoolean = unboxBooleanConv;
-    dataClass.unboxByteArr = unboxByteArrConv;
-    dataClass.gift = giftConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.collectLong = collectLongConv;
+    document.collectInteger = collectIntegerConv;
+    document.collectDouble = collectDoubleConv;
+    document.collectFloat = collectFloatConv;
+    document.collectBoolean = collectBooleanConv;
+    document.collectByteArr = collectByteArrConv;
+    document.collectString = collectStringConv;
+    document.collectGift = collectGiftConv;
+    document.arrBoxLong = arrBoxLongConv;
+    document.arrUnboxLong = arrUnboxLongConv;
+    document.arrBoxInteger = arrBoxIntegerConv;
+    document.arrUnboxInt = arrUnboxIntConv;
+    document.arrBoxDouble = arrBoxDoubleConv;
+    document.arrUnboxDouble = arrUnboxDoubleConv;
+    document.arrBoxFloat = arrBoxFloatConv;
+    document.arrUnboxFloat = arrUnboxFloatConv;
+    document.arrBoxBoolean = arrBoxBooleanConv;
+    document.arrUnboxBoolean = arrUnboxBooleanConv;
+    document.arrUnboxByteArr = arrUnboxByteArrConv;
+    document.boxByteArr = boxByteArrConv;
+    document.arrString = arrStringConv;
+    document.arrGift = arrGiftConv;
+    document.string = stringConv;
+    document.boxLong = boxLongConv;
+    document.unboxLong = unboxLongConv;
+    document.boxInteger = boxIntegerConv;
+    document.unboxInt = unboxIntConv;
+    document.boxDouble = boxDoubleConv;
+    document.unboxDouble = unboxDoubleConv;
+    document.boxFloat = boxFloatConv;
+    document.unboxFloat = unboxFloatConv;
+    document.boxBoolean = boxBooleanConv;
+    document.unboxBoolean = unboxBooleanConv;
+    document.unboxByteArr = unboxByteArrConv;
+    document.gift = giftConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
index 9be6cdb..d938e9e 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
@@ -1,47 +1,47 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("tokNone")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNone")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("tokPlain")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlain")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    String tokNoneCopy = dataClass.tokNone;
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String tokNoneCopy = document.tokNone;
     if (tokNoneCopy != null) {
       builder.setPropertyString("tokNone", tokNoneCopy);
     }
-    String tokPlainCopy = dataClass.tokPlain;
+    String tokPlainCopy = document.tokPlain;
     if (tokPlainCopy != null) {
       builder.setPropertyString("tokPlain", tokPlainCopy);
     }
@@ -50,7 +50,8 @@
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     String[] tokNoneCopy = genericDoc.getPropertyStringArray("tokNone");
     String tokNoneConv = null;
     if (tokNoneCopy != null && tokNoneCopy.length != 0) {
@@ -61,10 +62,11 @@
     if (tokPlainCopy != null && tokPlainCopy.length != 0) {
       tokPlainConv = tokPlainCopy[0];
     }
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.tokNone = tokNoneConv;
-    dataClass.tokPlain = tokPlainConv;
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.tokNone = tokNoneConv;
+    document.tokPlain = tokPlainConv;
+    return document;
   }
 }
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
index 890d43d..72c381d 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testWrite_multipleSetters.JAVA
@@ -1,47 +1,48 @@
 package com.example.appsearch;
 
 import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.DataClassFactory;
+import androidx.appsearch.app.DocumentClassFactory;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import java.lang.Override;
 import java.lang.String;
+import javax.annotation.Generated;
 
-public class $$__AppSearch__Gift implements DataClassFactory<Gift> {
-  private static final String SCHEMA_TYPE = "Gift";
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public class $$__AppSearch__Gift implements DocumentClassFactory<Gift> {
+  public static final String SCHEMA_NAME = "Gift";
 
   @Override
-  public String getSchemaType() {
-    return SCHEMA_TYPE;
+  public String getSchemaName() {
+    return SCHEMA_NAME;
   }
 
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
-    return new AppSearchSchema.Builder(SCHEMA_TYPE)
-          .addProperty(new AppSearchSchema.PropertyConfig.Builder("price")
-            .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+    return new AppSearchSchema.Builder(SCHEMA_NAME)
+          .addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("price")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-            .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
-            .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
             .build())
           .build();
   }
 
   @Override
-  public GenericDocument toGenericDocument(Gift dataClass) throws AppSearchException {
+  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
-        new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
-    builder.setPropertyLong("price", dataClass.getPrice());
+        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    builder.setPropertyLong("price", document.getPrice());
     return builder.build();
   }
 
   @Override
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
-    String uriConv = genericDoc.getUri();
+    String idConv = genericDoc.getId();
+    String namespaceConv = genericDoc.getNamespace();
     int priceConv = (int) genericDoc.getPropertyLong("price");
-    Gift dataClass = new Gift();
-    dataClass.uri = uriConv;
-    dataClass.setPrice(priceConv);
-    return dataClass;
+    Gift document = new Gift();
+    document.namespace = namespaceConv;
+    document.id = idConv;
+    document.setPrice(priceConv);
+    return document;
   }
 }
diff --git a/appsearch/debug-view/api/current.txt b/appsearch/debug-view/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appsearch/debug-view/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appsearch/debug-view/api/public_plus_experimental_current.txt b/appsearch/debug-view/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appsearch/debug-view/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appsearch/debug-view/api/res-current.txt b/appsearch/debug-view/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appsearch/debug-view/api/res-current.txt
diff --git a/appsearch/debug-view/api/restricted_current.txt b/appsearch/debug-view/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appsearch/debug-view/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appsearch/debug-view/build.gradle b/appsearch/debug-view/build.gradle
new file mode 100644
index 0000000..73c6412
--- /dev/null
+++ b/appsearch/debug-view/build.gradle
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+import static androidx.build.dependencies.DependenciesKt.CONSTRAINT_LAYOUT
+import static androidx.build.dependencies.DependenciesKt.GUAVA_ANDROID
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+android {
+    defaultConfig {
+        multiDexEnabled true
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation project(':appsearch:appsearch')
+    implementation project(':appsearch:appsearch-local-storage')
+    implementation('androidx.appcompat:appcompat:1.2.0')
+    implementation('androidx.concurrent:concurrent-futures:1.0.0')
+    implementation('androidx.fragment:fragment:1.3.0')
+    implementation('androidx.legacy:legacy-support-v4:1.0.0')
+    implementation('androidx.multidex:multidex:2.0.1')
+    implementation('androidx.navigation:navigation-fragment:2.3.4')
+    implementation('androidx.navigation:navigation-ui:2.3.4')
+    implementation('com.google.android.material:material:1.0.0')
+    implementation(CONSTRAINT_LAYOUT, { transitive = true })
+    implementation(GUAVA_ANDROID)
+}
+
+androidx {
+    name = "AndroidX AppSearch Debug View"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.APPSEARCH
+    mavenVersion = LibraryVersions.APPSEARCH
+    inceptionYear = "2021"
+    description = "A support library for AndroidX AppSearch that contains activities and views " +
+            "for debugging an application's integration with AppSearch."
+}
diff --git a/appsearch/debug-view/samples/build.gradle b/appsearch/debug-view/samples/build.gradle
new file mode 100644
index 0000000..1ed0834
--- /dev/null
+++ b/appsearch/debug-view/samples/build.gradle
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+}
+
+android {
+    defaultConfig {
+        applicationId "androidx.appsearch.debugview.sample"
+        versionCode 1
+        versionName "1.0"
+        multiDexEnabled true
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    annotationProcessor project(":appsearch:appsearch-compiler")
+
+    api('androidx.annotation:annotation:1.1.0')
+
+    implementation project(':appsearch:appsearch')
+    implementation project(':appsearch:appsearch-local-storage')
+    implementation project(':appsearch:appsearch-debug-view')
+    implementation('androidx.appcompat:appcompat:1.2.0')
+    implementation('androidx.concurrent:concurrent-futures:1.0.0')
+    implementation('androidx.multidex:multidex:2.0.1')
+    implementation('com.google.android.material:material:1.0.0')
+    implementation('com.google.code.gson:gson:2.6.2')
+    implementation(CONSTRAINT_LAYOUT, { transitive = true })
+    implementation(GUAVA_ANDROID)
+}
+
+androidx {
+    name = "AndroidX AppSearch Debug View Sample App"
+    type = LibraryType.SAMPLES
+    mavenGroup = LibraryGroups.APPSEARCH
+    mavenVersion = LibraryVersions.APPSEARCH
+    inceptionYear = "2021"
+    description = "Contains a sample app for integrating the Androidx AppSearch Debug View"
+}
diff --git a/appsearch/debug-view/samples/src/main/AndroidManifest.xml b/appsearch/debug-view/samples/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a295ffb3
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.appsearch.debugview.samples">
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.AppCompat">
+        <activity android:name=".NotesActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity android:name="androidx.appsearch.debugview.view.AppSearchDebugActivity" />
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/assets/sample_notes.json b/appsearch/debug-view/samples/src/main/assets/sample_notes.json
new file mode 100644
index 0000000..18438c0
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/assets/sample_notes.json
@@ -0,0 +1,34 @@
+{
+  "data" : [
+    {
+      "noteText": "Don't forget to grab lunch!",
+      "namespace": "namespace1",
+      "id": "note1"
+    },
+    {
+      "noteText": "I wonder what food I should get.",
+      "namespace": "namespace1",
+      "id": "note2"
+    },
+    {
+      "noteText": "Apples are my favorite fruit.",
+      "namespace": "namespace1",
+      "id": "note3"
+    },
+    {
+      "noteText": "The weather is great!",
+      "namespace": "namespace2",
+      "id": "note1"
+    },
+    {
+      "noteText": "I hope it doesn't rain.",
+      "namespace": "namespace2",
+      "id": "note2"
+    },
+    {
+      "noteText": "Tomorrow will be hot.",
+      "namespace": "namespace2",
+      "id": "note3"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
new file mode 100644
index 0000000..17cba8a
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.samples;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appsearch.debugview.samples.model.Note;
+import androidx.appsearch.debugview.view.AppSearchDebugActivity;
+import androidx.core.content.ContextCompat;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+
+/**
+ * Default Activity for AppSearch Debug View Sample App
+ *
+ * <p>This activity reads sample data, converts it into {@link Note} objects, and then indexes
+ * them into AppSearch.
+ *
+ * <p>Each sample note's text is added to the list view for display.
+ */
+public class NotesActivity extends AppCompatActivity {
+    private static final String DB_NAME = "notesDb";
+    private static final String SAMPLE_NOTES_FILENAME = "sample_notes.json";
+    private static final String TAG = "NotesActivity";
+
+    private final SettableFuture<NotesAppSearchManager> mNotesAppSearchManagerFuture =
+            SettableFuture.create();
+    private ArrayAdapter<Note> mNotesAdapter;
+    private ListView mListView;
+    private TextView mLoadingView;
+    private ListeningExecutorService mBackgroundExecutor;
+    private List<Note> mSampleNotes;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_notes);
+
+        mListView = findViewById(R.id.list_view);
+        mLoadingView = findViewById(R.id.text_view);
+
+        mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+
+        mNotesAppSearchManagerFuture.setFuture(NotesAppSearchManager.createNotesAppSearchManager(
+                getApplicationContext(), mBackgroundExecutor));
+        ListenableFuture<List<Note>> sampleNotesFuture =
+                mBackgroundExecutor.submit(() -> loadSampleNotes());
+
+        ListenableFuture<Void> insertNotesFuture =
+                Futures.whenAllSucceed(mNotesAppSearchManagerFuture, sampleNotesFuture).call(
+                        () -> {
+                            mSampleNotes = Futures.getDone(sampleNotesFuture);
+                            Futures.getDone(mNotesAppSearchManagerFuture).insertNotes(
+                                    mSampleNotes).get();
+                            return null;
+                        }, mBackgroundExecutor);
+
+        Futures.addCallback(insertNotesFuture,
+                new FutureCallback<Void>() {
+                    @Override
+                    public void onSuccess(Void result) {
+                        displayNotes();
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Toast.makeText(NotesActivity.this, "Failed to insert notes "
+                                + "into AppSearch.", Toast.LENGTH_LONG).show();
+                        Log.e(TAG, "Failed to insert notes into AppSearch.", t);
+                    }
+                }, ContextCompat.getMainExecutor(this));
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+        getMenuInflater().inflate(R.menu.debug_menu, menu);
+
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+        switch (item.getItemId()) {
+            case (R.id.app_search_debug):
+                Intent intent = new Intent(this, AppSearchDebugActivity.class);
+                intent.putExtra("databaseName", DB_NAME);
+                startActivity(intent);
+                return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void onStop() {
+        Futures.whenAllSucceed(mNotesAppSearchManagerFuture).call(() -> {
+            Futures.getDone(mNotesAppSearchManagerFuture).close();
+            return null;
+        }, mBackgroundExecutor);
+
+        super.onStop();
+    }
+
+    @WorkerThread
+    private List<Note> loadSampleNotes() {
+        List<Note> sampleNotes = new ArrayList<>();
+        Gson gson = new Gson();
+        try (InputStreamReader r = new InputStreamReader(
+                getAssets().open(SAMPLE_NOTES_FILENAME))) {
+            JsonObject samplesJson = gson.fromJson(r, JsonObject.class);
+            JsonArray sampleJsonArr = samplesJson.getAsJsonArray("data");
+            for (int i = 0; i < sampleJsonArr.size(); ++i) {
+                JsonObject noteJson = sampleJsonArr.get(i).getAsJsonObject();
+                sampleNotes.add(new Note.Builder().setId(noteJson.get("id").getAsString())
+                        .setNamespace(noteJson.get("namespace").getAsString())
+                        .setText(noteJson.get("noteText").getAsString())
+                        .build()
+                );
+            }
+        } catch (IOException e) {
+            Toast.makeText(NotesActivity.this, "Failed to load sample notes ",
+                    Toast.LENGTH_LONG).show();
+            Log.e(TAG, "Sample notes IO failed: ", e);
+        }
+        return sampleNotes;
+    }
+
+    private void displayNotes() {
+        mNotesAdapter = new ArrayAdapter<>(this,
+                android.R.layout.simple_list_item_1, mSampleNotes);
+        mListView.setAdapter(mNotesAdapter);
+
+        mLoadingView.setVisibility(View.GONE);
+        mListView.setVisibility(View.VISIBLE);
+    }
+}
diff --git a/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
new file mode 100644
index 0000000..7aba8a7
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesAppSearchManager.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.samples;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.debugview.samples.model.Note;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.LocalStorage;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.Closeable;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Manages interactions with AppSearch.
+ */
+public class NotesAppSearchManager implements Closeable {
+    private static final String DB_NAME = "notesDb";
+    private static final boolean FORCE_OVERRIDE = true;
+
+    private final Context mContext;
+    private final Executor mExecutor;
+    private final SettableFuture<AppSearchSession> mAppSearchSessionFuture =
+            SettableFuture.create();
+
+    private NotesAppSearchManager(@NonNull Context context, @NonNull Executor executor) {
+        mContext = context;
+        mExecutor = executor;
+    }
+
+    /**
+     * Factory for creating a {@link NotesAppSearchManager} instance.
+     *
+     * <p>This creates and initializes an {@link AppSearchSession}. It also resets existing
+     * {@link Note} objects from the index and re-adds the {@link Note} document class to the
+     * AppSearch schema.
+     *
+     * @param executor to run AppSearch operations on.
+     */
+    @NonNull
+    public static ListenableFuture<NotesAppSearchManager> createNotesAppSearchManager(
+            @NonNull Context context, @NonNull Executor executor) {
+        NotesAppSearchManager notesAppSearchManager = new NotesAppSearchManager(context, executor);
+        return Futures.transform(notesAppSearchManager.initialize(),
+                unused -> notesAppSearchManager, executor);
+    }
+
+    /**
+     * Closes the AppSearch session.
+     */
+    @Override
+    public void close() {
+        Futures.whenAllSucceed(mAppSearchSessionFuture).call(() -> {
+            Futures.getDone(mAppSearchSessionFuture).close();
+            return null;
+        }, mExecutor);
+    }
+
+    /**
+     * Inserts {@link Note} documents into the AppSearch database.
+     *
+     * @param notes list of notes to index in AppSearch.
+     */
+    @NonNull
+    public ListenableFuture<AppSearchBatchResult<String, Void>> insertNotes(
+            @NonNull List<Note> notes) {
+        try {
+            PutDocumentsRequest request = new PutDocumentsRequest.Builder().addDocuments(notes)
+                    .build();
+            return Futures.transformAsync(mAppSearchSessionFuture,
+                    session -> session.put(request), mExecutor);
+        } catch (Exception e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+
+    @NonNull
+    private ListenableFuture<Void> initialize() {
+        return Futures.transformAsync(createLocalSession(), session -> {
+            mAppSearchSessionFuture.set(session);
+            return Futures.transformAsync(resetDocuments(),
+                    unusedResetResult -> Futures.transform(setSchema(),
+                            unusedSetSchemaResult -> null,
+                            mExecutor),
+                    mExecutor);
+        }, mExecutor);
+    }
+
+    private ListenableFuture<AppSearchSession> createLocalSession() {
+        return LocalStorage.createSearchSession(
+                new LocalStorage.SearchContext.Builder(mContext, DB_NAME)
+                        .build()
+        );
+    }
+
+    private ListenableFuture<SetSchemaResponse> resetDocuments() {
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().setForceOverride(FORCE_OVERRIDE).build();
+        return Futures.transformAsync(mAppSearchSessionFuture,
+                session -> session.setSchema(request),
+                mExecutor);
+    }
+
+    private ListenableFuture<SetSchemaResponse> setSchema() {
+        try {
+            SetSchemaRequest request = new SetSchemaRequest.Builder().addDocumentClasses(Note.class)
+                    .build();
+            return Futures.transformAsync(mAppSearchSessionFuture,
+                    session -> session.setSchema(request), mExecutor);
+        } catch (AppSearchException e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+}
diff --git a/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/model/Note.java b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/model/Note.java
new file mode 100644
index 0000000..5486965
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/model/Note.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.samples.model;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.core.util.Preconditions;
+
+/**
+ * Encapsulates a Note document.
+ */
+@Document
+public class Note {
+
+    Note(@NonNull String namespace, @NonNull String id, @NonNull String text) {
+        this.id = Preconditions.checkNotNull(id);
+        this.namespace = Preconditions.checkNotNull(namespace);
+        this.text = Preconditions.checkNotNull(text);
+    }
+
+    // TODO (b/181623824): Add m-prefix to fields.
+    @Document.Id
+    private final String id;
+
+    @Document.Namespace private final String namespace;
+
+    @Document.Property(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+    private final String text;
+
+    /** Returns the ID of the {@link Note} object. */
+    @NonNull
+    public String getId() {
+        return id;
+    }
+
+    /** Returns the namespace of the {@link Note} object. */
+    @NonNull
+    public String getNamespace() {
+        return namespace;
+    }
+
+    /** Returns the text of the {@link Note} object. */
+    @NonNull
+    public String getText() {
+        return text;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return text;
+    }
+
+    /**
+     * Builder for {@link Note} objects.
+     *
+     * <p>Once {@link #build} is called, the instance can no longer be used.
+     */
+    public static final class Builder {
+        private String mNamespace = "";
+        private String mId = "";
+        private String mText = "";
+        private boolean mBuilt = false;
+
+        /**
+         * Sets the namespace of the {@link Note} object.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public Note.Builder setNamespace(@NonNull String namespace) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mNamespace = Preconditions.checkNotNull(namespace);
+            return this;
+        }
+
+        /**
+         * Sets the ID of the {@link Note} object.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public Note.Builder setId(@NonNull String id) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mId = Preconditions.checkNotNull(id);
+            return this;
+        }
+
+        /**
+         * Sets the text of the {@link Note} object.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public Note.Builder setText(@NonNull String text) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mText = Preconditions.checkNotNull(text);
+            return this;
+        }
+
+        /**
+         * Creates a new {@link Note} object.
+         *
+         * @throws IllegalStateException if the builder has already been used.
+         */
+        @NonNull
+        public Note build() {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            mBuilt = true;
+            return new Note(mNamespace, mId, mText);
+        }
+    }
+}
diff --git a/appsearch/debug-view/samples/src/main/res/drawable-v24/ic_launcher_foreground.xml b/appsearch/debug-view/samples/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..cb0581c
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,46 @@
+<!--
+  Copyright 2021 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeColor="#00000000"
+        android:strokeWidth="1" />
+</vector>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/drawable/ic_launcher_background.xml b/appsearch/debug-view/samples/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..6dcf0d3
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+</vector>
diff --git a/appsearch/debug-view/samples/src/main/res/layout/activity_notes.xml b/appsearch/debug-view/samples/src/main/res/layout/activity_notes.xml
new file mode 100644
index 0000000..6ce0f0f4
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/layout/activity_notes.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".NotesActivity">
+
+    <TextView
+        android:id="@+id/text_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:text="Loading sample notes..."
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <ListView
+        android:id="@+id/list_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.appcompat.widget.LinearLayoutCompat>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/menu/debug_menu.xml b/appsearch/debug-view/samples/src/main/res/menu/debug_menu.xml
new file mode 100644
index 0000000..6ba449f
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/menu/debug_menu.xml
@@ -0,0 +1,7 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/app_search_debug"
+        android:title="AppSearch Debug View"
+        >
+    </item>
+</menu>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..8da4add9
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..8da4add9
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/appsearch/debug-view/samples/src/main/res/values/colors.xml b/appsearch/debug-view/samples/src/main/res/values/colors.xml
new file mode 100644
index 0000000..cde477b
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+</resources>
\ No newline at end of file
diff --git a/appsearch/debug-view/samples/src/main/res/values/strings.xml b/appsearch/debug-view/samples/src/main/res/values/strings.xml
new file mode 100644
index 0000000..41c6b20
--- /dev/null
+++ b/appsearch/debug-view/samples/src/main/res/values/strings.xml
@@ -0,0 +1,19 @@
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <string name="app_name">AppSearch Debug View Sample App</string>
+</resources>
\ No newline at end of file
diff --git a/appsearch/debug-view/src/main/AndroidManifest.xml b/appsearch/debug-view/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..043ccbf
--- /dev/null
+++ b/appsearch/debug-view/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+<manifest package="androidx.appsearch.debugview" />
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
new file mode 100644
index 0000000..2f81c73
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.localstorage.LocalStorage;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Manages interactions with AppSearch.
+ *
+ * <p>Instances of {@link DebugAppSearchManager} are created by calling {@link #create}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DebugAppSearchManager implements Closeable {
+    private static final int PAGE_SIZE = 100;
+
+    private final Context mContext;
+    private final ExecutorService mExecutor;
+    private final SettableFuture<AppSearchSession> mAppSearchSessionFuture =
+            SettableFuture.create();
+
+    private DebugAppSearchManager(@NonNull Context context, @NonNull ExecutorService executor) {
+        mContext = Preconditions.checkNotNull(context);
+        mExecutor = Preconditions.checkNotNull(executor);
+    }
+
+    /**
+     * Factory for creating a {@link DebugAppSearchManager} instance.
+     *
+     * <p>This factory creates an {@link AppSearchSession} instance with the provided
+     * database name.
+     *
+     * @param context      application context.
+     * @param executor     executor to run AppSearch operations on.
+     * @param databaseName name of the database to open AppSearch debugging for.
+     */
+    @NonNull
+    public static ListenableFuture<DebugAppSearchManager> create(
+            @NonNull Context context,
+            @NonNull ExecutorService executor, @NonNull String databaseName) {
+        Preconditions.checkNotNull(context);
+        Preconditions.checkNotNull(executor);
+        Preconditions.checkNotNull(databaseName);
+
+        DebugAppSearchManager debugAppSearchManager =
+                new DebugAppSearchManager(context, executor);
+
+        return Futures.transform(debugAppSearchManager.initialize(databaseName),
+                unused -> debugAppSearchManager, executor);
+    }
+
+    /**
+     * Searches for all documents in the AppSearch database.
+     *
+     * <p>Each {@link GenericDocument} object is truncated of its properties by adding
+     * projection to the request.
+     *
+     * @return the {@link SearchResults} instance for exploring pages of results. Call
+     * {@link #getNextPage} to retrieve the {@link GenericDocument} objects for each page.
+     */
+    @NonNull
+    public ListenableFuture<SearchResults> getAllDocumentsSearchResults() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setResultCountPerPage(PAGE_SIZE)
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList())
+                .build();
+        String retrieveAllQueryString = "";
+
+        return Futures.transform(mAppSearchSessionFuture,
+                session -> session.search(retrieveAllQueryString, searchSpec), mExecutor);
+    }
+
+
+    /**
+     * Converts the next page from the provided {@link SearchResults} instance to a list of
+     * {@link GenericDocument} objects.
+     *
+     * @param results results to get next page for, and convert to a list of
+     *                {@link GenericDocument} objects.
+     */
+    @NonNull
+    public ListenableFuture<List<GenericDocument>> getNextPage(@NonNull SearchResults results) {
+        Preconditions.checkNotNull(results);
+
+        return Futures.transform(results.getNextPage(),
+                DebugAppSearchManager::convertResultsToGenericDocuments, mExecutor);
+    }
+
+    /**
+     * Closes the AppSearch session.
+     */
+    @Override
+    public void close() {
+        Futures.whenAllSucceed(mAppSearchSessionFuture).call(() -> {
+            Futures.getDone(mAppSearchSessionFuture).close();
+            return null;
+        }, mExecutor);
+    }
+
+    @NonNull
+    private ListenableFuture<AppSearchSession> initialize(@NonNull String databaseName) {
+        mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSession(
+                new LocalStorage.SearchContext.Builder(mContext, databaseName)
+                        .build())
+        );
+        return mAppSearchSessionFuture;
+    }
+
+    private static List<GenericDocument> convertResultsToGenericDocuments(
+            List<SearchResult> results) {
+        List<GenericDocument> docs = new ArrayList<>(results.size());
+
+        for (SearchResult result : results) {
+            docs.add(result.getGenericDocument());
+        }
+
+        return docs;
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
new file mode 100644
index 0000000..9461e87
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/model/DocumentListModel.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.model;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.core.util.Preconditions;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Documents ViewModel for the database's {@link GenericDocument} objects.
+ *
+ * <p>This model captures the data for displaying lists of {@link GenericDocument} objects. Each
+ * {@link GenericDocument} object is truncated of all properties.
+ *
+ * <p>Instances of {@link DocumentListModel} are created by {@link DocumentListModelFactory}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DocumentListModel extends ViewModel {
+    private static final String TAG = "DocumentListModel";
+
+    private final ExecutorService mExecutor;
+    private final DebugAppSearchManager mDebugAppSearchManager;
+    final MutableLiveData<List<GenericDocument>> mDocumentsLiveData =
+            new MutableLiveData<>();
+    final MutableLiveData<SearchResults> mDocumentsSearchResultsLiveData =
+            new MutableLiveData<>();
+
+    public DocumentListModel(@NonNull ExecutorService executor,
+            @NonNull DebugAppSearchManager debugAppSearchManager) {
+        mExecutor = Preconditions.checkNotNull(executor);
+        mDebugAppSearchManager = Preconditions.checkNotNull(debugAppSearchManager);
+    }
+
+    /**
+     * Gets the {@link SearchResults} instance for a search over all documents in the AppSearch
+     * database.
+     *
+     * <p>Call {@link #addAdditionalResultsPage} to get the next page of documents from the
+     * {@link SearchResults} instance.
+     */
+    @NonNull
+    public LiveData<SearchResults> getAllDocumentsSearchResults() {
+        Futures.addCallback(mDebugAppSearchManager.getAllDocumentsSearchResults(),
+                new FutureCallback<SearchResults>() {
+                    @Override
+                    public void onSuccess(SearchResults result) {
+                        mDocumentsSearchResultsLiveData.postValue(result);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Log.e(TAG, "Failed to get all documents.", t);
+                    }
+                }, mExecutor);
+        return mDocumentsSearchResultsLiveData;
+    }
+
+    /**
+     * Adds the next page of documents for the provided {@link SearchResults} instance to the
+     * running list of retrieved {@link GenericDocument} objects.
+     *
+     * <p>Each page is represented as a list of {@link GenericDocument} objects.
+     *
+     * @return a {@link LiveData} encapsulating the list of {@link GenericDocument} objects for
+     * documents retrieved from all previous pages and this additional page.
+     */
+    @NonNull
+    public LiveData<List<GenericDocument>> addAdditionalResultsPage(
+            @NonNull SearchResults results) {
+        Futures.addCallback(mDebugAppSearchManager.getNextPage(results),
+                new FutureCallback<List<GenericDocument>>() {
+                    @Override
+                    public void onSuccess(List<GenericDocument> result) {
+                        if (mDocumentsLiveData.getValue() == null) {
+                            mDocumentsLiveData.postValue(result);
+                        } else {
+                            mDocumentsLiveData.getValue().addAll(result);
+                            mDocumentsLiveData.postValue(mDocumentsLiveData.getValue());
+                        }
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Log.e(TAG, "Failed to get next page of documents.", t);
+                    }
+                }, mExecutor);
+
+        return mDocumentsLiveData;
+    }
+
+    /**
+     * Factory for creating a {@link DocumentListModel} instance.
+     */
+    public static class DocumentListModelFactory extends ViewModelProvider.NewInstanceFactory {
+        private final DebugAppSearchManager mDebugAppSearchManager;
+        private final ListeningExecutorService mExecutorService;
+
+        public DocumentListModelFactory(@NonNull ListeningExecutorService executor,
+                @NonNull DebugAppSearchManager debugAppSearchManager) {
+            mDebugAppSearchManager = debugAppSearchManager;
+            mExecutorService = executor;
+        }
+
+        @SuppressWarnings("unchecked")
+        @NonNull
+        @Override
+        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+            if (modelClass == DocumentListModel.class) {
+                return (T) new DocumentListModel(mExecutorService, mDebugAppSearchManager);
+            } else {
+                throw new IllegalArgumentException("Expected class: DocumentListModel.");
+            }
+        }
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
new file mode 100644
index 0000000..bd2aec7
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.appsearch.debugview.R;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.Executors;
+
+/**
+ * Debug Activity for AppSearch.
+ *
+ * <p>This activity provides a view of all the documents that have been put into an application's
+ * AppSearch database. The database is specified by creating an {@link android.content.Intent}
+ * with a {@code String} extra containing key: {@code databaseName} and value: name of AppSearch
+ * database.
+ *
+ * <p>To launch this activity, declare it in the application's manifest:
+ * <pre>
+ *     <activity android:name="androidx.appsearch.debugview.view.AppSearchDebugActivity" />
+ * </pre>
+ *
+ * <p>Next, create an {@link android.content.Intent} with the {@code databaseName} to view
+ * documents for, and start the activity:
+ * <pre>
+ *     Intent intent = new Intent(this, AppSearchDebugActivity.class);
+ *     intent.putExtra("databaseName", DB_NAME);
+ *     startActivity(intent);
+ * </pre>
+ *
+ * <p><b>Note:</b> Debugging is currently only compatible with local storage.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class AppSearchDebugActivity extends AppCompatActivity {
+    private static final String DB_INTENT_KEY = "databaseName";
+
+    private String mDbName;
+    private ListenableFuture<DebugAppSearchManager> mDebugAppSearchManager;
+    private ListeningExecutorService mBackgroundExecutor;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_appsearchdebug);
+
+        mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        mDbName = getIntent().getExtras().getString(DB_INTENT_KEY);
+        mDebugAppSearchManager = DebugAppSearchManager.create(
+                getApplicationContext(), mBackgroundExecutor, mDbName);
+    }
+
+    @Override
+    protected void onStop() {
+        Futures.whenAllSucceed(mDebugAppSearchManager).call(() -> {
+            Futures.getDone(mDebugAppSearchManager).close();
+            return null;
+        }, mBackgroundExecutor);
+
+        super.onStop();
+    }
+
+    /**
+     * Gets the {@link DebugAppSearchManager} instance created by the activity.
+     */
+    @NonNull
+    public ListenableFuture<DebugAppSearchManager> getDebugAppSearchManager() {
+        return mDebugAppSearchManager;
+    }
+
+    /**
+     * Gets the {@link ListeningExecutorService} instance created by the activity.
+     */
+    @NonNull
+    public ListeningExecutorService getBackgroundExecutor() {
+        return mBackgroundExecutor;
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListFragment.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListFragment.java
new file mode 100644
index 0000000..9d6444d
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListFragment.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.debugview.DebugAppSearchManager;
+import androidx.appsearch.debugview.R;
+import androidx.appsearch.debugview.model.DocumentListModel;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.util.ArrayList;
+
+/**
+ * A fragment for displaying a list of {@link GenericDocument} objects.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DocumentListFragment extends Fragment {
+    private static final String TAG = "DocumentListFragment";
+
+    private TextView mLoadingView;
+    private TextView mEmptyDocumentsView;
+    private RecyclerView mDocumentListRecyclerView;
+    private ListeningExecutorService mExecutor;
+    private ListenableFuture<DebugAppSearchManager> mDebugAppSearchManager;
+    private AppSearchDebugActivity mAppSearchDebugActivity;
+
+    protected int mPrevDocsSize = 0;
+    protected boolean mLoadingPage = false;
+    protected boolean mAdditionalPages = true;
+
+    @Nullable
+    protected DocumentListModel mDocumentListModel;
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_document_list, container, /*attachToRoot=*/
+                false);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        mLoadingView = getView().findViewById(R.id.loading_text_view);
+        mEmptyDocumentsView = getView().findViewById(R.id.empty_documents_text_view);
+        mDocumentListRecyclerView = getView().findViewById(R.id.document_list_recycler_view);
+
+        mAppSearchDebugActivity = (AppSearchDebugActivity) getActivity();
+        mExecutor = mAppSearchDebugActivity.getBackgroundExecutor();
+        mDebugAppSearchManager = mAppSearchDebugActivity.getDebugAppSearchManager();
+
+        Futures.addCallback(mDebugAppSearchManager,
+                new FutureCallback<DebugAppSearchManager>() {
+                    @Override
+                    public void onSuccess(DebugAppSearchManager debugAppSearchManager) {
+                        readDocuments(debugAppSearchManager);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Toast.makeText(getContext(),
+                                "Failed to initialize AppSearch: " + t.getMessage(),
+                                Toast.LENGTH_LONG).show();
+                        Log.e(TAG,
+                                "Failed to initialize AppSearch. Verify that the database name "
+                                        + "has been"
+                                        + " provided in the intent with key: databaseName", t);
+                    }
+                }, ContextCompat.getMainExecutor(mAppSearchDebugActivity));
+    }
+
+    /**
+     * Initializes a {@link DocumentListModel} ViewModel instance and sets observer for updating UI
+     * with document data.
+     */
+    protected void readDocuments(@NonNull DebugAppSearchManager debugAppSearchManager) {
+        mDocumentListModel =
+                new ViewModelProvider(this,
+                        new DocumentListModel.DocumentListModelFactory(mExecutor,
+                                debugAppSearchManager)).get(DocumentListModel.class);
+
+        mDocumentListModel.getAllDocumentsSearchResults().observe(this, results -> {
+            mLoadingView.setVisibility(View.GONE);
+            initDocumentListRecyclerView(results);
+        });
+    }
+
+    private void initDocumentListRecyclerView(@NonNull SearchResults searchResults) {
+        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(mAppSearchDebugActivity);
+        linearLayoutManager.setOrientation(RecyclerView.VERTICAL);
+
+        DocumentListItemAdapter documentListItemAdapter = new DocumentListItemAdapter(
+                new ArrayList<>());
+        mDocumentListRecyclerView.setAdapter(documentListItemAdapter);
+
+        mDocumentListRecyclerView.setLayoutManager(linearLayoutManager);
+        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(
+                mAppSearchDebugActivity, linearLayoutManager.getOrientation());
+        mDocumentListRecyclerView.addItemDecoration(dividerItemDecoration);
+
+        mDocumentListModel.addAdditionalResultsPage(searchResults).observe(this, docs -> {
+            if (docs.size() == 0) {
+                mEmptyDocumentsView.setVisibility(View.VISIBLE);
+                mDocumentListRecyclerView.setVisibility(View.GONE);
+            }
+            // Check if there are additional documents still being added.
+            if (docs.size() - mPrevDocsSize == 0) {
+                mAdditionalPages = false;
+                return;
+            }
+            documentListItemAdapter.setDocuments(docs);
+            mPrevDocsSize = docs.size();
+            mLoadingPage = false;
+        });
+
+        mDocumentListRecyclerView.addOnScrollListener(
+                new ScrollListener(linearLayoutManager) {
+                    @Override
+                    public void loadNextPage() {
+                        mLoadingPage = true;
+                        mDocumentListModel.addAdditionalResultsPage(searchResults);
+                    }
+
+                    @Override
+                    public boolean isLoading() {
+                        return mLoadingPage;
+                    }
+
+                    @Override
+                    public boolean hasAdditionalPages() {
+                        return mAdditionalPages;
+                    }
+                });
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListItemAdapter.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListItemAdapter.java
new file mode 100644
index 0000000..8ce7756
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/DocumentListItemAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.debugview.R;
+import androidx.core.util.Preconditions;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+/**
+ * Adapter for displaying a list of {@link GenericDocument} objects.
+ *
+ * <p>This adapter displays each item as a namespace and document ID.
+ *
+ * <p>Documents can be manually changed by calling {@link #setDocuments}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DocumentListItemAdapter extends
+        RecyclerView.Adapter<DocumentListItemAdapter.ViewHolder> {
+    private List<GenericDocument> mDocuments;
+
+    DocumentListItemAdapter(@NonNull List<GenericDocument> documents) {
+        mDocuments = Preconditions.checkNotNull(documents);
+    }
+
+    /**
+     * Sets the adapter's document list.
+     *
+     * @param documents list of {@link GenericDocument} objects to update adapter with.
+     */
+    public void setDocuments(@NonNull List<GenericDocument> documents) {
+        mDocuments = Preconditions.checkNotNull(documents);
+        notifyDataSetChanged();
+    }
+
+    @NonNull
+    @Override
+    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View view = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.adapter_document_list_item, parent, /*attachToRoot=*/false);
+
+        return new ViewHolder(view);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+        holder.getNamespaceLabel().setText(
+                "Namespace: " + "\"" + mDocuments.get(position).getNamespace() + "\"");
+        holder.getIdLabel().setText("ID: " + "\"" + mDocuments.get(position).getId() + "\"");
+    }
+
+    @Override
+    public int getItemCount() {
+        return mDocuments.size();
+    }
+
+    /**
+     * ViewHolder for {@link DocumentListItemAdapter}.
+     */
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        private final TextView mNamespaceLabel;
+        private final TextView mIdLabel;
+
+        public ViewHolder(@NonNull View view) {
+            super(view);
+
+            Preconditions.checkNotNull(view);
+
+            mNamespaceLabel = (TextView) view.findViewById(R.id.doc_item_namespace);
+            mIdLabel = (TextView) view.findViewById(R.id.doc_item_id);
+        }
+
+        @NonNull
+        public TextView getNamespaceLabel() {
+            return mNamespaceLabel;
+        }
+
+        @NonNull
+        public TextView getIdLabel() {
+            return mIdLabel;
+        }
+    }
+}
diff --git a/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/ScrollListener.java b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/ScrollListener.java
new file mode 100644
index 0000000..a2dd0e98
--- /dev/null
+++ b/appsearch/debug-view/src/main/java/androidx/appsearch/debugview/view/ScrollListener.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.debugview.view;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Listens for scrolling and loads the next page of results if the end of the view is reached.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public abstract class ScrollListener extends RecyclerView.OnScrollListener {
+    private final LinearLayoutManager mLayoutManager;
+
+    public ScrollListener(@NonNull LinearLayoutManager layoutManager) {
+        mLayoutManager = Preconditions.checkNotNull(layoutManager);
+    }
+
+    @Override
+    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+        super.onScrolled(recyclerView, dx, dy);
+
+        int itemsVisible = mLayoutManager.getChildCount();
+        int totalItems = mLayoutManager.getItemCount();
+        int firstItemInViewIndex = mLayoutManager.findFirstVisibleItemPosition();
+
+        // This value is true when the RecyclerView has additional rows that can be filled and
+        // the underlying adapter does not have sufficient items to fill them.
+        boolean hasAdditionalRowsToFill = (firstItemInViewIndex + itemsVisible) >= totalItems;
+
+        if (!isLoading() && hasAdditionalPages()) {
+            if (hasAdditionalRowsToFill && firstItemInViewIndex >= 0) {
+                loadNextPage();
+            }
+        }
+    }
+
+    /**
+     * Defines how to load the next page of results to display.
+     */
+    public abstract void loadNextPage();
+
+    /**
+     * Indicates whether a page is currently be loading.
+     *
+     * <p>{@link #loadNextPage()} will not be called if this is {@code true}.
+     */
+    public abstract boolean isLoading();
+
+    /**
+     * Indicates whether there are additional pages to load.
+     *
+     * <p>{@link #loadNextPage()} will not be called if this is {@code true}.
+     */
+    public abstract boolean hasAdditionalPages();
+}
diff --git a/appsearch/debug-view/src/main/res/layout/activity_appsearchdebug.xml b/appsearch/debug-view/src/main/res/layout/activity_appsearchdebug.xml
new file mode 100644
index 0000000..e33938c
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/activity_appsearchdebug.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".view.AppSearchDebugActivity" >
+
+    <androidx.fragment.app.FragmentContainerView
+        android:name="androidx.navigation.fragment.NavHostFragment"
+        app:navGraph="@navigation/document_list_graph"
+        app:defaultNavHost="true"
+        android:id="@+id/fragment_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</LinearLayout>
diff --git a/appsearch/debug-view/src/main/res/layout/adapter_document_list_item.xml b/appsearch/debug-view/src/main/res/layout/adapter_document_list_item.xml
new file mode 100644
index 0000000..7060743
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/adapter_document_list_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="2px"
+    android:minHeight="42px" >
+
+    <TextView
+        android:id="@+id/doc_item_namespace"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <TextView
+        android:id="@+id/doc_item_id"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</LinearLayout>
diff --git a/appsearch/debug-view/src/main/res/layout/fragment_document_list.xml b/appsearch/debug-view/src/main/res/layout/fragment_document_list.xml
new file mode 100644
index 0000000..8b197c6
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/layout/fragment_document_list.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".view.AppSearchDebugActivity" >
+
+    <TextView
+        android:id="@+id/loading_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:text="Loading AppSearch documents..."
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/document_list_recycler_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/empty_documents_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:text="@string/appsearch_no_documents_error"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/appsearch/debug-view/src/main/res/navigation/document_list_graph.xml b/appsearch/debug-view/src/main/res/navigation/document_list_graph.xml
new file mode 100644
index 0000000..efb953b
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/navigation/document_list_graph.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+             xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/document_list_graph"
+    app:startDestination="@id/documentListFragment">
+
+    <fragment
+        android:id="@+id/documentListFragment"
+        android:name="androidx.appsearch.debugview.view.DocumentListFragment"
+        android:label="fragment_documents"
+        tools:layout="@layout/fragment_document_list" >
+    </fragment>
+
+</navigation>
diff --git a/appsearch/debug-view/src/main/res/values/strings.xml b/appsearch/debug-view/src/main/res/values/strings.xml
new file mode 100644
index 0000000..9f894b2
--- /dev/null
+++ b/appsearch/debug-view/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+<!--
+  Copyright 2021 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.
+  -->
+
+<resources>
+    <string name="appsearch_no_documents_error">No AppSearch documents found in database.
+    </string>
+</resources>
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index c7b91f5..af86a4c 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -50,6 +50,7 @@
     def __init__(self, jetpack_appsearch_root, framework_appsearch_root):
         self._jetpack_appsearch_root = jetpack_appsearch_root
         self._framework_appsearch_root = framework_appsearch_root
+        self._written_files = []
 
     def _PruneDir(self, dir_to_prune):
         for walk_path, walk_folders, walk_files in os.walk(dir_to_prune):
@@ -74,18 +75,27 @@
         with open(dest_path, 'w') as fh:
             fh.write(contents)
 
-        # Run formatter
-        google_java_format_cmd = [GOOGLE_JAVA_FORMAT, '--aosp', '-i', dest_path]
-        print('$ ' + ' '.join(google_java_format_cmd))
-        subprocess.check_call(google_java_format_cmd, cwd=self._framework_appsearch_root)
+        # Save file for future formatting
+        self._written_files.append(dest_path)
 
     def _TransformCommonCode(self, contents):
-        # Apply strips
+        # Apply stripping
         contents = re.sub(
                 r'\/\/ @exportToFramework:startStrip\(\).*?\/\/ @exportToFramework:endStrip\(\)',
                 '',
                 contents,
                 flags=re.DOTALL)
+
+        # Add additional imports if required
+        imports_to_add = []
+        if '@exportToFramework:CurrentTimeMillisLong' in contents:
+            imports_to_add.append('android.annotation.CurrentTimeMillisLong')
+        for import_to_add in imports_to_add:
+            contents = re.sub(
+                    r'^(\s*package [^;]+;\s*)$', r'\1\nimport %s;\n' % import_to_add, contents,
+                    flags=re.MULTILINE)
+
+        # Apply in-place replacements
         return (contents
             .replace('androidx.appsearch.app', 'android.app.appsearch')
             .replace(
@@ -104,13 +114,17 @@
             .replace(
                     'androidx.core.util.ObjectsCompat',
                     'java.util.Objects')
+            # Preconditions.checkNotNull is replaced with Objects.requireNonNull. We add both
+            # imports and let google-java-format sort out which one is unused.
             .replace(
-                    'androidx.core.util.Preconditions',
-                    'com.android.internal.util.Preconditions')
+                    'import androidx.core.util.Preconditions;',
+                    'import java.util.Objects; import com.android.internal.util.Preconditions;')
             .replace('import androidx.annotation.RestrictTo;', '')
             .replace('@RestrictTo(RestrictTo.Scope.LIBRARY)', '')
             .replace('@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)', '')
+            .replace('Preconditions.checkNotNull(', 'Objects.requireNonNull(')
             .replace('ObjectsCompat.', 'Objects.')
+            .replace('/*@exportToFramework:CurrentTimeMillisLong*/', '@CurrentTimeMillisLong')
             .replace('// @exportToFramework:skipFile()', '')
         )
 
@@ -254,9 +268,15 @@
         self._TransformAndCopyFolder(
                 impl_test_source_dir, impl_test_dest_dir, transform_func=_TransformImplTestCode)
 
+    def _FormatWrittenFiles(self):
+        google_java_format_cmd = [GOOGLE_JAVA_FORMAT, '--aosp', '-i'] + self._written_files
+        print('$ ' + ' '.join(google_java_format_cmd))
+        subprocess.check_call(google_java_format_cmd, cwd=self._framework_appsearch_root)
+
     def ExportCode(self):
         self._ExportApiCode()
         self._ExportImplCode()
+        self._FormatWrittenFiles()
 
     def WriteChangeIdFile(self, changeid):
         """Copies the changeid of the most recent public CL into a file on the framework side.
diff --git a/appsearch/local-storage/api/current.txt b/appsearch/local-storage/api/current.txt
index e0d39ea..5eb0af5 100644
--- a/appsearch/local-storage/api/current.txt
+++ b/appsearch/local-storage/api/current.txt
@@ -5,22 +5,15 @@
     method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
   }
 
-  public static final class LocalStorage.GlobalSearchContext {
-  }
-
-  public static final class LocalStorage.GlobalSearchContext.Builder {
-    ctor public LocalStorage.GlobalSearchContext.Builder(android.content.Context);
-    method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext build();
-  }
-
   public static final class LocalStorage.SearchContext {
     method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
   }
 
   public static final class LocalStorage.SearchContext.Builder {
-    ctor public LocalStorage.SearchContext.Builder(android.content.Context);
+    ctor public LocalStorage.SearchContext.Builder(android.content.Context, String);
     method public androidx.appsearch.localstorage.LocalStorage.SearchContext build();
-    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setDatabaseName(String);
+    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
   }
 
 }
diff --git a/appsearch/local-storage/api/public_plus_experimental_current.txt b/appsearch/local-storage/api/public_plus_experimental_current.txt
index e0d39ea..5eb0af5 100644
--- a/appsearch/local-storage/api/public_plus_experimental_current.txt
+++ b/appsearch/local-storage/api/public_plus_experimental_current.txt
@@ -5,22 +5,15 @@
     method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
   }
 
-  public static final class LocalStorage.GlobalSearchContext {
-  }
-
-  public static final class LocalStorage.GlobalSearchContext.Builder {
-    ctor public LocalStorage.GlobalSearchContext.Builder(android.content.Context);
-    method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext build();
-  }
-
   public static final class LocalStorage.SearchContext {
     method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
   }
 
   public static final class LocalStorage.SearchContext.Builder {
-    ctor public LocalStorage.SearchContext.Builder(android.content.Context);
+    ctor public LocalStorage.SearchContext.Builder(android.content.Context, String);
     method public androidx.appsearch.localstorage.LocalStorage.SearchContext build();
-    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setDatabaseName(String);
+    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
   }
 
 }
diff --git a/appsearch/local-storage/api/restricted_current.txt b/appsearch/local-storage/api/restricted_current.txt
index e0d39ea..5eb0af5 100644
--- a/appsearch/local-storage/api/restricted_current.txt
+++ b/appsearch/local-storage/api/restricted_current.txt
@@ -5,22 +5,15 @@
     method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.localstorage.LocalStorage.SearchContext);
   }
 
-  public static final class LocalStorage.GlobalSearchContext {
-  }
-
-  public static final class LocalStorage.GlobalSearchContext.Builder {
-    ctor public LocalStorage.GlobalSearchContext.Builder(android.content.Context);
-    method public androidx.appsearch.localstorage.LocalStorage.GlobalSearchContext build();
-  }
-
   public static final class LocalStorage.SearchContext {
     method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
   }
 
   public static final class LocalStorage.SearchContext.Builder {
-    ctor public LocalStorage.SearchContext.Builder(android.content.Context);
+    ctor public LocalStorage.SearchContext.Builder(android.content.Context, String);
     method public androidx.appsearch.localstorage.LocalStorage.SearchContext build();
-    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setDatabaseName(String);
+    method public androidx.appsearch.localstorage.LocalStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
   }
 
 }
diff --git a/appsearch/local-storage/build.gradle b/appsearch/local-storage/build.gradle
index 62ccb52..d486b96 100644
--- a/appsearch/local-storage/build.gradle
+++ b/appsearch/local-storage/build.gradle
@@ -53,6 +53,7 @@
                 targets "icing"
             }
         }
+	multiDexEnabled true
     }
     externalNativeBuild {
         cmake {
@@ -69,6 +70,8 @@
 )
 
 dependencies {
+    def multidex_version = "2.0.1"
+
     releaseBundleInside(project(path: ":icing", configuration: "exportRelease"))
     debugBundleInside(project(path: ":icing", configuration: "exportDebug"))
 
@@ -77,10 +80,12 @@
     implementation(project(":appsearch:appsearch"))
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
     implementation("androidx.core:core:1.2.0")
+    implementation "androidx.multidex:multidex:$multidex_version"
 
     androidTestImplementation(ANDROIDX_TEST_CORE)
     androidTestImplementation(ANDROIDX_TEST_RULES)
     androidTestImplementation(TRUTH)
+    androidTestImplementation(MOCKITO_ANDROID)
     //TODO(b/149787478) upgrade and link to Dependencies.kt
     androidTestImplementation("junit:junit:4.13")
 }
diff --git a/appsearch/local-storage/proguard-rules.pro b/appsearch/local-storage/proguard-rules.pro
index b18f891..82c4b719 100644
--- a/appsearch/local-storage/proguard-rules.pro
+++ b/appsearch/local-storage/proguard-rules.pro
@@ -18,3 +18,13 @@
 -keepclassmembers class * extends com.google.android.icing.protobuf.GeneratedMessageLite {
   <fields>;
 }
+-keep class com.google.android.icing.BreakIteratorBatcher { *; }
+-keepclassmembers public class com.google.android.icing.IcingSearchEngine {
+  private long nativePointer;
+}
+
+# This prevents the names of native methods from being obfuscated and prevents
+# UnsatisfiedLinkErrors.
+-keepclasseswithmembernames,includedescriptorclasses class * {
+  native <methods>;
+}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index 4ce9593..c68de2a 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -16,21 +16,33 @@
 
 package androidx.appsearch.localstorage;
 
+import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument;
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
 
+import android.content.Context;
+
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.StorageInfo;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter;
-import androidx.appsearch.localstorage.converter.SchemaToProtoConverter;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.test.core.app.ApplicationProvider;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.GetOptimizeInfoResultProto;
+import com.google.android.icing.proto.PersistType;
 import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.PropertyProto;
 import com.google.android.icing.proto.SchemaProto;
@@ -40,6 +52,7 @@
 import com.google.android.icing.proto.StringIndexingConfig;
 import com.google.android.icing.proto.TermMatchType;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
 import org.junit.Before;
@@ -47,33 +60,27 @@
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 public class AppSearchImplTest {
     @Rule
     public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
     private AppSearchImpl mAppSearchImpl;
-    private SchemaTypeConfigProto mVisibilitySchemaProto;
 
     @Before
     public void setUp() throws Exception {
-        mAppSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder());
+        Context context = ApplicationProvider.getApplicationContext();
 
-        AppSearchSchema visibilitySchema = VisibilityStore.SCHEMA;
-
-        // We need to rewrite the schema type to follow AppSearchImpl's prefixing scheme.
-        AppSearchSchema.Builder rewrittenVisibilitySchema =
-                new AppSearchSchema.Builder(AppSearchImpl.createPrefix(VisibilityStore.PACKAGE_NAME,
-                        VisibilityStore.DATABASE_NAME) + VisibilityStore.SCHEMA_TYPE);
-        List<AppSearchSchema.PropertyConfig> visibilityProperties =
-                visibilitySchema.getProperties();
-        for (AppSearchSchema.PropertyConfig property : visibilityProperties) {
-            rewrittenVisibilitySchema.addProperty(property);
-        }
-        mVisibilitySchemaProto =
-                SchemaToProtoConverter.toSchemaTypeConfigProto(rewrittenVisibilitySchema.build());
+        // Give ourselves global query permissions
+        mAppSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder(),
+                context, VisibilityStore.NO_OP_USER_ID,
+                /*globalQuerierPackage=*/ context.getPackageName(),
+                /*logger=*/ null);
     }
 
     //TODO(b/175430168) add test to verify reset is working properly.
@@ -92,38 +99,52 @@
         // Create a copy so we can modify it.
         List<SchemaTypeConfigProto> existingTypes =
                 new ArrayList<>(existingSchemaBuilder.getTypesList());
-
-        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)
-                                .setStringIndexingConfig(StringIndexingConfig.newBuilder()
-                                        .setTokenizerType(
-                                                StringIndexingConfig.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")
+        SchemaTypeConfigProto schemaTypeConfigProto1 = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType("Foo").build();
+        SchemaTypeConfigProto schemaTypeConfigProto2 = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType("TestType")
+                .addProperties(PropertyConfigProto.newBuilder()
+                        .setPropertyName("subject")
+                        .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                        .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                        .setStringIndexingConfig(StringIndexingConfig.newBuilder()
+                                .setTokenizerType(
+                                        StringIndexingConfig.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();
+        SchemaTypeConfigProto schemaTypeConfigProto3 = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType("RefType").build();
+        SchemaProto newSchema = SchemaProto.newBuilder()
+                .addTypes(schemaTypeConfigProto1)
+                .addTypes(schemaTypeConfigProto2)
+                .addTypes(schemaTypeConfigProto3)
+                .build();
 
         AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = mAppSearchImpl.rewriteSchema(
-                AppSearchImpl.createPrefix("package", "newDatabase"), existingSchemaBuilder,
+                createPrefix("package", "newDatabase"), existingSchemaBuilder,
                 newSchema);
 
         // We rewrote all the new types that were added. And nothing was removed.
-        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes)
-                .containsExactly("package$newDatabase/Foo", "package$newDatabase/TestType");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet()).containsExactly(
+                "package$newDatabase/Foo", "package$newDatabase/TestType",
+                "package$newDatabase/RefType");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.get(
+                "package$newDatabase/Foo").getSchemaType()).isEqualTo(
+                "package$newDatabase/Foo");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.get(
+                "package$newDatabase/TestType").getSchemaType()).isEqualTo(
+                "package$newDatabase/TestType");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.get(
+                "package$newDatabase/RefType").getSchemaType()).isEqualTo(
+                "package$newDatabase/RefType");
         assertThat(rewrittenSchemaResults.mDeletedPrefixedTypes).isEmpty();
 
         SchemaProto expectedSchema = SchemaProto.newBuilder()
@@ -148,6 +169,8 @@
                                 .setSchemaType("package$newDatabase/RefType")
                                 .build()
                         ).build())
+                .addTypes(SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$newDatabase/RefType").build())
                 .build();
 
         existingTypes.addAll(expectedSchema.getTypesList());
@@ -170,12 +193,12 @@
                 .build();
 
         AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = mAppSearchImpl.rewriteSchema(
-                AppSearchImpl.createPrefix("package", "existingDatabase"), existingSchemaBuilder,
+                createPrefix("package", "existingDatabase"), existingSchemaBuilder,
                 newSchema);
 
         // Nothing was removed, but the method did rewrite the type name.
-        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes)
-                .containsExactly("package$existingDatabase/Foo");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet()).containsExactly(
+                "package$existingDatabase/Foo");
         assertThat(rewrittenSchemaResults.mDeletedPrefixedTypes).isEmpty();
 
         // Same schema since nothing was added.
@@ -200,13 +223,14 @@
                 .build();
 
         AppSearchImpl.RewrittenSchemaResults rewrittenSchemaResults = mAppSearchImpl.rewriteSchema(
-                AppSearchImpl.createPrefix("package", "existingDatabase"), existingSchemaBuilder,
+                createPrefix("package", "existingDatabase"), existingSchemaBuilder,
                 newSchema);
 
         // Bar type was rewritten, but Foo ended up being deleted since it wasn't included in the
         // new schema.
         assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes)
-                .containsExactly("package$existingDatabase/Bar");
+                .containsKey("package$existingDatabase/Bar");
+        assertThat(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet().size()).isEqualTo(1);
         assertThat(rewrittenSchemaResults.mDeletedPrefixedTypes)
                 .containsExactly("package$existingDatabase/Foo");
 
@@ -223,31 +247,31 @@
     @Test
     public void testAddDocumentTypePrefix() {
         DocumentProto insideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("type")
                 .setNamespace("namespace")
                 .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("type")
                 .setNamespace("namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
                 .build();
 
         DocumentProto expectedInsideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("package$databaseName/type")
                 .setNamespace("package$databaseName/namespace")
                 .build();
         DocumentProto expectedDocumentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("package$databaseName/type")
                 .setNamespace("package$databaseName/namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(expectedInsideDocument))
                 .build();
 
         DocumentProto.Builder actualDocument = documentProto.toBuilder();
-        mAppSearchImpl.addPrefixToDocument(actualDocument, AppSearchImpl.createPrefix("package",
+        addPrefixToDocument(actualDocument, createPrefix("package",
                 "databaseName"));
         assertThat(actualDocument.build()).isEqualTo(expectedDocumentProto);
     }
@@ -255,32 +279,32 @@
     @Test
     public void testRemoveDocumentTypePrefixes() throws Exception {
         DocumentProto insideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("package$databaseName/type")
                 .setNamespace("package$databaseName/namespace")
                 .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("package$databaseName/type")
                 .setNamespace("package$databaseName/namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
                 .build();
 
         DocumentProto expectedInsideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("type")
                 .setNamespace("namespace")
                 .build();
 
         DocumentProto expectedDocumentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("type")
                 .setNamespace("namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(expectedInsideDocument))
                 .build();
 
         DocumentProto.Builder actualDocument = documentProto.toBuilder();
-        assertThat(mAppSearchImpl.removePrefixesFromDocument(actualDocument)).isEqualTo(
+        assertThat(removePrefixesFromDocument(actualDocument)).isEqualTo(
                 "package$databaseName/");
         assertThat(actualDocument.build()).isEqualTo(expectedDocumentProto);
     }
@@ -289,14 +313,14 @@
     public void testRemoveDatabasesFromDocumentThrowsException() throws Exception {
         // Set two different database names in the document, which should never happen
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("prefix1/type")
                 .setNamespace("prefix2/namespace")
                 .build();
 
         DocumentProto.Builder actualDocument = documentProto.toBuilder();
         AppSearchException e = assertThrows(AppSearchException.class, () ->
-                mAppSearchImpl.removePrefixesFromDocument(actualDocument));
+                removePrefixesFromDocument(actualDocument));
         assertThat(e).hasMessageThat().contains("Found unexpected multiple prefix names");
     }
 
@@ -305,12 +329,12 @@
         // Set two different database names in the outer and inner document, which should never
         // happen.
         DocumentProto insideDocument = DocumentProto.newBuilder()
-                .setUri("inside-uri")
+                .setUri("inside-id")
                 .setSchema("prefix1/type")
                 .setNamespace("prefix1/namespace")
                 .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri("uri")
+                .setUri("id")
                 .setSchema("prefix2/type")
                 .setNamespace("prefix2/namespace")
                 .addProperties(PropertyProto.newBuilder().addDocumentValues(insideDocument))
@@ -318,7 +342,7 @@
 
         DocumentProto.Builder actualDocument = documentProto.toBuilder();
         AppSearchException e = assertThrows(AppSearchException.class, () ->
-                mAppSearchImpl.removePrefixesFromDocument(actualDocument));
+                removePrefixesFromDocument(actualDocument));
         assertThat(e).hasMessageThat().contains("Found unexpected multiple prefix names");
     }
 
@@ -328,38 +352,48 @@
         List<AppSearchSchema> schemas =
                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
         mAppSearchImpl.setSchema("package", "database", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
 
         // Insert enough documents.
         for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
                 + AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
             GenericDocument document =
-                    new GenericDocument.Builder<>("uri" + i, "type").setNamespace(
-                            "namespace").build();
-            mAppSearchImpl.putDocument("package", "database", document);
+                    new GenericDocument.Builder<>("namespace", "id" + i, "type").build();
+            mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
         }
 
         // Check optimize() will release 0 docs since there is no deletion.
         GetOptimizeInfoResultProto optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
         assertThat(optimizeInfo.getOptimizableDocs()).isEqualTo(0);
 
-        // delete 999 documents , we will reach the threshold to trigger optimize() in next
+        // 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("package", "database", "namespace", "uri" + i);
+            mAppSearchImpl.remove("package", "database", "namespace", "id" + i);
         }
 
-        // optimize() still not be triggered since we are in the interval to call getOptimizeInfo()
+        // Updates the check for optimize counter, checkForOptimize() will be triggered since
+        // CHECK_OPTIMIZE_INTERVAL is reached but optimize() won't since
+        // OPTIMIZE_THRESHOLD_DOC_COUNT is not.
+        mAppSearchImpl.checkForOptimize(
+                /*mutateBatchSize=*/ AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT - 1);
+
+        // Verify optimize() still not be triggered.
         optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
         assertThat(optimizeInfo.getOptimizableDocs())
                 .isEqualTo(AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT - 1);
 
-        // Keep delete docs, will reach the interval this time and trigger optimize().
+        // Keep delete docs
         for (int i = AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT;
                 i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
                         + AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
-            mAppSearchImpl.remove("package", "database", "namespace", "uri" + i);
+            mAppSearchImpl.remove("package", "database", "namespace", "id" + i);
         }
+        // updates the check for optimize counter, will reach both CHECK_OPTIMIZE_INTERVAL and
+        // OPTIMIZE_THRESHOLD_DOC_COUNT this time and trigger a optimize().
+        mAppSearchImpl.checkForOptimize(
+                /*mutateBatchSize*/ AppSearchImpl.CHECK_OPTIMIZE_INTERVAL);
 
         // Verify optimize() is triggered
         optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
@@ -376,16 +410,18 @@
         List<AppSearchSchema> schemas =
                 Collections.singletonList(new AppSearchSchema.Builder("type").build());
         mAppSearchImpl.setSchema("package", "database", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
 
         // Insert document
-        GenericDocument document = new GenericDocument.Builder<>("uri", "type").setNamespace(
-                "namespace").build();
-        mAppSearchImpl.putDocument("package", "database", document);
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
+                "type").build();
+        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
 
         // Rewrite SearchSpec
         mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
-                Collections.singleton(AppSearchImpl.createPrefix("package", "database")));
+                Collections.singleton(createPrefix("package", "database")),
+                ImmutableSet.of("package$database/type"));
         assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
                 "package$database/type");
         assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
@@ -402,23 +438,27 @@
                 new AppSearchSchema.Builder("typeA").build(),
                 new AppSearchSchema.Builder("typeB").build());
         mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
         mAppSearchImpl.setSchema("package", "database2", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
 
         // Insert documents
-        GenericDocument document1 = new GenericDocument.Builder<>("uri", "typeA").setNamespace(
-                "namespace").build();
-        mAppSearchImpl.putDocument("package", "database1", document1);
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id",
+                "typeA").build();
+        mAppSearchImpl.putDocument("package", "database1", document1, /*logger=*/ null);
 
-        GenericDocument document2 = new GenericDocument.Builder<>("uri", "typeB").setNamespace(
-                "namespace").build();
-        mAppSearchImpl.putDocument("package", "database2", document2);
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id",
+                "typeB").build();
+        mAppSearchImpl.putDocument("package", "database2", document2, /*logger=*/ null);
 
         // Rewrite SearchSpec
         mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
-                ImmutableSet.of(AppSearchImpl.createPrefix("package", "database1"),
-                        AppSearchImpl.createPrefix("package", "database2")));
+                ImmutableSet.of(createPrefix("package", "database1"),
+                        createPrefix("package", "database2")), ImmutableSet.of(
+                        "package$database1/typeA", "package$database1/typeB",
+                        "package$database2/typeA", "package$database2/typeB"));
         assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
                 "package$database1/typeA", "package$database1/typeB", "package$database2/typeA",
                 "package$database2/typeB");
@@ -427,6 +467,30 @@
     }
 
     @Test
+    public void testRewriteSearchSpec_ignoresSearchSpecSchemaFilters() throws Exception {
+        SearchSpecProto.Builder searchSpecProto =
+                SearchSpecProto.newBuilder().setQuery("").addSchemaTypeFilters("type");
+
+        // Insert schema
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema("package", "database", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Insert document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
+                "type").build();
+        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
+
+        // If 'allowedPrefixedSchemas' is empty, this returns false since there's nothing to
+        // search over. Despite the searchSpecProto having schema type filters.
+        assertThat(mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
+                Collections.singleton(createPrefix("package", "database")),
+                /*allowedPrefixedSchemas=*/ Collections.emptySet())).isFalse();
+    }
+
+    @Test
     public void testQueryEmptyDatabase() throws Exception {
         SearchSpec searchSpec =
                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
@@ -435,24 +499,120 @@
         assertThat(searchResultPage.getResults()).isEmpty();
     }
 
+    /**
+     * TODO(b/169883602): This should be an integration test at the cts-level. This is a
+     * short-term test until we have official support for multiple-apps indexing at once.
+     */
+    @Test
+    public void testQueryWithMultiplePackages_noPackageFilters() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema("package1", "database1", schema1,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Insert package2 schema
+        List<AppSearchSchema> schema2 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema2").build());
+        mAppSearchImpl.setSchema("package2", "database2", schema2,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Insert package1 document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id", "schema1")
+                .build();
+        mAppSearchImpl.putDocument("package1", "database1", document, /*logger=*/ null);
+
+        // No query filters specified, package2 shouldn't be able to query for package1's documents.
+        SearchSpec searchSpec =
+                new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package2", "database2", "",
+                searchSpec);
+        assertThat(searchResultPage.getResults()).isEmpty();
+
+        // Insert package2 document
+        document = new GenericDocument.Builder<>("namespace", "id", "schema2").build();
+        mAppSearchImpl.putDocument("package2", "database2", document, /*logger=*/ null);
+
+        // No query filters specified. package2 should only get its own documents back.
+        searchResultPage = mAppSearchImpl.query("package2", "database2", "", searchSpec);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
+    }
+
+    /**
+     * TODO(b/169883602): This should be an integration test at the cts-level. This is a
+     * short-term test until we have official support for multiple-apps indexing at once.
+     */
+    @Test
+    public void testQueryWithMultiplePackages_withPackageFilters() throws Exception {
+        // Insert package1 schema
+        List<AppSearchSchema> schema1 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema1").build());
+        mAppSearchImpl.setSchema("package1", "database1", schema1,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Insert package2 schema
+        List<AppSearchSchema> schema2 =
+                ImmutableList.of(new AppSearchSchema.Builder("schema2").build());
+        mAppSearchImpl.setSchema("package2", "database2", schema2,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Insert package1 document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
+                "schema1").build();
+        mAppSearchImpl.putDocument("package1", "database1", document, /*logger=*/ null);
+
+        // "package1" filter specified, but package2 shouldn't be able to query for package1's
+        // documents.
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .addFilterPackageNames("package1")
+                .build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package2", "database2", "",
+                searchSpec);
+        assertThat(searchResultPage.getResults()).isEmpty();
+
+        // Insert package2 document
+        document = new GenericDocument.Builder<>("namespace", "id", "schema2").build();
+        mAppSearchImpl.putDocument("package2", "database2", document, /*logger=*/ null);
+
+        // "package2" filter specified, package2 should only get its own documents back.
+        searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
+                .addFilterPackageNames("package2")
+                .build();
+        searchResultPage = mAppSearchImpl.query("package2", "database2", "", searchSpec);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
+    }
+
     @Test
     public void testGlobalQueryEmptyDatabase() throws Exception {
         SearchSpec searchSpec =
                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
-        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery("", searchSpec);
+        SearchResultPage searchResultPage = mAppSearchImpl.globalQuery("", searchSpec,
+                /*callerPackageName=*/ "", /*callerUid=*/ 0);
         assertThat(searchResultPage.getResults()).isEmpty();
     }
 
     @Test
     public void testRemoveEmptyDatabase_noExceptionThrown() throws Exception {
         SearchSpec searchSpec =
-                new SearchSpec.Builder().addSchemaType("FakeType").setTermMatch(
+                new SearchSpec.Builder().addFilterSchemas("FakeType").setTermMatch(
                         TermMatchType.Code.PREFIX_VALUE).build();
         mAppSearchImpl.removeByQuery("package", "EmptyDatabase",
                 "", searchSpec);
 
         searchSpec =
-                new SearchSpec.Builder().addNamespace("FakeNamespace").setTermMatch(
+                new SearchSpec.Builder().addFilterNamespaces("FakeNamespace").setTermMatch(
                         TermMatchType.Code.PREFIX_VALUE).build();
         mAppSearchImpl.removeByQuery("package", "EmptyDatabase",
                 "", searchSpec);
@@ -463,102 +623,120 @@
 
     @Test
     public void testSetSchema() throws Exception {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
+
         List<AppSearchSchema> schemas =
                 Collections.singletonList(new AppSearchSchema.Builder("Email").build());
         // Set schema Email to AppSearch database1
         mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .build();
 
         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
     }
 
     @Test
-    public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception {
-        String prefix = AppSearchImpl.createPrefix("package", "database");
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "schema1").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singletonList("schema1"), /*forceOverride=*/ false);
+    public void testSetSchema_incompatible() throws Exception {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
 
-        // "schema1" is platform hidden now
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "schema1")).isFalse();
+        List<AppSearchSchema> oldSchemas = new ArrayList<>();
+        oldSchemas.add(new AppSearchSchema.Builder("Email")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("foo")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .build());
+        oldSchemas.add(new AppSearchSchema.Builder("Text").build());
+        // Set schema Email to AppSearch database1
+        mAppSearchImpl.setSchema("package", "database1", oldSchemas,
+                /*schemasNotPlatformSurfaceable=*/  Collections.emptyList(),
+                /*schemasPackageAccessible=*/  Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
 
-        // Add a new schema, and include the already-existing "schema1"
-        mAppSearchImpl.setSchema(
-                "package", "database",
-                ImmutableList.of(
-                        new AppSearchSchema.Builder("schema1").build(),
-                        new AppSearchSchema.Builder("schema2").build()),
-                /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"),
-                /*forceOverride=*/ false);
+        // Create incompatible schema
+        List<AppSearchSchema> newSchemas =
+                Collections.singletonList(new AppSearchSchema.Builder("Email").build());
 
-        // Check that "schema1" is still platform hidden, but "schema2" is the default platform
-        // visible.
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "schema1")).isFalse();
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "schema2")).isTrue();
+        // set email incompatible and delete text
+        SetSchemaResponse setSchemaResponse = mAppSearchImpl.setSchema("package", "database1",
+                newSchemas,  /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ true, /*version=*/ 0);
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("Text");
+        assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("Email");
     }
 
     @Test
     public void testRemoveSchema() throws Exception {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
+
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Email").build(),
                 new AppSearchSchema.Builder("Document").build());
         // Set schema Email and Document to AppSearch database1
-        mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema("package", "database1", schemas,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database1/Document"))
+                        "package$database1/Document").setVersion(0))
                 .build();
 
         // Check both schema Email and Document saved correctly.
         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
 
         final List<AppSearchSchema> finalSchemas = Collections.singletonList(
-                new AppSearchSchema.Builder(
-                        "Email").build());
-        // Check the incompatible error has been thrown.
-        AppSearchException e = assertThrows(AppSearchException.class, () ->
-                mAppSearchImpl.setSchema("package", "database1",
-                        finalSchemas, /*schemasNotPlatformSurfaceable=*/
-                        Collections.emptyList(), /*forceOverride=*/ false));
-        assertThat(e).hasMessageThat().contains("Schema is incompatible");
-        assertThat(e).hasMessageThat().contains("Deleted types: [package$database1/Document]");
+                new AppSearchSchema.Builder("Email").build());
+        SetSchemaResponse setSchemaResponse =
+                mAppSearchImpl.setSchema("package", "database1", finalSchemas,
+                        /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                        /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                        /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Check the Document type has been deleted.
+        assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("Document");
 
         // ForceOverride to delete.
-        mAppSearchImpl.setSchema("package", "database1",
-                finalSchemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ true);
+        mAppSearchImpl.setSchema("package", "database1", finalSchemas,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ true, /*version=*/ 0);
 
         // Check Document schema is removed.
         expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .build();
 
         expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
@@ -566,173 +744,186 @@
 
     @Test
     public void testRemoveSchema_differentDataBase() throws Exception {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
+
         // Create schemas
         List<AppSearchSchema> schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Email").build(),
                 new AppSearchSchema.Builder("Document").build());
 
         // Set schema Email and Document to AppSearch database1 and 2
-        mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        mAppSearchImpl.setSchema("package", "database2", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema("package", "database1", schemas,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+        mAppSearchImpl.setSchema("package", "database2", schemas,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database1/Document"))
+                        "package$database1/Document").setVersion(0))
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database2/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database2/Email").setVersion(0))
                 .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database2/Document"))
+                        "package$database2/Document").setVersion(0))
                 .build();
 
         // Check Email and Document is saved in database 1 and 2 correctly.
         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
 
         // Save only Email to database1 this time.
         schemas = Collections.singletonList(new AppSearchSchema.Builder("Email").build());
-        mAppSearchImpl.setSchema("package", "database1", schemas, /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ true);
+        mAppSearchImpl.setSchema("package", "database1", schemas,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ true, /*version=*/ 0);
 
         // Create expected schemaType list, database 1 should only contain Email but database 2
         // remains in same.
         expectedProto = SchemaProto.newBuilder()
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database1/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Email").setVersion(0))
                 .addTypes(
-                        SchemaTypeConfigProto.newBuilder().setSchemaType("package$database2/Email"))
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database2/Email").setVersion(0))
                 .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database2/Document"))
+                        "package$database2/Document").setVersion(0))
                 .build();
 
         // Check nothing changed in database2.
         expectedTypes = new ArrayList<>();
-        expectedTypes.add(mVisibilitySchemaProto);
+        expectedTypes.addAll(existingSchemas);
         expectedTypes.addAll(expectedProto.getTypesList());
         assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
                 .containsExactlyElementsIn(expectedTypes);
     }
 
-
     @Test
-    public void testRemoveSchema_removedFromVisibilityStore() throws Exception {
-        String prefix = AppSearchImpl.createPrefix("package", "database");
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "schema1").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singletonList("schema1"), /*forceOverride=*/ false);
+    public void testClearPackageData() throws AppSearchException {
+        List<SchemaTypeConfigProto> existingSchemas =
+                mAppSearchImpl.getSchemaProtoLocked().getTypesList();
 
-        // "schema1" is platform hidden now
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "schema1")).isFalse();
+        // Insert package schema
+        List<AppSearchSchema> schema =
+                ImmutableList.of(new AppSearchSchema.Builder("schema").build());
+        mAppSearchImpl.setSchema("package", "database", schema,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
 
-        // Remove "schema1" by force overriding
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.emptyList(), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ true);
+        // Insert package document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
+                "schema").build();
+        mAppSearchImpl.putDocument("package", "database", document,
+                /*logger=*/ null);
 
-        // Check that "schema1" is no longer considered platform hidden
-        assertThat(
-                mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                        prefix, prefix + "schema1")).isTrue();
+        // Verify the document is indexed.
+        SearchSpec searchSpec =
+                new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
+        SearchResultPage searchResultPage = mAppSearchImpl.query("package",
+                "database",  /*queryExpression=*/ "", searchSpec);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
 
-        // Add "schema1" back, it gets default visibility settings which means it's not platform
-        // hidden.
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "schema1").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(
-                mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                        prefix, prefix + "schema1")).isTrue();
+        // Remove the package
+        mAppSearchImpl.clearPackageData("package");
+
+        // Verify the document is cleared.
+        searchResultPage = mAppSearchImpl.query("package2", "database2",
+                /*queryExpression=*/ "", searchSpec);
+        assertThat(searchResultPage.getResults()).isEmpty();
+
+        // Verify the schema is cleared.
+        assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
+                .containsExactlyElementsIn(existingSchemas);
     }
 
     @Test
-    public void testSetSchema_defaultPlatformVisible() throws Exception {
-        String prefix = AppSearchImpl.createPrefix("package", "database");
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "Schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(
-                mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                        prefix, prefix + "Schema")).isTrue();
-    }
-
-    @Test
-    public void testSetSchema_platformHidden() throws Exception {
-        String prefix = AppSearchImpl.createPrefix("package", "database");
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "Schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.singletonList("Schema"), /*forceOverride=*/ false);
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().isSchemaPlatformSurfaceable(
-                prefix, prefix + "Schema")).isFalse();
-    }
-
-    @Test
-    public void testHasSchemaType() throws Exception {
-        // Nothing exists yet
-        assertThat(mAppSearchImpl.hasSchemaTypeLocked("package", "database", "Schema")).isFalse();
-
-        mAppSearchImpl.setSchema("package", "database",
-                Collections.singletonList(new AppSearchSchema.Builder(
-                        "Schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(mAppSearchImpl.hasSchemaTypeLocked("package", "database", "Schema")).isTrue();
-
-        assertThat(mAppSearchImpl.hasSchemaTypeLocked("package", "database",
-                "UnknownSchema")).isFalse();
-    }
-
-    @Test
-    public void testGetDatabases() throws Exception {
-        // No client databases exist yet, but the VisibilityStore's does
-        assertThat(mAppSearchImpl.getPrefixesLocked()).containsExactly(
-                AppSearchImpl.createPrefix(VisibilityStore.PACKAGE_NAME,
-                        VisibilityStore.DATABASE_NAME));
+    public void testGetPackageToDatabases() throws Exception {
+        Map<String, Set<String>> existingMapping = mAppSearchImpl.getPackageToDatabases();
+        Map<String, Set<String>> expectedMapping = new ArrayMap<>();
+        expectedMapping.putAll(existingMapping);
 
         // Has database1
+        expectedMapping.put("package1", ImmutableSet.of("database1"));
+        mAppSearchImpl.setSchema("package1", "database1",
+                Collections.singletonList(new AppSearchSchema.Builder(
+                        "schema").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
+        assertThat(mAppSearchImpl.getPackageToDatabases()).containsExactlyEntriesIn(
+                expectedMapping);
+
+        // Has both databases
+        expectedMapping.put("package1", ImmutableSet.of("database1", "database2"));
+        mAppSearchImpl.setSchema("package1", "database2",
+                Collections.singletonList(new AppSearchSchema.Builder(
+                        "schema").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
+        assertThat(mAppSearchImpl.getPackageToDatabases()).containsExactlyEntriesIn(
+                expectedMapping);
+
+        // Has both packages
+        expectedMapping.put("package2", ImmutableSet.of("database1"));
+        mAppSearchImpl.setSchema("package2", "database1",
+                Collections.singletonList(new AppSearchSchema.Builder(
+                        "schema").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
+        assertThat(mAppSearchImpl.getPackageToDatabases()).containsExactlyEntriesIn(
+                expectedMapping);
+    }
+
+    @Test
+    public void testGetPrefixes() throws Exception {
+        Set<String> existingPrefixes = mAppSearchImpl.getPrefixesLocked();
+
+        // Has database1
+        Set<String> expectedPrefixes = new ArraySet<>(existingPrefixes);
+        expectedPrefixes.add(createPrefix("package", "database1"));
         mAppSearchImpl.setSchema("package", "database1",
                 Collections.singletonList(new AppSearchSchema.Builder(
                         "schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(mAppSearchImpl.getPrefixesLocked()).containsExactly(
-                AppSearchImpl.createPrefix(VisibilityStore.PACKAGE_NAME,
-                        VisibilityStore.DATABASE_NAME),
-                AppSearchImpl.createPrefix("package", "database1"));
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
+        assertThat(mAppSearchImpl.getPrefixesLocked()).containsExactlyElementsIn(expectedPrefixes);
 
         // Has both databases
+        expectedPrefixes.add(createPrefix("package", "database2"));
         mAppSearchImpl.setSchema("package", "database2",
                 Collections.singletonList(new AppSearchSchema.Builder(
                         "schema").build()), /*schemasNotPlatformSurfaceable=*/
-                Collections.emptyList(), /*forceOverride=*/ false);
-        assertThat(mAppSearchImpl.getPrefixesLocked()).containsExactly(
-                AppSearchImpl.createPrefix(VisibilityStore.PACKAGE_NAME,
-                        VisibilityStore.DATABASE_NAME),
-                AppSearchImpl.createPrefix("package", "database1"), AppSearchImpl.createPrefix(
-                        "package", "database2"));
+                Collections.emptyList(), /*schemasPackageAccessible=*/
+                Collections.emptyMap(), /*forceOverride=*/ false, /*version=*/ 0);
+        assertThat(mAppSearchImpl.getPrefixesLocked()).containsExactlyElementsIn(expectedPrefixes);
     }
 
     @Test
     public void testRewriteSearchResultProto() throws Exception {
-        final String database =
-                "com.package.foo" + AppSearchImpl.PACKAGE_DELIMITER + "databaseName"
-                        + AppSearchImpl.DATABASE_DELIMITER;
-        final String uri = "uri";
-        final String namespace = database + "namespace";
-        final String schemaType = database + "schema";
+        final String prefix =
+                "com.package.foo" + PrefixUtil.PACKAGE_DELIMITER + "databaseName"
+                        + PrefixUtil.DATABASE_DELIMITER;
+        final String id = "id";
+        final String namespace = prefix + "namespace";
+        final String schemaType = prefix + "schema";
 
         // Building the SearchResult received from query.
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri(uri)
+                .setUri(id)
                 .setNamespace(namespace)
                 .setSchema(schemaType)
                 .build();
@@ -742,16 +933,502 @@
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
                 .addResults(resultProto)
                 .build();
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType(schemaType)
+                        .build();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(prefix,
+                ImmutableMap.of(schemaType, schemaTypeConfigProto));
 
         DocumentProto.Builder strippedDocumentProto = documentProto.toBuilder();
-        AppSearchImpl.removePrefixesFromDocument(strippedDocumentProto);
+        removePrefixesFromDocument(strippedDocumentProto);
         SearchResultPage searchResultPage =
-                AppSearchImpl.rewriteSearchResultProto(searchResultProto);
+                AppSearchImpl.rewriteSearchResultProto(searchResultProto, schemaMap);
         for (SearchResult result : searchResultPage.getResults()) {
             assertThat(result.getPackageName()).isEqualTo("com.package.foo");
-            assertThat(result.getDocument()).isEqualTo(
+            assertThat(result.getDatabaseName()).isEqualTo("databaseName");
+            assertThat(result.getGenericDocument()).isEqualTo(
                     GenericDocumentToProtoConverter.toGenericDocument(
-                            strippedDocumentProto.build()));
+                            strippedDocumentProto.build(), prefix, schemaMap.get(prefix)));
         }
     }
+
+    @Test
+    public void testReportUsage() throws Exception {
+        // Insert schema
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema("package", "database", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Insert two docs
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace", "id1", "type").build();
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace", "id2", "type").build();
+        mAppSearchImpl.putDocument("package", "database", document1, /*logger=*/ null);
+        mAppSearchImpl.putDocument("package", "database", document2, /*logger=*/ null);
+
+        // Report some usages. id1 has 2 app and 1 system usage, id2 has 1 app and 2 system usage.
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id1", /*usageTimestampMillis=*/ 10, /*systemUsage=*/ false);
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id1", /*usageTimestampMillis=*/ 20, /*systemUsage=*/ false);
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id1", /*usageTimestampMillis=*/ 1000, /*systemUsage=*/ true);
+
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id2", /*usageTimestampMillis=*/ 100, /*systemUsage=*/ false);
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id2", /*usageTimestampMillis=*/ 200, /*systemUsage=*/ true);
+        mAppSearchImpl.reportUsage("package", "database", "namespace",
+                "id2", /*usageTimestampMillis=*/ 150, /*systemUsage=*/ true);
+
+        // Sort by app usage count: id1 should win
+        List<SearchResult> page = mAppSearchImpl.query("package", "database", "",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
+                        .build()).getResults();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
+
+        // Sort by app usage timestamp: id2 should win
+        page = mAppSearchImpl.query("package", "database", "",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
+                        .build()).getResults();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
+
+        // Sort by system usage count: id2 should win
+        page = mAppSearchImpl.query("package", "database", "",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT)
+                        .build()).getResults();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
+
+        // Sort by system usage timestamp: id1 should win
+        page = mAppSearchImpl.query("package", "database", "",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setRankingStrategy(
+                                SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP)
+                        .build()).getResults();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
+    }
+
+    @Test
+    public void testGetStorageInfoForPackage_nonexistentPackage() throws Exception {
+        // "package2" doesn't exist yet, so it shouldn't have any storage size
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("nonexistent.package");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForPackage_withoutDocument() throws Exception {
+        // Insert schema for "package1"
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema("package1", "database", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Since "package1" doesn't have a document, it get any space attributed to it.
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("package1");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForPackage_proportionalToDocuments() throws Exception {
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+
+        // Insert schema for "package1"
+        mAppSearchImpl.setSchema("package1", "database", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Insert document for "package1"
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id1", "type").build();
+        mAppSearchImpl.putDocument("package1", "database", document, /*logger=*/ null);
+
+        // Insert schema for "package2"
+        mAppSearchImpl.setSchema("package2", "database", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Insert two documents for "package2"
+        document = new GenericDocument.Builder<>("namespace", "id1", "type").build();
+        mAppSearchImpl.putDocument("package2", "database", document, /*logger=*/ null);
+        document = new GenericDocument.Builder<>("namespace", "id2", "type").build();
+        mAppSearchImpl.putDocument("package2", "database", document, /*logger=*/ null);
+
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("package1");
+        long size1 = storageInfo.getSizeBytes();
+        assertThat(size1).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(1);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
+
+        storageInfo = mAppSearchImpl.getStorageInfoForPackage("package2");
+        long size2 = storageInfo.getSizeBytes();
+        assertThat(size2).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(2);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
+
+        // Size is proportional to number of documents. Since "package2" has twice as many
+        // documents as "package1", its size is twice as much too.
+        assertThat(size2).isAtLeast(2 * size1);
+    }
+
+    @Test
+    public void testGetStorageInfoForDatabase_nonexistentPackage() throws Exception {
+        // "package2" doesn't exist yet, so it shouldn't have any storage size
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("nonexistent.package",
+                "nonexistentDatabase");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForDatabase_nonexistentDatabase() throws Exception {
+        // Insert schema for "package1"
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema("package1", "database", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // "package2" doesn't exist yet, so it shouldn't have any storage size
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1",
+                "nonexistentDatabase");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForDatabase_withoutDocument() throws Exception {
+        // Insert schema for "package1"
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema("package1", "database1", schemas,
+                /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Since "package1", "database1" doesn't have a document, it get any space attributed to it.
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database1");
+        assertThat(storageInfo.getSizeBytes()).isEqualTo(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(0);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetStorageInfoForDatabase_proportionalToDocuments() throws Exception {
+        // Insert schema for "package1", "database1" and "database2"
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema("package1", "database1", schemas,
+                /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+        mAppSearchImpl.setSchema("package1", "database2", schemas,
+                /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Add a document for "package1", "database1"
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument("package1", "database1", document, /*logger=*/ null);
+
+        // Add two documents for "package1", "database2"
+        document = new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        mAppSearchImpl.putDocument("package1", "database2", document, /*logger=*/ null);
+        document = new GenericDocument.Builder<>("namespace1", "id2", "type").build();
+        mAppSearchImpl.putDocument("package1", "database2", document, /*logger=*/ null);
+
+
+        StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database1");
+        long size1 = storageInfo.getSizeBytes();
+        assertThat(size1).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(1);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
+
+        storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database2");
+        long size2 = storageInfo.getSizeBytes();
+        assertThat(size2).isGreaterThan(0);
+        assertThat(storageInfo.getAliveDocumentsCount()).isEqualTo(2);
+        assertThat(storageInfo.getAliveNamespacesCount()).isEqualTo(1);
+
+        // Size is proportional to number of documents. Since "database2" has twice as many
+        // documents as "database1", its size is twice as much too.
+        assertThat(size2).isAtLeast(2 * size1);
+    }
+
+    @Test
+    public void testThrowsExceptionIfClosed() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder(),
+                context, VisibilityStore.NO_OP_USER_ID, /*globalQuerierPackage=*/ "", /*logger
+                =*/ null);
+
+        // Initial check that we could do something at first.
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        appSearchImpl.setSchema("package", "database", schemas,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        appSearchImpl.close();
+
+        // Check all our public APIs
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.setSchema("package", "database", schemas,
+                    /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                    /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                    /*forceOverride=*/ false, /*version=*/ 0);
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.getSchema("package", "database");
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.putDocument("package", "database", new GenericDocument.Builder<>(
+                    "namespace", "id",
+                    "type").build(), /*logger=*/ null);
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.getDocument("package", "database", "namespace", "id",
+                    Collections.emptyMap());
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.query("package", "database", "query",
+                    new SearchSpec.Builder().setTermMatch(
+                            TermMatchType.Code.PREFIX_VALUE).build());
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.globalQuery("query",
+                    new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build(),
+                    "package", /*callerUid=*/ 1);
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.getNextPage(/*nextPageToken=*/ 1L);
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.invalidateNextPageToken(/*nextPageToken=*/ 1L);
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.reportUsage("package", "database", "namespace", "id",
+                    /*usageTimestampMillis=*/ 1000L, /*systemUsage=*/ false);
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.remove("package", "database", "namespace", "id");
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.removeByQuery("package", "database", "query",
+                    new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build());
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.getStorageInfoForPackage("package");
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.getStorageInfoForDatabase("package", "database");
+        });
+
+        assertThrows(IllegalStateException.class, () -> {
+            appSearchImpl.persistToDisk(PersistType.Code.FULL);
+        });
+    }
+
+    @Test
+    public void testPutPersistsWithLiteFlush() throws Exception {
+        // Setup the index
+        Context context = ApplicationProvider.getApplicationContext();
+        File appsearchDir = mTemporaryFolder.newFolder();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(appsearchDir,
+                context, VisibilityStore.NO_OP_USER_ID, /*globalQuerierPackage=*/ "",
+                /*logger=*/ null);
+
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        appSearchImpl.setSchema("package", "database", schemas,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Add a document and persist it.
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        appSearchImpl.putDocument("package", "database", document, /*logger=*/null);
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        GenericDocument getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document);
+
+        // That document should be visible even from another instance.
+        AppSearchImpl appSearchImpl2 = AppSearchImpl.create(appsearchDir,
+                context, VisibilityStore.NO_OP_USER_ID, /*globalQuerierPackage=*/ "",
+                /*logger=*/ null);
+        getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document);
+    }
+
+    @Test
+    public void testDeletePersistsWithLiteFlush() throws Exception {
+        // Setup the index
+        Context context = ApplicationProvider.getApplicationContext();
+        File appsearchDir = mTemporaryFolder.newFolder();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(appsearchDir,
+                context, VisibilityStore.NO_OP_USER_ID, /*globalQuerierPackage=*/ "",
+                /*logger=*/ null);
+
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        appSearchImpl.setSchema("package", "database", schemas,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Add two documents and persist them.
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        appSearchImpl.putDocument("package", "database", document1, /*logger=*/null);
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace1", "id2", "type").build();
+        appSearchImpl.putDocument("package", "database", document2, /*logger=*/null);
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        GenericDocument getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document1);
+        getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Delete the first document
+        appSearchImpl.remove("package", "database", "namespace1", "id1");
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+        assertThrows(AppSearchException.class, () -> appSearchImpl.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Only the second document should be retrievable from another instance.
+        AppSearchImpl appSearchImpl2 = AppSearchImpl.create(appsearchDir,
+                context, VisibilityStore.NO_OP_USER_ID, /*globalQuerierPackage=*/ "",
+                /*logger=*/ null);
+        assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+    }
+
+    @Test
+    public void testDeleteByQueryPersistsWithLiteFlush() throws Exception {
+        // Setup the index
+        Context context = ApplicationProvider.getApplicationContext();
+        File appsearchDir = mTemporaryFolder.newFolder();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(appsearchDir,
+                context, VisibilityStore.NO_OP_USER_ID, /*globalQuerierPackage=*/ "",
+                /*logger=*/ null);
+
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        appSearchImpl.setSchema("package", "database", schemas,
+                /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
+                /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+
+        // Add two documents and persist them.
+        GenericDocument document1 =
+                new GenericDocument.Builder<>("namespace1", "id1", "type").build();
+        appSearchImpl.putDocument("package", "database", document1, /*logger=*/null);
+        GenericDocument document2 =
+                new GenericDocument.Builder<>("namespace2", "id2", "type").build();
+        appSearchImpl.putDocument("package", "database", document2, /*logger=*/null);
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+
+        GenericDocument getResult = appSearchImpl.getDocument("package", "database", "namespace1",
+                "id1",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document1);
+        getResult = appSearchImpl.getDocument("package", "database", "namespace2",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Delete the first document
+        appSearchImpl.removeByQuery("package", "database", "",
+                new SearchSpec.Builder().addFilterNamespaces("namespace1").setTermMatch(
+                        SearchSpec.TERM_MATCH_EXACT_ONLY).build());
+        appSearchImpl.persistToDisk(PersistType.Code.LITE);
+        assertThrows(AppSearchException.class, () -> appSearchImpl.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl.getDocument("package", "database", "namespace2",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+
+        // Only the second document should be retrievable from another instance.
+        AppSearchImpl appSearchImpl2 = AppSearchImpl.create(appsearchDir,
+                context, VisibilityStore.NO_OP_USER_ID, /*globalQuerierPackage=*/ "",
+                /*logger=*/ null);
+        assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
+                "database",
+                "namespace1",
+                "id1",
+                Collections.emptyMap()));
+        getResult = appSearchImpl2.getDocument("package", "database", "namespace2",
+                "id2",
+                Collections.emptyMap());
+        assertThat(getResult).isEqualTo(document2);
+    }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
new file mode 100644
index 0000000..53ae914
--- /dev/null
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.localstorage.stats.CallStats;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.PutDocumentStats;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.google.android.icing.proto.InitializeStatsProto;
+import com.google.android.icing.proto.PutDocumentStatsProto;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.util.Collections;
+import java.util.List;
+
+public class AppSearchLoggerTest {
+    @Rule
+    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+    private AppSearchImpl mAppSearchImpl;
+    private TestLogger mLogger;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+
+        // Give ourselves global query permissions
+        mAppSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder(),
+                context, VisibilityStore.NO_OP_USER_ID,
+                /*globalQuerierPackage=*/ context.getPackageName(),
+                /*logger=*/ null);
+        mLogger = new TestLogger();
+    }
+
+    // Test only not thread safe.
+    public class TestLogger implements AppSearchLogger {
+        @Nullable
+        CallStats mCallStats;
+        @Nullable
+        PutDocumentStats mPutDocumentStats;
+        @Nullable
+        InitializeStats mInitializeStats;
+
+        @Override
+        public void logStats(@NonNull CallStats stats) {
+            mCallStats = stats;
+        }
+
+        @Override
+        public void logStats(@NonNull PutDocumentStats stats) {
+            mPutDocumentStats = stats;
+        }
+
+        @Override
+        public void logStats(@NonNull InitializeStats stats) {
+            mInitializeStats = stats;
+        }
+    }
+
+    @Test
+    public void testAppSearchLoggerHelper_testCopyNativeStats_initialize() {
+        int nativeLatencyMillis = 3;
+        int nativeDocumentStoreRecoveryCause = InitializeStatsProto.RecoveryCause.DATA_LOSS_VALUE;
+        int nativeIndexRestorationCause =
+                InitializeStatsProto.RecoveryCause.INCONSISTENT_WITH_GROUND_TRUTH_VALUE;
+        int nativeSchemaStoreRecoveryCause =
+                InitializeStatsProto.RecoveryCause.TOTAL_CHECKSUM_MISMATCH_VALUE;
+        int nativeDocumentStoreRecoveryLatencyMillis = 7;
+        int nativeIndexRestorationLatencyMillis = 8;
+        int nativeSchemaStoreRecoveryLatencyMillis = 9;
+        int nativeDocumentStoreDataStatus =
+                InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE;
+        int nativeNumDocuments = 11;
+        int nativeNumSchemaTypes = 12;
+        InitializeStatsProto.Builder nativeInitBuilder = InitializeStatsProto.newBuilder()
+                .setLatencyMs(nativeLatencyMillis)
+                .setDocumentStoreRecoveryCause(InitializeStatsProto.RecoveryCause.forNumber(
+                        nativeDocumentStoreRecoveryCause))
+                .setIndexRestorationCause(
+                        InitializeStatsProto.RecoveryCause.forNumber(nativeIndexRestorationCause))
+                .setSchemaStoreRecoveryCause(
+                        InitializeStatsProto.RecoveryCause.forNumber(
+                                nativeSchemaStoreRecoveryCause))
+                .setDocumentStoreRecoveryLatencyMs(nativeDocumentStoreRecoveryLatencyMillis)
+                .setIndexRestorationLatencyMs(nativeIndexRestorationLatencyMillis)
+                .setSchemaStoreRecoveryLatencyMs(nativeSchemaStoreRecoveryLatencyMillis)
+                .setDocumentStoreDataStatus(InitializeStatsProto.DocumentStoreDataStatus.forNumber(
+                        nativeDocumentStoreDataStatus))
+                .setNumDocuments(nativeNumDocuments)
+                .setNumSchemaTypes(nativeNumSchemaTypes);
+        InitializeStats.Builder initBuilder = new InitializeStats.Builder();
+
+        AppSearchLoggerHelper.copyNativeStats(nativeInitBuilder.build(), initBuilder);
+
+        InitializeStats iStats = initBuilder.build();
+        assertThat(iStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(iStats.getDocumentStoreRecoveryCause()).isEqualTo(
+                nativeDocumentStoreRecoveryCause);
+        assertThat(iStats.getIndexRestorationCause()).isEqualTo(nativeIndexRestorationCause);
+        assertThat(iStats.getSchemaStoreRecoveryCause()).isEqualTo(
+                nativeSchemaStoreRecoveryCause);
+        assertThat(iStats.getDocumentStoreRecoveryLatencyMillis()).isEqualTo(
+                nativeDocumentStoreRecoveryLatencyMillis);
+        assertThat(iStats.getIndexRestorationLatencyMillis()).isEqualTo(
+                nativeIndexRestorationLatencyMillis);
+        assertThat(iStats.getSchemaStoreRecoveryLatencyMillis()).isEqualTo(
+                nativeSchemaStoreRecoveryLatencyMillis);
+        assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
+                nativeDocumentStoreDataStatus);
+        assertThat(iStats.getDocumentCount()).isEqualTo(nativeNumDocuments);
+        assertThat(iStats.getSchemaTypeCount()).isEqualTo(nativeNumSchemaTypes);
+    }
+
+    @Test
+    public void testAppSearchLoggerHelper_testCopyNativeStats_putDocument() {
+        final int nativeLatencyMillis = 3;
+        final int nativeDocumentStoreLatencyMillis = 4;
+        final int nativeIndexLatencyMillis = 5;
+        final int nativeIndexMergeLatencyMillis = 6;
+        final int nativeDocumentSize = 7;
+        final int nativeNumTokensIndexed = 8;
+        final boolean nativeExceededMaxNumTokens = true;
+        PutDocumentStatsProto nativePutDocumentStats = PutDocumentStatsProto.newBuilder()
+                .setLatencyMs(nativeLatencyMillis)
+                .setDocumentStoreLatencyMs(nativeDocumentStoreLatencyMillis)
+                .setIndexLatencyMs(nativeIndexLatencyMillis)
+                .setIndexMergeLatencyMs(nativeIndexMergeLatencyMillis)
+                .setDocumentSize(nativeDocumentSize)
+                .setTokenizationStats(PutDocumentStatsProto.TokenizationStats.newBuilder()
+                        .setNumTokensIndexed(nativeNumTokensIndexed)
+                        .setExceededMaxTokenNum(nativeExceededMaxNumTokens)
+                        .build())
+                .build();
+        PutDocumentStats.Builder pBuilder = new PutDocumentStats.Builder(
+                "packageName",
+                "database");
+
+        AppSearchLoggerHelper.copyNativeStats(nativePutDocumentStats, pBuilder);
+
+        PutDocumentStats pStats = pBuilder.build();
+        assertThat(pStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(pStats.getNativeDocumentStoreLatencyMillis()).isEqualTo(
+                nativeDocumentStoreLatencyMillis);
+        assertThat(pStats.getNativeIndexLatencyMillis()).isEqualTo(nativeIndexLatencyMillis);
+        assertThat(pStats.getNativeIndexMergeLatencyMillis()).isEqualTo(
+                nativeIndexMergeLatencyMillis);
+        assertThat(pStats.getNativeDocumentSizeBytes()).isEqualTo(nativeDocumentSize);
+        assertThat(pStats.getNativeNumTokensIndexed()).isEqualTo(nativeNumTokensIndexed);
+        assertThat(pStats.getNativeExceededMaxNumTokens()).isEqualTo(nativeExceededMaxNumTokens);
+    }
+
+
+    //
+    // Testing actual logging
+    //
+    @Test
+    public void testLoggingStats_initialize() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder(),
+                context,
+                VisibilityStore.NO_OP_USER_ID,
+                /*globalQuerierPackage=*/ context.getPackageName(),
+                mLogger);
+
+        InitializeStats iStats = mLogger.mInitializeStats;
+        assertThat(iStats).isNotNull();
+        assertThat(iStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+        assertThat(iStats.getTotalLatencyMillis()).isGreaterThan(0);
+        assertThat(iStats.hasDeSync()).isFalse();
+        assertThat(iStats.getNativeLatencyMillis()).isGreaterThan(0);
+        assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
+                InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE);
+        assertThat(iStats.getDocumentCount()).isEqualTo(0);
+        assertThat(iStats.getSchemaTypeCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testLoggingStats_putDocument() throws Exception {
+        // Insert schema
+        final String testPackageName = "testPackage";
+        final String testDatabase = "testDatabase";
+        List<AppSearchSchema> schemas =
+                Collections.singletonList(new AppSearchSchema.Builder("type").build());
+        mAppSearchImpl.setSchema(testPackageName, testDatabase, schemas,
+                /*schemasNotPlatformSurfaceable=*/
+                Collections.emptyList(), /*schemasPackageAccessible=*/ Collections.emptyMap(),
+                /*forceOverride=*/ false, /*version=*/ 0);
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "type").build();
+
+        mAppSearchImpl.putDocument(testPackageName, testDatabase, document, mLogger);
+
+        PutDocumentStats pStats = mLogger.mPutDocumentStats;
+        assertThat(pStats).isNotNull();
+        assertThat(pStats.getGeneralStats().getPackageName()).isEqualTo(testPackageName);
+        assertThat(pStats.getGeneralStats().getDatabase()).isEqualTo(testDatabase);
+        assertThat(pStats.getGeneralStats().getStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
+        // The rest of native stats have been tested in testCopyNativeStats
+        assertThat(pStats.getNativeDocumentSizeBytes()).isGreaterThan(0);
+    }
+}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java
index 254f8f7..4996ca5 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/LocalStorageTest.java
@@ -24,13 +24,17 @@
 
 import org.junit.Test;
 
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
 public class LocalStorageTest {
     @Test
     public void testSameInstance() throws Exception {
-        LocalStorage b1 =
-                LocalStorage.getOrCreateInstance(ApplicationProvider.getApplicationContext());
-        LocalStorage b2 =
-                LocalStorage.getOrCreateInstance(ApplicationProvider.getApplicationContext());
+        Executor executor = Executors.newCachedThreadPool();
+        LocalStorage b1 = LocalStorage.getOrCreateInstance(
+                ApplicationProvider.getApplicationContext(), executor);
+        LocalStorage b2 = LocalStorage.getOrCreateInstance(
+                ApplicationProvider.getApplicationContext(), executor);
         assertThat(b1).isSameInstanceAs(b2);
     }
 
@@ -40,13 +44,18 @@
         // in database name, add checker in SearchContext.Builder and reflect it in java doc.
         LocalStorage.SearchContext.Builder contextBuilder =
                 new LocalStorage.SearchContext.Builder(
-                        ApplicationProvider.getApplicationContext());
+                        ApplicationProvider.getApplicationContext(),
+                        /*databaseName=*/ "");
 
         IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> contextBuilder.setDatabaseName("testDatabaseNameEndWith/"));
+                () -> new LocalStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        "testDatabaseNameEndWith/").build());
         assertThat(e).hasMessageThat().isEqualTo("Database name cannot contain '/'");
         e = assertThrows(IllegalArgumentException.class,
-                () -> contextBuilder.setDatabaseName("/testDatabaseNameStartWith"));
+                () -> new LocalStorage.SearchContext.Builder(
+                        ApplicationProvider.getApplicationContext(),
+                        "/testDatabaseNameStartWith").build());
         assertThat(e).hasMessageThat().isEqualTo("Database name cannot contain '/'");
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
deleted file mode 100644
index 4577f5a..0000000
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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 androidx.appsearch.localstorage;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
-import java.util.Collections;
-
-public class VisibilityStoreTest {
-
-    @Rule
-    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
-    private AppSearchImpl mAppSearchImpl;
-    private VisibilityStore mVisibilityStore;
-
-    @Before
-    public void setUp() throws Exception {
-        mAppSearchImpl = AppSearchImpl.create(mTemporaryFolder.newFolder());
-        mVisibilityStore = mAppSearchImpl.getVisibilityStoreLocked();
-    }
-
-    /**
-     * Make sure that we don't conflict with any special characters that AppSearchImpl has
-     * reserved.
-     */
-    @Test
-    public void testValidPackageName() {
-        assertThat(VisibilityStore.PACKAGE_NAME).doesNotContain(
-                "" + AppSearchImpl.PACKAGE_DELIMITER); // Convert the chars to CharSequences
-        assertThat(VisibilityStore.PACKAGE_NAME).doesNotContain(
-                "" + AppSearchImpl.DATABASE_DELIMITER); // Convert the chars to CharSequences
-    }
-
-    /**
-     * Make sure that we don't conflict with any special characters that AppSearchImpl has
-     * reserved.
-     */
-    @Test
-    public void testValidDatabaseName() {
-        assertThat(VisibilityStore.DATABASE_NAME).doesNotContain(
-                "" + AppSearchImpl.PACKAGE_DELIMITER); // Convert the chars to CharSequences
-        assertThat(VisibilityStore.DATABASE_NAME).doesNotContain(
-                "" + AppSearchImpl.DATABASE_DELIMITER); // Convert the chars to CharSequences
-    }
-
-    @Test
-    public void testSetVisibility() throws Exception {
-        mVisibilityStore.setVisibility("prefix",
-                /*schemasNotPlatformSurfaceable=*/
-                ImmutableSet.of("prefix/schema1", "prefix/schema2"));
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema1")).isFalse();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema2")).isFalse();
-
-        // New .setVisibility() call completely overrides previous visibility settings. So
-        // "schema2" isn't preserved.
-        mVisibilityStore.setVisibility("prefix",
-                /*schemasNotPlatformSurfaceable=*/
-                ImmutableSet.of("prefix/schema1", "prefix/schema3"));
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema1")).isFalse();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema2")).isTrue();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema3")).isFalse();
-
-        mVisibilityStore.setVisibility(
-                "prefix", /*schemasNotPlatformSurfaceable=*/ Collections.emptySet());
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema1")).isTrue();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema2")).isTrue();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable("prefix", "prefix/schema3")).isTrue();
-    }
-
-    @Test
-    public void testEmptyPrefix() throws Exception {
-        mVisibilityStore.setVisibility(/*prefix=*/ "",
-                /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema2"));
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable(/*prefix=*/ "", "schema1")).isFalse();
-        assertThat(
-                mVisibilityStore.isSchemaPlatformSurfaceable(/*prefix=*/ "", "schema2")).isFalse();
-    }
-}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index 1245262..f22818d 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -21,8 +21,11 @@
 import androidx.appsearch.app.GenericDocument;
 
 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.SchemaTypeConfigProto;
 import com.google.android.icing.protobuf.ByteString;
+import com.google.common.collect.ImmutableMap;
 
 import org.junit.Test;
 
@@ -30,29 +33,42 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class GenericDocumentToProtoConverterTest {
     private static final byte[] BYTE_ARRAY_1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
     private static final byte[] BYTE_ARRAY_2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
+    private static final String SCHEMA_TYPE_1 = "sDocumentPropertiesSchemaType1";
+    private static final String SCHEMA_TYPE_2 = "sDocumentPropertiesSchemaType2";
     private static final GenericDocument DOCUMENT_PROPERTIES_1 =
             new GenericDocument.Builder<GenericDocument.Builder<?>>(
-                    "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
-            .setCreationTimestampMillis(12345L)
-            .build();
+                    "namespace", "sDocumentProperties1", SCHEMA_TYPE_1)
+                    .setCreationTimestampMillis(12345L)
+                    .build();
     private static final GenericDocument DOCUMENT_PROPERTIES_2 =
             new GenericDocument.Builder<GenericDocument.Builder<?>>(
-                    "sDocumentProperties2", "sDocumentPropertiesSchemaType2")
-            .setCreationTimestampMillis(6789L)
+                    "namespace", "sDocumentProperties2", SCHEMA_TYPE_2)
+                    .setCreationTimestampMillis(6789L)
+                    .build();
+    private static final SchemaTypeConfigProto SCHEMA_PROTO_1 = SchemaTypeConfigProto.newBuilder()
+            .setSchemaType(SCHEMA_TYPE_1)
             .build();
+    private static final SchemaTypeConfigProto SCHEMA_PROTO_2 = SchemaTypeConfigProto.newBuilder()
+            .setSchemaType(SCHEMA_TYPE_2)
+            .build();
+    private static final String PREFIX = "package$databaseName/";
+    private static final Map<String, SchemaTypeConfigProto> SCHEMA_MAP =
+            ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, SCHEMA_PROTO_1, PREFIX + SCHEMA_TYPE_2,
+                    SCHEMA_PROTO_2);
 
     @Test
     public void testDocumentProtoConvert() {
         GenericDocument document =
-                new GenericDocument.Builder<GenericDocument.Builder<?>>("uri1", "schemaType1")
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
                         .setCreationTimestampMillis(5L)
                         .setScore(1)
                         .setTtlMillis(1L)
-                        .setNamespace("namespace")
                         .setPropertyLong("longKey1", 1L)
                         .setPropertyDouble("doubleKey1", 1.0)
                         .setPropertyBoolean("booleanKey1", true)
@@ -64,8 +80,8 @@
 
         // Create the Document proto. Need to sort the property order by key.
         DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
-                .setUri("uri1")
-                .setSchema("schemaType1")
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE_1)
                 .setCreationTimestampMs(5L)
                 .setScore(1)
                 .setTtlMs(1L)
@@ -95,9 +111,123 @@
             documentProtoBuilder.addProperties(propertyProtoMap.get(key));
         }
         DocumentProto documentProto = documentProtoBuilder.build();
-        assertThat(GenericDocumentToProtoConverter.toDocumentProto(document))
-                .isEqualTo(documentProto);
-        assertThat(document)
-                .isEqualTo(GenericDocumentToProtoConverter.toGenericDocument(documentProto));
+
+        GenericDocument convertedGenericDocument =
+                GenericDocumentToProtoConverter.toGenericDocument(documentProto, PREFIX,
+                        SCHEMA_MAP);
+        DocumentProto convertedDocumentProto =
+                GenericDocumentToProtoConverter.toDocumentProto(document);
+
+        assertThat(convertedDocumentProto).isEqualTo(documentProto);
+        assertThat(convertedGenericDocument).isEqualTo(document);
+    }
+
+    @Test
+    public void testConvertDocument_whenPropertyHasEmptyList() {
+        String emptyStringPropertyName = "emptyStringProperty";
+        DocumentProto documentProto = DocumentProto.newBuilder()
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE_1)
+                .setCreationTimestampMs(5L)
+                .setNamespace("namespace")
+                .addProperties(
+                        PropertyProto.newBuilder()
+                                .setName(emptyStringPropertyName)
+                                .build()
+                ).build();
+
+        PropertyConfigProto emptyStringListProperty = PropertyConfigProto.newBuilder()
+                .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED)
+                .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                .setPropertyName(emptyStringPropertyName)
+                .build();
+        SchemaTypeConfigProto schemaTypeConfigProto = SchemaTypeConfigProto.newBuilder()
+                .addProperties(emptyStringListProperty)
+                .setSchemaType(SCHEMA_TYPE_1)
+                .build();
+        Map<String, SchemaTypeConfigProto> schemaMap =
+                ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, schemaTypeConfigProto);
+
+        GenericDocument convertedDocument = GenericDocumentToProtoConverter.toGenericDocument(
+                documentProto, PREFIX, schemaMap);
+
+        GenericDocument expectedDocument =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyString(emptyStringPropertyName)
+                        .build();
+        assertThat(convertedDocument).isEqualTo(expectedDocument);
+        assertThat(expectedDocument.getPropertyStringArray(emptyStringPropertyName)).isEmpty();
+    }
+
+    @Test
+    public void testConvertDocument_whenNestedDocumentPropertyHasEmptyList() {
+        String emptyStringPropertyName = "emptyStringProperty";
+        String documentPropertyName = "documentProperty";
+        DocumentProto nestedDocumentProto = DocumentProto.newBuilder()
+                .setUri("id2")
+                .setSchema(SCHEMA_TYPE_2)
+                .setCreationTimestampMs(5L)
+                .setNamespace("namespace")
+                .addProperties(
+                        PropertyProto.newBuilder()
+                                .setName(emptyStringPropertyName)
+                                .build()
+                ).build();
+        DocumentProto documentProto = DocumentProto.newBuilder()
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE_1)
+                .setCreationTimestampMs(5L)
+                .setNamespace("namespace")
+                .addProperties(
+                        PropertyProto.newBuilder()
+                                .addDocumentValues(nestedDocumentProto)
+                                .setName(documentPropertyName)
+                                .build()
+                ).build();
+
+        PropertyConfigProto documentProperty = PropertyConfigProto.newBuilder()
+                .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED)
+                .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
+                .setPropertyName(documentPropertyName)
+                .setSchemaType(SCHEMA_TYPE_2)
+                .build();
+        SchemaTypeConfigProto schemaTypeConfigProto = SchemaTypeConfigProto.newBuilder()
+                .addProperties(documentProperty)
+                .setSchemaType(SCHEMA_TYPE_1)
+                .build();
+        PropertyConfigProto emptyStringListProperty = PropertyConfigProto.newBuilder()
+                .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED)
+                .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                .setPropertyName(emptyStringPropertyName)
+                .build();
+        SchemaTypeConfigProto nestedSchemaTypeConfigProto = SchemaTypeConfigProto.newBuilder()
+                .addProperties(emptyStringListProperty)
+                .setSchemaType(SCHEMA_TYPE_2)
+                .build();
+        Map<String, SchemaTypeConfigProto> schemaMap =
+                ImmutableMap.of(PREFIX + SCHEMA_TYPE_1, schemaTypeConfigProto,
+                        PREFIX + SCHEMA_TYPE_2, nestedSchemaTypeConfigProto);
+
+        GenericDocument convertedDocument = GenericDocumentToProtoConverter.toGenericDocument(
+                documentProto, PREFIX, schemaMap);
+
+        GenericDocument expectedDocument =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace", "id1",
+                        SCHEMA_TYPE_1)
+                        .setCreationTimestampMillis(5L)
+                        .setPropertyDocument(documentPropertyName,
+                                new GenericDocument.Builder<GenericDocument.Builder<?>>("namespace",
+                                        "id2", SCHEMA_TYPE_2)
+                                        .setCreationTimestampMillis(5L)
+                                        .setPropertyString(emptyStringPropertyName)
+                                        .build()
+                        )
+                        .build();
+        assertThat(convertedDocument).isEqualTo(expectedDocument);
+        assertThat(
+                expectedDocument.getPropertyDocument(documentPropertyName).getPropertyStringArray(
+                        emptyStringPropertyName)).isEmpty();
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
index 889b0c7..25ffc46 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
@@ -31,22 +31,25 @@
     @Test
     public void testGetProto_Email() {
         AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
-                        .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
-                        .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+                ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder("body")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
                 ).build();
 
         SchemaTypeConfigProto expectedEmailProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("Email")
+                .setVersion(12345)
                 .addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("subject")
                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
@@ -69,7 +72,7 @@
                         )
                 ).build();
 
-        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema))
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema, /*version=*/12345))
                 .isEqualTo(expectedEmailProto);
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
                 .isEqualTo(emailSchema);
@@ -78,22 +81,21 @@
     @Test
     public void testGetProto_MusicRecording() {
         AppSearchSchema musicRecordingSchema = new AppSearchSchema.Builder("MusicRecording")
-                .addProperty(new AppSearchSchema.PropertyConfig.Builder("artist")
-                        .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("artist")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
                         .build()
-                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("pubDate")
-                        .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_INT64)
+                ).addProperty(new AppSearchSchema.Int64PropertyConfig.Builder("pubDate")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE)
-                        .setTokenizerType(AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_NONE)
                         .build()
                 ).build();
 
         SchemaTypeConfigProto expectedMusicRecordingProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("MusicRecording")
+                .setVersion(0)
                 .addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("artist")
                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
@@ -108,15 +110,10 @@
                         .setPropertyName("pubDate")
                         .setDataType(PropertyConfigProto.DataType.Code.INT64)
                         .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
-                        .setStringIndexingConfig(
-                                StringIndexingConfig.newBuilder()
-                                        .setTokenizerType(
-                                                StringIndexingConfig.TokenizerType.Code.NONE)
-                                        .setTermMatchType(TermMatchType.Code.UNKNOWN)
-                        )
                 ).build();
 
-        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(musicRecordingSchema))
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(
+                musicRecordingSchema, /*version=*/0))
                 .isEqualTo(expectedMusicRecordingProto);
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedMusicRecordingProto))
                 .isEqualTo(musicRecordingSchema);
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index 31bc328..d2d8787 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -20,9 +20,11 @@
 
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.localstorage.util.PrefixUtil;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SearchResultProto;
 import com.google.android.icing.proto.SnippetMatchProto;
 import com.google.android.icing.proto.SnippetProto;
@@ -30,189 +32,232 @@
 import org.junit.Test;
 
 import java.util.Collections;
+import java.util.Map;
 
 public class SnippetTest {
+    private static final String SCHEMA_TYPE = "schema1";
+    private static final String PACKAGE_NAME = "packageName";
+    private static final String DATABASE_NAME = "databaseName";
+    private static final String PREFIX = PrefixUtil.createPrefix(PACKAGE_NAME, DATABASE_NAME);
+    private static final SchemaTypeConfigProto SCHEMA_TYPE_CONFIG_PROTO =
+            SchemaTypeConfigProto.newBuilder()
+                    .setSchemaType(PREFIX + SCHEMA_TYPE)
+                    .build();
+    private static final Map<String, Map<String, SchemaTypeConfigProto>> SCHEMA_MAP =
+            Collections.singletonMap(PREFIX,
+                    Collections.singletonMap(PREFIX + SCHEMA_TYPE,
+                            SCHEMA_TYPE_CONFIG_PROTO));
 
     // TODO(tytytyww): Add tests for Double and Long Snippets.
     @Test
     public void testSingleStringSnippet() {
-
         final String propertyKeyString = "content";
         final String propertyValueString = "A commonly used fake word is foo.\n"
                 + "   Another nonsense word that’s used a lot\n"
                 + "   is bar.\n";
-        final String uri = "uri1";
-        final String schemaType = "schema1";
-        final String searchWord = "foo";
+        final String id = "id1";
         final String exactMatch = "foo";
         final String window = "is foo";
 
         // Building the SearchResult received from query.
-        PropertyProto property = PropertyProto.newBuilder()
-                .setName(propertyKeyString)
-                .addStringValues(propertyValueString)
-                .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri(uri)
-                .setSchema(schemaType)
-                .addProperties(property)
+                .setUri(id)
+                .setSchema(SCHEMA_TYPE)
+                .addProperties(PropertyProto.newBuilder()
+                        .setName(propertyKeyString)
+                        .addStringValues(propertyValueString))
                 .build();
         SnippetProto snippetProto = SnippetProto.newBuilder()
                 .addEntries(SnippetProto.EntryProto.newBuilder()
                         .setPropertyName(propertyKeyString)
                         .addSnippetMatches(SnippetMatchProto.newBuilder()
-                                .setValuesIndex(0)
                                 .setExactMatchPosition(29)
                                 .setExactMatchBytes(3)
                                 .setWindowPosition(26)
-                                .setWindowBytes(6)
-                                .build())
-                        .build())
-                .build();
-        SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
-                .setDocument(documentProto)
-                .setSnippet(snippetProto)
+                                .setWindowBytes(6)))
                 .build();
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
-                .addResults(resultProto)
+                .addResults(SearchResultProto.ResultProto.newBuilder()
+                        .setDocument(documentProto)
+                        .setSnippet(snippetProto))
                 .build();
 
         // Making ResultReader and getting Snippet values.
-        SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.toSearchResultPage(searchResultProto,
-                        Collections.singletonList("packageName"));
-        for (SearchResult result : searchResultPage.getResults()) {
-            SearchResult.MatchInfo match = result.getMatches().get(0);
-            assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
-            assertThat(match.getFullText()).isEqualTo(propertyValueString);
-            assertThat(match.getExactMatch()).isEqualTo(exactMatch);
-            assertThat(match.getExactMatchPosition()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/29, /*upper=*/32));
-            assertThat(match.getFullText()).isEqualTo(propertyValueString);
-            assertThat(match.getSnippetPosition()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/26, /*upper=*/32));
-            assertThat(match.getSnippet()).isEqualTo(window);
-        }
+        SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
+                searchResultProto,
+                Collections.singletonList(PACKAGE_NAME),
+                Collections.singletonList(DATABASE_NAME),
+                SCHEMA_MAP);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        SearchResult.MatchInfo match = searchResultPage.getResults().get(0).getMatches().get(0);
+        assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
+        assertThat(match.getFullText()).isEqualTo(propertyValueString);
+        assertThat(match.getExactMatch()).isEqualTo(exactMatch);
+        assertThat(match.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/29, /*upper=*/32));
+        assertThat(match.getFullText()).isEqualTo(propertyValueString);
+        assertThat(match.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/26, /*upper=*/32));
+        assertThat(match.getSnippet()).isEqualTo(window);
     }
 
     // TODO(tytytyww): Add tests for Double and Long Snippets.
     @Test
-    public void testNoSnippets() throws Exception {
-
+    public void testNoSnippets() {
         final String propertyKeyString = "content";
         final String propertyValueString = "A commonly used fake word is foo.\n"
                 + "   Another nonsense word that’s used a lot\n"
                 + "   is bar.\n";
-        final String uri = "uri1";
-        final String schemaType = "schema1";
-        final String searchWord = "foo";
-        final String exactMatch = "foo";
-        final String window = "is foo";
+        final String id = "id1";
 
         // Building the SearchResult received from query.
-        PropertyProto property = PropertyProto.newBuilder()
-                .setName(propertyKeyString)
-                .addStringValues(propertyValueString)
-                .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri(uri)
-                .setSchema(schemaType)
-                .addProperties(property)
-                .build();
-        SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
-                .setDocument(documentProto)
+                .setUri(id)
+                .setSchema(SCHEMA_TYPE)
+                .addProperties(PropertyProto.newBuilder()
+                        .setName(propertyKeyString)
+                        .addStringValues(propertyValueString))
                 .build();
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
-                .addResults(resultProto)
+                .addResults(SearchResultProto.ResultProto.newBuilder().setDocument(documentProto))
                 .build();
 
-        SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.toSearchResultPage(searchResultProto,
-                        Collections.singletonList("packageName"));
-        for (SearchResult result : searchResultPage.getResults()) {
-            assertThat(result.getMatches()).isEmpty();
-        }
+        SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
+                searchResultProto,
+                Collections.singletonList(PACKAGE_NAME),
+                Collections.singletonList(DATABASE_NAME),
+                SCHEMA_MAP);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        assertThat(searchResultPage.getResults().get(0).getMatches()).isEmpty();
     }
 
     @Test
-    public void testMultipleStringSnippet() throws Exception {
-        final String searchWord = "Test";
-
+    public void testMultipleStringSnippet() {
         // Building the SearchResult received from query.
-        PropertyProto property1 = PropertyProto.newBuilder()
-                .setName("sender.name")
-                .addStringValues("Test Name Jr.")
-                .build();
-        PropertyProto property2 = PropertyProto.newBuilder()
-                .setName("sender.email")
-                .addStringValues("[email protected]")
-                .build();
         DocumentProto documentProto = DocumentProto.newBuilder()
                 .setUri("uri1")
-                .setSchema("schema1")
-                .addProperties(property1)
-                .addProperties(property2)
+                .setSchema(SCHEMA_TYPE)
+                .addProperties(PropertyProto.newBuilder()
+                        .setName("senderName")
+                        .addStringValues("Test Name Jr."))
+                .addProperties(PropertyProto.newBuilder()
+                        .setName("senderEmail")
+                        .addStringValues("[email protected]"))
                 .build();
         SnippetProto snippetProto = SnippetProto.newBuilder()
-                .addEntries(
-                        SnippetProto.EntryProto.newBuilder()
-                                .setPropertyName("sender.name")
-                                .addSnippetMatches(
-                                        SnippetMatchProto.newBuilder()
-                                                .setValuesIndex(0)
-                                                .setExactMatchPosition(0)
-                                                .setExactMatchBytes(4)
-                                                .setWindowPosition(0)
-                                                .setWindowBytes(9)
-                                                .build())
-                                .build())
-                .addEntries(
-                        SnippetProto.EntryProto.newBuilder()
-                                .setPropertyName("sender.email")
-                                .addSnippetMatches(
-                                        SnippetMatchProto.newBuilder()
-                                                .setValuesIndex(0)
-                                                .setExactMatchPosition(0)
-                                                .setExactMatchBytes(20)
-                                                .setWindowPosition(0)
-                                                .setWindowBytes(20)
-                                                .build())
-                                .build()
-                )
-                .build();
-        SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
-                .setDocument(documentProto)
-                .setSnippet(snippetProto)
+                .addEntries(SnippetProto.EntryProto.newBuilder()
+                        .setPropertyName("senderName")
+                        .addSnippetMatches(SnippetMatchProto.newBuilder()
+                                .setExactMatchPosition(0)
+                                .setExactMatchBytes(4)
+                                .setWindowPosition(0)
+                                .setWindowBytes(9)))
+                .addEntries(SnippetProto.EntryProto.newBuilder()
+                        .setPropertyName("senderEmail")
+                        .addSnippetMatches(SnippetMatchProto.newBuilder()
+                                .setExactMatchPosition(0)
+                                .setExactMatchBytes(20)
+                                .setWindowPosition(0)
+                                .setWindowBytes(20)))
                 .build();
         SearchResultProto searchResultProto = SearchResultProto.newBuilder()
-                .addResults(resultProto)
+                .addResults(SearchResultProto.ResultProto.newBuilder()
+                        .setDocument(documentProto)
+                        .setSnippet(snippetProto))
                 .build();
 
         // Making ResultReader and getting Snippet values.
-        SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.toSearchResultPage(searchResultProto,
-                        Collections.singletonList("packageName"));
-        for (SearchResult result : searchResultPage.getResults()) {
+        SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
+                searchResultProto,
+                Collections.singletonList(PACKAGE_NAME),
+                Collections.singletonList(DATABASE_NAME),
+                SCHEMA_MAP);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatches().get(0);
+        assertThat(match1.getPropertyPath()).isEqualTo("senderName");
+        assertThat(match1.getFullText()).isEqualTo("Test Name Jr.");
+        assertThat(match1.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/4));
+        assertThat(match1.getExactMatch()).isEqualTo("Test");
+        assertThat(match1.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/9));
+        assertThat(match1.getSnippet()).isEqualTo("Test Name");
 
-            SearchResult.MatchInfo match1 = result.getMatches().get(0);
-            assertThat(match1.getPropertyPath()).isEqualTo("sender.name");
-            assertThat(match1.getFullText()).isEqualTo("Test Name Jr.");
-            assertThat(match1.getExactMatchPosition()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/0, /*upper=*/4));
-            assertThat(match1.getExactMatch()).isEqualTo("Test");
-            assertThat(match1.getSnippetPosition()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/0, /*upper=*/9));
-            assertThat(match1.getSnippet()).isEqualTo("Test Name");
+        SearchResult.MatchInfo match2 = searchResultPage.getResults().get(0).getMatches().get(1);
+        assertThat(match2.getPropertyPath()).isEqualTo("senderEmail");
+        assertThat(match2.getFullText()).isEqualTo("[email protected]");
+        assertThat(match2.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
+        assertThat(match2.getExactMatch()).isEqualTo("[email protected]");
+        assertThat(match2.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
+        assertThat(match2.getSnippet()).isEqualTo("[email protected]");
+    }
 
-            SearchResult.MatchInfo match2 = result.getMatches().get(1);
-            assertThat(match2.getPropertyPath()).isEqualTo("sender.email");
-            assertThat(match2.getFullText()).isEqualTo("[email protected]");
-            assertThat(match2.getExactMatchPosition()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
-            assertThat(match2.getExactMatch()).isEqualTo("[email protected]");
-            assertThat(match2.getSnippetPosition()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/0, /*upper=*/20));
-            assertThat(match2.getSnippet()).isEqualTo("[email protected]");
-        }
+    @Test
+    public void testNestedDocumentSnippet() {
+        // Building the SearchResult received from query.
+        DocumentProto documentProto = DocumentProto.newBuilder()
+                .setUri("id1")
+                .setSchema(SCHEMA_TYPE)
+                .addProperties(PropertyProto.newBuilder()
+                        .setName("sender")
+                        .addDocumentValues(DocumentProto.newBuilder()
+                                .addProperties(PropertyProto.newBuilder()
+                                        .setName("name")
+                                        .addStringValues("Test Name Jr."))
+                                .addProperties(PropertyProto.newBuilder()
+                                        .setName("email")
+                                        .addStringValues("[email protected]")
+                                        .addStringValues("[email protected]"))))
+                .build();
+        SnippetProto snippetProto = SnippetProto.newBuilder()
+                .addEntries(SnippetProto.EntryProto.newBuilder()
+                        .setPropertyName("sender.name")
+                        .addSnippetMatches(SnippetMatchProto.newBuilder()
+                                .setExactMatchPosition(0)
+                                .setExactMatchBytes(4)
+                                .setWindowPosition(0)
+                                .setWindowBytes(9)))
+                .addEntries(SnippetProto.EntryProto.newBuilder()
+                        .setPropertyName("sender.email[1]")
+                        .addSnippetMatches(SnippetMatchProto.newBuilder()
+                                .setExactMatchPosition(0)
+                                .setExactMatchBytes(21)
+                                .setWindowPosition(0)
+                                .setWindowBytes(21)))
+                .build();
+        SearchResultProto searchResultProto = SearchResultProto.newBuilder()
+                .addResults(SearchResultProto.ResultProto.newBuilder()
+                        .setDocument(documentProto)
+                        .setSnippet(snippetProto))
+                .build();
+
+        // Making ResultReader and getting Snippet values.
+        SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
+                searchResultProto,
+                Collections.singletonList(PACKAGE_NAME),
+                Collections.singletonList(DATABASE_NAME),
+                SCHEMA_MAP);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatches().get(0);
+        assertThat(match1.getPropertyPath()).isEqualTo("sender.name");
+        assertThat(match1.getFullText()).isEqualTo("Test Name Jr.");
+        assertThat(match1.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/4));
+        assertThat(match1.getExactMatch()).isEqualTo("Test");
+        assertThat(match1.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/9));
+        assertThat(match1.getSnippet()).isEqualTo("Test Name");
+
+        SearchResult.MatchInfo match2 = searchResultPage.getResults().get(0).getMatches().get(1);
+        assertThat(match2.getPropertyPath()).isEqualTo("sender.email[1]");
+        assertThat(match2.getFullText()).isEqualTo("[email protected]");
+        assertThat(match2.getExactMatchRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/21));
+        assertThat(match2.getExactMatch()).isEqualTo("[email protected]");
+        assertThat(match2.getSnippetRange()).isEqualTo(
+                new SearchResult.MatchRange(/*lower=*/0, /*upper=*/21));
+        assertThat(match2.getSnippet()).isEqualTo("[email protected]");
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
new file mode 100644
index 0000000..a9afb96
--- /dev/null
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchResult;
+
+import org.junit.Test;
+
+public class AppSearchStatsTest {
+    static final String TEST_PACKAGE_NAME = "com.google.test";
+    static final String TEST_DATA_BASE = "testDataBase";
+    static final int TEST_STATUS_CODE = AppSearchResult.RESULT_INTERNAL_ERROR;
+    static final int TEST_TOTAL_LATENCY_MILLIS = 20;
+
+    @Test
+    public void testAppSearchStats_GeneralStats() {
+        final GeneralStats gStats = new GeneralStats.Builder(TEST_PACKAGE_NAME, TEST_DATA_BASE)
+                .setStatusCode(TEST_STATUS_CODE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                .build();
+
+        assertThat(gStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(gStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(gStats.getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(gStats.getTotalLatencyMillis()).isEqualTo(TEST_TOTAL_LATENCY_MILLIS);
+    }
+
+    /**
+     * Make sure status code is UNKNOWN if not set in {@link GeneralStats}
+     */
+    @Test
+    public void testAppSearchStats_GeneralStats_defaultStatsCode_Unknown() {
+        final GeneralStats gStats = new GeneralStats.Builder(TEST_PACKAGE_NAME, TEST_DATA_BASE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                .build();
+
+        assertThat(gStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(gStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(gStats.getStatusCode()).isEqualTo(AppSearchResult.RESULT_UNKNOWN_ERROR);
+        assertThat(gStats.getTotalLatencyMillis()).isEqualTo(TEST_TOTAL_LATENCY_MILLIS);
+    }
+
+    @Test
+    public void testAppSearchStats_CallStats() {
+        final int estimatedBinderLatencyMillis = 1;
+        final int numOperationsSucceeded = 2;
+        final int numOperationsFailed = 3;
+        final @CallStats.CallType int callType =
+                CallStats.CALL_TYPE_PUT_DOCUMENTS;
+
+        final CallStats.Builder cStatsBuilder = new CallStats.Builder(TEST_PACKAGE_NAME,
+                TEST_DATA_BASE)
+                .setCallType(callType)
+                .setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
+                .setNumOperationsSucceeded(numOperationsSucceeded)
+                .setNumOperationsFailed(numOperationsFailed);
+        cStatsBuilder.getGeneralStatsBuilder()
+                .setStatusCode(TEST_STATUS_CODE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS);
+        final CallStats cStats = cStatsBuilder.build();
+
+        assertThat(cStats.getGeneralStats().getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(cStats.getGeneralStats().getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(cStats.getGeneralStats().getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(cStats.getGeneralStats().getTotalLatencyMillis()).isEqualTo(
+                TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(cStats.getEstimatedBinderLatencyMillis())
+                .isEqualTo(estimatedBinderLatencyMillis);
+        assertThat(cStats.getCallType()).isEqualTo(callType);
+        assertThat(cStats.getNumOperationsSucceeded()).isEqualTo(numOperationsSucceeded);
+        assertThat(cStats.getNumOperationsFailed()).isEqualTo(numOperationsFailed);
+    }
+
+    @Test
+    public void testAppSearchStats_PutDocumentStats() {
+        final int generateDocumentProtoLatencyMillis = 1;
+        final int rewriteDocumentTypesLatencyMillis = 2;
+        final int nativeLatencyMillis = 3;
+        final int nativeDocumentStoreLatencyMillis = 4;
+        final int nativeIndexLatencyMillis = 5;
+        final int nativeIndexMergeLatencyMillis = 6;
+        final int nativeDocumentSize = 7;
+        final int nativeNumTokensIndexed = 8;
+        final boolean nativeExceededMaxNumTokens = true;
+        final PutDocumentStats.Builder pStatsBuilder =
+                new PutDocumentStats.Builder(TEST_PACKAGE_NAME, TEST_DATA_BASE)
+                        .setGenerateDocumentProtoLatencyMillis(generateDocumentProtoLatencyMillis)
+                        .setRewriteDocumentTypesLatencyMillis(rewriteDocumentTypesLatencyMillis)
+                        .setNativeLatencyMillis(nativeLatencyMillis)
+                        .setNativeDocumentStoreLatencyMillis(nativeDocumentStoreLatencyMillis)
+                        .setNativeIndexLatencyMillis(nativeIndexLatencyMillis)
+                        .setNativeIndexMergeLatencyMillis(nativeIndexMergeLatencyMillis)
+                        .setNativeDocumentSizeBytes(nativeDocumentSize)
+                        .setNativeNumTokensIndexed(nativeNumTokensIndexed)
+                        .setNativeExceededMaxNumTokens(nativeExceededMaxNumTokens);
+        pStatsBuilder.getGeneralStatsBuilder()
+                .setStatusCode(TEST_STATUS_CODE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS);
+        final PutDocumentStats pStats = pStatsBuilder.build();
+
+        assertThat(pStats.getGeneralStats().getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(pStats.getGeneralStats().getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(pStats.getGeneralStats().getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(pStats.getGeneralStats().getTotalLatencyMillis()).isEqualTo(
+                TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(pStats.getGenerateDocumentProtoLatencyMillis()).isEqualTo(
+                generateDocumentProtoLatencyMillis);
+        assertThat(pStats.getRewriteDocumentTypesLatencyMillis()).isEqualTo(
+                rewriteDocumentTypesLatencyMillis);
+        assertThat(pStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(pStats.getNativeDocumentStoreLatencyMillis()).isEqualTo(
+                nativeDocumentStoreLatencyMillis);
+        assertThat(pStats.getNativeIndexLatencyMillis()).isEqualTo(nativeIndexLatencyMillis);
+        assertThat(pStats.getNativeIndexMergeLatencyMillis()).isEqualTo(
+                nativeIndexMergeLatencyMillis);
+        assertThat(pStats.getNativeDocumentSizeBytes()).isEqualTo(nativeDocumentSize);
+        assertThat(pStats.getNativeNumTokensIndexed()).isEqualTo(nativeNumTokensIndexed);
+        assertThat(pStats.getNativeExceededMaxNumTokens()).isEqualTo(nativeExceededMaxNumTokens);
+    }
+
+    @Test
+    public void testAppSearchStats_InitializeStats() {
+        int prepareSchemaAndNamespacesLatencyMillis = 1;
+        int prepareVisibilityFileLatencyMillis = 2;
+        int nativeLatencyMillis = 3;
+        int nativeDocumentStoreRecoveryCause = 4;
+        int nativeIndexRestorationCause = 5;
+        int nativeSchemaStoreRecoveryCause = 6;
+        int nativeDocumentStoreRecoveryLatencyMillis = 7;
+        int nativeIndexRestorationLatencyMillis = 8;
+        int nativeSchemaStoreRecoveryLatencyMillis = 9;
+        int nativeDocumentStoreDataStatus = 10;
+        int nativeNumDocuments = 11;
+        int nativeNumSchemaTypes = 12;
+
+        final InitializeStats.Builder iStatsBuilder = new InitializeStats.Builder()
+                .setStatusCode(TEST_STATUS_CODE)
+                .setTotalLatencyMillis(TEST_TOTAL_LATENCY_MILLIS)
+                .setHasDeSync(/* hasDeSyncs= */ true)
+                .setPrepareSchemaAndNamespacesLatencyMillis(prepareSchemaAndNamespacesLatencyMillis)
+                .setPrepareVisibilityStoreLatencyMillis(prepareVisibilityFileLatencyMillis)
+                .setNativeLatencyMillis(nativeLatencyMillis)
+                .setDocumentStoreRecoveryCause(nativeDocumentStoreRecoveryCause)
+                .setIndexRestorationCause(nativeIndexRestorationCause)
+                .setSchemaStoreRecoveryCause(nativeSchemaStoreRecoveryCause)
+                .setDocumentStoreRecoveryLatencyMillis(
+                        nativeDocumentStoreRecoveryLatencyMillis)
+                .setIndexRestorationLatencyMillis(nativeIndexRestorationLatencyMillis)
+                .setSchemaStoreRecoveryLatencyMillis(nativeSchemaStoreRecoveryLatencyMillis)
+                .setDocumentStoreDataStatus(nativeDocumentStoreDataStatus)
+                .setDocumentCount(nativeNumDocuments)
+                .setSchemaTypeCount(nativeNumSchemaTypes);
+        final InitializeStats iStats = iStatsBuilder.build();
+
+
+        assertThat(iStats.getStatusCode()).isEqualTo(TEST_STATUS_CODE);
+        assertThat(iStats.getTotalLatencyMillis()).isEqualTo(
+                TEST_TOTAL_LATENCY_MILLIS);
+        assertThat(iStats.hasDeSync()).isTrue();
+        assertThat(iStats.getPrepareSchemaAndNamespacesLatencyMillis()).isEqualTo(
+                prepareSchemaAndNamespacesLatencyMillis);
+        assertThat(iStats.getPrepareVisibilityStoreLatencyMillis()).isEqualTo(
+                prepareVisibilityFileLatencyMillis);
+        assertThat(iStats.getNativeLatencyMillis()).isEqualTo(nativeLatencyMillis);
+        assertThat(iStats.getDocumentStoreRecoveryCause()).isEqualTo(
+                nativeDocumentStoreRecoveryCause);
+        assertThat(iStats.getIndexRestorationCause()).isEqualTo(nativeIndexRestorationCause);
+        assertThat(iStats.getSchemaStoreRecoveryCause()).isEqualTo(
+                nativeSchemaStoreRecoveryCause);
+        assertThat(iStats.getDocumentStoreRecoveryLatencyMillis()).isEqualTo(
+                nativeDocumentStoreRecoveryLatencyMillis);
+        assertThat(iStats.getIndexRestorationLatencyMillis()).isEqualTo(
+                nativeIndexRestorationLatencyMillis);
+        assertThat(iStats.getSchemaStoreRecoveryLatencyMillis()).isEqualTo(
+                nativeSchemaStoreRecoveryLatencyMillis);
+        assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
+                nativeDocumentStoreDataStatus);
+        assertThat(iStats.getDocumentCount()).isEqualTo(nativeNumDocuments);
+        assertThat(iStats.getSchemaTypeCount()).isEqualTo(nativeNumSchemaTypes);
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index e97d137..41e6534 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -16,24 +16,46 @@
 
 package androidx.appsearch.localstorage;
 
+import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument;
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPackagePrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
+
+import android.content.Context;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
-import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.StorageInfo;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter;
+import androidx.appsearch.localstorage.converter.ResultCodeToProtoConverter;
 import androidx.appsearch.localstorage.converter.SchemaToProtoConverter;
 import androidx.appsearch.localstorage.converter.SearchResultToProtoConverter;
 import androidx.appsearch.localstorage.converter.SearchSpecToProtoConverter;
+import androidx.appsearch.localstorage.converter.SetSchemaResponseToProtoConverter;
+import androidx.appsearch.localstorage.converter.TypePropertyPathToProtoConverter;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.PutDocumentStats;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
@@ -41,6 +63,7 @@
 import com.google.android.icing.proto.DeleteByQueryResultProto;
 import com.google.android.icing.proto.DeleteResultProto;
 import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.DocumentStorageInfoProto;
 import com.google.android.icing.proto.GetAllNamespacesResultProto;
 import com.google.android.icing.proto.GetOptimizeInfoResultProto;
 import com.google.android.icing.proto.GetResultProto;
@@ -48,11 +71,13 @@
 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.NamespaceStorageInfoProto;
 import com.google.android.icing.proto.OptimizeResultProto;
 import com.google.android.icing.proto.PersistToDiskResultProto;
+import com.google.android.icing.proto.PersistType;
 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.ReportUsageResultProto;
 import com.google.android.icing.proto.ResetResultProto;
 import com.google.android.icing.proto.ResultSpecProto;
 import com.google.android.icing.proto.SchemaProto;
@@ -62,8 +87,11 @@
 import com.google.android.icing.proto.SearchSpecProto;
 import com.google.android.icing.proto.SetSchemaResultProto;
 import com.google.android.icing.proto.StatusProto;
+import com.google.android.icing.proto.StorageInfoResultProto;
 import com.google.android.icing.proto.TypePropertyMask;
+import com.google.android.icing.proto.UsageReport;
 
+import java.io.Closeable;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -109,16 +137,10 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @WorkerThread
-public final class AppSearchImpl {
+public final class AppSearchImpl implements Closeable {
     private static final String TAG = "AppSearchImpl";
 
     @VisibleForTesting
-    static final char DATABASE_DELIMITER = '/';
-
-    @VisibleForTesting
-    static final char PACKAGE_DELIMITER = '$';
-
-    @VisibleForTesting
     static final int OPTIMIZE_THRESHOLD_DOC_COUNT = 1000;
     @VisibleForTesting
     static final int OPTIMIZE_THRESHOLD_BYTES = 1_000_000; // 1MB
@@ -133,11 +155,12 @@
     @GuardedBy("mReadWriteLock")
     private final VisibilityStore mVisibilityStoreLocked;
 
-    // This map contains schemaTypes for all package-database prefixes. All values in the map are
-    // prefixed with the package-database prefix.
-    // TODO(b/172360376): Check if this can be replaced with an ArrayMap
+    // This map contains schema types and SchemaTypeConfigProtos for all package-database
+    // prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
+    // prefixed schema type to its respective SchemaTypeConfigProto.
     @GuardedBy("mReadWriteLock")
-    private final Map<String, Set<String>> mSchemaMapLocked = new HashMap<>();
+    private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMapLocked =
+            new ArrayMap<>();
 
     // This map contains namespaces for all package-database prefixes. All values in the map are
     // prefixed with the package-database prefix.
@@ -146,26 +169,71 @@
     private final Map<String, Set<String>> mNamespaceMapLocked = new HashMap<>();
 
     /**
-     * The counter to check when to call {@link #checkForOptimizeLocked(boolean)}. The
+     * The counter to check when to call {@link #checkForOptimize}. The
      * interval is
      * {@link #CHECK_OPTIMIZE_INTERVAL}.
      */
     @GuardedBy("mReadWriteLock")
     private int mOptimizeIntervalCountLocked = 0;
 
+    /** Whether this instance has been closed, and therefore unusable. */
+    @GuardedBy("mReadWriteLock")
+    private boolean mClosedLocked = false;
+
     /**
      * Creates and initializes an instance of {@link AppSearchImpl} which writes data to the given
      * folder.
+     *
+     * <p>Clients can pass a {@link AppSearchLogger} here through their AppSearchSession, but it
+     * can't be saved inside {@link AppSearchImpl}, because the impl will be shared by all the
+     * sessions for the same package in JetPack.
+     *
+     * <p>Instead, logger instance needs to be passed to each individual method, like create, query
+     * and putDocument.
+     *
+     * @param logger collects stats for initialization if provided.
      */
     @NonNull
-    public static AppSearchImpl create(@NonNull File icingDir) throws AppSearchException {
+    public static AppSearchImpl create(@NonNull File icingDir, @NonNull Context context, int userId,
+            @NonNull String globalQuerierPackage, @Nullable AppSearchLogger logger)
+            throws AppSearchException {
         Preconditions.checkNotNull(icingDir);
-        AppSearchImpl appSearchImpl = new AppSearchImpl(icingDir);
+        Preconditions.checkNotNull(context);
+        Preconditions.checkNotNull(globalQuerierPackage);
+
+        long totalLatencyStartMillis = SystemClock.elapsedRealtime();
+        InitializeStats.Builder initStatsBuilder = null;
+        if (logger != null) {
+            initStatsBuilder = new InitializeStats.Builder();
+        }
+
+        AppSearchImpl appSearchImpl =
+                new AppSearchImpl(icingDir, context, userId, globalQuerierPackage,
+                        initStatsBuilder);
+
+        long prepareVisibilityStoreLatencyStartMillis = SystemClock.elapsedRealtime();
         appSearchImpl.initializeVisibilityStore();
+        long prepareVisibilityStoreLatencyEndMillis = SystemClock.elapsedRealtime();
+
+        if (logger != null && initStatsBuilder != null) {
+            initStatsBuilder
+                    .setTotalLatencyMillis(
+                            (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis))
+                    .setPrepareVisibilityStoreLatencyMillis(
+                            (int) (prepareVisibilityStoreLatencyEndMillis
+                                    - prepareVisibilityStoreLatencyStartMillis));
+            logger.logStats(initStatsBuilder.build());
+        }
+
         return appSearchImpl;
     }
 
-    private AppSearchImpl(@NonNull File icingDir) throws AppSearchException {
+    /**
+     * @param initStatsBuilder collects stats for initialization if provided.
+     */
+    private AppSearchImpl(@NonNull File icingDir, @NonNull Context context, int userId,
+            @NonNull String globalQuerierPackage,
+            @Nullable InitializeStats.Builder initStatsBuilder) throws AppSearchException {
         mReadWriteLock.writeLock().lock();
 
         try {
@@ -174,12 +242,24 @@
             IcingSearchEngineOptions options = IcingSearchEngineOptions.newBuilder()
                     .setBaseDir(icingDir.getAbsolutePath()).build();
             mIcingSearchEngineLocked = new IcingSearchEngine(options);
-
-            mVisibilityStoreLocked = new VisibilityStore(this);
-
+            mVisibilityStoreLocked = new VisibilityStore(this, context, userId,
+                    globalQuerierPackage);
             InitializeResultProto initializeResultProto = mIcingSearchEngineLocked.initialize();
+
+            if (initStatsBuilder != null) {
+                initStatsBuilder
+                        .setStatusCode(
+                                statusProtoToAppSearchException(
+                                        initializeResultProto.getStatus()).getResultCode())
+                        // TODO(b/173532925) how to get DeSyncs value
+                        .setHasDeSync(false);
+                AppSearchLoggerHelper.copyNativeStats(
+                        initializeResultProto.getInitializeStats(), initStatsBuilder);
+            }
+
+            long prepareSchemaAndNamespacesLatencyStartMillis = SystemClock.elapsedRealtime();
             SchemaProto schemaProto;
-            GetAllNamespacesResultProto getAllNamespacesResultProto;
+            GetAllNamespacesResultProto getAllNamespacesResultProto = null;
             try {
                 checkSuccess(initializeResultProto.getStatus());
                 schemaProto = getSchemaProtoLocked();
@@ -187,8 +267,16 @@
                 checkSuccess(getAllNamespacesResultProto.getStatus());
             } catch (AppSearchException e) {
                 Log.w(TAG, "Error initializing, resetting IcingSearchEngine.", e);
+                if (initStatsBuilder != null && getAllNamespacesResultProto != null) {
+                    initStatsBuilder.setStatusCode(
+                            statusProtoToAppSearchException(
+                                    getAllNamespacesResultProto.getStatus()).getResultCode())
+                            .setPrepareSchemaAndNamespacesLatencyMillis(
+                                    (int) (SystemClock.elapsedRealtime()
+                                            - prepareSchemaAndNamespacesLatencyStartMillis));
+                }
                 // Some error. Reset and see if it fixes it.
-                reset();
+                resetLocked();
                 return;
             }
 
@@ -196,7 +284,7 @@
             for (SchemaTypeConfigProto schema : schemaProto.getTypesList()) {
                 String prefixedSchemaType = schema.getSchemaType();
                 addToMap(mSchemaMapLocked, getPrefix(prefixedSchemaType),
-                        prefixedSchemaType);
+                        schema);
             }
 
             // Populate namespace map
@@ -205,10 +293,12 @@
                         prefixedNamespace);
             }
 
-            // TODO(b/155939114): It's possible to optimize after init, which would reduce the time
-            //   to when we're able to serve queries. Consider moving this optimize call out.
-            checkForOptimizeLocked(/* force= */ true);
-
+            // logging prepare_schema_and_namespaces latency
+            if (initStatsBuilder != null) {
+                initStatsBuilder.setPrepareSchemaAndNamespacesLatencyMillis(
+                        (int) (SystemClock.elapsedRealtime()
+                                - prepareSchemaAndNamespacesLatencyStartMillis));
+            }
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
@@ -222,12 +312,45 @@
     void initializeVisibilityStore() throws AppSearchException {
         mReadWriteLock.writeLock().lock();
         try {
+            throwIfClosedLocked();
+
             mVisibilityStoreLocked.initialize();
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
     }
 
+    @GuardedBy("mReadWriteLock")
+    private void throwIfClosedLocked() {
+        if (mClosedLocked) {
+            throw new IllegalStateException("Trying to use a closed AppSearchImpl instance.");
+        }
+    }
+
+    /**
+     * Persists data to disk and closes the instance.
+     *
+     * <p>This instance is no longer usable after it's been closed. Call {@link #create} to
+     * create a new, usable instance.
+     */
+    @Override
+    public void close() {
+        mReadWriteLock.writeLock().lock();
+        try {
+            if (mClosedLocked) {
+                return;
+            }
+
+            persistToDisk(PersistType.Code.FULL);
+            mIcingSearchEngineLocked.close();
+            mClosedLocked = true;
+        } catch (AppSearchException e) {
+            Log.w(TAG, "Error when closing AppSearchImpl.", e);
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
+    }
+
     /**
      * Updates the AppSearch schema for this app.
      *
@@ -238,25 +361,37 @@
      * @param schemas                       Schemas to set for this app.
      * @param schemasNotPlatformSurfaceable Schema types that should not be surfaced on platform
      *                                      surfaces.
+     * @param schemasPackageAccessible      Schema types that are visible to the specified packages.
      * @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.
+     * @param version                       The overall version number of the request.
+     * @return The response contains deleted schema types and incompatible schema types of this
+     * call.
+     * @throws AppSearchException On IcingSearchEngine error. If the status code is
+     *                            FAILED_PRECONDITION for the incompatible change, the
+     *                            exception will be converted to the SetSchemaResponse.
      */
-    public void setSchema(
+    @NonNull
+    public SetSchemaResponse setSchema(
             @NonNull String packageName,
             @NonNull String databaseName,
             @NonNull List<AppSearchSchema> schemas,
             @NonNull List<String> schemasNotPlatformSurfaceable,
-            boolean forceOverride) throws AppSearchException {
+            @NonNull Map<String, List<PackageIdentifier>> schemasPackageAccessible,
+            boolean forceOverride,
+            int version) throws AppSearchException {
         mReadWriteLock.writeLock().lock();
         try {
+            throwIfClosedLocked();
+
             SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder();
 
             SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
             for (int i = 0; i < schemas.size(); i++) {
+                AppSearchSchema schema = schemas.get(i);
                 SchemaTypeConfigProto schemaTypeProto =
-                        SchemaToProtoConverter.toSchemaTypeConfigProto(schemas.get(i));
+                        SchemaToProtoConverter.toSchemaTypeConfigProto(schema, version);
                 newSchemaBuilder.addTypes(schemaTypeProto);
             }
 
@@ -276,22 +411,29 @@
             try {
                 checkSuccess(setSchemaResultProto.getStatus());
             } catch (AppSearchException e) {
-                // Improve the error message by merging in information about incompatible types.
-                if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
-                        || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0) {
-                    String newMessage = e.getMessage()
-                            + "\n  Deleted types: "
-                            + setSchemaResultProto.getDeletedSchemaTypesList()
-                            + "\n  Incompatible types: "
-                            + setSchemaResultProto.getIncompatibleSchemaTypesList();
-                    throw new AppSearchException(e.getResultCode(), newMessage, e.getCause());
+                // Swallow the exception for the incompatible change case. We will propagate
+                // those deleted schemas and incompatible types to the SetSchemaResponse.
+                boolean isFailedPrecondition = setSchemaResultProto.getStatus().getCode()
+                        == StatusProto.Code.FAILED_PRECONDITION;
+                boolean isIncompatible = setSchemaResultProto.getDeletedSchemaTypesCount() > 0
+                        || setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0;
+                if (isFailedPrecondition && isIncompatible) {
+                    return SetSchemaResponseToProtoConverter
+                            .toSetSchemaResponse(setSchemaResultProto, prefix);
                 } else {
                     throw e;
                 }
             }
 
             // Update derived data structures.
-            mSchemaMapLocked.put(prefix, rewrittenSchemaResults.mRewrittenPrefixedTypes);
+            for (SchemaTypeConfigProto schemaTypeConfigProto :
+                    rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
+                addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
+            }
+
+            for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
+                removeFromMap(mSchemaMapLocked, prefix, schemaType);
+            }
 
             Set<String> prefixedSchemasNotPlatformSurfaceable =
                     new ArraySet<>(schemasNotPlatformSurfaceable.size());
@@ -299,18 +441,19 @@
                 prefixedSchemasNotPlatformSurfaceable.add(
                         prefix + schemasNotPlatformSurfaceable.get(i));
             }
-            mVisibilityStoreLocked.setVisibility(prefix,
-                    prefixedSchemasNotPlatformSurfaceable);
 
-            // Determine whether to schedule an immediate optimize.
-            if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
-                    || (setSchemaResultProto.getIncompatibleSchemaTypesCount() > 0
-                    && forceOverride)) {
-                // Any existing schemas which is not in 'schemas' will be deleted, and all
-                // documents of these types were also deleted. And so well if we force override
-                // incompatible schemas.
-                checkForOptimizeLocked(/* force= */true);
+            Map<String, List<PackageIdentifier>> prefixedSchemasPackageAccessible =
+                    new ArrayMap<>(schemasPackageAccessible.size());
+            for (Map.Entry<String, List<PackageIdentifier>> entry :
+                    schemasPackageAccessible.entrySet()) {
+                prefixedSchemasPackageAccessible.put(prefix + entry.getKey(), entry.getValue());
             }
+
+            mVisibilityStoreLocked.setVisibility(prefix,
+                    prefixedSchemasNotPlatformSurfaceable, prefixedSchemasPackageAccessible);
+
+            return SetSchemaResponseToProtoConverter
+                    .toSetSchemaResponse(setSchemaResultProto, prefix);
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
@@ -326,47 +469,89 @@
      * @throws AppSearchException on IcingSearchEngine error.
      */
     @NonNull
-    public List<AppSearchSchema> getSchema(@NonNull String packageName,
+    public GetSchemaResponse getSchema(@NonNull String packageName,
             @NonNull String databaseName) throws AppSearchException {
-        SchemaProto fullSchema;
         mReadWriteLock.readLock().lock();
         try {
-            fullSchema = getSchemaProtoLocked();
+            throwIfClosedLocked();
+
+            SchemaProto fullSchema = getSchemaProtoLocked();
+
+            String prefix = createPrefix(packageName, databaseName);
+            GetSchemaResponse.Builder responseBuilder = new GetSchemaResponse.Builder();
+            for (int i = 0; i < fullSchema.getTypesCount(); i++) {
+                String typePrefix = getPrefix(fullSchema.getTypes(i).getSchemaType());
+                if (!prefix.equals(typePrefix)) {
+                    continue;
+                }
+                // Rewrite SchemaProto.types.schema_type
+                SchemaTypeConfigProto.Builder typeConfigBuilder = fullSchema.getTypes(
+                        i).toBuilder();
+                String newSchemaType =
+                        typeConfigBuilder.getSchemaType().substring(prefix.length());
+                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 = propertyConfigBuilder.getSchemaType()
+                                .substring(prefix.length());
+                        propertyConfigBuilder.setSchemaType(newPropertySchemaType);
+                        typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+                    }
+                }
+
+                AppSearchSchema schema = SchemaToProtoConverter.toAppSearchSchema(
+                        typeConfigBuilder);
+
+                //TODO(b/183050495) find a place to store the version for the database, rather
+                // than read from a schema.
+                responseBuilder.setVersion(fullSchema.getTypes(i).getVersion());
+                responseBuilder.addSchema(schema);
+            }
+            return responseBuilder.build();
         } finally {
             mReadWriteLock.readLock().unlock();
         }
+    }
 
-        String prefix = createPrefix(packageName, databaseName);
-        List<AppSearchSchema> result = new ArrayList<>();
-        for (int i = 0; i < fullSchema.getTypesCount(); i++) {
-            String typePrefix = getPrefix(fullSchema.getTypes(i).getSchemaType());
-            if (!prefix.equals(typePrefix)) {
-                continue;
-            }
-            // Rewrite SchemaProto.types.schema_type
-            SchemaTypeConfigProto.Builder typeConfigBuilder = fullSchema.getTypes(i).toBuilder();
-            String newSchemaType =
-                    typeConfigBuilder.getSchemaType().substring(prefix.length());
-            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 = propertyConfigBuilder.getSchemaType()
-                            .substring(prefix.length());
-                    propertyConfigBuilder.setSchemaType(newPropertySchemaType);
-                    typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+    /**
+     * Retrieves the list of namespaces with at least one document for this package name, database.
+     *
+     * <p>This method belongs to query group.
+     *
+     * @param packageName  Package name that owns this schema
+     * @param databaseName The name of the database where this schema lives.
+     * @throws AppSearchException on IcingSearchEngine error.
+     */
+    @NonNull
+    public List<String> getNamespaces(
+            @NonNull String packageName, @NonNull String databaseName) throws AppSearchException {
+        mReadWriteLock.readLock().lock();
+        try {
+            throwIfClosedLocked();
+            // We can't just use mNamespaceMap here because we have no way to prune namespaces from
+            // mNamespaceMap when they have no more documents (e.g. after setting schema to empty or
+            // using deleteByQuery).
+            GetAllNamespacesResultProto getAllNamespacesResultProto =
+                    mIcingSearchEngineLocked.getAllNamespaces();
+            checkSuccess(getAllNamespacesResultProto.getStatus());
+            String prefix = createPrefix(packageName, databaseName);
+            List<String> results = new ArrayList<>();
+            for (int i = 0; i < getAllNamespacesResultProto.getNamespacesCount(); i++) {
+                String prefixedNamespace = getAllNamespacesResultProto.getNamespaces(i);
+                if (prefixedNamespace.startsWith(prefix)) {
+                    results.add(prefixedNamespace.substring(prefix.length()));
                 }
             }
-
-            AppSearchSchema schema = SchemaToProtoConverter.toAppSearchSchema(typeConfigBuilder);
-            result.add(schema);
+            return results;
+        } finally {
+            mReadWriteLock.readLock().unlock();
         }
-        return result;
     }
 
     /**
@@ -380,57 +565,119 @@
      * @throws AppSearchException on IcingSearchEngine error.
      */
     public void putDocument(@NonNull String packageName, @NonNull String databaseName,
-            @NonNull GenericDocument document)
+            @NonNull GenericDocument document, @Nullable AppSearchLogger logger)
             throws AppSearchException {
-        DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.toDocumentProto(
-                document).toBuilder();
-        String prefix = createPrefix(packageName, databaseName);
-        addPrefixToDocument(documentBuilder, prefix);
+        PutDocumentStats.Builder pStatsBuilder = null;
+        if (logger != null) {
+            pStatsBuilder = new PutDocumentStats.Builder(packageName, databaseName);
+        }
+        long totalStartTimeMillis = SystemClock.elapsedRealtime();
 
-        PutResultProto putResultProto;
         mReadWriteLock.writeLock().lock();
         try {
-            putResultProto = mIcingSearchEngineLocked.put(documentBuilder.build());
+            throwIfClosedLocked();
+
+            // Generate Document Proto
+            long generateDocumentProtoStartTimeMillis = SystemClock.elapsedRealtime();
+            DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.toDocumentProto(
+                    document).toBuilder();
+            long generateDocumentProtoEndTimeMillis = SystemClock.elapsedRealtime();
+
+            // Rewrite Document Type
+            long rewriteDocumentTypeStartTimeMillis = SystemClock.elapsedRealtime();
+            String prefix = createPrefix(packageName, databaseName);
+            addPrefixToDocument(documentBuilder, prefix);
+            long rewriteDocumentTypeEndTimeMillis = SystemClock.elapsedRealtime();
+
+            PutResultProto putResultProto = mIcingSearchEngineLocked.put(documentBuilder.build());
             addToMap(mNamespaceMapLocked, prefix, documentBuilder.getNamespace());
-            // The existing documents with same URI will be deleted, so there maybe some resources
-            // could be released after optimize().
-            checkForOptimizeLocked(/* force= */ false);
+
+            // Logging stats
+            if (logger != null && pStatsBuilder != null) {
+                pStatsBuilder.getGeneralStatsBuilder().setStatusCode(
+                        statusProtoToAppSearchException(putResultProto.getStatus())
+                                .getResultCode());
+                pStatsBuilder
+                        .setGenerateDocumentProtoLatencyMillis(
+                                (int) (generateDocumentProtoEndTimeMillis
+                                        - generateDocumentProtoStartTimeMillis))
+                        .setRewriteDocumentTypesLatencyMillis(
+                                (int) (rewriteDocumentTypeEndTimeMillis
+                                        - rewriteDocumentTypeStartTimeMillis));
+                AppSearchLoggerHelper.copyNativeStats(putResultProto.getPutDocumentStats(),
+                        pStatsBuilder);
+            }
+
+            checkSuccess(putResultProto.getStatus());
         } finally {
             mReadWriteLock.writeLock().unlock();
+
+            if (logger != null && pStatsBuilder != null) {
+                long totalEndTimeMillis = SystemClock.elapsedRealtime();
+                pStatsBuilder.getGeneralStatsBuilder().setTotalLatencyMillis(
+                        (int) (totalEndTimeMillis - totalStartTimeMillis));
+                logger.logStats(pStatsBuilder.build());
+            }
         }
-        checkSuccess(putResultProto.getStatus());
     }
 
     /**
-     * Retrieves a document from the AppSearch index by URI.
+     * Retrieves a document from the AppSearch index by namespace and document ID.
      *
      * <p>This method belongs to query group.
      *
-     * @param packageName  The package that owns this document.
-     * @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.
+     * @param packageName       The package that owns this document.
+     * @param databaseName      The databaseName this document resides in.
+     * @param namespace         The namespace this document resides in.
+     * @param id                The ID of the document to get.
+     * @param typePropertyPaths A map of schema type to a list of property paths to return in the
+     *                          result.
      * @return The Document contents
      * @throws AppSearchException on IcingSearchEngine error.
      */
     @NonNull
-    public GenericDocument getDocument(@NonNull String packageName, @NonNull String databaseName,
+    public GenericDocument getDocument(
+            @NonNull String packageName, @NonNull String databaseName,
             @NonNull String namespace,
-            @NonNull String uri) throws AppSearchException {
-        GetResultProto getResultProto;
+            @NonNull String id,
+            @NonNull Map<String, List<String>> typePropertyPaths) throws AppSearchException {
         mReadWriteLock.readLock().lock();
         try {
-            getResultProto = mIcingSearchEngineLocked.get(
-                    createPrefix(packageName, databaseName) + namespace, uri,
-                    GetResultSpecProto.getDefaultInstance());
+            throwIfClosedLocked();
+            String prefix = createPrefix(packageName, databaseName);
+            List<TypePropertyMask> nonPrefixedPropertyMasks =
+                    TypePropertyPathToProtoConverter.toTypePropertyMaskList(typePropertyPaths);
+            List<TypePropertyMask> prefixedPropertyMasks =
+                    new ArrayList<>(nonPrefixedPropertyMasks.size());
+            for (int i = 0; i < nonPrefixedPropertyMasks.size(); ++i) {
+                TypePropertyMask typePropertyMask = nonPrefixedPropertyMasks.get(i);
+                String nonPrefixedType = typePropertyMask.getSchemaType();
+                String prefixedType = nonPrefixedType.equals(
+                        GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)
+                        ? nonPrefixedType : prefix + nonPrefixedType;
+                prefixedPropertyMasks.add(
+                        typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
+            }
+            GetResultSpecProto getResultSpec =
+                    GetResultSpecProto.newBuilder().addAllTypePropertyMasks(prefixedPropertyMasks
+                    ).build();
+
+            GetResultProto getResultProto = mIcingSearchEngineLocked.get(
+                    prefix + namespace, id,
+                    getResultSpec);
+            checkSuccess(getResultProto.getStatus());
+
+            // The schema type map cannot be null at this point. It could only be null if no
+            // schema had ever been set for that prefix. Given we have retrieved a document from
+            // the index, we know a schema had to have been set.
+            Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMapLocked.get(prefix);
+            DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
+            removePrefixesFromDocument(documentBuilder);
+            return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build(),
+                    prefix, schemaTypeMap);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
-        checkSuccess(getResultProto.getStatus());
-
-        DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
-        removePrefixesFromDocument(documentBuilder);
-        return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build());
     }
 
     /**
@@ -454,7 +701,20 @@
             @NonNull SearchSpec searchSpec) throws AppSearchException {
         mReadWriteLock.readLock().lock();
         try {
+            throwIfClosedLocked();
+
+            List<String> filterPackageNames = searchSpec.getFilterPackageNames();
+            if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) {
+                // Client wanted to query over some packages that weren't its own. This isn't
+                // allowed through local query so we can return early with no results.
+                return new SearchResultPage(Bundle.EMPTY);
+            }
+
+            String prefix = createPrefix(packageName, databaseName);
+            Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
+
             return doQueryLocked(Collections.singleton(createPrefix(packageName, databaseName)),
+                    allowedPrefixedSchemas,
                     queryExpression,
                     searchSpec);
         } finally {
@@ -468,8 +728,10 @@
      *
      * <p>This method belongs to query group.
      *
-     * @param queryExpression Query String to search.
-     * @param searchSpec      Spec for setting filters, raw query etc.
+     * @param queryExpression   Query String to search.
+     * @param searchSpec        Spec for setting filters, raw query etc.
+     * @param callerPackageName Package name of the caller, should belong to the {@code callerUid}.
+     * @param callerUid         UID of the client making the globalQuery call.
      * @return The results of performing this search. It may contain an empty list of results if
      * no documents matched the query.
      * @throws AppSearchException on IcingSearchEngine error.
@@ -477,21 +739,93 @@
     @NonNull
     public SearchResultPage globalQuery(
             @NonNull String queryExpression,
-            @NonNull SearchSpec searchSpec) throws AppSearchException {
-        // TODO(b/169883602): Check if the platform is querying us at a higher level. At this
-        //  point, we should add all platform-surfaceable schemas assuming the querier has been
-        //  verified.
+            @NonNull SearchSpec searchSpec,
+            @NonNull String callerPackageName,
+            int callerUid) throws AppSearchException {
         mReadWriteLock.readLock().lock();
         try {
-            // We use the mNamespaceMap.keySet here because it's the smaller set of valid prefixes
-            // that could exist.
-            Set<String> prefixes = mNamespaceMapLocked.keySet();
+            throwIfClosedLocked();
 
-            // Filter out any VisibilityStore documents which are AppSearch-internal only.
-            prefixes.remove(createPrefix(VisibilityStore.PACKAGE_NAME,
-                    VisibilityStore.DATABASE_NAME));
+            Set<String> packageFilters = new ArraySet<>(searchSpec.getFilterPackageNames());
+            Set<String> prefixFilters = new ArraySet<>();
+            Set<String> allPrefixes = mNamespaceMapLocked.keySet();
+            if (packageFilters.isEmpty()) {
+                // Client didn't restrict their search over packages. Try to query over all
+                // packages/prefixes
+                prefixFilters = allPrefixes;
+            } else {
+                // Client did restrict their search over packages. Only include the prefixes that
+                // belong to the specified packages.
+                for (String prefix : allPrefixes) {
+                    String packageName = getPackageName(prefix);
+                    if (packageFilters.contains(packageName)) {
+                        prefixFilters.add(prefix);
+                    }
+                }
+            }
 
-            return doQueryLocked(prefixes, queryExpression, searchSpec);
+            // Find which schemas the client is allowed to query over.
+            Set<String> allowedPrefixedSchemas = new ArraySet<>();
+            List<String> schemaFilters = searchSpec.getFilterSchemas();
+            for (String prefix : prefixFilters) {
+                String packageName = getPackageName(prefix);
+
+                if (!schemaFilters.isEmpty()) {
+                    for (String schema : schemaFilters) {
+                        // Client specified some schemas to search over, check each one
+                        String prefixedSchema = prefix + schema;
+                        if (packageName.equals(callerPackageName)
+                                || mVisibilityStoreLocked.isSchemaSearchableByCaller(prefix,
+                                prefixedSchema, callerUid)) {
+                            allowedPrefixedSchemas.add(prefixedSchema);
+                        }
+                    }
+                } else {
+                    // Client didn't specify certain schemas to search over, check all schemas
+                    Set<String> prefixedSchemas = mSchemaMapLocked.get(prefix).keySet();
+                    if (prefixedSchemas != null) {
+                        for (String prefixedSchema : prefixedSchemas) {
+                            if (packageName.equals(callerPackageName)
+                                    || mVisibilityStoreLocked.isSchemaSearchableByCaller(prefix,
+                                    prefixedSchema, callerUid)) {
+                                allowedPrefixedSchemas.add(prefixedSchema);
+                            }
+                        }
+                    }
+                }
+            }
+
+            return doQueryLocked(prefixFilters, allowedPrefixedSchemas, queryExpression,
+                    searchSpec);
+        } finally {
+            mReadWriteLock.readLock().unlock();
+        }
+    }
+
+    /**
+     * Returns a mapping of package names to all the databases owned by that package.
+     *
+     * <p>This method is inefficient to call repeatedly.
+     */
+    @NonNull
+    public Map<String, Set<String>> getPackageToDatabases() {
+        mReadWriteLock.readLock().lock();
+        try {
+            Map<String, Set<String>> packageToDatabases = new ArrayMap<>();
+            for (String prefix : mSchemaMapLocked.keySet()) {
+                String packageName = getPackageName(prefix);
+
+                Set<String> databases = packageToDatabases.get(packageName);
+                if (databases == null) {
+                    databases = new ArraySet<>();
+                    packageToDatabases.put(packageName, databases);
+                }
+
+                String databaseName = getDatabaseName(prefix);
+                databases.add(databaseName);
+            }
+
+            return packageToDatabases;
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -499,35 +833,45 @@
 
     @GuardedBy("mReadWriteLock")
     private SearchResultPage doQueryLocked(
-            @NonNull Set<String> prefixes, @NonNull String queryExpression,
+            @NonNull Set<String> prefixes,
+            @NonNull Set<String> allowedPrefixedSchemas,
+            @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec)
             throws AppSearchException {
         SearchSpecProto.Builder searchSpecBuilder =
                 SearchSpecToProtoConverter.toSearchSpecProto(searchSpec).toBuilder().setQuery(
                         queryExpression);
-        // rewriteSearchSpecForPrefixesLocked will return false if none of the prefixes that the
-        // client is trying to search on exist, so we can return an empty SearchResult and skip
+        // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
+        // over given their search filters, so we can return an empty SearchResult and skip
         // sending request to Icing.
-        if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder, prefixes)) {
+        if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder, prefixes,
+                allowedPrefixedSchemas)) {
             return new SearchResultPage(Bundle.EMPTY);
         }
 
         ResultSpecProto.Builder resultSpecBuilder =
                 SearchSpecToProtoConverter.toResultSpecProto(searchSpec).toBuilder();
 
-        // rewriteResultSpecForPrefixesLocked will return false if none of the prefixes that the
-        // client is trying to search on exist, so we can return an empty SearchResult and skip
-        // sending request to Icing.
-        if (!rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes)) {
-            return new SearchResultPage(Bundle.EMPTY);
+        int groupingType = searchSpec.getResultGroupingTypeFlags();
+        if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0
+                && (groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
+            addPerPackagePerNamespaceResultGroupingsLocked(resultSpecBuilder, prefixes,
+                    searchSpec.getResultGroupingLimit());
+        } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0) {
+            addPerPackageResultGroupingsLocked(resultSpecBuilder, prefixes,
+                    searchSpec.getResultGroupingLimit());
+        } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
+            addPerNamespaceResultGroupingsLocked(resultSpecBuilder, prefixes,
+                    searchSpec.getResultGroupingLimit());
         }
+        rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes, allowedPrefixedSchemas);
 
         ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec);
         SearchResultProto searchResultProto = mIcingSearchEngineLocked.search(
                 searchSpecBuilder.build(), scoringSpec, resultSpecBuilder.build());
         checkSuccess(searchResultProto.getStatus());
 
-        return rewriteSearchResultProto(searchResultProto);
+        return rewriteSearchResultProto(searchResultProto, mSchemaMapLocked);
     }
 
     /**
@@ -545,10 +889,12 @@
             throws AppSearchException {
         mReadWriteLock.readLock().lock();
         try {
+            throwIfClosedLocked();
+
             SearchResultProto searchResultProto = mIcingSearchEngineLocked.getNextPage(
                     nextPageToken);
             checkSuccess(searchResultProto.getStatus());
-            return rewriteSearchResultProto(searchResultProto);
+            return rewriteSearchResultProto(searchResultProto, mSchemaMapLocked);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -565,36 +911,70 @@
     public void invalidateNextPageToken(long nextPageToken) {
         mReadWriteLock.readLock().lock();
         try {
+            throwIfClosedLocked();
+
             mIcingSearchEngineLocked.invalidateNextPageToken(nextPageToken);
         } finally {
             mReadWriteLock.readLock().unlock();
         }
     }
 
+    /** Reports a usage of the given document at the given timestamp. */
+    public void reportUsage(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull String namespace,
+            @NonNull String documentId,
+            long usageTimestampMillis,
+            boolean systemUsage) throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            throwIfClosedLocked();
+
+            String prefixedNamespace = createPrefix(packageName, databaseName) + namespace;
+            UsageReport.UsageType usageType = systemUsage
+                    ? UsageReport.UsageType.USAGE_TYPE2 : UsageReport.UsageType.USAGE_TYPE1;
+            UsageReport report = UsageReport.newBuilder()
+                    .setDocumentNamespace(prefixedNamespace)
+                    .setDocumentUri(documentId)
+                    .setUsageTimestampMs(usageTimestampMillis)
+                    .setUsageType(usageType)
+                    .build();
+
+            ReportUsageResultProto result = mIcingSearchEngineLocked.reportUsage(report);
+            checkSuccess(result.getStatus());
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
+    }
+
     /**
-     * Removes the given document by URI.
+     * Removes the given document by id.
      *
      * <p>This method belongs to mutate group.
      *
      * @param packageName  The package name that owns the document.
      * @param databaseName The databaseName the document is in.
      * @param namespace    Namespace of the document to remove.
-     * @param uri          URI of the document to remove.
+     * @param id           ID of the document to remove.
      * @throws AppSearchException on IcingSearchEngine error.
      */
-    public void remove(@NonNull String packageName, @NonNull String databaseName,
+    public void remove(
+            @NonNull String packageName, @NonNull String databaseName,
             @NonNull String namespace,
-            @NonNull String uri) throws AppSearchException {
-        String prefixedNamespace = createPrefix(packageName, databaseName) + namespace;
-        DeleteResultProto deleteResultProto;
+            @NonNull String id) throws AppSearchException {
         mReadWriteLock.writeLock().lock();
         try {
-            deleteResultProto = mIcingSearchEngineLocked.delete(prefixedNamespace, uri);
-            checkForOptimizeLocked(/* force= */false);
+            throwIfClosedLocked();
+
+            String prefixedNamespace = createPrefix(packageName, databaseName) + namespace;
+            DeleteResultProto deleteResultProto = mIcingSearchEngineLocked.delete(prefixedNamespace,
+                    id);
+
+            checkSuccess(deleteResultProto.getStatus());
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
-        checkSuccess(deleteResultProto.getStatus());
     }
 
     /**
@@ -612,48 +992,232 @@
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec)
             throws AppSearchException {
-        SearchSpecProto searchSpecProto =
-                SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
-        SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder()
-                .setQuery(queryExpression);
-        DeleteByQueryResultProto deleteResultProto;
         mReadWriteLock.writeLock().lock();
         try {
-            // Only rewrite SearchSpec for non empty prefixes.
-            // rewriteSearchSpecForPrefixesLocked will return false for empty prefixes, we
-            // should skip sending request to Icing and return in here.
-            if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder,
-                    Collections.singleton(createPrefix(packageName, databaseName)))) {
+            throwIfClosedLocked();
+
+            List<String> filterPackageNames = searchSpec.getFilterPackageNames();
+            if (!filterPackageNames.isEmpty() && !filterPackageNames.contains(packageName)) {
+                // We're only removing documents within the parameter `packageName`. If we're not
+                // restricting our remove-query to this package name, then there's nothing for us to
+                // remove.
                 return;
             }
-            deleteResultProto = mIcingSearchEngineLocked.deleteByQuery(
+
+            SearchSpecProto searchSpecProto =
+                    SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
+            SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder()
+                    .setQuery(queryExpression);
+
+            String prefix = createPrefix(packageName, databaseName);
+            Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
+
+            // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
+            // over given their search filters, so we can return early and skip sending request
+            // to Icing.
+            if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder,
+                    Collections.singleton(prefix), allowedPrefixedSchemas)) {
+                return;
+            }
+            DeleteByQueryResultProto deleteResultProto = mIcingSearchEngineLocked.deleteByQuery(
                     searchSpecBuilder.build());
-            checkForOptimizeLocked(/* force= */true);
+
+            // It seems that the caller wants to get success if the data matching the query is
+            // not in the DB because it was not there or was successfully deleted.
+            checkCodeOneOf(deleteResultProto.getStatus(),
+                    StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
         } finally {
             mReadWriteLock.writeLock().unlock();
         }
-        // It seems that the caller wants to get success if the data matching the query is not in
-        // the DB because it was not there or was successfully deleted.
-        checkCodeOneOf(deleteResultProto.getStatus(),
-                StatusProto.Code.OK, StatusProto.Code.NOT_FOUND);
+    }
+
+    /** Estimates the storage usage info for a specific package. */
+    @NonNull
+    public StorageInfo getStorageInfoForPackage(@NonNull String packageName)
+            throws AppSearchException {
+        mReadWriteLock.readLock().lock();
+        try {
+            throwIfClosedLocked();
+
+            Map<String, Set<String>> packageToDatabases = getPackageToDatabases();
+            Set<String> databases = packageToDatabases.get(packageName);
+            if (databases == null) {
+                // Package doesn't exist, no storage info to report
+                return new StorageInfo.Builder().build();
+            }
+
+            // Accumulate all the namespaces we're interested in.
+            Set<String> wantedPrefixedNamespaces = new ArraySet<>();
+            for (String database : databases) {
+                Set<String> prefixedNamespaces = mNamespaceMapLocked.get(createPrefix(packageName,
+                        database));
+                if (prefixedNamespaces != null) {
+                    wantedPrefixedNamespaces.addAll(prefixedNamespaces);
+                }
+            }
+            if (wantedPrefixedNamespaces.isEmpty()) {
+                return new StorageInfo.Builder().build();
+            }
+
+            return getStorageInfoForNamespacesLocked(wantedPrefixedNamespaces);
+        } finally {
+            mReadWriteLock.readLock().unlock();
+        }
+    }
+
+    /** Estimates the storage usage info for a specific database in a package. */
+    @NonNull
+    public StorageInfo getStorageInfoForDatabase(@NonNull String packageName,
+            @NonNull String databaseName)
+            throws AppSearchException {
+        mReadWriteLock.readLock().lock();
+        try {
+            throwIfClosedLocked();
+
+            Map<String, Set<String>> packageToDatabases = getPackageToDatabases();
+            Set<String> databases = packageToDatabases.get(packageName);
+            if (databases == null) {
+                // Package doesn't exist, no storage info to report
+                return new StorageInfo.Builder().build();
+            }
+            if (!databases.contains(databaseName)) {
+                // Database doesn't exist, no storage info to report
+                return new StorageInfo.Builder().build();
+            }
+
+            Set<String> wantedPrefixedNamespaces =
+                    mNamespaceMapLocked.get(createPrefix(packageName, databaseName));
+            if (wantedPrefixedNamespaces == null || wantedPrefixedNamespaces.isEmpty()) {
+                return new StorageInfo.Builder().build();
+            }
+
+            return getStorageInfoForNamespacesLocked(wantedPrefixedNamespaces);
+        } finally {
+            mReadWriteLock.readLock().unlock();
+        }
+    }
+
+    @GuardedBy("mReadWriteLock")
+    @NonNull
+    private StorageInfo getStorageInfoForNamespacesLocked(@NonNull Set<String> prefixedNamespaces)
+            throws AppSearchException {
+        StorageInfoResultProto storageInfoResult = mIcingSearchEngineLocked.getStorageInfo();
+        checkSuccess(storageInfoResult.getStatus());
+        if (!storageInfoResult.hasStorageInfo()
+                || !storageInfoResult.getStorageInfo().hasDocumentStorageInfo()) {
+            return new StorageInfo.Builder().build();
+        }
+        long totalStorageSize = storageInfoResult.getStorageInfo().getTotalStorageSize();
+
+        DocumentStorageInfoProto documentStorageInfo =
+                storageInfoResult.getStorageInfo().getDocumentStorageInfo();
+        int totalDocuments =
+                documentStorageInfo.getNumAliveDocuments()
+                        + documentStorageInfo.getNumExpiredDocuments();
+
+        if (totalStorageSize == 0 || totalDocuments == 0) {
+            // Maybe we can exit early and also avoid a divide by 0 error.
+            return new StorageInfo.Builder().build();
+        }
+
+        // Accumulate stats across the package's namespaces.
+        int aliveDocuments = 0;
+        int expiredDocuments = 0;
+        int aliveNamespaces = 0;
+        List<NamespaceStorageInfoProto> namespaceStorageInfos =
+                documentStorageInfo.getNamespaceStorageInfoList();
+        for (int i = 0; i < namespaceStorageInfos.size(); i++) {
+            NamespaceStorageInfoProto namespaceStorageInfo = namespaceStorageInfos.get(i);
+            // The namespace from icing lib is already the prefixed format
+            if (prefixedNamespaces.contains(namespaceStorageInfo.getNamespace())) {
+                if (namespaceStorageInfo.getNumAliveDocuments() > 0) {
+                    aliveNamespaces++;
+                    aliveDocuments += namespaceStorageInfo.getNumAliveDocuments();
+                }
+                expiredDocuments += namespaceStorageInfo.getNumExpiredDocuments();
+            }
+        }
+        int namespaceDocuments = aliveDocuments + expiredDocuments;
+
+        // Since we don't have the exact size of all the documents, we do an estimation. Note
+        // that while the total storage takes into account schema, index, etc. in addition to
+        // documents, we'll only calculate the percentage based on number of documents a
+        // client has.
+        return new StorageInfo.Builder()
+                .setSizeBytes((long) (namespaceDocuments * 1.0 / totalDocuments * totalStorageSize))
+                .setAliveDocumentsCount(aliveDocuments)
+                .setAliveNamespacesCount(aliveNamespaces)
+                .build();
     }
 
     /**
      * Persists all update/delete requests to the disk.
      *
-     * <p>If the app crashes after a call to PersistToDisk(), Icing would be able to fully recover
-     * all data written up to this point without a costly recovery process.
+     * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#FULL}, Icing
+     * would be able to fully recover all data written up to this point without a costly recovery
+     * process.
      *
-     * <p>If the app crashes before a call to PersistToDisk(), Icing would trigger a costly
-     * recovery process in next initialization. After that, Icing would still be able to recover
-     * all written data.
+     * <p>If the app crashes after a call to PersistToDisk with {@link PersistType.Code#LITE}, Icing
+     * would trigger a costly recovery process in next initialization. After that, Icing would still
+     * be able to recover all written data - excepting Usage data. Usage data is only guaranteed
+     * to be safe after a call to PersistToDisk with {@link PersistType.Code#FULL}
+     *
+     * <p>If the app crashes after an update/delete request has been made, but before any call to
+     * PersistToDisk, then all data in Icing will be lost.
+     *
+     * @param persistType the amount of data to persist. {@link PersistType.Code#LITE} will only
+     *                   persist the minimal amount of data to ensure all data can be recovered.
+     *                   {@link PersistType.Code#FULL} will persist all data necessary to
+     *                    prevent data loss without needing data recovery.
+     *
+     * @throws AppSearchException on any error that AppSearch persist data to disk.
      */
-    public void persistToDisk() throws AppSearchException {
-        PersistToDiskResultProto persistToDiskResultProto =
-                mIcingSearchEngineLocked.persistToDisk();
-        checkSuccess(persistToDiskResultProto.getStatus());
+    public void persistToDisk(@NonNull PersistType.Code persistType) throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            throwIfClosedLocked();
+
+            PersistToDiskResultProto persistToDiskResultProto =
+                    mIcingSearchEngineLocked.persistToDisk(persistType);
+            checkSuccess(persistToDiskResultProto.getStatus());
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
     }
 
+    /**
+     * Remove all {@link AppSearchSchema}s and {@link GenericDocument}s under the given package.
+     *
+     * @param packageName The name of package to be removed.
+     * @throws AppSearchException if we cannot remove the data.
+     */
+    public void clearPackageData(@NonNull String packageName) throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            throwIfClosedLocked();
+
+            SchemaProto existingSchema = getSchemaProtoLocked();
+            SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
+
+            String prefix = createPackagePrefix(packageName);
+            for (int i = 0; i < existingSchema.getTypesCount(); i++) {
+                if (!existingSchema.getTypes(i).getSchemaType().startsWith(prefix)) {
+                    newSchemaBuilder.addTypes(existingSchema.getTypes(i));
+                }
+            }
+
+            // Apply schema, set force override to true to remove all schemas and documents under
+            // that package.
+            SetSchemaResultProto setSchemaResultProto =
+                    mIcingSearchEngineLocked.setSchema(newSchemaBuilder.build(),
+                            /*ignoreErrorsAndDeleteDocuments=*/ true);
+
+            // Determine whether it succeeded.
+            checkSuccess(setSchemaResultProto.getStatus());
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
+    }
 
     /**
      * Clears documents and schema across all packages and databaseNames.
@@ -665,21 +1229,16 @@
      *
      * @throws AppSearchException on IcingSearchEngine error.
      */
-    private void reset() throws AppSearchException {
-        ResetResultProto resetResultProto;
-        mReadWriteLock.writeLock().lock();
-        try {
-            resetResultProto = mIcingSearchEngineLocked.reset();
-            mOptimizeIntervalCountLocked = 0;
-            mSchemaMapLocked.clear();
-            mNamespaceMapLocked.clear();
+    @GuardedBy("mReadWriteLock")
+    private void resetLocked() throws AppSearchException {
+        ResetResultProto resetResultProto = mIcingSearchEngineLocked.reset();
+        mOptimizeIntervalCountLocked = 0;
+        mSchemaMapLocked.clear();
+        mNamespaceMapLocked.clear();
 
-            // Must be called after everything else since VisibilityStore may repopulate
-            // IcingSearchEngine with an initial schema.
-            mVisibilityStoreLocked.handleReset();
-        } finally {
-            mReadWriteLock.writeLock().unlock();
-        }
+        // Must be called after everything else since VisibilityStore may repopulate
+        // IcingSearchEngine with an initial schema.
+        mVisibilityStoreLocked.handleReset();
         checkSuccess(resetResultProto.getStatus());
     }
 
@@ -689,8 +1248,8 @@
         // Any prefixed types that used to exist in the schema, but are deleted in the new one.
         final Set<String> mDeletedPrefixedTypes = new ArraySet<>();
 
-        // Prefixed types that were part of the new schema.
-        final Set<String> mRewrittenPrefixedTypes = new ArraySet<>();
+        // Map of prefixed schema types to SchemaTypeConfigProtos that were part of the new schema.
+        final Map<String, SchemaTypeConfigProto> mRewrittenPrefixedTypes = new ArrayMap<>();
     }
 
     /**
@@ -738,7 +1297,7 @@
 
         // newTypesToProto is modified below, so we need a copy first
         RewrittenSchemaResults rewrittenSchemaResults = new RewrittenSchemaResults();
-        rewrittenSchemaResults.mRewrittenPrefixedTypes.addAll(newTypesToProto.keySet());
+        rewrittenSchemaResults.mRewrittenPrefixedTypes.putAll(newTypesToProto);
 
         // Combine the existing schema (which may have types from other prefixes) with this
         // prefix's new schema. Modifies the existingSchemaBuilder.
@@ -764,107 +1323,24 @@
     }
 
     /**
-     * Prepends {@code prefix} to all types and namespaces mentioned anywhere in
-     * {@code documentBuilder}.
+     * Rewrites the search spec filters with {@code prefixes}.
      *
-     * @param documentBuilder The document to mutate
-     * @param prefix          The prefix to add
-     */
-    @VisibleForTesting
-    static void addPrefixToDocument(
-            @NonNull DocumentProto.Builder documentBuilder,
-            @NonNull String prefix) {
-        // Rewrite the type name to include/remove the prefix.
-        String newSchema = prefix + documentBuilder.getSchema();
-        documentBuilder.setSchema(newSchema);
-
-        // Rewrite the namespace to include/remove the prefix.
-        documentBuilder.setNamespace(prefix + 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();
-                    addPrefixToDocument(derivedDocumentBuilder, prefix);
-                    propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
-                }
-                documentBuilder.setProperties(propertyIdx, propertyBuilder);
-            }
-        }
-    }
-
-    /**
-     * Removes any prefixes from types and namespaces mentioned anywhere in
-     * {@code documentBuilder}.
-     *
-     * @param documentBuilder The document to mutate
-     * @return Prefix name that was removed from the document.
-     * @throws AppSearchException if there are unexpected database prefixing errors.
-     */
-    @NonNull
-    @VisibleForTesting
-    static String removePrefixesFromDocument(@NonNull DocumentProto.Builder documentBuilder)
-            throws AppSearchException {
-        // Rewrite the type name and namespace to remove the prefix.
-        String schemaPrefix = getPrefix(documentBuilder.getSchema());
-        String namespacePrefix = getPrefix(documentBuilder.getNamespace());
-
-        if (!schemaPrefix.equals(namespacePrefix)) {
-            throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, "Found unexpected"
-                    + " multiple prefix names in document: " + schemaPrefix + ", "
-                    + namespacePrefix);
-        }
-
-        documentBuilder.setSchema(removePrefix(documentBuilder.getSchema()));
-        documentBuilder.setNamespace(removePrefix(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();
-                    String nestedPrefix = removePrefixesFromDocument(derivedDocumentBuilder);
-                    if (!nestedPrefix.equals(schemaPrefix)) {
-                        throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
-                                "Found unexpected multiple prefix names in document: "
-                                        + schemaPrefix + ", " + nestedPrefix);
-                    }
-                    propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
-                }
-                documentBuilder.setProperties(propertyIdx, propertyBuilder);
-            }
-        }
-
-        return schemaPrefix;
-    }
-
-    /**
-     * Rewrites the schemaTypeFilters and namespacesFilters that exist with {@code prefixes}.
-     *
-     * <p>If the searchSpec has empty filter lists, all prefixes filters will be added.
      * <p>This method should be only called in query methods and get the READ lock to keep thread
      * safety.
      *
-     * @return false if none of the requested prefixes exist.
+     * @param searchSpecBuilder      Client-provided SearchSpec
+     * @param prefixes               Prefixes that we should prepend to all our filters
+     * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over. This
+     *                               supersedes the schema filters that may exist on the {@code
+     *                               searchSpecBuilder}.
+     * @return false if none there would be nothing to search over.
      */
     @VisibleForTesting
     @GuardedBy("mReadWriteLock")
     boolean rewriteSearchSpecForPrefixesLocked(
             @NonNull SearchSpecProto.Builder searchSpecBuilder,
-            @NonNull Set<String> prefixes) {
+            @NonNull Set<String> prefixes,
+            @NonNull Set<String> allowedPrefixedSchemas) {
         // Create a copy since retainAll() modifies the original set.
         Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
         existingPrefixes.retainAll(prefixes);
@@ -874,29 +1350,28 @@
             return false;
         }
 
-        // Cache the schema type filters and namespaces before clearing everything.
-        List<String> schemaTypeFilters = searchSpecBuilder.getSchemaTypeFiltersList();
-        searchSpecBuilder.clearSchemaTypeFilters();
+        if (allowedPrefixedSchemas.isEmpty()) {
+            // Not allowed to search over any schemas, empty query.
+            return false;
+        }
 
+        // Clear the schema type filters since we'll be rewriting them with the
+        // allowedPrefixedSchemas.
+        searchSpecBuilder.clearSchemaTypeFilters();
+        searchSpecBuilder.addAllSchemaTypeFilters(allowedPrefixedSchemas);
+
+        // Cache the namespaces before clearing everything.
         List<String> namespaceFilters = searchSpecBuilder.getNamespaceFiltersList();
         searchSpecBuilder.clearNamespaceFilters();
 
-        // Rewrite filters to include a prefix.
+        // Rewrite non-schema filters to include a prefix.
         for (String prefix : existingPrefixes) {
-            Set<String> existingSchemaTypes = mSchemaMapLocked.get(prefix);
-            if (schemaTypeFilters.isEmpty()) {
-                // Include all schema types
-                searchSpecBuilder.addAllSchemaTypeFilters(existingSchemaTypes);
-            } else {
-                // Add the prefix to the given schema types
-                for (int i = 0; i < schemaTypeFilters.size(); i++) {
-                    String prefixedType = prefix + schemaTypeFilters.get(i);
-                    if (existingSchemaTypes.contains(prefixedType)) {
-                        searchSpecBuilder.addSchemaTypeFilters(prefixedType);
-                    }
-                }
-            }
+            // TODO(b/169883602): We currently grab every namespace for every prefix. We can
+            //  optimize this by checking if a prefix has any allowedSchemaTypes. If not, that
+            //  means we don't want to query over anything in that prefix anyways, so we don't
+            //  need to grab its namespaces either.
 
+            // Empty namespaces on the search spec means to query over all namespaces.
             Set<String> existingNamespaces = mNamespaceMapLocked.get(prefix);
             if (namespaceFilters.isEmpty()) {
                 // Include all namespaces
@@ -916,31 +1391,55 @@
     }
 
     /**
+     * Returns the set of allowed prefixed schemas that the {@code prefix} can query while taking
+     * into account the {@code searchSpec} schema filters.
+     *
+     * <p>This only checks intersection of schema filters on the search spec with those that the
+     * prefix owns itself. This does not check global query permissions.
+     */
+    @GuardedBy("mReadWriteLock")
+    private Set<String> getAllowedPrefixSchemasLocked(@NonNull String prefix,
+            @NonNull SearchSpec searchSpec) {
+        Set<String> allowedPrefixedSchemas = new ArraySet<>();
+
+        // Add all the schema filters the client specified.
+        List<String> schemaFilters = searchSpec.getFilterSchemas();
+        for (int i = 0; i < schemaFilters.size(); i++) {
+            allowedPrefixedSchemas.add(prefix + schemaFilters.get(i));
+        }
+
+        if (allowedPrefixedSchemas.isEmpty()) {
+            // If the client didn't specify any schema filters, search over all of their schemas
+            Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMapLocked.get(prefix);
+            if (prefixedSchemaMap != null) {
+                allowedPrefixedSchemas.addAll(prefixedSchemaMap.keySet());
+            }
+        }
+        return allowedPrefixedSchemas;
+    }
+
+    /**
      * Rewrites the typePropertyMasks that exist in {@code prefixes}.
      *
      * <p>This method should be only called in query methods and get the READ lock to keep thread
      * safety.
      *
-     * @return false if none of the requested prefixes exist.
+     * @param resultSpecBuilder      ResultSpecs as specified by client
+     * @param prefixes               Prefixes that we should prepend to all our filters
+     * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over.
      */
     @VisibleForTesting
     @GuardedBy("mReadWriteLock")
-    boolean rewriteResultSpecForPrefixesLocked(
+    void rewriteResultSpecForPrefixesLocked(
             @NonNull ResultSpecProto.Builder resultSpecBuilder,
-            @NonNull Set<String> prefixes) {
+            @NonNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas) {
         // Create a copy since retainAll() modifies the original set.
         Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
         existingPrefixes.retainAll(prefixes);
 
-        if (existingPrefixes.isEmpty()) {
-            // None of the prefixes exist, empty query.
-            return false;
-        }
-
         List<TypePropertyMask> prefixedTypePropertyMasks = new ArrayList<>();
         // Rewrite filters to include a database prefix.
         for (String prefix : existingPrefixes) {
-            Set<String> existingSchemaTypes = mSchemaMapLocked.get(prefix);
             // Qualify the given schema types
             for (TypePropertyMask typePropertyMask :
                     resultSpecBuilder.getTypePropertyMasksList()) {
@@ -948,7 +1447,7 @@
                 boolean isWildcard =
                         unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD);
                 String prefixedType = isWildcard ? unprefixedType : prefix + unprefixedType;
-                if (isWildcard || existingSchemaTypes.contains(prefixedType)) {
+                if (isWildcard || allowedPrefixedSchemas.contains(prefixedType)) {
                     prefixedTypePropertyMasks.add(
                             typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
                 }
@@ -956,7 +1455,148 @@
         }
         resultSpecBuilder.clearTypePropertyMasks().addAllTypePropertyMasks(
                 prefixedTypePropertyMasks);
-        return true;
+    }
+
+    /**
+     * Adds result groupings for each namespace in each package being queried for.
+     *
+     * <p>This method should be only called in query methods and get the READ lock to keep thread
+     * safety.
+     *
+     * @param resultSpecBuilder ResultSpecs as specified by client
+     * @param prefixes          Prefixes that we should prepend to all our filters
+     * @param maxNumResults     The maximum number of results for each grouping to support.
+     */
+    @GuardedBy("mReadWriteLock")
+    private void addPerPackagePerNamespaceResultGroupingsLocked(
+            @NonNull ResultSpecProto.Builder resultSpecBuilder,
+            @NonNull Set<String> prefixes, int maxNumResults) {
+        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
+        existingPrefixes.retainAll(prefixes);
+
+        // Create a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
+        // same as the list of namespaces. If one package has multiple databases, each with the same
+        // namespace, then those should be grouped together.
+        Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>();
+        for (String prefix : existingPrefixes) {
+            Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
+            String packageName = getPackageName(prefix);
+            // Create a new prefix without the database name. This will allow us to group namespaces
+            // that have the same name and package but a different database name together.
+            String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/"");
+            for (String prefixedNamespace : prefixedNamespaces) {
+                String namespace;
+                try {
+                    namespace = removePrefix(prefixedNamespace);
+                } catch (AppSearchException e) {
+                    // This should never happen. Skip this namespace if it does.
+                    Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
+                    continue;
+                }
+                String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace;
+                List<String> namespaceList =
+                        packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace);
+                if (namespaceList == null) {
+                    namespaceList = new ArrayList<>();
+                    packageAndNamespaceToNamespaces.put(emptyDatabasePrefixedNamespace,
+                            namespaceList);
+                }
+                namespaceList.add(prefixedNamespace);
+            }
+        }
+
+        for (List<String> namespaces : packageAndNamespaceToNamespaces.values()) {
+            resultSpecBuilder.addResultGroupings(
+                    ResultSpecProto.ResultGrouping.newBuilder()
+                            .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
+        }
+    }
+
+    /**
+     * Adds result groupings for each package being queried for.
+     *
+     * <p>This method should be only called in query methods and get the READ lock to keep thread
+     * safety.
+     *
+     * @param resultSpecBuilder ResultSpecs as specified by client
+     * @param prefixes          Prefixes that we should prepend to all our filters
+     * @param maxNumResults     The maximum number of results for each grouping to support.
+     */
+    @GuardedBy("mReadWriteLock")
+    private void addPerPackageResultGroupingsLocked(
+            @NonNull ResultSpecProto.Builder resultSpecBuilder,
+            @NonNull Set<String> prefixes, int maxNumResults) {
+        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
+        existingPrefixes.retainAll(prefixes);
+
+        // Build up a map of package to namespaces.
+        Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>();
+        for (String prefix : existingPrefixes) {
+            Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
+            String packageName = getPackageName(prefix);
+            List<String> packageNamespaceList = packageToNamespacesMap.get(packageName);
+            if (packageNamespaceList == null) {
+                packageNamespaceList = new ArrayList<>();
+                packageToNamespacesMap.put(packageName, packageNamespaceList);
+            }
+            packageNamespaceList.addAll(prefixedNamespaces);
+        }
+
+        for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) {
+            resultSpecBuilder.addResultGroupings(
+                    ResultSpecProto.ResultGrouping.newBuilder()
+                            .addAllNamespaces(prefixedNamespaces).setMaxResults(maxNumResults));
+        }
+    }
+
+    /**
+     * Adds result groupings for each namespace being queried for.
+     *
+     * <p>This method should be only called in query methods and get the READ lock to keep thread
+     * safety.
+     *
+     * @param resultSpecBuilder ResultSpecs as specified by client
+     * @param prefixes          Prefixes that we should prepend to all our filters
+     * @param maxNumResults     The maximum number of results for each grouping to support.
+     */
+    @GuardedBy("mReadWriteLock")
+    private void addPerNamespaceResultGroupingsLocked(
+            @NonNull ResultSpecProto.Builder resultSpecBuilder,
+            @NonNull Set<String> prefixes, int maxNumResults) {
+        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
+        existingPrefixes.retainAll(prefixes);
+
+        // Create a map of namespace to prefixedNamespaces. This is NOT necessarily the
+        // same as the list of namespaces. If a namespace exists under different packages and/or
+        // different databases, they should still be grouped together.
+        Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
+        for (String prefix : existingPrefixes) {
+            Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
+            for (String prefixedNamespace : prefixedNamespaces) {
+                String namespace;
+                try {
+                    namespace = removePrefix(prefixedNamespace);
+                } catch (AppSearchException e) {
+                    // This should never happen. Skip this namespace if it does.
+                    Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
+                    continue;
+                }
+                List<String> groupedPrefixedNamespaces =
+                        namespaceToPrefixedNamespaces.get(namespace);
+                if (groupedPrefixedNamespaces == null) {
+                    groupedPrefixedNamespaces = new ArrayList<>();
+                    namespaceToPrefixedNamespaces.put(namespace,
+                            groupedPrefixedNamespaces);
+                }
+                groupedPrefixedNamespaces.add(prefixedNamespace);
+            }
+        }
+
+        for (List<String> namespaces : namespaceToPrefixedNamespaces.values()) {
+            resultSpecBuilder.addResultGroupings(
+                    ResultSpecProto.ResultGrouping.newBuilder()
+                            .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
+        }
     }
 
     @VisibleForTesting
@@ -969,83 +1609,14 @@
         return schemaProto.getSchema();
     }
 
-    /**
-     * Returns true if the {@code packageName} and {@code databaseName} has the
-     * {@code schemaType}
-     */
-    @GuardedBy("mReadWriteLock")
-    boolean hasSchemaTypeLocked(@NonNull String packageName, @NonNull String databaseName,
-            @NonNull String schemaType) {
-        Preconditions.checkNotNull(packageName);
-        Preconditions.checkNotNull(databaseName);
-        Preconditions.checkNotNull(schemaType);
-
-        String prefix = createPrefix(packageName, databaseName);
-        Set<String> schemaTypes = mSchemaMapLocked.get(prefix);
-        if (schemaTypes == null) {
-            return false;
-        }
-
-        return schemaTypes.contains(prefix + schemaType);
-    }
-
     /** Returns a set of all prefixes AppSearchImpl knows about. */
+    // TODO(b/180058203): Remove this method once platform has switched away from using this method.
     @GuardedBy("mReadWriteLock")
     @NonNull
     Set<String> getPrefixesLocked() {
         return mSchemaMapLocked.keySet();
     }
 
-    @NonNull
-    static String createPrefix(@NonNull String packageName, @NonNull String databaseName) {
-        return packageName + PACKAGE_DELIMITER + databaseName + DATABASE_DELIMITER;
-    }
-
-    /**
-     * Returns the package name that's contained within the {@code prefix}.
-     *
-     * @param prefix Prefix string that contains the package name inside of it. The package name
-     *               must be in the front of the string, and separated from the rest of the
-     *               string by the {@link #PACKAGE_DELIMITER}.
-     * @return Valid package name.
-     */
-    @NonNull
-    private static String getPackageName(@NonNull String prefix) {
-        int delimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
-        if (delimiterIndex == -1) {
-            // This should never happen if we construct our prefixes properly
-            Log.wtf(TAG, "Malformed prefix doesn't contain package name: " + prefix);
-            return "";
-        }
-        return prefix.substring(0, delimiterIndex);
-    }
-
-    @NonNull
-    private static String removePrefix(@NonNull String prefixedString)
-            throws AppSearchException {
-        // The prefix is made up of the package, then the database. So we only need to find the
-        // database cutoff.
-        int delimiterIndex;
-        if ((delimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER)) != -1) {
-            // Add 1 to include the char size of the DATABASE_DELIMITER
-            return prefixedString.substring(delimiterIndex + 1);
-        }
-        throw new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
-                "The prefixed value doesn't contains a valid database name.");
-    }
-
-    @NonNull
-    private static String getPrefix(@NonNull String prefixedString) throws AppSearchException {
-        int databaseDelimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER);
-        if (databaseDelimiterIndex == -1) {
-            throw new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
-                    "The databaseName prefixed value doesn't contain a valid database name.");
-        }
-
-        // Add 1 to include the char size of the DATABASE_DELIMITER
-        return prefixedString.substring(0, databaseDelimiterIndex + 1);
-    }
-
     private static void addToMap(Map<String, Set<String>> map, String prefix,
             String prefixedValue) {
         Set<String> values = map.get(prefix);
@@ -1056,6 +1627,24 @@
         values.add(prefixedValue);
     }
 
+    private static void addToMap(Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix,
+            SchemaTypeConfigProto schemaTypeConfigProto) {
+        Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
+        if (schemaTypeMap == null) {
+            schemaTypeMap = new ArrayMap<>();
+            map.put(prefix, schemaTypeMap);
+        }
+        schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto);
+    }
+
+    private static void removeFromMap(Map<String, Map<String, SchemaTypeConfigProto>> map,
+            String prefix, String schemaType) {
+        Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
+        if (schemaTypeMap != null) {
+            schemaTypeMap.remove(schemaType);
+        }
+    }
+
     /**
      * Checks the given status code and throws an {@link AppSearchException} if code is an error.
      *
@@ -1091,34 +1680,75 @@
     /**
      * 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>This method should be only called after a mutation to local storage backend which
+     * deletes a mass of data and could release lots resources after
+     * {@link IcingSearchEngine#optimize()}.
+     *
+     * <p>This method will trigger {@link IcingSearchEngine#getOptimizeInfo()} to check
+     * resources that could be released for every {@link #CHECK_OPTIMIZE_INTERVAL} mutations.
+     *
      * <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()}.
+     * @param mutationSize The number of how many mutations have been executed for current request.
+     *                     An inside counter will accumulates it. Once the counter reaches
+     *                     {@link #CHECK_OPTIMIZE_INTERVAL},
+     *                     {@link IcingSearchEngine#getOptimizeInfo()} will be triggered and the
+     *                     counter will be reset.
      */
-    @GuardedBy("mReadWriteLock")
-    private void checkForOptimizeLocked(boolean force) throws AppSearchException {
-        ++mOptimizeIntervalCountLocked;
-        if (force || mOptimizeIntervalCountLocked >= CHECK_OPTIMIZE_INTERVAL) {
-            mOptimizeIntervalCountLocked = 0;
+    public void checkForOptimize(int mutationSize) throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            mOptimizeIntervalCountLocked += mutationSize;
+            if (mOptimizeIntervalCountLocked >= CHECK_OPTIMIZE_INTERVAL) {
+                checkForOptimize();
+            }
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * Checks whether {@link IcingSearchEngine#optimize()} should be called to release resources.
+     *
+     * <p>This method will directly trigger {@link IcingSearchEngine#getOptimizeInfo()} to check
+     * resources that could be released.
+     *
+     * <p>{@link IcingSearchEngine#optimize()} should be called only if
+     * {@link GetOptimizeInfoResultProto} shows there is enough resources could be released.
+     */
+    public void checkForOptimize() throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
             GetOptimizeInfoResultProto optimizeInfo = getOptimizeInfoResultLocked();
             checkSuccess(optimizeInfo.getStatus());
+            mOptimizeIntervalCountLocked = 0;
             // 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 = mIcingSearchEngineLocked.optimize();
-                checkSuccess(optimizeResultProto.getStatus());
+                optimize();
             }
-            // 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.
+        } finally {
+            mReadWriteLock.writeLock().unlock();
+        }
+        // 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.
+    }
+
+    /**
+     * Triggers {@link IcingSearchEngine#optimize()} directly.
+     *
+     * <p>This method should be only called as a scheduled task in AppSearch Platform backend.
+     */
+    public void optimize() throws AppSearchException {
+        mReadWriteLock.writeLock().lock();
+        try {
+            OptimizeResultProto optimizeResultProto = mIcingSearchEngineLocked.optimize();
+            checkSuccess(optimizeResultProto.getStatus());
+        } finally {
+            mReadWriteLock.writeLock().unlock();
         }
     }
 
@@ -1126,10 +1756,15 @@
     @NonNull
     @VisibleForTesting
     static SearchResultPage rewriteSearchResultProto(
-            @NonNull SearchResultProto searchResultProto) throws AppSearchException {
+            @NonNull SearchResultProto searchResultProto,
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
+            throws AppSearchException {
         // Parallel array of package names for each document search result.
         List<String> packageNames = new ArrayList<>(searchResultProto.getResultsCount());
 
+        // Parallel array of database names for each document search result.
+        List<String> databaseNames = new ArrayList<>(searchResultProto.getResultsCount());
+
         SearchResultProto.Builder resultsBuilder = searchResultProto.toBuilder();
         for (int i = 0; i < searchResultProto.getResultsCount(); i++) {
             SearchResultProto.ResultProto.Builder resultBuilder =
@@ -1137,10 +1772,12 @@
             DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
             String prefix = removePrefixesFromDocument(documentBuilder);
             packageNames.add(getPackageName(prefix));
+            databaseNames.add(getDatabaseName(prefix));
             resultBuilder.setDocument(documentBuilder);
             resultsBuilder.setResults(i, resultBuilder);
         }
-        return SearchResultToProtoConverter.toSearchResultPage(resultsBuilder, packageNames);
+        return SearchResultToProtoConverter.toSearchResultPage(resultsBuilder, packageNames,
+                databaseNames, schemaMap);
     }
 
     @GuardedBy("mReadWriteLock")
@@ -1150,6 +1787,7 @@
     }
 
     @GuardedBy("mReadWriteLock")
+    @NonNull
     @VisibleForTesting
     VisibilityStore getVisibilityStoreLocked() {
         return mVisibilityStoreLocked;
@@ -1164,27 +1802,8 @@
      * @return AppSearchException with the parallel error code.
      */
     private static 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());
-        }
+        return new AppSearchException(
+                ResultCodeToProtoConverter.toResultCode(statusProto.getCode()),
+                statusProto.getMessage());
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
new file mode 100644
index 0000000..cae0a7d
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
@@ -0,0 +1,54 @@
+/*
+ * 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 androidx.appsearch.localstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.stats.CallStats;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.PutDocumentStats;
+
+/**
+ * An interface for implementing client-defined logging AppSearch operations stats.
+ *
+ * <p>Any implementation needs to provide general information on how to log all the stats types.
+ * (e.g. {@link CallStats})
+ *
+ * <p>All implementations of this interface must be thread safe.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppSearchLogger {
+    /**
+     * Logs {@link CallStats}
+     */
+    void logStats(@NonNull CallStats stats) throws AppSearchException;
+
+    /**
+     * Logs {@link PutDocumentStats}
+     */
+    void logStats(@NonNull PutDocumentStats stats) throws AppSearchException;
+
+    /**
+     * Logs {@link InitializeStats}
+     */
+    void logStats(@NonNull InitializeStats stats) throws AppSearchException;
+
+    // TODO(b/173532925) Add remaining logStats once we add all the stats.
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
new file mode 100644
index 0000000..21fee38
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.localstorage.stats.InitializeStats;
+import androidx.appsearch.localstorage.stats.PutDocumentStats;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.InitializeStatsProto;
+import com.google.android.icing.proto.PutDocumentStatsProto;
+
+/**
+ * Class contains helper functions for logging.
+ *
+ * <p>E.g. we need to have helper functions to copy numbers from IcingLib to stats classes.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class AppSearchLoggerHelper {
+    private AppSearchLoggerHelper() {
+    }
+
+    /**
+     * Copies native PutDocument stats to builder.
+     *
+     * @param fromNativeStats stats copied from
+     * @param toStatsBuilder  stats copied to
+     */
+    static void copyNativeStats(@NonNull PutDocumentStatsProto fromNativeStats,
+            @NonNull PutDocumentStats.Builder toStatsBuilder) {
+        Preconditions.checkNotNull(fromNativeStats);
+        Preconditions.checkNotNull(toStatsBuilder);
+        toStatsBuilder
+                .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
+                .setNativeDocumentStoreLatencyMillis(
+                        fromNativeStats.getDocumentStoreLatencyMs())
+                .setNativeIndexLatencyMillis(fromNativeStats.getIndexLatencyMs())
+                .setNativeIndexMergeLatencyMillis(fromNativeStats.getIndexMergeLatencyMs())
+                .setNativeDocumentSizeBytes(fromNativeStats.getDocumentSize())
+                .setNativeNumTokensIndexed(
+                        fromNativeStats.getTokenizationStats().getNumTokensIndexed())
+                .setNativeExceededMaxNumTokens(
+                        fromNativeStats.getTokenizationStats().getExceededMaxTokenNum());
+    }
+
+    /**
+     * Copies native Initialize stats to builder.
+     *
+     * @param fromNativeStats stats copied from
+     * @param toStatsBuilder  stats copied to
+     */
+    static void copyNativeStats(@NonNull InitializeStatsProto fromNativeStats,
+            @NonNull InitializeStats.Builder toStatsBuilder) {
+        Preconditions.checkNotNull(fromNativeStats);
+        Preconditions.checkNotNull(toStatsBuilder);
+        toStatsBuilder
+                .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
+                .setDocumentStoreRecoveryCause(
+                        fromNativeStats.getDocumentStoreRecoveryCause().getNumber())
+                .setIndexRestorationCause(
+                        fromNativeStats.getIndexRestorationCause().getNumber())
+                .setSchemaStoreRecoveryCause(
+                        fromNativeStats.getSchemaStoreRecoveryCause().getNumber())
+                .setDocumentStoreRecoveryLatencyMillis(
+                        fromNativeStats.getDocumentStoreRecoveryLatencyMs())
+                .setIndexRestorationLatencyMillis(
+                        fromNativeStats.getIndexRestorationLatencyMs())
+                .setSchemaStoreRecoveryLatencyMillis(
+                        fromNativeStats.getSchemaStoreRecoveryLatencyMs())
+                .setDocumentStoreDataStatus(
+                        fromNativeStats.getDocumentStoreDataStatus().getNumber())
+                .setDocumentCount(fromNativeStats.getNumDocuments())
+                .setSchemaTypeCount(fromNativeStats.getNumSchemaTypes());
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
new file mode 100644
index 0000000..976e95d
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2021 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.localstorage;
+
+import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_SCHEMA;
+import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.PersistType;
+import com.google.android.icing.protobuf.CodedInputStream;
+import com.google.android.icing.protobuf.CodedOutputStream;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The helper class for {@link AppSearchSchema} migration.
+ *
+ * <p>It will query and migrate {@link GenericDocument} in given type to a new version.
+ */
+class AppSearchMigrationHelper implements Closeable {
+    private final AppSearchImpl mAppSearchImpl;
+    private final String mPackageName;
+    private final String mDatabaseName;
+    private final File mFile;
+    private final Set<String> mDestinationTypes;
+    private boolean mAreDocumentsMigrated = false;
+
+    AppSearchMigrationHelper(@NonNull AppSearchImpl appSearchImpl,
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull Set<AppSearchSchema> newSchemas) throws IOException {
+        mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
+        mPackageName = Preconditions.checkNotNull(packageName);
+        mDatabaseName = Preconditions.checkNotNull(databaseName);
+        Preconditions.checkNotNull(newSchemas);
+        mFile = File.createTempFile(/*prefix=*/"appsearch", /*suffix=*/null);
+        mDestinationTypes = new ArraySet<>(newSchemas.size());
+        for (AppSearchSchema newSchema : newSchemas) {
+            mDestinationTypes.add(newSchema.getSchemaType());
+        }
+    }
+
+    /**
+     * Queries all documents that need to be migrated to new version, and transform documents to
+     * new version by passing them to the provided Transformer.
+     *
+     * <p>This method will be invoked on the background worker thread.
+     *
+     * @param schemaType     The schema that need be updated and migrated {@link GenericDocument}
+     *                       under this type.
+     * @param migrator       The map of active {@link Migrator}s that will upgrade or downgrade a
+     *                       {@link GenericDocument} to new version.
+     * @param currentVersion The current version of the document's schema.
+     * @param finalVersion   The final version that documents need to be migrated to.
+     *
+     * @throws IOException        on i/o problem
+     * @throws AppSearchException on AppSearch problem
+     */
+    @WorkerThread
+    public void queryAndTransform(@NonNull Map<String, Migrator> migrators, int currentVersion,
+            int finalVersion)
+            throws IOException, AppSearchException {
+        Preconditions.checkState(mFile.exists(), "Internal temp file does not exist.");
+        try (FileOutputStream outputStream = new FileOutputStream(mFile, /*append=*/ true)) {
+            // TODO(b/151178558) change the output stream so that we can use it in platform
+            CodedOutputStream codedOutputStream = CodedOutputStream.newInstance(outputStream);
+            SearchResultPage searchResultPage = mAppSearchImpl.query(mPackageName, mDatabaseName,
+                    /*queryExpression=*/"",
+                    new SearchSpec.Builder()
+                            .addFilterSchemas(migrators.keySet())
+                            .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                            .build());
+            while (!searchResultPage.getResults().isEmpty()) {
+                for (int i = 0; i < searchResultPage.getResults().size(); i++) {
+                    GenericDocument document =
+                            searchResultPage.getResults().get(i).getGenericDocument();
+                    Migrator migrator = migrators.get(document.getSchemaType());
+                    GenericDocument newDocument;
+                    if (currentVersion < finalVersion) {
+                        newDocument = migrator.onUpgrade(currentVersion, finalVersion, document);
+                    } else {
+                        // if current version = final version. we will return empty active
+                        // migrators at SchemaMigrationUtils.getActivityMigrators and won't reach
+                        // here.
+                        newDocument = migrator.onDowngrade(currentVersion, finalVersion, document);
+                    }
+                    if (!mDestinationTypes.contains(newDocument.getSchemaType())) {
+                        // we exit before the new schema has been set to AppSearch. So no
+                        // observable changes will be applied to stored schemas and documents.
+                        // And the temp file will be deleted at close(), which will be triggered at
+                        // the end of try-with-resources when using AppSearchMigrationHelper.
+                        throw new AppSearchException(RESULT_INVALID_SCHEMA,
+                                "Receive a migrated document with schema type: "
+                                        + newDocument.getSchemaType()
+                                        + ". But the schema types doesn't exist in the request");
+                    }
+                    Bundle bundle = newDocument.getBundle();
+                    Parcel parcel = Parcel.obtain();
+                    parcel.writeBundle(bundle);
+                    byte[] serializedMessage = parcel.marshall();
+                    parcel.recycle();
+                    codedOutputStream.writeByteArrayNoTag(serializedMessage);
+                }
+                codedOutputStream.flush();
+                searchResultPage = mAppSearchImpl.getNextPage(searchResultPage.getNextPageToken());
+                outputStream.flush();
+            }
+        }
+        mAreDocumentsMigrated = true;
+    }
+
+    /**
+     * Reads {@link GenericDocument} from the temperate file and saves them to AppSearch.
+     *
+     * <p> This method should be only called once.
+     *
+     * @return  the {@link SetSchemaResponse} for this
+     *          {@link androidx.appsearch.app.AppSearchSession#setSchema} call.
+     *
+     * @throws IOException        on i/o problem
+     * @throws AppSearchException on AppSearch problem
+     */
+    @NonNull
+    @WorkerThread
+    public SetSchemaResponse readAndPutDocuments(@NonNull SetSchemaResponse.Builder responseBuilder)
+            throws IOException, AppSearchException {
+        Preconditions.checkState(mFile.exists(), "Internal temp file does not exist.");
+        if (!mAreDocumentsMigrated) {
+            return responseBuilder.build();
+        }
+        try (InputStream inputStream = new FileInputStream(mFile)) {
+            CodedInputStream codedInputStream = CodedInputStream.newInstance(inputStream);
+            while (!codedInputStream.isAtEnd()) {
+                GenericDocument document = readDocumentFromInputStream(codedInputStream);
+                try {
+                    mAppSearchImpl.putDocument(mPackageName, mDatabaseName, document,
+                            /*logger=*/ null);
+                } catch (Throwable t) {
+                    responseBuilder.addMigrationFailure(
+                            new SetSchemaResponse.MigrationFailure(
+                                    document.getNamespace(),
+                                    document.getId(),
+                                    document.getSchemaType(),
+                                    throwableToFailedResult(t)));
+                }
+            }
+            mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
+        }
+        return responseBuilder.build();
+    }
+
+    /**
+     * Reads {@link GenericDocument} from given {@link CodedInputStream}.
+     *
+     * @param codedInputStream The codedInputStream to read from
+     *
+     * @throws IOException        on File operation error.
+     */
+    @NonNull
+    private static GenericDocument readDocumentFromInputStream(
+            @NonNull CodedInputStream codedInputStream) throws IOException {
+        byte[] serializedMessage = codedInputStream.readByteArray();
+
+        Parcel parcel = Parcel.obtain();
+        parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
+        parcel.setDataPosition(0);
+        Bundle bundle = parcel.readBundle();
+        parcel.recycle();
+
+        return new GenericDocument(bundle);
+    }
+
+    @Override
+    public void close() {
+        mFile.delete();
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
index ac8fec2..fbb7995 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
@@ -16,17 +16,24 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.localstorage;
 
+import android.content.Context;
+
 import androidx.annotation.NonNull;
-import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.app.ReportSystemUsageRequest;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.util.FutureUtil;
 import androidx.core.util.Preconditions;
 
-import java.util.concurrent.ExecutorService;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
 
 /**
- * An implementation of {@link AppSearchSession} which stores data locally
+ * An implementation of {@link GlobalSearchSession} which stores data locally
  * in the app's storage space using a bundled version of the search native library.
  *
  * <p>Queries are executed multi-threaded, but a single thread is used for mutate requests (put,
@@ -34,27 +41,56 @@
  */
 class GlobalSearchSessionImpl implements GlobalSearchSession {
     private final AppSearchImpl mAppSearchImpl;
-    private final ExecutorService mExecutorService;
+    private final Executor mExecutor;
+    private final Context mContext;
+
+    private boolean mIsClosed = false;
 
     GlobalSearchSessionImpl(
             @NonNull AppSearchImpl appSearchImpl,
-            @NonNull ExecutorService executorService) {
+            @NonNull Executor executor,
+            @NonNull Context context) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
-        mExecutorService = Preconditions.checkNotNull(executorService);
+        mExecutor = Preconditions.checkNotNull(executor);
+        mContext = Preconditions.checkNotNull(context);
     }
 
     @NonNull
     @Override
-    public SearchResults query(
+    public SearchResults search(
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
+        Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
         return new SearchResultsImpl(
                 mAppSearchImpl,
-                mExecutorService,
-                /*packageName=*/ null,
+                mExecutor,
+                mContext.getPackageName(),
                 /*databaseName=*/ null,
                 queryExpression,
                 searchSpec);
     }
+
+    /**
+     * Reporting system usage is not supported in the local backend, so this method does nothing
+     * and always completes the return value with an
+     * {@link androidx.appsearch.exceptions.AppSearchException} having a result code of
+     * {@link AppSearchResult#RESULT_SECURITY_ERROR}.
+     */
+    @NonNull
+    @Override
+    public ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+        Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
+        return FutureUtil.execute(mExecutor, () -> {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_SECURITY_ERROR,
+                    mContext.getPackageName() + " does not have access to report system usage");
+        });
+    }
+
+    @Override
+    public void close() {
+        mIsClosed = true;
+    }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 8f6cfde..37a20b0 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -17,11 +17,13 @@
 package androidx.appsearch.localstorage;
 
 import android.content.Context;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
+import androidx.appsearch.annotation.Document;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -31,6 +33,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.io.File;
+import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -40,20 +43,14 @@
  *
  * <p>The search native library is an on-device searching library that allows apps to define
  * {@link androidx.appsearch.app.AppSearchSchema}s, save and query a variety of
- * {@link androidx.appsearch.annotation.AppSearchDocument}s. The library needs to be initialized
+ * {@link Document}s. The library needs to be initialized
  * before using, which will create a folder to save data in the app's storage space.
  *
  * <p>Queries are executed multi-threaded, but a single thread is used for mutate requests (put,
  * delete, etc..).
  */
 public class LocalStorage {
-    /**
-     * The default empty database name.
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @VisibleForTesting
-    public static final String DEFAULT_DATABASE_NAME = "";
+    private static final String TAG = "AppSearchLocalStorage";
 
     private static final String ICING_LIB_ROOT_DIR = "appsearch";
 
@@ -61,56 +58,78 @@
     public static final class SearchContext {
         final Context mContext;
         final String mDatabaseName;
+        final Executor mExecutor;
 
-        SearchContext(@NonNull Context context, @NonNull String databaseName) {
+        SearchContext(@NonNull Context context, @NonNull String databaseName,
+                @NonNull Executor executor) {
             mContext = Preconditions.checkNotNull(context);
             mDatabaseName = Preconditions.checkNotNull(databaseName);
+            mExecutor = Preconditions.checkNotNull(executor);
         }
 
         /**
          * Returns the name of the database to create or open.
-         *
-         * <p>Databases with different names are fully separate with distinct types, namespaces,
-         * and data.
          */
         @NonNull
         public String getDatabaseName() {
             return mDatabaseName;
         }
 
+        /**
+         * Returns the worker executor associated with {@link AppSearchSession}.
+         *
+         * <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
+         * be returned. You should never cast the executor to
+         * {@link java.util.concurrent.ExecutorService} and call
+         * {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
+         * since {@link Executor#execute} won't return anything, we will hang forever waiting for
+         * the execution.
+         */
+        @NonNull
+        public Executor getWorkerExecutor() {
+            return mExecutor;
+        }
+
         /** Builder for {@link SearchContext} objects. */
         public static final class Builder {
             private final Context mContext;
-            private String mDatabaseName = DEFAULT_DATABASE_NAME;
+            private final String mDatabaseName;
+            private Executor mExecutor;
             private boolean mBuilt = false;
 
-            public Builder(@NonNull Context context) {
-                mContext = Preconditions.checkNotNull(context);
-            }
-
             /**
-             * Sets the name of the database associated with {@link AppSearchSession}.
+             * Creates a {@link SearchContext.Builder} instance.
              *
              * <p>{@link AppSearchSession} will create or open a database under the given name.
              *
-             * <p>Databases with different names are fully separate with distinct types, namespaces,
-             * and data.
+             * <p>Databases with different names are fully separate with distinct schema types,
+             * namespaces, and documents.
              *
-             * <p>Database name cannot contain {@code '/'}.
-             *
-             * <p>If not specified, defaults to the empty string.
+             * <p>The database name cannot contain {@code '/'}.
              *
              * @param databaseName The name of the database.
              * @throws IllegalArgumentException if the databaseName contains {@code '/'}.
              */
-            @NonNull
-            public Builder setDatabaseName(@NonNull String databaseName) {
-                Preconditions.checkState(!mBuilt, "Builder has already been used");
+            public Builder(@NonNull Context context, @NonNull String databaseName) {
+                mContext = Preconditions.checkNotNull(context);
                 Preconditions.checkNotNull(databaseName);
                 if (databaseName.contains("/")) {
                     throw new IllegalArgumentException("Database name cannot contain '/'");
                 }
                 mDatabaseName = databaseName;
+            }
+
+            /**
+             * Sets the worker executor associated with {@link AppSearchSession}.
+             *
+             * <p>If an executor is not provided, the AppSearch default executor will be used.
+             *
+             * @param executor the worker executor used to run heavy background tasks.
+             */
+            @NonNull
+            public Builder setWorkerExecutor(@NonNull Executor executor) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                mExecutor = Preconditions.checkNotNull(executor);
                 return this;
             }
 
@@ -118,67 +137,92 @@
             @NonNull
             public SearchContext build() {
                 Preconditions.checkState(!mBuilt, "Builder has already been used");
+                if (mExecutor == null) {
+                    mExecutor = EXECUTOR;
+                }
                 mBuilt = true;
-                return new SearchContext(mContext, mDatabaseName);
+                return new SearchContext(mContext, mDatabaseName, mExecutor);
             }
         }
     }
 
     /**
      * Contains information relevant to creating a global search session.
+     * @hide
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public static final class GlobalSearchContext {
         final Context mContext;
+        final Executor mExecutor;
 
-        GlobalSearchContext(@NonNull Context context) {
+        GlobalSearchContext(@NonNull Context context, @NonNull Executor executor) {
             mContext = Preconditions.checkNotNull(context);
+            mExecutor = Preconditions.checkNotNull(executor);
+        }
+
+        /**
+         * Returns the worker executor associated with {@link GlobalSearchSession}.
+         *
+         * <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
+         * be returned. You should never cast the executor to
+         * {@link java.util.concurrent.ExecutorService} and call
+         * {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
+         * since {@link Executor#execute} won't return anything, we will hang forever waiting for
+         * the execution.
+         */
+        @NonNull
+        public Executor getWorkerExecutor() {
+            return mExecutor;
         }
 
         /** Builder for {@link GlobalSearchContext} objects. */
         public static final class Builder {
             private final Context mContext;
+            private Executor mExecutor;
             private boolean mBuilt = false;
 
             public Builder(@NonNull Context context) {
                 mContext = Preconditions.checkNotNull(context);
             }
 
+            /**
+             * Sets the worker executor associated with {@link GlobalSearchSession}.
+             *
+             * <p>If an executor is not provided, the AppSearch default executor will be used.
+             *
+             * @param executor the worker executor used to run heavy background tasks.
+             */
+            @NonNull
+            public Builder setWorkerExecutor(@NonNull Executor executor) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                Preconditions.checkNotNull(executor);
+                mExecutor = executor;
+                return this;
+            }
+
             /** Builds a {@link GlobalSearchContext} instance. */
             @NonNull
             public GlobalSearchContext build() {
                 Preconditions.checkState(!mBuilt, "Builder has already been used");
+                if (mExecutor == null) {
+                    mExecutor = EXECUTOR;
+                }
+
                 mBuilt = true;
-                return new GlobalSearchContext(mContext);
+                return new GlobalSearchContext(mContext, mExecutor);
             }
         }
     }
 
-    // Never call Executor.shutdownNow(), it will cancel the futures it's returned. And since
-    // execute() won't return anything, we will hang forever waiting for the execution.
     // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
     // mutate requests will need to gain write lock and query requests need to gain read lock.
-    private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
+    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+
     private static volatile LocalStorage sInstance;
 
     private final AppSearchImpl mAppSearchImpl;
 
     /**
-     * Opens a new {@link AppSearchSession} on this storage.
-     *
-     * <p>This process requires a native search library. If it's not created, the initialization
-     * process will create one.
-     *
-     * @param context The {@link SearchContext} contains all information to create a new
-     *                {@link AppSearchSession}
-     */
-    @NonNull
-    public static ListenableFuture<AppSearchSession> createSearchSession(
-            @NonNull SearchContext context) {
-        Preconditions.checkNotNull(context);
-        return createSearchSession(context, EXECUTOR_SERVICE);
-    }
-
-    /**
      * Opens a new {@link AppSearchSession} on this storage with executor.
      *
      * <p>This process requires a native search library. If it's not created, the initialization
@@ -186,19 +230,14 @@
      *
      * @param context  The {@link SearchContext} contains all information to create a new
      *                 {@link AppSearchSession}
-     * @param executor The executor of where tasks will execute.
-     * @hide
      */
     @NonNull
-    @VisibleForTesting
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public static ListenableFuture<AppSearchSession> createSearchSession(
-            @NonNull SearchContext context, @NonNull ExecutorService executor) {
+            @NonNull SearchContext context) {
         Preconditions.checkNotNull(context);
-        Preconditions.checkNotNull(executor);
-        return FutureUtil.execute(executor, () -> {
-            LocalStorage instance = getOrCreateInstance(context.mContext);
-            return instance.doCreateSearchSession(context, executor);
+        return FutureUtil.execute(context.mExecutor, () -> {
+            LocalStorage instance = getOrCreateInstance(context.mContext, context.mExecutor);
+            return instance.doCreateSearchSession(context);
         });
     }
 
@@ -208,17 +247,16 @@
      * <p>This process requires a native search library. If it's not created, the initialization
      * process will create one.
      *
-     * @param context The {@link GlobalSearchContext} contains all information to create a new
-     *                {@link GlobalSearchSession}
      * @hide
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @NonNull
     public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
             @NonNull GlobalSearchContext context) {
         Preconditions.checkNotNull(context);
-        return FutureUtil.execute(EXECUTOR_SERVICE, () -> {
-            LocalStorage instance = getOrCreateInstance(context.mContext);
-            return instance.doCreateGlobalSearchSession(EXECUTOR_SERVICE);
+        return FutureUtil.execute(context.mExecutor, () -> {
+            LocalStorage instance = getOrCreateInstance(context.mContext, context.mExecutor);
+            return instance.doCreateGlobalSearchSession(context);
         });
     }
 
@@ -231,12 +269,13 @@
     @NonNull
     @WorkerThread
     @VisibleForTesting
-    static LocalStorage getOrCreateInstance(@NonNull Context context) throws AppSearchException {
+    static LocalStorage getOrCreateInstance(@NonNull Context context, @NonNull Executor executor)
+            throws AppSearchException {
         Preconditions.checkNotNull(context);
         if (sInstance == null) {
             synchronized (LocalStorage.class) {
                 if (sInstance == null) {
-                    sInstance = new LocalStorage(context);
+                    sInstance = new LocalStorage(context, executor);
                 }
             }
         }
@@ -244,21 +283,34 @@
     }
 
     @WorkerThread
-    private LocalStorage(@NonNull Context context) throws AppSearchException {
+    private LocalStorage(@NonNull Context context, @NonNull Executor executor)
+            throws AppSearchException {
         Preconditions.checkNotNull(context);
         File icingDir = new File(context.getFilesDir(), ICING_LIB_ROOT_DIR);
-        mAppSearchImpl = AppSearchImpl.create(icingDir);
+
+        // There is no global querier for a local storage instance.
+        mAppSearchImpl = AppSearchImpl.create(icingDir, context, VisibilityStore.NO_OP_USER_ID,
+                /*globalQuerierPackage=*/ "",
+                /*logger=*/ null);
+
+        executor.execute(() -> {
+            try {
+                mAppSearchImpl.checkForOptimize();
+            } catch (AppSearchException e) {
+                Log.w(TAG, "Error occurred when check for optimize", e);
+            }
+        });
     }
 
     @NonNull
-    private AppSearchSession doCreateSearchSession(@NonNull SearchContext context,
-            @NonNull ExecutorService executor) {
-        return new SearchSessionImpl(mAppSearchImpl, executor,
-                context.mContext.getPackageName(), context.mDatabaseName);
+    private AppSearchSession doCreateSearchSession(@NonNull SearchContext context) {
+        return new SearchSessionImpl(mAppSearchImpl, context.mExecutor,
+                context.mContext.getPackageName(), context.mDatabaseName, /*logger=*/ null);
     }
 
     @NonNull
-    private GlobalSearchSession doCreateGlobalSearchSession(@NonNull ExecutorService executor) {
-        return new GlobalSearchSessionImpl(mAppSearchImpl, executor);
+    private GlobalSearchSession doCreateGlobalSearchSession(
+            @NonNull GlobalSearchContext context) {
+        return new GlobalSearchSessionImpl(mAppSearchImpl, context.mExecutor, context.mContext);
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
index e11f1f0..a838613 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchResultsImpl.java
@@ -16,6 +16,8 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.localstorage;
 
+import android.os.Process;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appsearch.app.AppSearchResult;
@@ -30,12 +32,12 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.List;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executor;
 
 class SearchResultsImpl implements SearchResults {
     private final AppSearchImpl mAppSearchImpl;
 
-    private final ExecutorService mExecutorService;
+    private final Executor mExecutor;
 
     // The package name to search over. If null, this will search over all package names.
     @Nullable
@@ -57,13 +59,13 @@
 
     SearchResultsImpl(
             @NonNull AppSearchImpl appSearchImpl,
-            @NonNull ExecutorService executorService,
+            @NonNull Executor executor,
             @Nullable String packageName,
             @Nullable String databaseName,
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
-        mExecutorService = Preconditions.checkNotNull(executorService);
+        mExecutor = Preconditions.checkNotNull(executor);
         mPackageName = packageName;
         mDatabaseName = databaseName;
         mQueryExpression = Preconditions.checkNotNull(queryExpression);
@@ -74,21 +76,18 @@
     @NonNull
     public ListenableFuture<List<SearchResult>> getNextPage() {
         Preconditions.checkState(!mIsClosed, "SearchResults has already been closed");
-        return FutureUtil.execute(mExecutorService, () -> {
+        return FutureUtil.execute(mExecutor, () -> {
             SearchResultPage searchResultPage;
             if (mIsFirstLoad) {
                 mIsFirstLoad = false;
-                if (mDatabaseName == null && mPackageName == null) {
-                    // Global query, there's no one package-database combination to check.
-                    searchResultPage = mAppSearchImpl.globalQuery(mQueryExpression, mSearchSpec);
-                } else if (mPackageName == null) {
+                if (mPackageName == null) {
                     throw new AppSearchException(
                             AppSearchResult.RESULT_INVALID_ARGUMENT,
                             "Invalid null package name for query");
                 } else if (mDatabaseName == null) {
-                    throw new AppSearchException(
-                            AppSearchResult.RESULT_INVALID_ARGUMENT,
-                            "Invalid null database name for query");
+                    // Global queries aren't restricted to a single database
+                    searchResultPage = mAppSearchImpl.globalQuery(mQueryExpression, mSearchSpec,
+                            mPackageName, Process.myUid());
                 } else {
                     // Normal local query, pass in specified database.
                     searchResultPage = mAppSearchImpl.query(
@@ -108,7 +107,7 @@
         // Checking the future result is not needed here since this is a cleanup step which is not
         // critical to the correct functioning of the system; also, the return value is void.
         if (!mIsClosed) {
-            FutureUtil.execute(mExecutorService, () -> {
+            FutureUtil.execute(mExecutor, () -> {
                 mAppSearchImpl.invalidateNextPageToken(mNextPageToken);
                 mIsClosed = true;
                 return null;
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index 973b724..0b458aa 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -18,28 +18,41 @@
 
 import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
 
+import android.util.Log;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appsearch.app.AppSearchBatchResult;
-import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.app.GetByUriRequest;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.Migrator;
+import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.PutDocumentsRequest;
-import androidx.appsearch.app.RemoveByUriRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.util.FutureUtil;
+import androidx.appsearch.util.SchemaMigrationUtil;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
+import com.google.android.icing.proto.PersistType;
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executor;
 
 /**
  * An implementation of {@link AppSearchSession} which stores data locally
@@ -49,92 +62,187 @@
  * delete, etc..).
  */
 class SearchSessionImpl implements AppSearchSession {
+    private static final String TAG = "AppSearchSessionImpl";
     private final AppSearchImpl mAppSearchImpl;
-    private final ExecutorService mExecutorService;
+    private final Executor mExecutor;
     private final String mPackageName;
     private final String mDatabaseName;
-    private boolean mIsMutated = false;
-    private boolean mIsClosed = false;
+    private volatile boolean mIsMutated = false;
+    private volatile boolean mIsClosed = false;
+    @Nullable private final AppSearchLogger mLogger;
 
     SearchSessionImpl(
             @NonNull AppSearchImpl appSearchImpl,
-            @NonNull ExecutorService executorService,
+            @NonNull Executor executor,
             @NonNull String packageName,
-            @NonNull String databaseName) {
+            @NonNull String databaseName,
+            @Nullable AppSearchLogger logger) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
-        mExecutorService = Preconditions.checkNotNull(executorService);
+        mExecutor = Preconditions.checkNotNull(executor);
         mPackageName = packageName;
         mDatabaseName = Preconditions.checkNotNull(databaseName);
+        mLogger = logger;
     }
 
     @Override
     @NonNull
-    public ListenableFuture<Void> setSchema(@NonNull SetSchemaRequest request) {
+    // TODO(b/151178558) return the batch result for migration documents.
+    public ListenableFuture<SetSchemaResponse> setSchema(
+            @NonNull SetSchemaRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
-        return execute(() -> {
-            mAppSearchImpl.setSchema(
+
+        ListenableFuture<SetSchemaResponse> future = execute(() -> {
+            // Convert the inner set into a List since Binder can't handle Set.
+            Map<String, Set<PackageIdentifier>> schemasPackageAccessible =
+                    request.getSchemasVisibleToPackagesInternal();
+            Map<String, List<PackageIdentifier>> copySchemasPackageAccessible = new ArrayMap<>();
+            for (Map.Entry<String, Set<PackageIdentifier>> entry :
+                    schemasPackageAccessible.entrySet()) {
+                copySchemasPackageAccessible.put(entry.getKey(),
+                        new ArrayList<>(entry.getValue()));
+            }
+
+            Map<String, Migrator> migrators = request.getMigrators();
+            // No need to trigger migration if user never set migrator.
+            if (migrators.size() == 0) {
+                return setSchemaNoMigrations(request, copySchemasPackageAccessible);
+            }
+
+            // Migration process
+            // 1. Validate and retrieve all active migrators.
+            GetSchemaResponse getSchemaResponse =
+                    mAppSearchImpl.getSchema(mPackageName, mDatabaseName);
+            int currentVersion = getSchemaResponse.getVersion();
+            int finalVersion = request.getVersion();
+            Map<String, Migrator> activeMigrators = SchemaMigrationUtil.getActiveMigrators(
+                    getSchemaResponse.getSchemas(), migrators, currentVersion, finalVersion);
+            // No need to trigger migration if no migrator is active.
+            if (activeMigrators.size() == 0) {
+                return setSchemaNoMigrations(request, copySchemasPackageAccessible);
+            }
+
+            // 2. SetSchema with forceOverride=false, to retrieve the list of incompatible/deleted
+            // types.
+            SetSchemaResponse setSchemaResponse = mAppSearchImpl.setSchema(
                     mPackageName,
                     mDatabaseName,
                     new ArrayList<>(request.getSchemas()),
-                    new ArrayList<>(request.getSchemasNotVisibleToSystemUi()),
-                    request.isForceOverride());
-            mIsMutated = true;
-            return null;
+                    new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
+                    copySchemasPackageAccessible,
+                    /*forceOverride=*/false,
+                    request.getVersion());
+
+
+            // 3. If forceOverride is false, check that all incompatible types will be migrated.
+            // If some aren't we must throw an error, rather than proceeding and deleting those
+            // types.
+            if (!request.isForceOverride()) {
+                SchemaMigrationUtil.checkDeletedAndIncompatibleAfterMigration(setSchemaResponse,
+                        activeMigrators.keySet());
+            }
+
+            try (AppSearchMigrationHelper migrationHelper = new AppSearchMigrationHelper(
+                    mAppSearchImpl, mPackageName, mDatabaseName, request.getSchemas())) {
+                // 4. Trigger migration for all activity migrators.
+                migrationHelper.queryAndTransform(activeMigrators, currentVersion, finalVersion);
+
+                // 5. SetSchema a second time with forceOverride=true if the first attempted failed
+                // due to backward incompatible changes.
+                if (!setSchemaResponse.getIncompatibleTypes().isEmpty()
+                        || !setSchemaResponse.getDeletedTypes().isEmpty()) {
+                    setSchemaResponse = mAppSearchImpl.setSchema(
+                            mPackageName,
+                            mDatabaseName,
+                            new ArrayList<>(request.getSchemas()),
+                            new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
+                            copySchemasPackageAccessible,
+                            /*forceOverride=*/ true,
+                            request.getVersion());
+                }
+                SetSchemaResponse.Builder responseBuilder = setSchemaResponse.toBuilder()
+                        .addMigratedTypes(activeMigrators.keySet());
+                mIsMutated = true;
+
+                // 6. Put all the migrated documents into the index, now that the new schema is set.
+                return migrationHelper.readAndPutDocuments(responseBuilder);
+            }
         });
+
+        // setSchema will sync the schemas in the request to AppSearch, any existing schemas which
+        // is not included in the request will be delete if we force override incompatible schemas.
+        // And all documents of these types will be deleted as well. We should checkForOptimize for
+        // these deletion.
+        checkForOptimize();
+        return future;
     }
 
     @Override
     @NonNull
-    public ListenableFuture<Set<AppSearchSchema>> getSchema() {
+    public ListenableFuture<GetSchemaResponse> getSchema() {
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+        return execute(() -> mAppSearchImpl.getSchema(mPackageName, mDatabaseName));
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Set<String>> getNamespaces() {
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
-            List<AppSearchSchema> schemas = mAppSearchImpl.getSchema(mPackageName, mDatabaseName);
-            return new ArraySet<>(schemas);
+            List<String> namespaces = mAppSearchImpl.getNamespaces(mPackageName, mDatabaseName);
+            return new ArraySet<>(namespaces);
         });
     }
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, Void>> putDocuments(
+    public ListenableFuture<AppSearchBatchResult<String, Void>> put(
             @NonNull PutDocumentsRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
-        return execute(() -> {
+        ListenableFuture<AppSearchBatchResult<String, Void>> future = execute(() -> {
             AppSearchBatchResult.Builder<String, Void> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
-            for (int i = 0; i < request.getDocuments().size(); i++) {
-                GenericDocument document = request.getDocuments().get(i);
+            for (int i = 0; i < request.getGenericDocuments().size(); i++) {
+                GenericDocument document = request.getGenericDocuments().get(i);
                 try {
-                    mAppSearchImpl.putDocument(mPackageName, mDatabaseName, document);
-                    resultBuilder.setSuccess(document.getUri(), /*result=*/ null);
+                    mAppSearchImpl.putDocument(mPackageName, mDatabaseName, document, mLogger);
+                    resultBuilder.setSuccess(document.getId(), /*result=*/ null);
                 } catch (Throwable t) {
-                    resultBuilder.setResult(document.getUri(), throwableToFailedResult(t));
+                    resultBuilder.setResult(document.getId(), throwableToFailedResult(t));
                 }
             }
+            // Now that the batch has been written. Persist the newly written data.
+            mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
             mIsMutated = true;
             return resultBuilder.build();
         });
+
+        // The existing documents with same ID will be deleted, so there may be some resources that
+        // could be released after optimize().
+        checkForOptimize(/*mutateBatchSize=*/ request.getGenericDocuments().size());
+        return future;
     }
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByUri(
-            @NonNull GetByUriRequest request) {
+    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+            @NonNull GetByDocumentIdRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
             AppSearchBatchResult.Builder<String, GenericDocument> resultBuilder =
                     new AppSearchBatchResult.Builder<>();
 
-            for (String uri : request.getUris()) {
+            Map<String, List<String>> typePropertyPaths = request.getProjectionsInternal();
+            for (String id : request.getIds()) {
                 try {
                     GenericDocument document =
                             mAppSearchImpl.getDocument(mPackageName, mDatabaseName,
-                                    request.getNamespace(), uri);
-                    resultBuilder.setSuccess(uri, document);
+                                    request.getNamespace(), id, typePropertyPaths);
+                    resultBuilder.setSuccess(id, document);
                 } catch (Throwable t) {
-                    resultBuilder.setResult(uri, throwableToFailedResult(t));
+                    resultBuilder.setResult(id, throwableToFailedResult(t));
                 }
             }
             return resultBuilder.build();
@@ -143,7 +251,7 @@
 
     @Override
     @NonNull
-    public SearchResults query(
+    public SearchResults search(
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
@@ -151,7 +259,7 @@
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return new SearchResultsImpl(
                 mAppSearchImpl,
-                mExecutorService,
+                mExecutor,
                 mPackageName,
                 mDatabaseName,
                 queryExpression,
@@ -160,38 +268,80 @@
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchBatchResult<String, Void>> removeByUri(
-            @NonNull RemoveByUriRequest request) {
+    public ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         return execute(() -> {
-            AppSearchBatchResult.Builder<String, Void> resultBuilder =
-                    new AppSearchBatchResult.Builder<>();
-            for (String uri : request.getUris()) {
-                try {
-                    mAppSearchImpl.remove(mPackageName, mDatabaseName, request.getNamespace(), uri);
-                    resultBuilder.setSuccess(uri, /*result=*/null);
-                } catch (Throwable t) {
-                    resultBuilder.setResult(uri, throwableToFailedResult(t));
-                }
-            }
+            mAppSearchImpl.reportUsage(
+                    mPackageName,
+                    mDatabaseName,
+                    request.getNamespace(),
+                    request.getDocumentId(),
+                    request.getUsageTimestampMillis(),
+                    /*systemUsage=*/ false);
             mIsMutated = true;
-            return resultBuilder.build();
+            return null;
         });
     }
 
     @Override
     @NonNull
-    public ListenableFuture<Void> removeByQuery(
+    public ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest request) {
+        Preconditions.checkNotNull(request);
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+        ListenableFuture<AppSearchBatchResult<String, Void>> future = execute(() -> {
+            AppSearchBatchResult.Builder<String, Void> resultBuilder =
+                    new AppSearchBatchResult.Builder<>();
+            for (String id : request.getIds()) {
+                try {
+                    mAppSearchImpl.remove(mPackageName, mDatabaseName, request.getNamespace(), id);
+                    resultBuilder.setSuccess(id, /*result=*/null);
+                } catch (Throwable t) {
+                    resultBuilder.setResult(id, throwableToFailedResult(t));
+                }
+            }
+            // Now that the batch has been written. Persist the newly written data.
+            mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
+            mIsMutated = true;
+            return resultBuilder.build();
+        });
+        checkForOptimize(/*mutateBatchSize=*/ request.getIds().size());
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<Void> remove(
             @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Preconditions.checkNotNull(queryExpression);
         Preconditions.checkNotNull(searchSpec);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
-        return execute(() -> {
+        ListenableFuture<Void> future = execute(() -> {
             mAppSearchImpl.removeByQuery(mPackageName, mDatabaseName, queryExpression, searchSpec);
+            // Now that the batch has been written. Persist the newly written data.
+            mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
             mIsMutated = true;
             return null;
         });
+        checkForOptimize();
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<StorageInfo> getStorageInfo() {
+        Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+        return execute(() -> mAppSearchImpl.getStorageInfoForDatabase(mPackageName, mDatabaseName));
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> maybeFlush() {
+        return execute(() -> {
+            mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
+            return null;
+        });
     }
 
     @Override
@@ -199,8 +349,8 @@
     public void close() {
         if (mIsMutated && !mIsClosed) {
             // No future is needed here since the method is void.
-            FutureUtil.execute(mExecutorService, () -> {
-                mAppSearchImpl.persistToDisk();
+            FutureUtil.execute(mExecutor, () -> {
+                mAppSearchImpl.persistToDisk(PersistType.Code.FULL);
                 mIsClosed = true;
                 return null;
             });
@@ -208,6 +358,53 @@
     }
 
     private <T> ListenableFuture<T> execute(Callable<T> callable) {
-        return FutureUtil.execute(mExecutorService, callable);
+        return FutureUtil.execute(mExecutor, callable);
+    }
+
+    /**
+     * Set schema to Icing for no-migration scenario.
+     *
+     * <p>We only need one time {@link #setSchema} call for no-migration scenario by using the
+     * forceoverride in the request.
+     */
+    private SetSchemaResponse setSchemaNoMigrations(@NonNull SetSchemaRequest request,
+            @NonNull Map<String, List<PackageIdentifier>> copySchemasPackageAccessible)
+            throws AppSearchException {
+        SetSchemaResponse setSchemaResponse = mAppSearchImpl.setSchema(
+                mPackageName,
+                mDatabaseName,
+                new ArrayList<>(request.getSchemas()),
+                new ArrayList<>(request.getSchemasNotDisplayedBySystem()),
+                copySchemasPackageAccessible,
+                request.isForceOverride(),
+                request.getVersion());
+        if (!request.isForceOverride()) {
+            // check both deleted types and incompatible types are empty. That's the only case we
+            // swallowed in the AppSearchImpl#setSchema().
+            SchemaMigrationUtil.checkDeletedAndIncompatible(setSchemaResponse.getDeletedTypes(),
+                    setSchemaResponse.getIncompatibleTypes());
+        }
+        mIsMutated = true;
+        return setSchemaResponse;
+    }
+
+    private void checkForOptimize(int mutateBatchSize) {
+        mExecutor.execute(() -> {
+            try {
+                mAppSearchImpl.checkForOptimize(mutateBatchSize);
+            } catch (AppSearchException e) {
+                Log.w(TAG, "Error occurred when check for optimize", e);
+            }
+        });
+    }
+
+    private void checkForOptimize() {
+        mExecutor.execute(() -> {
+            try {
+                mAppSearchImpl.checkForOptimize();
+            } catch (AppSearchException e) {
+                Log.w(TAG, "Error occurred when check for optimize", e);
+            }
+        });
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
index 186cf93..c57da92c 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 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.
@@ -13,214 +13,62 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+// @exportToFramework:skipFile()
 
 package androidx.appsearch.localstorage;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import androidx.appsearch.app.AppSearchResult;
-import androidx.appsearch.app.AppSearchSchema;
-import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.exceptions.AppSearchException;
-import androidx.collection.ArrayMap;
-import androidx.collection.ArraySet;
-import androidx.core.util.Preconditions;
+import android.content.Context;
 
-import java.util.Arrays;
-import java.util.Collections;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.PackageIdentifier;
+
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 /**
- * Manages any visibility settings for all the databases that AppSearchImpl knows about. Persists
- * the visibility settings and reloads them on initialization.
+ * TODO(b/169883602): figure out if we still need a VisibilityStore in localstorage depending on
+ * how we refactor the AppSearchImpl-VisibilityStore relationship.
  *
- * <p>The VisibilityStore creates a document for each database. This document holds the visibility
- * settings that apply to that database. The VisibilityStore also creates a schema for these
- * documents and has its own database so that its data doesn't interfere with any clients' data.
- * It persists the document and schema through AppSearchImpl.
- *
- * <p>These visibility settings are used to ensure AppSearch queries respect the clients'
- * settings on who their data is visible to.
- *
- * <p>This class doesn't handle any locking itself. Its callers should handle the locking at a
- * higher level.
- *
- * <p>NOTE: This class holds an instance of AppSearchImpl and AppSearchImpl holds an instance of
- * this class. Take care to not cause any circular dependencies.
+ * @hide
  */
-class VisibilityStore {
-    /** Schema type for documents that hold AppSearch's metadata, e.g. visibility settings */
-    @VisibleForTesting
-    static final String SCHEMA_TYPE = "Visibility";
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityStore {
+
+    /** No-op user id that won't have any visibility settings. */
+    public static final int NO_OP_USER_ID = -1;
 
     /**
-     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+     * These cannot have any of the special characters used by AppSearchImpl (e.g. {@code
+     * AppSearchImpl#PACKAGE_DELIMITER} or {@code AppSearchImpl#DATABASE_DELIMITER}.
      */
-    @VisibleForTesting
-    static final String NOT_PLATFORM_SURFACEABLE_PROPERTY = "notPlatformSurfaceable";
+    public static final String PACKAGE_NAME = "VS#Pkg";
 
-    /** Schema for the VisibilityStore's docuemnts. */
-    @VisibleForTesting
-    static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
-            .addProperty(new AppSearchSchema.PropertyConfig.Builder(
-                    NOT_PLATFORM_SURFACEABLE_PROPERTY)
-                    .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
-                    .setCardinality(
-                            AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                    .build())
-            .build();
+    public static final String DATABASE_NAME = "VS#Db";
 
-    /**
-     * These cannot have any of the special characters used by AppSearchImpl (e.g.
-     * {@link AppSearchImpl#PACKAGE_DELIMITER} or {@link AppSearchImpl#DATABASE_DELIMITER}.
-     */
-    static final String PACKAGE_NAME = "VS#Pkg";
-    static final String DATABASE_NAME = "VS#Db";
-
-    /**
-     * Prefix that AppSearchImpl creates for the VisibilityStore based on our package name and
-     * database name. Tracked here to tell when we're looking at our own prefix when looking
-     * through AppSearchImpl.
-     */
-    private static final String VISIBILITY_STORE_PREFIX = AppSearchImpl.createPrefix(PACKAGE_NAME,
-            DATABASE_NAME);
-
-    /** Namespace of documents that contain visibility settings */
-    private static final String NAMESPACE = GenericDocument.DEFAULT_NAMESPACE;
-
-    /**
-     * Prefix to add to all visibility document uri's. IcingSearchEngine doesn't allow empty
-     * uri's.
-     */
-    private static final String URI_PREFIX = "uri:";
-
-    private final AppSearchImpl mAppSearchImpl;
-
-    /**
-     * Maps prefixes to the set of schemas that are platform-hidden within that prefix. All schemas
-     * in the map are prefixed.
-     */
-    private final Map<String, Set<String>> mNotPlatformSurfaceableMap = new ArrayMap<>();
-
-    /**
-     * Creates an uninitialized VisibilityStore object. Callers must also call {@link #initialize()}
-     * before using the object.
-     *
-     * @param appSearchImpl AppSearchImpl instance
-     */
-    VisibilityStore(@NonNull AppSearchImpl appSearchImpl) {
-        mAppSearchImpl = appSearchImpl;
+    /** No-op implementation in local storage. */
+    public VisibilityStore(@NonNull AppSearchImpl appSearchImpl, @NonNull Context context,
+            int userId, @NonNull String globalQuerierPackage) {
     }
 
-    /**
-     * Initializes schemas and member variables to track visibility settings.
-     *
-     * <p>This is kept separate from the constructor because this will call methods on
-     * AppSearchImpl. Some may even then recursively call back into VisibilityStore (for example,
-     * {@link AppSearchImpl#setSchema} will call {@link #setVisibility(String, Set)}. We need to
-     * have both
-     * AppSearchImpl and VisibilityStore fully initialized for this call flow to work.
-     *
-     * @throws AppSearchException AppSearchException on AppSearchImpl error.
-     */
-    public void initialize() throws AppSearchException {
-        if (!mAppSearchImpl.hasSchemaTypeLocked(PACKAGE_NAME, DATABASE_NAME, SCHEMA_TYPE)) {
-            // Schema type doesn't exist yet. Add it.
-            mAppSearchImpl.setSchema(PACKAGE_NAME, DATABASE_NAME,
-                    Collections.singletonList(SCHEMA),
-                    /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
-                    /*forceOverride=*/ false);
-        }
-
-        // Populate visibility settings set
-        mNotPlatformSurfaceableMap.clear();
-        for (String prefix : mAppSearchImpl.getPrefixesLocked()) {
-            if (prefix.equals(VISIBILITY_STORE_PREFIX)) {
-                // Our own prefix. Skip
-                continue;
-            }
-
-            try {
-                // Note: We use the other clients' prefixed names as uris
-                GenericDocument document = mAppSearchImpl.getDocument(
-                        PACKAGE_NAME, DATABASE_NAME, NAMESPACE, /*uri=*/ addUriPrefix(prefix));
-
-                String[] schemas = document.getPropertyStringArray(
-                        NOT_PLATFORM_SURFACEABLE_PROPERTY);
-                mNotPlatformSurfaceableMap.put(prefix,
-                        new ArraySet<>(Arrays.asList(schemas)));
-            } catch (AppSearchException e) {
-                if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
-                    // TODO(b/172068212): This indicates some desync error. We were expecting a
-                    //  document, but didn't find one. Should probably reset AppSearch instead of
-                    //  ignoring it.
-                    continue;
-                }
-                // Otherwise, this is some other error we should pass up.
-                throw e;
-            }
-        }
+    /** No-op implementation in local storage. */
+    public void initialize() {
     }
 
-    /**
-     * Sets visibility settings for {@code prefix}. Any previous visibility settings will be
-     * overwritten.
-     *
-     * @param prefix                        Prefix that identifies who owns the {@code
-     *                                      schemasNotPlatformSurfaceable}.
-     * @param schemasNotPlatformSurfaceable Set of prefixed schemas that should be
-     *                                      hidden from the platform.
-     * @throws AppSearchException on AppSearchImpl error.
-     */
+    /** No-op implementation in local storage. */
     public void setVisibility(@NonNull String prefix,
-            @NonNull Set<String> schemasNotPlatformSurfaceable) throws AppSearchException {
-        Preconditions.checkNotNull(prefix);
-        Preconditions.checkNotNull(schemasNotPlatformSurfaceable);
-
-        // Persist the document
-        GenericDocument.Builder visibilityDocument = new GenericDocument.Builder(
-                /*uri=*/ addUriPrefix(prefix), SCHEMA_TYPE)
-                .setNamespace(NAMESPACE);
-        if (!schemasNotPlatformSurfaceable.isEmpty()) {
-            visibilityDocument.setPropertyString(NOT_PLATFORM_SURFACEABLE_PROPERTY,
-                    schemasNotPlatformSurfaceable.toArray(new String[0]));
-        }
-        mAppSearchImpl.putDocument(PACKAGE_NAME, DATABASE_NAME, visibilityDocument.build());
-
-        // Update derived data structures.
-        mNotPlatformSurfaceableMap.put(prefix, schemasNotPlatformSurfaceable);
+            @NonNull Set<String> schemasNotPlatformSurfaceable,
+            @NonNull Map<String, List<PackageIdentifier>> schemasPackageAccessible) {
     }
 
-    /** Returns if the schema is surfaceable by the platform. */
-    @NonNull
-    public boolean isSchemaPlatformSurfaceable(@NonNull String prefix,
-            @NonNull String prefixedSchema) {
-        Preconditions.checkNotNull(prefix);
-        Preconditions.checkNotNull(prefixedSchema);
-        Set<String> notPlatformSurfaceableSchemas = mNotPlatformSurfaceableMap.get(prefix);
-        if (notPlatformSurfaceableSchemas == null) {
-            return true;
-        }
-        return !notPlatformSurfaceableSchemas.contains(prefixedSchema);
+    /** No-op implementation in local storage. */
+    public boolean isSchemaSearchableByCaller(@NonNull String prefix,
+            @NonNull String prefixedSchema, int callerUid) {
+        return false;
     }
 
-    /**
-     * Handles an {@code AppSearchImpl#reset()} by clearing any cached state.
-     *
-     * <p> {@link #initialize()} must be called after this.
-     */
-    void handleReset() {
-        mNotPlatformSurfaceableMap.clear();
-    }
-
-    /**
-     * Adds a uri prefix to create a visibility store document's uri.
-     *
-     * @param uri Non-prefixed uri
-     * @return Prefixed uri
-     */
-    private static String addUriPrefix(String uri) {
-        return URI_PREFIX + uri;
+    /** No-op implementation in local storage. */
+    public void handleReset() {
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index c598045..6014a94 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -18,15 +18,18 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.PropertyProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.protobuf.ByteString;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Map;
 
 /**
  * Translates a {@link GenericDocument} into a {@link DocumentProto}.
@@ -35,15 +38,25 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class GenericDocumentToProtoConverter {
-    private GenericDocumentToProtoConverter() {}
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+    private static final long[] EMPTY_LONG_ARRAY = new long[0];
+    private static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
+    private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
+    private static final byte[][] EMPTY_BYTES_ARRAY = new byte[0][0];
+    private static final GenericDocument[] EMPTY_DOCUMENT_ARRAY = new GenericDocument[0];
 
-    /** Converts a {@link GenericDocument} into a {@link DocumentProto}. */
+    private GenericDocumentToProtoConverter() {
+    }
+
+    /**
+     * Converts a {@link GenericDocument} into a {@link DocumentProto}.
+     */
     @NonNull
     @SuppressWarnings("unchecked")
     public static DocumentProto toDocumentProto(@NonNull GenericDocument document) {
         Preconditions.checkNotNull(document);
         DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
-        mProtoBuilder.setUri(document.getUri())
+        mProtoBuilder.setUri(document.getId())
                 .setSchema(document.getSchemaType())
                 .setNamespace(document.getNamespace())
                 .setScore(document.getScore())
@@ -96,16 +109,34 @@
         return mProtoBuilder.build();
     }
 
-    /** Converts a {@link DocumentProto} into a {@link GenericDocument}. */
+    /**
+     * Converts a {@link DocumentProto} into a {@link GenericDocument}.
+     *
+     * <p>In the case that the {@link DocumentProto} object proto has no values set, the
+     * converter searches for the matching property name in the {@link SchemaTypeConfigProto}
+     * object for the document, and infers the correct default value to set for the empty
+     * property based on the data type of the property defined by the schema type.
+     *
+     * @param proto         the document to convert to a {@link GenericDocument} instance. The
+     *                      document proto should have its package + database prefix stripped
+     *                      from its fields.
+     * @param prefix        the package + database prefix used searching the {@code schemaTypeMap}.
+     * @param schemaTypeMap map of prefixed schema type to {@link SchemaTypeConfigProto}, used
+     *                      for looking up the default empty value to set for a document property
+     *                      that has all empty values.
+     */
     @NonNull
-    public static GenericDocument toGenericDocument(@NonNull DocumentProto proto) {
+    public static GenericDocument toGenericDocument(@NonNull DocumentProto proto,
+            @NonNull String prefix,
+            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap) {
         Preconditions.checkNotNull(proto);
         GenericDocument.Builder<?> documentBuilder =
-                new GenericDocument.Builder<>(proto.getUri(), proto.getSchema())
-                        .setNamespace(proto.getNamespace())
+                new GenericDocument.Builder<>(proto.getNamespace(), proto.getUri(),
+                        proto.getSchema())
                         .setScore(proto.getScore())
                         .setTtlMillis(proto.getTtlMs())
                         .setCreationTimestampMillis(proto.getCreationTimestampMs());
+        String prefixedSchemaType = prefix + proto.getSchema();
 
         for (int i = 0; i < proto.getPropertiesCount(); i++) {
             PropertyProto property = proto.getProperties(i);
@@ -143,13 +174,51 @@
             } else if (property.getDocumentValuesCount() > 0) {
                 GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()];
                 for (int j = 0; j < values.length; j++) {
-                    values[j] = toGenericDocument(property.getDocumentValues(j));
+                    values[j] = toGenericDocument(property.getDocumentValues(j), prefix,
+                            schemaTypeMap);
                 }
                 documentBuilder.setPropertyDocument(name, values);
             } else {
-                throw new IllegalStateException("Unknown type of value: " + name);
+                // TODO(b/184966497): Optimize by caching PropertyConfigProto
+                setEmptyProperty(name, documentBuilder,
+                        schemaTypeMap.get(prefixedSchemaType));
             }
         }
         return documentBuilder.build();
     }
+
+    private static void setEmptyProperty(@NonNull String propertyName,
+            @NonNull GenericDocument.Builder<?> documentBuilder,
+            @NonNull SchemaTypeConfigProto schema) {
+        @AppSearchSchema.PropertyConfig.DataType int dataType = 0;
+        for (int i = 0; i < schema.getPropertiesCount(); ++i) {
+            if (propertyName.equals(schema.getProperties(i).getPropertyName())) {
+                dataType = schema.getProperties(i).getDataType().getNumber();
+                break;
+            }
+        }
+
+        switch (dataType) {
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING:
+                documentBuilder.setPropertyString(propertyName, EMPTY_STRING_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_INT64:
+                documentBuilder.setPropertyLong(propertyName, EMPTY_LONG_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE:
+                documentBuilder.setPropertyDouble(propertyName, EMPTY_DOUBLE_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN:
+                documentBuilder.setPropertyBoolean(propertyName, EMPTY_BOOLEAN_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES:
+                documentBuilder.setPropertyBytes(propertyName, EMPTY_BYTES_ARRAY);
+                break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
+                documentBuilder.setPropertyDocument(propertyName, EMPTY_DOCUMENT_ARRAY);
+                break;
+            default:
+                throw new IllegalStateException("Unknown type of value: " + propertyName);
+        }
+    }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
new file mode 100644
index 0000000..e04d756
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.converter;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+
+import com.google.android.icing.proto.StatusProto;
+
+/**
+ * Translates an {@link StatusProto.Code} into a {@link AppSearchResult.ResultCode}
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ResultCodeToProtoConverter {
+
+    private static final String TAG = "AppSearchResultCodeToPr";
+    private ResultCodeToProtoConverter() {}
+
+    /** Converts an {@link StatusProto.Code} into a {@link AppSearchResult.ResultCode}. */
+    public static @AppSearchResult.ResultCode int toResultCode(
+            @NonNull StatusProto.Code statusCode) {
+        switch (statusCode) {
+            case OK:
+                return AppSearchResult.RESULT_OK;
+            case OUT_OF_SPACE:
+                return AppSearchResult.RESULT_OUT_OF_SPACE;
+            case INTERNAL:
+                return AppSearchResult.RESULT_INTERNAL_ERROR;
+            case UNKNOWN:
+                return AppSearchResult.RESULT_UNKNOWN_ERROR;
+            case NOT_FOUND:
+                return AppSearchResult.RESULT_NOT_FOUND;
+            case INVALID_ARGUMENT:
+                return AppSearchResult.RESULT_INVALID_ARGUMENT;
+            default:
+                // Some unknown/unsupported error
+                Log.e(TAG, "Cannot convert IcingSearchEngine status code: "
+                        + statusCode + " to AppSearchResultCode.");
+                return AppSearchResult.RESULT_INTERNAL_ERROR;
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
index f52c220..d999e92 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
@@ -23,6 +23,7 @@
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.core.util.Preconditions;
 
+import com.google.android.icing.proto.DocumentIndexingConfig;
 import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SchemaTypeConfigProtoOrBuilder;
@@ -46,10 +47,12 @@
      * {@link SchemaTypeConfigProto}.
      */
     @NonNull
-    public static SchemaTypeConfigProto toSchemaTypeConfigProto(@NonNull AppSearchSchema schema) {
+    public static SchemaTypeConfigProto toSchemaTypeConfigProto(@NonNull AppSearchSchema schema,
+            int version) {
         Preconditions.checkNotNull(schema);
-        SchemaTypeConfigProto.Builder protoBuilder =
-                SchemaTypeConfigProto.newBuilder().setSchemaType(schema.getSchemaType());
+        SchemaTypeConfigProto.Builder protoBuilder = SchemaTypeConfigProto.newBuilder()
+                .setSchemaType(schema.getSchemaType())
+                .setVersion(version);
         List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
             PropertyConfigProto propertyProto = toPropertyConfigProto(properties.get(i));
@@ -64,7 +67,6 @@
         Preconditions.checkNotNull(property);
         PropertyConfigProto.Builder builder = PropertyConfigProto.newBuilder()
                 .setPropertyName(property.getName());
-        StringIndexingConfig.Builder indexingConfig = StringIndexingConfig.newBuilder();
 
         // Set dataType
         @AppSearchSchema.PropertyConfig.DataType int dataType = property.getDataType();
@@ -75,12 +77,6 @@
         }
         builder.setDataType(dataTypeProto);
 
-        // Set schemaType
-        String schemaType = property.getSchemaType();
-        if (schemaType != null) {
-            builder.setSchemaType(schemaType);
-        }
-
         // Set cardinality
         @AppSearchSchema.PropertyConfig.Cardinality int cardinality = property.getCardinality();
         PropertyConfigProto.Cardinality.Code cardinalityProto =
@@ -90,36 +86,25 @@
         }
         builder.setCardinality(cardinalityProto);
 
-        // Set indexingType
-        @AppSearchSchema.PropertyConfig.IndexingType int indexingType = property.getIndexingType();
-        TermMatchType.Code termMatchTypeProto;
-        switch (indexingType) {
-            case AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE:
-                termMatchTypeProto = TermMatchType.Code.UNKNOWN;
-                break;
-            case AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS:
-                termMatchTypeProto = TermMatchType.Code.EXACT_ONLY;
-                break;
-            case AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES:
-                termMatchTypeProto = TermMatchType.Code.PREFIX;
-                break;
-            default:
-                throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
-        }
-        indexingConfig.setTermMatchType(termMatchTypeProto);
+        if (property instanceof AppSearchSchema.StringPropertyConfig) {
+            AppSearchSchema.StringPropertyConfig stringProperty =
+                    (AppSearchSchema.StringPropertyConfig) property;
+            StringIndexingConfig stringIndexingConfig = StringIndexingConfig.newBuilder()
+                    .setTermMatchType(convertTermMatchTypeToProto(stringProperty.getIndexingType()))
+                    .setTokenizerType(
+                            convertTokenizerTypeToProto(stringProperty.getTokenizerType()))
+                    .build();
+            builder.setStringIndexingConfig(stringIndexingConfig);
 
-        // Set tokenizerType
-        @AppSearchSchema.PropertyConfig.TokenizerType int tokenizerType =
-                property.getTokenizerType();
-        StringIndexingConfig.TokenizerType.Code tokenizerTypeProto =
-                StringIndexingConfig.TokenizerType.Code.forNumber(tokenizerType);
-        if (tokenizerTypeProto == null) {
-            throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType);
+        } else if (property instanceof AppSearchSchema.DocumentPropertyConfig) {
+            AppSearchSchema.DocumentPropertyConfig documentProperty =
+                    (AppSearchSchema.DocumentPropertyConfig) property;
+            builder
+                    .setSchemaType(documentProperty.getSchemaType())
+                    .setDocumentIndexingConfig(
+                            DocumentIndexingConfig.newBuilder().setIndexNestedProperties(
+                                    documentProperty.shouldIndexNestedProperties()));
         }
-        indexingConfig.setTokenizerType(tokenizerTypeProto);
-
-        // Build!
-        builder.setStringIndexingConfig(indexingConfig);
         return builder.build();
     }
 
@@ -130,7 +115,8 @@
     @NonNull
     public static AppSearchSchema toAppSearchSchema(@NonNull SchemaTypeConfigProtoOrBuilder proto) {
         Preconditions.checkNotNull(proto);
-        AppSearchSchema.Builder builder = new AppSearchSchema.Builder(proto.getSchemaType());
+        AppSearchSchema.Builder builder =
+                new AppSearchSchema.Builder(proto.getSchemaType());
         List<PropertyConfigProto> properties = proto.getPropertiesList();
         for (int i = 0; i < properties.size(); i++) {
             AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
@@ -143,39 +129,99 @@
     private static AppSearchSchema.PropertyConfig toPropertyConfig(
             @NonNull PropertyConfigProto proto) {
         Preconditions.checkNotNull(proto);
-        AppSearchSchema.PropertyConfig.Builder builder =
-                new AppSearchSchema.PropertyConfig.Builder(proto.getPropertyName())
-                        .setDataType(proto.getDataType().getNumber())
+        switch (proto.getDataType()) {
+            case STRING:
+                return toStringPropertyConfig(proto);
+            case INT64:
+                return new AppSearchSchema.Int64PropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .build();
+            case DOUBLE:
+                return new AppSearchSchema.DoublePropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .build();
+            case BOOLEAN:
+                return new AppSearchSchema.BooleanPropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .build();
+            case BYTES:
+                return new AppSearchSchema.BytesPropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .build();
+            case DOCUMENT:
+                return toDocumentPropertyConfig(proto);
+            default:
+                throw new IllegalArgumentException("Invalid dataType: " + proto.getDataType());
+        }
+    }
+
+    @NonNull
+    private static AppSearchSchema.StringPropertyConfig toStringPropertyConfig(
+            @NonNull PropertyConfigProto proto) {
+        AppSearchSchema.StringPropertyConfig.Builder builder =
+                new AppSearchSchema.StringPropertyConfig.Builder(proto.getPropertyName())
                         .setCardinality(proto.getCardinality().getNumber())
                         .setTokenizerType(
                                 proto.getStringIndexingConfig().getTokenizerType().getNumber());
 
-        // Set schema
-        if (!proto.getSchemaType().isEmpty()) {
-            builder.setSchemaType(proto.getSchemaType());
-        }
-
         // Set indexingType
-        @AppSearchSchema.PropertyConfig.IndexingType int indexingType;
         TermMatchType.Code termMatchTypeProto = proto.getStringIndexingConfig().getTermMatchType();
-        switch (termMatchTypeProto) {
+        builder.setIndexingType(convertTermMatchTypeFromProto(termMatchTypeProto));
+
+        return builder.build();
+    }
+
+    @NonNull
+    private static AppSearchSchema.DocumentPropertyConfig toDocumentPropertyConfig(
+            @NonNull PropertyConfigProto proto) {
+        return new AppSearchSchema.DocumentPropertyConfig.Builder(
+                proto.getPropertyName(), proto.getSchemaType())
+                .setCardinality(proto.getCardinality().getNumber())
+                .setShouldIndexNestedProperties(
+                        proto.getDocumentIndexingConfig().getIndexNestedProperties())
+                .build();
+    }
+
+    @NonNull
+    private static TermMatchType.Code convertTermMatchTypeToProto(
+            @AppSearchSchema.StringPropertyConfig.IndexingType int indexingType) {
+        switch (indexingType) {
+            case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE:
+                return TermMatchType.Code.UNKNOWN;
+            case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS:
+                return TermMatchType.Code.EXACT_ONLY;
+            case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES:
+                return TermMatchType.Code.PREFIX;
+            default:
+                throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
+        }
+    }
+
+    @AppSearchSchema.StringPropertyConfig.IndexingType
+    private static int convertTermMatchTypeFromProto(@NonNull TermMatchType.Code termMatchType) {
+        switch (termMatchType) {
             case UNKNOWN:
-                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
-                break;
+                return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
             case EXACT_ONLY:
-                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS;
-                break;
+                return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS;
             case PREFIX:
-                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
-                break;
+                return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
             default:
                 // Avoid crashing in the 'read' path; we should try to interpret the document to the
                 // extent possible.
-                Log.w(TAG, "Invalid indexingType: " + termMatchTypeProto.getNumber());
-                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+                Log.w(TAG, "Invalid indexingType: " + termMatchType.getNumber());
+                return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
         }
-        builder.setIndexingType(indexingType);
+    }
 
-        return builder.build();
+    @NonNull
+    private static StringIndexingConfig.TokenizerType.Code convertTokenizerTypeToProto(
+            @AppSearchSchema.StringPropertyConfig.TokenizerType int tokenizerType) {
+        StringIndexingConfig.TokenizerType.Code tokenizerTypeProto =
+                StringIndexingConfig.TokenizerType.Code.forNumber(tokenizerType);
+        if (tokenizerTypeProto == null) {
+            throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType);
+        }
+        return tokenizerTypeProto;
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index d53492e..3af5ec2 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -16,6 +16,8 @@
 
 package androidx.appsearch.localstorage.converter;
 
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+
 import android.os.Bundle;
 
 import androidx.annotation.NonNull;
@@ -25,6 +27,7 @@
 import androidx.appsearch.app.SearchResultPage;
 import androidx.core.util.Preconditions;
 
+import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SearchResultProto;
 import com.google.android.icing.proto.SearchResultProtoOrBuilder;
 import com.google.android.icing.proto.SnippetMatchProto;
@@ -32,6 +35,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Translates a {@link SearchResultProto} into {@link SearchResult}s.
@@ -46,23 +50,34 @@
     /**
      * Translate a {@link SearchResultProto} into {@link SearchResultPage}.
      *
-     * @param proto The {@link SearchResultProto} containing results.
-     * @param packageNames A parallel array of package names. The package name at index 'i' of
-     *                     this list should be the package that indexed the document at index 'i'
-     *                     of proto.getResults(i).
+     * @param proto         The {@link SearchResultProto} containing results.
+     * @param packageNames  A parallel array of package names. The package name at index 'i' of
+     *                      this list should be the package that indexed the document at index 'i'
+     *                      of proto.getResults(i).
+     * @param databaseNames A parallel array of database names. The database name at index 'i' of
+     *                      this list shold be the database that indexed the document at index 'i'
+     *                      of proto.getResults(i).
+     * @param schemaMap     A map of prefixes to an inner-map of prefixed schema type to
+     *                      SchemaTypeConfigProtos, used for setting a default value for results
+     *                      with DocumentProtos that have empty values.
      * @return {@link SearchResultPage} of results.
      */
     @NonNull
     public static SearchResultPage toSearchResultPage(@NonNull SearchResultProtoOrBuilder proto,
-            @NonNull List<String> packageNames) {
-        Preconditions.checkArgument(proto.getResultsCount() == packageNames.size(), "Size of "
-                + "results does not match the number of package names.");
+            @NonNull List<String> packageNames, @NonNull List<String> databaseNames,
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+        Preconditions.checkArgument(
+                proto.getResultsCount() == packageNames.size(),
+                "Size of results does not match the number of package names.");
         Bundle bundle = new Bundle();
         bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
         ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
         for (int i = 0; i < proto.getResultsCount(); i++) {
-            resultBundles.add(toSearchResultBundle(proto.getResults(i),
-                    packageNames.get(i)));
+            String prefix = createPrefix(packageNames.get(i), databaseNames.get(i));
+            Map<String, SchemaTypeConfigProto> schemaTypeMap = schemaMap.get(prefix);
+            SearchResult result = toSearchResult(
+                    proto.getResults(i), packageNames.get(i), databaseNames.get(i), schemaTypeMap);
+            resultBundles.add(result.getBundle());
         }
         bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
         return new SearchResultPage(bundle);
@@ -71,53 +86,53 @@
     /**
      * Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}.
      *
-     * @param proto The proto to be converted.
-     * @param packageName The package name associated with the document in {@code proto}.
+     * @param proto                The proto to be converted.
+     * @param packageName          The package name associated with the document in {@code proto}.
+     * @param databaseName         The database name associated with the document in {@code proto}.
+     * @param schemaTypeToProtoMap A map of prefixed schema types to their corresponding
+     *                             SchemaTypeConfigProto, used for setting a default value for
+     *                             results with DocumentProtos that have empty values.
      * @return A {@link SearchResult} bundle.
      */
     @NonNull
-    private static Bundle toSearchResultBundle(
-            @NonNull SearchResultProto.ResultProtoOrBuilder proto, @NonNull String packageName) {
-        Bundle bundle = new Bundle();
+    private static SearchResult toSearchResult(
+            @NonNull SearchResultProto.ResultProtoOrBuilder proto,
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeToProtoMap) {
+        String prefix = createPrefix(packageName, databaseName);
         GenericDocument document =
-                GenericDocumentToProtoConverter.toGenericDocument(proto.getDocument());
-        bundle.putBundle(SearchResult.DOCUMENT_FIELD, document.getBundle());
-        bundle.putString(SearchResult.PACKAGE_NAME_FIELD, packageName);
-
-        ArrayList<Bundle> matchList = new ArrayList<>();
+                GenericDocumentToProtoConverter.toGenericDocument(proto.getDocument(), prefix,
+                        schemaTypeToProtoMap);
+        SearchResult.Builder builder =
+                new SearchResult.Builder(packageName, databaseName)
+                        .setGenericDocument(document).setRankingSignal(proto.getScore());
         if (proto.hasSnippet()) {
             for (int i = 0; i < proto.getSnippet().getEntriesCount(); i++) {
                 SnippetProto.EntryProto entry = proto.getSnippet().getEntries(i);
                 for (int j = 0; j < entry.getSnippetMatchesCount(); j++) {
-                    Bundle matchInfoBundle = convertToMatchInfoBundle(
+                    SearchResult.MatchInfo matchInfo = toMatchInfo(
                             entry.getSnippetMatches(j), entry.getPropertyName());
-                    matchList.add(matchInfoBundle);
+                    builder.addMatch(matchInfo);
                 }
             }
         }
-        bundle.putParcelableArrayList(SearchResult.MATCHES_FIELD, matchList);
-
-        return bundle;
+        return builder.build();
     }
 
-    private static Bundle convertToMatchInfoBundle(
-            SnippetMatchProto snippetMatchProto, String propertyPath) {
-        Bundle bundle = new Bundle();
-        bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, propertyPath);
-        bundle.putInt(
-                SearchResult.MatchInfo.VALUES_INDEX_FIELD, snippetMatchProto.getValuesIndex());
-        bundle.putInt(
-                SearchResult.MatchInfo.EXACT_MATCH_POSITION_LOWER_FIELD,
-                snippetMatchProto.getExactMatchPosition());
-        bundle.putInt(
-                SearchResult.MatchInfo.EXACT_MATCH_POSITION_UPPER_FIELD,
-                snippetMatchProto.getExactMatchPosition() + snippetMatchProto.getExactMatchBytes());
-        bundle.putInt(
-                SearchResult.MatchInfo.WINDOW_POSITION_LOWER_FIELD,
-                snippetMatchProto.getWindowPosition());
-        bundle.putInt(
-                SearchResult.MatchInfo.WINDOW_POSITION_UPPER_FIELD,
-                snippetMatchProto.getWindowPosition() + snippetMatchProto.getWindowBytes());
-        return bundle;
+    private static SearchResult.MatchInfo toMatchInfo(
+            @NonNull SnippetMatchProto snippetMatchProto, @NonNull String propertyPath) {
+        return new SearchResult.MatchInfo.Builder(propertyPath)
+                .setExactMatchRange(
+                        new SearchResult.MatchRange(
+                                snippetMatchProto.getExactMatchPosition(),
+                                snippetMatchProto.getExactMatchPosition()
+                                        + snippetMatchProto.getExactMatchBytes()))
+                .setSnippetRange(
+                        new SearchResult.MatchRange(
+                                snippetMatchProto.getWindowPosition(),
+                                snippetMatchProto.getWindowPosition()
+                                        + snippetMatchProto.getWindowBytes()))
+                .build();
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index 485d361..abc97f3 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -25,10 +25,6 @@
 import com.google.android.icing.proto.ScoringSpecProto;
 import com.google.android.icing.proto.SearchSpecProto;
 import com.google.android.icing.proto.TermMatchType;
-import com.google.android.icing.proto.TypePropertyMask;
-
-import java.util.List;
-import java.util.Map;
 
 /**
  * Translates a {@link SearchSpec} into icing search protos.
@@ -45,8 +41,8 @@
     public static SearchSpecProto toSearchSpecProto(@NonNull SearchSpec spec) {
         Preconditions.checkNotNull(spec);
         SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder()
-                .addAllSchemaTypeFilters(spec.getSchemaTypes())
-                .addAllNamespaceFilters(spec.getNamespaces());
+                .addAllSchemaTypeFilters(spec.getFilterSchemas())
+                .addAllNamespaceFilters(spec.getFilterNamespaces());
 
         @SearchSpec.TermMatch int termMatchCode = spec.getTermMatch();
         TermMatchType.Code termMatchCodeProto = TermMatchType.Code.forNumber(termMatchCode);
@@ -62,20 +58,15 @@
     @NonNull
     public static ResultSpecProto toResultSpecProto(@NonNull SearchSpec spec) {
         Preconditions.checkNotNull(spec);
-        ResultSpecProto.Builder builder = ResultSpecProto.newBuilder()
+        return ResultSpecProto.newBuilder()
                 .setNumPerPage(spec.getResultCountPerPage())
                 .setSnippetSpec(
                         ResultSpecProto.SnippetSpecProto.newBuilder()
                                 .setNumToSnippet(spec.getSnippetCount())
                                 .setNumMatchesPerProperty(spec.getSnippetCountPerProperty())
-                                .setMaxWindowBytes(spec.getMaxSnippetSize()));
-        Map<String, List<String>> projectionTypePropertyPaths = spec.getProjections();
-        for (Map.Entry<String, List<String>> e : projectionTypePropertyPaths.entrySet()) {
-            builder.addTypePropertyMasks(
-                    TypePropertyMask.newBuilder().setSchemaType(
-                            e.getKey()).addAllPaths(e.getValue()));
-        }
-        return builder.build();
+                                .setMaxWindowBytes(spec.getMaxSnippetSize()))
+                .addAllTypePropertyMasks(TypePropertyPathToProtoConverter.toTypePropertyMaskList(
+                        spec.getProjections())).build();
     }
 
     /** Extracts {@link ScoringSpecProto} information from a {@link SearchSpec}. */
@@ -107,6 +98,14 @@
                 return ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP;
             case SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE:
                 return ScoringSpecProto.RankingStrategy.Code.RELEVANCE_SCORE;
+            case SearchSpec.RANKING_STRATEGY_USAGE_COUNT:
+                return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE1_COUNT;
+            case SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP:
+                return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE1_LAST_USED_TIMESTAMP;
+            case SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT:
+                return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE2_COUNT;
+            case SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP:
+                return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE2_LAST_USED_TIMESTAMP;
             default:
                 throw new IllegalArgumentException("Invalid result ranking strategy: "
                         + rankingStrategyCode);
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SetSchemaResponseToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SetSchemaResponseToProtoConverter.java
new file mode 100644
index 0000000..62d788c
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SetSchemaResponseToProtoConverter.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.converter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.SetSchemaResultProto;
+
+/**
+ * Translates a {@link SetSchemaResultProto} into {@link SetSchemaResponse}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SetSchemaResponseToProtoConverter {
+
+    private SetSchemaResponseToProtoConverter() {}
+
+    /**
+     * Translate a {@link SetSchemaResultProto} into {@link SetSchemaResponse}.
+     *
+     * @param proto  The {@link SetSchemaResultProto} containing results.
+     * @param prefix The prefix need to removed from schemaTypes
+     * @return The {@link SetSchemaResponse} object.
+     */
+    @NonNull
+    public static SetSchemaResponse toSetSchemaResponse(@NonNull SetSchemaResultProto proto,
+            @NonNull String prefix) {
+        Preconditions.checkNotNull(proto);
+        Preconditions.checkNotNull(prefix);
+        SetSchemaResponse.Builder builder = new SetSchemaResponse.Builder();
+
+        for (int i = 0; i < proto.getDeletedSchemaTypesCount(); i++) {
+            builder.addDeletedType(
+                    proto.getDeletedSchemaTypes(i).substring(prefix.length()));
+        }
+
+        for (int i = 0; i < proto.getIncompatibleSchemaTypesCount(); i++) {
+            builder.addIncompatibleType(
+                    proto.getIncompatibleSchemaTypes(i).substring(prefix.length()));
+        }
+
+        return builder.build();
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
new file mode 100644
index 0000000..98f5642
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.converter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import com.google.android.icing.proto.TypePropertyMask;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Translates a <code>Map<String, List<String>></code> into <code>List<TypePropertyMask></code>.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class TypePropertyPathToProtoConverter {
+    private TypePropertyPathToProtoConverter() {}
+
+    /** Extracts {@link TypePropertyMask} information from a {@link Map}. */
+    @NonNull
+    public static List<TypePropertyMask> toTypePropertyMaskList(@NonNull Map<String,
+            List<String>> typePropertyPaths) {
+        Preconditions.checkNotNull(typePropertyPaths);
+        List<TypePropertyMask> typePropertyMasks = new ArrayList<>(typePropertyPaths.size());
+        for (Map.Entry<String, List<String>> e : typePropertyPaths.entrySet()) {
+            typePropertyMasks.add(
+                    TypePropertyMask.newBuilder().setSchemaType(
+                            e.getKey()).addAllPaths(e.getValue()).build());
+        }
+        return typePropertyMasks;
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
new file mode 100644
index 0000000..587c849
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class for setting basic information to log for all function calls.
+ *
+ * <p>This class can set which stats to log for both batch and non-batch
+ * {@link androidx.appsearch.app.AppSearchSession} calls.
+ *
+ * <p>Some function calls like
+ * {@link androidx.appsearch.app.AppSearchSession#setSchema} have their own
+ * detailed stats class {@link placeholder}. However, {@link CallStats} can still be used along with
+ * the detailed stats class for easy aggregation/analysis with other function calls.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class CallStats {
+    @IntDef(value = {
+            CALL_TYPE_UNKNOWN,
+            CALL_TYPE_INITIALIZE,
+            CALL_TYPE_SET_SCHEMA,
+            CALL_TYPE_PUT_DOCUMENTS,
+            CALL_TYPE_GET_DOCUMENTS,
+            CALL_TYPE_REMOVE_DOCUMENTS,
+            CALL_TYPE_PUT_DOCUMENT,
+            CALL_TYPE_GET_DOCUMENT,
+            CALL_TYPE_REMOVE_DOCUMENT,
+            CALL_TYPE_QUERY,
+            CALL_TYPE_OPTIMIZE,
+            CALL_TYPE_FLUSH,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CallType {
+    }
+
+    public static final int CALL_TYPE_UNKNOWN = 0;
+    public static final int CALL_TYPE_INITIALIZE = 1;
+    public static final int CALL_TYPE_SET_SCHEMA = 2;
+    public static final int CALL_TYPE_PUT_DOCUMENTS = 3;
+    public static final int CALL_TYPE_GET_DOCUMENTS = 4;
+    public static final int CALL_TYPE_REMOVE_DOCUMENTS = 5;
+    public static final int CALL_TYPE_PUT_DOCUMENT = 6;
+    public static final int CALL_TYPE_GET_DOCUMENT = 7;
+    public static final int CALL_TYPE_REMOVE_DOCUMENT = 8;
+    public static final int CALL_TYPE_QUERY = 9;
+    public static final int CALL_TYPE_OPTIMIZE = 10;
+    public static final int CALL_TYPE_FLUSH = 11;
+
+    @NonNull
+    private final GeneralStats mGeneralStats;
+    @CallType
+    private final int mCallType;
+    private final int mEstimatedBinderLatencyMillis;
+    private final int mNumOperationsSucceeded;
+    private final int mNumOperationsFailed;
+
+    CallStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mGeneralStats = Preconditions.checkNotNull(builder.mGeneralStatsBuilder).build();
+        mCallType = builder.mCallType;
+        mEstimatedBinderLatencyMillis = builder.mEstimatedBinderLatencyMillis;
+        mNumOperationsSucceeded = builder.mNumOperationsSucceeded;
+        mNumOperationsFailed = builder.mNumOperationsFailed;
+    }
+
+    /** Returns general information for the call. */
+    @NonNull
+    public GeneralStats getGeneralStats() {
+        return mGeneralStats;
+    }
+
+    /** Returns type of the call. */
+    @CallType
+    public int getCallType() {
+        return mCallType;
+    }
+
+    /** Returns estimated binder latency, in milliseconds */
+    public int getEstimatedBinderLatencyMillis() {
+        return mEstimatedBinderLatencyMillis;
+    }
+
+    /**
+     * Returns number of operations succeeded.
+     *
+     * <p>For example, for
+     * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of individual
+     * successful put operations. In this case, how many documents are successfully indexed.
+     *
+     * <p>For non-batch calls such as
+     * {@link androidx.appsearch.app.AppSearchSession#setSchema}, the sum of
+     * {@link CallStats#getNumOperationsSucceeded()} and
+     * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
+     * operation.
+     */
+    public int getNumOperationsSucceeded() {
+        return mNumOperationsSucceeded;
+    }
+
+    /**
+     * Returns number of operations failed.
+     *
+     * <p>For example, for
+     * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of individual
+     * failed put operations. In this case, how many documents are failed to be indexed.
+     *
+     * <p>For non-batch calls such as {@link androidx.appsearch.app.AppSearchSession#setSchema},
+     * the sum of {@link CallStats#getNumOperationsSucceeded()} and
+     * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
+     * operation.
+     */
+    public int getNumOperationsFailed() {
+        return mNumOperationsFailed;
+    }
+
+    /** Builder for {@link CallStats}. */
+    public static class Builder {
+        @NonNull
+        final GeneralStats.Builder mGeneralStatsBuilder;
+        @CallType
+        int mCallType;
+        int mEstimatedBinderLatencyMillis;
+        int mNumOperationsSucceeded;
+        int mNumOperationsFailed;
+
+        /** Builder takes {@link GeneralStats.Builder}. */
+        public Builder(@NonNull String packageName, @NonNull String database) {
+            Preconditions.checkNotNull(packageName);
+            Preconditions.checkNotNull(database);
+            mGeneralStatsBuilder = new GeneralStats.Builder(packageName, database);
+        }
+
+        /** Returns {@link GeneralStats.Builder}. */
+        @NonNull
+        public GeneralStats.Builder getGeneralStatsBuilder() {
+            return mGeneralStatsBuilder;
+        }
+
+        /** Sets type of the call. */
+        @NonNull
+        public Builder setCallType(@CallType int callType) {
+            mCallType = callType;
+            return this;
+        }
+
+        /** Sets estimated binder latency, in milliseconds. */
+        @NonNull
+        public Builder setEstimatedBinderLatencyMillis(int estimatedBinderLatencyMillis) {
+            mEstimatedBinderLatencyMillis = estimatedBinderLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets number of operations succeeded.
+         *
+         * <p>For example, for
+         * {@link androidx.appsearch.app.AppSearchSession#put}, it is the total number of
+         * individual successful put operations. In this case, how many documents are
+         * successfully indexed.
+         *
+         * <p>For non-batch calls such as
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema}, the sum of
+         * {@link CallStats#getNumOperationsSucceeded()} and
+         * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
+         * operation.
+         */
+        @NonNull
+        public Builder setNumOperationsSucceeded(int numOperationsSucceeded) {
+            mNumOperationsSucceeded = numOperationsSucceeded;
+            return this;
+        }
+
+        /**
+         * Sets number of operations failed.
+         *
+         * <p>For example, for {@link androidx.appsearch.app.AppSearchSession#put}, it is the
+         * total number of individual failed put operations. In this case, how many documents
+         * are failed to be indexed.
+         *
+         * <p>For non-batch calls such as
+         * {@link androidx.appsearch.app.AppSearchSession#setSchema}, the sum of
+         * {@link CallStats#getNumOperationsSucceeded()} and
+         * {@link CallStats#getNumOperationsFailed()} is always 1 since there is only one
+         * operation.
+         */
+        @NonNull
+        public Builder setNumOperationsFailed(int numOperationsFailed) {
+            mNumOperationsFailed = numOperationsFailed;
+            return this;
+        }
+
+        /** Creates {@link CallStats} object from {@link Builder} instance. */
+        @NonNull
+        public CallStats build() {
+            return new CallStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/GeneralStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/GeneralStats.java
new file mode 100644
index 0000000..c24bc33
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/GeneralStats.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.core.util.Preconditions;
+
+/**
+ * A class for holding general logging information.
+ *
+ * <p>This class cannot be logged by
+ * {@link androidx.appsearch.localstorage.AppSearchLogger} directly. It is used for defining
+ * general logging information that is shared across different stats classes.
+ *
+ * @see PutDocumentStats
+ * @see CallStats
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class GeneralStats {
+    /** Package name of the application. */
+    @NonNull
+    private final String mPackageName;
+
+    /** Database name within AppSearch. */
+    @NonNull
+    private final String mDatabase;
+
+    /**
+     * The status code returned by {@link AppSearchResult#getResultCode()} for the call or
+     * internal state.
+     */
+    @AppSearchResult.ResultCode
+    private final int mStatusCode;
+    private final int mTotalLatencyMillis;
+
+    GeneralStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mPackageName = Preconditions.checkNotNull(builder.mPackageName);
+        mDatabase = Preconditions.checkNotNull(builder.mDatabase);
+        mStatusCode = builder.mStatusCode;
+        mTotalLatencyMillis = builder.mTotalLatencyMillis;
+    }
+
+    /** Returns package name. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /** Returns database name. */
+    @NonNull
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns result code from {@link AppSearchResult#getResultCode()} */
+    @AppSearchResult.ResultCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /** Returns total latency, in milliseconds. */
+    public int getTotalLatencyMillis() {
+        return mTotalLatencyMillis;
+    }
+
+    /** Builder for {@link GeneralStats}. */
+    public static class Builder {
+        @NonNull final String mPackageName;
+        @NonNull final String mDatabase;
+        @AppSearchResult.ResultCode int mStatusCode = AppSearchResult.RESULT_UNKNOWN_ERROR;
+        int mTotalLatencyMillis;
+
+        /**
+         * Constructor
+         *
+         * @param packageName name of the package logging stats
+         * @param database name of the database logging stats
+         */
+        public Builder(@NonNull String packageName, @NonNull String database) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+            mDatabase = Preconditions.checkNotNull(database);
+        }
+
+        /**
+         * Sets status code returned from {@link AppSearchResult#getResultCode()}
+         */
+        @NonNull
+        public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+            mStatusCode = statusCode;
+            return this;
+        }
+
+        /** Sets total latency, in milliseconds. */
+        @NonNull
+        public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+            mTotalLatencyMillis = totalLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Creates a new {@link GeneralStats} object from the contents of this {@link Builder}
+         * instance.
+         */
+        @NonNull
+        public GeneralStats build() {
+            return new GeneralStats(/* builder= */this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java
new file mode 100644
index 0000000..43be222
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/InitializeStats.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class holds detailed stats for initialization
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class InitializeStats {
+    /**
+     * The cause of IcingSearchEngine recovering from a previous bad state during initialization.
+     */
+    @IntDef(value = {
+            // It needs to be sync with RecoveryCause in
+            // external/icing/proto/icing/proto/logging.proto#InitializeStatsProto
+            RECOVERY_CAUSE_NONE,
+            RECOVERY_CAUSE_DATA_LOSS,
+            RECOVERY_CAUSE_INCONSISTENT_WITH_GROUND_TRUTH,
+            RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH,
+            RECOVERY_CAUSE_IO_ERROR,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RecoveryCause {
+    }
+
+    // No recovery happened.
+    public static final int RECOVERY_CAUSE_NONE = 0;
+    // Data loss in ground truth.
+    public static final int RECOVERY_CAUSE_DATA_LOSS = 1;
+    // Data in index is inconsistent with ground truth.
+    public static final int RECOVERY_CAUSE_INCONSISTENT_WITH_GROUND_TRUTH = 2;
+    // Total checksum of all the components does not match.
+    public static final int RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH = 3;
+    // Random I/O errors.
+    public static final int RECOVERY_CAUSE_IO_ERROR = 4;
+
+    /**
+     * Status regarding how much data is lost during the initialization.
+     */
+    @IntDef(value = {
+            // It needs to be sync with DocumentStoreDataStatus in
+            // external/icing/proto/icing/proto/logging.proto#InitializeStatsProto
+
+            DOCUMENT_STORE_DATA_STATUS_NO_DATA_LOSS,
+            DOCUMENT_STORE_DATA_STATUS_PARTIAL_LOSS,
+            DOCUMENT_STORE_DATA_STATUS_COMPLETE_LOSS,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DocumentStoreDataStatus {
+    }
+
+    // Document store is successfully initialized or fully recovered.
+    public static final int DOCUMENT_STORE_DATA_STATUS_NO_DATA_LOSS = 0;
+    // Ground truth data is partially lost.
+    public static final int DOCUMENT_STORE_DATA_STATUS_PARTIAL_LOSS = 1;
+    // Ground truth data is completely lost.
+    public static final int DOCUMENT_STORE_DATA_STATUS_COMPLETE_LOSS = 2;
+
+    @AppSearchResult.ResultCode
+    private final int mStatusCode;
+    private final int mTotalLatencyMillis;
+    /** Whether the initialize() detects deSync. */
+    private final boolean mHasDeSync;
+    /** Time used to read and process the schema and namespaces. */
+    private final int mPrepareSchemaAndNamespacesLatencyMillis;
+    /** Time used to read and process the visibility store. */
+    private final int mPrepareVisibilityStoreLatencyMillis;
+    /** Overall time used for the native function call. */
+    private final int mNativeLatencyMillis;
+    @RecoveryCause
+    private final int mNativeDocumentStoreRecoveryCause;
+    @RecoveryCause
+    private final int mNativeIndexRestorationCause;
+    @RecoveryCause
+    private final int mNativeSchemaStoreRecoveryCause;
+    /** Time used to recover the document store. */
+    private final int mNativeDocumentStoreRecoveryLatencyMillis;
+    /** Time used to restore the index. */
+    private final int mNativeIndexRestorationLatencyMillis;
+    /** Time used to recover the schema store. */
+    private final int mNativeSchemaStoreRecoveryLatencyMillis;
+    /** Status regarding how much data is lost during the initialization. */
+    private final int mNativeDocumentStoreDataStatus;
+    /**
+     * Returns number of documents currently in document store. Those may include alive, deleted,
+     * and expired documents.
+     */
+    private final int mNativeNumDocuments;
+    /** Returns number of schema types currently in the schema store. */
+    private final int mNativeNumSchemaTypes;
+
+    /** Returns the status of the initialization. */
+    @AppSearchResult.ResultCode
+    public int getStatusCode() {
+        return mStatusCode;
+    }
+
+    /** Returns the total latency in milliseconds for the initialization. */
+    public int getTotalLatencyMillis() {
+        return mTotalLatencyMillis;
+    }
+
+    /**
+     * Returns whether the initialize() detects deSync.
+     *
+     * <p>If there is a deSync, it means AppSearch and IcingSearchEngine have an inconsistent view
+     * of what data should exist.
+     */
+    public boolean hasDeSync() {
+        return mHasDeSync;
+    }
+
+    /** Returns time used to read and process the schema and namespaces. */
+    public int getPrepareSchemaAndNamespacesLatencyMillis() {
+        return mPrepareSchemaAndNamespacesLatencyMillis;
+    }
+
+    /** Returns time used to read and process the visibility file. */
+    public int getPrepareVisibilityStoreLatencyMillis() {
+        return mPrepareVisibilityStoreLatencyMillis;
+    }
+
+    /** Returns overall time used for the native function call. */
+    public int getNativeLatencyMillis() {
+        return mNativeLatencyMillis;
+    }
+
+    /** Returns recovery cause for document store.
+     *
+     *  <p> Possible recovery causes for document store:
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_DATA_LOSS}
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+     */
+    @RecoveryCause
+    public int getDocumentStoreRecoveryCause() {
+        return mNativeDocumentStoreRecoveryCause;
+    }
+
+    /** Returns restoration cause for index store.
+     *
+     *  <p> Possible causes:
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_INCONSISTENT_WITH_GROUND_TRUTH}
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
+     *      <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+     */
+    @RecoveryCause
+    public int getIndexRestorationCause() {
+        return mNativeIndexRestorationCause;
+    }
+
+    /** Returns recovery cause for schema store.
+     *
+     *  <p> Possible causes:
+     *      <li> IO_ERROR
+     */
+    @RecoveryCause
+    public int getSchemaStoreRecoveryCause() {
+        return mNativeSchemaStoreRecoveryCause;
+    }
+
+    /** Returns time used to recover the document store. */
+    public int getDocumentStoreRecoveryLatencyMillis() {
+        return mNativeDocumentStoreRecoveryLatencyMillis;
+    }
+
+    /** Returns time used to restore the index. */
+    public int getIndexRestorationLatencyMillis() {
+        return mNativeIndexRestorationLatencyMillis;
+    }
+
+    /** Returns time used to recover the schema store. */
+    public int getSchemaStoreRecoveryLatencyMillis() {
+        return mNativeSchemaStoreRecoveryLatencyMillis;
+    }
+
+    /** Returns status about how much data is lost during the initialization. */
+    @DocumentStoreDataStatus
+    public int getDocumentStoreDataStatus() {
+        return mNativeDocumentStoreDataStatus;
+    }
+
+    /**
+     * Returns number of documents currently in document store. Those may include alive, deleted,
+     * and expired documents.
+     */
+    public int getDocumentCount() {
+        return mNativeNumDocuments;
+    }
+
+    /** Returns number of schema types currently in the schema store. */
+    public int getSchemaTypeCount() {
+        return mNativeNumSchemaTypes;
+    }
+
+    InitializeStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mStatusCode = builder.mStatusCode;
+        mTotalLatencyMillis = builder.mTotalLatencyMillis;
+        mHasDeSync = builder.mHasDeSync;
+        mPrepareSchemaAndNamespacesLatencyMillis = builder.mPrepareSchemaAndNamespacesLatencyMillis;
+        mPrepareVisibilityStoreLatencyMillis = builder.mPrepareVisibilityStoreLatencyMillis;
+        mNativeLatencyMillis = builder.mNativeLatencyMillis;
+        mNativeDocumentStoreRecoveryCause = builder.mNativeDocumentStoreRecoveryCause;
+        mNativeIndexRestorationCause = builder.mNativeIndexRestorationCause;
+        mNativeSchemaStoreRecoveryCause = builder.mNativeSchemaStoreRecoveryCause;
+        mNativeDocumentStoreRecoveryLatencyMillis =
+                builder.mNativeDocumentStoreRecoveryLatencyMillis;
+        mNativeIndexRestorationLatencyMillis = builder.mNativeIndexRestorationLatencyMillis;
+        mNativeSchemaStoreRecoveryLatencyMillis = builder.mNativeSchemaStoreRecoveryLatencyMillis;
+        mNativeDocumentStoreDataStatus = builder.mNativeDocumentStoreDataStatus;
+        mNativeNumDocuments = builder.mNativeNumDocuments;
+        mNativeNumSchemaTypes = builder.mNativeNumSchemaTypes;
+    }
+
+    /** Builder for {@link InitializeStats}. */
+    public static class Builder {
+        @AppSearchResult.ResultCode
+        int mStatusCode;
+        int mTotalLatencyMillis;
+        boolean mHasDeSync;
+        int mPrepareSchemaAndNamespacesLatencyMillis;
+        int mPrepareVisibilityStoreLatencyMillis;
+        int mNativeLatencyMillis;
+        @RecoveryCause
+        int mNativeDocumentStoreRecoveryCause;
+        @RecoveryCause
+        int mNativeIndexRestorationCause;
+        @RecoveryCause
+        int mNativeSchemaStoreRecoveryCause;
+        int mNativeDocumentStoreRecoveryLatencyMillis;
+        int mNativeIndexRestorationLatencyMillis;
+        int mNativeSchemaStoreRecoveryLatencyMillis;
+        @DocumentStoreDataStatus
+        int mNativeDocumentStoreDataStatus;
+        int mNativeNumDocuments;
+        int mNativeNumSchemaTypes;
+
+        /** Sets the status of the initialization. */
+        @NonNull
+        public Builder setStatusCode(@AppSearchResult.ResultCode int statusCode) {
+            mStatusCode = statusCode;
+            return this;
+        }
+
+        /** Sets the total latency of the initialization in milliseconds. */
+        @NonNull
+        public Builder setTotalLatencyMillis(int totalLatencyMillis) {
+            mTotalLatencyMillis = totalLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets whether the initialize() detects deSync.
+         *
+         * <p>If there is a deSync, it means AppSearch and IcingSearchEngine have an inconsistent
+         * view of what data should exist.
+         */
+        @NonNull
+        public Builder setHasDeSync(boolean hasDeSync) {
+            mHasDeSync = hasDeSync;
+            return this;
+        }
+
+        /** Sets time used to read and process the schema and namespaces. */
+        @NonNull
+        public Builder setPrepareSchemaAndNamespacesLatencyMillis(
+                int prepareSchemaAndNamespacesLatencyMillis) {
+            mPrepareSchemaAndNamespacesLatencyMillis = prepareSchemaAndNamespacesLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to read and process the visibility file. */
+        @NonNull
+        public Builder setPrepareVisibilityStoreLatencyMillis(
+                int prepareVisibilityStoreLatencyMillis) {
+            mPrepareVisibilityStoreLatencyMillis = prepareVisibilityStoreLatencyMillis;
+            return this;
+        }
+
+        /** Sets overall time used for the native function call. */
+        @NonNull
+        public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
+            mNativeLatencyMillis = nativeLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets recovery cause for document store.
+         *
+         * <p> Possible recovery causes for document store:
+         * <li> {@link InitializeStats#RECOVERY_CAUSE_DATA_LOSS}
+         * <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
+         * <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+         */
+        @NonNull
+        public Builder setDocumentStoreRecoveryCause(
+                @RecoveryCause int documentStoreRecoveryCause) {
+            mNativeDocumentStoreRecoveryCause = documentStoreRecoveryCause;
+            return this;
+        }
+
+        /** Sets restoration cause for index store.
+         *
+         *  <p> Possible causes:
+         *      <li> {@link InitializeStats#DOCUMENT_STORE_DATA_STATUS_COMPLETE_LOSS}
+         *      <li> {@link InitializeStats#RECOVERY_CAUSE_TOTAL_CHECKSUM_MISMATCH}
+         *      <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+         */
+        @NonNull
+        public Builder setIndexRestorationCause(
+                @RecoveryCause int indexRestorationCause) {
+            mNativeIndexRestorationCause = indexRestorationCause;
+            return this;
+        }
+
+        /** Returns recovery cause for schema store.
+         *
+         *  <p> Possible causes:
+         *      <li> {@link InitializeStats#RECOVERY_CAUSE_IO_ERROR}
+         */
+        @NonNull
+        public Builder setSchemaStoreRecoveryCause(
+                @RecoveryCause int schemaStoreRecoveryCause) {
+            mNativeSchemaStoreRecoveryCause = schemaStoreRecoveryCause;
+            return this;
+        }
+
+        /** Sets time used to recover the document store. */
+        @NonNull
+        public Builder setDocumentStoreRecoveryLatencyMillis(
+                int documentStoreRecoveryLatencyMillis) {
+            mNativeDocumentStoreRecoveryLatencyMillis = documentStoreRecoveryLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to restore the index. */
+        @NonNull
+        public Builder setIndexRestorationLatencyMillis(
+                int indexRestorationLatencyMillis) {
+            mNativeIndexRestorationLatencyMillis = indexRestorationLatencyMillis;
+            return this;
+        }
+
+        /** Sets time used to recover the schema store. */
+        @NonNull
+        public Builder setSchemaStoreRecoveryLatencyMillis(
+                int schemaStoreRecoveryLatencyMillis) {
+            mNativeSchemaStoreRecoveryLatencyMillis = schemaStoreRecoveryLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets Native Document Store Data status.
+         * status is defined in external/icing/proto/icing/proto/logging.proto
+         */
+        @NonNull
+        public Builder setDocumentStoreDataStatus(
+                @DocumentStoreDataStatus int documentStoreDataStatus) {
+            mNativeDocumentStoreDataStatus = documentStoreDataStatus;
+            return this;
+        }
+
+        /**
+         * Sets number of documents currently in document store. Those may include alive, deleted,
+         * and expired documents.
+         */
+        @NonNull
+        public Builder setDocumentCount(int numDocuments) {
+            mNativeNumDocuments = numDocuments;
+            return this;
+        }
+
+        /** Sets number of schema types currently in the schema store. */
+        @NonNull
+        public Builder setSchemaTypeCount(int numSchemaTypes) {
+            mNativeNumSchemaTypes = numSchemaTypes;
+            return this;
+        }
+
+        /**
+         * Constructs a new {@link InitializeStats} from the contents of this
+         * {@link InitializeStats.Builder}
+         */
+        @NonNull
+        public InitializeStats build() {
+            return new InitializeStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
new file mode 100644
index 0000000..6667d92
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/stats/PutDocumentStats.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * A class for holding detailed stats to log for each individual document put by a
+ * {@link androidx.appsearch.app.AppSearchSession#put} call.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class PutDocumentStats {
+    /** {@link GeneralStats} holds the general stats. */
+    @NonNull
+    private final GeneralStats mGeneralStats;
+
+    /** Time used to generate a document proto from a Bundle. */
+    private final int mGenerateDocumentProtoLatencyMillis;
+
+    /** Time used to rewrite types and namespaces in the document. */
+    private final int mRewriteDocumentTypesLatencyMillis;
+
+    /** Overall time used for the native function call. */
+    private final int mNativeLatencyMillis;
+
+    /** Time used to store the document. */
+    private final int mNativeDocumentStoreLatencyMillis;
+
+    /** Time used to index the document. It doesn't include the time to merge indices. */
+    private final int mNativeIndexLatencyMillis;
+
+    /** Time used to merge the indices. */
+    private final int mNativeIndexMergeLatencyMillis;
+
+    /** Document size in bytes. */
+    private final int mNativeDocumentSizeBytes;
+
+    /** Number of tokens added to the index. */
+    private final int mNativeNumTokensIndexed;
+
+    /**
+     * Whether the number of tokens to be indexed exceeded the max number of tokens per
+     * document.
+     */
+    private final boolean mNativeExceededMaxNumTokens;
+
+    PutDocumentStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mGeneralStats = Preconditions.checkNotNull(builder.mGeneralStatsBuilder).build();
+        mGenerateDocumentProtoLatencyMillis = builder.mGenerateDocumentProtoLatencyMillis;
+        mRewriteDocumentTypesLatencyMillis = builder.mRewriteDocumentTypesLatencyMillis;
+        mNativeLatencyMillis = builder.mNativeLatencyMillis;
+        mNativeDocumentStoreLatencyMillis = builder.mNativeDocumentStoreLatencyMillis;
+        mNativeIndexLatencyMillis = builder.mNativeIndexLatencyMillis;
+        mNativeIndexMergeLatencyMillis = builder.mNativeIndexMergeLatencyMillis;
+        mNativeDocumentSizeBytes = builder.mNativeDocumentSizeBytes;
+        mNativeNumTokensIndexed = builder.mNativeNumTokensIndexed;
+        mNativeExceededMaxNumTokens = builder.mNativeExceededMaxNumTokens;
+    }
+
+    /**
+     * Returns the {@link GeneralStats} object attached to this instance.
+     */
+    @NonNull
+    public GeneralStats getGeneralStats() {
+        return mGeneralStats;
+    }
+
+    /** Returns time spent on generating document proto, in milliseconds. */
+    public int getGenerateDocumentProtoLatencyMillis() {
+        return mGenerateDocumentProtoLatencyMillis;
+    }
+
+    /** Returns time spent on rewriting types and namespaces in document, in milliseconds. */
+    public int getRewriteDocumentTypesLatencyMillis() {
+        return mRewriteDocumentTypesLatencyMillis;
+    }
+
+    /** Returns time spent in native, in milliseconds. */
+    public int getNativeLatencyMillis() {
+        return mNativeLatencyMillis;
+    }
+
+    /** Returns time spent on document store, in milliseconds. */
+    public int getNativeDocumentStoreLatencyMillis() {
+        return mNativeDocumentStoreLatencyMillis;
+    }
+
+    /** Returns time spent on indexing, in milliseconds. */
+    public int getNativeIndexLatencyMillis() {
+        return mNativeIndexLatencyMillis;
+    }
+
+    /** Returns time spent on merging indices, in milliseconds. */
+    public int getNativeIndexMergeLatencyMillis() {
+        return mNativeIndexMergeLatencyMillis;
+    }
+
+    /** Returns document size, in bytes. */
+    public int getNativeDocumentSizeBytes() {
+        return mNativeDocumentSizeBytes;
+    }
+
+    /** Returns number of tokens indexed. */
+    public int getNativeNumTokensIndexed() {
+        return mNativeNumTokensIndexed;
+    }
+
+    /**
+     * Returns whether the number of tokens to be indexed exceeded the max number of tokens per
+     * document.
+     */
+    public boolean getNativeExceededMaxNumTokens() {
+        return mNativeExceededMaxNumTokens;
+    }
+
+    /** Builder for {@link PutDocumentStats}. */
+    public static class Builder {
+        @NonNull
+        final GeneralStats.Builder mGeneralStatsBuilder;
+        int mGenerateDocumentProtoLatencyMillis;
+        int mRewriteDocumentTypesLatencyMillis;
+        int mNativeLatencyMillis;
+        int mNativeDocumentStoreLatencyMillis;
+        int mNativeIndexLatencyMillis;
+        int mNativeIndexMergeLatencyMillis;
+        int mNativeDocumentSizeBytes;
+        int mNativeNumTokensIndexed;
+        boolean mNativeExceededMaxNumTokens;
+
+        /** Builder takes {@link GeneralStats.Builder}. */
+        public Builder(@NonNull String packageName, @NonNull String database) {
+            Preconditions.checkNotNull(packageName);
+            Preconditions.checkNotNull(database);
+            mGeneralStatsBuilder = new GeneralStats.Builder(packageName, database);
+        }
+
+        /** Returns {@link GeneralStats.Builder}. */
+        @NonNull
+        public GeneralStats.Builder getGeneralStatsBuilder() {
+            return mGeneralStatsBuilder;
+        }
+
+        /** Sets how much time we spend for generating document proto, in milliseconds. */
+        @NonNull
+        public Builder setGenerateDocumentProtoLatencyMillis(
+                int generateDocumentProtoLatencyMillis) {
+            mGenerateDocumentProtoLatencyMillis = generateDocumentProtoLatencyMillis;
+            return this;
+        }
+
+        /**
+         * Sets how much time we spend for rewriting types and namespaces in document, in
+         * milliseconds.
+         */
+        @NonNull
+        public Builder setRewriteDocumentTypesLatencyMillis(int rewriteDocumentTypesLatencyMillis) {
+            mRewriteDocumentTypesLatencyMillis = rewriteDocumentTypesLatencyMillis;
+            return this;
+        }
+
+        /** Sets the native latency, in milliseconds. */
+        @NonNull
+        public Builder setNativeLatencyMillis(int nativeLatencyMillis) {
+            mNativeLatencyMillis = nativeLatencyMillis;
+            return this;
+        }
+
+        /** Sets how much time we spend on document store, in milliseconds. */
+        @NonNull
+        public Builder setNativeDocumentStoreLatencyMillis(int nativeDocumentStoreLatencyMillis) {
+            mNativeDocumentStoreLatencyMillis = nativeDocumentStoreLatencyMillis;
+            return this;
+        }
+
+        /** Sets the native index latency, in milliseconds. */
+        @NonNull
+        public Builder setNativeIndexLatencyMillis(int nativeIndexLatencyMillis) {
+            mNativeIndexLatencyMillis = nativeIndexLatencyMillis;
+            return this;
+        }
+
+        /** Sets how much time we spend on merging indices, in milliseconds. */
+        @NonNull
+        public Builder setNativeIndexMergeLatencyMillis(int nativeIndexMergeLatencyMillis) {
+            mNativeIndexMergeLatencyMillis = nativeIndexMergeLatencyMillis;
+            return this;
+        }
+
+        /** Sets document size, in bytes. */
+        @NonNull
+        public Builder setNativeDocumentSizeBytes(int nativeDocumentSizeBytes) {
+            mNativeDocumentSizeBytes = nativeDocumentSizeBytes;
+            return this;
+        }
+
+        /** Sets number of tokens indexed in native. */
+        @NonNull
+        public Builder setNativeNumTokensIndexed(int nativeNumTokensIndexed) {
+            mNativeNumTokensIndexed = nativeNumTokensIndexed;
+            return this;
+        }
+
+        /**
+         * Sets whether the number of tokens to be indexed exceeded the max number of tokens per
+         * document.
+         */
+        @NonNull
+        public Builder setNativeExceededMaxNumTokens(boolean nativeExceededMaxNumTokens) {
+            mNativeExceededMaxNumTokens = nativeExceededMaxNumTokens;
+            return this;
+        }
+
+        /**
+         * Creates a new {@link PutDocumentStats} object from the contents of this
+         * {@link Builder} instance.
+         */
+        @NonNull
+        public PutDocumentStats build() {
+            return new PutDocumentStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/FutureUtil.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/FutureUtil.java
index 99f5ae1..f22ceb0 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/FutureUtil.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/FutureUtil.java
@@ -24,7 +24,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executor;
 
 /**
  * Utilities for working with {@link com.google.common.util.concurrent.ListenableFuture}.
@@ -37,7 +37,7 @@
     /** Executes the given lambda on the given executor and returns a {@link ListenableFuture}. */
     @NonNull
     public static <T> ListenableFuture<T> execute(
-            @NonNull ExecutorService executor,
+            @NonNull Executor executor,
             @NonNull Callable<T> callable) {
         Preconditions.checkNotNull(executor);
         Preconditions.checkNotNull(callable);
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
new file mode 100644
index 0000000..590286b
--- /dev/null
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.localstorage.util;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.exceptions.AppSearchException;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.PropertyProto;
+
+/**
+ * Provides utility functions for working with package + database prefixes.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class PrefixUtil {
+    private static final String TAG = "AppSearchPrefixUtil";
+
+    @VisibleForTesting
+    public static final char DATABASE_DELIMITER = '/';
+
+    @VisibleForTesting
+    public static final char PACKAGE_DELIMITER = '$';
+
+    private PrefixUtil() {}
+
+    /**
+     * Creates prefix string for given package name and database name.
+     */
+    @NonNull
+    public static String createPrefix(@NonNull String packageName, @NonNull String databaseName) {
+        return packageName + PACKAGE_DELIMITER + databaseName + DATABASE_DELIMITER;
+    }
+    /**
+     * Creates prefix string for given package name.
+     */
+    @NonNull
+    public static String createPackagePrefix(@NonNull String packageName) {
+        return packageName + PACKAGE_DELIMITER;
+    }
+
+    /**
+     * Returns the package name that's contained within the {@code prefix}.
+     *
+     * @param prefix Prefix string that contains the package name inside of it. The package name
+     *               must be in the front of the string, and separated from the rest of the
+     *               string by the {@link #PACKAGE_DELIMITER}.
+     * @return Valid package name.
+     */
+    @NonNull
+    public static String getPackageName(@NonNull String prefix) {
+        int delimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
+        if (delimiterIndex == -1) {
+            // This should never happen if we construct our prefixes properly
+            Log.wtf(TAG, "Malformed prefix doesn't contain package delimiter: " + prefix);
+            return "";
+        }
+        return prefix.substring(0, delimiterIndex);
+    }
+
+    /**
+     * Returns the database name that's contained within the {@code prefix}.
+     *
+     * @param prefix Prefix string that contains the database name inside of it. The database name
+     *               must be between the {@link #PACKAGE_DELIMITER} and {@link #DATABASE_DELIMITER}
+     * @return Valid database name.
+     */
+    @NonNull
+    public static String getDatabaseName(@NonNull String prefix) {
+        // TODO (b/184050178) Start database delimiter index search from after package delimiter
+        int packageDelimiterIndex = prefix.indexOf(PACKAGE_DELIMITER);
+        int databaseDelimiterIndex = prefix.indexOf(DATABASE_DELIMITER);
+        if (packageDelimiterIndex == -1) {
+            // This should never happen if we construct our prefixes properly
+            Log.wtf(TAG, "Malformed prefix doesn't contain package delimiter: " + prefix);
+            return "";
+        }
+        if (databaseDelimiterIndex == -1) {
+            // This should never happen if we construct our prefixes properly
+            Log.wtf(TAG, "Malformed prefix doesn't contain database delimiter: " + prefix);
+            return "";
+        }
+        return prefix.substring(packageDelimiterIndex + 1, databaseDelimiterIndex);
+    }
+
+    /**
+     * Creates a string with the package and database prefix removed from the input string.
+     *
+     * @param prefixedString a string containing a package and database prefix.
+     * @return a string with the package and database prefix removed.
+     * @throws AppSearchException if the prefixed value does not contain a valid database name.
+     */
+    @NonNull
+    public static String removePrefix(@NonNull String prefixedString)
+            throws AppSearchException {
+        // The prefix is made up of the package, then the database. So we only need to find the
+        // database cutoff.
+        int delimiterIndex;
+        if ((delimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER)) != -1) {
+            // Add 1 to include the char size of the DATABASE_DELIMITER
+            return prefixedString.substring(delimiterIndex + 1);
+        }
+        throw new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
+                "The prefixed value doesn't contains a valid database name.");
+    }
+
+    /**
+     * Creates a package and database prefix string from the input string.
+     *
+     * @param prefixedString a string containing a package and database prefix.
+     * @return a string with the package and database prefix
+     * @throws AppSearchException if the prefixed value does not contain a valid database name.
+     */
+    @NonNull
+    public static String getPrefix(@NonNull String prefixedString) throws AppSearchException {
+        int databaseDelimiterIndex = prefixedString.indexOf(DATABASE_DELIMITER);
+        if (databaseDelimiterIndex == -1) {
+            throw new AppSearchException(AppSearchResult.RESULT_UNKNOWN_ERROR,
+                    "The databaseName prefixed value doesn't contain a valid database name.");
+        }
+
+        // Add 1 to include the char size of the DATABASE_DELIMITER
+        return prefixedString.substring(0, databaseDelimiterIndex + 1);
+    }
+
+    /**
+     * Prepends {@code prefix} to all types and namespaces mentioned anywhere in
+     * {@code documentBuilder}.
+     *
+     * @param documentBuilder The document to mutate
+     * @param prefix          The prefix to add
+     */
+    public static void addPrefixToDocument(
+            @NonNull DocumentProto.Builder documentBuilder,
+            @NonNull String prefix) {
+        // Rewrite the type name to include/remove the prefix.
+        String newSchema = prefix + documentBuilder.getSchema();
+        documentBuilder.setSchema(newSchema);
+
+        // Rewrite the namespace to include/remove the prefix.
+        documentBuilder.setNamespace(prefix + 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();
+                    addPrefixToDocument(derivedDocumentBuilder, prefix);
+                    propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
+                }
+                documentBuilder.setProperties(propertyIdx, propertyBuilder);
+            }
+        }
+    }
+
+    /**
+     * Removes any prefixes from types and namespaces mentioned anywhere in
+     * {@code documentBuilder}.
+     *
+     * @param documentBuilder The document to mutate
+     * @return Prefix name that was removed from the document.
+     * @throws AppSearchException if there are unexpected database prefixing errors.
+     */
+    @NonNull
+    public static String removePrefixesFromDocument(@NonNull DocumentProto.Builder documentBuilder)
+            throws AppSearchException {
+        // Rewrite the type name and namespace to remove the prefix.
+        String schemaPrefix = getPrefix(documentBuilder.getSchema());
+        String namespacePrefix = getPrefix(documentBuilder.getNamespace());
+
+        if (!schemaPrefix.equals(namespacePrefix)) {
+            throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, "Found unexpected"
+                    + " multiple prefix names in document: " + schemaPrefix + ", "
+                    + namespacePrefix);
+        }
+
+        documentBuilder.setSchema(removePrefix(documentBuilder.getSchema()));
+        documentBuilder.setNamespace(removePrefix(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();
+                    String nestedPrefix = removePrefixesFromDocument(derivedDocumentBuilder);
+                    if (!nestedPrefix.equals(schemaPrefix)) {
+                        throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR,
+                                "Found unexpected multiple prefix names in document: "
+                                        + schemaPrefix + ", " + nestedPrefix);
+                    }
+                    propertyBuilder.setDocumentValues(documentIdx, derivedDocumentBuilder);
+                }
+                documentBuilder.setProperties(propertyIdx, propertyBuilder);
+            }
+        }
+
+        return schemaPrefix;
+    }
+}
diff --git a/appsearch/platform-storage/api/current.txt b/appsearch/platform-storage/api/current.txt
new file mode 100644
index 0000000..881789d
--- /dev/null
+++ b/appsearch/platform-storage/api/current.txt
@@ -0,0 +1,31 @@
+// Signature format: 4.0
+package androidx.appsearch.platformstorage {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext {
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext.Builder {
+    ctor public PlatformStorage.GlobalSearchContext.Builder(android.content.Context);
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+  public static final class PlatformStorage.SearchContext {
+    method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.SearchContext.Builder {
+    ctor public PlatformStorage.SearchContext.Builder(android.content.Context, String);
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+}
+
diff --git a/appsearch/platform-storage/api/public_plus_experimental_current.txt b/appsearch/platform-storage/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..881789d
--- /dev/null
+++ b/appsearch/platform-storage/api/public_plus_experimental_current.txt
@@ -0,0 +1,31 @@
+// Signature format: 4.0
+package androidx.appsearch.platformstorage {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext {
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext.Builder {
+    ctor public PlatformStorage.GlobalSearchContext.Builder(android.content.Context);
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+  public static final class PlatformStorage.SearchContext {
+    method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.SearchContext.Builder {
+    ctor public PlatformStorage.SearchContext.Builder(android.content.Context, String);
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+}
+
diff --git a/appsearch/platform-storage/api/res-current.txt b/appsearch/platform-storage/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appsearch/platform-storage/api/res-current.txt
diff --git a/appsearch/platform-storage/api/restricted_current.txt b/appsearch/platform-storage/api/restricted_current.txt
new file mode 100644
index 0000000..881789d
--- /dev/null
+++ b/appsearch/platform-storage/api/restricted_current.txt
@@ -0,0 +1,31 @@
+// Signature format: 4.0
+package androidx.appsearch.platformstorage {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GlobalSearchSession!> createGlobalSearchSession(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchSession!> createSearchSession(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext {
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.GlobalSearchContext.Builder {
+    ctor public PlatformStorage.GlobalSearchContext.Builder(android.content.Context);
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+  public static final class PlatformStorage.SearchContext {
+    method public String getDatabaseName();
+    method public java.util.concurrent.Executor getWorkerExecutor();
+  }
+
+  public static final class PlatformStorage.SearchContext.Builder {
+    ctor public PlatformStorage.SearchContext.Builder(android.content.Context, String);
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext build();
+    method public androidx.appsearch.platformstorage.PlatformStorage.SearchContext.Builder setWorkerExecutor(java.util.concurrent.Executor);
+  }
+
+}
+
diff --git a/appsearch/platform-storage/build.gradle b/appsearch/platform-storage/build.gradle
new file mode 100644
index 0000000..e591be2
--- /dev/null
+++ b/appsearch/platform-storage/build.gradle
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    api("androidx.annotation:annotation:1.1.0")
+
+    implementation project(":appsearch:appsearch")
+    implementation("androidx.concurrent:concurrent-futures:1.0.0")
+    implementation("androidx.core:core:1.2.0")
+}
+
+androidx {
+    name = "AppSearch Platform Storage"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.APPSEARCH
+    mavenVersion = LibraryVersions.APPSEARCH
+    inceptionYear = "2021"
+    description =
+        "An implementation of AppSearchSession which uses the AppSearch service on Android S+"
+}
diff --git a/appsearch/platform-storage/lint-baseline.xml b/appsearch/platform-storage/lint-baseline.xml
new file mode 100644
index 0000000..8f1aa4b
--- /dev/null
+++ b/appsearch/platform-storage/lint-baseline.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+
+</issues>
diff --git a/appsearch/platform-storage/src/main/AndroidManifest.xml b/appsearch/platform-storage/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3cd15c8
--- /dev/null
+++ b/appsearch/platform-storage/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2021 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.
+-->
+
+<manifest package="androidx.appsearch.platformstorage"/>
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
new file mode 100644
index 0000000..7ea1490
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.app.ReportSystemUsageRequest;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+/**
+ * An implementation of {@link androidx.appsearch.app.GlobalSearchSession} which proxies to a
+ * platform {@link android.app.appsearch.GlobalSearchSession}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+class GlobalSearchSessionImpl implements GlobalSearchSession {
+    private final android.app.appsearch.GlobalSearchSession mPlatformSession;
+    private final Executor mExecutor;
+
+    GlobalSearchSessionImpl(
+            @NonNull android.app.appsearch.GlobalSearchSession platformSession,
+            @NonNull Executor executor) {
+        mPlatformSession = Preconditions.checkNotNull(platformSession);
+        mExecutor = Preconditions.checkNotNull(executor);
+    }
+
+    @Override
+    @NonNull
+    public SearchResults search(
+            @NonNull String queryExpression,
+            @NonNull SearchSpec searchSpec) {
+        Preconditions.checkNotNull(queryExpression);
+        Preconditions.checkNotNull(searchSpec);
+        android.app.appsearch.SearchResults platformSearchResults =
+                mPlatformSession.search(
+                        queryExpression,
+                        SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec));
+        return new SearchResultsImpl(platformSearchResults, mExecutor);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request) {
+        Preconditions.checkNotNull(request);
+        // TODO(b/183031844): Call system reportSystemUsage API when it's created
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void close() {
+        mPlatformSession.close();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
new file mode 100644
index 0000000..5fe37a2
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage;
+
+import android.app.appsearch.AppSearchManager;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.platformstorage.converter.SearchContextToPlatformConverter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * An AppSearch storage system which stores data in the central AppSearch service, available on
+ * Android S+.
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public final class PlatformStorage {
+
+    private PlatformStorage() {
+    }
+
+    /** Contains information about how to create the search session. */
+    public static final class SearchContext {
+        final Context mContext;
+        final String mDatabaseName;
+        final Executor mExecutor;
+
+        SearchContext(@NonNull Context context, @NonNull String databaseName,
+                @NonNull Executor executor) {
+            mContext = Preconditions.checkNotNull(context);
+            mDatabaseName = Preconditions.checkNotNull(databaseName);
+            mExecutor = Preconditions.checkNotNull(executor);
+        }
+
+        /**
+         * Returns the name of the database to create or open.
+         */
+        @NonNull
+        public String getDatabaseName() {
+            return mDatabaseName;
+        }
+
+        /**
+         * Returns the worker executor associated with {@link AppSearchSession}.
+         *
+         * <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
+         * be returned. You should never cast the executor to
+         * {@link java.util.concurrent.ExecutorService} and call
+         * {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
+         * since {@link Executor#execute} won't return anything, we will hang forever waiting for
+         * the execution.
+         */
+        @NonNull
+        public Executor getWorkerExecutor() {
+            return mExecutor;
+        }
+
+        /** Builder for {@link SearchContext} objects. */
+        public static final class Builder {
+            private final Context mContext;
+            private final String mDatabaseName;
+            private Executor mExecutor;
+            private boolean mBuilt = false;
+
+            /**
+             * Creates a {@link SearchContext.Builder} instance.
+             *
+             * <p>{@link AppSearchSession} will create or open a database under the given name.
+             *
+             * <p>Databases with different names are fully separate with distinct schema types,
+             * namespaces, and documents.
+             *
+             * <p>The database name cannot contain {@code '/'}.
+             *
+             * <p>The database name will be visible to all system UI or third-party applications
+             * that have been granted access to any of the database's documents (for example,
+             * using {@link
+             * androidx.appsearch.app.SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}).
+             *
+             * @param databaseName The name of the database.
+             * @throws IllegalArgumentException if the databaseName contains {@code '/'}.
+             */
+            public Builder(@NonNull Context context, @NonNull String databaseName) {
+                mContext = Preconditions.checkNotNull(context);
+                Preconditions.checkNotNull(databaseName);
+                if (databaseName.contains("/")) {
+                    throw new IllegalArgumentException("Database name cannot contain '/'");
+                }
+                mDatabaseName = databaseName;
+            }
+
+            /**
+             * Sets the worker executor associated with {@link AppSearchSession}.
+             *
+             * <p>If an executor is not provided, the AppSearch default executor will be used.
+             *
+             * @param executor the worker executor used to run heavy background tasks.
+             */
+            @NonNull
+            public Builder setWorkerExecutor(@NonNull Executor executor) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                mExecutor = Preconditions.checkNotNull(executor);
+                return this;
+            }
+
+            /** Builds a {@link SearchContext} instance. */
+            @NonNull
+            public SearchContext build() {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                if (mExecutor == null) {
+                    mExecutor = EXECUTOR;
+                }
+                mBuilt = true;
+                return new SearchContext(mContext, mDatabaseName, mExecutor);
+            }
+        }
+    }
+
+    /** Contains information relevant to creating a global search session. */
+    public static final class GlobalSearchContext {
+        final Context mContext;
+        final Executor mExecutor;
+
+        GlobalSearchContext(@NonNull Context context, @NonNull Executor executor) {
+            mContext = Preconditions.checkNotNull(context);
+            mExecutor = Preconditions.checkNotNull(executor);
+        }
+
+        /**
+         * Returns the worker executor associated with {@link GlobalSearchSession}.
+         *
+         * <p>If an executor is not provided to {@link Builder}, the AppSearch default executor will
+         * be returned. You should never cast the executor to
+         * {@link java.util.concurrent.ExecutorService} and call
+         * {@link ExecutorService#shutdownNow()}. It will cancel the futures it's returned. And
+         * since {@link Executor#execute} won't return anything, we will hang forever waiting for
+         * the execution.
+         */
+        @NonNull
+        public Executor getWorkerExecutor() {
+            return mExecutor;
+        }
+
+        /** Builder for {@link GlobalSearchContext} objects. */
+        public static final class Builder {
+            private final Context mContext;
+            private Executor mExecutor;
+            private boolean mBuilt = false;
+
+            public Builder(@NonNull Context context) {
+                mContext = Preconditions.checkNotNull(context);
+            }
+
+            /**
+             * Sets the worker executor associated with {@link GlobalSearchSession}.
+             *
+             * <p>If an executor is not provided, the AppSearch default executor will be used.
+             *
+             * @param executor the worker executor used to run heavy background tasks.
+             */
+            @NonNull
+            public Builder setWorkerExecutor(@NonNull Executor executor) {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                Preconditions.checkNotNull(executor);
+                mExecutor = executor;
+                return this;
+            }
+
+            /** Builds a {@link GlobalSearchContext} instance. */
+            @NonNull
+            public GlobalSearchContext build() {
+                Preconditions.checkState(!mBuilt, "Builder has already been used");
+                if (mExecutor == null) {
+                    mExecutor = EXECUTOR;
+                }
+
+                mBuilt = true;
+                return new GlobalSearchContext(mContext, mExecutor);
+            }
+        }
+    }
+
+    // Never call Executor.shutdownNow(), it will cancel the futures it's returned. And since
+    // execute() won't return anything, we will hang forever waiting for the execution.
+    // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
+    // mutate requests will need to gain write lock and query requests need to gain read lock.
+    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+
+    /**
+     * Opens a new {@link AppSearchSession} on this storage.
+     *
+     * @param context The {@link SearchContext} contains all information to create a new
+     *                {@link AppSearchSession}
+     */
+    @NonNull
+    public static ListenableFuture<AppSearchSession> createSearchSession(
+            @NonNull SearchContext context) {
+        Preconditions.checkNotNull(context);
+        AppSearchManager appSearchManager =
+                context.mContext.getSystemService(AppSearchManager.class);
+        ResolvableFuture<AppSearchSession> future = ResolvableFuture.create();
+        appSearchManager.createSearchSession(
+                SearchContextToPlatformConverter.toPlatformSearchContext(context),
+                context.mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        future.set(
+                                new SearchSessionImpl(result.getResultValue(), context.mExecutor));
+                    } else {
+                        future.setException(
+                                new AppSearchException(
+                                        result.getResultCode(), result.getErrorMessage()));
+                    }
+                });
+        return future;
+    }
+
+    /**
+     * Opens a new {@link GlobalSearchSession} on this storage.
+     */
+    @NonNull
+    public static ListenableFuture<GlobalSearchSession> createGlobalSearchSession(
+            @NonNull GlobalSearchContext context) {
+        Preconditions.checkNotNull(context);
+        AppSearchManager appSearchManager =
+                context.mContext.getSystemService(AppSearchManager.class);
+        ResolvableFuture<GlobalSearchSession> future = ResolvableFuture.create();
+        appSearchManager.createGlobalSearchSession(
+                context.mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        future.set(new GlobalSearchSessionImpl(
+                                result.getResultValue(), context.mExecutor));
+                    } else {
+                        future.setException(
+                                new AppSearchException(
+                                        result.getResultCode(), result.getErrorMessage()));
+                    }
+                });
+        return future;
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
new file mode 100644
index 0000000..52b15cf3
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchResultsImpl.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.platformstorage.converter.SearchResultToPlatformConverter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Platform implementation of {@link SearchResults} which proxies to the platform's
+ * {@link android.app.appsearch.SearchResults}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+class SearchResultsImpl implements SearchResults {
+    private final android.app.appsearch.SearchResults mPlatformResults;
+    private final Executor mExecutor;
+
+    SearchResultsImpl(
+            @NonNull android.app.appsearch.SearchResults platformResults,
+            @NonNull Executor executor) {
+        mPlatformResults = Preconditions.checkNotNull(platformResults);
+        mExecutor = Preconditions.checkNotNull(executor);
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<List<SearchResult>> getNextPage() {
+        ResolvableFuture<List<SearchResult>> future = ResolvableFuture.create();
+        mPlatformResults.getNextPage(mExecutor, result -> {
+            if (result.isSuccess()) {
+                List<android.app.appsearch.SearchResult> frameworkResults = result.getResultValue();
+                List<SearchResult> jetpackResults = new ArrayList<>(frameworkResults.size());
+                for (int i = 0; i < frameworkResults.size(); i++) {
+                    SearchResult jetpackResult =
+                            SearchResultToPlatformConverter.toJetpackSearchResult(
+                                    frameworkResults.get(i));
+                    jetpackResults.add(jetpackResult);
+                }
+                future.set(jetpackResults);
+            } else {
+                future.setException(
+                        new AppSearchException(result.getResultCode(), result.getErrorMessage()));
+            }
+        });
+        return future;
+    }
+
+    @Override
+    public void close() {
+        mPlatformResults.close();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
new file mode 100644
index 0000000..2ac7f44
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.app.StorageInfo;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.SchemaToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
+import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * An implementation of {@link AppSearchSession} which proxies to a platform
+ * {@link android.app.appsearch.AppSearchSession}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+class SearchSessionImpl implements AppSearchSession {
+    private final android.app.appsearch.AppSearchSession mPlatformSession;
+    private final Executor mExecutor;
+
+    SearchSessionImpl(
+            @NonNull android.app.appsearch.AppSearchSession platformSession,
+            @NonNull Executor executor) {
+        mPlatformSession = Preconditions.checkNotNull(platformSession);
+        mExecutor = Preconditions.checkNotNull(executor);
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<SetSchemaResponse> future = ResolvableFuture.create();
+        mPlatformSession.setSchema(
+                RequestToPlatformConverter.toPlatformSetSchemaRequest(request),
+                mExecutor,
+                mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        SetSchemaResponse jetpackResponse =
+                                RequestToPlatformConverter.toJetpackSetSchemaResponse(
+                                        result.getResultValue());
+                        future.set(jetpackResponse);
+                    } else {
+                        handleFailedPlatformResult(result, future);
+                    }
+                });
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<GetSchemaResponse> getSchema() {
+        ResolvableFuture<GetSchemaResponse> future = ResolvableFuture.create();
+        mPlatformSession.getSchema(
+                mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        android.app.appsearch.GetSchemaResponse platformGetResponse =
+                                result.getResultValue();
+                        GetSchemaResponse.Builder jetpackResponseBuilder =
+                                new GetSchemaResponse.Builder();
+                        for (android.app.appsearch.AppSearchSchema platformSchema :
+                                platformGetResponse.getSchemas()) {
+                            jetpackResponseBuilder.addSchema(
+                                    SchemaToPlatformConverter.toJetpackSchema(platformSchema));
+                        }
+                        jetpackResponseBuilder.setVersion(platformGetResponse.getVersion());
+                        future.set(jetpackResponseBuilder.build());
+                    } else {
+                        handleFailedPlatformResult(result, future);
+                    }
+                });
+        return future;
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Set<String>> getNamespaces() {
+        // TODO(b/183042276): Implement this once getNamespaces() is exposed in the platform SDK
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<AppSearchBatchResult<String, Void>> put(
+            @NonNull PutDocumentsRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<AppSearchBatchResult<String, Void>> future = ResolvableFuture.create();
+        mPlatformSession.put(
+                RequestToPlatformConverter.toPlatformPutDocumentsRequest(request),
+                mExecutor,
+                BatchResultCallbackAdapter.forSameValueType(future));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<AppSearchBatchResult<String, GenericDocument>> getByDocumentId(
+            @NonNull GetByDocumentIdRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<AppSearchBatchResult<String, GenericDocument>> future =
+                ResolvableFuture.create();
+        mPlatformSession.getByUri(
+                RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request),
+                mExecutor,
+                new BatchResultCallbackAdapter<>(
+                        future, GenericDocumentToPlatformConverter::toJetpackGenericDocument));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public SearchResults search(
+            @NonNull String queryExpression,
+            @NonNull SearchSpec searchSpec) {
+        Preconditions.checkNotNull(queryExpression);
+        Preconditions.checkNotNull(searchSpec);
+        android.app.appsearch.SearchResults platformSearchResults =
+                mPlatformSession.search(
+                        queryExpression,
+                        SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec));
+        return new SearchResultsImpl(platformSearchResults, mExecutor);
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<Void> reportUsage(@NonNull ReportUsageRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<Void> future = ResolvableFuture.create();
+        mPlatformSession.reportUsage(
+                RequestToPlatformConverter.toPlatformReportUsageRequest(request),
+                mExecutor,
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result, future));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest request) {
+        Preconditions.checkNotNull(request);
+        ResolvableFuture<AppSearchBatchResult<String, Void>> future = ResolvableFuture.create();
+        mPlatformSession.remove(
+                RequestToPlatformConverter.toPlatformRemoveByDocumentIdRequest(request),
+                mExecutor,
+                BatchResultCallbackAdapter.forSameValueType(future));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<Void> remove(
+            @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+        Preconditions.checkNotNull(queryExpression);
+        Preconditions.checkNotNull(searchSpec);
+        ResolvableFuture<Void> future = ResolvableFuture.create();
+        mPlatformSession.remove(
+                queryExpression,
+                SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec),
+                mExecutor,
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(
+                        result, future));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public ListenableFuture<StorageInfo> getStorageInfo() {
+        ResolvableFuture<StorageInfo> future = ResolvableFuture.create();
+        // TODO(b/182909475): Implement this if we decide to expose an API on platform.
+        future.set(new StorageInfo.Builder().build());
+        return future;
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Void> maybeFlush() {
+        ResolvableFuture<Void> future = ResolvableFuture.create();
+        // The data in platform will be flushed by scheduled task. This api won't do anything extra
+        // flush.
+        future.set(null);
+        return future;
+    }
+
+    @Override
+    public void close() {
+        mPlatformSession.close();
+    }
+
+    private void handleFailedPlatformResult(
+            @NonNull android.app.appsearch.AppSearchResult<?> platformResult,
+            @NonNull ResolvableFuture<?> future) {
+        future.setException(
+                new AppSearchException(
+                        platformResult.getResultCode(), platformResult.getErrorMessage()));
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
new file mode 100644
index 0000000..1510626
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/AppSearchResultToPlatformConverter.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Translates {@link androidx.appsearch.app.AppSearchResult} and
+ * {@link androidx.appsearch.app.AppSearchBatchResult} to platform versions.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class AppSearchResultToPlatformConverter {
+    private AppSearchResultToPlatformConverter() {}
+
+    /**
+     * Converts an {@link android.app.appsearch.AppSearchResult} into a jetpack
+     * {@link androidx.appsearch.app.AppSearchResult}.
+     */
+    @NonNull
+    public static <T> AppSearchResult<T> platformAppSearchResultToJetpack(
+            @NonNull android.app.appsearch.AppSearchResult<T> platformResult) {
+        Preconditions.checkNotNull(platformResult);
+        if (platformResult.isSuccess()) {
+            return AppSearchResult.newSuccessfulResult(platformResult.getResultValue());
+        }
+        return AppSearchResult.newFailedResult(
+                platformResult.getResultCode(), platformResult.getErrorMessage());
+    }
+
+    /**
+     * Uses the given {@link android.app.appsearch.AppSearchResult} to populate the given
+     * {@link ResolvableFuture}.
+     */
+    public static <T> void platformAppSearchResultToFuture(
+            @NonNull android.app.appsearch.AppSearchResult<T> platformResult,
+            @NonNull ResolvableFuture<T> future) {
+        Preconditions.checkNotNull(platformResult);
+        Preconditions.checkNotNull(future);
+        if (platformResult.isSuccess()) {
+            future.set(platformResult.getResultValue());
+        } else {
+            future.setException(
+                    new AppSearchException(
+                            platformResult.getResultCode(), platformResult.getErrorMessage()));
+        }
+    }
+
+    /**
+     * Converts the given platform {@link android.app.appsearch.AppSearchBatchResult} to a Jetpack
+     * {@link AppSearchBatchResult}.
+     *
+     * <p>Each value is translated using the provided {@code valueMapper} function.
+     */
+    @NonNull
+    public static <K, PlatformValue, JetpackValue> AppSearchBatchResult<K, JetpackValue>
+            platformAppSearchBatchResultToJetpack(
+            @NonNull android.app.appsearch.AppSearchBatchResult<K, PlatformValue> platformResult,
+            @NonNull Function<PlatformValue, JetpackValue> valueMapper) {
+        Preconditions.checkNotNull(platformResult);
+        Preconditions.checkNotNull(valueMapper);
+        AppSearchBatchResult.Builder<K, JetpackValue> jetpackResult =
+                new AppSearchBatchResult.Builder<>();
+        for (Map.Entry<K, PlatformValue> success : platformResult.getSuccesses().entrySet()) {
+            JetpackValue jetpackValue = valueMapper.apply(success.getValue());
+            jetpackResult.setSuccess(success.getKey(), jetpackValue);
+        }
+        for (Map.Entry<K, android.app.appsearch.AppSearchResult<PlatformValue>> failure :
+                platformResult.getFailures().entrySet()) {
+            jetpackResult.setFailure(
+                    failure.getKey(),
+                    failure.getValue().getResultCode(),
+                    failure.getValue().getErrorMessage());
+        }
+        return jetpackResult.build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
new file mode 100644
index 0000000..e79bd5b
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link GenericDocument}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class GenericDocumentToPlatformConverter {
+    /**
+     * Translates a jetpack {@link androidx.appsearch.app.GenericDocument} into a platform
+     * {@link android.app.appsearch.GenericDocument}.
+     */
+    @NonNull
+    public static android.app.appsearch.GenericDocument toPlatformGenericDocument(
+            @NonNull GenericDocument jetpackDocument) {
+        Preconditions.checkNotNull(jetpackDocument);
+        android.app.appsearch.GenericDocument.Builder<
+                android.app.appsearch.GenericDocument.Builder<?>> platformBuilder =
+                new android.app.appsearch.GenericDocument.Builder<>(
+                        jetpackDocument.getNamespace(),
+                        jetpackDocument.getId(),
+                        jetpackDocument.getSchemaType());
+        platformBuilder
+                .setScore(jetpackDocument.getScore())
+                .setTtlMillis(jetpackDocument.getTtlMillis())
+                .setCreationTimestampMillis(jetpackDocument.getCreationTimestampMillis());
+        for (String propertyName : jetpackDocument.getPropertyNames()) {
+            Object property = jetpackDocument.getProperty(propertyName);
+            if (property instanceof String[]) {
+                platformBuilder.setPropertyString(propertyName, (String[]) property);
+            } else if (property instanceof long[]) {
+                platformBuilder.setPropertyLong(propertyName, (long[]) property);
+            } else if (property instanceof double[]) {
+                platformBuilder.setPropertyDouble(propertyName, (double[]) property);
+            } else if (property instanceof boolean[]) {
+                platformBuilder.setPropertyBoolean(propertyName, (boolean[]) property);
+            } else if (property instanceof byte[][]) {
+                platformBuilder.setPropertyBytes(propertyName, (byte[][]) property);
+            } else if (property instanceof GenericDocument[]) {
+                GenericDocument[] documentValues = (GenericDocument[]) property;
+                android.app.appsearch.GenericDocument[] platformSubDocuments =
+                        new android.app.appsearch.GenericDocument[documentValues.length];
+                for (int j = 0; j < documentValues.length; j++) {
+                    platformSubDocuments[j] = toPlatformGenericDocument(documentValues[j]);
+                }
+                platformBuilder.setPropertyDocument(propertyName, platformSubDocuments);
+            } else {
+                throw new IllegalStateException(
+                        String.format("Property \"%s\" has unsupported value type %s", propertyName,
+                                property.getClass().toString()));
+            }
+        }
+        return platformBuilder.build();
+    }
+
+    /**
+     * Translates a platform {@link android.app.appsearch.GenericDocument} into a jetpack
+     * {@link androidx.appsearch.app.GenericDocument}.
+     */
+    @NonNull
+    public static GenericDocument toJetpackGenericDocument(
+            @NonNull android.app.appsearch.GenericDocument platformDocument) {
+        Preconditions.checkNotNull(platformDocument);
+        GenericDocument.Builder<GenericDocument.Builder<?>> jetpackBuilder =
+                new GenericDocument.Builder<>(
+                        platformDocument.getNamespace(), platformDocument.getUri(),
+                        platformDocument.getSchemaType());
+        jetpackBuilder
+                .setScore(platformDocument.getScore())
+                .setTtlMillis(platformDocument.getTtlMillis())
+                .setCreationTimestampMillis(platformDocument.getCreationTimestampMillis());
+        for (String propertyName : platformDocument.getPropertyNames()) {
+            Object property = platformDocument.getProperty(propertyName);
+            if (property instanceof String[]) {
+                jetpackBuilder.setPropertyString(propertyName, (String[]) property);
+            } else if (property instanceof long[]) {
+                jetpackBuilder.setPropertyLong(propertyName, (long[]) property);
+            } else if (property instanceof double[]) {
+                jetpackBuilder.setPropertyDouble(propertyName, (double[]) property);
+            } else if (property instanceof boolean[]) {
+                jetpackBuilder.setPropertyBoolean(propertyName, (boolean[]) property);
+            } else if (property instanceof byte[][]) {
+                jetpackBuilder.setPropertyBytes(propertyName, (byte[][]) property);
+            } else if (property instanceof android.app.appsearch.GenericDocument[]) {
+                android.app.appsearch.GenericDocument[] documentValues =
+                        (android.app.appsearch.GenericDocument[]) property;
+                GenericDocument[] jetpackSubDocuments = new GenericDocument[documentValues.length];
+                for (int j = 0; j < documentValues.length; j++) {
+                    jetpackSubDocuments[j] = toJetpackGenericDocument(documentValues[j]);
+                }
+                jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments);
+            } else {
+                throw new IllegalStateException(
+                        String.format("Property \"%s\" has unsupported value type %s", propertyName,
+                                property.getClass().toString()));
+            }
+        }
+        return jetpackBuilder.build();
+    }
+
+    private GenericDocumentToPlatformConverter() {}
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
new file mode 100644
index 0000000..c9bb9a3
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
+import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Translates between Platform and Jetpack versions of requests.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class RequestToPlatformConverter {
+    private RequestToPlatformConverter() {}
+
+    /**
+     * Translates a jetpack {@link androidx.appsearch.app.SetSchemaRequest} into a platform
+     * {@link android.app.appsearch.SetSchemaRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.SetSchemaRequest toPlatformSetSchemaRequest(
+            @NonNull SetSchemaRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        android.app.appsearch.SetSchemaRequest.Builder platformBuilder =
+                new android.app.appsearch.SetSchemaRequest.Builder();
+        for (AppSearchSchema jetpackSchema : jetpackRequest.getSchemas()) {
+            platformBuilder.addSchemas(SchemaToPlatformConverter.toPlatformSchema(jetpackSchema));
+        }
+        for (String schemaNotDisplayedBySystem : jetpackRequest.getSchemasNotDisplayedBySystem()) {
+            platformBuilder.setSchemaTypeDisplayedBySystem(
+                    schemaNotDisplayedBySystem, /*displayed=*/ false);
+        }
+        for (Map.Entry<String, Set<PackageIdentifier>> jetpackSchemaVisibleToPackage :
+                jetpackRequest.getSchemasVisibleToPackagesInternal().entrySet()) {
+            for (PackageIdentifier jetpackPackageIdentifier :
+                    jetpackSchemaVisibleToPackage.getValue()) {
+                platformBuilder.setSchemaTypeVisibilityForPackage(
+                        jetpackSchemaVisibleToPackage.getKey(),
+                        /*visible=*/ true,
+                        new android.app.appsearch.PackageIdentifier(
+                                jetpackPackageIdentifier.getPackageName(),
+                                jetpackPackageIdentifier.getSha256Certificate()));
+            }
+        }
+        platformBuilder.setForceOverride(jetpackRequest.isForceOverride());
+        return platformBuilder.build();
+    }
+
+    /**
+     * Translates a platform {@link android.app.appsearch.SetSchemaResponse} into a jetpack
+     * {@link androidx.appsearch.app.SetSchemaResponse}.
+     */
+    @NonNull
+    public static SetSchemaResponse toJetpackSetSchemaResponse(
+            @NonNull android.app.appsearch.SetSchemaResponse platformResponse) {
+        Preconditions.checkNotNull(platformResponse);
+        SetSchemaResponse.Builder jetpackBuilder = new SetSchemaResponse.Builder()
+                .addDeletedTypes(platformResponse.getDeletedTypes())
+                .addIncompatibleTypes(platformResponse.getIncompatibleTypes())
+                .addMigratedTypes(platformResponse.getMigratedTypes());
+        for (android.app.appsearch.SetSchemaResponse.MigrationFailure migrationFailure :
+                platformResponse.getMigrationFailures()) {
+            jetpackBuilder.addMigrationFailure(new SetSchemaResponse.MigrationFailure(
+                    migrationFailure.getNamespace(),
+                    migrationFailure.getUri(),
+                    migrationFailure.getSchemaType(),
+                    AppSearchResultToPlatformConverter.platformAppSearchResultToJetpack(
+                            migrationFailure.getAppSearchResult())));
+        }
+        return jetpackBuilder.build();
+    }
+
+    /**
+     * Translates a jetpack {@link PutDocumentsRequest} into a platform
+     * {@link android.app.appsearch.PutDocumentsRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.PutDocumentsRequest toPlatformPutDocumentsRequest(
+            @NonNull PutDocumentsRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        android.app.appsearch.PutDocumentsRequest.Builder platformBuilder =
+                new android.app.appsearch.PutDocumentsRequest.Builder();
+        for (GenericDocument jetpackDocument : jetpackRequest.getGenericDocuments()) {
+            platformBuilder.addGenericDocuments(
+                    GenericDocumentToPlatformConverter.toPlatformGenericDocument(jetpackDocument));
+        }
+        return platformBuilder.build();
+    }
+
+    /**
+     * Translates a jetpack {@link GetByDocumentIdRequest} into a platform
+     * {@link android.app.appsearch.GetByUriRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.GetByUriRequest toPlatformGetByDocumentIdRequest(
+            @NonNull GetByDocumentIdRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        android.app.appsearch.GetByUriRequest.Builder platformBuilder =
+                new android.app.appsearch.GetByUriRequest.Builder(jetpackRequest.getNamespace())
+                        .addUris(jetpackRequest.getIds());
+        for (Map.Entry<String, List<String>> projection :
+                jetpackRequest.getProjectionsInternal().entrySet()) {
+            platformBuilder.addProjection(projection.getKey(), projection.getValue());
+        }
+        return platformBuilder.build();
+    }
+
+    /**
+     * Translates a jetpack {@link RemoveByDocumentIdRequest} into a platform
+     * {@link android.app.appsearch.RemoveByUriRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.RemoveByUriRequest toPlatformRemoveByDocumentIdRequest(
+            @NonNull RemoveByDocumentIdRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        return new android.app.appsearch.RemoveByUriRequest.Builder(jetpackRequest.getNamespace())
+                .addUris(jetpackRequest.getIds())
+                .build();
+    }
+
+    /**
+     * Translates a jetpack {@link androidx.appsearch.app.ReportUsageRequest} into a platform
+     * {@link android.app.appsearch.ReportUsageRequest}.
+     */
+    @NonNull
+    public static android.app.appsearch.ReportUsageRequest toPlatformReportUsageRequest(
+            @NonNull ReportUsageRequest jetpackRequest) {
+        Preconditions.checkNotNull(jetpackRequest);
+        return new android.app.appsearch.ReportUsageRequest.Builder(jetpackRequest.getNamespace())
+                .setUri(jetpackRequest.getDocumentId())
+                .setUsageTimeMillis(jetpackRequest.getUsageTimestampMillis())
+                .build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
new file mode 100644
index 0000000..b78e22d
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Translates a jetpack {@link androidx.appsearch.app.AppSearchSchema} into a platform
+ * {@link android.app.appsearch.AppSearchSchema}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class SchemaToPlatformConverter {
+    private SchemaToPlatformConverter() {}
+
+    /**
+     * Translates a jetpack {@link androidx.appsearch.app.AppSearchSchema} into a platform
+     * {@link android.app.appsearch.AppSearchSchema}.
+     */
+    @NonNull
+    public static android.app.appsearch.AppSearchSchema toPlatformSchema(
+            @NonNull AppSearchSchema jetpackSchema) {
+        Preconditions.checkNotNull(jetpackSchema);
+        android.app.appsearch.AppSearchSchema.Builder platformBuilder =
+                new android.app.appsearch.AppSearchSchema.Builder(jetpackSchema.getSchemaType());
+        List<AppSearchSchema.PropertyConfig> properties = jetpackSchema.getProperties();
+        for (int i = 0; i < properties.size(); i++) {
+            android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty =
+                    toPlatformProperty(properties.get(i));
+            platformBuilder.addProperty(platformProperty);
+        }
+        return platformBuilder.build();
+    }
+
+    /**
+     * Translates a platform {@link android.app.appsearch.AppSearchSchema} to a jetpack
+     * {@link androidx.appsearch.app.AppSearchSchema}.
+     */
+    @NonNull
+    public static AppSearchSchema toJetpackSchema(
+            @NonNull android.app.appsearch.AppSearchSchema platformSchema) {
+        Preconditions.checkNotNull(platformSchema);
+        AppSearchSchema.Builder jetpackBuilder =
+                new AppSearchSchema.Builder(platformSchema.getSchemaType());
+        List<android.app.appsearch.AppSearchSchema.PropertyConfig> properties =
+                platformSchema.getProperties();
+        for (int i = 0; i < properties.size(); i++) {
+            AppSearchSchema.PropertyConfig jetpackProperty = toJetpackProperty(properties.get(i));
+            jetpackBuilder.addProperty(jetpackProperty);
+        }
+        return jetpackBuilder.build();
+    }
+
+    @NonNull
+    private static android.app.appsearch.AppSearchSchema.PropertyConfig toPlatformProperty(
+            @NonNull AppSearchSchema.PropertyConfig jetpackProperty) {
+        Preconditions.checkNotNull(jetpackProperty);
+        if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) {
+            AppSearchSchema.StringPropertyConfig stringProperty =
+                    (AppSearchSchema.StringPropertyConfig) jetpackProperty;
+            return new android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder(
+                    stringProperty.getName())
+                    .setCardinality(stringProperty.getCardinality())
+                    .setIndexingType(stringProperty.getIndexingType())
+                    .setTokenizerType(stringProperty.getTokenizerType())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.Int64PropertyConfig) {
+            return new android.app.appsearch.AppSearchSchema.Int64PropertyConfig.Builder(
+                    jetpackProperty.getName())
+                    .setCardinality(jetpackProperty.getCardinality())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.DoublePropertyConfig) {
+            return new android.app.appsearch.AppSearchSchema.DoublePropertyConfig.Builder(
+                    jetpackProperty.getName())
+                    .setCardinality(jetpackProperty.getCardinality())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.BooleanPropertyConfig) {
+            return new android.app.appsearch.AppSearchSchema.BooleanPropertyConfig.Builder(
+                    jetpackProperty.getName())
+                    .setCardinality(jetpackProperty.getCardinality())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.BytesPropertyConfig) {
+            return new android.app.appsearch.AppSearchSchema.BytesPropertyConfig.Builder(
+                    jetpackProperty.getName())
+                    .setCardinality(jetpackProperty.getCardinality())
+                    .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.DocumentPropertyConfig) {
+            AppSearchSchema.DocumentPropertyConfig documentProperty =
+                    (AppSearchSchema.DocumentPropertyConfig) jetpackProperty;
+            return new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(
+                    documentProperty.getName())
+                    .setCardinality(documentProperty.getCardinality())
+                    .setSchemaType(documentProperty.getSchemaType())
+                    .setIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
+                    .build();
+        } else {
+            throw new IllegalArgumentException(
+                    "Invalid dataType: " + jetpackProperty.getDataType());
+        }
+    }
+
+    @NonNull
+    private static AppSearchSchema.PropertyConfig toJetpackProperty(
+            @NonNull android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty) {
+        Preconditions.checkNotNull(platformProperty);
+        if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.StringPropertyConfig) {
+            android.app.appsearch.AppSearchSchema.StringPropertyConfig stringProperty =
+                    (android.app.appsearch.AppSearchSchema.StringPropertyConfig) platformProperty;
+            return new AppSearchSchema.StringPropertyConfig.Builder(stringProperty.getName())
+                    .setCardinality(stringProperty.getCardinality())
+                    .setIndexingType(stringProperty.getIndexingType())
+                    .setTokenizerType(stringProperty.getTokenizerType())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.Int64PropertyConfig) {
+            return new AppSearchSchema.Int64PropertyConfig.Builder(platformProperty.getName())
+                    .setCardinality(platformProperty.getCardinality())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.DoublePropertyConfig) {
+            return new AppSearchSchema.DoublePropertyConfig.Builder(platformProperty.getName())
+                    .setCardinality(platformProperty.getCardinality())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.BooleanPropertyConfig) {
+            return new AppSearchSchema.BooleanPropertyConfig.Builder(platformProperty.getName())
+                    .setCardinality(platformProperty.getCardinality())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.BytesPropertyConfig) {
+            return new AppSearchSchema.BytesPropertyConfig.Builder(platformProperty.getName())
+                    .setCardinality(platformProperty.getCardinality())
+                    .build();
+        } else if (platformProperty
+                instanceof android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) {
+            android.app.appsearch.AppSearchSchema.DocumentPropertyConfig documentProperty =
+                    (android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) platformProperty;
+            return new AppSearchSchema.DocumentPropertyConfig.Builder(
+                    documentProperty.getName(),
+                    documentProperty.getSchemaType())
+                    .setCardinality(documentProperty.getCardinality())
+                    .setShouldIndexNestedProperties(documentProperty.isIndexNestedProperties())
+                    .build();
+        } else {
+            throw new IllegalArgumentException(
+                    "Invalid property type " + platformProperty.getClass()
+                            + ": " + platformProperty);
+        }
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchContextToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchContextToPlatformConverter.java
new file mode 100644
index 0000000..49b0564
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchContextToPlatformConverter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.app.appsearch.AppSearchManager;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.core.util.Preconditions;
+
+/**
+ * Translates a Jetpack {@link androidx.appsearch.platformstorage.PlatformStorage.SearchContext}
+ * into a platform {@link android.app.appsearch.AppSearchManager.SearchContext}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class SearchContextToPlatformConverter {
+    private SearchContextToPlatformConverter() {}
+
+    /**
+     * Translates a Jetpack {@link androidx.appsearch.platformstorage.PlatformStorage.SearchContext}
+     * into a platform {@link android.app.appsearch.AppSearchManager.SearchContext}.
+     */
+    @NonNull
+    public static AppSearchManager.SearchContext toPlatformSearchContext(
+            @NonNull PlatformStorage.SearchContext jetpackSearchContext) {
+        Preconditions.checkNotNull(jetpackSearchContext);
+        return new AppSearchManager.SearchContext.Builder(jetpackSearchContext.getDatabaseName())
+                .build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
new file mode 100644
index 0000000..7616cd5
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchResultToPlatformConverter.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.SearchResult;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link SearchResult}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public class SearchResultToPlatformConverter {
+    private SearchResultToPlatformConverter() {}
+
+    /** Translates from Platform to Jetpack versions of {@link SearchResult}. */
+    @NonNull
+    public static SearchResult toJetpackSearchResult(
+            @NonNull android.app.appsearch.SearchResult platformResult) {
+        Preconditions.checkNotNull(platformResult);
+        GenericDocument document = GenericDocumentToPlatformConverter.toJetpackGenericDocument(
+                platformResult.getGenericDocument());
+        SearchResult.Builder builder = new SearchResult.Builder(
+                platformResult.getPackageName(), platformResult.getDatabaseName())
+                .setGenericDocument(document);
+        List<android.app.appsearch.SearchResult.MatchInfo> platformMatches =
+                platformResult.getMatches();
+        for (int i = 0; i < platformMatches.size(); i++) {
+            SearchResult.MatchInfo jetpackMatchInfo = toJetpackMatchInfo(platformMatches.get(i));
+            builder.addMatch(jetpackMatchInfo);
+        }
+        return builder.build();
+    }
+
+    @NonNull
+    private static SearchResult.MatchInfo toJetpackMatchInfo(
+            @NonNull android.app.appsearch.SearchResult.MatchInfo platformMatchInfo) {
+        Preconditions.checkNotNull(platformMatchInfo);
+        return new SearchResult.MatchInfo.Builder(platformMatchInfo.getPropertyPath())
+                .setExactMatchRange(
+                        new SearchResult.MatchRange(
+                                platformMatchInfo.getExactMatchRange().getStart(),
+                                platformMatchInfo.getExactMatchRange().getEnd()))
+                .setSnippetRange(
+                        new SearchResult.MatchRange(
+                                platformMatchInfo.getSnippetRange().getStart(),
+                                platformMatchInfo.getSnippetRange().getEnd()))
+                .build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
new file mode 100644
index 0000000..8464f29
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.converter;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.SearchSpec;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Translates between Platform and Jetpack versions of {@link SearchSpec}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class SearchSpecToPlatformConverter {
+    private SearchSpecToPlatformConverter() {
+    }
+
+    /** Translates from Jetpack to Platform version of {@link SearchSpec}. */
+    @NonNull
+    public static android.app.appsearch.SearchSpec toPlatformSearchSpec(
+            @NonNull SearchSpec jetpackSearchSpec) {
+        Preconditions.checkNotNull(jetpackSearchSpec);
+        android.app.appsearch.SearchSpec.Builder platformBuilder =
+                new android.app.appsearch.SearchSpec.Builder();
+        platformBuilder
+                .setTermMatch(jetpackSearchSpec.getTermMatch())
+                .addFilterSchemas(jetpackSearchSpec.getFilterSchemas())
+                .addFilterNamespaces(jetpackSearchSpec.getFilterNamespaces())
+                .addFilterPackageNames(jetpackSearchSpec.getFilterPackageNames())
+                .setResultCountPerPage(jetpackSearchSpec.getResultCountPerPage())
+                .setRankingStrategy(jetpackSearchSpec.getRankingStrategy())
+                .setOrder(jetpackSearchSpec.getOrder())
+                .setSnippetCount(jetpackSearchSpec.getSnippetCount())
+                .setSnippetCountPerProperty(jetpackSearchSpec.getSnippetCountPerProperty())
+                .setMaxSnippetSize(jetpackSearchSpec.getMaxSnippetSize());
+        // TODO(b/180429302) When calling setResultGrouping, check that
+        //  getResultGroupingType doesn't return 0 before calling setResultGrouping.
+        for (Map.Entry<String, List<String>> projection :
+                jetpackSearchSpec.getProjections().entrySet()) {
+            platformBuilder.addProjection(projection.getKey(), projection.getValue());
+        }
+        return platformBuilder.build();
+    }
+}
diff --git a/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/util/BatchResultCallbackAdapter.java b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/util/BatchResultCallbackAdapter.java
new file mode 100644
index 0000000..226e274
--- /dev/null
+++ b/appsearch/platform-storage/src/main/java/androidx/appsearch/platformstorage/util/BatchResultCallbackAdapter.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021 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 androidx.appsearch.platformstorage.util;
+
+import android.app.appsearch.BatchResultCallback;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.util.Preconditions;
+
+import java.util.function.Function;
+
+/**
+ * An implementation of the framework API's {@link android.app.appsearch.BatchResultCallback} which
+ * return the result as a {@link com.google.common.util.concurrent.ListenableFuture}.
+ *
+ * @param <K>             The type of key in the batch result (both Framework and Jetpack)
+ * @param <PlatformValue> The type of value in the Framework's
+ *                        {@link android.app.appsearch.AppSearchBatchResult}.
+ * @param <JetpackValue>  The type of value in Jetpack's {@link AppSearchBatchResult}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(Build.VERSION_CODES.S)
+public final class BatchResultCallbackAdapter<K, PlatformValue, JetpackValue>
+        implements BatchResultCallback<K, PlatformValue> {
+    private final ResolvableFuture<AppSearchBatchResult<K, JetpackValue>> mFuture;
+    private final Function<PlatformValue, JetpackValue> mValueMapper;
+
+    public BatchResultCallbackAdapter(
+            @NonNull ResolvableFuture<AppSearchBatchResult<K, JetpackValue>> future,
+            @NonNull Function<PlatformValue, JetpackValue> valueMapper) {
+        mFuture = Preconditions.checkNotNull(future);
+        mValueMapper = Preconditions.checkNotNull(valueMapper);
+    }
+
+    @Override
+    public void onResult(
+            @NonNull android.app.appsearch.AppSearchBatchResult<K, PlatformValue> platformResult) {
+        AppSearchBatchResult<K, JetpackValue> jetpackResult =
+                AppSearchResultToPlatformConverter.platformAppSearchBatchResultToJetpack(
+                        platformResult, mValueMapper);
+        mFuture.set(jetpackResult);
+    }
+
+    @Override
+    public void onSystemError(@Nullable Throwable t) {
+        mFuture.setException(t);
+    }
+
+    /**
+     * Returns a {@link androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter} where
+     * the Platform value is identical to the Jetpack value, needing no transformation.
+     */
+    @NonNull
+    public static <K, V> BatchResultCallbackAdapter<K, V, V> forSameValueType(
+            @NonNull ResolvableFuture<AppSearchBatchResult<K, V>> future) {
+        return new BatchResultCallbackAdapter<>(future, Function.identity());
+    }
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
index 5ed7290..f355053 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
@@ -98,6 +98,7 @@
     val VIEWPAGER = LibraryGroup("androidx.viewpager", LibraryVersions.VIEWPAGER)
     val VIEWPAGER2 = LibraryGroup("androidx.viewpager2", LibraryVersions.VIEWPAGER2)
     val WEAR = LibraryGroup("androidx.wear", null)
+    val WEAR_COMPOSE = LibraryGroup("androidx.wear.compose", LibraryVersions.WEAR_COMPOSE)
     val WEAR_TILES = LibraryGroup("androidx.wear.tiles", LibraryVersions.WEAR_TILES)
     val WEBKIT = LibraryGroup("androidx.webkit", LibraryVersions.WEBKIT)
     val WINDOW = LibraryGroup("androidx.window", null)
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 8dd795e..93ac3b8 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -130,6 +130,7 @@
     val WEAR = Version("1.2.0-alpha08")
     val WEAR_COMPLICATIONS_DATA = Version("1.0.0-alpha13")
     val WEAR_COMPLICATIONS_PROVIDER = Version("1.0.0-alpha13")
+    val WEAR_COMPOSE = Version("1.0.0-alpha01")
     val WEAR_INPUT = Version("1.1.0-alpha02")
     val WEAR_INPUT_TESTING = WEAR_INPUT
     val WEAR_ONGOING = Version("1.0.0-alpha04")
@@ -148,5 +149,5 @@
     val WINDOW = Version("1.0.0-alpha06")
     val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
     val WINDOW_SIDECAR = Version("0.1.0-alpha01")
-    val WORK = Version("2.6.0-alpha02")
+    val WORK = Version("2.7.0-alpha03")
 }
diff --git a/buildSrc/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/src/main/kotlin/androidx/build/SupportConfig.kt
index a098cf3..265e69d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/SupportConfig.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/SupportConfig.kt
@@ -38,7 +38,7 @@
      * Either an integer value or a pre-release platform code, prefixed with "android-" (ex.
      * "android-28" or "android-Q") as you would see within the SDK's platforms directory.
      */
-    const val COMPILE_SDK_VERSION = "android-30"
+    const val COMPILE_SDK_VERSION = "android-S"
 
     /**
      * The Android SDK version to use for targetSdkVersion meta-data.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
index a276b2c..20e45f0 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -42,6 +42,7 @@
  * This includes things like default template and session parameters, as well as maximum resolution
  * and aspect ratios for the display.
  */
+@Suppress("DEPRECATION")
 class CameraUseCaseAdapter(context: Context) : UseCaseConfigFactory {
 
     private val display: Display by lazy {
@@ -135,4 +136,4 @@
             // Unused.
         }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
index 3c1377e..03e6bce7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
@@ -33,6 +33,7 @@
 }
 
 @RequiresApi(Build.VERSION_CODES.P)
+@Suppress("DEPRECATION")
 internal object Api28Compat {
     @JvmStatic
     fun getAvailablePhysicalCameraRequestKeys(
@@ -54,4 +55,4 @@
     ): Map<String, CaptureResult>? {
         return totalCaptureResult.physicalCameraResults
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
index e5404ef..b917b5f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/ImageUtil.java
@@ -146,6 +146,7 @@
 
     /** Crops byte array with given {@link android.graphics.Rect}. */
     @NonNull
+    @SuppressWarnings("deprecation")
     public static byte[] cropByteArray(@NonNull byte[] data, @Nullable Rect cropRect)
             throws CodecFailedException {
         if (cropRect == null) {
diff --git a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/Viewfinder.kt b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/Viewfinder.kt
index 0e2f0f4..6d82f85 100644
--- a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/Viewfinder.kt
+++ b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/Viewfinder.kt
@@ -41,6 +41,7 @@
  *
  * To use the viewfinder, call configure with the desired surface size, mode, and format.
  */
+@Suppress("DEPRECATION")
 class Viewfinder(
     context: Context?,
     attrs: AttributeSet?,
@@ -553,4 +554,4 @@
 
 internal fun Size.area(): Long {
     return this.width * this.height.toLong()
-}
\ No newline at end of file
+}
diff --git a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapperProvider.java b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapperProvider.java
index 713443d..ef0331c 100644
--- a/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapperProvider.java
+++ b/car/app/app-activity/src/main/java/androidx/car/app/activity/renderer/surface/SurfaceWrapperProvider.java
@@ -51,6 +51,7 @@
         return new SurfaceWrapper(hostToken, width, height, displayId, densityDpi, surface);
     }
 
+    @SuppressWarnings("deprecation")
     private int densityDpi() {
         DisplayMetrics displayMetrics = new DisplayMetrics();
         mSurfaceView.getDisplay().getRealMetrics(displayMetrics);
diff --git a/core/core-appdigest/build.gradle b/core/core-appdigest/build.gradle
index 8270040..dac74b7 100644
--- a/core/core-appdigest/build.gradle
+++ b/core/core-appdigest/build.gradle
@@ -33,7 +33,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.0.0")
-    api("androidx.core:core:1.0.0")
+    implementation project(path: ':core:core')
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
diff --git a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
index 6d09208..16eff3c 100644
--- a/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
+++ b/core/core-appdigest/src/androidTest/java/androidx/core/appdigest/ChecksumsTest.java
@@ -27,6 +27,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.Manifest;
@@ -43,6 +44,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -90,6 +92,8 @@
     private static final String TEST_FIXED_APK_VERITY = "CtsPkgInstallTinyAppV2V3V4-Verity.apk";
 
     private static final String TEST_FIXED_APK_MD5 = "c19868da017dc01467169f8ea7c5bc57";
+    private static final String TEST_FIXED_APK_V2_SHA256 =
+            "1eec9e86e322b8d7e48e255fc3f2df2dbc91036e63982ff9850597c6a37bbeb3";
     private static final String TEST_FIXED_APK_SHA256 =
             "91aa30c1ce8d0474052f71cb8210691d41f534989c5521e27e794ec4f754c5ef";
     private static final String TEST_FIXED_APK_SHA512 =
@@ -100,7 +104,8 @@
             TYPE_WHOLE_MERKLE_ROOT_4K_SHA256 | TYPE_WHOLE_MD5 | TYPE_WHOLE_SHA1 | TYPE_WHOLE_SHA256
                     | TYPE_WHOLE_SHA512
                     | TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256 | TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512;
-
+    private static final char[] HEX_LOWER_CASE_DIGITS =
+            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
     private Context mContext;
     private Executor mExecutor;
 
@@ -109,225 +114,6 @@
         uninstallPackageSilently(FIXED_PACKAGE_NAME);
     }
 
-    @Before
-    public void onBefore() throws Exception {
-        mContext = ApplicationProvider.getApplicationContext();
-        mExecutor = Executors.newCachedThreadPool();
-    }
-
-    @After
-    public void onAfter() throws Exception {
-        uninstallPackageSilently(V4_PACKAGE_NAME);
-        assertFalse(isAppInstalled(V4_PACKAGE_NAME));
-        uninstallPackageSilently(FIXED_PACKAGE_NAME);
-        assertFalse(isAppInstalled(FIXED_PACKAGE_NAME));
-    }
-
-    @SmallTest
-    @Test
-    public void testDefaultChecksums() throws Exception {
-        Checksum[] checksums = getChecksums(V2V3_PACKAGE_NAME, true, 0, Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(0, checksums.length);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testSplitsSha256() throws Exception {
-        installSplits(new String[]{TEST_V4_APK, TEST_V4_SPLIT0, TEST_V4_SPLIT1, TEST_V4_SPLIT2,
-                TEST_V4_SPLIT3, TEST_V4_SPLIT4});
-        assertTrue(isAppInstalled(V4_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(V4_PACKAGE_NAME, true, TYPE_WHOLE_SHA256,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(6, checksums.length);
-        assertEquals(checksums[0].getSplitName(), null);
-        assertEquals(checksums[0].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[0].getValue()),
-                "ce4ad41be1191ab3cdfef09ab6fb3c5d057e15cb3553661b393f770d9149f1cc");
-        assertEquals(checksums[1].getSplitName(), "config.hdpi");
-        assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[1].getValue()),
-                "336a47c278f6b6c22abffefa6a62971fd0bd718d6947143e6ed1f6f6126a8196");
-        assertEquals(checksums[2].getSplitName(), "config.mdpi");
-        assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[2].getValue()),
-                "17fe9f85e6f29a7354932002c8bc4cb829e1f4acf7f30626bd298c810bb13215");
-        assertEquals(checksums[3].getSplitName(), "config.xhdpi");
-        assertEquals(checksums[3].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[3].getValue()),
-                "71a0b0ac5970def7ad80071c909be1e446174a9b39ea5cbf3004db05f87bcc4b");
-        assertEquals(checksums[4].getSplitName(), "config.xxhdpi");
-        assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[4].getValue()),
-                "cf6eaee309cf906df5519b9a449ab136841cec62857e283fb4fd20dcd2ea14aa");
-        assertEquals(checksums[5].getSplitName(), "config.xxxhdpi");
-        assertEquals(checksums[5].getType(), TYPE_WHOLE_SHA256);
-        assertEquals(bytesToHexString(checksums[5].getValue()),
-                "e7c51a01794d33e13d005b62e5ae96a39215bc588e0a2ef8f6161e1e360a17cc");
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedDefaultChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(0, checksums.length);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedV1DefaultChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK_V1);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(0, checksums.length);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedSha512DefaultChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK_V2_SHA512);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(0, checksums.length);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedVerityDefaultChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK_VERITY);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        // No usable hashes as verity-in-v2-signature does not cover the whole file.
-        assertEquals(0, checksums.length);
-    }
-
-    @LargeTest
-    @Test
-    public void testAllChecksums() throws Exception {
-        Checksum[] checksums = getChecksums(V2V3_PACKAGE_NAME, true, ALL_CHECKSUMS,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(5, checksums.length);
-        assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
-        assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
-        assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
-        assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
-        assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedAllChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS,
-                Checksums.TRUST_NONE);
-        validateFixedAllChecksums(checksums);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedAllChecksumsDirectExecutor() throws Exception {
-        installPackage(TEST_FIXED_APK);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(mContext, new Executor() {
-                    @Override
-                    public void execute(Runnable command) {
-                        command.run();
-                    }
-                }, FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, Checksums.TRUST_NONE);
-        validateFixedAllChecksums(checksums);
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedAllChecksumsSingleThread() throws Exception {
-        installPackage(TEST_FIXED_APK);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(mContext, Executors.newSingleThreadExecutor(),
-                FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, Checksums.TRUST_NONE);
-        validateFixedAllChecksums(checksums);
-    }
-
-    private void validateFixedAllChecksums(Checksum[] checksums) {
-        assertNotNull(checksums);
-        assertEquals(5, checksums.length);
-        assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
-        assertEquals("90553b8d221ab1b900b242a93e4cc659ace3a2ff1d5c62e502488b385854e66a",
-                bytesToHexString(checksums[0].getValue()));
-        assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
-        assertEquals(TEST_FIXED_APK_MD5, bytesToHexString(checksums[1].getValue()));
-        assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
-        assertEquals("331eef6bc57671de28cbd7e32089d047285ade6a",
-                bytesToHexString(checksums[2].getValue()));
-        assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
-        assertEquals(TEST_FIXED_APK_SHA256, bytesToHexString(checksums[3].getValue()));
-        assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
-        assertEquals(TEST_FIXED_APK_SHA512, bytesToHexString(checksums[4].getValue()));
-    }
-
-    @SdkSuppress(minSdkVersion = 29)
-    @LargeTest
-    @Test
-    public void testFixedV1AllChecksums() throws Exception {
-        installPackage(TEST_FIXED_APK_V1);
-        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
-
-        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS,
-                Checksums.TRUST_NONE);
-        assertNotNull(checksums);
-        assertEquals(5, checksums.length);
-        assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
-        assertEquals("1e8f831ef35257ca30d11668520aaafc6da243e853531caabc3b7867986f8886",
-                bytesToHexString(checksums[0].getValue()));
-        assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
-        assertEquals(bytesToHexString(checksums[1].getValue()), "78e51e8c51e4adc6870cd71389e0f3db");
-        assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
-        assertEquals("f6654505f2274fd9bfc098b660cdfdc2e4da6d53",
-                bytesToHexString(checksums[2].getValue()));
-        assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
-        assertEquals("43755d36ec944494f6275ee92662aca95079b3aa6639f2d35208c5af15adff78",
-                bytesToHexString(checksums[3].getValue()));
-        assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
-        assertEquals("030fc815a4957c163af2bc6f30dd5b48ac09c94c25a824a514609e1476f91421"
-                        + "e2c8b6baa16ef54014ad6c5b90c37b26b0f5c8aeb01b63a1db2eca133091c8d1",
-                bytesToHexString(checksums[4].getValue()));
-    }
-
-    private Checksum[] getChecksums(@NonNull String packageName, boolean includeSplits,
-            @Checksum.Type int required, @NonNull List<Certificate> trustedInstallers)
-            throws Exception {
-        return getChecksums(mContext, mExecutor, packageName, includeSplits, required,
-                trustedInstallers);
-    }
-
     private static Checksum[] getChecksums(@NonNull Context context, @NonNull Executor executor,
             @NonNull String packageName,
             boolean includeSplits,
@@ -357,111 +143,6 @@
         return checksums;
     }
 
-    private void installPackage(String baseName) throws Exception {
-        installSplits(new String[]{baseName});
-    }
-
-    void installSplits(String[] names) throws Exception {
-        if (Build.VERSION.SDK_INT >= 24) {
-            new InstallerApi24(mContext).installSplits(names);
-        }
-    }
-
-    @RequiresApi(24)
-    static class InstallerApi24 {
-        private Context mContext;
-
-        InstallerApi24(Context context) {
-            mContext = context;
-        }
-
-        void installSplits(String[] names) throws Exception {
-            getUiAutomation().adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES);
-            try {
-                final PackageInstaller installer =
-                        mContext.getPackageManager().getPackageInstaller();
-                final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
-                        PackageInstaller.SessionParams.MODE_FULL_INSTALL);
-
-                final int sessionId = installer.createSession(params);
-                PackageInstaller.Session session = installer.openSession(sessionId);
-
-                for (String name : names) {
-                    writeFileToSession(session, name, name);
-                }
-
-                commitSession(session);
-            } finally {
-                getUiAutomation().dropShellPermissionIdentity();
-            }
-        }
-
-        private static void writeFileToSession(PackageInstaller.Session session, String name,
-                String apk) throws IOException {
-            try (OutputStream os = session.openWrite(name, 0, -1);
-                 InputStream is = getResourceAsStream(apk)) {
-                assertNotNull(name, is);
-                writeFullStream(is, os, -1);
-            }
-        }
-
-        private void commitSession(PackageInstaller.Session session) throws Exception {
-            final ResolvableFuture<Intent> result = ResolvableFuture.create();
-
-            // Create a single-use broadcast receiver
-            BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    context.unregisterReceiver(this);
-                    result.set(intent);
-                }
-            };
-
-            // Create a matching intent-filter and register the receiver
-            final int resultId = result.hashCode();
-            final String action = "androidx.core.appdigest.COMMIT_COMPLETE." + resultId;
-            IntentFilter intentFilter = new IntentFilter();
-            intentFilter.addAction(action);
-            mContext.registerReceiver(broadcastReceiver, intentFilter);
-
-            Intent intent = new Intent(action);
-            PendingIntent sender = PendingIntent.getBroadcast(mContext, resultId, intent,
-                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
-            session.commit(sender.getIntentSender());
-
-            Intent commitResult = result.get();
-            final int status = commitResult.getIntExtra(PackageInstaller.EXTRA_STATUS,
-                    PackageInstaller.STATUS_FAILURE);
-            assertEquals(commitResult.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + " OR "
-                            + commitResult.getExtras().get(Intent.EXTRA_INTENT),
-                    PackageInstaller.STATUS_SUCCESS, status);
-        }
-
-        static UiAutomation getUiAutomation() {
-            return InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        }
-
-        static String executeShellCommand(String command) throws IOException {
-            final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command);
-            try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) {
-                return readFullStream(inputStream);
-            }
-        }
-
-        static boolean isAppInstalled(final String packageName) throws IOException {
-            final String commandResult = executeShellCommand("pm list packages");
-            final int prefixLength = "package:".length();
-            return Arrays.stream(commandResult.split("\\r?\\n"))
-                    .anyMatch(new Predicate<String>() {
-                        @Override
-                        public boolean test(String line) {
-                            return line.substring(prefixLength).equals(packageName);
-                        }
-                    });
-        }
-    }
-
     public static InputStream getResourceAsStream(String name) {
         return Thread.currentThread().getContextClassLoader().getResourceAsStream(name);
     }
@@ -498,16 +179,6 @@
         executeShellCommand("pm uninstall " + packageName);
     }
 
-    private boolean isAppInstalled(String packageName) throws IOException {
-        if (Build.VERSION.SDK_INT >= 24) {
-            return InstallerApi24.isAppInstalled(packageName);
-        }
-        return false;
-    }
-
-    private static final char[] HEX_LOWER_CASE_DIGITS =
-            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
-
     @NonNull
     private static String bytesToHexString(byte[] array) {
         int offset = 0;
@@ -536,4 +207,468 @@
         }
         return data;
     }
+
+    @Before
+    public void onBefore() throws Exception {
+        mContext = ApplicationProvider.getApplicationContext();
+        mExecutor = Executors.newCachedThreadPool();
+    }
+
+    @After
+    public void onAfter() throws Exception {
+        uninstallPackageSilently(V4_PACKAGE_NAME);
+        assertFalse(isAppInstalled(V4_PACKAGE_NAME));
+        uninstallPackageSilently(FIXED_PACKAGE_NAME);
+        assertFalse(isAppInstalled(FIXED_PACKAGE_NAME));
+    }
+
+    @SmallTest
+    @Test
+    public void testDefaultChecksums() throws Exception {
+        Checksum[] checksums = getChecksums(V2V3_PACKAGE_NAME, true, 0, Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(1, checksums.length);
+            assertEquals(checksums[0].getType(),
+                    android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+        } else {
+            assertEquals(0, checksums.length);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testSplitsDefaultChecksums() throws Exception {
+        installSplits(new String[]{TEST_V4_APK, TEST_V4_SPLIT0, TEST_V4_SPLIT1, TEST_V4_SPLIT2,
+                TEST_V4_SPLIT3, TEST_V4_SPLIT4});
+        assertTrue(isAppInstalled(V4_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(V4_PACKAGE_NAME, true, 0, Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(checksums.length, 6);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(null, checksums[0].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[0].getType());
+            assertEquals("config.hdpi", checksums[1].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[1].getType());
+            assertEquals("config.mdpi", checksums[2].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[2].getType());
+            assertEquals("config.xhdpi", checksums[3].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[3].getType());
+            assertEquals("config.xxhdpi", checksums[4].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[4].getType());
+            assertEquals("config.xxxhdpi", checksums[5].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[5].getType());
+        } else {
+            assertEquals(0, checksums.length);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testSplitsSha256() throws Exception {
+        installSplits(new String[]{TEST_V4_APK, TEST_V4_SPLIT0, TEST_V4_SPLIT1, TEST_V4_SPLIT2,
+                TEST_V4_SPLIT3, TEST_V4_SPLIT4});
+        assertTrue(isAppInstalled(V4_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(V4_PACKAGE_NAME, true, TYPE_WHOLE_SHA256,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(checksums.length, 12);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(null, checksums[0].getSplitName());
+            assertEquals(TYPE_WHOLE_SHA256, checksums[0].getType());
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "ce4ad41be1191ab3cdfef09ab6fb3c5d057e15cb3553661b393f770d9149f1cc");
+            assertEquals(null, checksums[1].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[1].getType());
+            assertEquals(checksums[2].getSplitName(), "config.hdpi");
+            assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[2].getValue()),
+                    "336a47c278f6b6c22abffefa6a62971fd0bd718d6947143e6ed1f6f6126a8196");
+            assertEquals("config.hdpi", checksums[3].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[3].getType());
+            assertEquals(checksums[4].getSplitName(), "config.mdpi");
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "17fe9f85e6f29a7354932002c8bc4cb829e1f4acf7f30626bd298c810bb13215");
+            assertEquals("config.mdpi", checksums[5].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[5].getType());
+            assertEquals(checksums[6].getSplitName(), "config.xhdpi");
+            assertEquals(checksums[6].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[6].getValue()),
+                    "71a0b0ac5970def7ad80071c909be1e446174a9b39ea5cbf3004db05f87bcc4b");
+            assertEquals("config.xhdpi", checksums[7].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[7].getType());
+            assertEquals(checksums[8].getSplitName(), "config.xxhdpi");
+            assertEquals(checksums[8].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[8].getValue()),
+                    "cf6eaee309cf906df5519b9a449ab136841cec62857e283fb4fd20dcd2ea14aa");
+            assertEquals("config.xxhdpi", checksums[9].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[9].getType());
+            assertEquals(checksums[10].getSplitName(), "config.xxxhdpi");
+            assertEquals(checksums[10].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[10].getValue()),
+                    "e7c51a01794d33e13d005b62e5ae96a39215bc588e0a2ef8f6161e1e360a17cc");
+            assertEquals("config.xxxhdpi", checksums[11].getSplitName());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[11].getType());
+        } else {
+            assertEquals(6, checksums.length);
+            assertEquals(checksums[0].getSplitName(), null);
+            assertEquals(checksums[0].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "ce4ad41be1191ab3cdfef09ab6fb3c5d057e15cb3553661b393f770d9149f1cc");
+            assertEquals(checksums[1].getSplitName(), "config.hdpi");
+            assertEquals(checksums[1].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[1].getValue()),
+                    "336a47c278f6b6c22abffefa6a62971fd0bd718d6947143e6ed1f6f6126a8196");
+            assertEquals(checksums[2].getSplitName(), "config.mdpi");
+            assertEquals(checksums[2].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[2].getValue()),
+                    "17fe9f85e6f29a7354932002c8bc4cb829e1f4acf7f30626bd298c810bb13215");
+            assertEquals(checksums[3].getSplitName(), "config.xhdpi");
+            assertEquals(checksums[3].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[3].getValue()),
+                    "71a0b0ac5970def7ad80071c909be1e446174a9b39ea5cbf3004db05f87bcc4b");
+            assertEquals(checksums[4].getSplitName(), "config.xxhdpi");
+            assertEquals(checksums[4].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "cf6eaee309cf906df5519b9a449ab136841cec62857e283fb4fd20dcd2ea14aa");
+            assertEquals(checksums[5].getSplitName(), "config.xxxhdpi");
+            assertEquals(checksums[5].getType(), TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[5].getValue()),
+                    "e7c51a01794d33e13d005b62e5ae96a39215bc588e0a2ef8f6161e1e360a17cc");
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedDefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(1, checksums.length);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[0].getType());
+            assertEquals(TEST_FIXED_APK_V2_SHA256, bytesToHexString(checksums[0].getValue()));
+            assertNull(checksums[0].getInstallerCertificate());
+        } else {
+            assertEquals(0, checksums.length);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedV1DefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_V1);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        assertEquals(0, checksums.length);
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedSha512DefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_V2_SHA512);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(1, checksums.length);
+            // v2/v3 signature use 1M merkle tree.
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, checksums[0].getType());
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "6b866e8a54a3e358dfc20007960fb96123845f6c6d6c45f5fddf88150d71677f"
+                            + "4c3081a58921c88651f7376118aca312cf764b391cdfb8a18c6710f9f27916a0");
+            assertNull(checksums[0].getInstallerCertificate());
+        } else {
+            assertEquals(0, checksums.length);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedVerityDefaultChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_VERITY);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, 0,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        // No usable hashes as verity-in-v2-signature does not cover the whole file.
+        assertEquals(0, checksums.length);
+    }
+
+    @LargeTest
+    @Test
+    public void testAllChecksums() throws Exception {
+        Checksum[] checksums = getChecksums(V2V3_PACKAGE_NAME, true, ALL_CHECKSUMS,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastS()) {
+            assertEquals(checksums.length, 7);
+            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
+            assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
+            assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
+            assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
+            assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, checksums[5].getType());
+            assertEquals(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, checksums[6].getType());
+        } else {
+            assertEquals(5, checksums.length);
+            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
+            assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
+            assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
+            assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
+            assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedAllChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS,
+                Checksums.TRUST_NONE);
+        validateFixedAllChecksums(checksums);
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedAllChecksumsDirectExecutor() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(mContext, new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        }, FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, Checksums.TRUST_NONE);
+        validateFixedAllChecksums(checksums);
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedAllChecksumsSingleThread() throws Exception {
+        installPackage(TEST_FIXED_APK);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(mContext, Executors.newSingleThreadExecutor(),
+                FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS, Checksums.TRUST_NONE);
+        validateFixedAllChecksums(checksums);
+    }
+
+    @SdkSuppress(minSdkVersion = 29)
+    @LargeTest
+    @Test
+    public void testFixedV1AllChecksums() throws Exception {
+        installPackage(TEST_FIXED_APK_V1);
+        assertTrue(isAppInstalled(FIXED_PACKAGE_NAME));
+
+        Checksum[] checksums = getChecksums(FIXED_PACKAGE_NAME, true, ALL_CHECKSUMS,
+                Checksums.TRUST_NONE);
+        assertNotNull(checksums);
+        assertEquals(5, checksums.length);
+        assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
+        assertEquals("1e8f831ef35257ca30d11668520aaafc6da243e853531caabc3b7867986f8886",
+                bytesToHexString(checksums[0].getValue()));
+        assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
+        assertEquals(bytesToHexString(checksums[1].getValue()), "78e51e8c51e4adc6870cd71389e0f3db");
+        assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
+        assertEquals("f6654505f2274fd9bfc098b660cdfdc2e4da6d53",
+                bytesToHexString(checksums[2].getValue()));
+        assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
+        assertEquals("43755d36ec944494f6275ee92662aca95079b3aa6639f2d35208c5af15adff78",
+                bytesToHexString(checksums[3].getValue()));
+        assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
+        assertEquals("030fc815a4957c163af2bc6f30dd5b48ac09c94c25a824a514609e1476f91421"
+                        + "e2c8b6baa16ef54014ad6c5b90c37b26b0f5c8aeb01b63a1db2eca133091c8d1",
+                bytesToHexString(checksums[4].getValue()));
+    }
+
+    private void validateFixedAllChecksums(Checksum[] checksums) {
+        assertNotNull(checksums);
+        if (BuildCompat.isAtLeastR()) {
+            assertEquals(checksums.length, 7);
+            assertEquals(checksums[0].getType(),
+                    android.content.pm.Checksum.TYPE_WHOLE_MERKLE_ROOT_4K_SHA256);
+            assertEquals(bytesToHexString(checksums[0].getValue()),
+                    "90553b8d221ab1b900b242a93e4cc659ace3a2ff1d5c62e502488b385854e66a");
+            assertEquals(checksums[1].getType(), android.content.pm.Checksum.TYPE_WHOLE_MD5);
+            assertEquals(bytesToHexString(checksums[1].getValue()), TEST_FIXED_APK_MD5);
+            assertEquals(checksums[2].getType(), android.content.pm.Checksum.TYPE_WHOLE_SHA1);
+            assertEquals(bytesToHexString(checksums[2].getValue()),
+                    "331eef6bc57671de28cbd7e32089d047285ade6a");
+            assertEquals(checksums[3].getType(), android.content.pm.Checksum.TYPE_WHOLE_SHA256);
+            assertEquals(bytesToHexString(checksums[3].getValue()), TEST_FIXED_APK_SHA256);
+            assertEquals(checksums[4].getType(), android.content.pm.Checksum.TYPE_WHOLE_SHA512);
+            assertEquals(bytesToHexString(checksums[4].getValue()),
+                    "b59467fe578ebc81974ab3aaa1e0d2a76fef3e4ea7212a6f2885cec1af5253571"
+                            + "1e2e94496224cae3eba8dc992144ade321540ebd458ec5b9e6a4cc51170e018");
+            assertEquals(checksums[5].getType(),
+                    android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256);
+            assertEquals(bytesToHexString(checksums[5].getValue()), TEST_FIXED_APK_V2_SHA256);
+            assertEquals(checksums[6].getType(),
+                    android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512);
+            assertEquals(bytesToHexString(checksums[6].getValue()),
+                    "ef80a8630283f60108e8557c924307d0ccdfb6bbbf2c0176bd49af342f43bc84"
+                            + "5f2888afcb71524196dda0d6dd16a6a3292bb75b431b8ff74fb60d796e882f80");
+        } else {
+            assertEquals(5, checksums.length);
+            assertEquals(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, checksums[0].getType());
+            assertEquals("90553b8d221ab1b900b242a93e4cc659ace3a2ff1d5c62e502488b385854e66a",
+                    bytesToHexString(checksums[0].getValue()));
+            assertEquals(TYPE_WHOLE_MD5, checksums[1].getType());
+            assertEquals(TEST_FIXED_APK_MD5, bytesToHexString(checksums[1].getValue()));
+            assertEquals(TYPE_WHOLE_SHA1, checksums[2].getType());
+            assertEquals("331eef6bc57671de28cbd7e32089d047285ade6a",
+                    bytesToHexString(checksums[2].getValue()));
+            assertEquals(TYPE_WHOLE_SHA256, checksums[3].getType());
+            assertEquals(TEST_FIXED_APK_SHA256, bytesToHexString(checksums[3].getValue()));
+            assertEquals(TYPE_WHOLE_SHA512, checksums[4].getType());
+            assertEquals(TEST_FIXED_APK_SHA512, bytesToHexString(checksums[4].getValue()));
+        }
+    }
+
+    private Checksum[] getChecksums(@NonNull String packageName, boolean includeSplits,
+            @Checksum.Type int required, @NonNull List<Certificate> trustedInstallers)
+            throws Exception {
+        return getChecksums(mContext, mExecutor, packageName, includeSplits, required,
+                trustedInstallers);
+    }
+
+    private void installPackage(String baseName) throws Exception {
+        installSplits(new String[]{baseName});
+    }
+
+    void installSplits(String[] names) throws Exception {
+        if (Build.VERSION.SDK_INT >= 24) {
+            new InstallerApi24(mContext).installSplits(names);
+        }
+    }
+
+    private boolean isAppInstalled(String packageName) throws IOException {
+        if (Build.VERSION.SDK_INT >= 24) {
+            return InstallerApi24.isAppInstalled(packageName);
+        }
+        return false;
+    }
+
+    @RequiresApi(24)
+    static class InstallerApi24 {
+        private Context mContext;
+
+        InstallerApi24(Context context) {
+            mContext = context;
+        }
+
+        private static void writeFileToSession(PackageInstaller.Session session, String name,
+                String apk) throws IOException {
+            try (OutputStream os = session.openWrite(name, 0, -1);
+                 InputStream is = getResourceAsStream(apk)) {
+                assertNotNull(name, is);
+                writeFullStream(is, os, -1);
+            }
+        }
+
+        static UiAutomation getUiAutomation() {
+            return InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        }
+
+        static String executeShellCommand(String command) throws IOException {
+            final ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command);
+            try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) {
+                return readFullStream(inputStream);
+            }
+        }
+
+        static boolean isAppInstalled(final String packageName) throws IOException {
+            final String commandResult = executeShellCommand("pm list packages");
+            final int prefixLength = "package:".length();
+            return Arrays.stream(commandResult.split("\\r?\\n"))
+                    .anyMatch(new Predicate<String>() {
+                        @Override
+                        public boolean test(String line) {
+                            return line.substring(prefixLength).equals(packageName);
+                        }
+                    });
+        }
+
+        void installSplits(String[] names) throws Exception {
+            getUiAutomation().adoptShellPermissionIdentity(Manifest.permission.INSTALL_PACKAGES);
+            try {
+                final PackageInstaller installer =
+                        mContext.getPackageManager().getPackageInstaller();
+                final PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+                        PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+
+                final int sessionId = installer.createSession(params);
+                PackageInstaller.Session session = installer.openSession(sessionId);
+
+                for (String name : names) {
+                    writeFileToSession(session, name, name);
+                }
+
+                commitSession(session);
+            } finally {
+                getUiAutomation().dropShellPermissionIdentity();
+            }
+        }
+
+        private void commitSession(PackageInstaller.Session session) throws Exception {
+            final ResolvableFuture<Intent> result = ResolvableFuture.create();
+
+            // Create a single-use broadcast receiver
+            BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    context.unregisterReceiver(this);
+                    result.set(intent);
+                }
+            };
+
+            // Create a matching intent-filter and register the receiver
+            final int resultId = result.hashCode();
+            final String action = "androidx.core.appdigest.COMMIT_COMPLETE." + resultId;
+            IntentFilter intentFilter = new IntentFilter();
+            intentFilter.addAction(action);
+            mContext.registerReceiver(broadcastReceiver, intentFilter);
+
+            Intent intent = new Intent(action);
+            PendingIntent sender = PendingIntent.getBroadcast(mContext, resultId, intent,
+                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+
+            session.commit(sender.getIntentSender());
+
+            Intent commitResult = result.get();
+            final int status = commitResult.getIntExtra(PackageInstaller.EXTRA_STATUS,
+                    PackageInstaller.STATUS_FAILURE);
+            assertEquals(commitResult.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + " OR "
+                            + commitResult.getExtras().get(Intent.EXTRA_INTENT),
+                    PackageInstaller.STATUS_SUCCESS, status);
+        }
+    }
 }
diff --git a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
index 2046117..ff05b32 100644
--- a/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
+++ b/core/core-appdigest/src/main/java/androidx/core/appdigest/Checksums.java
@@ -23,6 +23,7 @@
 import static androidx.core.appdigest.Checksum.TYPE_WHOLE_SHA512;
 
 import android.content.Context;
+import android.content.pm.ApkChecksum;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.os.Build;
@@ -30,8 +31,10 @@
 import android.util.Pair;
 import android.util.SparseArray;
 
+import androidx.annotation.ChecksSdkIntAtLeast;
 import androidx.annotation.NonNull;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -115,6 +118,11 @@
         Preconditions.checkNotNull(trustedInstallers);
         Preconditions.checkNotNull(executor);
 
+        if (BuildCompat.isAtLeastS()) {
+            return ApiSImpl.getChecksums(context, packageName, includeSplits, required,
+                    trustedInstallers, executor);
+        }
+
         final ApplicationInfo applicationInfo =
                 context.getPackageManager().getApplicationInfo(packageName, 0);
         if (applicationInfo == null) {
@@ -158,6 +166,56 @@
         return result;
     }
 
+    private static class ApiSImpl {
+        private ApiSImpl() {}
+
+        @ChecksSdkIntAtLeast(codename = "S") static
+        @NonNull ListenableFuture<Checksum[]> getChecksums(@NonNull Context context,
+                @NonNull String packageName, boolean includeSplits, @Checksum.Type int required,
+                @NonNull List<Certificate> trustedInstallers, @NonNull Executor executor)
+                throws CertificateEncodingException, PackageManager.NameNotFoundException {
+            final ResolvableFuture<Checksum[]> result = ResolvableFuture.create();
+
+            if (trustedInstallers == TRUST_ALL) {
+                trustedInstallers = PackageManager.TRUST_ALL;
+            } else if (trustedInstallers == TRUST_NONE) {
+                trustedInstallers = PackageManager.TRUST_NONE;
+            } else if (trustedInstallers.isEmpty()) {
+                throw new IllegalArgumentException(
+                        "trustedInstallers has to be one of TRUST_ALL/TRUST_NONE or a non-empty "
+                                + "list of certificates.");
+            }
+
+            context.getPackageManager().requestChecksums(packageName, includeSplits, required,
+                    trustedInstallers, new PackageManager.OnChecksumsReadyListener() {
+                        @Override
+                        public void onChecksumsReady(List<ApkChecksum> apkChecksums) {
+                            if (apkChecksums == null) {
+                                result.setException(
+                                        new IllegalStateException("Checksums missing."));
+                                return;
+                            }
+
+                            try {
+                                Checksum[] checksums = new Checksum[apkChecksums.size()];
+                                for (int i = 0, size = apkChecksums.size(); i < size; ++i) {
+                                    ApkChecksum apkChecksum = apkChecksums.get(i);
+                                    checksums[i] = new Checksum(apkChecksum.getSplitName(),
+                                            apkChecksum.getType(), apkChecksum.getValue(),
+                                            apkChecksum.getInstallerPackageName(),
+                                            apkChecksum.getInstallerCertificate());
+                                }
+                                result.set(checksums);
+                            } catch (Throwable e) {
+                                result.setException(e);
+                            }
+                        }
+                    });
+
+            return result;
+        }
+    }
+
     private static void getChecksumsSync(@NonNull List<Pair<String, File>> filesToChecksum,
             @Checksum.Type int required, ResolvableFuture<Checksum[]> result) {
         List<Checksum> allChecksums = new ArrayList<>();
diff --git a/core/core-ktx/src/main/java/androidx/core/util/SparseArray.kt b/core/core-ktx/src/main/java/androidx/core/util/SparseArray.kt
index f2f373e..5d922f5 100644
--- a/core/core-ktx/src/main/java/androidx/core/util/SparseArray.kt
+++ b/core/core-ktx/src/main/java/androidx/core/util/SparseArray.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+@file:Suppress("NOTHING_TO_INLINE", "EXTENSION_SHADOWED_BY_MEMBER")
 
 package androidx.core.util
 
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index c19af96..19089d2 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -3381,15 +3381,20 @@
 
   public final class EdgeEffectCompat {
     ctor @Deprecated public EdgeEffectCompat(android.content.Context!);
+    method public static android.widget.EdgeEffect create(android.content.Context, android.util.AttributeSet?);
     method @Deprecated public boolean draw(android.graphics.Canvas!);
     method @Deprecated public void finish();
+    method public static float getDistance(android.widget.EdgeEffect);
+    method public static int getType(android.widget.EdgeEffect);
     method @Deprecated public boolean isFinished();
     method @Deprecated public boolean onAbsorb(int);
     method @Deprecated public boolean onPull(float);
     method @Deprecated public boolean onPull(float, float);
     method public static void onPull(android.widget.EdgeEffect, float, float);
+    method public static float onPullDistance(android.widget.EdgeEffect, float, float);
     method @Deprecated public boolean onRelease();
     method @Deprecated public void setSize(int, int);
+    method public static void setType(android.widget.EdgeEffect, int);
   }
 
   public class ImageViewCompat {
@@ -3434,6 +3439,7 @@
     method public boolean executeKeyEvent(android.view.KeyEvent);
     method public void fling(int);
     method public boolean fullScroll(int);
+    method public int getEdgeEffectType();
     method public int getMaxScrollAmount();
     method public boolean hasNestedScrollingParent(int);
     method public boolean isFillViewport();
@@ -3446,6 +3452,7 @@
     method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
     method public void onStopNestedScroll(android.view.View, int);
     method public boolean pageScroll(int);
+    method public void setEdgeEffectType(int);
     method public void setFillViewport(boolean);
     method public void setOnScrollChangeListener(androidx.core.widget.NestedScrollView.OnScrollChangeListener?);
     method public void setSmoothScrollingEnabled(boolean);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 63f3358..d4b1393 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -3379,15 +3379,20 @@
 
   public final class EdgeEffectCompat {
     ctor @Deprecated public EdgeEffectCompat(android.content.Context!);
+    method public static android.widget.EdgeEffect create(android.content.Context, android.util.AttributeSet?);
     method @Deprecated public boolean draw(android.graphics.Canvas!);
     method @Deprecated public void finish();
+    method public static float getDistance(android.widget.EdgeEffect);
+    method public static int getType(android.widget.EdgeEffect);
     method @Deprecated public boolean isFinished();
     method @Deprecated public boolean onAbsorb(int);
     method @Deprecated public boolean onPull(float);
     method @Deprecated public boolean onPull(float, float);
     method public static void onPull(android.widget.EdgeEffect, float, float);
+    method public static float onPullDistance(android.widget.EdgeEffect, float, float);
     method @Deprecated public boolean onRelease();
     method @Deprecated public void setSize(int, int);
+    method public static void setType(android.widget.EdgeEffect, int);
   }
 
   public class ImageViewCompat {
@@ -3432,6 +3437,7 @@
     method public boolean executeKeyEvent(android.view.KeyEvent);
     method public void fling(int);
     method public boolean fullScroll(int);
+    method public int getEdgeEffectType();
     method public int getMaxScrollAmount();
     method public boolean hasNestedScrollingParent(int);
     method public boolean isFillViewport();
@@ -3444,6 +3450,7 @@
     method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
     method public void onStopNestedScroll(android.view.View, int);
     method public boolean pageScroll(int);
+    method public void setEdgeEffectType(int);
     method public void setFillViewport(boolean);
     method public void setOnScrollChangeListener(androidx.core.widget.NestedScrollView.OnScrollChangeListener?);
     method public void setSmoothScrollingEnabled(boolean);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index c1fef62..13d2794 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -3840,15 +3840,20 @@
 
   public final class EdgeEffectCompat {
     ctor @Deprecated public EdgeEffectCompat(android.content.Context!);
+    method public static android.widget.EdgeEffect create(android.content.Context, android.util.AttributeSet?);
     method @Deprecated public boolean draw(android.graphics.Canvas!);
     method @Deprecated public void finish();
+    method public static float getDistance(android.widget.EdgeEffect);
+    method public static int getType(android.widget.EdgeEffect);
     method @Deprecated public boolean isFinished();
     method @Deprecated public boolean onAbsorb(int);
     method @Deprecated public boolean onPull(float);
     method @Deprecated public boolean onPull(float, float);
     method public static void onPull(android.widget.EdgeEffect, float, float);
+    method public static float onPullDistance(android.widget.EdgeEffect, float, float);
     method @Deprecated public boolean onRelease();
     method @Deprecated public void setSize(int, int);
+    method public static void setType(android.widget.EdgeEffect, int);
   }
 
   public class ImageViewCompat {
@@ -3893,6 +3898,7 @@
     method public boolean executeKeyEvent(android.view.KeyEvent);
     method public void fling(int);
     method public boolean fullScroll(int);
+    method public int getEdgeEffectType();
     method public int getMaxScrollAmount();
     method public boolean hasNestedScrollingParent(int);
     method public boolean isFillViewport();
@@ -3905,6 +3911,7 @@
     method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
     method public void onStopNestedScroll(android.view.View, int);
     method public boolean pageScroll(int);
+    method public void setEdgeEffectType(int);
     method public void setFillViewport(boolean);
     method public void setOnScrollChangeListener(androidx.core.widget.NestedScrollView.OnScrollChangeListener?);
     method public void setSmoothScrollingEnabled(boolean);
diff --git a/core/core/proguard-rules.pro b/core/core/proguard-rules.pro
index 47a95b5..5de666f 100644
--- a/core/core/proguard-rules.pro
+++ b/core/core/proguard-rules.pro
@@ -11,3 +11,6 @@
 -keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.os.UserHandleCompat$Api*Impl {
   <methods>;
 }
+-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.widget.EdgeEffectCompat$EdgeEffectCompatApi31 {
+  <methods>;
+}
diff --git a/core/core/src/androidTest/AndroidManifest.xml b/core/core/src/androidTest/AndroidManifest.xml
index acea6c4..6a884e9 100644
--- a/core/core/src/androidTest/AndroidManifest.xml
+++ b/core/core/src/androidTest/AndroidManifest.xml
@@ -88,6 +88,8 @@
 
         <activity android:name="androidx.core.app.FrameMetricsActivity"/>
         <activity android:name="androidx.core.app.FrameMetricsSubActivity"/>
+        <activity
+            android:name="androidx.core.widget.EdgeEffectCompatTest$EdgeEffectCompatTestActivity"/>
 
         <activity
             android:name="androidx.core.view.WindowCompatActivity"
diff --git a/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java b/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java
new file mode 100644
index 0000000..70a0610
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/widget/EdgeEffectCompatTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2021 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 androidx.core.widget;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.support.v4.BaseInstrumentationTestCase;
+import android.support.v4.BaseTestActivity;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.EdgeEffect;
+
+import androidx.core.test.R;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EdgeEffectCompatTest extends
+        BaseInstrumentationTestCase<EdgeEffectCompatTest.EdgeEffectCompatTestActivity> {
+    private ViewWithEdgeEffect mView;
+    private EdgeEffect mEdgeEffect;
+
+    public EdgeEffectCompatTest() {
+        super(EdgeEffectCompatTestActivity.class);
+    }
+
+    @Before
+    public void setUp() {
+        Activity activity = mActivityTestRule.getActivity();
+        mView = activity.findViewById(R.id.edgeEffectView);
+        mEdgeEffect = mView.mEdgeEffect;
+    }
+    // TODO(b/181171227): Change to R
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    public void edgeEffectTypeApi30() {
+        assertEquals(EdgeEffect.TYPE_GLOW, EdgeEffectCompat.getType(mEdgeEffect));
+        EdgeEffectCompat.setType(mEdgeEffect, EdgeEffect.TYPE_STRETCH);
+        // nothing should change for R and earlier
+        assertEquals(EdgeEffect.TYPE_GLOW, EdgeEffectCompat.getType(mEdgeEffect));
+    }
+
+    // TODO(b/181171227): Change to S
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    public void edgeEffectTypeApi31() {
+        // TODO(b/181171227): Remove this condition
+        if (isSOrHigher()) {
+            assertEquals(EdgeEffect.TYPE_STRETCH, EdgeEffectCompat.getType(mEdgeEffect));
+            EdgeEffectCompat.setType(mEdgeEffect, EdgeEffect.TYPE_GLOW);
+            assertEquals(EdgeEffect.TYPE_GLOW, EdgeEffectCompat.getType(mEdgeEffect));
+        } else {
+            edgeEffectTypeApi30();
+        }
+    }
+
+    // TODO(b/181171227): Change to R
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    public void distanceApi30() {
+        assertEquals(0, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+        assertEquals(1f, EdgeEffectCompat.onPullDistance(mEdgeEffect, 1, 0.5f), 0f);
+        assertEquals(0, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+    }
+
+    // TODO(b/181171227): Change to S
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+    @Test
+    public void distanceApi31() {
+        // TODO(b/181171227): Remove this condition
+        if (isSOrHigher()) {
+            assertEquals(0, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+            assertEquals(1f, EdgeEffectCompat.onPullDistance(mEdgeEffect, 1, 0.5f), 0f);
+            assertEquals(1, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+            assertEquals(-1f, EdgeEffectCompat.onPullDistance(mEdgeEffect, -1.5f, 0.5f), 0f);
+            assertEquals(0, EdgeEffectCompat.getDistance(mEdgeEffect), 0f);
+        } else {
+            distanceApi30();
+        }
+    }
+
+    // TODO(b/181171227): Remove this.
+    private static boolean isSOrHigher() {
+        int sdk = Build.VERSION.SDK_INT;
+        return sdk > Build.VERSION_CODES.R
+                || (sdk == Build.VERSION_CODES.R && Build.VERSION.PREVIEW_SDK_INT != 0);
+    }
+
+    public static class EdgeEffectCompatTestActivity extends BaseTestActivity {
+        @Override
+        protected int getContentViewLayoutResId() {
+            return R.layout.edge_effect_compat;
+        }
+    }
+
+    public static class ViewWithEdgeEffect extends View {
+        public EdgeEffect mEdgeEffect;
+
+        public ViewWithEdgeEffect(Context context) {
+            this(context, null);
+        }
+
+        public ViewWithEdgeEffect(Context context, AttributeSet attrs) {
+            this(context, attrs, 0);
+        }
+
+        public ViewWithEdgeEffect(Context context, AttributeSet attrs, int defStyleAttr) {
+            this(context, attrs, defStyleAttr, 0);
+        }
+
+        public ViewWithEdgeEffect(Context context, AttributeSet attrs, int defStyleAttr,
+                int defStyleRes) {
+            super(context, attrs, defStyleAttr, defStyleRes);
+
+            mEdgeEffect = EdgeEffectCompat.create(context, attrs);
+        }
+    }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java
index a6b9e414..a35720e 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTest.java
@@ -16,16 +16,26 @@
 
 package androidx.core.widget;
 
+import static android.widget.EdgeEffect.TYPE_GLOW;
+import static android.widget.EdgeEffect.TYPE_STRETCH;
+
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
 import android.os.Parcelable;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
+import android.widget.EdgeEffect;
 
+import androidx.core.test.R;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -303,6 +313,90 @@
         assertThat(mNestedScrollView.getScrollY(), is(100));
     }
 
+    @Test
+    public void testEdgeEffectType() {
+        Context context = ApplicationProvider.getApplicationContext();
+        LayoutInflater layoutInflater = LayoutInflater.from(context);
+        mNestedScrollView = (NestedScrollView) layoutInflater.inflate(
+                R.layout.nested_scroll_view_stretch, null);
+
+        if (isSOrHigher()) {
+            assertEquals(TYPE_STRETCH, mNestedScrollView.getEdgeEffectType());
+            mNestedScrollView.setEdgeEffectType(EdgeEffect.TYPE_GLOW);
+            assertEquals(TYPE_GLOW, mNestedScrollView.getEdgeEffectType());
+        } else {
+            // Older versions can't change. They're always glow edge effects.
+            assertEquals(TYPE_GLOW, mNestedScrollView.getEdgeEffectType());
+            mNestedScrollView.setEdgeEffectType(TYPE_STRETCH);
+            assertEquals(TYPE_GLOW, mNestedScrollView.getEdgeEffectType());
+        }
+    }
+
+    @Test
+    public void testTopEdgeEffectReversal() {
+        setup(200);
+        setChildMargins(0, 0);
+        measureAndLayout(100);
+        swipeDown(false);
+        assertEquals(0, mNestedScrollView.getScrollY());
+        swipeUp(true);
+        if (isSOrHigher()) {
+            // This should just reverse the overscroll effect
+            assertEquals(0, mNestedScrollView.getScrollY());
+        } else {
+            // Can't catch the overscroll effect for R and earlier
+            assertNotEquals(0, mNestedScrollView.getScrollY());
+        }
+    }
+
+    @Test
+    public void testBottomEdgeEffectReversal() {
+        setup(200);
+        setChildMargins(0, 0);
+        measureAndLayout(100);
+        int scrollRange = mNestedScrollView.getScrollRange();
+        mNestedScrollView.scrollTo(0, scrollRange);
+        assertEquals(scrollRange, mNestedScrollView.getScrollY());
+        swipeUp(false);
+        assertEquals(scrollRange, mNestedScrollView.getScrollY());
+        swipeDown(true);
+        if (isSOrHigher()) {
+            // This should just reverse the overscroll effect
+            assertEquals(scrollRange, mNestedScrollView.getScrollY());
+        } else {
+            // Can't catch the overscroll effect for R and earlier
+            assertNotEquals(scrollRange, mNestedScrollView.getScrollY());
+        }
+    }
+
+    private void swipeDown(boolean shortSwipe) {
+        float endY = shortSwipe ? mNestedScrollView.getHeight() / 2f :
+                mNestedScrollView.getHeight() - 1;
+        swipe(0, endY);
+    }
+
+    private void swipeUp(boolean shortSwipe) {
+        float endY = shortSwipe ? mNestedScrollView.getHeight() / 2f : 0;
+        swipe(mNestedScrollView.getHeight() - 1, endY);
+    }
+
+    private void swipe(float startY, float endY) {
+        float x = mNestedScrollView.getWidth() / 2f;
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x, startY, 0);
+        mNestedScrollView.dispatchTouchEvent(down);
+        MotionEvent move = MotionEvent.obtain(0, 10, MotionEvent.ACTION_MOVE, x, endY, 0);
+        mNestedScrollView.dispatchTouchEvent(move);
+        MotionEvent up = MotionEvent.obtain(0, 1000, MotionEvent.ACTION_UP, x, endY, 0);
+        mNestedScrollView.dispatchTouchEvent(up);
+    }
+
+    private static boolean isSOrHigher() {
+        // TODO(b/181171227): Simplify this
+        int sdk = Build.VERSION.SDK_INT;
+        return sdk > Build.VERSION_CODES.R
+                || (sdk == Build.VERSION_CODES.R && Build.VERSION.PREVIEW_SDK_INT != 0);
+    }
+
     private void setup(int childHeight) {
         Context context = ApplicationProvider.getApplicationContext();
 
diff --git a/core/core/src/androidTest/res/layout-v31/edge_effect_compat.xml b/core/core/src/androidTest/res/layout-v31/edge_effect_compat.xml
new file mode 100644
index 0000000..7562e44
--- /dev/null
+++ b/core/core/src/androidTest/res/layout-v31/edge_effect_compat.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<view
+    class="androidx.core.widget.EdgeEffectCompatTest$ViewWithEdgeEffect"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/edgeEffectView"
+    android:edgeEffectType="stretch"
+    android:layout_width="100px"
+    android:layout_height="100px" />
diff --git a/core/core/src/androidTest/res/layout-v31/nested_scroll_view_stretch.xml b/core/core/src/androidTest/res/layout-v31/nested_scroll_view_stretch.xml
new file mode 100644
index 0000000..d45054a4
--- /dev/null
+++ b/core/core/src/androidTest/res/layout-v31/nested_scroll_view_stretch.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<androidx.core.widget.NestedScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/nestedScrollView"
+    android:edgeEffectType="stretch"
+    android:layout_width="100px"
+    android:layout_height="100px" />
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/layout/edge_effect_compat.xml b/core/core/src/androidTest/res/layout/edge_effect_compat.xml
new file mode 100644
index 0000000..fda0658
--- /dev/null
+++ b/core/core/src/androidTest/res/layout/edge_effect_compat.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<view
+    class="androidx.core.widget.EdgeEffectCompatTest$ViewWithEdgeEffect"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/edgeEffectView"
+    android:layout_width="100px"
+    android:layout_height="100px" />
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/layout/nested_scroll_view_stretch.xml b/core/core/src/androidTest/res/layout/nested_scroll_view_stretch.xml
new file mode 100644
index 0000000..c18e4ad
--- /dev/null
+++ b/core/core/src/androidTest/res/layout/nested_scroll_view_stretch.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<androidx.core.widget.NestedScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/nestedScrollView"
+    android:layout_width="100px"
+    android:layout_height="100px" />
\ No newline at end of file
diff --git a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
index 9ca7d09..c8b4123 100644
--- a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
@@ -16,6 +16,7 @@
 
 package androidx.core.app;
 
+import android.annotation.SuppressLint;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.os.Build;
@@ -54,6 +55,7 @@
      * @see android.content.Context#registerReceiver
      * @see android.content.Intent#filterEquals
      */
+    @SuppressLint("MissingPermission")
     public static void setAlarmClock(@NonNull AlarmManager alarmManager, long triggerTime,
             @NonNull PendingIntent showIntent, @NonNull PendingIntent operation) {
         if (Build.VERSION.SDK_INT >= 21) {
diff --git a/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java b/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java
index 4bea6a5..30a1641 100644
--- a/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/EdgeEffectCompat.java
@@ -15,12 +15,24 @@
  */
 package androidx.core.widget;
 
+import static android.widget.EdgeEffect.TYPE_GLOW;
+import static android.widget.EdgeEffect.TYPE_STRETCH;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.os.Build;
+import android.util.AttributeSet;
 import android.widget.EdgeEffect;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * Helper for accessing {@link android.widget.EdgeEffect}.
@@ -33,6 +45,13 @@
 public final class EdgeEffectCompat {
     private EdgeEffect mEdgeEffect;
 
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef({TYPE_GLOW, TYPE_STRETCH})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EdgeEffectType {
+    }
+
     /**
      * Construct a new EdgeEffect themed using the given context.
      *
@@ -41,7 +60,8 @@
      *
      * @param context Context to use for theming the effect
      *
-     * @deprecated Use {@link EdgeEffect} constructor directly.
+     * @deprecated Use {@link EdgeEffect} constructor directly or
+     * {@link EdgeEffectCompat#create(Context, AttributeSet)}.
      */
     @Deprecated
     public EdgeEffectCompat(Context context) {
@@ -49,6 +69,71 @@
     }
 
     /**
+     * Constructs and returns a new EdgeEffect themed using the given context, allowing support
+     * for the <code>edgeEffectType</code> attribute in the tag.
+     *
+     * @param context Context to use for theming the effect
+     * @param attrs The attributes of the XML tag that is inflating the view
+     */
+    @NonNull
+    public static EdgeEffect create(@NonNull Context context, @Nullable AttributeSet attrs) {
+        if (BuildCompat.isAtLeastS()) {
+            return EdgeEffectCompatApi31.create(context, attrs);
+        }
+
+        return new EdgeEffect(context);
+    }
+
+    /**
+     * Return the edge effect type to use. This will always be {@link EdgeEffect#TYPE_GLOW} for
+     * API versions {@link Build.VERSION_CODES#R} and earlier.
+     *
+     * @return The edge effect type to use.
+     * @attr ref android.R.styleable#EdgeEffect_edgeEffectType
+     */
+    @EdgeEffectType
+    public static int getType(@NonNull EdgeEffect edgeEffect) {
+        if (BuildCompat.isAtLeastS()) {
+            return EdgeEffectCompatApi31.getType(edgeEffect);
+        }
+        return TYPE_GLOW;
+    }
+
+    /**
+     * Sets the edge effect type to use. The default without a theme attribute set is
+     * {@link EdgeEffect#TYPE_GLOW}. This does not affect the edge effect type for versions
+     * {@link Build.VERSION_CODES#R} and earlier.
+     *
+     * @param type The edge effect type to use.
+     * @attr ref android.R.styleable#EdgeEffect_edgeEffectType
+     */
+    public static void setType(@NonNull EdgeEffect edgeEffect, @EdgeEffectType int type) {
+        if (BuildCompat.isAtLeastS()) {
+            EdgeEffectCompatApi31.setType(edgeEffect, type);
+        }
+    }
+
+    /**
+     * Returns the pull distance needed to be released to remove the showing effect.
+     * It is determined by the {@link #onPull(float, float)} <code>deltaDistance</code> and
+     * any animating values, including from {@link #onAbsorb(int)} and {@link #onRelease()}.
+     *
+     * This can be used in conjunction with {@link #onPullDistance(EdgeEffect, float, float)} to
+     * release the currently showing effect.
+     *
+     * On {@link Build.VERSION_CODES#R} and earlier, this will return 0.
+     *
+     * @return The pull distance that must be released to remove the showing effect or 0 for
+     * versions {@link Build.VERSION_CODES#R} and earlier.
+     */
+    public static float getDistance(@NonNull EdgeEffect edgeEffect) {
+        if (BuildCompat.isAtLeastS()) {
+            return EdgeEffectCompatApi31.getDistance(edgeEffect);
+        }
+        return 0;
+    }
+
+    /**
      * Set the size of this edge effect in pixels.
      *
      * @param width Effect width in pixels
@@ -157,6 +242,51 @@
     }
 
     /**
+     * A view should call this when content is pulled away from an edge by the user.
+     * This will update the state of the current visual effect and its associated animation.
+     * The host view should always {@link android.view.View#invalidate()} after this
+     * and draw the results accordingly. This works similarly to {@link #onPull(float, float)},
+     * but returns the amount of <code>deltaDistance</code> that has been consumed. For versions
+     * {@link Build.VERSION_CODES#S} and above, if the {@link #getDistance(EdgeEffect)} is currently
+     * 0 and <code>deltaDistance</code> is negative, this function will return 0 and the drawn value
+     * will remain unchanged. For versions {@link Build.VERSION_CODES#R} and below, this will
+     * consume all of the provided value and return <code>deltaDistance</code>.
+     *
+     * This method can be used to reverse the effect from a pull or absorb and partially consume
+     * some of a motion:
+     *
+     * <pre class="prettyprint">
+     *     if (deltaY < 0 && EdgeEffectCompat.getDistance(edgeEffect) != 0) {
+     *         float displacement = x / getWidth();
+     *         float dist = deltaY / getHeight();
+     *         float consumed = EdgeEffectCompat.onPullDistance(edgeEffect, dist, displacement);
+     *         deltaY -= consumed * getHeight();
+     *         if (edgeEffect.getDistance() == 0f) edgeEffect.onRelease();
+     *     }
+     * </pre>
+     *
+     * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+     *                      1.f (full length of the view) or negative values to express change
+     *                      back toward the edge reached to initiate the effect.
+     * @param displacement The displacement from the starting side of the effect of the point
+     *                     initiating the pull. In the case of touch this is the finger position.
+     *                     Values may be from 0-1.
+     * @return The amount of <code>deltaDistance</code> that was consumed, a number between
+     * 0 and <code>deltaDistance</code>.
+     */
+    public static float onPullDistance(
+            @NonNull EdgeEffect edgeEffect,
+            float deltaDistance,
+            float displacement
+    ) {
+        if (BuildCompat.isAtLeastS()) {
+            return EdgeEffectCompatApi31.onPullDistance(edgeEffect, deltaDistance, displacement);
+        }
+        onPull(edgeEffect, deltaDistance, displacement);
+        return deltaDistance;
+    }
+
+    /**
      * Call when the object is released after being pulled.
      * This will begin the "decay" phase of the effect. After calling this method
      * the host view should {@link android.view.View#invalidate()} if this method
@@ -207,4 +337,55 @@
     public boolean draw(Canvas canvas) {
         return mEdgeEffect.draw(canvas);
     }
+
+    // TODO(b/181171227): This actually requires S, but we don't have a version for S yet.
+    @RequiresApi(Build.VERSION_CODES.R)
+    private static class EdgeEffectCompatApi31 {
+        private EdgeEffectCompatApi31() {}
+
+        public static EdgeEffect create(Context context, AttributeSet attrs) {
+            try {
+                return new EdgeEffect(context, attrs);
+            } catch (Throwable t) {
+                return new EdgeEffect(context); // Old preview release
+            }
+        }
+
+        public static float onPullDistance(
+                EdgeEffect edgeEffect,
+                float deltaDistance,
+                float displacement
+        ) {
+            try {
+                return edgeEffect.onPullDistance(deltaDistance, displacement);
+            } catch (Throwable t) {
+                edgeEffect.onPull(deltaDistance, displacement); // Old preview release
+                return 0;
+            }
+        }
+
+        public static float getDistance(EdgeEffect edgeEffect) {
+            try {
+                return edgeEffect.getDistance();
+            } catch (Throwable t) {
+                return 0; // Old preview release
+            }
+        }
+
+        public static int getType(EdgeEffect edgeEffect) {
+            try {
+                return edgeEffect.getType();
+            } catch (Throwable t) {
+                return TYPE_GLOW; // Old preview release
+            }
+        }
+
+        public static void setType(EdgeEffect edgeEffect, int type) {
+            try {
+                edgeEffect.setType(type);
+            } catch (Throwable t) {
+                // do nothing for old preview releases
+            }
+        }
+    }
 }
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index 68a1b37..dc0b724 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -102,8 +102,8 @@
 
     private final Rect mTempRect = new Rect();
     private OverScroller mScroller;
-    private EdgeEffect mEdgeGlowTop;
-    private EdgeEffect mEdgeGlowBottom;
+    private final EdgeEffect mEdgeGlowTop;
+    private final EdgeEffect mEdgeGlowBottom;
 
     /**
      * Position of the last motion event.
@@ -198,6 +198,9 @@
     public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+        mEdgeGlowTop = EdgeEffectCompat.create(context, attrs);
+        mEdgeGlowBottom = EdgeEffectCompat.create(context, attrs);
+
         initScrollView();
 
         final TypedArray a = context.obtainStyledAttributes(
@@ -216,6 +219,28 @@
         ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE);
     }
 
+    /**
+     * Returns the {@link EdgeEffect#getType()} for the edge effects.
+     * @return the {@link EdgeEffect#getType()} for the edge effects.
+     * @attr ref android.R.styleable#EdgeEffect_edgeEffectType
+     */
+    @EdgeEffectCompat.EdgeEffectType
+    public int getEdgeEffectType() {
+        // Both bottom and top will have the same type.
+        return EdgeEffectCompat.getType(mEdgeGlowBottom);
+    }
+
+    /**
+     * Sets the {@link EdgeEffect#setType(int)} for the edge effects.
+     * @param type The edge effect type to use for the edge effects.
+     * @attr ref android.R.styleable#EdgeEffect_edgeEffectType
+     */
+    public void setEdgeEffectType(@EdgeEffectCompat.EdgeEffectType int type) {
+        EdgeEffectCompat.setType(mEdgeGlowTop, type);
+        EdgeEffectCompat.setType(mEdgeGlowBottom, type);
+        invalidate();
+    }
+
     // NestedScrollingChild3
 
     @Override
@@ -775,7 +800,7 @@
             case MotionEvent.ACTION_DOWN: {
                 final int y = (int) ev.getY();
                 if (!inChild((int) ev.getX(), y)) {
-                    mIsBeingDragged = false;
+                    mIsBeingDragged = stopGlowAnimations(ev) || !mScroller.isFinished();
                     recycleVelocityTracker();
                     break;
                 }
@@ -792,11 +817,12 @@
                 /*
                  * If being flinged and user touches the screen, initiate drag;
                  * otherwise don't. mScroller.isFinished should be false when
-                 * being flinged. We need to call computeScrollOffset() first so that
+                 * being flinged. We also want to catch the edge glow and start dragging
+                 * if one is being animated. We need to call computeScrollOffset() first so that
                  * isFinished() is correct.
                 */
                 mScroller.computeScrollOffset();
-                mIsBeingDragged = !mScroller.isFinished();
+                mIsBeingDragged = stopGlowAnimations(ev) || !mScroller.isFinished();
                 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                 break;
             }
@@ -842,7 +868,7 @@
                 if (getChildCount() == 0) {
                     return false;
                 }
-                if ((mIsBeingDragged = !mScroller.isFinished())) {
+                if (mIsBeingDragged) {
                     final ViewParent parent = getParent();
                     if (parent != null) {
                         parent.requestDisallowInterceptTouchEvent(true);
@@ -872,6 +898,7 @@
 
                 final int y = (int) ev.getY(activePointerIndex);
                 int deltaY = mLastMotionY - y;
+                deltaY -= releaseVerticalGlow(deltaY, ev.getX(activePointerIndex));
                 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                     final ViewParent parent = getParent();
                     if (parent != null) {
@@ -922,24 +949,23 @@
 
                     if (canOverscroll) {
                         deltaY -= mScrollConsumed[1];
-                        ensureGlows();
                         final int pulledToY = oldY + deltaY;
                         if (pulledToY < 0) {
-                            EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
+                            EdgeEffectCompat.onPullDistance(mEdgeGlowTop,
+                                    (float) -deltaY / getHeight(),
                                     ev.getX(activePointerIndex) / getWidth());
                             if (!mEdgeGlowBottom.isFinished()) {
                                 mEdgeGlowBottom.onRelease();
                             }
                         } else if (pulledToY > range) {
-                            EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
-                                    1.f - ev.getX(activePointerIndex)
-                                            / getWidth());
+                            EdgeEffectCompat.onPullDistance(mEdgeGlowBottom,
+                                    (float) deltaY / getHeight(),
+                                    1.f - ev.getX(activePointerIndex) / getWidth());
                             if (!mEdgeGlowTop.isFinished()) {
                                 mEdgeGlowTop.onRelease();
                             }
                         }
-                        if (mEdgeGlowTop != null
-                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
+                        if (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished()) {
                             ViewCompat.postInvalidateOnAnimation(this);
                         }
                     }
@@ -991,6 +1017,30 @@
         return true;
     }
 
+    /**
+     * This stops any edge glow animation that is currently running by applying a
+     * 0 length pull at the displacement given by the provided MotionEvent. On pre-S devices,
+     * this method does nothing, allowing any animating edge effect to continue animating and
+     * returning <code>false</code> always.
+     *
+     * @param e The motion event to use to indicate the finger position for the displacement of
+     *          the current pull.
+     * @return <code>true</code> if any edge effect had an existing effect to be drawn ond the
+     * animation was stopped or <code>false</code> if no edge effect had a value to display.
+     */
+    private boolean stopGlowAnimations(MotionEvent e) {
+        boolean stopped = false;
+        if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) {
+            EdgeEffectCompat.onPullDistance(mEdgeGlowTop, 0, e.getY() / getHeight());
+            stopped = true;
+        }
+        if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) {
+            EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, 0, 1 - e.getY() / getHeight());
+            stopped = true;
+        }
+        return stopped;
+    }
+
     private void onSecondaryPointerUp(MotionEvent ev) {
         final int pointerIndex = ev.getActionIndex();
         final int pointerId = ev.getPointerId(pointerIndex);
@@ -1639,7 +1689,6 @@
             final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS
                     || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
             if (canOverscroll) {
-                ensureGlows();
                 if (unconsumed < 0) {
                     if (mEdgeGlowTop.isFinished()) {
                         mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
@@ -1660,6 +1709,40 @@
         }
     }
 
+    /**
+     * If either of the vertical edge glows are currently active, this consumes part or all of
+     * deltaY on the edge glow.
+     *
+     * @param deltaY The pointer motion, in pixels, in the vertical direction, positive
+     *                         for moving down and negative for moving up.
+     * @param x The vertical position of the pointer.
+     * @return The amount of <code>deltaY</code> that has been consumed by the
+     * edge glow.
+     */
+    private int releaseVerticalGlow(int deltaY, float x) {
+        // First allow releasing existing overscroll effect:
+        float consumed = 0;
+        float displacement = x / getWidth();
+        float pullDistance = (float) deltaY / getHeight();
+        if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) {
+            consumed = -EdgeEffectCompat.onPullDistance(mEdgeGlowTop, -pullDistance, displacement);
+            if (EdgeEffectCompat.getDistance(mEdgeGlowTop) == 0) {
+                mEdgeGlowTop.onRelease();
+            }
+        } else if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) {
+            consumed = EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, pullDistance,
+                    1 - displacement);
+            if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) == 0) {
+                mEdgeGlowBottom.onRelease();
+            }
+        }
+        int pixelsConsumed = Math.round(consumed * getHeight());
+        if (pixelsConsumed != 0) {
+            invalidate();
+        }
+        return pixelsConsumed;
+    }
+
     private void runAnimatedScroll(boolean participateInNestedScrolling) {
         if (participateInNestedScrolling) {
             startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
@@ -1952,10 +2035,8 @@
         recycleVelocityTracker();
         stopNestedScroll(ViewCompat.TYPE_TOUCH);
 
-        if (mEdgeGlowTop != null) {
-            mEdgeGlowTop.onRelease();
-            mEdgeGlowBottom.onRelease();
-        }
+        mEdgeGlowTop.onRelease();
+        mEdgeGlowBottom.onRelease();
     }
 
     /**
@@ -1981,67 +2062,52 @@
         }
     }
 
-    private void ensureGlows() {
-        if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
-            if (mEdgeGlowTop == null) {
-                Context context = getContext();
-                mEdgeGlowTop = new EdgeEffect(context);
-                mEdgeGlowBottom = new EdgeEffect(context);
-            }
-        } else {
-            mEdgeGlowTop = null;
-            mEdgeGlowBottom = null;
-        }
-    }
-
     @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
-        if (mEdgeGlowTop != null) {
-            final int scrollY = getScrollY();
-            if (!mEdgeGlowTop.isFinished()) {
-                final int restoreCount = canvas.save();
-                int width = getWidth();
-                int height = getHeight();
-                int xTranslation = 0;
-                int yTranslation = Math.min(0, scrollY);
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
-                    width -= getPaddingLeft() + getPaddingRight();
-                    xTranslation += getPaddingLeft();
-                }
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
-                    height -= getPaddingTop() + getPaddingBottom();
-                    yTranslation += getPaddingTop();
-                }
-                canvas.translate(xTranslation, yTranslation);
-                mEdgeGlowTop.setSize(width, height);
-                if (mEdgeGlowTop.draw(canvas)) {
-                    ViewCompat.postInvalidateOnAnimation(this);
-                }
-                canvas.restoreToCount(restoreCount);
+        final int scrollY = getScrollY();
+        if (!mEdgeGlowTop.isFinished()) {
+            final int restoreCount = canvas.save();
+            int width = getWidth();
+            int height = getHeight();
+            int xTranslation = 0;
+            int yTranslation = Math.min(0, scrollY);
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
+                width -= getPaddingLeft() + getPaddingRight();
+                xTranslation += getPaddingLeft();
             }
-            if (!mEdgeGlowBottom.isFinished()) {
-                final int restoreCount = canvas.save();
-                int width = getWidth();
-                int height = getHeight();
-                int xTranslation = 0;
-                int yTranslation = Math.max(getScrollRange(), scrollY) + height;
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
-                    width -= getPaddingLeft() + getPaddingRight();
-                    xTranslation += getPaddingLeft();
-                }
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
-                    height -= getPaddingTop() + getPaddingBottom();
-                    yTranslation -= getPaddingBottom();
-                }
-                canvas.translate(xTranslation - width, yTranslation);
-                canvas.rotate(180, width, 0);
-                mEdgeGlowBottom.setSize(width, height);
-                if (mEdgeGlowBottom.draw(canvas)) {
-                    ViewCompat.postInvalidateOnAnimation(this);
-                }
-                canvas.restoreToCount(restoreCount);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
+                height -= getPaddingTop() + getPaddingBottom();
+                yTranslation += getPaddingTop();
             }
+            canvas.translate(xTranslation, yTranslation);
+            mEdgeGlowTop.setSize(width, height);
+            if (mEdgeGlowTop.draw(canvas)) {
+                ViewCompat.postInvalidateOnAnimation(this);
+            }
+            canvas.restoreToCount(restoreCount);
+        }
+        if (!mEdgeGlowBottom.isFinished()) {
+            final int restoreCount = canvas.save();
+            int width = getWidth();
+            int height = getHeight();
+            int xTranslation = 0;
+            int yTranslation = Math.max(getScrollRange(), scrollY) + height;
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) {
+                width -= getPaddingLeft() + getPaddingRight();
+                xTranslation += getPaddingLeft();
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) {
+                height -= getPaddingTop() + getPaddingBottom();
+                yTranslation -= getPaddingBottom();
+            }
+            canvas.translate(xTranslation - width, yTranslation);
+            canvas.rotate(180, width, 0);
+            mEdgeGlowBottom.setSize(width, height);
+            if (mEdgeGlowBottom.draw(canvas)) {
+                ViewCompat.postInvalidateOnAnimation(this);
+            }
+            canvas.restoreToCount(restoreCount);
         }
     }
 
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 50eaab9..0db3ce1 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -243,6 +243,8 @@
     docs(project(":wear:wear-complications-data"))
     docs(project(":wear:wear-complications-provider"))
     samples(project(":wear:wear-complications-provider-samples"))
+    docs(project(":wear:compose:compose-foundation"))
+    docs(project(":wear:compose:compose-material"))
     docs(project(":wear:wear-input"))
     docs(project(":wear:wear-input-testing"))
     docs(project(":wear:wear-ongoing"))
diff --git a/gradle.properties b/gradle.properties
index 5edf07a8..045f100 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -16,7 +16,9 @@
 kotlin.mpp.stability.nowarn=true
 # Workaround for b/141364941
 android.forceJacocoOutOfProcess=true
-androidx.writeVersionedApiFiles=true
+
+# Don't generate versioned API files
+androidx.writeVersionedApiFiles=false
 
 # Disable features we do not use
 android.defaults.buildfeatures.aidl=false
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index de39296..5dd020d 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -1037,6 +1037,10 @@
       "to": "android/support/v4/widget/annotations"
     },
     {
+      "from": "androidx/viewpager/widget/annotations",
+      "to": "android/support/v4/view/annotations"
+    },
+    {
       "from": "androidx/annotation/experimental/(.*)",
       "to": "ignore"
     },
diff --git a/leanback/leanback/src/main/res/values-hy/strings.xml b/leanback/leanback/src/main/res/values-hy/strings.xml
index cc12a17..b8b1267 100644
--- a/leanback/leanback/src/main/res/values-hy/strings.xml
+++ b/leanback/leanback/src/main/res/values-hy/strings.xml
@@ -53,7 +53,7 @@
     <string name="lb_playback_controls_hidden" msgid="5859666950961624736">"Մեդիայի կառավարման տարրերը թաքցված են։ Ցուցադրելու համար սեղմեք D-pad-ը:"</string>
     <string name="lb_guidedaction_finish_title" msgid="3330958750346333890">"Ավարտել"</string>
     <string name="lb_guidedaction_continue_title" msgid="893619591225519922">"Շարունակել"</string>
-    <string name="lb_media_player_error" msgid="3228326776757666747">"Մեդիա նվագարկիչի սխալի կոդ` %1$d (լրացուցիչ %2$d)"</string>
+    <string name="lb_media_player_error" msgid="3228326776757666747">"Մեդիա նվագարկչի սխալի կոդ` %1$d (լրացուցիչ %2$d)"</string>
     <string name="lb_onboarding_get_started" msgid="5549711907371161292">"ՍԿՍԵL"</string>
     <string name="lb_onboarding_accessibility_next" msgid="2394451791544864917">"Հաջորդը"</string>
 </resources>
diff --git a/recyclerview/recyclerview/api/current.txt b/recyclerview/recyclerview/api/current.txt
index e1ffd69..2f9d61d 100644
--- a/recyclerview/recyclerview/api/current.txt
+++ b/recyclerview/recyclerview/api/current.txt
@@ -433,6 +433,7 @@
     method public androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate? getCompatAccessibilityDelegate();
     method public void getDecoratedBoundsWithMargins(android.view.View, android.graphics.Rect);
     method public androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory getEdgeEffectFactory();
+    method public int getEdgeEffectType();
     method public androidx.recyclerview.widget.RecyclerView.ItemAnimator? getItemAnimator();
     method public androidx.recyclerview.widget.RecyclerView.ItemDecoration getItemDecorationAt(int);
     method public int getItemDecorationCount();
@@ -470,6 +471,7 @@
     method public void setAdapter(androidx.recyclerview.widget.RecyclerView.Adapter?);
     method public void setChildDrawingOrderCallback(androidx.recyclerview.widget.RecyclerView.ChildDrawingOrderCallback?);
     method public void setEdgeEffectFactory(androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory);
+    method public void setEdgeEffectType(int);
     method public void setHasFixedSize(boolean);
     method public void setItemAnimator(androidx.recyclerview.widget.RecyclerView.ItemAnimator?);
     method public void setItemViewCacheSize(int);
@@ -565,6 +567,7 @@
   public static class RecyclerView.EdgeEffectFactory {
     ctor public RecyclerView.EdgeEffectFactory();
     method protected android.widget.EdgeEffect createEdgeEffect(androidx.recyclerview.widget.RecyclerView, @androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.EdgeDirection int);
+    method protected android.widget.EdgeEffect createEdgeEffect(androidx.recyclerview.widget.RecyclerView, @androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.EdgeDirection int, int);
     field public static final int DIRECTION_BOTTOM = 3; // 0x3
     field public static final int DIRECTION_LEFT = 0; // 0x0
     field public static final int DIRECTION_RIGHT = 2; // 0x2
diff --git a/recyclerview/recyclerview/api/public_plus_experimental_current.txt b/recyclerview/recyclerview/api/public_plus_experimental_current.txt
index e1ffd69..3ca11ed 100644
--- a/recyclerview/recyclerview/api/public_plus_experimental_current.txt
+++ b/recyclerview/recyclerview/api/public_plus_experimental_current.txt
@@ -433,6 +433,7 @@
     method public androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate? getCompatAccessibilityDelegate();
     method public void getDecoratedBoundsWithMargins(android.view.View, android.graphics.Rect);
     method public androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory getEdgeEffectFactory();
+    method @androidx.core.widget.EdgeEffectCompat.EdgeEffectType public int getEdgeEffectType();
     method public androidx.recyclerview.widget.RecyclerView.ItemAnimator? getItemAnimator();
     method public androidx.recyclerview.widget.RecyclerView.ItemDecoration getItemDecorationAt(int);
     method public int getItemDecorationCount();
@@ -470,6 +471,7 @@
     method public void setAdapter(androidx.recyclerview.widget.RecyclerView.Adapter?);
     method public void setChildDrawingOrderCallback(androidx.recyclerview.widget.RecyclerView.ChildDrawingOrderCallback?);
     method public void setEdgeEffectFactory(androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory);
+    method public void setEdgeEffectType(@androidx.core.widget.EdgeEffectCompat.EdgeEffectType int);
     method public void setHasFixedSize(boolean);
     method public void setItemAnimator(androidx.recyclerview.widget.RecyclerView.ItemAnimator?);
     method public void setItemViewCacheSize(int);
@@ -565,6 +567,7 @@
   public static class RecyclerView.EdgeEffectFactory {
     ctor public RecyclerView.EdgeEffectFactory();
     method protected android.widget.EdgeEffect createEdgeEffect(androidx.recyclerview.widget.RecyclerView, @androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.EdgeDirection int);
+    method protected android.widget.EdgeEffect createEdgeEffect(androidx.recyclerview.widget.RecyclerView, @androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.EdgeDirection int, @androidx.core.widget.EdgeEffectCompat.EdgeEffectType int);
     field public static final int DIRECTION_BOTTOM = 3; // 0x3
     field public static final int DIRECTION_LEFT = 0; // 0x0
     field public static final int DIRECTION_RIGHT = 2; // 0x2
diff --git a/recyclerview/recyclerview/api/restricted_current.txt b/recyclerview/recyclerview/api/restricted_current.txt
index 5240856..6153609 100644
--- a/recyclerview/recyclerview/api/restricted_current.txt
+++ b/recyclerview/recyclerview/api/restricted_current.txt
@@ -433,6 +433,7 @@
     method public androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate? getCompatAccessibilityDelegate();
     method public void getDecoratedBoundsWithMargins(android.view.View, android.graphics.Rect);
     method public androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory getEdgeEffectFactory();
+    method @androidx.core.widget.EdgeEffectCompat.EdgeEffectType public int getEdgeEffectType();
     method public androidx.recyclerview.widget.RecyclerView.ItemAnimator? getItemAnimator();
     method public androidx.recyclerview.widget.RecyclerView.ItemDecoration getItemDecorationAt(int);
     method public int getItemDecorationCount();
@@ -470,6 +471,7 @@
     method public void setAdapter(androidx.recyclerview.widget.RecyclerView.Adapter?);
     method public void setChildDrawingOrderCallback(androidx.recyclerview.widget.RecyclerView.ChildDrawingOrderCallback?);
     method public void setEdgeEffectFactory(androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory);
+    method public void setEdgeEffectType(@androidx.core.widget.EdgeEffectCompat.EdgeEffectType int);
     method public void setHasFixedSize(boolean);
     method public void setItemAnimator(androidx.recyclerview.widget.RecyclerView.ItemAnimator?);
     method public void setItemViewCacheSize(int);
@@ -565,6 +567,7 @@
   public static class RecyclerView.EdgeEffectFactory {
     ctor public RecyclerView.EdgeEffectFactory();
     method protected android.widget.EdgeEffect createEdgeEffect(androidx.recyclerview.widget.RecyclerView, @androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.EdgeDirection int);
+    method protected android.widget.EdgeEffect createEdgeEffect(androidx.recyclerview.widget.RecyclerView, @androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.EdgeDirection int, @androidx.core.widget.EdgeEffectCompat.EdgeEffectType int);
     field public static final int DIRECTION_BOTTOM = 3; // 0x3
     field public static final int DIRECTION_LEFT = 0; // 0x0
     field public static final int DIRECTION_RIGHT = 2; // 0x2
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index a6fe293..734675f 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -11,7 +11,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.core:core:1.3.2")
+    api project(":core:core")
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.customview:customview:1.0.0")
 
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/CustomEdgeEffectTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/CustomEdgeEffectTest.java
index 0153bb90..495e073 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/CustomEdgeEffectTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/CustomEdgeEffectTest.java
@@ -84,6 +84,7 @@
         assertNull(factory.mBottom);
         assertNotNull(factory.mTop);
         assertTrue(factory.mTop.mPullDistance > 0);
+        scrollViewBy(-3);
 
         scrollToPosition(NUM_ITEMS - 1);
         waitForIdleScroll(mRecyclerView);
@@ -144,6 +145,7 @@
     private class TestEdgeEffect extends EdgeEffect {
 
         private float mPullDistance;
+        private float mDistance;
 
         TestEdgeEffect(Context context) {
             super(context);
@@ -157,6 +159,19 @@
         @Override
         public void onPull(float deltaDistance) {
             mPullDistance = deltaDistance;
+            mDistance += deltaDistance;
+        }
+
+        @Override
+        public float onPullDistance(float deltaDistance, float displacement) {
+            float maxDelta = Math.max(-mDistance, deltaDistance);
+            onPull(maxDelta);
+            return maxDelta;
+        }
+
+        @Override
+        public float getDistance() {
+            return mDistance;
         }
     }
 }
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StretchEdgeEffectTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StretchEdgeEffectTest.java
new file mode 100644
index 0000000..c344917
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StretchEdgeEffectTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2021 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 androidx.recyclerview.widget;
+
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.Build;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.widget.EdgeEffect;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.InputDeviceCompat;
+import androidx.core.widget.EdgeEffectCompat;
+import androidx.recyclerview.test.R;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class StretchEdgeEffectTest extends BaseRecyclerViewInstrumentationTest {
+    private static final int NUM_ITEMS = 10;
+
+    private RecyclerView mRecyclerView;
+    private LinearLayoutManager mLayoutManager;
+
+    @Before
+    public void setup() throws Throwable {
+        mLayoutManager = new LinearLayoutManager(getActivity());
+        mLayoutManager.ensureLayoutState();
+
+        mRecyclerView = new RecyclerView(getActivity());
+        mRecyclerView.setLayoutManager(mLayoutManager);
+        mRecyclerView.setAdapter(new TestAdapter(NUM_ITEMS) {
+
+            @Override
+            public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+                    int viewType) {
+                TestViewHolder holder = super.onCreateViewHolder(parent, viewType);
+                holder.itemView.setMinimumHeight(mRecyclerView.getMeasuredHeight() * 2 / NUM_ITEMS);
+                holder.itemView.setMinimumWidth(mRecyclerView.getMeasuredWidth() * 2 / NUM_ITEMS);
+                return holder;
+            }
+        });
+        setRecyclerView(mRecyclerView);
+        getInstrumentation().waitForIdleSync();
+        assertThat("Assumption check", mRecyclerView.getChildCount() > 0, is(true));
+    }
+
+    @Test
+    public void testEdgeEffectTypeAttribute() throws Throwable {
+        mLayoutManager = new LinearLayoutManager(getActivity());
+        mLayoutManager.ensureLayoutState();
+
+        LayoutInflater inflater = LayoutInflater.from(getActivity());
+        mRecyclerView = (RecyclerView) inflater.inflate(R.layout.stretch_rv, null);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+        mRecyclerView.setAdapter(new TestAdapter(NUM_ITEMS) {
+
+            @Override
+            public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+                    int viewType) {
+                TestViewHolder holder = super.onCreateViewHolder(parent, viewType);
+                holder.itemView.setMinimumHeight(mRecyclerView.getMeasuredHeight() * 2 / NUM_ITEMS);
+                holder.itemView.setMinimumWidth(mRecyclerView.getMeasuredWidth() * 2 / NUM_ITEMS);
+                return holder;
+            }
+        });
+        setRecyclerView(mRecyclerView);
+        getInstrumentation().waitForIdleSync();
+        assertThat("Assumption check", mRecyclerView.getChildCount() > 0, is(true));
+
+        TestEdgeEffectFactory
+                factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+
+        int expectedType = isSOrHigher() ? EdgeEffect.TYPE_STRETCH : EdgeEffect.TYPE_GLOW;
+        assertEquals(expectedType, mRecyclerView.getEdgeEffectType());
+
+        scrollToPosition(0);
+        waitForIdleScroll(mRecyclerView);
+        scrollVerticalBy(3);
+
+        assertEquals(expectedType, factory.mEdgeEffectType);
+        mRecyclerView.setEdgeEffectType(EdgeEffect.TYPE_GLOW);
+        assertEquals(EdgeEffect.TYPE_GLOW, mRecyclerView.getEdgeEffectType());
+
+        waitForIdleScroll(mRecyclerView);
+        scrollVerticalBy(3);
+        waitForIdleScroll(mRecyclerView);
+        assertEquals(EdgeEffect.TYPE_GLOW, factory.mEdgeEffectType);
+    }
+
+    /**
+     * After pulling the edge effect, releasing should return the edge effect to 0.
+     */
+    @Test
+    public void testLeftEdgeEffectRetract() throws Throwable {
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
+            }
+        });
+        TestEdgeEffectFactory
+                factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(0);
+        waitForIdleScroll(mRecyclerView);
+        scrollHorizontalBy(-3);
+        if (isSOrHigher()) {
+            assertTrue(EdgeEffectCompat.getDistance(factory.mLeft) > 0);
+        }
+        scrollHorizontalBy(4);
+        assertEquals(0f, EdgeEffectCompat.getDistance(factory.mLeft), 0f);
+        if (isSOrHigher()) {
+            assertTrue(factory.mLeft.isFinished());
+        }
+        assertEquals(EdgeEffect.TYPE_GLOW, factory.mEdgeEffectType);
+    }
+
+    /**
+     * After pulling the edge effect, releasing should return the edge effect to 0.
+     */
+    @Test
+    public void testTopEdgeEffectRetract() throws Throwable {
+        TestEdgeEffectFactory
+                factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(0);
+        waitForIdleScroll(mRecyclerView);
+        scrollVerticalBy(3);
+        if (isSOrHigher()) {
+            assertTrue(EdgeEffectCompat.getDistance(factory.mTop) > 0);
+        }
+        scrollVerticalBy(-4);
+        assertEquals(0f, EdgeEffectCompat.getDistance(factory.mTop), 0f);
+        if (isSOrHigher()) {
+            assertTrue(factory.mTop.isFinished());
+        }
+        assertEquals(EdgeEffect.TYPE_GLOW, factory.mEdgeEffectType);
+    }
+
+    /**
+     * After pulling the edge effect, releasing should return the edge effect to 0.
+     */
+    @Test
+    public void testRightEdgeEffectRetract() throws Throwable {
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
+            }
+        });
+        TestEdgeEffectFactory
+                factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(NUM_ITEMS - 1);
+        waitForIdleScroll(mRecyclerView);
+        scrollHorizontalBy(3);
+        if (isSOrHigher()) {
+            assertTrue(EdgeEffectCompat.getDistance(factory.mRight) > 0);
+        }
+        scrollHorizontalBy(-4);
+        assertEquals(0f, EdgeEffectCompat.getDistance(factory.mRight), 0f);
+        if (isSOrHigher()) {
+            assertTrue(factory.mRight.isFinished());
+        }
+        assertEquals(EdgeEffect.TYPE_GLOW, factory.mEdgeEffectType);
+    }
+
+    /**
+     * After pulling the edge effect, releasing should return the edge effect to 0.
+     */
+    @Test
+    public void testBottomEdgeEffectRetract() throws Throwable {
+        TestEdgeEffectFactory factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(NUM_ITEMS - 1);
+        waitForIdleScroll(mRecyclerView);
+        scrollVerticalBy(-3);
+        if (isSOrHigher()) {
+            assertTrue(EdgeEffectCompat.getDistance(factory.mBottom) > 0);
+        }
+
+        scrollVerticalBy(4);
+        if (isSOrHigher()) {
+            assertEquals(0f, EdgeEffectCompat.getDistance(factory.mBottom), 0f);
+            assertTrue(factory.mBottom.isFinished());
+        }
+        assertEquals(EdgeEffect.TYPE_GLOW, factory.mEdgeEffectType);
+    }
+
+    private static boolean isSOrHigher() {
+        // TODO(b/181171227): Simplify this
+        int sdk = Build.VERSION.SDK_INT;
+        return sdk > Build.VERSION_CODES.R
+                || (sdk == Build.VERSION_CODES.R && Build.VERSION.PREVIEW_SDK_INT != 0);
+    }
+
+    private void scrollVerticalBy(final int value) throws Throwable {
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                TouchUtils.scrollView(MotionEvent.AXIS_VSCROLL, value,
+                        InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
+            }
+        });
+    }
+
+    private void scrollHorizontalBy(final int value) throws Throwable {
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                TouchUtils.scrollView(MotionEvent.AXIS_HSCROLL, value,
+                        InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
+            }
+        });
+    }
+
+    private class TestEdgeEffectFactory extends RecyclerView.EdgeEffectFactory {
+        int mEdgeEffectType = -1;
+
+        TestEdgeEffect mTop, mBottom, mLeft, mRight;
+
+        @NonNull
+        @Override
+        protected EdgeEffect createEdgeEffect(RecyclerView view, int direction,
+                int edgeEffectType) {
+            mEdgeEffectType = edgeEffectType;
+            TestEdgeEffect effect = new TestEdgeEffect(view.getContext());
+            EdgeEffectCompat.setType(effect, edgeEffectType);
+            switch (direction) {
+                case DIRECTION_LEFT:
+                    mLeft = effect;
+                    break;
+                case DIRECTION_TOP:
+                    mTop = effect;
+                    break;
+                case DIRECTION_RIGHT:
+                    mRight = effect;
+                    break;
+                case DIRECTION_BOTTOM:
+                    mBottom = effect;
+                    break;
+            }
+            return effect;
+        }
+    }
+
+    private class TestEdgeEffect extends EdgeEffect {
+
+        private float mDistance;
+
+        TestEdgeEffect(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onPull(float deltaDistance, float displacement) {
+            onPull(deltaDistance);
+        }
+
+        @Override
+        public void onPull(float deltaDistance) {
+            mDistance += deltaDistance;
+        }
+
+        @Override
+        public float onPullDistance(float deltaDistance, float displacement) {
+            float maxDelta = Math.max(-mDistance, deltaDistance);
+            onPull(maxDelta);
+            return maxDelta;
+        }
+
+        @Override
+        public float getDistance() {
+            return mDistance;
+        }
+    }
+}
diff --git a/recyclerview/recyclerview/src/androidTest/res/layout-v31/stretch_rv.xml b/recyclerview/recyclerview/src/androidTest/res/layout-v31/stretch_rv.xml
new file mode 100644
index 0000000..60378ed
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/res/layout-v31/stretch_rv.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2021 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.
+  -->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/recycler_view"
+    android:edgeEffectType="stretch"
+    android:layout_width="90px"
+    android:layout_height="90px" />
diff --git a/recyclerview/recyclerview/src/androidTest/res/layout/stretch_rv.xml b/recyclerview/recyclerview/src/androidTest/res/layout/stretch_rv.xml
new file mode 100644
index 0000000..133ae7b
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/res/layout/stretch_rv.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2021 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.
+  -->
+
+<androidx.recyclerview.widget.RecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/recycler_view"
+    android:layout_width="90px"
+    android:layout_height="90px" />
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index a29811d..1079376 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -506,7 +506,7 @@
     private int mDispatchScrollCounter = 0;
 
     @NonNull
-    private EdgeEffectFactory mEdgeEffectFactory = new EdgeEffectFactory();
+    private EdgeEffectFactory mEdgeEffectFactory = sDefaultEdgeEffectFactory;
     private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
 
     ItemAnimator mItemAnimator = new DefaultItemAnimator();
@@ -588,6 +588,8 @@
     // Reusable int array to be passed to method calls that mutate it in order to "return" two ints.
     final int[] mReusableIntPair = new int[2];
 
+    private int mEdgeEffectType;
+
     /**
      * These are views that had their a11y importance changed during a layout. We defer these events
      * until the end of the layout because a11y service may make sync calls back to the RV while
@@ -614,6 +616,9 @@
         }
     };
 
+    static final StretchEdgeEffectFactory sDefaultEdgeEffectFactory =
+            new StretchEdgeEffectFactory();
+
     // These fields are only used to track whether we need to layout and measure RV children in
     // onLayout.
     //
@@ -716,6 +721,8 @@
 
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
                 defStyleAttr, 0);
+        mEdgeEffectType = EdgeEffectCompat.getType(EdgeEffectCompat.create(context, attrs));
+
         ViewCompat.saveAttributeDataForStyleable(this, context, R.styleable.RecyclerView,
                 attrs, a, defStyleAttr, 0);
         String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager);
@@ -1103,6 +1110,35 @@
         return mHasFixedSize;
     }
 
+    /**
+     * Returns the {@link EdgeEffect#getType()} passed into
+     * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int, int)}.
+     *
+     * @return the {@link EdgeEffect#getType()} passed into
+     *      * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int, int)}.
+     * @attr R.styleable.RecyclerView_android_edgeEffectType
+     */
+    @EdgeEffectCompat.EdgeEffectType
+    public int getEdgeEffectType() {
+        return mEdgeEffectType;
+    }
+
+    /**
+     * Sets the {@link EdgeEffect#getType()} passed into
+     * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int, int)} and any existing
+     * over-scroll effects are cleared and new effects are created as needed using
+     * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int, int)}
+     *
+     * @param type the {@link EdgeEffect#getType()} to pass into
+     * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int, int)}.
+     * @attr R.styleable.RecyclerView_android_edgeEffectType
+     */
+    public void setEdgeEffectType(@EdgeEffectCompat.EdgeEffectType int type) {
+        mEdgeEffectType = type;
+        invalidateGlows();
+        invalidate();
+    }
+
     @Override
     public void setClipToPadding(boolean clipToPadding) {
         if (clipToPadding != mClipToPadding) {
@@ -1924,6 +1960,12 @@
         if (canScrollVertical) {
             nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
         }
+
+        // If there is no MotionEvent, treat it as center-aligned edge effect:
+        float verticalDisplacement = motionEvent == null ? getHeight() / 2f : motionEvent.getY();
+        float horizontalDisplacement = motionEvent == null ? getWidth() / 2f : motionEvent.getX();
+        x -= releaseHorizontalGlow(x, verticalDisplacement);
+        y -= releaseVerticalGlow(y, horizontalDisplacement);
         startNestedScroll(nestedScrollAxis, type);
         if (dispatchNestedPreScroll(
                 canScrollHorizontal ? x : 0,
@@ -2108,6 +2150,73 @@
     }
 
     /**
+     * If either of the horizontal edge glows are currently active, this consumes part or all of
+     * deltaX on the edge glow.
+     *
+     * @param deltaX The pointer motion, in pixels, in the horizontal direction, positive
+     *                         for moving down and negative for moving up.
+     * @param y The vertical position of the pointer.
+     * @return The amount of <code>deltaX</code> that has been consumed by the
+     * edge glow.
+     */
+    private int releaseHorizontalGlow(int deltaX, float y) {
+        // First allow releasing existing overscroll effect:
+        float consumed = 0;
+        float displacement = y / getHeight();
+        float pullDistance = (float) deltaX / getWidth();
+        if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0) {
+            consumed = -EdgeEffectCompat.onPullDistance(mLeftGlow, -pullDistance, 1 - displacement);
+            if (EdgeEffectCompat.getDistance(mLeftGlow) == 0) {
+                mLeftGlow.onRelease();
+            }
+        } else if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0) {
+            consumed = EdgeEffectCompat.onPullDistance(mRightGlow, pullDistance, displacement);
+            if (EdgeEffectCompat.getDistance(mRightGlow) == 0) {
+                mRightGlow.onRelease();
+            }
+        }
+        int pixelsConsumed = Math.round(consumed * getWidth());
+        if (pixelsConsumed != 0) {
+            invalidate();
+        }
+        return pixelsConsumed;
+    }
+
+    /**
+     * If either of the vertical edge glows are currently active, this consumes part or all of
+     * deltaY on the edge glow.
+     *
+     * @param deltaY The pointer motion, in pixels, in the vertical direction, positive
+     *                         for moving down and negative for moving up.
+     * @param x The vertical position of the pointer.
+     * @return The amount of <code>deltaY</code> that has been consumed by the
+     * edge glow.
+     */
+    private int releaseVerticalGlow(int deltaY, float x) {
+        // First allow releasing existing overscroll effect:
+        float consumed = 0;
+        float displacement = x / getWidth();
+        float pullDistance = (float) deltaY / getHeight();
+        if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0) {
+            consumed = -EdgeEffectCompat.onPullDistance(mTopGlow, -pullDistance, displacement);
+            if (EdgeEffectCompat.getDistance(mTopGlow) == 0) {
+                mTopGlow.onRelease();
+            }
+        } else if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0) {
+            consumed = EdgeEffectCompat.onPullDistance(mBottomGlow, pullDistance,
+                    1 - displacement);
+            if (EdgeEffectCompat.getDistance(mBottomGlow) == 0) {
+                mBottomGlow.onRelease();
+            }
+        }
+        int pixelsConsumed = Math.round(consumed * getHeight());
+        if (pixelsConsumed != 0) {
+            invalidate();
+        }
+        return pixelsConsumed;
+    }
+
+    /**
      * <p>Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal
      * range. This value is used to compute the length of the thumb within the scrollbar's track.
      * </p>
@@ -2663,21 +2772,23 @@
         boolean invalidate = false;
         if (overscrollX < 0) {
             ensureLeftGlow();
-            EdgeEffectCompat.onPull(mLeftGlow, -overscrollX / getWidth(), 1f - y / getHeight());
+            EdgeEffectCompat.onPullDistance(mLeftGlow, -overscrollX / getWidth(),
+                    1f - y / getHeight());
             invalidate = true;
         } else if (overscrollX > 0) {
             ensureRightGlow();
-            EdgeEffectCompat.onPull(mRightGlow, overscrollX / getWidth(), y / getHeight());
+            EdgeEffectCompat.onPullDistance(mRightGlow, overscrollX / getWidth(), y / getHeight());
             invalidate = true;
         }
 
         if (overscrollY < 0) {
             ensureTopGlow();
-            EdgeEffectCompat.onPull(mTopGlow, -overscrollY / getHeight(), x / getWidth());
+            EdgeEffectCompat.onPullDistance(mTopGlow, -overscrollY / getHeight(), x / getWidth());
             invalidate = true;
         } else if (overscrollY > 0) {
             ensureBottomGlow();
-            EdgeEffectCompat.onPull(mBottomGlow, overscrollY / getHeight(), 1f - x / getWidth());
+            EdgeEffectCompat.onPullDistance(mBottomGlow, overscrollY / getHeight(),
+                    1f - x / getWidth());
             invalidate = true;
         }
 
@@ -2766,7 +2877,8 @@
         if (mLeftGlow != null) {
             return;
         }
-        mLeftGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_LEFT);
+        mLeftGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_LEFT,
+                mEdgeEffectType);
         if (mClipToPadding) {
             mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
                     getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
@@ -2779,7 +2891,8 @@
         if (mRightGlow != null) {
             return;
         }
-        mRightGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_RIGHT);
+        mRightGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_RIGHT,
+                mEdgeEffectType);
         if (mClipToPadding) {
             mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
                     getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
@@ -2792,7 +2905,8 @@
         if (mTopGlow != null) {
             return;
         }
-        mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP);
+        mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP,
+                mEdgeEffectType);
         if (mClipToPadding) {
             mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                     getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
@@ -2806,7 +2920,8 @@
         if (mBottomGlow != null) {
             return;
         }
-        mBottomGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_BOTTOM);
+        mBottomGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_BOTTOM,
+                mEdgeEffectType);
         if (mClipToPadding) {
             mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                     getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
@@ -2824,7 +2939,7 @@
      * <p>
      * When a new {@link EdgeEffectFactory} is set, any existing over-scroll effects are cleared
      * and new effects are created as needed using
-     * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int)}
+     * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int, int)}
      *
      * @param edgeEffectFactory The {@link EdgeEffectFactory} instance.
      */
@@ -3331,7 +3446,7 @@
                 mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                 mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
 
-                if (mScrollState == SCROLL_STATE_SETTLING) {
+                if (stopGlowAnimations(e) || mScrollState == SCROLL_STATE_SETTLING) {
                     getParent().requestDisallowInterceptTouchEvent(true);
                     setScrollState(SCROLL_STATE_DRAGGING);
                     stopNestedScroll(TYPE_NON_TOUCH);
@@ -3403,6 +3518,38 @@
         return mScrollState == SCROLL_STATE_DRAGGING;
     }
 
+    /**
+     * This stops any edge glow animation that is currently running by applying a
+     * 0 length pull at the displacement given by the provided MotionEvent. On pre-S devices,
+     * this method does nothing, allowing any animating edge effect to continue animating and
+     * returning <code>false</code> always.
+     *
+     * @param e The motion event to use to indicate the finger position for the displacement of
+     *          the current pull.
+     * @return <code>true</code> if any edge effect had an existing effect to be drawn ond the
+     * animation was stopped or <code>false</code> if no edge effect had a value to display.
+     */
+    private boolean stopGlowAnimations(MotionEvent e) {
+        boolean stopped = false;
+        if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0) {
+            EdgeEffectCompat.onPullDistance(mLeftGlow, 0, 1 - (e.getY() / getHeight()));
+            stopped = true;
+        }
+        if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0) {
+            EdgeEffectCompat.onPullDistance(mRightGlow, 0, e.getY() / getHeight());
+            stopped = true;
+        }
+        if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0) {
+            EdgeEffectCompat.onPullDistance(mTopGlow, 0, e.getX() / getWidth());
+            stopped = true;
+        }
+        if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0) {
+            EdgeEffectCompat.onPullDistance(mBottomGlow, 0, 1 - e.getX() / getWidth());
+            stopped = true;
+        }
+        return stopped;
+    }
+
     @Override
     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
         final int listenerCount = mOnItemTouchListeners.size();
@@ -3511,6 +3658,9 @@
                 if (mScrollState == SCROLL_STATE_DRAGGING) {
                     mReusableIntPair[0] = 0;
                     mReusableIntPair[1] = 0;
+                    dx -= releaseHorizontalGlow(dx, e.getY());
+                    dy -= releaseVerticalGlow(dy, e.getX());
+
                     if (dispatchNestedPreScroll(
                             canScrollHorizontally ? dx : 0,
                             canScrollVertically ? dy : 0,
@@ -5803,6 +5953,31 @@
                         @EdgeDirection int direction) {
             return new EdgeEffect(view.getContext());
         }
+
+        /**
+         * Create a new EdgeEffect for the provided direction and the given EdgeEffect type.
+         * By default, this returns {@link #createEdgeEffect(RecyclerView, int)}.
+         */
+        protected @NonNull EdgeEffect createEdgeEffect(@NonNull RecyclerView view,
+                        @EdgeDirection int direction,
+                @EdgeEffectCompat.EdgeEffectType int edgeEffectType) {
+            return createEdgeEffect(view, direction);
+        }
+    }
+
+    /**
+     * The default EdgeEffectFactory sets the edge effect type of the EdgeEffect.
+     */
+    static class StretchEdgeEffectFactory extends EdgeEffectFactory {
+        @NonNull
+        @Override
+        protected EdgeEffect createEdgeEffect(
+                @NonNull RecyclerView view, int direction,
+                @EdgeEffectCompat.EdgeEffectType int edgeEffectType) {
+            EdgeEffect edgeEffect = new EdgeEffect(view.getContext());
+            EdgeEffectCompat.setType(edgeEffect, edgeEffectType);
+            return edgeEffect;
+        }
     }
 
     /**
diff --git a/settings.gradle b/settings.gradle
index 07fef9b..568d536 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -167,7 +167,11 @@
 includeProject(":appcompat:integration-tests:receive-content-testapp", "appcompat/integration-tests/receive-content-testapp", [BuildType.MAIN])
 includeProject(":appsearch:appsearch", "appsearch/appsearch", [BuildType.MAIN])
 includeProject(":appsearch:appsearch-compiler", "appsearch/compiler", [BuildType.MAIN])
+includeProject(":appsearch:appsearch-debug-view", "appsearch/debug-view", [BuildType.MAIN])
+includeProject(":appsearch:appsearch-debug-view:samples", "appsearch/debug-view/samples",
+        [BuildType.MAIN])
 includeProject(":appsearch:appsearch-local-storage", "appsearch/local-storage", [BuildType.MAIN])
+includeProject(":appsearch:appsearch-platform-storage", "appsearch/platform-storage", [BuildType.MAIN])
 includeProject(":arch:core:core-common", "arch/core/core-common", [BuildType.MAIN])
 includeProject(":arch:core:core-runtime", "arch/core/core-runtime", [BuildType.MAIN])
 includeProject(":arch:core:core-testing", "arch/core/core-testing", [BuildType.MAIN])
@@ -572,6 +576,11 @@
 includeProject(":wear:wear-complications-data", "wear/wear-complications-data", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:wear-complications-provider", "wear/wear-complications-provider", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:wear-complications-provider-samples", "wear/wear-complications-provider/samples", [BuildType.MAIN, BuildType.WEAR])
+includeProject(":wear:compose:compose-foundation", "wear/compose/foundation", [BuildType.MAIN,
+                                                                            BuildType
+        .WEAR])
+includeProject(":wear:compose:compose-material", "wear/compose/material", [BuildType.MAIN, BuildType
+        .WEAR])
 includeProject(":wear:wear-input", "wear/wear-input", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:wear-input-testing", "wear/wear-input-testing", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:wear-ongoing", "wear/wear-ongoing", [BuildType.MAIN, BuildType.WEAR])
diff --git a/viewpager/viewpager/api/current.txt b/viewpager/viewpager/api/current.txt
index c0a4ddd..e8231bd 100644
--- a/viewpager/viewpager/api/current.txt
+++ b/viewpager/viewpager/api/current.txt
@@ -63,6 +63,7 @@
     method public void fakeDragBy(float);
     method public androidx.viewpager.widget.PagerAdapter? getAdapter();
     method public int getCurrentItem();
+    method public int getEdgeEffectType();
     method public int getOffscreenPageLimit();
     method public int getPageMargin();
     method public boolean isDragInGutterEnabled();
@@ -76,6 +77,7 @@
     method public void setCurrentItem(int);
     method public void setCurrentItem(int, boolean);
     method public void setDragInGutterEnabled(boolean);
+    method public void setEdgeEffectType(int);
     method public void setOffscreenPageLimit(int);
     method @Deprecated public void setOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener!);
     method public void setPageMargin(int);
diff --git a/viewpager/viewpager/api/public_plus_experimental_current.txt b/viewpager/viewpager/api/public_plus_experimental_current.txt
index c0a4ddd..e8231bd 100644
--- a/viewpager/viewpager/api/public_plus_experimental_current.txt
+++ b/viewpager/viewpager/api/public_plus_experimental_current.txt
@@ -63,6 +63,7 @@
     method public void fakeDragBy(float);
     method public androidx.viewpager.widget.PagerAdapter? getAdapter();
     method public int getCurrentItem();
+    method public int getEdgeEffectType();
     method public int getOffscreenPageLimit();
     method public int getPageMargin();
     method public boolean isDragInGutterEnabled();
@@ -76,6 +77,7 @@
     method public void setCurrentItem(int);
     method public void setCurrentItem(int, boolean);
     method public void setDragInGutterEnabled(boolean);
+    method public void setEdgeEffectType(int);
     method public void setOffscreenPageLimit(int);
     method @Deprecated public void setOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener!);
     method public void setPageMargin(int);
diff --git a/viewpager/viewpager/api/restricted_current.txt b/viewpager/viewpager/api/restricted_current.txt
index c0a4ddd..e8231bd 100644
--- a/viewpager/viewpager/api/restricted_current.txt
+++ b/viewpager/viewpager/api/restricted_current.txt
@@ -63,6 +63,7 @@
     method public void fakeDragBy(float);
     method public androidx.viewpager.widget.PagerAdapter? getAdapter();
     method public int getCurrentItem();
+    method public int getEdgeEffectType();
     method public int getOffscreenPageLimit();
     method public int getPageMargin();
     method public boolean isDragInGutterEnabled();
@@ -76,6 +77,7 @@
     method public void setCurrentItem(int);
     method public void setCurrentItem(int, boolean);
     method public void setDragInGutterEnabled(boolean);
+    method public void setEdgeEffectType(int);
     method public void setOffscreenPageLimit(int);
     method @Deprecated public void setOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener!);
     method public void setPageMargin(int);
diff --git a/viewpager/viewpager/build.gradle b/viewpager/viewpager/build.gradle
index 7194986..e10cfb8 100644
--- a/viewpager/viewpager/build.gradle
+++ b/viewpager/viewpager/build.gradle
@@ -17,7 +17,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    implementation("androidx.core:core:1.3.0-beta01")
+    implementation project(":core:core")
     api("androidx.customview:customview:1.0.0")
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
@@ -27,6 +27,7 @@
     androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
     androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation project(':internal-testutils-espresso')
 }
 
 androidx {
diff --git a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java
index 15cfc63..58663e6 100644
--- a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java
+++ b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java
@@ -23,6 +23,7 @@
 import static android.support.v4.testutils.TestUtilsMatchers.startAlignedToParent;
 
 import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.actionWithAssertions;
 import static androidx.test.espresso.action.ViewActions.pressKey;
 import static androidx.test.espresso.action.ViewActions.swipeLeft;
 import static androidx.test.espresso.action.ViewActions.swipeRight;
@@ -54,6 +55,7 @@
 
 import android.app.Activity;
 import android.graphics.Color;
+import android.os.Build;
 import android.support.v4.testutils.TestUtilsMatchers;
 import android.text.TextUtils;
 import android.util.Pair;
@@ -66,10 +68,15 @@
 
 import androidx.test.espresso.ViewAction;
 import androidx.test.espresso.action.EspressoKey;
+import androidx.test.espresso.action.GeneralLocation;
+import androidx.test.espresso.action.GeneralSwipeAction;
+import androidx.test.espresso.action.Press;
+import androidx.test.espresso.action.Swipe;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
+import androidx.testutils.TranslatedCoordinatesProvider;
 import androidx.viewpager.test.R;
 
 import org.junit.After;
@@ -94,6 +101,13 @@
     @Rule
     public final ActivityTestRule<T> mActivityTestRule;
 
+    /**
+     * The distance of a swipe's start position from the view's edge, in terms of the view's length.
+     * We do not start the swipe exactly on the view's edge, but somewhat more inward, since swiping
+     * from the exact edge may behave in an unexpected way (e.g. may open a navigation drawer).
+     */
+    private static final float EDGE_FUZZ_FACTOR = 0.083f;
+
     private static final int DIRECTION_LEFT = -1;
     private static final int DIRECTION_RIGHT = 1;
     protected ViewPager mViewPager;
@@ -418,8 +432,13 @@
         onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()), ViewPagerActions.wrap(swipeLeft()));
         assertEquals("Swipe twice left", 2, mViewPager.getCurrentItem());
 
-        onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()), ViewPagerActions.wrap(swipeRight()));
-        assertEquals("Swipe left beyond last page and then right", 1, mViewPager.getCurrentItem());
+        onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()),
+                ViewPagerActions.wrap(slowSwipeRight()));
+        // On S and above, the swipe right will be absorbed by the EdgeEffect created during
+        // swipe left.
+        int leftRightPage = isSOrHigher() ? 2 : 1;
+        assertEquals("Swipe left beyond last page and then right", leftRightPage,
+                mViewPager.getCurrentItem());
 
         onView(withId(R.id.pager)).perform(
                 ViewPagerActions.wrap(swipeRight()), ViewPagerActions.wrap(swipeRight()));
@@ -427,8 +446,47 @@
                 mViewPager.getCurrentItem());
 
         onView(withId(R.id.pager)).perform(
-                ViewPagerActions.wrap(swipeRight()), ViewPagerActions.wrap(swipeLeft()));
-        assertEquals("Swipe right beyond first page and then left", 1, mViewPager.getCurrentItem());
+                ViewPagerActions.wrap(swipeRight()), ViewPagerActions.wrap(slowSwipeLeft()));
+        // On S and above, the swipe left will be absorbed by the EdgeEffect created during
+        // swipe right.
+        int rightLeftPage = isSOrHigher() ? 0 : 1;
+        assertEquals("Swipe right beyond first page and then left", rightLeftPage,
+                mViewPager.getCurrentItem());
+    }
+
+    /**
+     * Returns an action that performs a slow swipe left-to-right across the vertical center of the
+     * view. The swipe doesn't start at the very edge of the view, but is a bit offset.<br>
+     */
+    public static ViewAction slowSwipeRight() {
+        return actionWithAssertions(
+                new GeneralSwipeAction(
+                        Swipe.SLOW,
+                        new TranslatedCoordinatesProvider(
+                                GeneralLocation.CENTER_LEFT, EDGE_FUZZ_FACTOR, 0),
+                        GeneralLocation.CENTER_RIGHT,
+                        Press.FINGER));
+    }
+
+    /**
+     * Returns an action that performs a slow swipe left-to-right across the vertical center of the
+     * view. The swipe doesn't start at the very edge of the view, but is a bit offset.<br>
+     */
+    public static ViewAction slowSwipeLeft() {
+        return actionWithAssertions(
+                new GeneralSwipeAction(
+                        Swipe.SLOW,
+                        new TranslatedCoordinatesProvider(
+                                GeneralLocation.CENTER_RIGHT, -EDGE_FUZZ_FACTOR, 0),
+                        GeneralLocation.CENTER_LEFT,
+                        Press.FINGER));
+    }
+
+    public static boolean isSOrHigher() {
+        // TODO(b/181171227): Simplify this
+        int sdk = Build.VERSION.SDK_INT;
+        return sdk > Build.VERSION_CODES.R
+                || (sdk == Build.VERSION_CODES.R && Build.VERSION.PREVIEW_SDK_INT != 0);
     }
 
     private void verifyPageContent(boolean smoothScroll) {
diff --git a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/ViewPagerTest.java b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/ViewPagerTest.java
index 78f5a07..b50af86 100644
--- a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/ViewPagerTest.java
+++ b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/ViewPagerTest.java
@@ -16,6 +16,9 @@
 
 package androidx.viewpager.widget;
 
+import static androidx.viewpager.widget.BaseViewPagerTest.isSOrHigher;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -23,12 +26,15 @@
 import android.os.Bundle;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.EdgeEffect;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
+import androidx.viewpager.test.R;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -71,6 +77,25 @@
         assertTrue(adapter.primaryCalled);
     }
 
+    @Test
+    @UiThreadTest
+    public void testEdgeEffectType() throws Throwable {
+        activityRule.getActivity().setContentView(R.layout.view_pager_with_stretch);
+        ViewPager viewPager = (ViewPager) activityRule.getActivity().findViewById(R.id.pager);
+        if (isSOrHigher()) {
+            // Starts out as stretch because the attribute is set
+            assertEquals(EdgeEffect.TYPE_STRETCH, viewPager.getEdgeEffectType());
+            // Set the type to glow
+            viewPager.setEdgeEffectType(EdgeEffect.TYPE_GLOW);
+            assertEquals(EdgeEffect.TYPE_GLOW, viewPager.getEdgeEffectType());
+        } else {
+            // Earlier versions only support glow
+            assertEquals(EdgeEffect.TYPE_GLOW, viewPager.getEdgeEffectType());
+            viewPager.setEdgeEffectType(EdgeEffect.TYPE_STRETCH);
+            assertEquals(EdgeEffect.TYPE_GLOW, viewPager.getEdgeEffectType());
+        }
+    }
+
     static final class PrimaryItemPagerAdapter extends PagerAdapter {
         public volatile int count;
         public volatile boolean primaryCalled;
diff --git a/viewpager/viewpager/src/androidTest/res/layout-v31/view_pager_with_stretch.xml b/viewpager/viewpager/src/androidTest/res/layout-v31/view_pager_with_stretch.xml
new file mode 100644
index 0000000..377370ac
--- /dev/null
+++ b/viewpager/viewpager/src/androidTest/res/layout-v31/view_pager_with_stretch.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<androidx.viewpager.widget.ViewPager
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/pager"
+    android:edgeEffectType="stretch"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
+
diff --git a/viewpager/viewpager/src/androidTest/res/layout/view_pager_with_stretch.xml b/viewpager/viewpager/src/androidTest/res/layout/view_pager_with_stretch.xml
new file mode 100644
index 0000000..15f3e79
--- /dev/null
+++ b/viewpager/viewpager/src/androidTest/res/layout/view_pager_with_stretch.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<androidx.viewpager.widget.ViewPager
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/pager"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
+
diff --git a/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java b/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
index 76b4a6b..4b02198 100644
--- a/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
+++ b/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
@@ -16,6 +16,9 @@
 
 package androidx.viewpager.widget;
 
+import static android.widget.EdgeEffect.TYPE_GLOW;
+import static android.widget.EdgeEffect.TYPE_STRETCH;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -46,15 +49,18 @@
 
 import androidx.annotation.CallSuper;
 import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
+import androidx.annotation.RestrictTo;
 import androidx.core.content.ContextCompat;
 import androidx.core.graphics.Insets;
 import androidx.core.view.AccessibilityDelegateCompat;
 import androidx.core.view.ViewCompat;
 import androidx.core.view.WindowInsetsCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.core.widget.EdgeEffectCompat;
 import androidx.customview.view.AbsSavedState;
 
 import java.lang.annotation.ElementType;
@@ -125,6 +131,13 @@
         android.R.attr.layout_gravity
     };
 
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef({TYPE_GLOW, TYPE_STRETCH})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EdgeEffectType {
+    }
+
     /**
      * Used to track what the expected number of items in the adapter should be.
      * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
@@ -391,19 +404,18 @@
 
     public ViewPager(@NonNull Context context) {
         super(context);
-        initViewPager();
+        initViewPager(context, null);
     }
 
     public ViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
-        initViewPager();
+        initViewPager(context, attrs);
     }
 
-    void initViewPager() {
+    void initViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
         setWillNotDraw(false);
         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
         setFocusable(true);
-        final Context context = getContext();
         mScroller = new Scroller(context, sInterpolator);
         final ViewConfiguration configuration = ViewConfiguration.get(context);
         final float density = context.getResources().getDisplayMetrics().density;
@@ -411,8 +423,8 @@
         mTouchSlop = configuration.getScaledPagingTouchSlop();
         mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
-        mLeftEdge = new EdgeEffect(context);
-        mRightEdge = new EdgeEffect(context);
+        mLeftEdge = EdgeEffectCompat.create(context, attrs);
+        mRightEdge = EdgeEffectCompat.create(context, attrs);
 
         mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
         mCloseEnough = (int) (CLOSE_ENOUGH * density);
@@ -477,6 +489,27 @@
                 });
     }
 
+    /**
+     * Returns the {@link EdgeEffect#getType()} for the edge effects.
+     * @return the {@link EdgeEffect#getType()} for the edge effects.
+     * @attr ref android.R.styleable#EdgeEffect_edgeEffectType
+     */
+    @EdgeEffectType
+    public int getEdgeEffectType() {
+        return EdgeEffectCompat.getType(mLeftEdge);
+    }
+
+    /**
+     * Sets the {@link EdgeEffect#setType(int)} for the edge effects.
+     * @param type The edge effect type to use for the edge effects.
+     * @attr ref android.R.styleable#EdgeEffect_edgeEffectType
+     */
+    public void setEdgeEffectType(@EdgeEffectType int type) {
+        EdgeEffectCompat.setType(mLeftEdge, type);
+        EdgeEffectCompat.setType(mRightEdge, type);
+        invalidate();
+    }
+
     @Override
     protected void onDetachedFromWindow() {
         removeCallbacks(mEndScrollRunnable);
@@ -2107,7 +2140,7 @@
                 }
                 if (mIsBeingDragged) {
                     // Scroll to follow the motion event
-                    if (performDrag(x)) {
+                    if (performDrag(x, y)) {
                         ViewCompat.postInvalidateOnAnimation(this);
                     }
                 }
@@ -2135,6 +2168,18 @@
                     mIsBeingDragged = true;
                     requestParentDisallowInterceptTouchEvent(true);
                     setScrollState(SCROLL_STATE_DRAGGING);
+                } else if (EdgeEffectCompat.getDistance(mLeftEdge) != 0
+                        || EdgeEffectCompat.getDistance(mRightEdge) != 0) {
+                    // Caught the edge glow animation
+                    mIsBeingDragged = true;
+                    setScrollState(SCROLL_STATE_DRAGGING);
+                    if (EdgeEffectCompat.getDistance(mLeftEdge) != 0) {
+                        EdgeEffectCompat.onPullDistance(mLeftEdge, 0f,
+                                1 - mLastMotionY / getHeight());
+                    }
+                    if (EdgeEffectCompat.getDistance(mRightEdge) != 0) {
+                        EdgeEffectCompat.onPullDistance(mRightEdge, 0f, mLastMotionY / getHeight());
+                    }
                 } else {
                     completeScroll(false);
                     mIsBeingDragged = false;
@@ -2243,7 +2288,7 @@
                     // Scroll to follow the motion event
                     final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                     final float x = ev.getX(activePointerIndex);
-                    needsInvalidate |= performDrag(x);
+                    needsInvalidate |= performDrag(x, ev.getY(activePointerIndex));
                 }
                 break;
             case MotionEvent.ACTION_UP:
@@ -2310,11 +2355,42 @@
         }
     }
 
-    private boolean performDrag(float x) {
+    /**
+     * If either of the horizontal edge glows are currently active, this consumes part or all of
+     * deltaX on the edge glow.
+     *
+     * @param deltaX The pointer motion, in pixels, in the horizontal direction, positive
+     *                         for moving down and negative for moving up.
+     * @param y The vertical position of the pointer.
+     * @return The amount of <code>deltaX</code> that has been consumed by the
+     * edge glow.
+     */
+    private float releaseHorizontalGlow(float deltaX, float y) {
+        // First allow releasing existing overscroll effect:
+        float consumed = 0;
+        float displacement = y / getHeight();
+        float pullDistance = (float) deltaX / getWidth();
+        if (EdgeEffectCompat.getDistance(mLeftEdge) != 0) {
+            consumed = -EdgeEffectCompat.onPullDistance(mLeftEdge, -pullDistance, 1 - displacement);
+        } else if (EdgeEffectCompat.getDistance(mRightEdge) != 0) {
+            consumed = EdgeEffectCompat.onPullDistance(mRightEdge, pullDistance, displacement);
+        }
+        return consumed * getWidth();
+    }
+
+    private boolean performDrag(float x, float y) {
         boolean needsInvalidate = false;
 
-        final float deltaX = mLastMotionX - x;
+        final float dX = mLastMotionX - x;
         mLastMotionX = x;
+        final float releaseConsumed = releaseHorizontalGlow(dX, y);
+        final float deltaX = dX - releaseConsumed;
+        if (releaseConsumed != 0) {
+            needsInvalidate = true;
+        }
+        if (Math.abs(deltaX) < 0.0001f) { // ignore rounding errors from releaseHorizontalGlow()
+            return needsInvalidate;
+        }
 
         float oldScrollX = getScrollX();
         float scrollX = oldScrollX + deltaX;
@@ -2339,14 +2415,14 @@
         if (scrollX < leftBound) {
             if (leftAbsolute) {
                 float over = leftBound - scrollX;
-                mLeftEdge.onPull(Math.abs(over) / width);
+                EdgeEffectCompat.onPullDistance(mLeftEdge, over / width, 1 - y / getHeight());
                 needsInvalidate = true;
             }
             scrollX = leftBound;
         } else if (scrollX > rightBound) {
             if (rightAbsolute) {
                 float over = scrollX - rightBound;
-                mRightEdge.onPull(Math.abs(over) / width);
+                EdgeEffectCompat.onPullDistance(mRightEdge, over / width, y / getHeight());
                 needsInvalidate = true;
             }
             scrollX = rightBound;
@@ -2407,7 +2483,9 @@
 
     private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
         int targetPage;
-        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
+        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity
+                && EdgeEffectCompat.getDistance(mLeftEdge) == 0 // don't fling while stretched
+                && EdgeEffectCompat.getDistance(mRightEdge) == 0) {
             targetPage = velocity > 0 ? currentPage : currentPage + 1;
         } else {
             final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
diff --git a/wear/compose/foundation/build.gradle b/wear/compose/foundation/build.gradle
new file mode 100644
index 0000000..7fdcc03
--- /dev/null
+++ b/wear/compose/foundation/build.gradle
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 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.
+ */
+
+
+import androidx.build.RunApiTasks
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    api("androidx.annotation:annotation:1.1.0")
+    api(GUAVA_LISTENABLE_FUTURE)
+
+    implementation 'androidx.annotation:annotation:1.2.0-alpha01'
+
+    testImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    testImplementation(ANDROIDX_TEST_EXT_TRUTH)
+    testImplementation(ANDROIDX_TEST_CORE)
+    testImplementation(ANDROIDX_TEST_RUNNER)
+    testImplementation(ANDROIDX_TEST_RULES)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(MOCKITO_CORE)
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 25
+    }
+    buildTypes.all {
+        consumerProguardFiles "proguard-rules.pro"
+    }
+
+    // Use Robolectric 4.+
+    testOptions.unitTests.includeAndroidResources = true
+}
+
+androidx {
+    name = "Android Wear Compose Foundation"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.WEAR_COMPOSE
+    mavenVersion = LibraryVersions.WEAR_COMPOSE
+    inceptionYear = "2021"
+    description = "Android Wear Compose Foundation"
+    runApiTasks = new RunApiTasks.No("API tracking disabled while the package is empty")
+}
diff --git a/wear/compose/foundation/src/main/AndroidManifest.xml b/wear/compose/foundation/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8886e0b
--- /dev/null
+++ b/wear/compose/foundation/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<manifest package="androidx.wear.compose.foundation" />
\ No newline at end of file
diff --git a/wear/compose/material/build.gradle b/wear/compose/material/build.gradle
new file mode 100644
index 0000000..83fad91
--- /dev/null
+++ b/wear/compose/material/build.gradle
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 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.
+ */
+
+
+import androidx.build.RunApiTasks
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    api("androidx.annotation:annotation:1.1.0")
+    api(GUAVA_LISTENABLE_FUTURE)
+
+    implementation 'androidx.annotation:annotation:1.2.0-alpha01'
+
+    testImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    testImplementation(ANDROIDX_TEST_EXT_TRUTH)
+    testImplementation(ANDROIDX_TEST_CORE)
+    testImplementation(ANDROIDX_TEST_RUNNER)
+    testImplementation(ANDROIDX_TEST_RULES)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(MOCKITO_CORE)
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 25
+    }
+    buildTypes.all {
+        consumerProguardFiles "proguard-rules.pro"
+    }
+
+    // Use Robolectric 4.+
+    testOptions.unitTests.includeAndroidResources = true
+}
+
+androidx {
+    name = "Android Wear Compose Material"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.WEAR_COMPOSE
+    mavenVersion = LibraryVersions.WEAR_COMPOSE
+    inceptionYear = "2021"
+    description = "Android Wear Compose Material"
+    runApiTasks = new RunApiTasks.No("API tracking disabled while the package is empty")
+}
diff --git a/wear/compose/material/src/main/AndroidManifest.xml b/wear/compose/material/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8bbc869f
--- /dev/null
+++ b/wear/compose/material/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 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.
+  -->
+
+<manifest package="androidx.wear.compose.material" />
\ No newline at end of file
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index a84b95f..cc17f5f 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -38,6 +38,8 @@
         }
     }
     defaultConfig {
+        // This is necessary because "S" is a pre-release SDK.
+        targetSdkVersion "S"
         javaCompileOptions {
             annotationProcessorOptions {
                 arguments = [
diff --git a/work/integration-tests/testapp/lint-baseline.xml b/work/integration-tests/testapp/lint-baseline.xml
index 9f8217d..56d1907 100644
--- a/work/integration-tests/testapp/lint-baseline.xml
+++ b/work/integration-tests/testapp/lint-baseline.xml
@@ -8,7 +8,7 @@
         errorLine2="                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt"
-            line="81"
+            line="93"
             column="23"/>
     </issue>
 
@@ -19,7 +19,7 @@
         errorLine2="                ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt"
-            line="82"
+            line="94"
             column="17"/>
     </issue>
 
@@ -30,7 +30,7 @@
         errorLine2="                ~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt"
-            line="83"
+            line="95"
             column="17"/>
     </issue>
 
@@ -41,7 +41,7 @@
         errorLine2="                            ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt"
-            line="84"
+            line="96"
             column="29"/>
     </issue>
 
@@ -85,7 +85,7 @@
         errorLine2="                                 ~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/MainActivity.java"
-            line="523"
+            line="527"
             column="34"/>
     </issue>
 
@@ -96,7 +96,7 @@
         errorLine2="                                     ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/MainActivity.java"
-            line="525"
+            line="529"
             column="38"/>
     </issue>
 
@@ -107,7 +107,7 @@
         errorLine2="                                ~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/MainActivity.java"
-            line="526"
+            line="530"
             column="33"/>
     </issue>
 
@@ -118,7 +118,7 @@
         errorLine2="                                         ~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/MainActivity.java"
-            line="529"
+            line="533"
             column="42"/>
     </issue>
 
@@ -129,7 +129,7 @@
         errorLine2="                                         ~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/MainActivity.java"
-            line="530"
+            line="534"
             column="42"/>
     </issue>
 
@@ -140,7 +140,7 @@
         errorLine2="                         ~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/MainActivity.java"
-            line="174"
+            line="177"
             column="26"/>
     </issue>
 
@@ -437,7 +437,7 @@
         errorLine2="                            ~~~~~~">
         <location
             file="src/main/java/androidx/work/integration/testapp/MainActivity.java"
-            line="76"
+            line="79"
             column="29"/>
     </issue>
 
diff --git a/work/integration-tests/testapp/src/main/AndroidManifest.xml b/work/integration-tests/testapp/src/main/AndroidManifest.xml
index 17f5540..c6d608d 100644
--- a/work/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/work/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -25,6 +25,7 @@
         <activity android:name=".sherlockholmes.AnalyzeSherlockHolmesActivity" />
         <activity
             android:name="androidx.work.integration.testapp.MainActivity"
+            android:exported="true"
             android:windowSoftInputMode="stateHidden">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -32,8 +33,9 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <activity android:name=".imageprocessing.ImageProcessingActivity" />
-        <activity android:name=".RetryActivity">
+        <activity android:name=".imageprocessing.ImageProcessingActivity"
+            android:exported="false" />
+        <activity android:name=".RetryActivity" android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
index 1bc08db..d131c1d 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/ForegroundWorker.kt
@@ -23,8 +23,10 @@
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.core.app.NotificationCompat
+import androidx.core.os.BuildCompat
 import androidx.work.CoroutineWorker
 import androidx.work.Data
+import androidx.work.ExperimentalExpeditedWork
 import androidx.work.ForegroundInfo
 import androidx.work.WorkerParameters
 import androidx.work.workDataOf
@@ -41,18 +43,28 @@
     override suspend fun doWork(): Result {
         val notificationId = inputData.getInt(InputNotificationId, NotificationId)
         val delayTime = inputData.getLong(InputDelayTime, Delay)
-        // Run in the context of a Foreground service
-        setForeground(getForegroundInfo(notificationId))
         val range = 20
         for (i in 1..range) {
             delay(delayTime)
             progress = workDataOf(Progress to i * (100 / range))
             setProgress(progress)
-            setForeground(getForegroundInfo(notificationId))
+            if (!BuildCompat.isAtLeastS()) {
+                // No need for notifications starting S.
+                notificationManager.notify(
+                    notificationId,
+                    getForegroundInfo(notificationId).notification
+                )
+            }
         }
         return Result.success()
     }
 
+    @ExperimentalExpeditedWork
+    override suspend fun getForegroundInfo(): ForegroundInfo {
+        val notificationId = inputData.getInt(InputNotificationId, NotificationId)
+        return getForegroundInfo(notificationId)
+    }
+
     private fun getForegroundInfo(notificationId: Int): ForegroundInfo {
         val percent = progress.getInt(Progress, 0)
         val id = applicationContext.getString(R.string.channel_id)
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
index f65cae7..6ab514d 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
@@ -44,8 +44,10 @@
 import androidx.work.Constraints;
 import androidx.work.Data;
 import androidx.work.ExistingWorkPolicy;
+import androidx.work.ExperimentalExpeditedWork;
 import androidx.work.NetworkType;
 import androidx.work.OneTimeWorkRequest;
+import androidx.work.OutOfQuotaPolicy;
 import androidx.work.PeriodicWorkRequest;
 import androidx.work.WorkContinuation;
 import androidx.work.WorkInfo;
@@ -64,6 +66,7 @@
 /**
  * Main Activity
  */
+@ExperimentalExpeditedWork
 public class MainActivity extends AppCompatActivity {
 
     private static final String PACKAGE_NAME = "androidx.work.integration.testapp";
@@ -413,6 +416,7 @@
                 OneTimeWorkRequest request =
                         new OneTimeWorkRequest.Builder(ForegroundWorker.class)
                                 .setInputData(inputData)
+                                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
                                 .setConstraints(new Constraints.Builder()
                                         .setRequiredNetworkType(NetworkType.CONNECTED).build()
                                 ).build();
diff --git a/work/workmanager-ktx/api/current.txt b/work/workmanager-ktx/api/current.txt
index 2c5f419..6551353 100644
--- a/work/workmanager-ktx/api/current.txt
+++ b/work/workmanager-ktx/api/current.txt
@@ -6,7 +6,7 @@
     method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
     method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
     method public final void onStopped();
-    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @Deprecated public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
     property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
diff --git a/work/workmanager-ktx/api/public_plus_experimental_current.txt b/work/workmanager-ktx/api/public_plus_experimental_current.txt
index 2c5f419..301ee025 100644
--- a/work/workmanager-ktx/api/public_plus_experimental_current.txt
+++ b/work/workmanager-ktx/api/public_plus_experimental_current.txt
@@ -5,8 +5,10 @@
     ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
     method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
     method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method @androidx.work.ExperimentalExpeditedWork public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo> $completion);
+    method @androidx.work.ExperimentalExpeditedWork public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
     method public final void onStopped();
-    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @Deprecated public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
     property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
diff --git a/work/workmanager-ktx/api/restricted_current.txt b/work/workmanager-ktx/api/restricted_current.txt
index 2c5f419..6551353 100644
--- a/work/workmanager-ktx/api/restricted_current.txt
+++ b/work/workmanager-ktx/api/restricted_current.txt
@@ -6,7 +6,7 @@
     method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
     method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
     method public final void onStopped();
-    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @Deprecated public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
     property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
diff --git a/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt b/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt
index 81d2a56..33d2bfc 100644
--- a/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt
+++ b/work/workmanager-ktx/src/main/java/androidx/work/CoroutineWorker.kt
@@ -24,6 +24,7 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
+import java.lang.IllegalStateException
 
 /**
  * A [ListenableWorker] implementation that provides interop with Kotlin Coroutines.  Override
@@ -62,7 +63,6 @@
 
     @Suppress("DEPRECATION")
     public final override fun startWork(): ListenableFuture<Result> {
-
         val coroutineScope = CoroutineScope(coroutineContext + job)
         coroutineScope.launch {
             try {
@@ -72,7 +72,6 @@
                 future.setException(t)
             }
         }
-
         return future
     }
 
@@ -93,6 +92,17 @@
     public abstract suspend fun doWork(): Result
 
     /**
+     * @return The [ForegroundInfo] instance if the [WorkRequest] is marked as expedited.
+     *
+     * @throws [IllegalStateException] when not overridden. Override this method when the
+     * corresponding [WorkRequest] is marked expedited.
+     */
+    @ExperimentalExpeditedWork
+    public open suspend fun getForegroundInfo(): ForegroundInfo {
+        throw IllegalStateException("Not implemented")
+    }
+
+    /**
      * Updates the progress for the [CoroutineWorker]. This is a suspending function unlike the
      * [setProgressAsync] API which returns a [ListenableFuture].
      *
@@ -109,12 +119,30 @@
      *
      * @param foregroundInfo The [ForegroundInfo]
      */
+    @Deprecated(
+        message = "Use WorkRequest.Builder.setExpedited() and ListenableWorker.getForegroundInfo()",
+        replaceWith = ReplaceWith("TODO(\"Replace with getForegroundInfo()\")"),
+        level = DeprecationLevel.WARNING
+    )
+    @Suppress("DEPRECATION")
     public suspend fun setForeground(foregroundInfo: ForegroundInfo) {
         setForegroundAsync(foregroundInfo).await()
     }
 
+    @Suppress("DEPRECATION")
+    @ExperimentalExpeditedWork
+    public final override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
+        val job = Job()
+        val scope = CoroutineScope(coroutineContext + job)
+        val jobFuture = JobListenableFuture<ForegroundInfo>(job)
+        scope.launch {
+            jobFuture.complete(getForegroundInfo())
+        }
+        return jobFuture
+    }
+
     public final override fun onStopped() {
         super.onStopped()
         future.cancel(false)
     }
-}
\ No newline at end of file
+}
diff --git a/work/workmanager-ktx/src/main/java/androidx/work/ListenableFuture.kt b/work/workmanager-ktx/src/main/java/androidx/work/ListenableFuture.kt
index de621e3..6ae7f33 100644
--- a/work/workmanager-ktx/src/main/java/androidx/work/ListenableFuture.kt
+++ b/work/workmanager-ktx/src/main/java/androidx/work/ListenableFuture.kt
@@ -19,7 +19,9 @@
 package androidx.work
 
 import androidx.annotation.RestrictTo
+import androidx.work.impl.utils.futures.SettableFuture
 import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.suspendCancellableCoroutine
 import java.util.concurrent.CancellationException
 import java.util.concurrent.ExecutionException
@@ -60,3 +62,26 @@
         )
     }
 }
+
+/**
+ * A special [Job] to [ListenableFuture] wrapper.
+ */
+internal class JobListenableFuture<R>(
+    private val job: Job,
+    private val underlying: SettableFuture<R> = SettableFuture.create()
+) : ListenableFuture<R> by underlying {
+
+    public fun complete(result: R) {
+        underlying.set(result)
+    }
+
+    init {
+        job.invokeOnCompletion { throwable: Throwable? ->
+            when (throwable) {
+                null -> require(underlying.isDone)
+                is CancellationException -> underlying.cancel(true)
+                else -> underlying.setException(throwable.cause ?: throwable)
+            }
+        }
+    }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
index d98d45f..ea7ed12 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
@@ -25,6 +25,7 @@
 import androidx.work.Data
 import androidx.work.NetworkType
 import androidx.work.OneTimeWorkRequest
+import androidx.work.OutOfQuotaPolicy
 import androidx.work.WorkRequest
 import androidx.work.multiprocess.parcelable.ParcelConverters
 import androidx.work.multiprocess.parcelable.ParcelableWorkRequest
@@ -120,6 +121,7 @@
             requests += OneTimeWorkRequest.Builder(TestWorker::class.java)
                 .addTag("Test Worker")
                 .keepResultsForAtLeast(1, TimeUnit.DAYS)
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
                 .build()
         }
         assertOn(requests)
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
index e784b12..4923b28 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
@@ -18,8 +18,12 @@
 
 import static androidx.work.impl.model.WorkTypeConverters.backoffPolicyToInt;
 import static androidx.work.impl.model.WorkTypeConverters.intToBackoffPolicy;
+import static androidx.work.impl.model.WorkTypeConverters.intToOutOfQuotaPolicy;
 import static androidx.work.impl.model.WorkTypeConverters.intToState;
+import static androidx.work.impl.model.WorkTypeConverters.outOfQuotaPolicyToInt;
 import static androidx.work.impl.model.WorkTypeConverters.stateToInt;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.readBooleanValue;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.writeBooleanValue;
 
 import android.annotation.SuppressLint;
 import android.os.Parcel;
@@ -89,6 +93,10 @@
         workSpec.minimumRetentionDuration = in.readLong();
         // scheduleRequestedAt
         workSpec.scheduleRequestedAt = in.readLong();
+        // expedited
+        workSpec.expedited = readBooleanValue(in);
+        // fallback
+        workSpec.outOfQuotaPolicy = intToOutOfQuotaPolicy(in.readInt());
         mWorkRequest = new WorkRequestHolder(UUID.fromString(id), workSpec, tagsSet);
     }
 
@@ -149,6 +157,10 @@
         parcel.writeLong(workSpec.minimumRetentionDuration);
         // scheduleRequestedAt
         parcel.writeLong(workSpec.scheduleRequestedAt);
+        // expedited
+        writeBooleanValue(parcel, workSpec.expedited);
+        // fallback
+        parcel.writeInt(outOfQuotaPolicyToInt(workSpec.outOfQuotaPolicy));
     }
 
     @NonNull
diff --git a/work/workmanager/api/current.txt b/work/workmanager/api/current.txt
index 54713f5..e22e04e 100644
--- a/work/workmanager/api/current.txt
+++ b/work/workmanager/api/current.txt
@@ -163,7 +163,7 @@
     method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
     method public final boolean isStopped();
     method public void onStopped();
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method @Deprecated public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
     method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
   }
diff --git a/work/workmanager/api/public_plus_experimental_current.txt b/work/workmanager/api/public_plus_experimental_current.txt
index 54713f5..9d9affa 100644
--- a/work/workmanager/api/public_plus_experimental_current.txt
+++ b/work/workmanager/api/public_plus_experimental_current.txt
@@ -129,6 +129,9 @@
     enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
   }
 
+  @experimental.Experimental(level=androidx.annotation.experimental.Experimental.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PACKAGE}) public @interface ExperimentalExpeditedWork {
+  }
+
   public final class ForegroundInfo {
     ctor public ForegroundInfo(int, android.app.Notification);
     ctor public ForegroundInfo(int, android.app.Notification, int);
@@ -154,6 +157,7 @@
   public abstract class ListenableWorker {
     ctor @Keep public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
     method public final android.content.Context getApplicationContext();
+    method @androidx.work.ExperimentalExpeditedWork public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
     method public final java.util.UUID getId();
     method public final androidx.work.Data getInputData();
     method @RequiresApi(28) public final android.net.Network? getNetwork();
@@ -163,7 +167,7 @@
     method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
     method public final boolean isStopped();
     method public void onStopped();
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method @Deprecated public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
     method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
   }
@@ -215,6 +219,11 @@
   public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
   }
 
+  @androidx.work.ExperimentalExpeditedWork public enum OutOfQuotaPolicy {
+    enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+    enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+  }
+
   public final class OverwritingInputMerger extends androidx.work.InputMerger {
     ctor public OverwritingInputMerger();
     method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
@@ -341,6 +350,7 @@
     method public final B setBackoffCriteria(androidx.work.BackoffPolicy, long, java.util.concurrent.TimeUnit);
     method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy, java.time.Duration);
     method public final B setConstraints(androidx.work.Constraints);
+    method @androidx.work.ExperimentalExpeditedWork public B setExpedited(androidx.work.OutOfQuotaPolicy);
     method public B setInitialDelay(long, java.util.concurrent.TimeUnit);
     method @RequiresApi(26) public B setInitialDelay(java.time.Duration);
     method public final B setInputData(androidx.work.Data);
diff --git a/work/workmanager/api/restricted_current.txt b/work/workmanager/api/restricted_current.txt
index 54713f5..e22e04e 100644
--- a/work/workmanager/api/restricted_current.txt
+++ b/work/workmanager/api/restricted_current.txt
@@ -163,7 +163,7 @@
     method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
     method public final boolean isStopped();
     method public void onStopped();
-    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method @Deprecated public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
     method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
   }
diff --git a/work/workmanager/build.gradle b/work/workmanager/build.gradle
index 2ba9ca2..ed9b024 100644
--- a/work/workmanager/build.gradle
+++ b/work/workmanager/build.gradle
@@ -58,11 +58,13 @@
 }
 
 dependencies {
+    implementation("androidx.core:core:1.5.0-beta01")
     annotationProcessor("androidx.room:room-compiler:2.2.5")
     implementation("androidx.room:room-runtime:2.2.5")
     androidTestImplementation("androidx.room:room-testing:2.2.5")
     implementation("androidx.sqlite:sqlite:2.1.0")
     implementation("androidx.sqlite:sqlite-framework:2.1.0")
+    api("androidx.annotation:annotation-experimental:1.0.0")
     api(GUAVA_LISTENABLE_FUTURE)
     api("androidx.lifecycle:lifecycle-livedata:2.1.0")
     api("androidx.startup:startup-runtime:1.0.0")
diff --git a/work/workmanager/lint-baseline.xml b/work/workmanager/lint-baseline.xml
index ef161ab..70a0e49 100644
--- a/work/workmanager/lint-baseline.xml
+++ b/work/workmanager/lint-baseline.xml
@@ -96,7 +96,7 @@
         errorLine2="                                                  ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/Constraints.java"
-            line="408"
+            line="429"
             column="51"/>
     </issue>
 
@@ -107,7 +107,7 @@
         errorLine2="                                               ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/Constraints.java"
-            line="443"
+            line="464"
             column="48"/>
     </issue>
 
@@ -239,7 +239,7 @@
         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="102"
+            line="104"
             column="25"/>
     </issue>
 
@@ -250,7 +250,7 @@
         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="109"
+            line="111"
             column="25"/>
     </issue>
 
@@ -261,7 +261,7 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="111"
+            line="113"
             column="21"/>
     </issue>
 
@@ -272,7 +272,7 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="112"
+            line="114"
             column="21"/>
     </issue>
 
@@ -283,7 +283,7 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="119"
+            line="121"
             column="21"/>
     </issue>
 
@@ -294,7 +294,7 @@
         errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="120"
+            line="122"
             column="21"/>
     </issue>
 
@@ -305,7 +305,7 @@
         errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java"
-            line="130"
+            line="136"
             column="16"/>
     </issue>
 
@@ -393,7 +393,7 @@
         errorLine2="                                                       ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/WorkRequest.java"
-            line="174"
+            line="175"
             column="56"/>
     </issue>
 
@@ -404,7 +404,7 @@
         errorLine2="                                                          ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/WorkRequest.java"
-            line="251"
+            line="252"
             column="59"/>
     </issue>
 
@@ -415,7 +415,7 @@
         errorLine2="                                              ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/WorkRequest.java"
-            line="283"
+            line="284"
             column="47"/>
     </issue>
 
@@ -496,11 +496,11 @@
         errorLine2="                   ~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="178"
+            line="188"
             column="20"/>
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="191"
+            line="201"
             column="17"/>
     </issue>
 
@@ -852,7 +852,7 @@
         errorLine2="                                                    ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/Constraints.java"
-            line="407"
+            line="428"
             column="53"/>
     </issue>
 
@@ -863,7 +863,7 @@
         errorLine2="                                                 ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/Constraints.java"
-            line="442"
+            line="463"
             column="50"/>
     </issue>
 
@@ -1666,7 +1666,7 @@
         errorLine2="            ~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="77"
+            line="78"
             column="13"/>
     </issue>
 
@@ -1677,7 +1677,7 @@
         errorLine2="            ~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="78"
+            line="79"
             column="13"/>
     </issue>
 
@@ -1688,7 +1688,7 @@
         errorLine2="            ~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="79"
+            line="80"
             column="13"/>
     </issue>
 
@@ -1699,7 +1699,7 @@
         errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="80"
+            line="81"
             column="13"/>
     </issue>
 
@@ -1710,7 +1710,7 @@
         errorLine2="                                 ~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java"
-            line="180"
+            line="181"
             column="34"/>
     </issue>
 
@@ -1908,7 +1908,7 @@
         errorLine2="           ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="76"
+            line="77"
             column="12"/>
     </issue>
 
@@ -1919,7 +1919,7 @@
         errorLine2="               ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="365"
+            line="377"
             column="16"/>
     </issue>
 
@@ -1930,7 +1930,7 @@
         errorLine2="               ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="368"
+            line="380"
             column="16"/>
     </issue>
 
@@ -1941,7 +1941,7 @@
         errorLine2="               ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="395"
+            line="407"
             column="16"/>
     </issue>
 
@@ -1952,7 +1952,7 @@
         errorLine2="               ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="398"
+            line="410"
             column="16"/>
     </issue>
 
@@ -1963,7 +1963,7 @@
         errorLine2="               ~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="401"
+            line="413"
             column="16"/>
     </issue>
 
@@ -1974,7 +1974,7 @@
         errorLine2="               ~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="411"
+            line="423"
             column="16"/>
     </issue>
 
@@ -1985,7 +1985,7 @@
         errorLine2="               ~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkSpec.java"
-            line="420"
+            line="432"
             column="16"/>
     </issue>
 
@@ -2051,7 +2051,7 @@
         errorLine2="                                 ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="88"
+            line="98"
             column="34"/>
     </issue>
 
@@ -2062,7 +2062,7 @@
         errorLine2="                  ~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="121"
+            line="131"
             column="19"/>
     </issue>
 
@@ -2073,7 +2073,7 @@
         errorLine2="                                         ~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="154"
+            line="164"
             column="42"/>
     </issue>
 
@@ -2084,7 +2084,7 @@
         errorLine2="                  ~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="175"
+            line="185"
             column="19"/>
     </issue>
 
@@ -2095,7 +2095,7 @@
         errorLine2="                                       ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="196"
+            line="206"
             column="40"/>
     </issue>
 
@@ -2106,7 +2106,7 @@
         errorLine2="                  ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="226"
+            line="236"
             column="19"/>
     </issue>
 
@@ -2117,7 +2117,7 @@
         errorLine2="                  ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="255"
+            line="304"
             column="19"/>
     </issue>
 
@@ -2128,7 +2128,7 @@
         errorLine2="                                                       ~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="255"
+            line="304"
             column="56"/>
     </issue>
 
@@ -2139,7 +2139,7 @@
         errorLine2="                  ~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="293"
+            line="342"
             column="19"/>
     </issue>
 
@@ -2150,7 +2150,7 @@
         errorLine2="                                                                   ~~~~~~">
         <location
             file="src/main/java/androidx/work/impl/model/WorkTypeConverters.java"
-            line="293"
+            line="342"
             column="68"/>
     </issue>
 
@@ -2165,15 +2165,4 @@
             column="16"/>
     </issue>
 
-    <issue
-        id="UnknownNullness"
-        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
-        errorLine1="        public WorkerWrapper build() {"
-        errorLine2="               ~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/work/impl/WorkerWrapper.java"
-            line="685"
-            column="16"/>
-    </issue>
-
 </issues>
diff --git a/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java b/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
index 4ecbbe1..a593404 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
@@ -19,6 +19,7 @@
 import static android.content.Context.MODE_PRIVATE;
 import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL;
 
+import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_11_12;
 import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_3_4;
 import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_4_5;
 import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_6_7;
@@ -27,6 +28,7 @@
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_1;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_10;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_11;
+import static androidx.work.impl.WorkDatabaseMigrations.VERSION_12;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_2;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_3;
 import static androidx.work.impl.WorkDatabaseMigrations.VERSION_4;
@@ -85,6 +87,7 @@
     private static final String COLUMN_SYSTEM_ID = "system_id";
     private static final String COLUMN_ALARM_ID = "alarm_id";
     private static final String COLUMN_RUN_IN_FOREGROUND = "run_in_foreground";
+    private static final String COLUMN_OUT_OF_QUOTA_POLICY = "out_of_quota_policy";
 
     // Queries
     private static final String INSERT_ALARM_INFO = "INSERT INTO alarmInfo VALUES (?, ?)";
@@ -436,6 +439,22 @@
         database.close();
     }
 
+    @Test
+    @MediumTest
+    public void testMigrationVersion11To12() throws IOException {
+        SupportSQLiteDatabase database =
+                mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_11);
+        database = mMigrationTestHelper.runMigrationsAndValidate(
+                TEST_DATABASE,
+                VERSION_12,
+                VALIDATE_DROPPED_TABLES,
+                MIGRATION_11_12);
+
+        assertThat(checkColumnExists(database, TABLE_WORKSPEC, COLUMN_OUT_OF_QUOTA_POLICY),
+                is(true));
+        database.close();
+    }
+
     @NonNull
     private ContentValues contentValues(String workSpecId) {
         ContentValues contentValues = new ContentValues();
diff --git a/work/workmanager/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt b/work/workmanager/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt
new file mode 100644
index 0000000..8babf02
--- /dev/null
+++ b/work/workmanager/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt
@@ -0,0 +1,179 @@
+/*
+ * 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 androidx.work
+
+import android.app.Notification
+import android.content.Context
+import android.util.Log
+import androidx.core.os.BuildCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.work.impl.utils.SynchronousExecutor
+import androidx.work.impl.utils.WorkForegroundRunnable
+import androidx.work.impl.utils.futures.SettableFuture
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.worker.TestWorker
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.util.UUID
+import java.util.concurrent.Executor
+
+@RunWith(AndroidJUnit4::class)
+public class WorkForegroundRunnableTest : DatabaseTest() {
+    private lateinit var context: Context
+    private lateinit var configuration: Configuration
+    private lateinit var executor: Executor
+    private lateinit var progressUpdater: ProgressUpdater
+    private lateinit var foregroundUpdater: ForegroundUpdater
+    private lateinit var taskExecutor: TaskExecutor
+
+    @Before
+    public fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().targetContext
+        executor = SynchronousExecutor()
+        configuration = Configuration.Builder()
+            .setMinimumLoggingLevel(Log.DEBUG)
+            .setExecutor(executor)
+            .build()
+        progressUpdater = mock(ProgressUpdater::class.java)
+        foregroundUpdater = mock(ForegroundUpdater::class.java)
+        taskExecutor = InstantWorkTaskExecutor()
+    }
+
+    @Test
+    @MediumTest
+    @SdkSuppress(maxSdkVersion = 30)
+    public fun doesNothing_forRegularWorkRequests() {
+        val work = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .build()
+
+        insertWork(work)
+        val worker = spy(
+            configuration.mWorkerFactory.createWorkerWithDefaultFallback(
+                context,
+                work.workSpec.workerClassName,
+                newWorkerParams(work)
+            )!!
+        )
+        val runnable = WorkForegroundRunnable(
+            context,
+            work.workSpec,
+            worker,
+            foregroundUpdater,
+            taskExecutor
+        )
+        runnable.run()
+        assertThat(runnable.future.isDone, `is`(equalTo(true)))
+        verifyNoMoreInteractions(foregroundUpdater)
+    }
+
+    @Test
+    @MediumTest
+    public fun callGetForeground_forExpeditedWork1() {
+        if (BuildCompat.isAtLeastS()) {
+            return
+        }
+
+        val work = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+            .build()
+
+        insertWork(work)
+        val worker = spy(
+            configuration.mWorkerFactory.createWorkerWithDefaultFallback(
+                context,
+                work.workSpec.workerClassName,
+                newWorkerParams(work)
+            )!!
+        )
+        val runnable = WorkForegroundRunnable(
+            context,
+            work.workSpec,
+            worker,
+            foregroundUpdater,
+            taskExecutor
+        )
+        runnable.run()
+        verify(worker).foregroundInfoAsync
+        assertThat(runnable.future.isDone, `is`(equalTo(true)))
+    }
+
+    @Test
+    @SmallTest
+    public fun callGetForeground_forExpeditedWork2() {
+        if (BuildCompat.isAtLeastS()) {
+            return
+        }
+
+        val work = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+            .build()
+
+        insertWork(work)
+        val worker = spy(
+            configuration.mWorkerFactory.createWorkerWithDefaultFallback(
+                context,
+                work.workSpec.workerClassName,
+                newWorkerParams(work)
+            )!!
+        )
+
+        val notification = mock(Notification::class.java)
+        val id = 10
+        val foregroundInfo = ForegroundInfo(id, notification)
+        val foregroundFuture = SettableFuture.create<ForegroundInfo>()
+        foregroundFuture.set(foregroundInfo)
+        `when`(worker.foregroundInfoAsync).thenReturn(foregroundFuture)
+        val runnable = WorkForegroundRunnable(
+            context,
+            work.workSpec,
+            worker,
+            foregroundUpdater,
+            taskExecutor
+        )
+        runnable.run()
+        verify(worker).foregroundInfoAsync
+        verify(foregroundUpdater).setForegroundAsync(context, work.id, foregroundInfo)
+        assertThat(runnable.future.isDone, `is`(equalTo(true)))
+    }
+
+    private fun newWorkerParams(workRequest: WorkRequest) = WorkerParameters(
+        UUID.fromString(workRequest.stringId),
+        Data.EMPTY,
+        listOf<String>(),
+        WorkerParameters.RuntimeExtras(),
+        1,
+        executor,
+        taskExecutor,
+        configuration.mWorkerFactory,
+        progressUpdater,
+        foregroundUpdater
+    )
+}
diff --git a/work/workmanager/src/androidTest/java/androidx/work/WorkTest.java b/work/workmanager/src/androidTest/java/androidx/work/WorkTest.java
index 5237099..4c526d8 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/WorkTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/WorkTest.java
@@ -16,11 +16,15 @@
 
 package androidx.work;
 
+import static androidx.work.NetworkType.METERED;
+import static androidx.work.NetworkType.NOT_REQUIRED;
+
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 import androidx.work.impl.model.WorkSpec;
@@ -139,4 +143,97 @@
                 .setInitialDelay(Long.MAX_VALUE - now, TimeUnit.MILLISECONDS)
                 .build();
     }
+
+    @Test
+    public void testBuild_expedited_noConstraints() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(NOT_REQUIRED));
+    }
+
+    @Test
+    public void testBuild_expedited_networkConstraints() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .setConstraints(new Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.METERED)
+                        .build()
+                )
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(METERED));
+    }
+
+    @Test
+    public void testBuild_expedited_networkStorageConstraints() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .setConstraints(new Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.METERED)
+                        .setRequiresStorageNotLow(true)
+                        .build()
+                )
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(METERED));
+    }
+
+    @Test
+    public void testBuild_expedited_withUnspportedConstraints() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        mThrown.expect(IllegalArgumentException.class);
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .setConstraints(new Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.METERED)
+                        .setRequiresStorageNotLow(true)
+                        .setRequiresCharging(true)
+                        .build()
+                )
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(METERED));
+    }
+
+    @Test
+    public void testBuild_expedited_withUnspportedConstraints2() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        mThrown.expect(IllegalArgumentException.class);
+        OneTimeWorkRequest request = mBuilder
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .setConstraints(new Constraints.Builder()
+                        .setRequiredNetworkType(NetworkType.METERED)
+                        .setRequiresStorageNotLow(true)
+                        .setRequiresDeviceIdle(true)
+                        .build()
+                )
+                .build();
+        WorkSpec workSpec = request.getWorkSpec();
+        Constraints constraints = workSpec.constraints;
+        assertThat(constraints.getRequiredNetworkType(), is(METERED));
+    }
 }
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
index 32786ae..ac13c43 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
@@ -35,6 +35,7 @@
 import android.net.Uri;
 import android.os.Build;
 
+import androidx.core.os.BuildCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
@@ -241,6 +242,19 @@
         assertThat(jobInfo.isImportantWhileForeground(), is(false));
     }
 
+    @Test
+    @SmallTest
+    public void testConvert_expedited() {
+        if (!BuildCompat.isAtLeastS()) {
+            return;
+        }
+
+        WorkSpec workSpec = new WorkSpec("id", TestWorker.class.getName());
+        workSpec.expedited = true;
+        JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
+        assertThat(jobInfo.isExpedited(), is(true));
+    }
+
     private void convertWithRequiredNetworkType(NetworkType networkType,
                                                 int jobInfoNetworkType,
                                                 int minSdkVersion) {
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/WorkForegroundUpdaterTest.kt b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/WorkForegroundUpdaterTest.kt
index 9d10994..5473eb8 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/utils/WorkForegroundUpdaterTest.kt
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/utils/WorkForegroundUpdaterTest.kt
@@ -18,6 +18,7 @@
 
 import android.app.Notification
 import android.content.Context
+import androidx.core.os.BuildCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
@@ -28,6 +29,7 @@
 import androidx.work.impl.model.WorkSpecDao
 import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor
 import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -37,9 +39,8 @@
 import java.util.UUID
 
 @RunWith(AndroidJUnit4::class)
-// Mockito tries to class load android.os.CancellationSignal which is only available on API >= 16
-@SdkSuppress(minSdkVersion = 16)
-class WorkForegroundUpdaterTest {
+
+public class WorkForegroundUpdaterTest {
 
     private lateinit var mContext: Context
     private lateinit var mDatabase: WorkDatabase
@@ -49,7 +50,7 @@
     private lateinit var mForegroundInfo: ForegroundInfo
 
     @Before
-    fun setUp() {
+    public fun setUp() {
         mContext = mock(Context::class.java)
         mDatabase = mock(WorkDatabase::class.java)
         mWorkSpecDao = mock(WorkSpecDao::class.java)
@@ -62,7 +63,9 @@
 
     @Test(expected = IllegalStateException::class)
     @MediumTest
-    fun setForeground_whenWorkReplaced() {
+    // Mockito tries to class load android.os.CancellationSignal which is only available on API >= 16
+    @SdkSuppress(minSdkVersion = 16, maxSdkVersion = 29)
+    public fun setForeground_whenWorkReplaced() {
         val foregroundUpdater =
             WorkForegroundUpdater(mDatabase, mForegroundProcessor, mTaskExecutor)
         val uuid = UUID.randomUUID()
@@ -75,7 +78,9 @@
 
     @Test(expected = IllegalStateException::class)
     @MediumTest
-    fun setForeground_whenWorkFinished() {
+    // Mockito tries to class load android.os.CancellationSignal which is only available on API >= 16
+    @SdkSuppress(minSdkVersion = 16, maxSdkVersion = 29)
+    public fun setForeground_whenWorkFinished() {
         `when`(mWorkSpecDao.getState(anyString())).thenReturn(WorkInfo.State.SUCCEEDED)
         val foregroundUpdater =
             WorkForegroundUpdater(mDatabase, mForegroundProcessor, mTaskExecutor)
@@ -86,4 +91,23 @@
             throw exception.cause ?: exception
         }
     }
+
+    @MediumTest
+    @SdkSuppress(minSdkVersion = 16)
+    public fun setForeground_onSApi() {
+        if (!BuildCompat.isAtLeastS()) {
+            return
+        }
+        `when`(mWorkSpecDao.getState(anyString())).thenReturn(WorkInfo.State.RUNNING)
+        var exceptional = false
+        val foregroundUpdater =
+            WorkForegroundUpdater(mDatabase, mForegroundProcessor, mTaskExecutor)
+        val uuid = UUID.randomUUID()
+        try {
+            foregroundUpdater.setForegroundAsync(mContext, uuid, mForegroundInfo).get()
+        } catch (exception: IllegalStateException) {
+            exceptional = true
+        }
+        assertTrue(exceptional)
+    }
 }
diff --git a/work/workmanager/src/androidTest/java/androidx/work/worker/StopAwareForegroundWorker.kt b/work/workmanager/src/androidTest/java/androidx/work/worker/StopAwareForegroundWorker.kt
index 9197f1d..0c12308 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/worker/StopAwareForegroundWorker.kt
+++ b/work/workmanager/src/androidTest/java/androidx/work/worker/StopAwareForegroundWorker.kt
@@ -21,6 +21,8 @@
 import androidx.work.ForegroundInfo
 import androidx.work.Worker
 import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
 
 public open class StopAwareForegroundWorker(
     private val context: Context,
@@ -29,13 +31,18 @@
     Worker(context, parameters) {
 
     override fun doWork(): Result {
-        setForegroundAsync(getNotification())
         while (!isStopped) {
             // Do nothing
         }
         return Result.success()
     }
 
+    override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
+        val future = SettableFuture.create<ForegroundInfo>()
+        future.set(getNotification())
+        return future
+    }
+
     private fun getNotification(): ForegroundInfo {
         val notification = NotificationCompat.Builder(context, ChannelId)
             .setOngoing(true)
diff --git a/work/workmanager/src/androidTest/java/androidx/work/worker/TestForegroundWorker.kt b/work/workmanager/src/androidTest/java/androidx/work/worker/TestForegroundWorker.kt
index d75a5ec..2968cf7 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/worker/TestForegroundWorker.kt
+++ b/work/workmanager/src/androidTest/java/androidx/work/worker/TestForegroundWorker.kt
@@ -21,6 +21,8 @@
 import androidx.work.ForegroundInfo
 import androidx.work.Worker
 import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
 
 public open class TestForegroundWorker(
     private val context: Context,
@@ -29,10 +31,15 @@
     Worker(context, parameters) {
 
     override fun doWork(): Result {
-        setForegroundAsync(getNotification()).get()
         return Result.success()
     }
 
+    override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
+        val future = SettableFuture.create<ForegroundInfo>()
+        future.set(getNotification())
+        return future
+    }
+
     private fun getNotification(): ForegroundInfo {
         val notification = NotificationCompat.Builder(context, ChannelId)
             .setOngoing(true)
diff --git a/work/workmanager/src/main/java/androidx/work/Constraints.java b/work/workmanager/src/main/java/androidx/work/Constraints.java
index ad6d5d0..ffca8e3 100644
--- a/work/workmanager/src/main/java/androidx/work/Constraints.java
+++ b/work/workmanager/src/main/java/androidx/work/Constraints.java
@@ -290,6 +290,27 @@
         long mTriggerContentMaxDelay = -1;
         ContentUriTriggers mContentUriTriggers = new ContentUriTriggers();
 
+        public Builder() {
+            // default public constructor
+        }
+
+        /**
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public Builder(@NonNull Constraints constraints) {
+            mRequiresCharging = constraints.requiresCharging();
+            mRequiresDeviceIdle = Build.VERSION.SDK_INT >= 23 && constraints.requiresDeviceIdle();
+            mRequiredNetworkType = constraints.getRequiredNetworkType();
+            mRequiresBatteryNotLow = constraints.requiresBatteryNotLow();
+            mRequiresStorageNotLow = constraints.requiresStorageNotLow();
+            if (Build.VERSION.SDK_INT >= 24) {
+                mTriggerContentUpdateDelay = constraints.getTriggerContentUpdateDelay();
+                mTriggerContentMaxDelay = constraints.getTriggerMaxContentDelay();
+                mContentUriTriggers = constraints.getContentUriTriggers();
+            }
+        }
+
         /**
          * Sets whether device should be charging for the {@link WorkRequest} to run.  The
          * default value is {@code false}.
diff --git a/work/workmanager/src/main/java/androidx/work/ExperimentalExpeditedWork.java b/work/workmanager/src/main/java/androidx/work/ExperimentalExpeditedWork.java
new file mode 100644
index 0000000..0829924
--- /dev/null
+++ b/work/workmanager/src/main/java/androidx/work/ExperimentalExpeditedWork.java
@@ -0,0 +1,39 @@
+/*
+ * 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 androidx.work;
+
+import static androidx.annotation.experimental.Experimental.Level.ERROR;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.experimental.Experimental;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * An API surface for expedited {@link WorkRequest}s.
+ */
+@Retention(CLASS)
+@Target({TYPE, METHOD, PACKAGE})
+@Experimental(level = ERROR)
+public @interface ExperimentalExpeditedWork {
+
+}
diff --git a/work/workmanager/src/main/java/androidx/work/ListenableWorker.java b/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
index 71b38e0..0c8877a 100644
--- a/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
+++ b/work/workmanager/src/main/java/androidx/work/ListenableWorker.java
@@ -28,6 +28,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.work.impl.utils.futures.SettableFuture;
 import androidx.work.impl.utils.taskexecutor.TaskExecutor;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -219,8 +220,11 @@
      * @param foregroundInfo The {@link ForegroundInfo}
      * @return A {@link ListenableFuture} which resolves after the {@link ListenableWorker}
      * transitions to running in the context of a foreground {@link android.app.Service}.
+     * @deprecated Use {@link WorkRequest.Builder#setExpedited(OutOfQuotaPolicy)} and
+     * {@link ListenableWorker#getForegroundInfoAsync()} instead.
      */
     @NonNull
+    @Deprecated
     public final ListenableFuture<Void> setForegroundAsync(@NonNull ForegroundInfo foregroundInfo) {
         mRunInForeground = true;
         return mWorkerParams.getForegroundUpdater()
@@ -228,11 +232,36 @@
     }
 
     /**
+     * Return an instance of {@link  ForegroundInfo} if the {@link WorkRequest} is important to
+     * the user.  In this case, WorkManager provides a signal to the OS that the process should
+     * be kept alive while this work is executing.
+     * <p>
+     * Prior to Android S, WorkManager manages and runs a foreground service on your behalf to
+     * execute the WorkRequest, showing the notification provided in the {@link ForegroundInfo}.
+     * To update this notification subsequently, the application can use
+     * {@link android.app.NotificationManager}.
+     * <p>
+     * Starting in Android S and above, WorkManager manages this WorkRequest using an immediate job.
+     *
+     * @return A {@link ListenableFuture} of {@link ForegroundInfo} instance if the WorkRequest
+     * is marked immediate. For more information look at
+     * {@link WorkRequest.Builder#setExpedited(OutOfQuotaPolicy)}.
+     */
+    @NonNull
+    @ExperimentalExpeditedWork
+    public ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
+        SettableFuture<ForegroundInfo> future = SettableFuture.create();
+        future.setException(new IllegalStateException("Not implemented"));
+        return future;
+    }
+
+    /**
      * Returns {@code true} if this Worker has been told to stop.  This could be because of an
      * explicit cancellation signal by the user, or because the system has decided to preempt the
      * task. In these cases, the results of the work will be ignored by WorkManager and it is safe
      * to stop the computation.  WorkManager will retry the work at a later time if necessary.
      *
+     *
      * @return {@code true} if the work operation has been interrupted
      */
     public final boolean isStopped() {
@@ -296,6 +325,14 @@
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public void setRunInForeground(boolean runInForeground) {
+        mRunInForeground = runInForeground;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public @NonNull Executor getBackgroundExecutor() {
         return mWorkerParams.getBackgroundExecutor();
     }
diff --git a/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java b/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java
index 51ba3e6..d33851e 100644
--- a/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/OneTimeWorkRequest.java
@@ -107,12 +107,6 @@
                 throw new IllegalArgumentException(
                         "Cannot set backoff criteria on an idle mode job");
             }
-            if (mWorkSpec.runInForeground
-                    && Build.VERSION.SDK_INT >= 23
-                    && mWorkSpec.constraints.requiresDeviceIdle()) {
-                throw new IllegalArgumentException(
-                        "Cannot run in foreground with an idle mode constraint");
-            }
             return new OneTimeWorkRequest(this);
         }
 
diff --git a/work/workmanager/src/main/java/androidx/work/OutOfQuotaPolicy.java b/work/workmanager/src/main/java/androidx/work/OutOfQuotaPolicy.java
new file mode 100644
index 0000000..5f25216
--- /dev/null
+++ b/work/workmanager/src/main/java/androidx/work/OutOfQuotaPolicy.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 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 androidx.work;
+
+/**
+ * An enumeration of policies that help determine out of quota behavior for expedited jobs.
+ */
+@ExperimentalExpeditedWork
+public enum OutOfQuotaPolicy {
+
+    /**
+     * When the app does not have any expedited job quota, the expedited work request will
+     * fallback to a regular work request.
+     */
+    RUN_AS_NON_EXPEDITED_WORK_REQUEST,
+
+    /**
+     * When the app does not have any expedited job quota, the expedited work request will
+     * we dropped and no work requests are enqueued.
+     */
+    DROP_WORK_REQUEST;
+}
diff --git a/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java b/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java
index 6fd553a..fb44604 100644
--- a/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/PeriodicWorkRequest.java
@@ -189,12 +189,6 @@
                 throw new IllegalArgumentException(
                         "Cannot set backoff criteria on an idle mode job");
             }
-            if (mWorkSpec.runInForeground
-                    && Build.VERSION.SDK_INT >= 23
-                    && mWorkSpec.constraints.requiresDeviceIdle()) {
-                throw new IllegalArgumentException(
-                        "Cannot run in foreground with an idle mode constraint");
-            }
             return new PeriodicWorkRequest(this);
         }
 
diff --git a/work/workmanager/src/main/java/androidx/work/WorkRequest.java b/work/workmanager/src/main/java/androidx/work/WorkRequest.java
index 25fed73..247ed84 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkRequest.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkRequest.java
@@ -16,6 +16,7 @@
 package androidx.work;
 
 import android.annotation.SuppressLint;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
@@ -289,12 +290,38 @@
         }
 
         /**
+         * Marks the {@link WorkRequest} as important to the user.  In this case, WorkManager
+         * provides an additional signal to the OS that this work is important.
+         *
+         * @param policy The {@link OutOfQuotaPolicy} to be used.
+         */
+        @ExperimentalExpeditedWork
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public @NonNull B setExpedited(@NonNull OutOfQuotaPolicy policy) {
+            mWorkSpec.expedited = true;
+            mWorkSpec.outOfQuotaPolicy = policy;
+            return getThis();
+        }
+
+        /**
          * Builds a {@link WorkRequest} based on this {@link Builder}.
          *
          * @return A {@link WorkRequest} based on this {@link Builder}
          */
         public final @NonNull W build() {
             W returnValue = buildInternal();
+            Constraints constraints = mWorkSpec.constraints;
+            // Check for unsupported constraints.
+            boolean hasUnsupportedConstraints =
+                    (Build.VERSION.SDK_INT >= 24 && constraints.hasContentUriTriggers())
+                            || constraints.requiresBatteryNotLow()
+                            || constraints.requiresCharging()
+                            || (Build.VERSION.SDK_INT >= 23 && constraints.requiresDeviceIdle());
+
+            if (mWorkSpec.expedited && hasUnsupportedConstraints) {
+                throw new IllegalArgumentException(
+                        "Expedited jobs only support network and storage constraints");
+            }
             // Create a new id and WorkSpec so this WorkRequest.Builder can be used multiple times.
             mId = UUID.randomUUID();
             mWorkSpec = new WorkSpec(mWorkSpec);
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
index cccc41a..5073e66 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabase.java
@@ -75,7 +75,7 @@
         WorkName.class,
         WorkProgress.class,
         Preference.class},
-        version = 11)
+        version = 12)
 @TypeConverters(value = {Data.class, WorkTypeConverters.class})
 public abstract class WorkDatabase extends RoomDatabase {
     // Delete rows in the workspec table that...
@@ -150,6 +150,7 @@
                 .addMigrations(
                         new WorkDatabaseMigrations.RescheduleMigration(context, VERSION_10,
                                 VERSION_11))
+                .addMigrations(WorkDatabaseMigrations.MIGRATION_11_12)
                 .fallbackToDestructiveMigration()
                 .build();
     }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
index 9261f37..7a1d045 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
@@ -59,6 +59,7 @@
     public static final int VERSION_9 = 9;
     public static final int VERSION_10 = 10;
     public static final int VERSION_11 = 11;
+    public static final int VERSION_12 = 12;
 
     private static final String CREATE_SYSTEM_ID_INFO =
             "CREATE TABLE IF NOT EXISTS `SystemIdInfo` (`work_spec_id` TEXT NOT NULL, `system_id`"
@@ -106,6 +107,9 @@
             "CREATE TABLE IF NOT EXISTS `Preference` (`key` TEXT NOT NULL, `long_value` INTEGER, "
                     + "PRIMARY KEY(`key`))";
 
+    private static final String CREATE_OUT_OF_QUOTA_POLICY =
+            "ALTER TABLE workspec ADD COLUMN `out_of_quota_policy` INTEGER NOT NULL DEFAULT 0";
+
     /**
      * Removes the {@code alarmInfo} table and substitutes it for a more general
      * {@code SystemIdInfo} table.
@@ -228,4 +232,15 @@
             IdGenerator.migrateLegacyIdGenerator(mContext, database);
         }
     }
+
+    /**
+     * Adds a notification_provider to the {@link WorkSpec}.
+     */
+    @NonNull
+    public static Migration MIGRATION_11_12 = new Migration(VERSION_11, VERSION_12) {
+        @Override
+        public void migrate(@NonNull SupportSQLiteDatabase database) {
+            database.execSQL(CREATE_OUT_OF_QUOTA_POLICY);
+        }
+    };
 }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
index 744d88d..a156b0a 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkManagerImpl.java
@@ -16,6 +16,7 @@
 
 package androidx.work.impl;
 
+import static android.app.PendingIntent.FLAG_MUTABLE;
 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 import static android.text.TextUtils.isEmpty;
 
@@ -31,6 +32,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.arch.core.util.Function;
+import androidx.core.os.BuildCompat;
 import androidx.lifecycle.LiveData;
 import androidx.work.Configuration;
 import androidx.work.ExistingPeriodicWorkPolicy;
@@ -475,7 +477,11 @@
     @Override
     public PendingIntent createCancelPendingIntent(@NonNull UUID id) {
         Intent intent = createCancelWorkIntent(mContext, id.toString());
-        return PendingIntent.getService(mContext, 0, intent, FLAG_UPDATE_CURRENT);
+        int flags = FLAG_UPDATE_CURRENT;
+        if (BuildCompat.isAtLeastS()) {
+            flags |= FLAG_MUTABLE;
+        }
+        return PendingIntent.getService(mContext, 0, intent, flags);
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
index 26654a4..12b8eee 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
@@ -48,6 +48,7 @@
 import androidx.work.impl.model.WorkSpecDao;
 import androidx.work.impl.model.WorkTagDao;
 import androidx.work.impl.utils.PackageManagerHelper;
+import androidx.work.impl.utils.WorkForegroundRunnable;
 import androidx.work.impl.utils.WorkForegroundUpdater;
 import androidx.work.impl.utils.WorkProgressUpdater;
 import androidx.work.impl.utils.futures.SettableFuture;
@@ -82,13 +83,13 @@
     // Avoid Synthetic accessor
     WorkSpec mWorkSpec;
     ListenableWorker mWorker;
+    TaskExecutor mWorkTaskExecutor;
 
     // Package-private for synthetic accessor.
     @NonNull
     ListenableWorker.Result mResult = ListenableWorker.Result.failure();
 
     private Configuration mConfiguration;
-    private TaskExecutor mWorkTaskExecutor;
     private ForegroundProcessor mForegroundProcessor;
     private WorkDatabase mWorkDatabase;
     private WorkSpecDao mWorkSpecDao;
@@ -226,7 +227,7 @@
             input = inputMerger.merge(inputs);
         }
 
-        WorkerParameters params = new WorkerParameters(
+        final WorkerParameters params = new WorkerParameters(
                 UUID.fromString(mWorkSpecId),
                 input,
                 mTags,
@@ -272,22 +273,32 @@
             }
 
             final SettableFuture<ListenableWorker.Result> future = SettableFuture.create();
-            // Call mWorker.startWork() on the main thread.
-            mWorkTaskExecutor.getMainThreadExecutor()
-                    .execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            try {
-                                Logger.get().debug(TAG, String.format("Starting work for %s",
-                                        mWorkSpec.workerClassName));
-                                mInnerFuture = mWorker.startWork();
-                                future.setFuture(mInnerFuture);
-                            } catch (Throwable e) {
-                                future.setException(e);
-                            }
+            final WorkForegroundRunnable foregroundRunnable =
+                    new WorkForegroundRunnable(
+                            mAppContext,
+                            mWorkSpec,
+                            mWorker,
+                            params.getForegroundUpdater(),
+                            mWorkTaskExecutor
+                    );
+            mWorkTaskExecutor.getMainThreadExecutor().execute(foregroundRunnable);
 
-                        }
-                    });
+            final ListenableFuture<Void> runExpedited = foregroundRunnable.getFuture();
+            runExpedited.addListener(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        runExpedited.get();
+                        Logger.get().debug(TAG,
+                                String.format("Starting work for %s", mWorkSpec.workerClassName));
+                        // Call mWorker.startWork() on the main thread.
+                        mInnerFuture = mWorker.startWork();
+                        future.setFuture(mInnerFuture);
+                    } catch (Throwable e) {
+                        future.setException(e);
+                    }
+                }
+            }, mWorkTaskExecutor.getMainThreadExecutor());
 
             // Avoid synthetic accessors.
             final String workDescription = mWorkDescription;
@@ -681,6 +692,7 @@
         /**
          * @return The instance of {@link WorkerWrapper}.
          */
+        @NonNull
         public WorkerWrapper build() {
             return new WorkerWrapper(this);
         }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index ac259b5..3ed5f949 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -31,6 +31,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
 import androidx.work.BackoffPolicy;
 import androidx.work.Constraints;
 import androidx.work.ContentUriTriggers;
@@ -66,7 +67,7 @@
      * Note: All {@link JobInfo} are set to persist on reboot.
      *
      * @param workSpec The {@link WorkSpec} to convert
-     * @param jobId The {@code jobId} to use. This is useful when de-duping jobs on reschedule.
+     * @param jobId    The {@code jobId} to use. This is useful when de-duping jobs on reschedule.
      * @return The {@link JobInfo} representing the same information as the {@link WorkSpec}
      */
     JobInfo convert(WorkSpec workSpec, int jobId) {
@@ -97,11 +98,12 @@
             // always setMinimumLatency to make sure we have at least one constraint.
             // See aosp/5434530 & b/6771687
             builder.setMinimumLatency(offset);
-        } else  {
+        } else {
             if (offset > 0) {
                 // Only set a minimum latency when applicable.
                 builder.setMinimumLatency(offset);
-            } else {
+            } else if (!workSpec.expedited) {
+                // Only set this if the workSpec is not expedited.
                 builder.setImportantWhileForeground(true);
             }
         }
@@ -122,6 +124,10 @@
             builder.setRequiresBatteryNotLow(constraints.requiresBatteryNotLow());
             builder.setRequiresStorageNotLow(constraints.requiresStorageNotLow());
         }
+
+        if (BuildCompat.isAtLeastS() && workSpec.expedited) {
+            builder.setExpedited(true);
+        }
         return builder.build();
     }
 
@@ -163,7 +169,7 @@
      */
     @SuppressWarnings("MissingCasesInEnumSwitch")
     static int convertNetworkType(NetworkType networkType) {
-        switch(networkType) {
+        switch (networkType) {
             case NOT_REQUIRED:
                 return JobInfo.NETWORK_TYPE_NONE;
             case CONNECTED:
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index 6e39078..dcbb0d5e 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -17,6 +17,7 @@
 
 import static android.content.Context.JOB_SCHEDULER_SERVICE;
 
+import static androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
 import static androidx.work.impl.background.systemjob.SystemJobInfoConverter.EXTRA_WORK_SPEC_ID;
 import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
 
@@ -183,7 +184,20 @@
                 TAG,
                 String.format("Scheduling work ID %s Job ID %s", workSpec.id, jobId));
         try {
-            mJobScheduler.schedule(jobInfo);
+            int result = mJobScheduler.schedule(jobInfo);
+            if (result == JobScheduler.RESULT_FAILURE) {
+                Logger.get()
+                        .warning(TAG, String.format("Unable to schedule work ID %s", workSpec.id));
+                if (workSpec.expedited
+                        && workSpec.outOfQuotaPolicy == RUN_AS_NON_EXPEDITED_WORK_REQUEST) {
+                    // Falling back to a non-expedited job.
+                    workSpec.expedited = false;
+                    String message = String.format(
+                            "Scheduling a non-expedited job (work ID %s)", workSpec.id);
+                    Logger.get().debug(TAG, message);
+                    scheduleInternal(workSpec, jobId);
+                }
+            }
         } catch (IllegalStateException e) {
             // This only gets thrown if we exceed 100 jobs.  Let's figure out if WorkManager is
             // responsible for all these jobs.
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
index 86bfa80..245f432 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpec.java
@@ -36,6 +36,7 @@
 import androidx.work.Constraints;
 import androidx.work.Data;
 import androidx.work.Logger;
+import androidx.work.OutOfQuotaPolicy;
 import androidx.work.WorkInfo;
 import androidx.work.WorkRequest;
 
@@ -129,10 +130,19 @@
     public long scheduleRequestedAt = SCHEDULE_NOT_REQUESTED_YET;
 
     /**
-     * This is {@code true} when the WorkSpec needs to be hosted by a foreground service.
+     * This is {@code true} when the WorkSpec needs to be hosted by a foreground service or a
+     * high priority job.
      */
     @ColumnInfo(name = "run_in_foreground")
-    public boolean runInForeground;
+    public boolean expedited;
+
+    /**
+     * When set to <code>true</code> this {@link WorkSpec} falls back to a regular job when
+     * an application runs out of expedited job quota.
+     */
+    @NonNull
+    @ColumnInfo(name = "out_of_quota_policy")
+    public OutOfQuotaPolicy outOfQuotaPolicy = OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
 
     public WorkSpec(@NonNull String id, @NonNull String workerClassName) {
         this.id = id;
@@ -156,7 +166,8 @@
         periodStartTime = other.periodStartTime;
         minimumRetentionDuration = other.minimumRetentionDuration;
         scheduleRequestedAt = other.scheduleRequestedAt;
-        runInForeground = other.runInForeground;
+        expedited = other.expedited;
+        outOfQuotaPolicy = other.outOfQuotaPolicy;
     }
 
     /**
@@ -174,7 +185,6 @@
         this.backoffDelayDuration = backoffDelayDuration;
     }
 
-
     public boolean isPeriodic() {
         return intervalDuration != 0L;
     }
@@ -301,7 +311,7 @@
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
-        if (!(o instanceof WorkSpec)) return false;
+        if (o == null || getClass() != o.getClass()) return false;
 
         WorkSpec workSpec = (WorkSpec) o;
 
@@ -313,7 +323,7 @@
         if (periodStartTime != workSpec.periodStartTime) return false;
         if (minimumRetentionDuration != workSpec.minimumRetentionDuration) return false;
         if (scheduleRequestedAt != workSpec.scheduleRequestedAt) return false;
-        if (runInForeground != workSpec.runInForeground) return false;
+        if (expedited != workSpec.expedited) return false;
         if (!id.equals(workSpec.id)) return false;
         if (state != workSpec.state) return false;
         if (!workerClassName.equals(workSpec.workerClassName)) return false;
@@ -325,7 +335,8 @@
         if (!input.equals(workSpec.input)) return false;
         if (!output.equals(workSpec.output)) return false;
         if (!constraints.equals(workSpec.constraints)) return false;
-        return backoffPolicy == workSpec.backoffPolicy;
+        if (backoffPolicy != workSpec.backoffPolicy) return false;
+        return outOfQuotaPolicy == workSpec.outOfQuotaPolicy;
     }
 
     @Override
@@ -346,7 +357,8 @@
         result = 31 * result + (int) (periodStartTime ^ (periodStartTime >>> 32));
         result = 31 * result + (int) (minimumRetentionDuration ^ (minimumRetentionDuration >>> 32));
         result = 31 * result + (int) (scheduleRequestedAt ^ (scheduleRequestedAt >>> 32));
-        result = 31 * result + (runInForeground ? 1 : 0);
+        result = 31 * result + (expedited ? 1 : 0);
+        result = 31 * result + outOfQuotaPolicy.hashCode();
         return result;
     }
 
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
index 4e665ea..65e842a 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
@@ -28,10 +28,12 @@
 import android.net.Uri;
 import android.os.Build;
 
+import androidx.annotation.NonNull;
 import androidx.room.TypeConverter;
 import androidx.work.BackoffPolicy;
 import androidx.work.ContentUriTriggers;
 import androidx.work.NetworkType;
+import androidx.work.OutOfQuotaPolicy;
 import androidx.work.WorkInfo;
 
 import java.io.ByteArrayInputStream;
@@ -81,6 +83,14 @@
     }
 
     /**
+     * Integer identifiers that map to {@link OutOfQuotaPolicy}.
+     */
+    public interface OutOfPolicyIds {
+        int RUN_AS_NON_EXPEDITED_WORK_REQUEST = 0;
+        int DROP_WORK_REQUEST = 1;
+    }
+
+    /**
      * TypeConverter for a State to an int.
      *
      * @param state The input State
@@ -258,6 +268,45 @@
     }
 
     /**
+     * Converts a {@link OutOfQuotaPolicy} to an int.
+     *
+     * @param policy The {@link OutOfQuotaPolicy} policy being used
+     * @return the corresponding int representation.
+     */
+    @TypeConverter
+    public static int outOfQuotaPolicyToInt(@NonNull OutOfQuotaPolicy policy) {
+        switch (policy) {
+            case RUN_AS_NON_EXPEDITED_WORK_REQUEST:
+                return OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+            case DROP_WORK_REQUEST:
+                return OutOfPolicyIds.DROP_WORK_REQUEST;
+            default:
+                throw new IllegalArgumentException(
+                        "Could not convert " + policy + " to int");
+        }
+    }
+
+    /**
+     * Converter from an int to a {@link OutOfQuotaPolicy}.
+     *
+     * @param value The input integer
+     * @return An {@link OutOfQuotaPolicy}
+     */
+    @TypeConverter
+    @NonNull
+    public static OutOfQuotaPolicy intToOutOfQuotaPolicy(int value) {
+        switch (value) {
+            case OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST:
+                return OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+            case OutOfPolicyIds.DROP_WORK_REQUEST:
+                return OutOfQuotaPolicy.DROP_WORK_REQUEST;
+            default:
+                throw new IllegalArgumentException(
+                        "Could not convert " + value + " to OutOfQuotaPolicy");
+        }
+    }
+
+    /**
      * Converts a list of {@link ContentUriTriggers.Trigger}s to byte array representation
      * @param triggers the list of {@link ContentUriTriggers.Trigger}s to convert
      * @return corresponding byte array representation
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
index 6610214..0c4d3bb 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
@@ -17,13 +17,17 @@
 package androidx.work.impl.utils;
 
 import static android.app.AlarmManager.RTC_WAKEUP;
+import static android.app.ApplicationExitInfo.REASON_USER_REQUESTED;
+import static android.app.PendingIntent.FLAG_MUTABLE;
 import static android.app.PendingIntent.FLAG_NO_CREATE;
 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
 
 import static androidx.work.WorkInfo.State.ENQUEUED;
 import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
 
+import android.app.ActivityManager;
 import android.app.AlarmManager;
+import android.app.ApplicationExitInfo;
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -40,6 +44,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
 import androidx.work.Configuration;
 import androidx.work.InitializationExceptionHandler;
 import androidx.work.Logger;
@@ -150,13 +155,38 @@
         // Even though API 23, 24 are probably safe, OEMs may choose to do
         // something different.
         try {
-            PendingIntent pendingIntent = getPendingIntent(mContext, FLAG_NO_CREATE);
-            if (pendingIntent == null) {
+            int flags = FLAG_NO_CREATE;
+            if (BuildCompat.isAtLeastS()) {
+                flags |= FLAG_MUTABLE;
+            }
+            PendingIntent pendingIntent = getPendingIntent(mContext, flags);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                // We no longer need the alarm.
+                if (pendingIntent != null) {
+                    pendingIntent.cancel();
+                }
+                ActivityManager activityManager =
+                        (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+                List<ApplicationExitInfo> exitInfoList =
+                        activityManager.getHistoricalProcessExitReasons(
+                                null /* match caller uid */,
+                                0, // ignore
+                                0 // ignore
+                        );
+
+                if (exitInfoList != null && !exitInfoList.isEmpty()) {
+                    for (int i = 0; i < exitInfoList.size(); i++) {
+                        ApplicationExitInfo info = exitInfoList.get(i);
+                        if (info.getReason() == REASON_USER_REQUESTED) {
+                            return true;
+                        }
+                    }
+                }
+            } else if (pendingIntent == null) {
                 setAlarm(mContext);
                 return true;
-            } else {
-                return false;
             }
+            return false;
         } catch (SecurityException exception) {
             // Setting Alarms on some devices fails due to OEM introduced bugs in AlarmManager.
             // When this happens, there is not much WorkManager can do, other can reschedule
@@ -274,7 +304,7 @@
     }
 
     /**
-     * @param flags   The {@link PendingIntent} flags.
+     * @param flags The {@link PendingIntent} flags.
      * @return an instance of the {@link PendingIntent}.
      */
     private static PendingIntent getPendingIntent(Context context, int flags) {
@@ -296,7 +326,11 @@
     static void setAlarm(Context context) {
         AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
         // Using FLAG_UPDATE_CURRENT, because we only ever want once instance of this alarm.
-        PendingIntent pendingIntent = getPendingIntent(context, FLAG_UPDATE_CURRENT);
+        int flags = FLAG_UPDATE_CURRENT;
+        if (BuildCompat.isAtLeastS()) {
+            flags |= FLAG_MUTABLE;
+        }
+        PendingIntent pendingIntent = getPendingIntent(context, flags);
         long triggerAt = System.currentTimeMillis() + TEN_YEARS;
         if (alarmManager != null) {
             if (Build.VERSION.SDK_INT >= 19) {
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundRunnable.java b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundRunnable.java
new file mode 100644
index 0000000..dad9d9d
--- /dev/null
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundRunnable.java
@@ -0,0 +1,113 @@
+/*
+ * 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 androidx.work.impl.utils;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+import androidx.work.ForegroundInfo;
+import androidx.work.ForegroundUpdater;
+import androidx.work.ListenableWorker;
+import androidx.work.Logger;
+import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.futures.SettableFuture;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class WorkForegroundRunnable implements Runnable {
+
+    // Synthetic access
+    static final String TAG = Logger.tagWithPrefix("WorkForegroundRunnable");
+
+    final SettableFuture<Void> mFuture;
+
+    final Context mContext;
+    final WorkSpec mWorkSpec;
+    final ListenableWorker mWorker;
+    final ForegroundUpdater mForegroundUpdater;
+    final TaskExecutor mTaskExecutor;
+
+    @SuppressLint("LambdaLast")
+    public WorkForegroundRunnable(
+            @NonNull Context context,
+            @NonNull WorkSpec workSpec,
+            @NonNull ListenableWorker worker,
+            @NonNull ForegroundUpdater foregroundUpdater,
+            @NonNull TaskExecutor taskExecutor) {
+
+        mFuture = SettableFuture.create();
+        mContext = context;
+        mWorkSpec = workSpec;
+        mWorker = worker;
+        mForegroundUpdater = foregroundUpdater;
+        mTaskExecutor = taskExecutor;
+    }
+
+    @NonNull
+    public ListenableFuture<Void> getFuture() {
+        return mFuture;
+    }
+
+    @Override
+    @SuppressLint("UnsafeExperimentalUsageError")
+    public void run() {
+        if (!mWorkSpec.expedited || BuildCompat.isAtLeastS()) {
+            mFuture.set(null);
+            return;
+        }
+
+        final SettableFuture<ForegroundInfo> foregroundFuture = SettableFuture.create();
+        mTaskExecutor.getMainThreadExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                foregroundFuture.setFuture(mWorker.getForegroundInfoAsync());
+            }
+        });
+
+        foregroundFuture.addListener(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    ForegroundInfo foregroundInfo = foregroundFuture.get();
+                    if (foregroundInfo == null) {
+                        String message =
+                                String.format("Worker was marked important (%s) but did not "
+                                        + "provide ForegroundInfo", mWorkSpec.workerClassName);
+                        throw new IllegalStateException(message);
+                    }
+                    Logger.get().debug(TAG, String.format("Updating notification for %s",
+                            mWorkSpec.workerClassName));
+                    // Mark as running in the foreground
+                    mWorker.setRunInForeground(true);
+                    mFuture.setFuture(
+                            mForegroundUpdater.setForegroundAsync(
+                                    mContext, mWorker.getId(), foregroundInfo));
+                } catch (Throwable throwable) {
+                    mFuture.setException(throwable);
+                }
+            }
+        }, mTaskExecutor.getMainThreadExecutor());
+    }
+}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java
index 8e0ac60..27b780e 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/utils/WorkForegroundUpdater.java
@@ -23,8 +23,10 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
 import androidx.work.ForegroundInfo;
 import androidx.work.ForegroundUpdater;
+import androidx.work.Logger;
 import androidx.work.WorkInfo;
 import androidx.work.impl.WorkDatabase;
 import androidx.work.impl.foreground.ForegroundProcessor;
@@ -46,6 +48,8 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class WorkForegroundUpdater implements ForegroundUpdater {
 
+    private static final String TAG = Logger.tagWithPrefix("WMFgUpdater");
+
     private final TaskExecutor mTaskExecutor;
 
     // Synthetic access
@@ -79,6 +83,9 @@
             @Override
             public void run() {
                 try {
+                    if (BuildCompat.isAtLeastS()) {
+                        throw new IllegalStateException("Use an expedited job instead.");
+                    }
                     if (!future.isCancelled()) {
                         String workSpecId = id.toString();
                         WorkInfo.State state = mWorkSpecDao.getState(workSpecId);
diff --git a/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/12.json b/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/12.json
new file mode 100644
index 0000000..dc1d4dd
--- /dev/null
+++ b/work/workmanager/src/schemas/androidx.work.impl.WorkDatabase/12.json
@@ -0,0 +1,460 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 12,
+    "identityHash": "c103703e120ae8cc73c9248622f3cd1e",
+    "entities": [
+      {
+        "tableName": "Dependency",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`work_spec_id` TEXT NOT NULL, `prerequisite_id` TEXT NOT NULL, PRIMARY KEY(`work_spec_id`, `prerequisite_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`prerequisite_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "prerequisiteId",
+            "columnName": "prerequisite_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "work_spec_id",
+            "prerequisite_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_Dependency_work_spec_id",
+            "unique": false,
+            "columnNames": [
+              "work_spec_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Dependency_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+          },
+          {
+            "name": "index_Dependency_prerequisite_id",
+            "unique": false,
+            "columnNames": [
+              "prerequisite_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Dependency_prerequisite_id` ON `${TABLE_NAME}` (`prerequisite_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          },
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "prerequisite_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "WorkSpec",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `state` INTEGER NOT NULL, `worker_class_name` TEXT NOT NULL, `input_merger_class_name` TEXT, `input` BLOB NOT NULL, `output` BLOB NOT NULL, `initial_delay` INTEGER NOT NULL, `interval_duration` INTEGER NOT NULL, `flex_duration` INTEGER NOT NULL, `run_attempt_count` INTEGER NOT NULL, `backoff_policy` INTEGER NOT NULL, `backoff_delay_duration` INTEGER NOT NULL, `period_start_time` INTEGER NOT NULL, `minimum_retention_duration` INTEGER NOT NULL, `schedule_requested_at` INTEGER NOT NULL, `run_in_foreground` INTEGER NOT NULL, `out_of_quota_policy` INTEGER NOT NULL, `required_network_type` INTEGER, `requires_charging` INTEGER NOT NULL, `requires_device_idle` INTEGER NOT NULL, `requires_battery_not_low` INTEGER NOT NULL, `requires_storage_not_low` INTEGER NOT NULL, `trigger_content_update_delay` INTEGER NOT NULL, `trigger_max_content_delay` INTEGER NOT NULL, `content_uri_triggers` BLOB, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "state",
+            "columnName": "state",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "workerClassName",
+            "columnName": "worker_class_name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "inputMergerClassName",
+            "columnName": "input_merger_class_name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "input",
+            "columnName": "input",
+            "affinity": "BLOB",
+            "notNull": true
+          },
+          {
+            "fieldPath": "output",
+            "columnName": "output",
+            "affinity": "BLOB",
+            "notNull": true
+          },
+          {
+            "fieldPath": "initialDelay",
+            "columnName": "initial_delay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "intervalDuration",
+            "columnName": "interval_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "flexDuration",
+            "columnName": "flex_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "runAttemptCount",
+            "columnName": "run_attempt_count",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "backoffPolicy",
+            "columnName": "backoff_policy",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "backoffDelayDuration",
+            "columnName": "backoff_delay_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "periodStartTime",
+            "columnName": "period_start_time",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "minimumRetentionDuration",
+            "columnName": "minimum_retention_duration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "scheduleRequestedAt",
+            "columnName": "schedule_requested_at",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "expedited",
+            "columnName": "run_in_foreground",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "outOfQuotaPolicy",
+            "columnName": "out_of_quota_policy",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mRequiredNetworkType",
+            "columnName": "required_network_type",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "constraints.mRequiresCharging",
+            "columnName": "requires_charging",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mRequiresDeviceIdle",
+            "columnName": "requires_device_idle",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mRequiresBatteryNotLow",
+            "columnName": "requires_battery_not_low",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mRequiresStorageNotLow",
+            "columnName": "requires_storage_not_low",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mTriggerContentUpdateDelay",
+            "columnName": "trigger_content_update_delay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mTriggerMaxContentDelay",
+            "columnName": "trigger_max_content_delay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "constraints.mContentUriTriggers",
+            "columnName": "content_uri_triggers",
+            "affinity": "BLOB",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_WorkSpec_schedule_requested_at",
+            "unique": false,
+            "columnNames": [
+              "schedule_requested_at"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkSpec_schedule_requested_at` ON `${TABLE_NAME}` (`schedule_requested_at`)"
+          },
+          {
+            "name": "index_WorkSpec_period_start_time",
+            "unique": false,
+            "columnNames": [
+              "period_start_time"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkSpec_period_start_time` ON `${TABLE_NAME}` (`period_start_time`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "WorkTag",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `work_spec_id` TEXT NOT NULL, PRIMARY KEY(`tag`, `work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "tag",
+            "columnName": "tag",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "tag",
+            "work_spec_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_WorkTag_work_spec_id",
+            "unique": false,
+            "columnNames": [
+              "work_spec_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkTag_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "SystemIdInfo",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`work_spec_id` TEXT NOT NULL, `system_id` INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "systemId",
+            "columnName": "system_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "work_spec_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "WorkName",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `work_spec_id` TEXT NOT NULL, PRIMARY KEY(`name`, `work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "workSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "name",
+            "work_spec_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_WorkName_work_spec_id",
+            "unique": false,
+            "columnNames": [
+              "work_spec_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_WorkName_work_spec_id` ON `${TABLE_NAME}` (`work_spec_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "WorkProgress",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`work_spec_id` TEXT NOT NULL, `progress` BLOB NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`) REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "mWorkSpecId",
+            "columnName": "work_spec_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "mProgress",
+            "columnName": "progress",
+            "affinity": "BLOB",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "work_spec_id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": [
+          {
+            "table": "WorkSpec",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "work_spec_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "Preference",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `long_value` INTEGER, PRIMARY KEY(`key`))",
+        "fields": [
+          {
+            "fieldPath": "mKey",
+            "columnName": "key",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "mValue",
+            "columnName": "long_value",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "key"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c103703e120ae8cc73c9248622f3cd1e')"
+    ]
+  }
+}
\ No newline at end of file