Calling opChanged on package suspend / unsuspend

checkAudioOperation returned MODE_IGNORED when a package was suspended,
but any AppOpsWatcher registered for audio operation did not callback
when a package got suspended. This lead to inconsistent state for
services that were watching for app op changes.

Test: atest FrameworksServicesTests:SuspendPackagesTest

Bug: 112486945
Bug: 110077884
Change-Id: Ibfc378dd4ea8dd38ef002c1ac668c479afa8fd47
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 67b86c0..b17fa2a 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -5808,16 +5808,16 @@
      * {@code android.permission.SUSPEND_APPS} can put any app on the device into a suspended state.
      *
      * <p>While in this state, the application's notifications will be hidden, any of its started
-     * activities will be stopped and it will not be able to show toasts or dialogs or ring the
-     * device. When the user tries to launch a suspended app, the system will, instead, show a
+     * activities will be stopped and it will not be able to show toasts or dialogs or play audio.
+     * When the user tries to launch a suspended app, the system will, instead, show a
      * dialog to the user informing them that they cannot use this app while it is suspended.
      *
      * <p>When an app is put into this state, the broadcast action
      * {@link Intent#ACTION_MY_PACKAGE_SUSPENDED} will be delivered to any of its broadcast
      * receivers that included this action in their intent-filters, <em>including manifest
      * receivers.</em> Similarly, a broadcast action {@link Intent#ACTION_MY_PACKAGE_UNSUSPENDED}
-     * is delivered when a previously suspended app is taken out of this state.
-     * </p>
+     * is delivered when a previously suspended app is taken out of this state. Apps are expected to
+     * use these to gracefully deal with transitions to and from this state.
      *
      * @return {@code true} if the calling package has been suspended, {@code false} otherwise.
      *
diff --git a/services/core/java/com/android/server/AppOpsService.java b/services/core/java/com/android/server/AppOpsService.java
index cd98263..dedb237 100644
--- a/services/core/java/com/android/server/AppOpsService.java
+++ b/services/core/java/com/android/server/AppOpsService.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import static android.app.AppOpsManager.OP_PLAY_AUDIO;
 import static android.app.AppOpsManager.UID_STATE_BACKGROUND;
 import static android.app.AppOpsManager.UID_STATE_CACHED;
 import static android.app.AppOpsManager.UID_STATE_FOREGROUND;
@@ -36,8 +37,11 @@
 import android.app.AppOpsManager.HistoricalPackageOps;
 import android.app.AppOpsManagerInternal;
 import android.app.AppOpsManagerInternal.CheckOpsDelegate;
+import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
@@ -645,6 +649,26 @@
             }
         }
 
+        final IntentFilter packageSuspendFilter = new IntentFilter();
+        packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
+        packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final int[] changedUids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST);
+                final String[] changedPkgs = intent.getStringArrayExtra(
+                        Intent.EXTRA_CHANGED_PACKAGE_LIST);
+                final ArraySet<ModeCallback> callbacks = mOpModeWatchers.get(OP_PLAY_AUDIO);
+                for (int i = 0; i < changedUids.length; i++) {
+                    final int changedUid = changedUids[i];
+                    final String changedPkg = changedPkgs[i];
+                    // We trust packagemanager to insert matching uid and packageNames in the extras
+                    mHandler.sendMessage(PooledLambda.obtainMessage(AppOpsService::notifyOpChanged,
+                            AppOpsService.this, callbacks, OP_PLAY_AUDIO, changedUid, changedPkg));
+                }
+            }
+        }, packageSuspendFilter);
+
         PackageManagerInternal packageManagerInternal = LocalServices.getService(
                 PackageManagerInternal.class);
         packageManagerInternal.setExternalSourcesPolicy(
diff --git a/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java b/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java
index 553d234a..c6be1c0 100644
--- a/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java
@@ -16,6 +16,10 @@
 
 package com.android.server.pm;
 
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OP_PLAY_AUDIO;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -35,12 +39,14 @@
 import android.content.pm.PackageManager;
 import android.content.pm.SuspendDialogInfo;
 import android.content.res.Resources;
+import android.media.AudioAttributes;
 import android.os.BaseBundle;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
@@ -54,6 +60,8 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsService;
 import com.android.servicestests.apps.suspendtestapp.SuspendTestActivity;
 import com.android.servicestests.apps.suspendtestapp.SuspendTestReceiver;
 
@@ -550,6 +558,32 @@
         assertEquals(ACTION_REPORT_MY_PACKAGE_UNSUSPENDED, intentFromApp.getAction());
     }
 
+    @Test
+    public void testAudioOpBlockedOnSuspend() throws Exception {
+        final IAppOpsService iAppOps = IAppOpsService.Stub.asInterface(
+                ServiceManager.getService(Context.APP_OPS_SERVICE));
+        final CountDownLatch latch = new CountDownLatch(1);
+        final IAppOpsCallback watcher = new IAppOpsCallback.Stub() {
+            @Override
+            public void opChanged(int op, int uid, String packageName) {
+                if (op == OP_PLAY_AUDIO && packageName.equals(TEST_APP_PACKAGE_NAME)) {
+                    latch.countDown();
+                }
+            }
+        };
+        iAppOps.startWatchingMode(OP_PLAY_AUDIO, TEST_APP_PACKAGE_NAME, watcher);
+        final int testPackageUid = mPackageManager.getPackageUid(TEST_APP_PACKAGE_NAME, 0);
+        int audioOpMode = iAppOps.checkAudioOperation(OP_PLAY_AUDIO,
+                AudioAttributes.USAGE_UNKNOWN, testPackageUid, TEST_APP_PACKAGE_NAME);
+        assertEquals("Audio muted for unsuspended package", MODE_ALLOWED, audioOpMode);
+        suspendTestPackage(null, null, null);
+        assertTrue("AppOpsWatcher did not callback", latch.await(5, TimeUnit.SECONDS));
+        audioOpMode = iAppOps.checkAudioOperation(OP_PLAY_AUDIO,
+                AudioAttributes.USAGE_UNKNOWN, testPackageUid, TEST_APP_PACKAGE_NAME);
+        assertEquals("Audio not muted for suspended package", MODE_IGNORED, audioOpMode);
+        iAppOps.stopWatchingMode(watcher);
+    }
+
     @After
     public void tearDown() throws IOException {
         mAppCommsReceiver.unregister();