• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 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 static android.app.appsearch.AppSearchResult.RESULT_INVALID_SCHEMA;
20 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
21 import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
22 
23 import android.annotation.NonNull;
24 import android.annotation.WorkerThread;
25 import android.app.appsearch.aidl.AppSearchResultParcel;
26 import android.app.appsearch.aidl.IAppSearchManager;
27 import android.app.appsearch.aidl.IAppSearchResultCallback;
28 import android.app.appsearch.exceptions.AppSearchException;
29 import android.content.AttributionSource;
30 import android.os.Bundle;
31 import android.os.Parcel;
32 import android.os.ParcelFileDescriptor;
33 import android.os.RemoteException;
34 import android.os.UserHandle;
35 import android.util.ArraySet;
36 
37 import java.io.Closeable;
38 import java.io.DataInputStream;
39 import java.io.DataOutputStream;
40 import java.io.EOFException;
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.util.List;
46 import java.util.Objects;
47 import java.util.Set;
48 import java.util.concurrent.CompletableFuture;
49 import java.util.concurrent.ExecutionException;
50 
51 /**
52  * The helper class for {@link AppSearchSchema} migration.
53  *
54  * <p>It will query and migrate {@link GenericDocument} in given type to a new version.
55  * @hide
56  */
57 public class AppSearchMigrationHelper implements Closeable {
58     private final IAppSearchManager mService;
59     private final AttributionSource mCallerAttributionSource;
60     private final String mDatabaseName;
61     private final UserHandle mUserHandle;
62     private final File mMigratedFile;
63     private final Set<String> mDestinationTypes;
64     private boolean mAreDocumentsMigrated = false;
65 
AppSearchMigrationHelper(@onNull IAppSearchManager service, @NonNull UserHandle userHandle, @NonNull AttributionSource callerAttributionSource, @NonNull String databaseName, @NonNull Set<AppSearchSchema> newSchemas)66     AppSearchMigrationHelper(@NonNull IAppSearchManager service,
67             @NonNull UserHandle userHandle,
68             @NonNull AttributionSource callerAttributionSource,
69             @NonNull String databaseName,
70             @NonNull Set<AppSearchSchema> newSchemas) throws IOException {
71         mService = Objects.requireNonNull(service);
72         mUserHandle = Objects.requireNonNull(userHandle);
73         mCallerAttributionSource = Objects.requireNonNull(callerAttributionSource);
74         mDatabaseName = Objects.requireNonNull(databaseName);
75         mMigratedFile = File.createTempFile(/*prefix=*/"appsearch", /*suffix=*/null);
76         mDestinationTypes = new ArraySet<>(newSchemas.size());
77         for (AppSearchSchema newSchema : newSchemas) {
78             mDestinationTypes.add(newSchema.getSchemaType());
79         }
80     }
81 
82     /**
83      * Queries all documents that need to be migrated to a different version and transform
84      * documents to that version by passing them to the provided {@link Migrator}.
85      *
86      * <p>The method will be executed on the executor provided to
87      * {@link AppSearchSession#setSchema}.
88      *
89      * @param schemaType The schema type that needs to be updated and whose {@link GenericDocument}
90      *                   need to be migrated.
91      * @param migrator The {@link Migrator} that will upgrade or downgrade a {@link
92      *     GenericDocument} to new version.
93      */
94     @WorkerThread
queryAndTransform(@onNull String schemaType, @NonNull Migrator migrator, int currentVersion, int finalVersion)95     public void queryAndTransform(@NonNull String schemaType, @NonNull Migrator migrator,
96             int currentVersion, int finalVersion)
97             throws IOException, AppSearchException, InterruptedException, ExecutionException {
98         File queryFile = File.createTempFile(/*prefix=*/"appsearch", /*suffix=*/null);
99         try (ParcelFileDescriptor fileDescriptor =
100                      ParcelFileDescriptor.open(queryFile, MODE_WRITE_ONLY)) {
101             CompletableFuture<AppSearchResult<Void>> future = new CompletableFuture<>();
102             mService.writeQueryResultsToFile(mCallerAttributionSource, mDatabaseName,
103                     fileDescriptor,
104                     /*queryExpression=*/ "",
105                     new SearchSpec.Builder()
106                             .addFilterSchemas(schemaType)
107                             .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
108                             .build().getBundle(),
109                     mUserHandle,
110                     new IAppSearchResultCallback.Stub() {
111                         @Override
112                         public void onResult(AppSearchResultParcel resultParcel) {
113                             future.complete(resultParcel.getResult());
114                         }
115                     });
116             AppSearchResult<Void> result = future.get();
117             if (!result.isSuccess()) {
118                 throw new AppSearchException(result.getResultCode(), result.getErrorMessage());
119             }
120             readAndTransform(queryFile, migrator, currentVersion, finalVersion);
121         } catch (RemoteException e) {
122             throw e.rethrowFromSystemServer();
123         } finally {
124             queryFile.delete();
125         }
126     }
127 
128     /**
129      * Puts all {@link GenericDocument} migrated from the previous call to
130      * {@link #queryAndTransform} into AppSearch.
131      *
132      * <p> This method should be only called once.
133      *
134      * @param responseBuilder a SetSchemaResponse builder whose result will be returned by this
135      *                        function with any
136      *                        {@link android.app.appsearch.SetSchemaResponse.MigrationFailure}
137      *                        added in.
138      * @return the {@link SetSchemaResponse} for {@link AppSearchSession#setSchema} call.
139      */
140     @NonNull
putMigratedDocuments( @onNull SetSchemaResponse.Builder responseBuilder)141     AppSearchResult<SetSchemaResponse> putMigratedDocuments(
142             @NonNull SetSchemaResponse.Builder responseBuilder) {
143         if (!mAreDocumentsMigrated) {
144             return AppSearchResult.newSuccessfulResult(responseBuilder.build());
145         }
146         try (ParcelFileDescriptor fileDescriptor =
147                      ParcelFileDescriptor.open(mMigratedFile, MODE_READ_ONLY)) {
148             CompletableFuture<AppSearchResult<List<Bundle>>> future = new CompletableFuture<>();
149             mService.putDocumentsFromFile(mCallerAttributionSource, mDatabaseName, fileDescriptor,
150                     mUserHandle,
151                     new IAppSearchResultCallback.Stub() {
152                         @Override
153                         public void onResult(AppSearchResultParcel resultParcel) {
154                             future.complete(resultParcel.getResult());
155                         }
156                     });
157             AppSearchResult<List<Bundle>> result = future.get();
158             if (!result.isSuccess()) {
159                 return AppSearchResult.newFailedResult(result);
160             }
161             List<Bundle> migratedFailureBundles = result.getResultValue();
162             for (int i = 0; i < migratedFailureBundles.size(); i++) {
163                 responseBuilder.addMigrationFailure(
164                         new SetSchemaResponse.MigrationFailure(migratedFailureBundles.get(i)));
165             }
166         } catch (RemoteException e) {
167             throw e.rethrowFromSystemServer();
168         } catch (Throwable t) {
169             return AppSearchResult.throwableToFailedResult(t);
170         } finally {
171             mMigratedFile.delete();
172         }
173         return AppSearchResult.newSuccessfulResult(responseBuilder.build());
174     }
175 
176     /**
177      * Reads all saved {@link GenericDocument}s from the given {@link File}.
178      *
179      * <p>Transforms those {@link GenericDocument}s to the final version.
180      *
181      * <p>Save migrated {@link GenericDocument}s to the {@link #mMigratedFile}.
182      */
readAndTransform(@onNull File file, @NonNull Migrator migrator, int currentVersion, int finalVersion)183     private void readAndTransform(@NonNull File file, @NonNull Migrator migrator,
184             int currentVersion, int finalVersion)
185             throws IOException, AppSearchException {
186         try (DataInputStream inputStream = new DataInputStream(new FileInputStream(file));
187              DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(
188                      mMigratedFile, /*append=*/ true))) {
189             GenericDocument document;
190             while (true) {
191                 try {
192                     document = readDocumentFromInputStream(inputStream);
193                 } catch (EOFException e) {
194                     break;
195                     // Nothing wrong. We just finished reading.
196                 }
197 
198                 GenericDocument newDocument;
199                 if (currentVersion < finalVersion) {
200                     newDocument = migrator.onUpgrade(currentVersion, finalVersion, document);
201                 } else {
202                     // currentVersion == finalVersion case won't trigger migration and get here.
203                     newDocument = migrator.onDowngrade(currentVersion, finalVersion, document);
204                 }
205 
206                 if (!mDestinationTypes.contains(newDocument.getSchemaType())) {
207                     // we exit before the new schema has been set to AppSearch. So no
208                     // observable changes will be applied to stored schemas and documents.
209                     // And the temp file will be deleted at close(), which will be triggered at
210                     // the end of try-with-resources block of SearchSessionImpl.
211                     throw new AppSearchException(
212                             RESULT_INVALID_SCHEMA,
213                             "Receive a migrated document with schema type: "
214                                     + newDocument.getSchemaType()
215                                     + ". But the schema types doesn't exist in the request");
216                 }
217                 writeBundleToOutputStream(outputStream, newDocument.getBundle());
218             }
219             mAreDocumentsMigrated = true;
220         }
221     }
222 
223     /**
224      * Reads the {@link Bundle} of a {@link GenericDocument} from given {@link DataInputStream}.
225      *
226      * @param inputStream The inputStream to read from
227      *
228      * @throws IOException        on read failure.
229      * @throws EOFException       if {@link java.io.InputStream} reaches the end.
230      */
231     @NonNull
readDocumentFromInputStream( @onNull DataInputStream inputStream)232     public static GenericDocument readDocumentFromInputStream(
233             @NonNull DataInputStream inputStream) throws IOException {
234         int length = inputStream.readInt();
235         if (length == 0) {
236             throw new EOFException();
237         }
238         byte[] serializedMessage = new byte[length];
239         inputStream.read(serializedMessage);
240 
241         Parcel parcel = Parcel.obtain();
242         try {
243             parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
244             parcel.setDataPosition(0);
245             Bundle bundle = parcel.readBundle();
246             return new GenericDocument(bundle);
247         } finally {
248             parcel.recycle();
249         }
250     }
251 
252     /**
253      * Serializes a {@link Bundle} and writes into the given {@link DataOutputStream}.
254      */
writeBundleToOutputStream( @onNull DataOutputStream outputStream, @NonNull Bundle bundle)255     public static void writeBundleToOutputStream(
256             @NonNull DataOutputStream outputStream, @NonNull Bundle bundle)
257             throws IOException {
258         Parcel parcel = Parcel.obtain();
259         try {
260             parcel.writeBundle(bundle);
261             byte[] serializedMessage = parcel.marshall();
262             outputStream.writeInt(serializedMessage.length);
263             outputStream.write(serializedMessage);
264         } finally {
265             parcel.recycle();
266         }
267     }
268 
269     @Override
close()270     public void close() throws IOException {
271         mMigratedFile.delete();
272     }
273 }
274