Merge "Use MetadataSyncAdapter in AppFunctionManagerService." into main
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java
index c3b7087..1f98334 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionExecutors.java
@@ -16,7 +16,15 @@
package com.android.server.appfunctions;
+import android.annotation.NonNull;
+import android.os.UserHandle;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -33,5 +41,50 @@
/* unit= */ TimeUnit.SECONDS,
/* workQueue= */ new LinkedBlockingQueue<>());
+ /** A map of per-user executors for queued work. */
+ @GuardedBy("sLock")
+ private static final SparseArray<ExecutorService> mPerUserExecutorsLocked = new SparseArray<>();
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Returns a per-user executor for queued metadata sync request.
+ *
+ * <p>The work submitted to these executor (Sync request) needs to be synchronous per user hence
+ * the use of a single thread.
+ *
+ * <p>Note: Use a different executor if not calling {@code submitSyncRequest} on a {@code
+ * MetadataSyncAdapter}.
+ */
+ // TODO(b/357551503): Restrict the scope of this executor to the MetadataSyncAdapter itself.
+ public static ExecutorService getPerUserSyncExecutor(@NonNull UserHandle user) {
+ synchronized (sLock) {
+ ExecutorService executor = mPerUserExecutorsLocked.get(user.getIdentifier(), null);
+ if (executor == null) {
+ executor = Executors.newSingleThreadExecutor();
+ mPerUserExecutorsLocked.put(user.getIdentifier(), executor);
+ }
+ return executor;
+ }
+ }
+
+ /**
+ * Shuts down and removes the per-user executor for queued work.
+ *
+ * <p>This should be called when the user is removed.
+ */
+ public static void shutDownAndRemoveUserExecutor(@NonNull UserHandle user)
+ throws InterruptedException {
+ ExecutorService executor;
+ synchronized (sLock) {
+ executor = mPerUserExecutorsLocked.get(user.getIdentifier());
+ mPerUserExecutorsLocked.remove(user.getIdentifier());
+ }
+ if (executor != null) {
+ executor.shutdown();
+ var unused = executor.awaitTermination(30, TimeUnit.SECONDS);
+ }
+ }
+
private AppFunctionExecutors() {}
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
index 02800cb..c293087 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.appfunctions;
+import android.annotation.NonNull;
import android.app.appfunctions.AppFunctionManagerConfiguration;
import android.content.Context;
@@ -36,4 +37,14 @@
publishBinderService(Context.APP_FUNCTION_SERVICE, mServiceImpl);
}
}
+
+ @Override
+ public void onUserUnlocked(@NonNull TargetUser user) {
+ mServiceImpl.onUserUnlocked(user);
+ }
+
+ @Override
+ public void onUserStopping(@NonNull TargetUser user) {
+ mServiceImpl.onUserStopping(user);
+ }
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 2362b91..cf039df 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -19,29 +19,35 @@
import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_EXECUTOR;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appfunctions.AppFunctionStaticMetadataHelper;
import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
import android.app.appfunctions.ExecuteAppFunctionResponse;
import android.app.appfunctions.IAppFunctionManager;
import android.app.appfunctions.IAppFunctionService;
import android.app.appfunctions.IExecuteAppFunctionCallback;
import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.observer.DocumentChangeInfo;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.app.appsearch.observer.SchemaChangeInfo;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Slog;
-import android.app.appsearch.AppSearchResult;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService.TargetUser;
import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
+import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CompletionException;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
/** Implementation of the AppFunctionManagerService. */
public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
@@ -51,9 +57,11 @@
private final CallerValidator mCallerValidator;
private final ServiceHelper mInternalServiceHelper;
private final ServiceConfig mServiceConfig;
+ private final Context mContext;
public AppFunctionManagerServiceImpl(@NonNull Context context) {
this(
+ context,
new RemoteServiceCallerImpl<>(
context, IAppFunctionService.Stub::asInterface, THREAD_POOL_EXECUTOR),
new CallerValidatorImpl(context),
@@ -63,10 +71,12 @@
@VisibleForTesting
AppFunctionManagerServiceImpl(
+ Context context,
RemoteServiceCaller<IAppFunctionService> remoteServiceCaller,
CallerValidator callerValidator,
ServiceHelper appFunctionInternalServiceHelper,
ServiceConfig serviceConfig) {
+ mContext = Objects.requireNonNull(context);
mRemoteServiceCaller = Objects.requireNonNull(remoteServiceCaller);
mCallerValidator = Objects.requireNonNull(callerValidator);
mInternalServiceHelper = Objects.requireNonNull(appFunctionInternalServiceHelper);
@@ -90,6 +100,26 @@
}
}
+ /** Called when the user is unlocked. */
+ public void onUserUnlocked(TargetUser user) {
+ Objects.requireNonNull(user);
+
+ registerAppSearchObserver(user);
+ trySyncRuntimeMetadata(user);
+ }
+
+ /** Called when the user is stopping. */
+ public void onUserStopping(@NonNull TargetUser user) {
+ Objects.requireNonNull(user);
+
+ try {
+ AppFunctionExecutors.shutDownAndRemoveUserExecutor(user.getUserHandle());
+ MetadataSyncPerUser.removeUserSyncAdapter(user.getUserHandle());
+ } catch (InterruptedException e) {
+ Slog.e(TAG, "Unable to remove data for: " + user.getUserHandle(), e);
+ }
+ }
+
private void executeAppFunctionInternal(
ExecuteAppFunctionAidlRequest requestInternal,
SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) {
@@ -132,53 +162,55 @@
return;
}
- var unused = mCallerValidator
- .verifyCallerCanExecuteAppFunction(
- validatedCallingPackage,
- targetPackageName,
- requestInternal.getClientRequest().getFunctionIdentifier())
- .thenAccept(
- canExecute -> {
- if (!canExecute) {
- safeExecuteAppFunctionCallback.onResult(
- ExecuteAppFunctionResponse.newFailure(
- ExecuteAppFunctionResponse.RESULT_DENIED,
- "Caller does not have permission to execute the"
- + " appfunction",
- /* extras= */ null));
- return;
- }
- Intent serviceIntent =
- mInternalServiceHelper.resolveAppFunctionService(
- targetPackageName, targetUser);
- if (serviceIntent == null) {
- safeExecuteAppFunctionCallback.onResult(
- ExecuteAppFunctionResponse.newFailure(
- ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
- "Cannot find the target service.",
- /* extras= */ null));
- return;
- }
- final long token = Binder.clearCallingIdentity();
- try {
- bindAppFunctionServiceUnchecked(
- requestInternal,
- serviceIntent,
- targetUser,
- safeExecuteAppFunctionCallback,
- /* bindFlags= */ Context.BIND_AUTO_CREATE,
- /* timeoutInMillis= */ mServiceConfig
- .getExecuteAppFunctionTimeoutMillis());
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- })
- .exceptionally(
- ex -> {
- safeExecuteAppFunctionCallback.onResult(
- mapExceptionToExecuteAppFunctionResponse(ex));
- return null;
- });
+ var unused =
+ mCallerValidator
+ .verifyCallerCanExecuteAppFunction(
+ validatedCallingPackage,
+ targetPackageName,
+ requestInternal.getClientRequest().getFunctionIdentifier())
+ .thenAccept(
+ canExecute -> {
+ if (!canExecute) {
+ safeExecuteAppFunctionCallback.onResult(
+ ExecuteAppFunctionResponse.newFailure(
+ ExecuteAppFunctionResponse.RESULT_DENIED,
+ "Caller does not have permission to execute"
+ + " the appfunction",
+ /* extras= */ null));
+ return;
+ }
+ Intent serviceIntent =
+ mInternalServiceHelper.resolveAppFunctionService(
+ targetPackageName, targetUser);
+ if (serviceIntent == null) {
+ safeExecuteAppFunctionCallback.onResult(
+ ExecuteAppFunctionResponse.newFailure(
+ ExecuteAppFunctionResponse
+ .RESULT_INTERNAL_ERROR,
+ "Cannot find the target service.",
+ /* extras= */ null));
+ return;
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ bindAppFunctionServiceUnchecked(
+ requestInternal,
+ serviceIntent,
+ targetUser,
+ safeExecuteAppFunctionCallback,
+ /* bindFlags= */ Context.BIND_AUTO_CREATE,
+ /* timeoutInMillis= */ mServiceConfig
+ .getExecuteAppFunctionTimeoutMillis());
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ })
+ .exceptionally(
+ ex -> {
+ safeExecuteAppFunctionCallback.onResult(
+ mapExceptionToExecuteAppFunctionResponse(ex));
+ return null;
+ });
}
private void bindAppFunctionServiceUnchecked(
@@ -256,7 +288,7 @@
}
private ExecuteAppFunctionResponse mapExceptionToExecuteAppFunctionResponse(Throwable e) {
- if(e instanceof CompletionException) {
+ if (e instanceof CompletionException) {
e = e.getCause();
}
@@ -291,4 +323,103 @@
}
return ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR;
}
+
+ private void registerAppSearchObserver(@NonNull TargetUser user) {
+ AppSearchManager perUserAppSearchManager =
+ mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0)
+ .getSystemService(AppSearchManager.class);
+ if (perUserAppSearchManager == null) {
+ Slog.d(TAG, "AppSearch Manager not found for user: " + user.getUserIdentifier());
+ return;
+ }
+ try (FutureGlobalSearchSession futureGlobalSearchSession =
+ new FutureGlobalSearchSession(
+ perUserAppSearchManager, AppFunctionExecutors.THREAD_POOL_EXECUTOR)) {
+ AppFunctionMetadataObserver appFunctionMetadataObserver =
+ new AppFunctionMetadataObserver(
+ user.getUserHandle(),
+ mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0));
+ var unused =
+ futureGlobalSearchSession
+ .registerObserverCallbackAsync(
+ "android",
+ new ObserverSpec.Builder().build(),
+ THREAD_POOL_EXECUTOR,
+ appFunctionMetadataObserver)
+ .whenComplete(
+ (voidResult, ex) -> {
+ if (ex != null) {
+ Slog.e(TAG, "Failed to register observer: ", ex);
+ }
+ });
+
+ } catch (IOException ex) {
+ Slog.e(TAG, "Failed to close observer session: ", ex);
+ }
+ }
+
+ private void trySyncRuntimeMetadata(@NonNull TargetUser user) {
+ MetadataSyncAdapter metadataSyncAdapter =
+ MetadataSyncPerUser.getPerUserMetadataSyncAdapter(
+ user.getUserHandle(),
+ mContext.createContextAsUser(user.getUserHandle(), /* flags= */ 0));
+ if (metadataSyncAdapter != null) {
+ var unused =
+ metadataSyncAdapter
+ .submitSyncRequest()
+ .whenComplete(
+ (isSuccess, ex) -> {
+ if (ex != null || !isSuccess) {
+ Slog.e(TAG, "Sync was not successful");
+ }
+ });
+ }
+ }
+
+ private static class AppFunctionMetadataObserver implements ObserverCallback {
+ @Nullable private final MetadataSyncAdapter mPerUserMetadataSyncAdapter;
+
+ AppFunctionMetadataObserver(@NonNull UserHandle userHandle, @NonNull Context userContext) {
+ mPerUserMetadataSyncAdapter =
+ MetadataSyncPerUser.getPerUserMetadataSyncAdapter(userHandle, userContext);
+ }
+
+ @Override
+ public void onDocumentChanged(@NonNull DocumentChangeInfo documentChangeInfo) {
+ if (mPerUserMetadataSyncAdapter == null) {
+ return;
+ }
+ if (documentChangeInfo
+ .getDatabaseName()
+ .equals(AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)
+ && documentChangeInfo
+ .getNamespace()
+ .equals(
+ AppFunctionStaticMetadataHelper
+ .APP_FUNCTION_STATIC_NAMESPACE)) {
+ var unused = mPerUserMetadataSyncAdapter.submitSyncRequest();
+ }
+ }
+
+ @Override
+ public void onSchemaChanged(@NonNull SchemaChangeInfo schemaChangeInfo) {
+ if (mPerUserMetadataSyncAdapter == null) {
+ return;
+ }
+ if (schemaChangeInfo
+ .getDatabaseName()
+ .equals(AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)) {
+ boolean shouldInitiateSync = false;
+ for (String schemaName : schemaChangeInfo.getChangedSchemaNames()) {
+ if (schemaName.startsWith(AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE)) {
+ shouldInitiateSync = true;
+ break;
+ }
+ }
+ if (shouldInitiateSync) {
+ var unused = mPerUserMetadataSyncAdapter.submitSyncRequest();
+ }
+ }
+ }
+ }
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
index e2573590..8c6f50e 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -24,6 +24,8 @@
import android.app.appfunctions.AppFunctionRuntimeMetadata;
import android.app.appfunctions.AppFunctionStaticMetadataHelper;
import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchManager.SearchContext;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.PackageIdentifier;
@@ -61,9 +63,8 @@
*/
public class MetadataSyncAdapter {
private static final String TAG = MetadataSyncAdapter.class.getSimpleName();
- private final FutureAppSearchSession mRuntimeMetadataSearchSession;
- private final FutureAppSearchSession mStaticMetadataSearchSession;
private final Executor mSyncExecutor;
+ private final AppSearchManager mAppSearchManager;
private final PackageManager mPackageManager;
// Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility
@@ -73,13 +74,11 @@
public MetadataSyncAdapter(
@NonNull Executor syncExecutor,
- @NonNull FutureAppSearchSession runtimeMetadataSearchSession,
- @NonNull FutureAppSearchSession staticMetadataSearchSession,
- @NonNull PackageManager packageManager) {
+ @NonNull PackageManager packageManager,
+ @NonNull AppSearchManager appSearchManager) {
mSyncExecutor = Objects.requireNonNull(syncExecutor);
- mRuntimeMetadataSearchSession = Objects.requireNonNull(runtimeMetadataSearchSession);
- mStaticMetadataSearchSession = Objects.requireNonNull(staticMetadataSearchSession);
mPackageManager = Objects.requireNonNull(packageManager);
+ mAppSearchManager = Objects.requireNonNull(appSearchManager);
}
/**
@@ -89,31 +88,54 @@
* synchronization was successful.
*/
public AndroidFuture<Boolean> submitSyncRequest() {
+ SearchContext staticMetadataSearchContext =
+ new SearchContext.Builder(
+ AppFunctionStaticMetadataHelper.APP_FUNCTION_STATIC_METADATA_DB)
+ .build();
+ SearchContext runtimeMetadataSearchContext =
+ new SearchContext.Builder(
+ AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB)
+ .build();
AndroidFuture<Boolean> settableSyncStatus = new AndroidFuture<>();
mSyncExecutor.execute(
() -> {
- try {
- trySyncAppFunctionMetadataBlocking();
+ try (FutureAppSearchSession staticMetadataSearchSession =
+ new FutureAppSearchSessionImpl(
+ mAppSearchManager,
+ AppFunctionExecutors.THREAD_POOL_EXECUTOR,
+ staticMetadataSearchContext);
+ FutureAppSearchSession runtimeMetadataSearchSession =
+ new FutureAppSearchSessionImpl(
+ mAppSearchManager,
+ AppFunctionExecutors.THREAD_POOL_EXECUTOR,
+ runtimeMetadataSearchContext)) {
+
+ trySyncAppFunctionMetadataBlocking(
+ staticMetadataSearchSession, runtimeMetadataSearchSession);
settableSyncStatus.complete(true);
- } catch (Exception e) {
- settableSyncStatus.completeExceptionally(e);
+
+ } catch (Exception ex) {
+ settableSyncStatus.completeExceptionally(ex);
}
});
return settableSyncStatus;
}
@WorkerThread
- private void trySyncAppFunctionMetadataBlocking()
+ @VisibleForTesting
+ void trySyncAppFunctionMetadataBlocking(
+ @NonNull FutureAppSearchSession staticMetadataSearchSession,
+ @NonNull FutureAppSearchSession runtimeMetadataSearchSession)
throws ExecutionException, InterruptedException {
ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap =
getPackageToFunctionIdMap(
- mStaticMetadataSearchSession,
+ staticMetadataSearchSession,
AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE,
AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID,
AppFunctionStaticMetadataHelper.PROPERTY_PACKAGE_NAME);
ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap =
getPackageToFunctionIdMap(
- mRuntimeMetadataSearchSession,
+ runtimeMetadataSearchSession,
RUNTIME_SCHEMA_TYPE,
AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID,
AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME);
@@ -134,7 +156,7 @@
RemoveByDocumentIdRequest removeByDocumentIdRequest =
buildRemoveRuntimeMetadataRequest(removedFunctionsDiffMap);
AppSearchBatchResult<String, Void> removeDocumentBatchResult =
- mRuntimeMetadataSearchSession.remove(removeByDocumentIdRequest).get();
+ runtimeMetadataSearchSession.remove(removeByDocumentIdRequest).get();
if (!removeDocumentBatchResult.isSuccess()) {
throw convertFailedAppSearchResultToException(
removeDocumentBatchResult.getFailures().values());
@@ -144,13 +166,14 @@
if (!addedFunctionsDiffMap.isEmpty()) {
// TODO(b/357551503): only set schema on package diff
SetSchemaRequest addSetSchemaRequest =
- buildSetSchemaRequestForRuntimeMetadataSchemas(appRuntimeMetadataSchemas);
+ buildSetSchemaRequestForRuntimeMetadataSchemas(
+ mPackageManager, appRuntimeMetadataSchemas);
Objects.requireNonNull(
- mRuntimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
+ runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
PutDocumentsRequest putDocumentsRequest =
buildPutRuntimeMetadataRequest(addedFunctionsDiffMap);
AppSearchBatchResult<String, Void> putDocumentBatchResult =
- mRuntimeMetadataSearchSession.put(putDocumentsRequest).get();
+ runtimeMetadataSearchSession.put(putDocumentsRequest).get();
if (!putDocumentBatchResult.isSuccess()) {
throw convertFailedAppSearchResultToException(
putDocumentBatchResult.getFailures().values());
@@ -211,6 +234,7 @@
@NonNull
private SetSchemaRequest buildSetSchemaRequestForRuntimeMetadataSchemas(
+ @NonNull PackageManager packageManager,
@NonNull Set<AppSearchSchema> metadataSchemaSet) {
Objects.requireNonNull(metadataSchemaSet);
SetSchemaRequest.Builder setSchemaRequestBuilder =
@@ -220,7 +244,7 @@
String packageName =
AppFunctionRuntimeMetadata.getPackageNameFromSchema(
runtimeMetadataSchema.getSchemaType());
- byte[] packageCert = getCertificate(packageName);
+ byte[] packageCert = getCertificate(packageManager, packageName);
if (packageCert == null) {
continue;
}
@@ -399,13 +423,15 @@
/** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found. */
@Nullable
- private byte[] getCertificate(@NonNull String packageName) {
+ private byte[] getCertificate(
+ @NonNull PackageManager packageManager, @NonNull String packageName) {
+ Objects.requireNonNull(packageManager);
Objects.requireNonNull(packageName);
PackageInfo packageInfo;
try {
packageInfo =
Objects.requireNonNull(
- mPackageManager.getPackageInfo(
+ packageManager.getPackageInfo(
packageName,
PackageManager.GET_META_DATA
| PackageManager.GET_SIGNING_CERTIFICATES));
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java
new file mode 100644
index 0000000..f421527
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appfunctions;
+
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+/** A Singleton class that manages per-user metadata sync adapters. */
+public final class MetadataSyncPerUser {
+ private static final String TAG = MetadataSyncPerUser.class.getSimpleName();
+
+ /** A map of per-user adapter for synchronizing appFunction metadata. */
+ @GuardedBy("sLock")
+ private static final SparseArray<MetadataSyncAdapter> sPerUserMetadataSyncAdapter =
+ new SparseArray<>();
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Returns the per-user metadata sync adapter for the given user.
+ *
+ * @param user The user for which to get the metadata sync adapter.
+ * @param userContext The user context for the given user.
+ * @return The metadata sync adapter for the given user.
+ */
+ @Nullable
+ public static MetadataSyncAdapter getPerUserMetadataSyncAdapter(
+ UserHandle user, Context userContext) {
+ synchronized (sLock) {
+ MetadataSyncAdapter metadataSyncAdapter =
+ sPerUserMetadataSyncAdapter.get(user.getIdentifier(), null);
+ if (metadataSyncAdapter == null) {
+ AppSearchManager perUserAppSearchManager =
+ userContext.getSystemService(AppSearchManager.class);
+ PackageManager perUserPackageManager = userContext.getPackageManager();
+ if (perUserAppSearchManager != null) {
+ metadataSyncAdapter =
+ new MetadataSyncAdapter(
+ AppFunctionExecutors.getPerUserSyncExecutor(user),
+ perUserPackageManager,
+ perUserAppSearchManager);
+ sPerUserMetadataSyncAdapter.put(user.getIdentifier(), metadataSyncAdapter);
+ return metadataSyncAdapter;
+ }
+ }
+ return metadataSyncAdapter;
+ }
+ }
+
+ /**
+ * Removes the per-user metadata sync adapter for the given user.
+ *
+ * @param user The user for which to remove the metadata sync adapter.
+ */
+ public static void removeUserSyncAdapter(UserHandle user) {
+ synchronized (sLock) {
+ sPerUserMetadataSyncAdapter.remove(user.getIdentifier());
+ }
+ }
+}
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
index 63cf7bf..c05c381 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -139,16 +139,15 @@
runtimeSearchSession.put(putDocumentsRequest).get()
staticSearchSession.put(putDocumentsRequest).get()
val metadataSyncAdapter =
- MetadataSyncAdapter(
- testExecutor,
- runtimeSearchSession,
+ MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
+
+ val submitSyncRequest =
+ metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
staticSearchSession,
- packageManager,
+ runtimeSearchSession,
)
- val submitSyncRequest = metadataSyncAdapter.submitSyncRequest()
-
- assertThat(submitSyncRequest.get()).isTrue()
+ assertThat(submitSyncRequest).isInstanceOf(Unit::class.java)
}
@Test
@@ -182,16 +181,15 @@
PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
staticSearchSession.put(putDocumentsRequest).get()
val metadataSyncAdapter =
- MetadataSyncAdapter(
- testExecutor,
- runtimeSearchSession,
+ MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
+
+ val submitSyncRequest =
+ metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
staticSearchSession,
- packageManager,
+ runtimeSearchSession,
)
- val submitSyncRequest = metadataSyncAdapter.submitSyncRequest()
-
- assertThat(submitSyncRequest.get()).isTrue()
+ assertThat(submitSyncRequest).isInstanceOf(Unit::class.java)
}
@Test
@@ -239,16 +237,15 @@
PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
runtimeSearchSession.put(putDocumentsRequest).get()
val metadataSyncAdapter =
- MetadataSyncAdapter(
- testExecutor,
- runtimeSearchSession,
+ MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
+
+ val submitSyncRequest =
+ metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
staticSearchSession,
- packageManager,
+ runtimeSearchSession,
)
- val submitSyncRequest = metadataSyncAdapter.submitSyncRequest()
-
- assertThat(submitSyncRequest.get()).isTrue()
+ assertThat(submitSyncRequest).isInstanceOf(Unit::class.java)
}
@Test