All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.app.appsearch.AppSearchMigrationHelper Maven / Gradle / Ivy

/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.app.appsearch;

import static android.app.appsearch.AppSearchResult.RESULT_INVALID_SCHEMA;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;

import android.annotation.NonNull;
import android.annotation.WorkerThread;
import android.app.appsearch.aidl.AppSearchResultParcel;
import android.app.appsearch.aidl.IAppSearchManager;
import android.app.appsearch.aidl.IAppSearchResultCallback;
import android.app.appsearch.exceptions.AppSearchException;
import android.os.Bundle;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArraySet;

import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * The helper class for {@link AppSearchSchema} migration.
 *
 * 

It will query and migrate {@link GenericDocument} in given type to a new version. * @hide */ public class AppSearchMigrationHelper implements Closeable { private final IAppSearchManager mService; private final String mPackageName; private final String mDatabaseName; private final UserHandle mUserHandle; private final File mMigratedFile; private final Set mDestinationTypes; private boolean mAreDocumentsMigrated = false; AppSearchMigrationHelper(@NonNull IAppSearchManager service, @NonNull UserHandle userHandle, @NonNull String packageName, @NonNull String databaseName, @NonNull Set newSchemas) throws IOException { mService = Objects.requireNonNull(service); mUserHandle = Objects.requireNonNull(userHandle); mPackageName = Objects.requireNonNull(packageName); mDatabaseName = Objects.requireNonNull(databaseName); mMigratedFile = File.createTempFile(/*prefix=*/"appsearch", /*suffix=*/null); mDestinationTypes = new ArraySet<>(newSchemas.size()); for (AppSearchSchema newSchema : newSchemas) { mDestinationTypes.add(newSchema.getSchemaType()); } } /** * Queries all documents that need to be migrated to a different version and transform * documents to that version by passing them to the provided {@link Migrator}. * *

The method will be executed on the executor provided to * {@link AppSearchSession#setSchema}. * * @param schemaType The schema type that needs to be updated and whose {@link GenericDocument} * need to be migrated. * @param migrator The {@link Migrator} that will upgrade or downgrade a {@link * GenericDocument} to new version. */ @WorkerThread public void queryAndTransform(@NonNull String schemaType, @NonNull Migrator migrator, int currentVersion, int finalVersion) throws IOException, AppSearchException, InterruptedException, ExecutionException { File queryFile = File.createTempFile(/*prefix=*/"appsearch", /*suffix=*/null); try (ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(queryFile, MODE_WRITE_ONLY)) { CompletableFuture> future = new CompletableFuture<>(); mService.writeQueryResultsToFile(mPackageName, mDatabaseName, fileDescriptor, /*queryExpression=*/ "", new SearchSpec.Builder() .addFilterSchemas(schemaType) .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) .build().getBundle(), mUserHandle, new IAppSearchResultCallback.Stub() { @Override public void onResult(AppSearchResultParcel resultParcel) { future.complete(resultParcel.getResult()); } }); AppSearchResult result = future.get(); if (!result.isSuccess()) { throw new AppSearchException(result.getResultCode(), result.getErrorMessage()); } readAndTransform(queryFile, migrator, currentVersion, finalVersion); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } finally { queryFile.delete(); } } /** * Puts all {@link GenericDocument} migrated from the previous call to * {@link #queryAndTransform} into AppSearch. * *

This method should be only called once. * * @param responseBuilder a SetSchemaResponse builder whose result will be returned by this * function with any * {@link android.app.appsearch.SetSchemaResponse.MigrationFailure} * added in. * @return the {@link SetSchemaResponse} for {@link AppSearchSession#setSchema} call. */ @NonNull AppSearchResult putMigratedDocuments( @NonNull SetSchemaResponse.Builder responseBuilder) { if (!mAreDocumentsMigrated) { return AppSearchResult.newSuccessfulResult(responseBuilder.build()); } try (ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(mMigratedFile, MODE_READ_ONLY)) { CompletableFuture>> future = new CompletableFuture<>(); mService.putDocumentsFromFile(mPackageName, mDatabaseName, fileDescriptor, mUserHandle, new IAppSearchResultCallback.Stub() { @Override public void onResult(AppSearchResultParcel resultParcel) { future.complete(resultParcel.getResult()); } }); AppSearchResult> result = future.get(); if (!result.isSuccess()) { return AppSearchResult.newFailedResult(result); } List migratedFailureBundles = result.getResultValue(); for (int i = 0; i < migratedFailureBundles.size(); i++) { responseBuilder.addMigrationFailure( new SetSchemaResponse.MigrationFailure(migratedFailureBundles.get(i))); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (Throwable t) { return AppSearchResult.throwableToFailedResult(t); } finally { mMigratedFile.delete(); } return AppSearchResult.newSuccessfulResult(responseBuilder.build()); } /** * Reads all saved {@link GenericDocument}s from the given {@link File}. * *

Transforms those {@link GenericDocument}s to the final version. * *

Save migrated {@link GenericDocument}s to the {@link #mMigratedFile}. */ private void readAndTransform(@NonNull File file, @NonNull Migrator migrator, int currentVersion, int finalVersion) throws IOException, AppSearchException { try (DataInputStream inputStream = new DataInputStream(new FileInputStream(file)); DataOutputStream outputStream = new DataOutputStream(new FileOutputStream( mMigratedFile, /*append=*/ true))) { GenericDocument document; while (true) { try { document = readDocumentFromInputStream(inputStream); } catch (EOFException e) { break; // Nothing wrong. We just finished reading. } GenericDocument newDocument; if (currentVersion < finalVersion) { newDocument = migrator.onUpgrade(currentVersion, finalVersion, document); } else { // currentVersion == finalVersion case won't trigger migration and get here. newDocument = migrator.onDowngrade(currentVersion, finalVersion, document); } if (!mDestinationTypes.contains(newDocument.getSchemaType())) { // we exit before the new schema has been set to AppSearch. So no // observable changes will be applied to stored schemas and documents. // And the temp file will be deleted at close(), which will be triggered at // the end of try-with-resources block of SearchSessionImpl. throw new AppSearchException( RESULT_INVALID_SCHEMA, "Receive a migrated document with schema type: " + newDocument.getSchemaType() + ". But the schema types doesn't exist in the request"); } writeBundleToOutputStream(outputStream, newDocument.getBundle()); } mAreDocumentsMigrated = true; } } /** * Reads the {@link Bundle} of a {@link GenericDocument} from given {@link DataInputStream}. * * @param inputStream The inputStream to read from * * @throws IOException on read failure. * @throws EOFException if {@link java.io.InputStream} reaches the end. */ @NonNull public static GenericDocument readDocumentFromInputStream( @NonNull DataInputStream inputStream) throws IOException { int length = inputStream.readInt(); if (length == 0) { throw new EOFException(); } byte[] serializedMessage = new byte[length]; inputStream.read(serializedMessage); Parcel parcel = Parcel.obtain(); try { parcel.unmarshall(serializedMessage, 0, serializedMessage.length); parcel.setDataPosition(0); Bundle bundle = parcel.readBundle(); return new GenericDocument(bundle); } finally { parcel.recycle(); } } /** * Serializes a {@link Bundle} and writes into the given {@link DataOutputStream}. */ public static void writeBundleToOutputStream( @NonNull DataOutputStream outputStream, @NonNull Bundle bundle) throws IOException { Parcel parcel = Parcel.obtain(); try { parcel.writeBundle(bundle); byte[] serializedMessage = parcel.marshall(); outputStream.writeInt(serializedMessage.length); outputStream.write(serializedMessage); } finally { parcel.recycle(); } } @Override public void close() throws IOException { mMigratedFile.delete(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy