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