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