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();