diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java index da9b8bd47b2cc..d628fb5142724 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSession.java @@ -23,6 +23,9 @@ import android.os.Bundle; import android.os.ParcelableException; import android.os.RemoteException; import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.List; @@ -39,10 +42,13 @@ import java.util.function.Consumer; * This class is thread safe. */ public final class AppSearchSession { + private static final String TAG = "AppSearchSession"; private final String mDatabaseName; @UserIdInt private final int mUserId; private final IAppSearchManager mService; + private boolean mIsMutated = false; + private boolean mIsClosed = false; static void createSearchSession( @NonNull AppSearchManager.SearchContext searchContext, @@ -150,6 +156,7 @@ public final class AppSearchSession { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(callback); + Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed"); List schemaBundles = new ArrayList<>(request.getSchemas().size()); for (AppSearchSchema schema : request.getSchemas()) { schemaBundles.add(schema.getBundle()); @@ -166,6 +173,7 @@ public final class AppSearchSession { executor.execute(() -> callback.accept(result)); } }); + mIsMutated = true; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -182,6 +190,7 @@ public final class AppSearchSession { @NonNull Consumer>> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); + Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed"); try { mService.getSchema( mDatabaseName, @@ -232,6 +241,7 @@ public final class AppSearchSession { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(callback); + Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed"); List documents = request.getDocuments(); List documentBundles = new ArrayList<>(documents.size()); for (int i = 0; i < documents.size(); i++) { @@ -248,6 +258,7 @@ public final class AppSearchSession { executor.execute(() -> callback.onSystemError(exception.getCause())); } }); + mIsMutated = true; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -275,6 +286,7 @@ public final class AppSearchSession { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(callback); + Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed"); try { mService.getDocuments(mDatabaseName, request.getNamespace(), new ArrayList<>(request.getUris()), mUserId, @@ -379,6 +391,7 @@ public final class AppSearchSession { Objects.requireNonNull(queryExpression); Objects.requireNonNull(searchSpec); Objects.requireNonNull(executor); + Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed"); return new SearchResults(mService, mDatabaseName, queryExpression, searchSpec, mUserId, executor); } @@ -404,6 +417,7 @@ public final class AppSearchSession { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(callback); + Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed"); try { mService.removeByUri(mDatabaseName, request.getNamespace(), new ArrayList<>(request.getUris()), mUserId, @@ -416,6 +430,7 @@ public final class AppSearchSession { executor.execute(() -> callback.onSystemError(exception.getCause())); } }); + mIsMutated = true; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -448,6 +463,7 @@ public final class AppSearchSession { Objects.requireNonNull(searchSpec); Objects.requireNonNull(executor); Objects.requireNonNull(callback); + Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed"); try { mService.removeByQuery(mDatabaseName, queryExpression, searchSpec.getBundle(), mUserId, new IAppSearchResultCallback.Stub() { @@ -455,8 +471,26 @@ public final class AppSearchSession { executor.execute(() -> callback.accept(result)); } }); + mIsMutated = true; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + + /** + * Closes the SearchSessionImpl to persists all update/delete requests to the disk. + * + * @hide + */ + // TODO(b/175637134) when unhide it, implement Closeable and remove this method. + public void close() { + if (mIsMutated && !mIsClosed) { + try { + mService.persistToDisk(mUserId); + mIsClosed = true; + } catch (RemoteException e) { + Log.e(TAG, "Unable to close the AppSearchSession", e); + } + } + } } diff --git a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl index c1c8047db7360..af8b6132bb7e3 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl +++ b/apex/appsearch/framework/java/android/app/appsearch/IAppSearchManager.aidl @@ -192,6 +192,13 @@ interface IAppSearchManager { in int userId, in IAppSearchResultCallback callback); + /** + * Persists all update/delete requests to the disk. + * + * @param userId Id of the calling user + */ + void persistToDisk(in int userId); + /** * Creates and initializes AppSearchImpl for the calling app. * diff --git a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java index 82c2d26214dca..0cb0ea42483e8 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java +++ b/apex/appsearch/framework/java/android/app/appsearch/SearchResults.java @@ -24,6 +24,8 @@ import android.os.Bundle; import android.os.RemoteException; import android.util.Log; +import com.android.internal.util.Preconditions; + import java.io.Closeable; import java.util.List; import java.util.Objects; @@ -62,6 +64,8 @@ public class SearchResults implements Closeable { private boolean mIsFirstLoad = true; + private boolean mIsClosed = false; + SearchResults( @NonNull IAppSearchManager service, @Nullable String databaseName, @@ -88,6 +92,7 @@ public class SearchResults implements Closeable { * @param callback Callback to receive the pending result of performing this operation. */ public void getNextPage(@NonNull Consumer>> callback) { + Preconditions.checkState(!mIsClosed, "SearchResults has already been closed"); try { if (mIsFirstLoad) { mIsFirstLoad = false; @@ -111,10 +116,13 @@ public class SearchResults implements Closeable { @Override public void close() { - try { - mService.invalidateNextPageToken(mNextPageToken, mUserId); - } catch (RemoteException e) { - Log.d(TAG, "Unable to close the SearchResults", e); + if (!mIsClosed) { + try { + mService.invalidateNextPageToken(mNextPageToken, mUserId); + mIsClosed = true; + } catch (RemoteException e) { + Log.e(TAG, "Unable to close the SearchResults", e); + } } } diff --git a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java index 8cf1ca0927d96..87c41e53dc63a 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/AppSearchManagerService.java @@ -279,7 +279,7 @@ public class AppSearchManagerService extends SystemService { AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); impl.invalidateNextPageToken(nextPageToken); } catch (Throwable t) { - Log.d(TAG, "Unable to invalidate the query page token", t); + Log.e(TAG, "Unable to invalidate the query page token", t); } finally { Binder.restoreCallingIdentity(callingIdentity); } @@ -347,6 +347,21 @@ public class AppSearchManagerService extends SystemService { } } + @Override + public void persistToDisk(@UserIdInt int userId) { + int callingUid = Binder.getCallingUidOrThrow(); + int callingUserId = handleIncomingUser(userId, callingUid); + final long callingIdentity = Binder.clearCallingIdentity(); + try { + AppSearchImpl impl = ImplInstanceManager.getInstance(getContext(), callingUserId); + impl.persistToDisk(); + } catch (Throwable t) { + Log.e(TAG, "Unable to persist the data to disk", t); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } + } + @Override public void initialize(@UserIdInt int userId, @NonNull IAppSearchResultCallback callback) { Preconditions.checkNotNull(callback); @@ -388,7 +403,7 @@ public class AppSearchManagerService extends SystemService { try { callback.onResult(result); } catch (RemoteException e) { - Log.d(TAG, "Unable to send result to the callback", e); + Log.e(TAG, "Unable to send result to the callback", e); } } @@ -398,7 +413,7 @@ public class AppSearchManagerService extends SystemService { try { callback.onResult(result); } catch (RemoteException e) { - Log.d(TAG, "Unable to send result to the callback", e); + Log.e(TAG, "Unable to send result to the callback", e); } } @@ -411,7 +426,7 @@ public class AppSearchManagerService extends SystemService { try { callback.onResult(throwableToFailedResult(throwable)); } catch (RemoteException e) { - Log.d(TAG, "Unable to send result to the callback", e); + Log.e(TAG, "Unable to send result to the callback", e); } } @@ -425,7 +440,7 @@ public class AppSearchManagerService extends SystemService { try { callback.onSystemError(new ParcelableException(throwable)); } catch (RemoteException e) { - Log.d(TAG, "Unable to send error to the callback", e); + Log.e(TAG, "Unable to send error to the callback", e); } } } diff --git a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java index 1d5287b1309c7..47a81eb37c7dd 100644 --- a/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java +++ b/apex/appsearch/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java @@ -46,6 +46,7 @@ import com.google.android.icing.proto.GetSchemaResultProto; import com.google.android.icing.proto.IcingSearchEngineOptions; import com.google.android.icing.proto.InitializeResultProto; import com.google.android.icing.proto.OptimizeResultProto; +import com.google.android.icing.proto.PersistToDiskResultProto; import com.google.android.icing.proto.PropertyConfigProto; import com.google.android.icing.proto.PropertyProto; import com.google.android.icing.proto.PutResultProto; @@ -630,6 +631,24 @@ public final class AppSearchImpl { deleteResultProto.getStatus(), StatusProto.Code.OK, StatusProto.Code.NOT_FOUND); } + /** + * Persists all update/delete requests to the disk. + * + *

If the app crashes after a call to PersistToDisk(), Icing would be able to fully recover + * all data written up to this point without a costly recovery process. + * + *

If the app crashes before a call to PersistToDisk(), Icing would trigger a costly recovery + * process in next initialization. After that, Icing would still be able to recover all written + * data. + * + * @throws AppSearchException + */ + public void persistToDisk() throws AppSearchException { + PersistToDiskResultProto persistToDiskResultProto = + mIcingSearchEngineLocked.persistToDisk(); + checkSuccess(persistToDiskResultProto.getStatus()); + } + /** * Clears documents and schema across all packages and databaseNames. * diff --git a/apex/appsearch/synced_jetpack_changeid.txt b/apex/appsearch/synced_jetpack_changeid.txt index 5b818c761b406..2b1ec08e1d8eb 100644 --- a/apex/appsearch/synced_jetpack_changeid.txt +++ b/apex/appsearch/synced_jetpack_changeid.txt @@ -1 +1 @@ -Idd770a064edfeb6dc648571fc6706c087b8e605a +I596ad1269b4d3a4f26db67f5d970aeaa3bf94a9d diff --git a/apex/appsearch/testing/java/com/android/server/appsearch/testing/AppSearchSessionShimImpl.java b/apex/appsearch/testing/java/com/android/server/appsearch/testing/AppSearchSessionShimImpl.java index 1d2382a3be9cb..b0478d59f2379 100644 --- a/apex/appsearch/testing/java/com/android/server/appsearch/testing/AppSearchSessionShimImpl.java +++ b/apex/appsearch/testing/java/com/android/server/appsearch/testing/AppSearchSessionShimImpl.java @@ -58,10 +58,17 @@ public class AppSearchSessionShimImpl implements AppSearchSessionShim { @NonNull public static ListenableFuture createSearchSession( @NonNull AppSearchManager.SearchContext searchContext) { + return createSearchSession(searchContext, Executors.newCachedThreadPool()); + } + + /** Creates the SearchSession with given ExecutorService. */ + @NonNull + public static ListenableFuture createSearchSession( + @NonNull AppSearchManager.SearchContext searchContext, + @NonNull ExecutorService executor) { Context context = ApplicationProvider.getApplicationContext(); AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class); SettableFuture> future = SettableFuture.create(); - ExecutorService executor = Executors.newCachedThreadPool(); appSearchManager.createSearchSession(searchContext, executor, future::set); return Futures.transform( future, @@ -138,6 +145,11 @@ public class AppSearchSessionShimImpl implements AppSearchSessionShim { return Futures.transformAsync(future, this::transformResult, mExecutor); } + @Override + public void close() { + mAppSearchSession.close(); + } + private ListenableFuture transformResult( @NonNull AppSearchResult result) throws AppSearchException { if (!result.isSuccess()) { diff --git a/apex/appsearch/testing/java/com/android/server/appsearch/testing/external/AppSearchSessionShim.java b/apex/appsearch/testing/java/com/android/server/appsearch/testing/external/AppSearchSessionShim.java index 8c42b2dd239a2..8383df4dd0cdb 100644 --- a/apex/appsearch/testing/java/com/android/server/appsearch/testing/external/AppSearchSessionShim.java +++ b/apex/appsearch/testing/java/com/android/server/appsearch/testing/external/AppSearchSessionShim.java @@ -208,4 +208,13 @@ public interface AppSearchSessionShim { @NonNull ListenableFuture removeByQuery( @NonNull String queryExpression, @NonNull SearchSpec searchSpec); + + /** + * Closes the SearchSessionImpl to persists all update/delete requests to the disk. + * + * @hide + */ + + // TODO(b/175637134) when unhide it, extends Closeable and remove this method. + void close(); }