1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appsearch.app;
18 
19 import android.os.Parcel;
20 import android.os.Parcelable;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import androidx.annotation.RestrictTo;
25 import androidx.appsearch.flags.FlaggedApi;
26 import androidx.appsearch.flags.Flags;
27 import androidx.appsearch.safeparcel.AbstractSafeParcelable;
28 import androidx.appsearch.safeparcel.SafeParcelable;
29 import androidx.appsearch.safeparcel.stub.StubCreators.AppSearchBlobHandleCreator;
30 import androidx.appsearch.util.IndentingStringBuilder;
31 import androidx.core.util.Preconditions;
32 
33 import java.util.Arrays;
34 import java.util.Objects;
35 
36 /**
37  * An identifier to represent a blob in AppSearch.
38  *
39  * <p>A "blob" is a large binary object. It is used to store a significant amount of data that is
40  * not searchable, such as images, videos, audio files, or other binary data. Unlike other fields in
41  * AppSearch, blobs are stored as blob files on disk rather than in memory, and use
42  * {@link android.os.ParcelFileDescriptor} to read and write. This allows for efficient handling of
43  * large, non-searchable content.
44  *
45  * <p> {@link AppSearchBlobHandle} is a light-weight {@code Property} of {@link GenericDocument},
46  * which is a pointer to the heavy-weight blob data.
47  *
48  * <p> The blob data could be written via {@link AppSearchSession#openBlobForWriteAsync} and read
49  * via {@link AppSearchSession#openBlobForReadAsync}.
50  *
51  * <p> A {@link GenericDocument} with {@link AppSearchBlobHandle} {@code Property} could be put and
52  * read without the large blob data. This offers lazy retrieval to blob data when searching
53  * {@link GenericDocument} in AppSearch.
54  *
55  * @see GenericDocument.Builder#setPropertyBlobHandle
56  */
57 @FlaggedApi(Flags.FLAG_ENABLE_BLOB_STORE)
58 // TODO(b/384721898): Switch to JSpecify annotations
59 @SuppressWarnings({"HiddenSuperclass", "JSpecifyNullness"})
60 @SafeParcelable.Class(creator = "AppSearchBlobHandleCreator")
61 @ExperimentalAppSearchApi
62 public final class AppSearchBlobHandle extends AbstractSafeParcelable {
63     /** The length of the SHA-256 digest in bytes. SHA-256 produces a 256-bit (32-byte) digest. */
64     private static final int SHA_256_DIGEST_BYTE_LENGTH = 32;
65 
66     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
67     @NonNull
68     public static final Parcelable.Creator<AppSearchBlobHandle> CREATOR =
69             new AppSearchBlobHandleCreator();
70     @Field(id = 1, getter = "getSha256Digest")
71     private final @NonNull byte[] mSha256Digest;
72 
73     @Field(id = 2, getter = "getPackageName")
74     private final @NonNull String mPackageName;
75 
76     @Field(id = 3, getter = "getDatabaseName")
77     private final @NonNull String mDatabaseName;
78 
79     @Field(id = 4, getter = "getNamespace")
80     private final @NonNull String mNamespace;
81 
82     private @Nullable Integer mHashCode;
83 
84     /**
85      * Build an {@link AppSearchBlobHandle}.
86      * @exportToFramework:hide
87      */
88     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
89     @Constructor
AppSearchBlobHandle( @aramid = 1) @onNull byte[] sha256Digest, @Param(id = 2) @NonNull String packageName, @Param(id = 3) @NonNull String databaseName, @Param(id = 4) @NonNull String namespace)90     AppSearchBlobHandle(
91             @Param(id = 1) @NonNull byte[] sha256Digest,
92             @Param(id = 2) @NonNull String packageName,
93             @Param(id = 3) @NonNull String databaseName,
94             @Param(id = 4) @NonNull String namespace) {
95         mSha256Digest = Preconditions.checkNotNull(sha256Digest);
96         Preconditions.checkState(sha256Digest.length == SHA_256_DIGEST_BYTE_LENGTH,
97                 "The input digest isn't a sha-256 digest.");
98         mPackageName = Preconditions.checkNotNull(packageName);
99         mDatabaseName = Preconditions.checkNotNull(databaseName);
100         mNamespace = Preconditions.checkNotNull(namespace);
101     }
102 
103     /**
104      * Returns the SHA-256 hash of the blob that this object is representing.
105      *
106      * <p> For two objects of {@link AppSearchBlobHandle} to be considered equal, the
107      * {@code packageName}, {@code database}, {@code namespace} and {@code digest} must be equal.
108      */
getSha256Digest()109     public @NonNull byte[] getSha256Digest() {
110         return mSha256Digest;
111     }
112 
113     /**
114      * Returns the package name indicating the owner app of the blob that this object is
115      * representing.
116      *
117      * <p> For two objects of {@link AppSearchBlobHandle} to be considered equal, the
118      * {@code packageName}, {@code database}, {@code namespace} and {@code digest} must be equal.
119      */
getPackageName()120     public @NonNull String getPackageName() {
121         return mPackageName;
122     }
123 
124     /**
125      * Returns the name of database stored the blob that this object is representing.
126      *
127      * <p> For two objects of {@link AppSearchBlobHandle} to be considered equal, the
128      * {@code packageName}, {@code database}, {@code namespace} and {@code digest} must be equal.
129      */
getDatabaseName()130     public @NonNull String getDatabaseName() {
131         return mDatabaseName;
132     }
133 
134     /**
135      * Returns the app-defined namespace this blob resides in.
136      *
137      * <p> For two objects of {@link AppSearchBlobHandle} to be considered equal, the
138      * {@code packageName}, {@code database}, {@code namespace} and {@code digest} must be equal.
139      */
getNamespace()140     public @NonNull String getNamespace() {
141         return mNamespace;
142     }
143 
144     @Override
equals(Object o)145     public boolean equals(Object o) {
146         if (this == o) return true;
147         if (!(o instanceof AppSearchBlobHandle)) return false;
148 
149         AppSearchBlobHandle that = (AppSearchBlobHandle) o;
150         if (!Arrays.equals(mSha256Digest, that.mSha256Digest)) return false;
151         return mPackageName.equals(that.mPackageName) && mDatabaseName.equals(that.mDatabaseName)
152                 && mNamespace.equals(that.mNamespace);
153     }
154 
155     @Override
hashCode()156     public int hashCode() {
157         if (mHashCode == null) {
158             mHashCode = Objects.hash(
159                     Arrays.hashCode(mSha256Digest), mPackageName, mDatabaseName, mNamespace);
160         }
161         return mHashCode;
162     }
163 
164     @Override
toString()165     public @NonNull String toString() {
166         IndentingStringBuilder builder = new IndentingStringBuilder();
167         builder.append("{\n");
168         builder.increaseIndentLevel();
169         builder.append("packageName: \"").append(mPackageName).append("\",\n");
170         builder.append("databaseName: \"").append(mDatabaseName).append("\",\n");
171         builder.append("namespace: \"").append(mNamespace).append("\",\n");
172         builder.append("digest: \"");
173         for (byte b : mSha256Digest) {
174             String hex = Integer.toHexString(0xFF & b);
175             if (hex.length() == 1) {
176                 builder.append('0');
177             }
178             builder.append(hex);
179         }
180         builder.append("\",\n").decreaseIndentLevel();
181         builder.append("}");
182 
183         return builder.toString();
184     }
185 
186     /**
187      * Create a new AppSearch blob identifier with given digest, package, database and namespace.
188      *
189      * <p> The package name and database name indicated where this blob will be stored. To write,
190      * commit or read this blob via {@link AppSearchSession}, it must match the package name and
191      * database name of {@link AppSearchSession}.
192      *
193      * <p> For two objects of {@link AppSearchBlobHandle} to be considered equal, the
194      * {@code packageName}, {@code database}, {@code namespace} and {@code digest} must be equal.
195      *
196      * @param digest        The SHA-256 hash of the blob this is representing.
197      * @param packageName   The package name of the owner of this Blob.
198      * @param databaseName  The database name of this blob to stored into.
199      * @param namespace     The namespace of this blob resides in.
200      *
201      * @return a new instance of {@link AppSearchBlobHandle} object.
202      */
createWithSha256(@onNull byte[] digest, @NonNull String packageName, @NonNull String databaseName, @NonNull String namespace)203     public static @NonNull AppSearchBlobHandle createWithSha256(@NonNull byte[] digest,
204             @NonNull String packageName, @NonNull String databaseName, @NonNull String namespace) {
205         Preconditions.checkNotNull(digest);
206         Preconditions.checkArgument(digest.length == SHA_256_DIGEST_BYTE_LENGTH,
207                 "The digest is not a SHA-256 digest");
208         Preconditions.checkNotNull(packageName);
209         Preconditions.checkNotNull(databaseName);
210         Preconditions.checkNotNull(namespace);
211         return new AppSearchBlobHandle(digest, packageName, databaseName, namespace);
212     }
213 
214     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
215     @Override
writeToParcel(@onNull Parcel dest, int flags)216     public void writeToParcel(@NonNull Parcel dest, int flags) {
217         AppSearchBlobHandleCreator.writeToParcel(this, dest, flags);
218     }
219 }
220