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