/*
* 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 android.app.appsearch;
import android.annotation.CurrentTimeMillisLong;
import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.appsearch.annotation.CanIgnoreReturnValue;
import android.app.appsearch.flags.Flags;
import android.app.appsearch.safeparcel.GenericDocumentParcel;
import android.app.appsearch.safeparcel.PropertyParcel;
import android.app.appsearch.util.IndentingStringBuilder;
import android.util.Log;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Represents a document unit.
*
*
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.
*
*
Documents are constructed by using the {@link GenericDocument.Builder}.
*
* @see AppSearchSession#put
* @see AppSearchSession#getByDocumentId
* @see AppSearchSession#search
*/
public class GenericDocument {
private static final String TAG = "AppSearchGenericDocumen";
/** The maximum number of indexed properties a document can have. */
private static final int MAX_INDEXED_PROPERTIES = 16;
/** @hide */
public static final String PARENT_TYPES_SYNTHETIC_PROPERTY = "$$__AppSearch__parentTypes";
/**
* An immutable empty {@link GenericDocument}.
*
* @hide
*/
public static final GenericDocument EMPTY = new GenericDocument.Builder<>("", "", "").build();
/**
* The maximum number of indexed properties a document can have.
*
*
Indexed properties are properties which are strings where the {@link
* AppSearchSchema.StringPropertyConfig#getIndexingType} value is anything other than {@link
* AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE}, as well as long properties where
* the {@link AppSearchSchema.LongPropertyConfig#getIndexingType} value is {@link
* AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}.
*/
public static int getMaxIndexedProperties() {
return MAX_INDEXED_PROPERTIES;
}
/** The class to hold all meta data and properties for this {@link GenericDocument}. */
private final GenericDocumentParcel mDocumentParcel;
/**
* Rebuilds a {@link GenericDocument} from a {@link GenericDocumentParcel}.
*
* @param documentParcel Packaged {@link GenericDocument} data, such as the result of {@link
* #getDocumentParcel()}.
* @hide
*/
@SuppressWarnings("deprecation")
public GenericDocument(@NonNull GenericDocumentParcel documentParcel) {
mDocumentParcel = Objects.requireNonNull(documentParcel);
}
/**
* Creates a new {@link GenericDocument} from an existing instance.
*
*
This method should be only used by constructor of a subclass.
*/
protected GenericDocument(@NonNull GenericDocument document) {
this(document.mDocumentParcel);
}
/**
* Returns the {@link GenericDocumentParcel} holding the values for this {@link
* GenericDocument}.
*
* @hide
*/
@NonNull
public GenericDocumentParcel getDocumentParcel() {
return mDocumentParcel;
}
/** Returns the unique identifier of the {@link GenericDocument}. */
@NonNull
public String getId() {
return mDocumentParcel.getId();
}
/** Returns the namespace of the {@link GenericDocument}. */
@NonNull
public String getNamespace() {
return mDocumentParcel.getNamespace();
}
/** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
@NonNull
public String getSchemaType() {
return mDocumentParcel.getSchemaType();
}
/**
* Returns the list of parent types of the {@link GenericDocument}'s type.
*
*
It is guaranteed that child types appear before parent types in the list.
*
* @hide
*/
@Nullable
public List getParentTypes() {
List result = mDocumentParcel.getParentTypes();
if (result == null) {
return null;
}
return Collections.unmodifiableList(result);
}
/**
* Returns the creation timestamp of the {@link GenericDocument}, in milliseconds.
*
*
The value is in the {@link System#currentTimeMillis} time base.
*/
@CurrentTimeMillisLong
public long getCreationTimestampMillis() {
return mDocumentParcel.getCreationTimestampMillis();
}
/**
* Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
*
*
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.
*
*
The default value is 0, which means the document is permanent and won't be auto-deleted
* until the app is uninstalled or {@link AppSearchSession#remove} is called.
*/
public long getTtlMillis() {
return mDocumentParcel.getTtlMillis();
}
/**
* Returns the score of the {@link GenericDocument}.
*
*
The score is a query-independent measure of the document's quality, relative to other
* {@link GenericDocument} objects of the same {@link AppSearchSchema} type.
*
*
Results may be sorted by score using {@link SearchSpec.Builder#setRankingStrategy}.
* Documents with higher scores are considered better than documents with lower scores.
*
*
Any non-negative integer can be used a score.
*/
public int getScore() {
return mDocumentParcel.getScore();
}
/** Returns the names of all properties defined in this document. */
@NonNull
public Set getPropertyNames() {
return Collections.unmodifiableSet(mDocumentParcel.getPropertyNames());
}
/**
* Retrieves the property value with the given path as {@link Object}.
*
*
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]}.
*
*
For example, given the following {@link GenericDocument}:
*
*
Here are some example paths and their results:
*
*
*
{@code "from"} returns {@code "sender@example.com"} as a {@link String} array with one
* element
*
{@code "to"} returns the two nested documents containing contact information as a
* {@link GenericDocument} array with two elements
*
{@code "to[1]"} returns the second nested document containing Marie Curie's contact
* information as a {@link GenericDocument} array with one element
*
{@code "to[100].email"} returns {@code null} as this particular document does not have
* that many elements in its {@code "to"} array.
*
{@code "to.email"} aggregates emails across all nested documents that have them,
* returning {@code ["einstein@example.com", "curie@example.com"]} as a {@link String}
* array with two elements.
*
*
*
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}.
*
*
If the property was assigned as an empty array using one of the {@code
* Builder#setProperty} functions, this method will return an empty array. If no such property
* exists at all, this method returns {@code null}.
*
*
Note: If the property is an empty {@link GenericDocument}[] or {@code byte[][]}, this
* method will return a {@code null} value in versions of Android prior to {@link
* android.os.Build.VERSION_CODES#TIRAMISU Android T}. Starting in Android T it will return an
* empty array if the property has been set as an empty array, matching the behavior of other
* property types.
*
* @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 path) {
Objects.requireNonNull(path);
Object rawValue =
getRawPropertyFromRawDocument(
new PropertyPath(path),
/* pathIndex= */ 0,
mDocumentParcel.getPropertyMap());
// Unpack the raw value into the types the user expects, if required.
if (rawValue instanceof GenericDocumentParcel) {
// getRawPropertyFromRawDocument may return a document as a bare documentParcel
// as a performance optimization for lookups.
GenericDocument document = new GenericDocument((GenericDocumentParcel) rawValue);
return new GenericDocument[] {document};
}
if (rawValue instanceof GenericDocumentParcel[]) {
// The underlying parcelable of nested GenericDocuments is packed into
// a Parcelable array.
// We must unpack it into GenericDocument instances.
GenericDocumentParcel[] docParcels = (GenericDocumentParcel[]) rawValue;
GenericDocument[] documents = new GenericDocument[docParcels.length];
for (int i = 0; i < docParcels.length; i++) {
if (docParcels[i] == null) {
Log.e(TAG, "The inner parcel is null at " + i + ", for path: " + path);
continue;
}
documents[i] = new GenericDocument(docParcels[i]);
}
return documents;
}
// Otherwise the raw property is the same as the final property and needs no transformation.
return rawValue;
}
/**
* Looks up a property path within the given document bundle.
*
*
The return value may be any of GenericDocument's internal repeated storage types
* (String[], long[], double[], boolean[], ArrayList<Bundle>, Parcelable[]).
*
*
Usually, this method takes a path and loops over it to get a property from the bundle. But
* in the case where we collect documents across repeated nested documents, we need to recurse
* back into this method, and so we also keep track of the index into the path.
*
* @param path the PropertyPath object representing the path
* @param pathIndex the index into the path we start at
* @param propertyMap the map containing the path we are looking up
* @return the raw property
*/
@Nullable
@SuppressWarnings("deprecation")
private static Object getRawPropertyFromRawDocument(
@NonNull PropertyPath path,
int pathIndex,
@NonNull Map propertyMap) {
Objects.requireNonNull(path);
Objects.requireNonNull(propertyMap);
for (int i = pathIndex; i < path.size(); i++) {
PropertyPath.PathSegment segment = path.get(i);
Object currentElementValue = propertyMap.get(segment.getPropertyName());
if (currentElementValue == null) {
return null;
}
// If the current PathSegment has an index, we now need to update currentElementValue to
// contain the value of the indexed property. For example, for a path segment like
// "recipients[0]", currentElementValue now contains the value of "recipients" while we
// need the value of "recipients[0]".
int index = segment.getPropertyIndex();
if (index != PropertyPath.PathSegment.NON_REPEATED_CARDINALITY) {
// For properties bundle, now we will only get PropertyParcel as the value.
PropertyParcel propertyParcel = (PropertyParcel) currentElementValue;
// Extract the right array element
Object extractedValue = null;
if (propertyParcel.getStringValues() != null) {
String[] stringValues = propertyParcel.getStringValues();
if (stringValues != null && index < stringValues.length) {
extractedValue = Arrays.copyOfRange(stringValues, index, index + 1);
}
} else if (propertyParcel.getLongValues() != null) {
long[] longValues = propertyParcel.getLongValues();
if (longValues != null && index < longValues.length) {
extractedValue = Arrays.copyOfRange(longValues, index, index + 1);
}
} else if (propertyParcel.getDoubleValues() != null) {
double[] doubleValues = propertyParcel.getDoubleValues();
if (doubleValues != null && index < doubleValues.length) {
extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1);
}
} else if (propertyParcel.getBooleanValues() != null) {
boolean[] booleanValues = propertyParcel.getBooleanValues();
if (booleanValues != null && index < booleanValues.length) {
extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1);
}
} else if (propertyParcel.getBytesValues() != null) {
byte[][] bytesValues = propertyParcel.getBytesValues();
if (bytesValues != null && index < bytesValues.length) {
extractedValue = Arrays.copyOfRange(bytesValues, index, index + 1);
}
} else if (propertyParcel.getDocumentValues() != null) {
// Special optimization: to avoid creating new singleton arrays for traversing
// paths we return the bare document parcel in this particular case.
GenericDocumentParcel[] docValues = propertyParcel.getDocumentValues();
if (docValues != null && index < docValues.length) {
extractedValue = docValues[index];
}
} else {
throw new IllegalStateException(
"Unsupported value type: " + currentElementValue);
}
currentElementValue = extractedValue;
}
// at the end of the path, either something like "...foo" or "...foo[1]"
if (currentElementValue == null || i == path.size() - 1) {
if (currentElementValue != null && currentElementValue instanceof PropertyParcel) {
// Unlike previous bundle-based implementation, now each
// value is wrapped in PropertyParcel.
// Here we need to get and return the actual value for non-repeated fields.
currentElementValue = ((PropertyParcel) currentElementValue).getValues();
}
return currentElementValue;
}
// currentElementValue is now a GenericDocumentParcel or PropertyParcel,
// we can continue down the path.
if (currentElementValue instanceof GenericDocumentParcel) {
propertyMap = ((GenericDocumentParcel) currentElementValue).getPropertyMap();
} else if (currentElementValue instanceof PropertyParcel
&& ((PropertyParcel) currentElementValue).getDocumentValues() != null) {
GenericDocumentParcel[] docParcels =
((PropertyParcel) currentElementValue).getDocumentValues();
if (docParcels != null && docParcels.length == 1) {
propertyMap = docParcels[0].getPropertyMap();
continue;
}
// 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.
if (docParcels != null) {
List