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<Bundle>, 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