Minor improvements on AdServicesManagerService and related classes:

- Added @GuardeBy annotations.
- Made collections final.
- Implemented Dumpable.
- Removed redundant unit test.

Test: adb shell dumpsys system_server_dumper --name AdServices # on UDC
Test: adb shell dumpsys sdk_sandbox --AdServices # on TM

Test: atest AdServicesManagerServiceTest UserInstanceManagerTest com.android.server.adservices.tests.TopicsDaoTest TopicsDbHelperTest SdkSandboxManagerServiceUnitTest
Bug: 280677793

Change-Id: I2da6527977d0188f5cc0616505280720a841d160
(cherry picked from commit d106cb6bfe3a9cc1b792a685ab27b377101f19e1)
diff --git a/adservices/service/java/com/android/server/adservices/AdServicesManagerService.java b/adservices/service/java/com/android/server/adservices/AdServicesManagerService.java
index ffe862f..a4c3bf0 100644
--- a/adservices/service/java/com/android/server/adservices/AdServicesManagerService.java
+++ b/adservices/service/java/com/android/server/adservices/AdServicesManagerService.java
@@ -41,7 +41,9 @@
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.util.ArrayMap;
+import android.util.Dumpable;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.SystemService;
@@ -49,7 +51,9 @@
 import com.android.server.adservices.feature.PrivacySandboxFeatureType;
 import com.android.server.sdksandbox.SdkSandboxManagerLocal;
 
+import java.io.FileDescriptor;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -88,16 +92,30 @@
 
     private final Context mContext;
 
+    @GuardedBy("mRegisterReceiverLock")
     private BroadcastReceiver mSystemServicePackageChangedReceiver;
+
+    @GuardedBy("mRegisterReceiverLock")
     private BroadcastReceiver mSystemServiceUserActionReceiver;
 
+    @GuardedBy("mRegisterReceiverLock")
     private HandlerThread mHandlerThread;
+
+    @GuardedBy("mRegisterReceiverLock")
     private Handler mHandler;
 
+    @GuardedBy("mSetPackageVersionLock")
     private int mAdServicesModuleVersion;
+
+    @GuardedBy("mSetPackageVersionLock")
     private String mAdServicesModuleName;
-    private Map<Integer, VersionedPackage> mAdServicesPackagesRolledBackFrom = new ArrayMap<>();
-    private Map<Integer, VersionedPackage> mAdServicesPackagesRolledBackTo = new ArrayMap<>();
+
+    @GuardedBy("mRollbackCheckLock")
+    private final Map<Integer, VersionedPackage> mAdServicesPackagesRolledBackFrom =
+            new ArrayMap<>();
+
+    @GuardedBy("mRollbackCheckLock")
+    private final Map<Integer, VersionedPackage> mAdServicesPackagesRolledBackTo = new ArrayMap<>();
 
     // This will be triggered when there is a flag change.
     private final DeviceConfig.OnPropertiesChangedListener mOnFlagsChangedListener =
@@ -128,8 +146,8 @@
     }
 
     /** @hide */
-    public static class Lifecycle extends SystemService {
-        private AdServicesManagerService mService;
+    public static final class Lifecycle extends SystemService implements Dumpable {
+        private final AdServicesManagerService mService;
 
         /** @hide */
         public Lifecycle(Context context) {
@@ -138,6 +156,7 @@
             mService =
                     new AdServicesManagerService(
                             context, new UserInstanceManager(topicsDao, ADSERVICES_BASE_DIR));
+            LogUtil.d("AdServicesManagerService constructed!");
         }
 
         /** @hide */
@@ -162,6 +181,17 @@
                         "SdkSandboxManagerLocal not found when registering AdServicesManager!");
             }
         }
+
+        @Override
+        public String getDumpableName() {
+            return "AdServices";
+        }
+
+        @Override
+        public void dump(PrintWriter writer, String[] args) {
+            // Usage: adb shell dumpsys system_server_dumper --name AdServices
+            mService.dump(/* fd= */ null, writer, args);
+        }
     }
 
     @Override
@@ -761,6 +791,25 @@
         }
     }
 
+    @Override
+    @RequiresPermission(android.Manifest.permission.DUMP)
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mContext.enforceCallingPermission(android.Manifest.permission.DUMP, /* message= */ null);
+
+        synchronized (mSetPackageVersionLock) {
+            pw.printf("mAdServicesModuleName: %s\n", mAdServicesModuleName);
+            pw.printf("mAdServicesModuleVersion: %d\n", mAdServicesModuleVersion);
+        }
+        synchronized (mRegisterReceiverLock) {
+            pw.printf("mHandlerThread: %s\n", mHandlerThread);
+        }
+        synchronized (mRollbackCheckLock) {
+            pw.printf("mAdServicesPackagesRolledBackFrom: %s\n", mAdServicesPackagesRolledBackFrom);
+            pw.printf("mAdServicesPackagesRolledBackTo: %s\n", mAdServicesPackagesRolledBackTo);
+        }
+        mUserInstanceManager.dump(pw, args);
+    }
+
     @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_MANAGER)
     public void recordAdServicesDeletionOccurred(
             @AdServicesManager.DeletionApiType int deletionType) {
@@ -939,16 +988,14 @@
         synchronized (mRollbackCheckLock) {
             if (!FlagsFactory.getFlags().getAdServicesSystemServiceEnabled()) {
                 LogUtil.d("AdServicesSystemServiceEnabled is FALSE.");
-                mAdServicesPackagesRolledBackFrom = new ArrayMap<>();
-                mAdServicesPackagesRolledBackTo = new ArrayMap<>();
+                resetRollbackArraysRCLocked();
                 return;
             }
 
             RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
             if (rollbackManager == null) {
                 LogUtil.d("Failed to get the RollbackManager service.");
-                mAdServicesPackagesRolledBackFrom = new ArrayMap<>();
-                mAdServicesPackagesRolledBackTo = new ArrayMap<>();
+                resetRollbackArraysRCLocked();
                 return;
             }
             List<RollbackInfo> recentlyCommittedRollbacks =
@@ -974,6 +1021,12 @@
         }
     }
 
+    @GuardedBy("mRollbackCheckLock")
+    private void resetRollbackArraysRCLocked() {
+        mAdServicesPackagesRolledBackFrom.clear();
+        mAdServicesPackagesRolledBackTo.clear();
+    }
+
     @VisibleForTesting
     Map<Integer, VersionedPackage> getAdServicesPackagesRolledBackFrom() {
         return mAdServicesPackagesRolledBackFrom;
diff --git a/adservices/service/java/com/android/server/adservices/UserInstanceManager.java b/adservices/service/java/com/android/server/adservices/UserInstanceManager.java
index 6903efa..fe7d478 100644
--- a/adservices/service/java/com/android/server/adservices/UserInstanceManager.java
+++ b/adservices/service/java/com/android/server/adservices/UserInstanceManager.java
@@ -28,6 +28,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -153,6 +154,27 @@
         }
     }
 
+    void dump(PrintWriter writer, String[] args) {
+        writer.println("UserInstanceManager");
+        String prefix = "  ";
+        writer.printf("%smAdServicesBaseDir: %s\n", prefix, mAdServicesBaseDir);
+        synchronized (mLock) {
+            writer.printf("%smConsentManagerMapLocked: %s\n", prefix, mConsentManagerMapLocked);
+            writer.printf(
+                    "%smAppConsentManagerMapLocked: %s\n", prefix, mAppConsentManagerMapLocked);
+            writer.printf(
+                    "%smRollbackHandlingManagerMapLocked: %s\n",
+                    prefix, mRollbackHandlingManagerMapLocked);
+        }
+        synchronized (UserInstanceManager.class) {
+            writer.printf(
+                    "%smBlockedTopicsManagerMapLocked=%s\n",
+                    prefix, mBlockedTopicsManagerMapLocked);
+        }
+
+        mTopicsDao.dump(writer, prefix, args);
+    }
+
     @VisibleForTesting
     void tearDownForTesting() {
         synchronized (mLock) {
diff --git a/adservices/service/java/com/android/server/adservices/data/topics/TopicsDao.java b/adservices/service/java/com/android/server/adservices/data/topics/TopicsDao.java
index 36ce77f..c6741e1 100644
--- a/adservices/service/java/com/android/server/adservices/data/topics/TopicsDao.java
+++ b/adservices/service/java/com/android/server/adservices/data/topics/TopicsDao.java
@@ -29,6 +29,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.adservices.LogUtil;
 
+import java.io.PrintWriter;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -204,4 +205,9 @@
             LogUtil.e("Failed to remove all blocked topics." + e.getMessage());
         }
     }
+
+    /** Dumps its internal state. */
+    public void dump(PrintWriter writer, String prefix, String[] args) {
+        mTopicsDbHelper.dump(writer, prefix, args);
+    }
 }
diff --git a/adservices/service/java/com/android/server/adservices/data/topics/TopicsDbHelper.java b/adservices/service/java/com/android/server/adservices/data/topics/TopicsDbHelper.java
index 25f9e44..174aa60 100644
--- a/adservices/service/java/com/android/server/adservices/data/topics/TopicsDbHelper.java
+++ b/adservices/service/java/com/android/server/adservices/data/topics/TopicsDbHelper.java
@@ -27,6 +27,7 @@
 import com.android.server.adservices.LogUtil;
 
 import java.io.File;
+import java.io.PrintWriter;
 
 /**
  * Helper to manage the Topics API system service database. Designed as a singleton to make sure
@@ -115,4 +116,13 @@
             return null;
         }
     }
+
+    /** Dumps its internal state. */
+    public void dump(PrintWriter writer, String prefix, String[] args) {
+        writer.printf("%sTopicsDbHelper\n", prefix);
+        String prefix2 = prefix + "  ";
+        writer.printf("%sCURRENT_DATABASE_VERSION: %d\n", prefix2, CURRENT_DATABASE_VERSION);
+        writer.printf("%smDbFile: %s\n", prefix2, mDbFile);
+        writer.printf("%smDbVersion: %d\n", prefix2, mDbVersion);
+    }
 }
diff --git a/adservices/tests/unittest/system-service/src/com/android/server/adservices/AdServicesManagerServiceTest.java b/adservices/tests/unittest/system-service/src/com/android/server/adservices/AdServicesManagerServiceTest.java
index f169982..de7adba 100644
--- a/adservices/tests/unittest/system-service/src/com/android/server/adservices/AdServicesManagerServiceTest.java
+++ b/adservices/tests/unittest/system-service/src/com/android/server/adservices/AdServicesManagerServiceTest.java
@@ -19,11 +19,14 @@
 import static com.android.server.adservices.PhFlags.KEY_ADSERVICES_SYSTEM_SERVICE_ENABLED;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
@@ -71,6 +74,8 @@
 import org.mockito.MockitoAnnotations;
 
 import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -108,6 +113,7 @@
     private static final int TEST_ROLLED_BACK_FROM_MODULE_VERSION = 339990000;
     private static final int TEST_ROLLED_BACK_TO_MODULE_VERSION = 330000000;
     private static final int ROLLBACK_ID = 1768705420;
+    private static final String USER_INSTANCE_MANAGER_DUMP = "D'OHump!";
 
     @Before
     public void setup() {
@@ -119,7 +125,13 @@
         TopicsDao topicsDao = new TopicsDao(mDBHelper);
         mUserInstanceManager =
                 new UserInstanceManager(
-                        topicsDao, /* adservicesBaseDir */ context.getFilesDir().getAbsolutePath());
+                        topicsDao,
+                        /* adServicesBaseDir= */ context.getFilesDir().getAbsolutePath()) {
+                    @Override
+                    public void dump(PrintWriter writer, String[] args) {
+                        writer.println(USER_INSTANCE_MANAGER_DUMP);
+                    }
+                };
 
         InstrumentationRegistry.getInstrumentation()
                 .getUiAutomation()
@@ -947,6 +959,36 @@
                 .isEqualTo(PrivacySandboxFeatureType.PRIVACY_SANDBOX_UNSUPPORTED.name());
     }
 
+    @Test
+    public void testDump_noPermission() throws Exception {
+        mService = new AdServicesManagerService(mSpyContext, mUserInstanceManager);
+
+        assertThrows(
+                SecurityException.class,
+                () -> mService.dump(/* fd= */ null, /* pw= */ null, /* args= */ null));
+    }
+
+    @Test
+    public void testDump() throws Exception {
+        doNothing()
+                .when(mSpyContext)
+                .enforceCallingPermission(eq(android.Manifest.permission.DUMP), isNull());
+        mService = new AdServicesManagerService(mSpyContext, mUserInstanceManager);
+
+        String dump;
+        try (StringWriter sw = new StringWriter()) {
+            PrintWriter pw = new PrintWriter(sw);
+
+            mService.dump(/* fd= */ null, pw, /* args= */ null);
+
+            pw.flush();
+            dump = sw.toString();
+        }
+        // Content doesn't matter much, we just wanna make sure it doesn't crash (for example,
+        // by using the wrong %s / %d tokens) and that its components are dumped
+        assertWithMessage("content of dump()").that(dump).contains(USER_INSTANCE_MANAGER_DUMP);
+    }
+
     // Mock the call to get the AdServices module version from the PackageManager.
     private void setAdServicesModuleVersion(AdServicesManagerService service, int version) {
         doReturn(version).when(service).getAdServicesApexVersion();
diff --git a/adservices/tests/unittest/system-service/src/com/android/server/adservices/UserInstanceManagerTest.java b/adservices/tests/unittest/system-service/src/com/android/server/adservices/UserInstanceManagerTest.java
index 14f9cb0..403afef 100644
--- a/adservices/tests/unittest/system-service/src/com/android/server/adservices/UserInstanceManagerTest.java
+++ b/adservices/tests/unittest/system-service/src/com/android/server/adservices/UserInstanceManagerTest.java
@@ -19,6 +19,7 @@
 import static com.android.server.adservices.data.topics.TopicsTables.DUMMY_MODEL_VERSION;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.adservices.topics.Topic;
 import android.app.adservices.topics.TopicParcel;
@@ -39,6 +40,8 @@
 import org.junit.Test;
 
 import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.List;
 import java.util.Set;
 
@@ -47,6 +50,8 @@
     private static final Context APPLICATION_CONTEXT = ApplicationProvider.getApplicationContext();
     private static final String TEST_BASE_PATH =
             APPLICATION_CONTEXT.getFilesDir().getAbsolutePath();
+    private static final String TOPICS_DAO_DUMP = "D'OHump!";
+
     private final TopicsDbHelper mDBHelper = TopicsDbTestUtil.getDbHelperForTest();
     private TopicsDao mTopicsDao;
 
@@ -229,4 +234,31 @@
         assertThat(blockedTopics1).hasSize(1);
         assertThat(blockedTopics1).containsExactly(topicToBlock1);
     }
+
+    @Test
+    public void testDump() throws Exception {
+        TopicsDao topicsDao =
+                new TopicsDao(mDBHelper) {
+                    @Override
+                    public void dump(PrintWriter writer, String prefix, String[] args) {
+                        writer.println(TOPICS_DAO_DUMP);
+                    }
+                };
+        UserInstanceManager mgr = new UserInstanceManager(topicsDao, TEST_BASE_PATH);
+
+        String dump;
+        String[] args = new String[0];
+        try (StringWriter sw = new StringWriter()) {
+            PrintWriter pw = new PrintWriter(sw);
+
+            mgr.dump(pw, args);
+
+            pw.flush();
+            dump = sw.toString();
+        }
+
+        // Content doesn't matter much, we just wanna make sure it doesn't crash (for example,
+        // by using the wrong %s / %d tokens) and that its components are dumped
+        assertWithMessage("content of dump()").that(dump).contains(TOPICS_DAO_DUMP);
+    }
 }
diff --git a/adservices/tests/unittest/system-service/src/com/android/server/adservices/data/topics/TopicsDaoTest.java b/adservices/tests/unittest/system-service/src/com/android/server/adservices/data/topics/TopicsDaoTest.java
index db25b32..d4acd60 100644
--- a/adservices/tests/unittest/system-service/src/com/android/server/adservices/data/topics/TopicsDaoTest.java
+++ b/adservices/tests/unittest/system-service/src/com/android/server/adservices/data/topics/TopicsDaoTest.java
@@ -20,6 +20,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
 import android.adservices.topics.Topic;
 
 import org.junit.After;
@@ -27,6 +30,7 @@
 import org.junit.Test;
 import org.mockito.MockitoAnnotations;
 
+import java.io.PrintWriter;
 import java.util.List;
 import java.util.Set;
 
@@ -176,4 +180,19 @@
         assertThat(blockedTopics1).hasSize(1);
         assertThat(blockedTopics1).containsExactly(topicToBlock1);
     }
+
+    @Test
+    public void testDump() {
+        // Currently, TopicsDao just delegates to helper, so we're being pragmactic and just
+        // testing that
+        PrintWriter writer = mock(PrintWriter.class);
+        String prefix = "TOPICS DAO";
+        String[] args = new String[] {"Y", "U", "NO", "FAKE?"};
+        TopicsDbHelper mockTopicsDbHelper = mock(TopicsDbHelper.class);
+        TopicsDao topicsDao = new TopicsDao(mockTopicsDbHelper);
+
+        topicsDao.dump(writer, prefix, args);
+
+        verify(mockTopicsDbHelper).dump(writer, prefix, args);
+    }
 }
diff --git a/adservices/tests/unittest/system-service/src/com/android/server/adservices/data/topics/TopicsDbHelperTest.java b/adservices/tests/unittest/system-service/src/com/android/server/adservices/data/topics/TopicsDbHelperTest.java
index 3700974..0532626 100644
--- a/adservices/tests/unittest/system-service/src/com/android/server/adservices/data/topics/TopicsDbHelperTest.java
+++ b/adservices/tests/unittest/system-service/src/com/android/server/adservices/data/topics/TopicsDbHelperTest.java
@@ -18,13 +18,23 @@
 
 import static com.android.server.adservices.data.topics.TopicsDbTestUtil.doesTableExistAndColumnCountMatch;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.database.sqlite.SQLiteDatabase;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import org.junit.Test;
 
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
 /** Unit test to test class {@link TopicsDbHelper} */
 public class TopicsDbHelperTest {
     @Test
@@ -33,4 +43,49 @@
         assertNotNull(db);
         assertTrue(doesTableExistAndColumnCountMatch(db, "blocked_topics", 4));
     }
+
+    @Test
+    public void testDump() throws Exception {
+        String dump;
+        String prefix = "fixed, pre is:";
+        try (StringWriter sw = new StringWriter()) {
+            PrintWriter pw = new PrintWriter(sw);
+
+            TopicsDbHelper dao =
+                    TopicsDbHelper.getInstance(
+                            InstrumentationRegistry.getInstrumentation().getTargetContext());
+            dao.dump(pw, prefix, /* args= */ null);
+
+            pw.flush();
+            dump = sw.toString();
+        }
+
+        // Content doesn't matter much, we just wanna make sure it doesn't crash (for example,
+        // by using the wrong %s / %d tokens) and every line dumps the prefix
+        assertDumpHasPrefix(dump, prefix);
+    }
+
+    // TODO(b/280677793): move to common code?
+    static String[] assertDumpHasPrefix(String dump, String prefix) {
+        assertWithMessage("content of dump()").that(dump).isNotEmpty();
+
+        String[] lines = dump.split("\n");
+        List<Integer> violatedLineNumbers = new ArrayList<>();
+        for (int i = 0; i < lines.length; i++) {
+            String line = lines[i];
+            if (!line.startsWith(prefix)) {
+                violatedLineNumbers.add(i);
+            }
+        }
+        if (!violatedLineNumbers.isEmpty()) {
+            fail(
+                    "Every line should start with '"
+                            + prefix
+                            + "', but some ("
+                            + violatedLineNumbers
+                            + ") did not. Full dump(): \n"
+                            + dump);
+        }
+        return lines;
+    }
 }
diff --git a/sdksandbox/service/java/com/android/server/sdksandbox/SdkSandboxManagerService.java b/sdksandbox/service/java/com/android/server/sdksandbox/SdkSandboxManagerService.java
index 664879b..8f4e945 100644
--- a/sdksandbox/service/java/com/android/server/sdksandbox/SdkSandboxManagerService.java
+++ b/sdksandbox/service/java/com/android/server/sdksandbox/SdkSandboxManagerService.java
@@ -33,6 +33,7 @@
 import static com.android.server.wm.ActivityInterceptorCallback.MAINLINE_SDK_SANDBOX_ORDER_ID;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.WorkerThread;
 import android.app.ActivityManager;
@@ -122,6 +123,8 @@
     private static final String SANDBOX_NOT_AVAILABLE_MSG = "Sandbox is unavailable";
     private static final String SANDBOX_DISABLED_MSG = "SDK sandbox is disabled";
 
+    private static final String DUMP_ARG_AD_SERVICES = "--AdServices";
+
     private final Context mContext;
 
     private final ActivityManager mActivityManager;
@@ -227,6 +230,13 @@
 
     private static final String WEBVIEW_SAFE_MODE_CONTENT_PROVIDER = "SafeModeContentProvider";
 
+    // On UDC, AdServicesManagerService.Lifecycle implements dumpable so it's dumped as part of
+    // SystemServer.
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    static final String POST_UDC_DUMP_AD_SERVICES_MESSAGE =
+            "Don't need to dump AdServices on UDC+ - use "
+                    + "'dumpsys system_server_dumper --name AdServices instead'";
+
     static class Injector {
         private final Context mContext;
         private SdkSandboxManagerLocal mLocalManager;
@@ -1163,6 +1173,11 @@
         mContext.enforceCallingPermission(android.Manifest.permission.DUMP,
                 "Can't dump " + TAG);
 
+        if (args != null && args.length > 0 && args[0].equals(DUMP_ARG_AD_SERVICES)) {
+            dumpAdServices(fd, writer, args, /* quiet= */ false);
+            return;
+        }
+
         // TODO(b/211575098): Use IndentingPrintWriter for better formatting
         synchronized (mLock) {
             writer.println("Checked Webview visibility patch exists: " + mCheckedVisibilityPatch);
@@ -1192,6 +1207,41 @@
         writer.println("mServiceProvider:");
         mServiceProvider.dump(writer);
         writer.println();
+
+        dumpAdServices(fd, writer, args, /* quiet= */ true);
+    }
+
+    private void dumpAdServices(
+            @Nullable FileDescriptor fd, PrintWriter writer, String[] args, boolean quiet) {
+        if (SdkLevel.isAtLeastU()) {
+            // On UDC, AdServicesManagerService.Lifecycle implements dumpable so it's dumped as
+            // part of SystemServer, hence this special arg option is not needed
+            if (quiet) {
+                Log.d(TAG, POST_UDC_DUMP_AD_SERVICES_MESSAGE);
+            } else {
+                writer.println(POST_UDC_DUMP_AD_SERVICES_MESSAGE);
+            }
+            return;
+        }
+        writer.print("AdServices:");
+        IBinder adServicesManager = getAdServicesManager();
+        if (adServicesManager == null) {
+            // Should not happen on "real life", but it could on unit tests.
+            Log.e(TAG, "dumpAdServices(): mAdServicesManager not set");
+            writer.println(" N/A");
+            return;
+        }
+        writer.println();
+        writer.println();
+        writer.flush(); // must flush, other raw dump on fd below will be printed before it
+        try {
+            adServicesManager.dump(fd, args);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to dump AdServices", e);
+            // Shouldn't happen, but it doesn't hurt to catch
+            writer.printf("Failed to dump Adservices: %s\n", e);
+        }
+        writer.println();
     }
 
     @Override
@@ -1522,7 +1572,8 @@
         }
     }
 
-    private void registerAdServicesManagerService(IBinder iBinder) {
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    void registerAdServicesManagerService(IBinder iBinder) {
         synchronized (mLock) {
             mAdServicesManager = iBinder;
         }
diff --git a/sdksandbox/tests/unittest/src/com/android/server/sdksandbox/SdkSandboxManagerServiceUnitTest.java b/sdksandbox/tests/unittest/src/com/android/server/sdksandbox/SdkSandboxManagerServiceUnitTest.java
index 2087c02..15af3bd 100644
--- a/sdksandbox/tests/unittest/src/com/android/server/sdksandbox/SdkSandboxManagerServiceUnitTest.java
+++ b/sdksandbox/tests/unittest/src/com/android/server/sdksandbox/SdkSandboxManagerServiceUnitTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.sdksandbox;
 
+import static android.Manifest.permission.DUMP;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
 import static android.app.sdksandbox.ISharedPreferencesSyncCallback.PREFERENCES_SYNC_INTERNAL_ERROR;
 import static android.app.sdksandbox.SdkSandboxManager.ACTION_START_SANDBOXED_ACTIVITY;
@@ -36,6 +37,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.eq;
 
@@ -76,6 +78,7 @@
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.util.ArrayMap;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -102,6 +105,7 @@
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoSession;
 
@@ -120,10 +124,12 @@
  */
 public class SdkSandboxManagerServiceUnitTest {
 
+    private static final String TAG = SdkSandboxManagerServiceUnitTest.class.getSimpleName();
+
     private SdkSandboxManagerService mService;
     private ActivityManager mAmSpy;
     private FakeSdkSandboxService mSdkSandboxService;
-    private MockitoSession mStaticMockSession = null;
+    private MockitoSession mStaticMockSession;
     private Context mSpyContext;
     private SdkSandboxManagerService.Injector mInjector;
     private int mClientAppUid;
@@ -133,6 +139,8 @@
             ArgumentCaptor.forClass(ActivityInterceptorCallback.class);
     private SdkSandboxStorageManagerUtility mSdkSandboxStorageManagerUtility;
 
+    @Mock private IBinder mAdServicesManager;
+
     private static FakeSdkSandboxProvider sProvider;
     private static SdkSandboxPulledAtoms sSdkSandboxPulledAtoms;
 
@@ -185,7 +193,8 @@
                 ExtendedMockito.mockitoSession()
                         .mockStatic(LocalManagerRegistry.class)
                         .mockStatic(SdkSandboxStatsLog.class)
-                        .spyStatic(Process.class);
+                        .spyStatic(Process.class)
+                        .initMocks(this);
         if (SdkLevel.isAtLeastU()) {
             mockitoSessionBuilder =
                     mockitoSessionBuilder.mockStatic(ActivityInterceptorCallbackRegistry.class);
@@ -1000,11 +1009,6 @@
         assertThat(lifecycleCallback2.getSdkSandboxDeathCount()).isEqualTo(1);
     }
 
-    @Test(expected = SecurityException.class)
-    public void testDumpWithoutPermission() {
-        mService.dump(new FileDescriptor(), new PrintWriter(new StringWriter()), new String[0]);
-    }
-
     @Test
     public void testSdkSandboxServiceUnbindingWhenAppDied() throws Exception {
         disableKillUid();
@@ -1713,15 +1717,100 @@
     }
 
     @Test
-    public void testDump() throws Exception {
-        Mockito.doNothing()
-                .when(mSpyContext)
-                .enforceCallingPermission(
-                        Mockito.eq("android.permission.DUMP"), Mockito.anyString());
+    public void testDump_preU() throws Exception {
+        requiresAtLeastU(false);
+        mockGrantedPermission(DUMP);
+        mService.registerAdServicesManagerService(mAdServicesManager);
 
-        final StringWriter stringWriter = new StringWriter();
-        mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), null);
-        assertThat(stringWriter.toString()).contains("FakeDump");
+        String dump;
+        try (StringWriter stringWriter = new StringWriter()) {
+            // Mock call to mAdServicesManager.dump();
+            FileDescriptor fd = new FileDescriptor();
+            PrintWriter writer = new PrintWriter(stringWriter);
+            String[] args = new String[0];
+            Mockito.doAnswer(
+                    (inv) -> {
+                        writer.println("FakeAdServiceDump");
+                        return null;
+                    })
+                    .when(mAdServicesManager)
+                    .dump(fd, args);
+
+            mService.dump(fd, writer, args);
+
+            dump = stringWriter.toString();
+        }
+
+        assertThat(dump).contains("FakeDump");
+        assertThat(dump).contains("FakeAdServiceDump");
+    }
+
+    @Test
+    public void testDump_atLeastU() throws Exception {
+        requiresAtLeastU(true);
+        mockGrantedPermission(DUMP);
+        mService.registerAdServicesManagerService(mAdServicesManager);
+
+        String dump;
+        try (StringWriter stringWriter = new StringWriter()) {
+            mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]);
+            dump = stringWriter.toString();
+        }
+
+        assertThat(dump).contains("FakeDump");
+
+        Mockito.verify(mAdServicesManager, Mockito.never())
+                .dump(ArgumentMatchers.any(), ArgumentMatchers.any());
+    }
+
+    @Test
+    public void testDump_adServices_preU() throws Exception {
+        requiresAtLeastU(false);
+        mockGrantedPermission(DUMP);
+        mService.registerAdServicesManagerService(mAdServicesManager);
+
+        String dump;
+        try (StringWriter stringWriter = new StringWriter()) {
+            // Mock call to mAdServicesManager.dump();
+            FileDescriptor fd = new FileDescriptor();
+            PrintWriter writer = new PrintWriter(stringWriter);
+            String[] args = new String[] {"--AdServices"};
+            Mockito.doAnswer(
+                    (inv) -> {
+                        writer.println("FakeAdServiceDump");
+                        return null;
+                    })
+                    .when(mAdServicesManager)
+                    .dump(fd, args);
+
+            mService.dump(fd, writer, args);
+
+            dump = stringWriter.toString();
+        }
+
+        assertThat(dump).isEqualTo("AdServices:\n\nFakeAdServiceDump\n\n");
+    }
+
+    @Test
+    public void testDump_adServices_atLeastU() throws Exception {
+        requiresAtLeastU(true);
+        mockGrantedPermission(DUMP);
+        mService.registerAdServicesManagerService(mAdServicesManager);
+
+        String dump;
+        try (StringWriter stringWriter = new StringWriter()) {
+            mService.dump(
+                    new FileDescriptor(),
+                    new PrintWriter(stringWriter),
+                    new String[] {"--AdServices"});
+            dump = stringWriter.toString();
+        }
+
+        assertThat(dump)
+                .isEqualTo(SdkSandboxManagerService.POST_UDC_DUMP_AD_SERVICES_MESSAGE + "\n");
+
+        Mockito.verify(mAdServicesManager, Mockito.never())
+                .dump(ArgumentMatchers.any(), ArgumentMatchers.any());
     }
 
     @Test(expected = SecurityException.class)
@@ -3356,6 +3445,29 @@
         return mSpyContext.getPackageManager().getSdkSandboxPackageName();
     }
 
+    private void mockGrantedPermission(String permission) {
+        Log.d(TAG, "mockGrantedPermission(" + permission + ")");
+        Mockito.doNothing()
+                .when(mSpyContext)
+                .enforceCallingPermission(Mockito.eq(permission), Mockito.anyString());
+    }
+
+    private void requiresAtLeastU(boolean required) {
+        Log.d(
+                TAG,
+                "requireAtLeastU("
+                        + required
+                        + "): SdkLevel.isAtLeastU()="
+                        + SdkLevel.isAtLeastU());
+        // TODO(b/280677793): rather than assuming it's the given version, mock it:
+        //     ExtendedMockito.doReturn(required).when(() -> SdkLevel.isAtLeastU());
+        if (required) {
+            assumeTrue("Device must be at least U", SdkLevel.isAtLeastU());
+        } else {
+            assumeFalse("Device must be less than U", SdkLevel.isAtLeastU());
+        }
+    }
+
     /** Fake service provider that returns local instance of {@link SdkSandboxServiceProvider} */
     private static class FakeSdkSandboxProvider implements SdkSandboxServiceProvider {
         private FakeSdkSandboxService mSdkSandboxService;