1 /*
2  * Copyright 2021 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 android.app.appsearch;
18 
19 import android.annotation.FlaggedApi;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.app.appsearch.annotation.CanIgnoreReturnValue;
23 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
24 import android.app.appsearch.safeparcel.SafeParcelable;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.util.ArraySet;
28 
29 import com.android.appsearch.flags.Flags;
30 import com.android.internal.util.Preconditions;
31 
32 import java.util.ArrayList;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.List;
36 import java.util.Objects;
37 import java.util.Set;
38 
39 /** The response class of {@link AppSearchSession#setSchema} */
40 @SafeParcelable.Class(creator = "SetSchemaResponseCreator")
41 @SuppressWarnings("HiddenSuperclass")
42 public final class SetSchemaResponse extends AbstractSafeParcelable {
43 
44     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
45     @NonNull
46     public static final Parcelable.Creator<SetSchemaResponse> CREATOR =
47             new SetSchemaResponseCreator();
48 
49     @Field(id = 1)
50     final List<String> mDeletedTypes;
51 
52     @Field(id = 2)
53     final List<String> mIncompatibleTypes;
54 
55     @Field(id = 3)
56     final List<String> mMigratedTypes;
57 
58     /**
59      * The migrationFailures won't be saved as a SafeParcelable field. Since:
60      *
61      * <ul>
62      *   <li>{@link MigrationFailure} is generated in {@link AppSearchSession} which will be the SDK
63      *       side in platform. We don't need to pass it from service side via binder as a part of
64      *       {@link SetSchemaResponse}.
65      *   <li>Writing multiple {@link MigrationFailure}s to SafeParcelable in {@link Builder} and
66      *       then back in constructor will be a huge waste.
67      * </ul>
68      */
69     private final List<MigrationFailure> mMigrationFailures;
70 
71     /**
72      * Cache of the inflated deleted schema types. Comes from inflating mDeletedTypes at first use
73      */
74     @Nullable private Set<String> mDeletedTypesCached;
75 
76     /**
77      * Cache of the inflated migrated schema types. Comes from inflating mMigratedTypes at first
78      * use.
79      */
80     @Nullable private Set<String> mMigratedTypesCached;
81 
82     /**
83      * Cache of the inflated incompatible schema types. Comes from inflating mIncompatibleTypes at
84      * first use.
85      */
86     @Nullable private Set<String> mIncompatibleTypesCached;
87 
88     @Constructor
SetSchemaResponse( @aramid = 1) @onNull List<String> deletedTypes, @Param(id = 2) @NonNull List<String> incompatibleTypes, @Param(id = 3) @NonNull List<String> migratedTypes)89     SetSchemaResponse(
90             @Param(id = 1) @NonNull List<String> deletedTypes,
91             @Param(id = 2) @NonNull List<String> incompatibleTypes,
92             @Param(id = 3) @NonNull List<String> migratedTypes) {
93         mDeletedTypes = deletedTypes;
94         mIncompatibleTypes = incompatibleTypes;
95         mMigratedTypes = migratedTypes;
96         mMigrationFailures = Collections.emptyList();
97     }
98 
SetSchemaResponse( @onNull List<String> deletedTypes, @NonNull List<String> incompatibleTypes, @NonNull List<String> migratedTypes, @NonNull List<MigrationFailure> migrationFailures)99     SetSchemaResponse(
100             @NonNull List<String> deletedTypes,
101             @NonNull List<String> incompatibleTypes,
102             @NonNull List<String> migratedTypes,
103             @NonNull List<MigrationFailure> migrationFailures) {
104         mDeletedTypes = deletedTypes;
105         mIncompatibleTypes = incompatibleTypes;
106         mMigratedTypes = migratedTypes;
107         mMigrationFailures = Objects.requireNonNull(migrationFailures);
108     }
109 
110     /**
111      * Returns a {@link List} of all failed {@link MigrationFailure}.
112      *
113      * <p>A {@link MigrationFailure} will be generated if the system trying to save a post-migrated
114      * {@link GenericDocument} but fail.
115      *
116      * <p>{@link MigrationFailure} contains the namespace, id and schemaType of the post-migrated
117      * {@link GenericDocument} and the error reason. Mostly it will be mismatch the schema it
118      * migrated to.
119      */
120     @NonNull
getMigrationFailures()121     public List<MigrationFailure> getMigrationFailures() {
122         return Collections.unmodifiableList(mMigrationFailures);
123     }
124 
125     /**
126      * Returns a {@link Set} of deleted schema types.
127      *
128      * <p>A "deleted" type is a schema type that was previously a part of the database schema but
129      * was not present in the {@link SetSchemaRequest} object provided in the {@link
130      * AppSearchSession#setSchema} call.
131      *
132      * <p>Documents for a deleted type are removed from the database.
133      */
134     @NonNull
getDeletedTypes()135     public Set<String> getDeletedTypes() {
136         if (mDeletedTypesCached == null) {
137             mDeletedTypesCached = new ArraySet<>(Objects.requireNonNull(mDeletedTypes));
138         }
139         return Collections.unmodifiableSet(mDeletedTypesCached);
140     }
141 
142     /**
143      * Returns a {@link Set} of schema type that were migrated by the {@link
144      * AppSearchSession#setSchema} call.
145      *
146      * <p>A "migrated" type is a schema type that has triggered a {@link Migrator} instance to
147      * migrate documents of the schema type to another schema type, or to another version of the
148      * schema type.
149      *
150      * <p>If a document fails to be migrated, a {@link MigrationFailure} will be generated for that
151      * document.
152      *
153      * @see Migrator
154      */
155     @NonNull
getMigratedTypes()156     public Set<String> getMigratedTypes() {
157         if (mMigratedTypesCached == null) {
158             mMigratedTypesCached = new ArraySet<>(Objects.requireNonNull(mMigratedTypes));
159         }
160         return Collections.unmodifiableSet(mMigratedTypesCached);
161     }
162 
163     /**
164      * Returns a {@link Set} of schema type whose new definitions set in the {@link
165      * AppSearchSession#setSchema} call were incompatible with the pre-existing schema.
166      *
167      * <p>If a {@link Migrator} is provided for this type and the migration is success triggered.
168      * The type will also appear in {@link #getMigratedTypes()}.
169      *
170      * @see SetSchemaRequest
171      * @see AppSearchSession#setSchema
172      * @see SetSchemaRequest.Builder#setForceOverride
173      */
174     @NonNull
getIncompatibleTypes()175     public Set<String> getIncompatibleTypes() {
176         if (mIncompatibleTypesCached == null) {
177             mIncompatibleTypesCached = new ArraySet<>(Objects.requireNonNull(mIncompatibleTypes));
178         }
179         return Collections.unmodifiableSet(mIncompatibleTypesCached);
180     }
181 
182     /** Builder for {@link SetSchemaResponse} objects. */
183     public static final class Builder {
184         private List<MigrationFailure> mMigrationFailures = new ArrayList<>();
185         private ArrayList<String> mDeletedTypes = new ArrayList<>();
186         private ArrayList<String> mMigratedTypes = new ArrayList<>();
187         private ArrayList<String> mIncompatibleTypes = new ArrayList<>();
188         private boolean mBuilt = false;
189 
190         /**
191          * Creates a new {@link SetSchemaResponse.Builder} from the given SetSchemaResponse.
192          *
193          * @hide
194          */
Builder(@onNull SetSchemaResponse setSchemaResponse)195         public Builder(@NonNull SetSchemaResponse setSchemaResponse) {
196             Objects.requireNonNull(setSchemaResponse);
197             mDeletedTypes.addAll(setSchemaResponse.getDeletedTypes());
198             mIncompatibleTypes.addAll(setSchemaResponse.getIncompatibleTypes());
199             mMigratedTypes.addAll(setSchemaResponse.getMigratedTypes());
200             mMigrationFailures.addAll(setSchemaResponse.getMigrationFailures());
201         }
202 
203         /** Create a {@link Builder} object} */
Builder()204         public Builder() {}
205 
206         /** Adds {@link MigrationFailure}s to the list of migration failures. */
207         @CanIgnoreReturnValue
208         @NonNull
addMigrationFailures( @onNull Collection<MigrationFailure> migrationFailures)209         public Builder addMigrationFailures(
210                 @NonNull Collection<MigrationFailure> migrationFailures) {
211             Objects.requireNonNull(migrationFailures);
212             resetIfBuilt();
213             mMigrationFailures.addAll(migrationFailures);
214             return this;
215         }
216 
217         /** Adds a {@link MigrationFailure} to the list of migration failures. */
218         @CanIgnoreReturnValue
219         @NonNull
addMigrationFailure(@onNull MigrationFailure migrationFailure)220         public Builder addMigrationFailure(@NonNull MigrationFailure migrationFailure) {
221             Objects.requireNonNull(migrationFailure);
222             resetIfBuilt();
223             mMigrationFailures.add(migrationFailure);
224             return this;
225         }
226 
227         /** Adds {@code deletedTypes} to the list of deleted schema types. */
228         @CanIgnoreReturnValue
229         @NonNull
addDeletedTypes(@onNull Collection<String> deletedTypes)230         public Builder addDeletedTypes(@NonNull Collection<String> deletedTypes) {
231             Objects.requireNonNull(deletedTypes);
232             resetIfBuilt();
233             mDeletedTypes.addAll(deletedTypes);
234             return this;
235         }
236 
237         /** Adds one {@code deletedType} to the list of deleted schema types. */
238         @CanIgnoreReturnValue
239         @NonNull
addDeletedType(@onNull String deletedType)240         public Builder addDeletedType(@NonNull String deletedType) {
241             Objects.requireNonNull(deletedType);
242             resetIfBuilt();
243             mDeletedTypes.add(deletedType);
244             return this;
245         }
246 
247         /** Adds {@code incompatibleTypes} to the list of incompatible schema types. */
248         @CanIgnoreReturnValue
249         @NonNull
addIncompatibleTypes(@onNull Collection<String> incompatibleTypes)250         public Builder addIncompatibleTypes(@NonNull Collection<String> incompatibleTypes) {
251             Objects.requireNonNull(incompatibleTypes);
252             resetIfBuilt();
253             mIncompatibleTypes.addAll(incompatibleTypes);
254             return this;
255         }
256 
257         /** Adds one {@code incompatibleType} to the list of incompatible schema types. */
258         @CanIgnoreReturnValue
259         @NonNull
addIncompatibleType(@onNull String incompatibleType)260         public Builder addIncompatibleType(@NonNull String incompatibleType) {
261             Objects.requireNonNull(incompatibleType);
262             resetIfBuilt();
263             mIncompatibleTypes.add(incompatibleType);
264             return this;
265         }
266 
267         /** Adds {@code migratedTypes} to the list of migrated schema types. */
268         @CanIgnoreReturnValue
269         @NonNull
addMigratedTypes(@onNull Collection<String> migratedTypes)270         public Builder addMigratedTypes(@NonNull Collection<String> migratedTypes) {
271             Objects.requireNonNull(migratedTypes);
272             resetIfBuilt();
273             mMigratedTypes.addAll(migratedTypes);
274             return this;
275         }
276 
277         /** Adds one {@code migratedType} to the list of migrated schema types. */
278         @CanIgnoreReturnValue
279         @NonNull
addMigratedType(@onNull String migratedType)280         public Builder addMigratedType(@NonNull String migratedType) {
281             Objects.requireNonNull(migratedType);
282             resetIfBuilt();
283             mMigratedTypes.add(migratedType);
284             return this;
285         }
286 
287         /** Builds a {@link SetSchemaResponse} object. */
288         @NonNull
build()289         public SetSchemaResponse build() {
290             mBuilt = true;
291             // Avoid converting the potential thousands of MigrationFailures to Pracelable and
292             // back just for put in bundle. In platform, we should set MigrationFailures in
293             // AppSearchSession after we pass SetSchemaResponse via binder.
294             return new SetSchemaResponse(
295                     mDeletedTypes, mIncompatibleTypes, mMigratedTypes, mMigrationFailures);
296         }
297 
resetIfBuilt()298         private void resetIfBuilt() {
299             if (mBuilt) {
300                 mMigrationFailures = new ArrayList<>(mMigrationFailures);
301                 mDeletedTypes = new ArrayList<>(mDeletedTypes);
302                 mMigratedTypes = new ArrayList<>(mMigratedTypes);
303                 mIncompatibleTypes = new ArrayList<>(mIncompatibleTypes);
304                 mBuilt = false;
305             }
306         }
307     }
308 
309     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
310     @Override
writeToParcel(@onNull Parcel dest, int flags)311     public void writeToParcel(@NonNull Parcel dest, int flags) {
312         SetSchemaResponseCreator.writeToParcel(this, dest, flags);
313     }
314 
315     /**
316      * The class represents a post-migrated {@link GenericDocument} that failed to be saved by
317      * {@link AppSearchSession#setSchema}.
318      */
319     @SafeParcelable.Class(creator = "MigrationFailureCreator")
320     @SuppressWarnings("HiddenSuperclass")
321     public static class MigrationFailure extends AbstractSafeParcelable {
322 
323         @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
324         @NonNull
325         public static final Parcelable.Creator<MigrationFailure> CREATOR =
326                 new MigrationFailureCreator();
327 
328         @Field(id = 1, getter = "getNamespace")
329         private final String mNamespace;
330 
331         @Field(id = 2, getter = "getDocumentId")
332         private final String mDocumentId;
333 
334         @Field(id = 3, getter = "getSchemaType")
335         private final String mSchemaType;
336 
337         @Field(id = 4)
338         @Nullable
339         final String mErrorMessage;
340 
341         @Field(id = 5)
342         final int mResultCode;
343 
344         @Constructor
MigrationFailure( @aramid = 1) @onNull String namespace, @Param(id = 2) @NonNull String documentId, @Param(id = 3) @NonNull String schemaType, @Param(id = 4) @Nullable String errorMessage, @Param(id = 5) int resultCode)345         MigrationFailure(
346                 @Param(id = 1) @NonNull String namespace,
347                 @Param(id = 2) @NonNull String documentId,
348                 @Param(id = 3) @NonNull String schemaType,
349                 @Param(id = 4) @Nullable String errorMessage,
350                 @Param(id = 5) int resultCode) {
351             mNamespace = namespace;
352             mDocumentId = documentId;
353             mSchemaType = schemaType;
354             mErrorMessage = errorMessage;
355             mResultCode = resultCode;
356         }
357 
358         /**
359          * Constructs a new {@link MigrationFailure}.
360          *
361          * @param namespace The namespace of the document which failed to be migrated.
362          * @param documentId The id of the document which failed to be migrated.
363          * @param schemaType The type of the document which failed to be migrated.
364          * @param failedResult The reason why the document failed to be indexed.
365          * @throws IllegalArgumentException if the provided {@code failedResult} was not a failure.
366          */
MigrationFailure( @onNull String namespace, @NonNull String documentId, @NonNull String schemaType, @NonNull AppSearchResult<?> failedResult)367         public MigrationFailure(
368                 @NonNull String namespace,
369                 @NonNull String documentId,
370                 @NonNull String schemaType,
371                 @NonNull AppSearchResult<?> failedResult) {
372             mNamespace = namespace;
373             mDocumentId = documentId;
374             mSchemaType = schemaType;
375 
376             Objects.requireNonNull(failedResult);
377             Preconditions.checkArgument(
378                     !failedResult.isSuccess(), "failedResult was actually successful");
379             mErrorMessage = failedResult.getErrorMessage();
380             mResultCode = failedResult.getResultCode();
381         }
382 
383         /** Returns the namespace of the {@link GenericDocument} that failed to be migrated. */
384         @NonNull
getNamespace()385         public String getNamespace() {
386             return mNamespace;
387         }
388 
389         /** Returns the id of the {@link GenericDocument} that failed to be migrated. */
390         @NonNull
getDocumentId()391         public String getDocumentId() {
392             return mDocumentId;
393         }
394 
395         /** Returns the schema type of the {@link GenericDocument} that failed to be migrated. */
396         @NonNull
getSchemaType()397         public String getSchemaType() {
398             return mSchemaType;
399         }
400 
401         /**
402          * Returns the {@link AppSearchResult} that indicates why the post-migration {@link
403          * GenericDocument} failed to be indexed.
404          */
405         @NonNull
getAppSearchResult()406         public AppSearchResult<Void> getAppSearchResult() {
407             return AppSearchResult.newFailedResult(mResultCode, mErrorMessage);
408         }
409 
410         @NonNull
411         @Override
toString()412         public String toString() {
413             return "MigrationFailure { schemaType: "
414                     + getSchemaType()
415                     + ", namespace: "
416                     + getNamespace()
417                     + ", documentId: "
418                     + getDocumentId()
419                     + ", appSearchResult: "
420                     + getAppSearchResult().toString()
421                     + "}";
422         }
423 
424         @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
425         @Override
writeToParcel(@onNull Parcel dest, int flags)426         public void writeToParcel(@NonNull Parcel dest, int flags) {
427             MigrationFailureCreator.writeToParcel(this, dest, flags);
428         }
429     }
430 }
431