diff --git a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java index be0e02595b60b..6af24be3a3725 100644 --- a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java +++ b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java @@ -227,6 +227,9 @@ public class EconomyManager { public static final String KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP = "js_min_satiated_balance_other_app"; /** @hide */ + public static final String KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER = + "js_min_satiated_balance_increment_updater"; + /** @hide */ public static final String KEY_JS_MAX_SATIATED_BALANCE = "js_max_satiated_balance"; /** @hide */ @@ -509,6 +512,15 @@ public class EconomyManager { public static final long DEFAULT_JS_REWARD_OTHER_USER_INTERACTION_ONGOING_CAKES = arcToCake(0); /** @hide */ public static final long DEFAULT_JS_REWARD_OTHER_USER_INTERACTION_MAX_CAKES = arcToCake(5000); + /** + * How many credits to increase the updating app's min satiated balance by for each app that it + * is responsible for updating. + * @hide + */ + public static final long DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES = + // Research indicates that the median time between popular app updates is 13-14 days, + // so adjust by 14 to amortize over that time. + DEFAULT_JS_REWARD_APP_INSTALL_INSTANT_CAKES / 14; /** @hide */ public static final long DEFAULT_JS_ACTION_JOB_MAX_START_CTP_CAKES = arcToCake(3); /** @hide */ diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java index c13e1dd978d6f..4a26d213a48a5 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java @@ -81,6 +81,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -164,6 +165,10 @@ public class InternalResourceService extends SystemService { @GuardedBy("mLock") private final SparseArrayMap mVipOverrides = new SparseArrayMap<>(); + /** Set of apps each installer is responsible for installing. */ + @GuardedBy("mLock") + private final SparseArrayMap> mInstallers = new SparseArrayMap<>(); + private volatile boolean mHasBattery = true; private volatile boolean mIsEnabled; private volatile int mBootPhase; @@ -353,6 +358,14 @@ public class InternalResourceService extends SystemService { return mCompleteEconomicPolicy; } + /** Returns the number of apps that this app is expected to update at some point. */ + int getAppUpdateResponsibilityCount(final int userId, @NonNull final String pkgName) { + synchronized (mLock) { + // TODO(248274798): return 0 if the app has lost the install permission + return ArrayUtils.size(mInstallers.get(userId, pkgName)); + } + } + @NonNull SparseArrayMap getInstalledPackages() { synchronized (mLock) { @@ -525,7 +538,8 @@ public class InternalResourceService extends SystemService { } synchronized (mLock) { final InstalledPackageInfo ipo = new InstalledPackageInfo(packageInfo); - mPkgCache.add(userId, pkgName, ipo); + final InstalledPackageInfo oldIpo = mPkgCache.add(userId, pkgName, ipo); + maybeUpdateInstallerStatusLocked(oldIpo, ipo); mUidToPackageCache.add(uid, pkgName); // TODO: only do this when the user first launches the app (app leaves stopped state) mAgent.grantBirthrightLocked(userId, pkgName); @@ -552,7 +566,14 @@ public class InternalResourceService extends SystemService { synchronized (mLock) { mUidToPackageCache.remove(uid, pkgName); mVipOverrides.delete(userId, pkgName); - mPkgCache.delete(userId, pkgName); + final InstalledPackageInfo ipo = mPkgCache.delete(userId, pkgName); + mInstallers.delete(userId, pkgName); + if (ipo != null && ipo.installerPackageName != null) { + final ArraySet list = mInstallers.get(userId, ipo.installerPackageName); + if (list != null) { + list.remove(pkgName); + } + } mAgent.onPackageRemovedLocked(userId, pkgName); } } @@ -574,7 +595,8 @@ public class InternalResourceService extends SystemService { mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId); for (int i = pkgs.size() - 1; i >= 0; --i) { final InstalledPackageInfo ipo = new InstalledPackageInfo(pkgs.get(i)); - mPkgCache.add(userId, ipo.packageName, ipo); + final InstalledPackageInfo oldIpo = mPkgCache.add(userId, ipo.packageName, ipo); + maybeUpdateInstallerStatusLocked(oldIpo, ipo); } mAgent.grantBirthrightsLocked(userId); } @@ -590,6 +612,7 @@ public class InternalResourceService extends SystemService { mUidToPackageCache.remove(pkgInfo.uid); } } + mInstallers.delete(userId); mPkgCache.delete(userId); mAgent.onUserRemovedLocked(userId); } @@ -746,11 +769,49 @@ public class InternalResourceService extends SystemService { mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId); for (int i = pkgs.size() - 1; i >= 0; --i) { final InstalledPackageInfo ipo = new InstalledPackageInfo(pkgs.get(i)); - mPkgCache.add(userId, ipo.packageName, ipo); + final InstalledPackageInfo oldIpo = mPkgCache.add(userId, ipo.packageName, ipo); + maybeUpdateInstallerStatusLocked(oldIpo, ipo); } } } + /** + * Used to update the set of installed apps for each installer. This only has an effect if the + * installer package name is different between {@code oldIpo} and {@code newIpo}. + */ + @GuardedBy("mLock") + private void maybeUpdateInstallerStatusLocked(@Nullable InstalledPackageInfo oldIpo, + @NonNull InstalledPackageInfo newIpo) { + final boolean changed; + if (oldIpo == null) { + changed = newIpo.installerPackageName != null; + } else { + changed = !Objects.equals(oldIpo.installerPackageName, newIpo.installerPackageName); + } + if (!changed) { + return; + } + // InstallSourceInfo doesn't track userId, so for now, assume the installer on the package's + // user profile did the installation. + // TODO(246640162): use the actual installer's user ID + final int userId = UserHandle.getUserId(newIpo.uid); + final String pkgName = newIpo.packageName; + if (oldIpo != null) { + final ArraySet oldList = mInstallers.get(userId, oldIpo.installerPackageName); + if (oldList != null) { + oldList.remove(pkgName); + } + } + if (newIpo.installerPackageName != null) { + ArraySet newList = mInstallers.get(userId, newIpo.installerPackageName); + if (newList == null) { + newList = new ArraySet<>(); + mInstallers.add(userId, newIpo.installerPackageName, newList); + } + newList.add(pkgName); + } + } + private void registerListeners() { final IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_BATTERY_CHANGED); @@ -1359,6 +1420,23 @@ public class InternalResourceService extends SystemService { } pw.println(); + pw.println(); + pw.println("Installers:"); + pw.increaseIndent(); + for (int u = 0; u < mInstallers.numMaps(); ++u) { + final int userId = mInstallers.keyAt(u); + + for (int p = 0; p < mInstallers.numElementsForKeyAt(u); ++p) { + final String pkgName = mInstallers.keyAt(u, p); + + pw.print(appToString(userId, pkgName)); + pw.print(": "); + pw.print(mInstallers.valueAt(u, p).size()); + pw.println(" apps"); + } + } + pw.decreaseIndent(); + pw.println(); mCompleteEconomicPolicy.dump(pw); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java index 55cc3520961e3..322e824286d50 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java @@ -42,6 +42,7 @@ import static android.app.tare.EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_ import static android.app.tare.EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES; +import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_REWARD_APP_INSTALL_INSTANT_CAKES; import static android.app.tare.EconomyManager.DEFAULT_JS_REWARD_APP_INSTALL_MAX_CAKES; @@ -87,6 +88,7 @@ import static android.app.tare.EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT; import static android.app.tare.EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT; import static android.app.tare.EconomyManager.KEY_JS_MAX_SATIATED_BALANCE; import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED; +import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER; import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP; import static android.app.tare.EconomyManager.KEY_JS_REWARD_APP_INSTALL_INSTANT; import static android.app.tare.EconomyManager.KEY_JS_REWARD_APP_INSTALL_MAX; @@ -154,6 +156,7 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { private long mMinSatiatedBalanceExempted; private long mMinSatiatedBalanceOther; + private long mMinSatiatedBalanceIncrementalAppUpdater; private long mMaxSatiatedBalance; private long mInitialSatiatedConsumptionLimit; private long mHardSatiatedConsumptionLimit; @@ -183,11 +186,20 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { if (mIrs.isPackageRestricted(userId, pkgName)) { return 0; } + + final long baseBalance; if (mIrs.isPackageExempted(userId, pkgName)) { - return mMinSatiatedBalanceExempted; + baseBalance = mMinSatiatedBalanceExempted; + } else { + baseBalance = mMinSatiatedBalanceOther; } - // TODO: take other exemptions into account - return mMinSatiatedBalanceOther; + + long minBalance = baseBalance; + + final int updateResponsibilityCount = mIrs.getAppUpdateResponsibilityCount(userId, pkgName); + minBalance += updateResponsibilityCount * mMinSatiatedBalanceIncrementalAppUpdater; + + return Math.min(minBalance, mMaxSatiatedBalance); } @Override @@ -242,6 +254,9 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { mMinSatiatedBalanceExempted = getConstantAsCake(mParser, properties, KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES, mMinSatiatedBalanceOther); + mMinSatiatedBalanceIncrementalAppUpdater = getConstantAsCake(mParser, properties, + KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER, + DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES); mMaxSatiatedBalance = getConstantAsCake(mParser, properties, KEY_JS_MAX_SATIATED_BALANCE, DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES, Math.max(arcToCake(1), mMinSatiatedBalanceExempted)); @@ -397,10 +412,11 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { @Override void dump(IndentingPrintWriter pw) { - pw.println("Min satiated balances:"); + pw.println("Min satiated balance:"); pw.increaseIndent(); pw.print("Exempted", cakeToString(mMinSatiatedBalanceExempted)).println(); pw.print("Other", cakeToString(mMinSatiatedBalanceOther)).println(); + pw.print("+App Updater", cakeToString(mMinSatiatedBalanceIncrementalAppUpdater)).println(); pw.decreaseIndent(); pw.print("Max satiated balance", cakeToString(mMaxSatiatedBalance)).println(); pw.print("Consumption limits: ["); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java index 620d1a0da76f2..a68170c9bac7b 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java @@ -286,7 +286,7 @@ class Ledger { final int idx = (mRewardBucketIndex - b + NUM_REWARD_BUCKET_WINDOWS) % NUM_REWARD_BUCKET_WINDOWS; final RewardBucket rewardBucket = mRewardBuckets[idx]; - if (rewardBucket == null) { + if (rewardBucket == null || rewardBucket.startTimeMs == 0) { continue; } diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java index 19b798da5aabe..b7bbcd7562cd9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java @@ -134,15 +134,36 @@ public class JobSchedulerEconomicPolicyTest { mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES, mEconomicPolicy.getHardSatiatedConsumptionLimit()); + final String pkgRestricted = "com.pkg.restricted"; when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true); + assertEquals(0, mEconomicPolicy.getMinSatiatedBalance(0, pkgRestricted)); assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted)); - assertEquals(EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES, - mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); + final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES, mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); + assertEquals(EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES, + mEconomicPolicy.getMaxSatiatedBalance(0, pkgExempted)); + + final String pkgUpdater = "com.pkg.updater"; + when(mIrs.getAppUpdateResponsibilityCount(anyInt(), eq(pkgUpdater))).thenReturn(5); + assertEquals(5 * EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES + + EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP_CAKES, + mEconomicPolicy.getMinSatiatedBalance(0, pkgUpdater)); + assertEquals(EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES, + mEconomicPolicy.getMaxSatiatedBalance(0, pkgUpdater)); + // Make sure it doesn't suggest a min balance greater than max. + final int updateCount = (int) (EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES + / EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES); + when(mIrs.getAppUpdateResponsibilityCount(anyInt(), eq(pkgUpdater))) + .thenReturn(updateCount); + assertEquals(EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES, + mEconomicPolicy.getMinSatiatedBalance(0, pkgUpdater)); + + assertEquals(EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES, + mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); assertEquals(EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP_CAKES, mEconomicPolicy.getMinSatiatedBalance(0, "com.any.other.app")); } @@ -152,8 +173,10 @@ public class JobSchedulerEconomicPolicyTest { setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(5)); setDeviceConfigCakes(EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT, arcToCake(25)); setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(10)); - setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(9)); - setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(7)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(6)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(4)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER, + arcToCake(1)); assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(arcToCake(25), mEconomicPolicy.getHardSatiatedConsumptionLimit()); @@ -163,8 +186,12 @@ public class JobSchedulerEconomicPolicyTest { assertEquals(arcToCake(10), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app")); final String pkgExempted = "com.pkg.exempted"; when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); - assertEquals(arcToCake(9), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); - assertEquals(arcToCake(7), mEconomicPolicy.getMinSatiatedBalance(0, "com.any.other.app")); + assertEquals(arcToCake(6), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); + assertEquals(arcToCake(4), mEconomicPolicy.getMinSatiatedBalance(0, "com.any.other.app")); + final String pkgUpdater = "com.pkg.updater"; + when(mIrs.getAppUpdateResponsibilityCount(anyInt(), eq(pkgUpdater))).thenReturn(3); + assertEquals(arcToCake(4) + 3 * arcToCake(1), + mEconomicPolicy.getMinSatiatedBalance(0, pkgUpdater)); } @Test @@ -175,6 +202,8 @@ public class JobSchedulerEconomicPolicyTest { setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(-1)); setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(-2)); setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(-3)); + setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER, + arcToCake(-4)); assertEquals(arcToCake(1), mEconomicPolicy.getInitialSatiatedConsumptionLimit()); assertEquals(arcToCake(1), mEconomicPolicy.getHardSatiatedConsumptionLimit()); @@ -186,6 +215,10 @@ public class JobSchedulerEconomicPolicyTest { when(mIrs.isPackageExempted(anyInt(), eq(pkgExempted))).thenReturn(true); assertEquals(arcToCake(0), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted)); assertEquals(arcToCake(0), mEconomicPolicy.getMinSatiatedBalance(0, "com.any.other.app")); + final String pkgUpdater = "com.pkg.updater"; + when(mIrs.getAppUpdateResponsibilityCount(anyInt(), eq(pkgUpdater))).thenReturn(5); + assertEquals(arcToCake(0) + 5 * arcToCake(0), + mEconomicPolicy.getMinSatiatedBalance(0, pkgUpdater)); // Test min+max reversed. setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(5));