Merge "Remove unnecessary semicolon" into main
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index caf3152..81f2cf9 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -184,7 +184,9 @@
 
     @GuardedBy("TrafficStats.class")
     private static INetworkStatsService sStatsService;
-    @GuardedBy("TrafficStats.class")
+
+    // The variable will only be accessed in the test, which is effectively
+    // single-threaded.
     private static INetworkStatsService sStatsServiceForTest = null;
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
@@ -209,9 +211,7 @@
      */
     @VisibleForTesting(visibility = PRIVATE)
     public static void setServiceForTest(INetworkStatsService statsService) {
-        synchronized (TrafficStats.class) {
-            sStatsServiceForTest = statsService;
-        }
+        sStatsServiceForTest = statsService;
     }
 
     /**
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
index 851d09a..bde55c3 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
@@ -17,14 +17,19 @@
 package com.android.net.module.util
 
 import android.util.Log
+import com.android.testutils.TryTestConfig
 import com.android.testutils.tryTest
+import java.util.function.Consumer
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertTrue
-import kotlin.test.fail
 
 private val TAG = CleanupTest::class.simpleName
 
@@ -34,6 +39,18 @@
     class TestException2 : Exception()
     class TestException3 : Exception()
 
+    private var originalDiagnosticsCollector: Consumer<Throwable>? = null
+
+    @Before
+    fun setUp() {
+        originalDiagnosticsCollector = TryTestConfig.swapDiagnosticsCollector(null)
+    }
+
+    @After
+    fun tearDown() {
+        TryTestConfig.swapDiagnosticsCollector(originalDiagnosticsCollector)
+    }
+
     @Test
     fun testNotThrow() {
         var x = 1
@@ -220,4 +237,74 @@
         assertTrue(thrown.suppressedExceptions[1] is TestException3)
         assert(x == 7)
     }
+
+    @Test
+    fun testNoErrorReportingWhenCaught() {
+        var error: Throwable? = null
+        TryTestConfig.swapDiagnosticsCollector {
+            error = it
+        }
+        var x = 1
+        tryTest {
+            x = 2
+            throw TestException1()
+            x = 3
+        }.catch<TestException1> {
+            x = 4
+        } cleanup {
+            x = 5
+        }
+
+        assertEquals(5, x)
+        assertNull(error)
+    }
+
+    @Test
+    fun testErrorReportingInTry() {
+        var error: Throwable? = null
+        TryTestConfig.swapDiagnosticsCollector {
+            assertNull(error)
+            error = it
+        }
+        var x = 1
+        assertFailsWith<TestException1> {
+            tryTest {
+                x = 2
+                throw TestException1()
+                x = 3
+            } cleanupStep {
+                throw TestException2()
+                x = 4
+            } cleanup {
+                x = 5
+            }
+        }
+
+        assertEquals(5, x)
+        assertTrue(error is TestException1)
+    }
+
+    @Test
+    fun testErrorReportingInCatch() {
+        var error: Throwable? = null
+        TryTestConfig.swapDiagnosticsCollector {
+            assertNull(error)
+            error = it
+        }
+        var x = 1
+        assertFailsWith<TestException2> {
+            tryTest {
+                throw TestException1()
+                x = 2
+            }.catch<TestException1> {
+                throw TestException2()
+                x = 3
+            } cleanup {
+                x = 4
+            }
+        }
+
+        assertEquals(4, x)
+        assertTrue(error is TestException2)
+    }
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index 9e63910..e5b8471 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -114,7 +114,7 @@
     override fun onSetUp() {
         assertNull(instance, "ConnectivityDiagnosticsCollectors were set up multiple times")
         instance = this
-        TryTestConfig.setDiagnosticsCollector { throwable ->
+        TryTestConfig.swapDiagnosticsCollector { throwable ->
             if (runOnFailure(throwable)) {
                 collectTestFailureDiagnostics(throwable)
             }
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
index dcd422c..45c69c9 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
@@ -75,13 +75,21 @@
  */
 
 object TryTestConfig {
-    internal var diagnosticsCollector: Consumer<Throwable>? = null
+    private var diagnosticsCollector: Consumer<Throwable>? = null
 
     /**
      * Set the diagnostics collector to be used in case of failure in [tryTest].
+     *
+     * @return The previous collector.
      */
-    fun setDiagnosticsCollector(collector: Consumer<Throwable>) {
+    fun swapDiagnosticsCollector(collector: Consumer<Throwable>?): Consumer<Throwable>? {
+        val oldCollector = diagnosticsCollector
         diagnosticsCollector = collector
+        return oldCollector
+    }
+
+    fun reportError(e: Throwable) {
+        diagnosticsCollector?.accept(e)
     }
 }
 
@@ -90,14 +98,10 @@
         try {
             Result.success(block())
         } catch (e: Throwable) {
-            TryTestConfig.diagnosticsCollector?.accept(e)
             Result.failure(e)
-        })
+        }, skipErrorReporting = false)
 
-// Some downstream branches have an older kotlin that doesn't know about value classes.
-// TODO : Change this to "value class" when aosp no longer merges into such branches.
-@Suppress("INLINE_CLASS_DEPRECATED")
-inline class TryExpr<T>(val result: Result<T>) {
+class TryExpr<T>(val result: Result<T>, val skipErrorReporting: Boolean) {
     inline infix fun <reified E : Throwable> catch(block: (E) -> T): TryExpr<T> {
         val originalException = result.exceptionOrNull()
         if (originalException !is E) return this
@@ -105,23 +109,32 @@
             Result.success(block(originalException))
         } catch (e: Throwable) {
             Result.failure(e)
-        })
+        }, this.skipErrorReporting)
     }
 
     @CheckReturnValue
     inline infix fun cleanupStep(block: () -> Unit): TryExpr<T> {
+        // Report errors before the cleanup step, but after catch blocks that may suppress it
+        val originalException = result.exceptionOrNull()
+        var nextSkipErrorReporting = skipErrorReporting
+        if (!skipErrorReporting && originalException != null) {
+            TryTestConfig.reportError(originalException)
+            nextSkipErrorReporting = true
+        }
         try {
             block()
         } catch (e: Throwable) {
-            val originalException = result.exceptionOrNull()
-            return TryExpr(if (null == originalException) {
-                Result.failure(e)
+            return if (null == originalException) {
+                if (!skipErrorReporting) {
+                    TryTestConfig.reportError(e)
+                }
+                TryExpr(Result.failure(e), skipErrorReporting = true)
             } else {
                 originalException.addSuppressed(e)
-                Result.failure(originalException)
-            })
+                TryExpr(Result.failure(originalException), true)
+            }
         }
-        return this
+        return TryExpr(result, nextSkipErrorReporting)
     }
 
     inline infix fun cleanup(block: () -> Unit): T = cleanupStep(block).result.getOrThrow()
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 8e77b5d..320622b 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -21,6 +21,8 @@
 
 import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
 import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.net.ConnectivityManager
 import android.net.Network
@@ -50,6 +52,7 @@
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.PowerManager
+import android.os.UserManager
 import android.platform.test.annotations.AppModeFull
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
@@ -141,15 +144,34 @@
         fun turnScreenOff() {
             if (!wakeLock.isHeld()) wakeLock.acquire()
             runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
-            val result = pollingCheck({ !powerManager.isInteractive() }, timeout_ms = 2000)
-            assertThat(result).isTrue()
+            waitForInteractiveState(false)
         }
 
         fun turnScreenOn() {
             if (wakeLock.isHeld()) wakeLock.release()
             runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
-            val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
-            assertThat(result).isTrue()
+            waitForInteractiveState(true)
+        }
+
+        private fun waitForInteractiveState(interactive: Boolean) {
+            // TODO(b/366037029): This test condition should be removed once
+            // PowerManager#isInteractive is fully implemented on automotive
+            // form factor with visible background user.
+            if (isAutomotiveWithVisibleBackgroundUser()) {
+                // Wait for 2 seconds to ensure the interactive state is updated.
+                // This is a workaround for b/366037029.
+                Thread.sleep(2000L)
+            } else {
+                val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
+                assertThat(result).isEqualTo(interactive)
+            }
+        }
+
+        private fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
+            val packageManager = context.getPackageManager()
+            val userManager = context.getSystemService(UserManager::class.java)!!
+            return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE)
+                    && userManager.isVisibleBackgroundUsersSupported)
         }
 
         @BeforeClass
@@ -157,9 +179,11 @@
         @Suppress("ktlint:standard:no-multi-spaces")
         fun setupOnce() {
             // TODO: assertions thrown in @BeforeClass / @AfterClass are not well supported in the
-            // test infrastructure. Consider saving excepion and throwing it in setUp().
+            // test infrastructure. Consider saving exception and throwing it in setUp().
+
             // APF must run when the screen is off and the device is not interactive.
             turnScreenOff()
+
             // Wait for APF to become active.
             Thread.sleep(1000)
             // TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been