From 95cd952b470e2369749ab9cc98c85714157344cd Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Fri, 8 May 2020 09:56:53 -0700 Subject: [PATCH] Partially exempt headless system apps from app standby. The user can't interact with headless system apps (pre-installed apps without any activities) but they're expected to work properly. We don't want to fully exempt the apps from app standby, but they should be fine operating in the ACTIVE bucket, so we make sure that headless system apps never fall below the ACTIVE bucket. Bug: 155761007 Test: atest FrameworksServicesTests:AppIdleHistoryTests Test: atest FrameworksServicesTests:AppStandbyControllerTests Change-Id: I1549bb81eca293be31691b079bab2142cbcdf8a7 --- .../android/server/usage/AppIdleHistory.java | 10 +- .../server/usage/AppStandbyController.java | 121 ++++++++++++++---- .../usage/AppStandbyControllerTests.java | 72 +++++++++-- 3 files changed, 168 insertions(+), 35 deletions(-) diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java index 46d449a9257c3..372ec981df02a 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java @@ -79,6 +79,12 @@ public class AppIdleHistory { private static final int STANDBY_BUCKET_UNKNOWN = -1; + /** + * The bucket beyond which apps are considered idle. Any apps in this bucket or lower are + * considered idle while those in higher buckets are not considered idle. + */ + static final int IDLE_BUCKET_CUTOFF = STANDBY_BUCKET_RARE; + @VisibleForTesting static final String APP_IDLE_FILENAME = "app_idle_stats.xml"; private static final String TAG_PACKAGES = "packages"; @@ -350,7 +356,7 @@ public class AppIdleHistory { ArrayMap userHistory = getUserHistory(userId); AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, elapsedRealtime, true); - return appUsageHistory.currentBucket >= STANDBY_BUCKET_RARE; + return appUsageHistory.currentBucket >= IDLE_BUCKET_CUTOFF; } public AppUsageHistory getAppUsageHistory(String packageName, int userId, @@ -487,7 +493,7 @@ public class AppIdleHistory { final int newBucket; final int reason; if (idle) { - newBucket = STANDBY_BUCKET_RARE; + newBucket = IDLE_BUCKET_CUTOFF; reason = REASON_MAIN_FORCED_BY_USER; } else { newBucket = STANDBY_BUCKET_ACTIVE; diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index 980372d58f336..2834ab14f28d6 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -54,6 +54,7 @@ import static com.android.server.SystemService.PHASE_BOOT_COMPLETED; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppGlobals; @@ -92,6 +93,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings.Global; import android.telephony.TelephonyManager; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.KeyValueListParser; import android.util.Slog; @@ -227,6 +229,13 @@ public class AppStandbyController implements AppStandbyInternal { @GuardedBy("mActiveAdminApps") private final SparseArray> mActiveAdminApps = new SparseArray<>(); + /** + * Set of system apps that are headless (don't have any declared activities, enabled or + * disabled). Presence in this map indicates that the app is a headless system app. + */ + @GuardedBy("mAppIdleLock") + private final ArrayMap mHeadlessSystemApps = new ArrayMap<>(); + private final CountDownLatch mAdminDataAvailableLatch = new CountDownLatch(1); // Messages for the handler @@ -667,20 +676,22 @@ public class AppStandbyController implements AppStandbyInternal { return; } } - final boolean isSpecial = isAppSpecial(packageName, + final int minBucket = getAppMinBucket(packageName, UserHandle.getAppId(uid), userId); if (DEBUG) { - Slog.d(TAG, " Checking idle state for " + packageName + " special=" + - isSpecial); + Slog.d(TAG, " Checking idle state for " + packageName + + " minBucket=" + minBucket); } - if (isSpecial) { + if (minBucket <= STANDBY_BUCKET_ACTIVE) { + // No extra processing needed for ACTIVE or higher since apps can't drop into lower + // buckets. synchronized (mAppIdleLock) { mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, - STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT); + minBucket, REASON_MAIN_DEFAULT); } maybeInformListeners(packageName, userId, elapsedRealtime, - STANDBY_BUCKET_EXEMPTED, REASON_MAIN_DEFAULT, false); + minBucket, REASON_MAIN_DEFAULT, false); } else { synchronized (mAppIdleLock) { final AppIdleHistory.AppUsageHistory app = @@ -761,6 +772,14 @@ public class AppStandbyController implements AppStandbyInternal { Slog.d(TAG, "Bringing up from RESTRICTED to RARE due to off switch"); } } + if (newBucket > minBucket) { + newBucket = minBucket; + // Leave the reason alone. + if (DEBUG) { + Slog.d(TAG, "Bringing up from " + newBucket + " to " + minBucket + + " due to min bucketing"); + } + } if (DEBUG) { Slog.d(TAG, " Old bucket=" + oldBucket + ", newBucket=" + newBucket); @@ -1027,20 +1046,35 @@ public class AppStandbyController implements AppStandbyInternal { return isAppIdleFiltered(packageName, getAppId(packageName), userId, elapsedRealtime); } - private boolean isAppSpecial(String packageName, int appId, int userId) { - if (packageName == null) return false; + @StandbyBuckets + private int getAppMinBucket(String packageName, int userId) { + try { + final int uid = mPackageManager.getPackageUidAsUser(packageName, userId); + return getAppMinBucket(packageName, UserHandle.getAppId(uid), userId); + } catch (PackageManager.NameNotFoundException e) { + // Not a valid package for this user, nothing to do + return STANDBY_BUCKET_NEVER; + } + } + + /** + * Return the lowest bucket this app should ever enter. + */ + @StandbyBuckets + private int getAppMinBucket(String packageName, int appId, int userId) { + if (packageName == null) return STANDBY_BUCKET_NEVER; // If not enabled at all, of course nobody is ever idle. if (!mAppIdleEnabled) { - return true; + return STANDBY_BUCKET_EXEMPTED; } if (appId < Process.FIRST_APPLICATION_UID) { // System uids never go idle. - return true; + return STANDBY_BUCKET_EXEMPTED; } if (packageName.equals("android")) { // Nor does the framework (which should be redundant with the above, but for MR1 we will // retain this for safety). - return true; + return STANDBY_BUCKET_EXEMPTED; } if (mSystemServicesReady) { try { @@ -1048,42 +1082,51 @@ public class AppStandbyController implements AppStandbyInternal { // for idle mode, because app idle (aka app standby) is really not as big an issue // for controlling who participates vs. doze mode. if (mInjector.isNonIdleWhitelisted(packageName)) { - return true; + return STANDBY_BUCKET_EXEMPTED; } } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } if (isActiveDeviceAdmin(packageName, userId)) { - return true; + return STANDBY_BUCKET_EXEMPTED; } if (isActiveNetworkScorer(packageName)) { - return true; + return STANDBY_BUCKET_EXEMPTED; } if (mAppWidgetManager != null && mInjector.isBoundWidgetPackage(mAppWidgetManager, packageName, userId)) { - return true; + // TODO: consider lowering to ACTIVE + return STANDBY_BUCKET_EXEMPTED; } if (isDeviceProvisioningPackage(packageName)) { - return true; + return STANDBY_BUCKET_EXEMPTED; } } // Check this last, as it can be the most expensive check if (isCarrierApp(packageName)) { - return true; + return STANDBY_BUCKET_EXEMPTED; } - return false; + if (isHeadlessSystemApp(packageName)) { + return STANDBY_BUCKET_ACTIVE; + } + + return STANDBY_BUCKET_NEVER; + } + + private boolean isHeadlessSystemApp(String packageName) { + return mHeadlessSystemApps.containsKey(packageName); } @Override public boolean isAppIdleFiltered(String packageName, int appId, int userId, long elapsedRealtime) { - if (isAppSpecial(packageName, appId, userId)) { + if (getAppMinBucket(packageName, appId, userId) < AppIdleHistory.IDLE_BUCKET_CUTOFF) { return false; } else { synchronized (mAppIdleLock) { @@ -1423,6 +1466,8 @@ public class AppStandbyController implements AppStandbyInternal { } } + // Make sure we don't put the app in a lower bucket than it's supposed to be in. + newBucket = Math.min(newBucket, getAppMinBucket(packageName, userId)); mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, reason, resetTimeout); } @@ -1617,14 +1662,16 @@ public class AppStandbyController implements AppStandbyInternal { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); + final String pkgName = intent.getData().getSchemeSpecificPart(); + final int userId = getSendingUserId(); if (Intent.ACTION_PACKAGE_ADDED.equals(action) || Intent.ACTION_PACKAGE_CHANGED.equals(action)) { clearCarrierPrivilegedApps(); + // ACTION_PACKAGE_ADDED is called even for system app downgrades. + evaluateSystemAppException(pkgName, userId); } if ((Intent.ACTION_PACKAGE_REMOVED.equals(action) || Intent.ACTION_PACKAGE_ADDED.equals(action))) { - final String pkgName = intent.getData().getSchemeSpecificPart(); - final int userId = getSendingUserId(); if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { maybeUnrestrictBuggyApp(pkgName, userId); } else { @@ -1634,6 +1681,34 @@ public class AppStandbyController implements AppStandbyInternal { } } + private void evaluateSystemAppException(String packageName, int userId) { + if (!mSystemServicesReady) { + // The app will be evaluated in initializeDefaultsForSystemApps() when possible. + return; + } + try { + PackageInfo pi = mPackageManager.getPackageInfoAsUser(packageName, + PackageManager.GET_ACTIVITIES | PackageManager.MATCH_DISABLED_COMPONENTS, + userId); + evaluateSystemAppException(pi); + } catch (PackageManager.NameNotFoundException e) { + mHeadlessSystemApps.remove(packageName); + } + } + + private void evaluateSystemAppException(@Nullable PackageInfo pkgInfo) { + if (pkgInfo.applicationInfo != null && pkgInfo.applicationInfo.isSystemApp()) { + synchronized (mAppIdleLock) { + if (pkgInfo.activities == null || pkgInfo.activities.length == 0) { + // Headless system app. + mHeadlessSystemApps.put(pkgInfo.packageName, true); + } else { + mHeadlessSystemApps.remove(pkgInfo.packageName); + } + } + } + } + @Override public void initializeDefaultsForSystemApps(int userId) { if (!mSystemServicesReady) { @@ -1645,7 +1720,7 @@ public class AppStandbyController implements AppStandbyInternal { + "appIdleEnabled=" + mAppIdleEnabled); final long elapsedRealtime = mInjector.elapsedRealtime(); List packages = mPackageManager.getInstalledPackagesAsUser( - PackageManager.MATCH_DISABLED_COMPONENTS, + PackageManager.GET_ACTIVITIES | PackageManager.MATCH_DISABLED_COMPONENTS, userId); final int packageCount = packages.size(); synchronized (mAppIdleLock) { @@ -1658,6 +1733,8 @@ public class AppStandbyController implements AppStandbyInternal { mAppIdleHistory.reportUsage(packageName, userId, STANDBY_BUCKET_ACTIVE, REASON_SUB_USAGE_SYSTEM_UPDATE, 0, elapsedRealtime + mSystemUpdateUsageTimeoutMillis); + + evaluateSystemAppException(pi); } } // Immediately persist defaults to disk diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java index 48e22f6c685c7..e4102205ddbbd 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java @@ -63,6 +63,7 @@ import android.app.usage.UsageEvents; import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.ContextWrapper; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -107,6 +108,10 @@ public class AppStandbyControllerTests { private static final int UID_1 = 10000; private static final String PACKAGE_EXEMPTED_1 = "com.android.exempted"; private static final int UID_EXEMPTED_1 = 10001; + private static final String PACKAGE_SYSTEM_HEADFULL = "com.example.system.headfull"; + private static final int UID_SYSTEM_HEADFULL = 10002; + private static final String PACKAGE_SYSTEM_HEADLESS = "com.example.system.headless"; + private static final int UID_SYSTEM_HEADLESS = 10003; private static final int USER_ID = 0; private static final int USER_ID2 = 10; private static final UserHandle USER_HANDLE_USER2 = new UserHandle(USER_ID2); @@ -305,18 +310,33 @@ public class AppStandbyControllerTests { pie.packageName = PACKAGE_EXEMPTED_1; packages.add(pie); + PackageInfo pis = new PackageInfo(); + pis.activities = new ActivityInfo[]{mock(ActivityInfo.class)}; + pis.applicationInfo = new ApplicationInfo(); + pis.applicationInfo.uid = UID_SYSTEM_HEADFULL; + pis.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; + pis.packageName = PACKAGE_SYSTEM_HEADFULL; + packages.add(pis); + + PackageInfo pish = new PackageInfo(); + pish.applicationInfo = new ApplicationInfo(); + pish.applicationInfo.uid = UID_SYSTEM_HEADLESS; + pish.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; + pish.packageName = PACKAGE_SYSTEM_HEADLESS; + packages.add(pish); + doReturn(packages).when(mockPm).getInstalledPackagesAsUser(anyInt(), anyInt()); try { - doReturn(UID_1).when(mockPm).getPackageUidAsUser(eq(PACKAGE_1), anyInt()); - doReturn(UID_1).when(mockPm).getPackageUidAsUser(eq(PACKAGE_1), anyInt(), anyInt()); - doReturn(UID_EXEMPTED_1).when(mockPm).getPackageUidAsUser(eq(PACKAGE_EXEMPTED_1), - anyInt()); - doReturn(UID_EXEMPTED_1).when(mockPm).getPackageUidAsUser(eq(PACKAGE_EXEMPTED_1), - anyInt(), anyInt()); - doReturn(pi.applicationInfo).when(mockPm).getApplicationInfo(eq(pi.packageName), - anyInt()); - doReturn(pie.applicationInfo).when(mockPm).getApplicationInfo(eq(pie.packageName), - anyInt()); + for (int i = 0; i < packages.size(); ++i) { + PackageInfo pkg = packages.get(i); + + doReturn(pkg.applicationInfo.uid).when(mockPm) + .getPackageUidAsUser(eq(pkg.packageName), anyInt()); + doReturn(pkg.applicationInfo.uid).when(mockPm) + .getPackageUidAsUser(eq(pkg.packageName), anyInt(), anyInt()); + doReturn(pkg.applicationInfo).when(mockPm) + .getApplicationInfo(eq(pkg.packageName), anyInt()); + } } catch (PackageManager.NameNotFoundException nnfe) {} } @@ -514,7 +534,11 @@ public class AppStandbyControllerTests { } private void assertBucket(int bucket) { - assertEquals(bucket, getStandbyBucket(mController, PACKAGE_1)); + assertBucket(bucket, PACKAGE_1); + } + + private void assertBucket(int bucket, String pkg) { + assertEquals(bucket, getStandbyBucket(mController, pkg)); } private void assertNotBucket(int bucket) { @@ -1462,6 +1486,32 @@ public class AppStandbyControllerTests { assertBucket(STANDBY_BUCKET_RESTRICTED); } + @Test + public void testSystemHeadlessAppElevated() { + reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1); + reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, + PACKAGE_SYSTEM_HEADFULL); + reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, + PACKAGE_SYSTEM_HEADLESS); + mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD; + + + mController.setAppStandbyBucket(PACKAGE_SYSTEM_HEADFULL, USER_ID, STANDBY_BUCKET_RARE, + REASON_MAIN_TIMEOUT); + assertBucket(STANDBY_BUCKET_RARE, PACKAGE_SYSTEM_HEADFULL); + + // Make sure headless system apps don't get lowered. + mController.setAppStandbyBucket(PACKAGE_SYSTEM_HEADLESS, USER_ID, STANDBY_BUCKET_RARE, + REASON_MAIN_TIMEOUT); + assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_SYSTEM_HEADLESS); + + // Package 1 doesn't have activities and is headless, but is not a system app, so it can + // be lowered. + mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, + REASON_MAIN_TIMEOUT); + assertBucket(STANDBY_BUCKET_RARE, PACKAGE_1); + } + private String getAdminAppsStr(int userId) { return getAdminAppsStr(userId, mController.getActiveAdminAppsForTest(userId)); }