diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java index dade7c3d84a8d..53e81c789d2bd 100644 --- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java +++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java @@ -27,7 +27,6 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.compat.annotation.ChangeId; -import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; @@ -285,11 +284,10 @@ public class AlarmManager { * The permission {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will be denied, unless the * user explicitly allows it from Settings. * - * TODO (b/226439802): Either enable it in the next SDK or replace it with a better alternative. * @hide */ @ChangeId - @Disabled + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) public static final long SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = 226439802L; @UnsupportedAppUsage diff --git a/services/core/java/com/android/server/appop/AppOpsServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsServiceImpl.java index 70f3bcc64ef0c..ddcccb527ff41 100644 --- a/services/core/java/com/android/server/appop/AppOpsServiceImpl.java +++ b/services/core/java/com/android/server/appop/AppOpsServiceImpl.java @@ -44,6 +44,7 @@ import static android.app.AppOpsManager.OP_PLAY_AUDIO; import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD; +import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM; import static android.app.AppOpsManager.OP_VIBRATE; import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED; import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED; @@ -130,6 +131,8 @@ import com.android.modules.utils.TypedXmlSerializer; import com.android.server.LocalServices; import com.android.server.LockGuard; import com.android.server.SystemServerInitThreadPool; +import com.android.server.pm.UserManagerInternal; +import com.android.server.pm.permission.PermissionManagerServiceInternal; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.component.ParsedAttribution; @@ -163,12 +166,30 @@ class AppOpsServiceImpl implements AppOpsServiceInterface { static final String TAG = "AppOps"; static final boolean DEBUG = false; + /** + * Sentinel integer version to denote that there was no appops.xml found on boot. + * This will happen when a device boots with no existing userdata. + */ + private static final int NO_FILE_VERSION = -2; + + /** + * Sentinel integer version to denote that there was no version in the appops.xml found on boot. + * This means the file is coming from a build before versioning was added. + */ private static final int NO_VERSION = -1; + /** * Increment by one every time and add the corresponding upgrade logic in - * {@link #upgradeLocked(int)} below. The first version was 1 + * {@link #upgradeLocked(int)} below. The first version was 1. */ - private static final int CURRENT_VERSION = 1; + @VisibleForTesting + static final int CURRENT_VERSION = 2; + + /** + * This stores the version of appops.xml seen at boot. If this is smaller than + * {@link #CURRENT_VERSION}, then we will run {@link #upgradeLocked(int)} on startup. + */ + private int mVersionAtBoot = NO_FILE_VERSION; // Write at most every 30 minutes. static final long WRITE_DELAY = DEBUG ? 1000 : 30 * 60 * 1000; @@ -936,6 +957,10 @@ class AppOpsServiceImpl implements AppOpsServiceInterface { @Override public void systemReady() { + synchronized (this) { + upgradeLocked(mVersionAtBoot); + } + mConstants.startMonitoring(mContext.getContentResolver()); mHistoricalRegistry.systemReady(mContext.getContentResolver()); @@ -3191,7 +3216,6 @@ class AppOpsServiceImpl implements AppOpsServiceInterface { @Override public void readState() { - int oldVersion = NO_VERSION; synchronized (mFile) { synchronized (this) { FileInputStream stream; @@ -3216,7 +3240,7 @@ class AppOpsServiceImpl implements AppOpsServiceInterface { throw new IllegalStateException("no start tag found"); } - oldVersion = parser.getAttributeInt(null, "v", NO_VERSION); + mVersionAtBoot = parser.getAttributeInt(null, "v", NO_VERSION); int outerDepth = parser.getDepth(); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT @@ -3261,12 +3285,11 @@ class AppOpsServiceImpl implements AppOpsServiceInterface { } } } - synchronized (this) { - upgradeLocked(oldVersion); - } } - private void upgradeRunAnyInBackgroundLocked() { + @VisibleForTesting + @GuardedBy("this") + void upgradeRunAnyInBackgroundLocked() { for (int i = 0; i < mUidStates.size(); i++) { final UidState uidState = mUidStates.valueAt(i); if (uidState == null) { @@ -3303,8 +3326,45 @@ class AppOpsServiceImpl implements AppOpsServiceInterface { } } + /** + * The interpretation of the default mode - MODE_DEFAULT - for OP_SCHEDULE_EXACT_ALARM is + * changing. Simultaneously, we want to change this op's mode from MODE_DEFAULT to MODE_ALLOWED + * for already installed apps. For newer apps, it will stay as MODE_DEFAULT. + */ + @VisibleForTesting + @GuardedBy("this") + void upgradeScheduleExactAlarmLocked() { + final PermissionManagerServiceInternal pmsi = LocalServices.getService( + PermissionManagerServiceInternal.class); + final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class); + final PackageManagerInternal pmi = getPackageManagerInternal(); + + final String[] packagesDeclaringPermission = pmsi.getAppOpPermissionPackages( + AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM)); + final int[] userIds = umi.getUserIds(); + + for (final String pkg : packagesDeclaringPermission) { + for (int userId : userIds) { + final int uid = pmi.getPackageUid(pkg, 0, userId); + + UidState uidState = mUidStates.get(uid); + if (uidState == null) { + uidState = new UidState(uid); + mUidStates.put(uid, uidState); + } + final int oldMode = uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM); + if (oldMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) { + uidState.setUidMode(OP_SCHEDULE_EXACT_ALARM, MODE_ALLOWED); + } + } + // This appop is meant to be controlled at a uid level. So we leave package modes as + // they are. + } + } + + @GuardedBy("this") private void upgradeLocked(int oldVersion) { - if (oldVersion >= CURRENT_VERSION) { + if (oldVersion == NO_FILE_VERSION || oldVersion >= CURRENT_VERSION) { return; } Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION); @@ -3313,6 +3373,9 @@ class AppOpsServiceImpl implements AppOpsServiceInterface { upgradeRunAnyInBackgroundLocked(); // fall through case 1: + upgradeScheduleExactAlarmLocked(); + // fall through + case 2: // for future upgrades } scheduleFastWriteLocked(); diff --git a/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-1.xml b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-1.xml new file mode 100644 index 0000000000000..1dde7dcc47208 --- /dev/null +++ b/services/tests/mockingservicestests/assets/AppOpsUpgradeTest/appops-version-1.xml @@ -0,0 +1,781 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java index 298dbf47d86d3..302fa0f0c5284 100644 --- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java @@ -16,23 +16,32 @@ package com.android.server.appop; +import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; import android.app.AppOpsManager; import android.content.Context; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.content.res.AssetManager; import android.os.Handler; -import android.os.HandlerThread; +import android.os.UserHandle; import android.util.Log; import android.util.SparseArray; import android.util.SparseIntArray; @@ -42,11 +51,20 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.util.ArrayUtils; import com.android.modules.utils.TypedXmlPullParser; +import com.android.server.LocalServices; +import com.android.server.SystemServerInitThreadPool; +import com.android.server.pm.UserManagerInternal; +import com.android.server.pm.permission.PermissionManagerServiceInternal; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; import org.xmlpull.v1.XmlPullParser; import java.io.File; @@ -64,29 +82,39 @@ public class AppOpsUpgradeTest { private static final String TAG = AppOpsUpgradeTest.class.getSimpleName(); private static final String APP_OPS_UNVERSIONED_ASSET_PATH = "AppOpsUpgradeTest/appops-unversioned.xml"; + private static final String APP_OPS_VERSION_1_ASSET_PATH = + "AppOpsUpgradeTest/appops-version-1.xml"; private static final String APP_OPS_FILENAME = "appops-test.xml"; private static final int NON_DEFAULT_OPS_IN_FILE = 4; - private static final int CURRENT_VERSION = 1; - private File mAppOpsFile; - private Context mContext; + private static final Context sContext = InstrumentationRegistry.getTargetContext(); + private static final File sAppOpsFile = new File(sContext.getFilesDir(), APP_OPS_FILENAME); + + private Context mTestContext; + private MockitoSession mMockitoSession; + + @Mock + private PackageManagerInternal mPackageManagerInternal; + @Mock + private PackageManager mPackageManager; + @Mock + private UserManagerInternal mUserManagerInternal; + @Mock + private PermissionManagerServiceInternal mPermissionManagerInternal; + @Mock private Handler mHandler; - private void extractAppOpsFile() { - mAppOpsFile.getParentFile().mkdirs(); - if (mAppOpsFile.exists()) { - mAppOpsFile.delete(); - } - try (FileOutputStream out = new FileOutputStream(mAppOpsFile); - InputStream in = mContext.getAssets().open(APP_OPS_UNVERSIONED_ASSET_PATH, - AssetManager.ACCESS_BUFFER)) { + private static void extractAppOpsFile(String assetPath) { + sAppOpsFile.getParentFile().mkdirs(); + try (FileOutputStream out = new FileOutputStream(sAppOpsFile); + InputStream in = sContext.getAssets().open(assetPath, AssetManager.ACCESS_BUFFER)) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = in.read(buffer)) >= 0) { out.write(buffer, 0, bytesRead); } out.flush(); - Log.d(TAG, "Successfully copied xml to " + mAppOpsFile.getAbsolutePath()); + Log.d(TAG, "Successfully copied xml to " + sAppOpsFile.getAbsolutePath()); } catch (IOException exc) { Log.e(TAG, "Exception while copying appops xml", exc); fail(); @@ -98,7 +126,7 @@ public class AppOpsUpgradeTest { int numberOfNonDefaultOps = 0; final int defaultModeOp1 = AppOpsManager.opToDefaultMode(op1); final int defaultModeOp2 = AppOpsManager.opToDefaultMode(op2); - for(int i = 0; i < uidStates.size(); i++) { + for (int i = 0; i < uidStates.size(); i++) { final AppOpsServiceImpl.UidState uidState = uidStates.valueAt(i); SparseIntArray opModes = uidState.getNonDefaultUidModes(); if (opModes != null) { @@ -132,41 +160,191 @@ public class AppOpsUpgradeTest { @Before public void setUp() { - mContext = InstrumentationRegistry.getTargetContext(); - mAppOpsFile = new File(mContext.getFilesDir(), APP_OPS_FILENAME); - extractAppOpsFile(); - HandlerThread handlerThread = new HandlerThread(TAG); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper()); + if (sAppOpsFile.exists()) { + sAppOpsFile.delete(); + } + + mMockitoSession = mockitoSession() + .initMocks(this) + .spyStatic(LocalServices.class) + .mockStatic(SystemServerInitThreadPool.class) + .strictness(Strictness.LENIENT) + .startMocking(); + + doReturn(mPermissionManagerInternal).when( + () -> LocalServices.getService(PermissionManagerServiceInternal.class)); + doReturn(mUserManagerInternal).when( + () -> LocalServices.getService(UserManagerInternal.class)); + doReturn(mPackageManagerInternal).when( + () -> LocalServices.getService(PackageManagerInternal.class)); + + mTestContext = spy(sContext); + + // Pretend everybody has all permissions + doNothing().when(mTestContext).enforcePermission(anyString(), anyInt(), anyInt(), + nullable(String.class)); + + doReturn(mPackageManager).when(mTestContext).getPackageManager(); + + // Stub out package calls to disable AppOpsService#updatePermissionRevokedCompat + doReturn(null).when(mPackageManager).getPackagesForUid(anyInt()); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); } @Test - public void testUpgradeFromNoVersion() throws Exception { - AppOpsDataParser parser = new AppOpsDataParser(mAppOpsFile); + public void upgradeRunAnyInBackground() { + extractAppOpsFile(APP_OPS_UNVERSIONED_ASSET_PATH); + + AppOpsServiceImpl testService = new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext); + + testService.upgradeRunAnyInBackgroundLocked(); + assertSameModes(testService.mUidStates, AppOpsManager.OP_RUN_IN_BACKGROUND, + AppOpsManager.OP_RUN_ANY_IN_BACKGROUND); + } + + private static int getModeInFile(int uid) { + switch (uid) { + case 10198: + return 0; + case 10200: + return 1; + case 1110200: + case 10267: + case 1110181: + return 2; + default: + return AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM); + } + } + + @Test + public void upgradeScheduleExactAlarm() { + extractAppOpsFile(APP_OPS_VERSION_1_ASSET_PATH); + + String[] packageNames = {"p1", "package2", "pkg3", "package.4", "pkg-5", "pkg.6"}; + int[] appIds = {10267, 10181, 10198, 10199, 10200, 4213}; + int[] userIds = {0, 10, 11}; + + doReturn(userIds).when(mUserManagerInternal).getUserIds(); + + doReturn(packageNames).when(mPermissionManagerInternal).getAppOpPermissionPackages( + AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM)); + + doAnswer(invocation -> { + String pkg = invocation.getArgument(0); + int index = ArrayUtils.indexOf(packageNames, pkg); + if (index < 0) { + return index; + } + int userId = invocation.getArgument(2); + return UserHandle.getUid(userId, appIds[index]); + }).when(mPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt()); + + AppOpsServiceImpl testService = new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext); + + testService.upgradeScheduleExactAlarmLocked(); + + for (int userId : userIds) { + for (int appId : appIds) { + final int uid = UserHandle.getUid(userId, appId); + final int previousMode = getModeInFile(uid); + + final int expectedMode; + if (previousMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) { + expectedMode = AppOpsManager.MODE_ALLOWED; + } else { + expectedMode = previousMode; + } + final AppOpsServiceImpl.UidState uidState = testService.mUidStates.get(uid); + assertEquals(expectedMode, uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM)); + } + } + + // These uids don't even declare the permission. So should stay as default / empty. + int[] unrelatedUidsInFile = {10225, 10178}; + + for (int uid : unrelatedUidsInFile) { + final AppOpsServiceImpl.UidState uidState = testService.mUidStates.get(uid); + assertEquals(AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM), + uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM)); + } + } + + @Test + public void upgradeFromNoFile() { + assertFalse(sAppOpsFile.exists()); + + AppOpsServiceImpl testService = spy( + new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext)); + + doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); + doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + + // trigger upgrade + testService.systemReady(); + + verify(testService, never()).upgradeRunAnyInBackgroundLocked(); + verify(testService, never()).upgradeScheduleExactAlarmLocked(); + + testService.writeState(); + + assertTrue(sAppOpsFile.exists()); + + AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile); + assertTrue(parser.parse()); + assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion); + } + + @Test + public void upgradeFromNoVersion() { + extractAppOpsFile(APP_OPS_UNVERSIONED_ASSET_PATH); + AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile); assertTrue(parser.parse()); assertEquals(AppOpsDataParser.NO_VERSION, parser.mVersion); - // Use mock context and package manager to fake permision package manager calls. - Context testContext = spy(mContext); - - // Pretent everybody has all permissions - doNothing().when(testContext).enforcePermission(anyString(), anyInt(), anyInt(), - nullable(String.class)); - - PackageManager testPM = mock(PackageManager.class); - when(testContext.getPackageManager()).thenReturn(testPM); - - // Stub out package calls to disable AppOpsService#updatePermissionRevokedCompat - when(testPM.getPackagesForUid(anyInt())).thenReturn(null); - AppOpsServiceImpl testService = spy( - new AppOpsServiceImpl(mAppOpsFile, mHandler, testContext)); // trigger upgrade - assertSameModes(testService.mUidStates, AppOpsManager.OP_RUN_IN_BACKGROUND, - AppOpsManager.OP_RUN_ANY_IN_BACKGROUND); - mHandler.removeCallbacks(testService.mWriteRunner); + new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext)); + + doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); + doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + + // trigger upgrade + testService.systemReady(); + + verify(testService).upgradeRunAnyInBackgroundLocked(); + verify(testService).upgradeScheduleExactAlarmLocked(); + testService.writeState(); assertTrue(parser.parse()); - assertEquals(CURRENT_VERSION, parser.mVersion); + assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion); + } + + @Test + public void upgradeFromVersion1() { + extractAppOpsFile(APP_OPS_VERSION_1_ASSET_PATH); + AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile); + assertTrue(parser.parse()); + assertEquals(1, parser.mVersion); + + AppOpsServiceImpl testService = spy( + new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext)); + + doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); + doNothing().when(testService).upgradeScheduleExactAlarmLocked(); + + // trigger upgrade + testService.systemReady(); + + verify(testService, never()).upgradeRunAnyInBackgroundLocked(); + verify(testService).upgradeScheduleExactAlarmLocked(); + + testService.writeState(); + assertTrue(parser.parse()); + assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion); } /** @@ -174,7 +352,7 @@ public class AppOpsUpgradeTest { * Other fields may be added as and when required for testing. */ private static final class AppOpsDataParser { - static final int NO_VERSION = -1; + static final int NO_VERSION = -123; int mVersion; private File mFile;