Merge "Revert "Batch calls to the Metadata Syncer"" 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/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 1e723b5..b4713d9 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -95,7 +95,12 @@
     public void onUserStopping(@NonNull TargetUser user) {
         Objects.requireNonNull(user);
 
-        MetadataSyncPerUser.removeUserSyncAdapter(user.getUserHandle());
+        try {
+            AppFunctionExecutors.shutDownAndRemoveUserExecutor(user.getUserHandle());
+            MetadataSyncPerUser.removeUserSyncAdapter(user.getUserHandle());
+        } catch (InterruptedException e) {
+            Slog.e(TAG, "Unable to remove data for: " + user.getUserHandle(), e);
+        }
     }
 
     @Override
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
index 759f02e..e29b6e4 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -42,7 +42,6 @@
 import android.util.ArraySet;
 import android.util.Slog;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.infra.AndroidFuture;
 import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults;
@@ -53,12 +52,8 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.Executor;
 
 /**
  * This class implements helper methods for synchronously interacting with AppSearch while
@@ -68,14 +63,9 @@
  */
 public class MetadataSyncAdapter {
     private static final String TAG = MetadataSyncAdapter.class.getSimpleName();
-
-    private final ExecutorService mExecutor;
-
+    private final Executor mSyncExecutor;
     private final AppSearchManager mAppSearchManager;
     private final PackageManager mPackageManager;
-    private final Object mLock = new Object();
-    @GuardedBy("mLock")
-    private Future<AndroidFuture<Boolean>> mCurrentSyncTask;
 
     // Hidden constants in {@link SetSchemaRequest} that restricts runtime metadata visibility
     // by permissions.
@@ -83,10 +73,12 @@
     public static final int EXECUTE_APP_FUNCTIONS_TRUSTED = 10;
 
     public MetadataSyncAdapter(
-            @NonNull PackageManager packageManager, @NonNull AppSearchManager appSearchManager) {
+            @NonNull Executor syncExecutor,
+            @NonNull PackageManager packageManager,
+            @NonNull AppSearchManager appSearchManager) {
+        mSyncExecutor = Objects.requireNonNull(syncExecutor);
         mPackageManager = Objects.requireNonNull(packageManager);
         mAppSearchManager = Objects.requireNonNull(appSearchManager);
-        mExecutor = Executors.newSingleThreadExecutor();
     }
 
     /**
@@ -105,7 +97,7 @@
                                 AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB)
                         .build();
         AndroidFuture<Boolean> settableSyncStatus = new AndroidFuture<>();
-        Callable<AndroidFuture<Boolean>> callableTask =
+        mSyncExecutor.execute(
                 () -> {
                     try (FutureAppSearchSession staticMetadataSearchSession =
                                     new FutureAppSearchSessionImpl(
@@ -125,28 +117,10 @@
                     } catch (Exception ex) {
                         settableSyncStatus.completeExceptionally(ex);
                     }
-                    return settableSyncStatus;
-                };
-
-        synchronized (mLock) {
-            if (mCurrentSyncTask != null && !mCurrentSyncTask.isDone()) {
-                boolean cancel = mCurrentSyncTask.cancel(false);
-            }
-            mCurrentSyncTask = mExecutor.submit(callableTask);
-        }
-
+                });
         return settableSyncStatus;
     }
 
-    /** This method shuts down the {@link MetadataSyncAdapter} scheduler. */
-    public void shutDown() {
-        try {
-            var unused = mExecutor.awaitTermination(30, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Slog.e(TAG, "Error shutting down MetadataSyncAdapter scheduler", e);
-        }
-    }
-
     @WorkerThread
     @VisibleForTesting
     void trySyncAppFunctionMetadataBlocking(
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java
index e933ec1..f421527 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncPerUser.java
@@ -55,7 +55,10 @@
                 PackageManager perUserPackageManager = userContext.getPackageManager();
                 if (perUserAppSearchManager != null) {
                     metadataSyncAdapter =
-                            new MetadataSyncAdapter(perUserPackageManager, perUserAppSearchManager);
+                            new MetadataSyncAdapter(
+                                    AppFunctionExecutors.getPerUserSyncExecutor(user),
+                                    perUserPackageManager,
+                                    perUserAppSearchManager);
                     sPerUserMetadataSyncAdapter.put(user.getIdentifier(), metadataSyncAdapter);
                     return metadataSyncAdapter;
                 }
@@ -71,12 +74,7 @@
      */
     public static void removeUserSyncAdapter(UserHandle user) {
         synchronized (sLock) {
-            MetadataSyncAdapter metadataSyncAdapter =
-                    sPerUserMetadataSyncAdapter.get(user.getIdentifier(), null);
-            if (metadataSyncAdapter != null) {
-                metadataSyncAdapter.shutDown();
-                sPerUserMetadataSyncAdapter.remove(user.getIdentifier());
-            }
+            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 bc64e15..c05c381 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -36,6 +36,7 @@
 import com.android.internal.infra.AndroidFuture
 import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults
 import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
 import java.util.concurrent.atomic.AtomicBoolean
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -45,6 +46,7 @@
 class MetadataSyncAdapterTest {
     private val context = InstrumentationRegistry.getInstrumentation().targetContext
     private val appSearchManager = context.getSystemService(AppSearchManager::class.java)
+    private val testExecutor = MoreExecutors.directExecutor()
     private val packageManager = context.packageManager
 
     @Test
@@ -136,7 +138,8 @@
             PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
         runtimeSearchSession.put(putDocumentsRequest).get()
         staticSearchSession.put(putDocumentsRequest).get()
-        val metadataSyncAdapter = MetadataSyncAdapter(packageManager, appSearchManager)
+        val metadataSyncAdapter =
+            MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
 
         val submitSyncRequest =
             metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
@@ -177,7 +180,8 @@
         val putDocumentsRequest: PutDocumentsRequest =
             PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
         staticSearchSession.put(putDocumentsRequest).get()
-        val metadataSyncAdapter = MetadataSyncAdapter(packageManager, appSearchManager)
+        val metadataSyncAdapter =
+            MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
 
         val submitSyncRequest =
             metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(
@@ -232,7 +236,8 @@
         val putDocumentsRequest: PutDocumentsRequest =
             PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
         runtimeSearchSession.put(putDocumentsRequest).get()
-        val metadataSyncAdapter = MetadataSyncAdapter(packageManager, appSearchManager)
+        val metadataSyncAdapter =
+            MetadataSyncAdapter(testExecutor, packageManager, appSearchManager)
 
         val submitSyncRequest =
             metadataSyncAdapter.trySyncAppFunctionMetadataBlocking(