Merge "Frozen-aware AppOpsService op noted callback" into main
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 1d654e13..7944be1 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -239,6 +239,13 @@
 }
 
 flag {
+  name: "use_frozen_aware_remote_callback_list"
+  namespace: "permissions"
+  description: "Whether to use the new frozen-aware RemoteCallbackList API for op noted callbacks."
+  bug: "361157077"
+}
+
+flag {
     name: "wallet_role_icon_property_enabled"
     is_exported: true
     namespace: "wallet_integration"
diff --git a/core/tests/coretests/AppThatUsesAppOps/src/android/app/appops/appthatusesappops/AppOpsUserService.kt b/core/tests/coretests/AppThatUsesAppOps/src/android/app/appops/appthatusesappops/AppOpsUserService.kt
index 48053c1..c5f07ff 100644
--- a/core/tests/coretests/AppThatUsesAppOps/src/android/app/appops/appthatusesappops/AppOpsUserService.kt
+++ b/core/tests/coretests/AppThatUsesAppOps/src/android/app/appops/appthatusesappops/AppOpsUserService.kt
@@ -22,7 +22,9 @@
 import android.app.Service
 import android.app.SyncNotedAppOp
 import android.content.Intent
+import android.os.Handler
 import android.os.IBinder
+import android.os.Looper
 import android.util.Log
 import com.android.frameworks.coretests.aidl.IAppOpsUserClient
 import com.android.frameworks.coretests.aidl.IAppOpsUserService
@@ -71,6 +73,7 @@
     override fun onBind(intent: Intent?): IBinder {
         return object : IAppOpsUserService.Stub() {
             private val appOpsManager = getSystemService(AppOpsManager::class.java)!!
+            private val handler = Handler(Looper.getMainLooper())
 
             // Collected note-op calls inside of this process
             private val noted = mutableListOf<Pair<SyncNotedAppOp, Array<StackTraceElement>>>()
@@ -182,6 +185,18 @@
                 }
             }
 
+            override fun callFreezeAndNoteSyncOp(client: IAppOpsUserClient) {
+                handler.post {
+                    client.freezeAndNoteSyncOp()
+                }
+            }
+
+            override fun assertEmptyAsyncNoted() {
+                forwardThrowableFrom {
+                    assertThat(asyncNoted).isEmpty()
+                }
+            }
+
             override fun callApiThatNotesAsyncOpNativelyAndCheckCustomMessage(
                 client: IAppOpsUserClient
             ) {
diff --git a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserClient.aidl b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserClient.aidl
index 68b393c0..11300b0 100644
--- a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserClient.aidl
+++ b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserClient.aidl
@@ -20,6 +20,7 @@
     void noteSyncOpNative();
     void noteNonPermissionSyncOpNative();
     oneway void noteSyncOpOnewayNative();
+    void freezeAndNoteSyncOp();
     void noteSyncOpOtherUidNative();
     void noteAsyncOpNative();
     void noteAsyncOpNativeWithCustomMessage();
diff --git a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserService.aidl b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserService.aidl
index f5673c4..c6dc7b0 100644
--- a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserService.aidl
+++ b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserService.aidl
@@ -25,4 +25,6 @@
     void callApiThatNotesSyncOpOtherUidNativelyAndCheckLog(in IAppOpsUserClient client);
     void callApiThatNotesAsyncOpNativelyAndCheckCustomMessage(in IAppOpsUserClient client);
     void callApiThatNotesAsyncOpNativelyAndCheckLog(in IAppOpsUserClient client);
+    void callFreezeAndNoteSyncOp(in IAppOpsUserClient client);
+    void assertEmptyAsyncNoted();
 }
diff --git a/core/tests/coretests/src/android/app/AppOpsLoggingTest.kt b/core/tests/coretests/src/android/app/AppOpsLoggingTest.kt
index a10d6a9..ff4e532 100644
--- a/core/tests/coretests/src/android/app/AppOpsLoggingTest.kt
+++ b/core/tests/coretests/src/android/app/AppOpsLoggingTest.kt
@@ -33,6 +33,9 @@
 import android.os.Looper
 import android.os.Process
 import android.platform.test.annotations.AppModeFull
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.frameworks.coretests.aidl.IAppOpsUserClient
@@ -41,9 +44,15 @@
 import org.junit.After
 import org.junit.Assert.fail
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import java.util.concurrent.CompletableFuture
+import java.util.concurrent.LinkedBlockingQueue
 import java.util.concurrent.TimeUnit.MILLISECONDS
+import java.util.concurrent.TimeUnit
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.TimeSource
 
 private const val LOG_TAG = "AppOpsLoggingTest"
 
@@ -71,8 +80,11 @@
 
     private var wasLocationEnabled = false
 
+    @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
     private lateinit var testService: IAppOpsUserService
     private lateinit var serviceConnection: ServiceConnection
+    private lateinit var freezingTestCompletion: CompletableFuture<Unit>
 
     // Collected note-op calls inside of this process
     private val noted = mutableListOf<Pair<SyncNotedAppOp, Array<StackTraceElement>>>()
@@ -123,6 +135,7 @@
 
         context.bindService(serviceIntent, serviceConnection, BIND_AUTO_CREATE)
         testService = newService.get(TIMEOUT_MILLIS, MILLISECONDS)
+        freezingTestCompletion = CompletableFuture<Unit>()
     }
 
     private fun clearCollectedNotedOps() {
@@ -253,6 +266,26 @@
         }
     }
 
+    @Test
+    @RequiresFlagsEnabled(android.os.Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK,
+            android.permission.flags.Flags.FLAG_USE_FROZEN_AWARE_REMOTE_CALLBACK_LIST)
+    fun dropAsyncOpNotedWhenFrozen() {
+        // Here's what the test does:
+        // 1. AppOpsLoggingTest calls AppOpsUserService
+        // 2. AppOpsUserService calls freezeAndNoteSyncOp in AppOpsLoggingTest
+        // 3. freezeAndNoteSyncOp freezes AppOpsUserService
+        // 4. freezeAndNoteSyncOp calls nativeNoteOp which leads to an async op noted callback
+        // 5. AppOpsService is expected to drop the callback (via RemoteCallbackList) since
+        //    AppOpsUserService is frozen
+        // 6. freezeAndNoteSyncOp unfreezes AppOpsUserService
+        // 7. AppOpsLoggingTest calls AppOpsUserService.assertEmptyAsyncNoted
+        rethrowThrowableFrom {
+            testService.callFreezeAndNoteSyncOp(AppOpsUserClient(context))
+            freezingTestCompletion.get()
+            testService.assertEmptyAsyncNoted()
+        }
+    }
+
     @After
     fun removeNotedAppOpsCollector() {
         appOpsManager.setOnOpNotedCallback(null, null)
@@ -263,6 +296,20 @@
         context.unbindService(serviceConnection)
     }
 
+    fun <T> waitForState(queue: LinkedBlockingQueue<T>, state: T, duration: Duration): T? {
+        val timeSource = TimeSource.Monotonic
+        val start = timeSource.markNow()
+        var remaining = duration
+        while (remaining.inWholeMilliseconds > 0) {
+            val v = queue.poll(remaining.inWholeMilliseconds, TimeUnit.MILLISECONDS)
+            if (v == state) {
+                return v
+            }
+            remaining -= timeSource.markNow() - start
+        }
+        return null
+    }
+
     private inner class AppOpsUserClient(
         context: Context
     ) : IAppOpsUserClient.Stub() {
@@ -285,6 +332,31 @@
             nativeNoteOp(strOpToOp(OPSTR_COARSE_LOCATION), Binder.getCallingUid(), TEST_SERVICE_PKG)
         }
 
+        override fun freezeAndNoteSyncOp() {
+            handler.post {
+                var stateChanges = LinkedBlockingQueue<Int>()
+                // Leave some time for any pending binder transactions to complete.
+                //
+                // TODO(327047060) Remove this sleep and instead make am freeze wait for binder
+                // transactions to complete
+                Thread.sleep(1000)
+                testService.asBinder().addFrozenStateChangeCallback {
+                    _, state -> stateChanges.put(state)
+                }
+                InstrumentationRegistry.getInstrumentation().uiAutomation
+                    .executeShellCommand("am freeze $TEST_SERVICE_PKG")
+                waitForState(stateChanges, IBinder.FrozenStateChangeCallback.STATE_FROZEN,
+                    1000.milliseconds)
+                nativeNoteOp(strOpToOp(OPSTR_COARSE_LOCATION), Binder.getCallingUid(),
+                    TEST_SERVICE_PKG)
+                InstrumentationRegistry.getInstrumentation().uiAutomation
+                    .executeShellCommand("am unfreeze $TEST_SERVICE_PKG")
+                waitForState(stateChanges, IBinder.FrozenStateChangeCallback.STATE_UNFROZEN,
+                    1000.milliseconds)
+                freezingTestCompletion.complete(Unit)
+            }
+        }
+
         override fun noteSyncOpOtherUidNative() {
             nativeNoteOp(strOpToOp(OPSTR_COARSE_LOCATION), myUid, myPackage)
         }
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index b4cce7d..03afaea 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -70,8 +70,10 @@
 import static android.content.Intent.EXTRA_REPLACING;
 import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
 import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
-import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;
+import static android.os.Flags.binderFrozenStateChangeCallback;
 import static android.permission.flags.Flags.checkOpValidatePackage;
+import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;
+import static android.permission.flags.Flags.useFrozenAwareRemoteCallbackList;
 
 import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED;
 import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION;
@@ -3543,20 +3545,23 @@
 
         synchronized (this) {
             RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key);
+            if (callbacks == null && binderFrozenStateChangeCallback()
+                    && useFrozenAwareRemoteCallbackList()) {
+                callbacks = new RemoteCallbackList.Builder<IAppOpsAsyncNotedCallback>(
+                        RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
+                        .setInterfaceDiedCallback((rcl, cb, cookie) ->
+                            stopWatchingAsyncNoted(packageName, callback)
+                        ).build();
+            }
             if (callbacks == null) {
                 callbacks = new RemoteCallbackList<IAppOpsAsyncNotedCallback>() {
-                    @Override
-                    public void onCallbackDied(IAppOpsAsyncNotedCallback callback) {
-                        synchronized (AppOpsService.this) {
-                            if (getRegisteredCallbackCount() == 0) {
-                                mAsyncOpWatchers.remove(key);
-                            }
+                        @Override
+                        public void onCallbackDied(IAppOpsAsyncNotedCallback cb) {
+                            stopWatchingAsyncNoted(packageName, callback);
                         }
-                    }
-                };
-                mAsyncOpWatchers.put(key, callbacks);
+                    };
             }
-
+            mAsyncOpWatchers.put(key, callbacks);
             callbacks.register(callback);
         }
     }