Reclaim unused credits from unused apps.
Apps will start to lose 10% of their allocated ARCs every 24 hours after
72 hours of no use. However, we make sure not to take the app's balance
below the min balance as indicated by the EconomicPolicy.
Bug: 158300259
Test: Android builds
Change-Id: I55c7d74e5748a07965bee81df08855726e8b1660
diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
index 8c06338..968c6e5 100644
--- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
@@ -71,6 +71,13 @@
long getTimeSinceLastJobRun(String packageName, int userId);
+ /**
+ * Returns the time (in milliseconds) since the app was last interacted with by the user.
+ * This can be larger than the current elapsedRealtime, in case it happened before boot or
+ * a really large value if the app was never interacted with.
+ */
+ long getTimeSinceLastUsedByUser(String packageName, int userId);
+
void onUserRemoved(int userId);
void addListener(AppIdleStateChangeListener listener);
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index f8f0bd0..43e906d 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -21,6 +21,7 @@
import static com.android.server.tare.EconomicPolicy.REGULATION_BASIC_INCOME;
import static com.android.server.tare.EconomicPolicy.REGULATION_BIRTHRIGHT;
+import static com.android.server.tare.EconomicPolicy.REGULATION_WEALTH_RECLAMATION;
import static com.android.server.tare.EconomicPolicy.TYPE_ACTION;
import static com.android.server.tare.EconomicPolicy.TYPE_REWARD;
import static com.android.server.tare.EconomicPolicy.eventToString;
@@ -46,6 +47,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
+import com.android.server.usage.AppStandbyInternal;
import java.util.List;
import java.util.Objects;
@@ -53,7 +55,7 @@
/**
* Other half of the IRS. The agent handles the nitty gritty details, interacting directly with
- * ledgers, carrying out specific events such as tax collection and granting initial balances or
+ * ledgers, carrying out specific events such as wealth reclamation, granting initial balances or
* replenishing balances, and tracking ongoing events.
*/
class Agent {
@@ -62,6 +64,11 @@
|| Log.isLoggable(TAG, Log.DEBUG);
/**
+ * The minimum amount of time an app must not have been used by the user before we start
+ * regularly reclaiming ARCs from it.
+ */
+ private static final long MIN_UNUSED_TIME_MS = 3 * 24 * HOUR_IN_MILLIS;
+ /**
* The maximum amount of time we'll keep a transaction around for.
* For now, only keep transactions we actually have a use for. We can increase it if we want
* to use older transactions or provide older transactions to apps.
@@ -75,6 +82,8 @@
private final Handler mHandler;
private final InternalResourceService mIrs;
+ private final AppStandbyInternal mAppStandbyInternal;
+
@GuardedBy("mLock")
private final SparseArrayMap<String, Ledger> mLedgers = new SparseArrayMap<>();
@@ -97,6 +106,7 @@
mIrs = irs;
mCompleteEconomicPolicy = completeEconomicPolicy;
mHandler = new AgentHandler(TareHandlerThread.get().getLooper());
+ mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
}
@GuardedBy("mLock")
@@ -198,6 +208,47 @@
}
}
+ /**
+ * Reclaim a percentage of unused ARCs from every app that hasn't been used recently. The
+ * reclamation will not reduce an app's balance below its minimum balance as dictated by the
+ * EconomicPolicy.
+ *
+ * @param percentage A value between 0 and 1 to indicate how much of the unused balance should
+ * be reclaimed.
+ */
+ @GuardedBy("mLock")
+ void reclaimUnusedAssetsLocked(double percentage) {
+ final List<PackageInfo> pkgs = mIrs.getInstalledPackages();
+ final long now = System.currentTimeMillis();
+ for (int i = 0; i < pkgs.size(); ++i) {
+ final int userId = UserHandle.getUserId(pkgs.get(i).applicationInfo.uid);
+ final String pkgName = pkgs.get(i).packageName;
+ final Ledger ledger = getLedgerLocked(userId, pkgName);
+ // AppStandby only counts elapsed time for things like this
+ // TODO: should we use clock time instead?
+ final long timeSinceLastUsedMs =
+ mAppStandbyInternal.getTimeSinceLastUsedByUser(pkgName, userId);
+ if (timeSinceLastUsedMs >= MIN_UNUSED_TIME_MS) {
+ // Use a constant floor instead of the scaled floor from the IRS.
+ final long minBalance =
+ mCompleteEconomicPolicy.getMinSatiatedBalance(userId, pkgName);
+ final long curBalance = ledger.getCurrentBalance();
+ long toReclaim = (long) (curBalance * percentage);
+ if (curBalance - toReclaim < minBalance) {
+ toReclaim = curBalance - minBalance;
+ }
+ if (toReclaim > 0) {
+ Slog.i(TAG, "Reclaiming unused wealth! Taking " + toReclaim
+ + " from <" + userId + ">" + pkgName);
+
+ recordTransactionLocked(userId, pkgName, ledger,
+ new Ledger.Transaction(
+ now, now, REGULATION_WEALTH_RECLAMATION, null, -toReclaim));
+ }
+ }
+ }
+ }
+
@GuardedBy("mLock")
void distributeBasicIncomeLocked(int batteryLevel) {
List<PackageInfo> pkgs = mIrs.getInstalledPackages();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
index 0e54163..0b5ff1b 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
@@ -55,7 +55,7 @@
static final int REGULATION_BASIC_INCOME = TYPE_REGULATION | 0;
static final int REGULATION_BIRTHRIGHT = TYPE_REGULATION | 1;
- static final int REGULATION_TAX = TYPE_REGULATION | 2;
+ static final int REGULATION_WEALTH_RECLAMATION = TYPE_REGULATION | 2;
static final int REWARD_NOTIFICATION_SEEN = TYPE_REWARD | 0;
static final int REWARD_NOTIFICATION_INTERACTION = TYPE_REWARD | 1;
@@ -347,8 +347,8 @@
return "BASIC_INCOME";
case REGULATION_BIRTHRIGHT:
return "BIRTHRIGHT";
- case REGULATION_TAX:
- return "TAX";
+ case REGULATION_WEALTH_RECLAMATION:
+ return "WEALTH_RECLAMATION";
}
return "UNKNOWN_REGULATION:" + Integer.toHexString(eventId);
}
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 3ccf263..652f6ef 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -16,8 +16,12 @@
package com.android.server.tare;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.AlarmManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -29,6 +33,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Log;
@@ -57,6 +62,10 @@
public static final String TAG = "TARE-IRS";
public static final boolean DEBUG = Log.isLoggable("TARE", Log.DEBUG);
+ static final long UNUSED_RECLAMATION_PERIOD_MS = 24 * HOUR_IN_MILLIS;
+ /** How much of an app's unused wealth should be reclaimed periodically. */
+ private static final float DEFAULT_UNUSED_RECLAMATION_PERCENTAGE = .1f;
+
/** Global local for all resource economy state. */
private final Object mLock = new Object();
@@ -83,6 +92,9 @@
// In the range [0,100] to represent 0% to 100% battery.
@GuardedBy("mLock")
private int mCurrentBatteryLevel;
+ // TODO: load from disk
+ @GuardedBy("mLock")
+ private long mLastUnusedReclamationTime;
@SuppressWarnings("FieldCanBeLocal")
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -133,7 +145,21 @@
}
};
+ private final AlarmManager.OnAlarmListener mUnusedWealthReclamationListener =
+ new AlarmManager.OnAlarmListener() {
+ @Override
+ public void onAlarm() {
+ synchronized (mLock) {
+ mAgent.reclaimUnusedAssetsLocked(DEFAULT_UNUSED_RECLAMATION_PERCENTAGE);
+ mLastUnusedReclamationTime = System.currentTimeMillis();
+ scheduleUnusedWealthReclamationLocked();
+ }
+ }
+ };
+
private static final int MSG_NOTIFY_BALANCE_CHANGE_LISTENERS = 0;
+ private static final int MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT = 1;
+ private static final String ALARM_TAG_WEALTH_RECLAMATION = "*tare.reclamation*";
/**
* Initializes the system service.
@@ -186,6 +212,8 @@
} else {
mIsSetup = true;
}
+ scheduleUnusedWealthReclamationLocked();
+ mCompleteEconomicPolicy.onSystemServicesReady();
}
}
}
@@ -314,6 +342,26 @@
.sendToTarget();
}
+ @GuardedBy("mLock")
+ private void scheduleUnusedWealthReclamationLocked() {
+ final long now = System.currentTimeMillis();
+ final long nextReclamationTime =
+ Math.max(mLastUnusedReclamationTime + UNUSED_RECLAMATION_PERIOD_MS, now + 30_000);
+ mHandler.post(() -> {
+ // Never call out to AlarmManager with the lock held. This sits below AM.
+ AlarmManager alarmManager = getContext().getSystemService(AlarmManager.class);
+ if (alarmManager != null) {
+ alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + (nextReclamationTime - now),
+ 30 * MINUTE_IN_MILLIS,
+ ALARM_TAG_WEALTH_RECLAMATION, mUnusedWealthReclamationListener, mHandler);
+ } else {
+ mHandler.sendEmptyMessageDelayed(
+ MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT, 30_000);
+ }
+ });
+ }
+
private int getCurrentBatteryLevel() {
return mBatteryManagerInternal.getBatteryLevel();
}
@@ -351,6 +399,13 @@
}
}
break;
+
+ case MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT:
+ removeMessages(MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT);
+ synchronized (mLock) {
+ scheduleUnusedWealthReclamationLocked();
+ }
+ break;
}
}
}
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 d532e20..187422b 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -462,6 +462,17 @@
return getElapsedTime(elapsedRealtime) - appUsageHistory.lastJobRunTime;
}
+ public long getTimeSinceLastUsedByUser(String packageName, int userId, long elapsedRealtime) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, false);
+ if (appUsageHistory == null || appUsageHistory.lastUsedByUserElapsedTime == Long.MIN_VALUE
+ || appUsageHistory.lastUsedByUserElapsedTime == 0) {
+ return Long.MAX_VALUE;
+ }
+ return getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedByUserElapsedTime;
+ }
+
public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) {
ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
AppUsageHistory appUsageHistory =
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 4b081d2..2fa10f0 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -1082,6 +1082,14 @@
}
@Override
+ public long getTimeSinceLastUsedByUser(String packageName, int userId) {
+ final long elapsedRealtime = mInjector.elapsedRealtime();
+ synchronized (mAppIdleLock) {
+ return mAppIdleHistory.getTimeSinceLastUsedByUser(packageName, userId, elapsedRealtime);
+ }
+ }
+
+ @Override
public void onUserRemoved(int userId) {
synchronized (mAppIdleLock) {
mAppIdleHistory.onUserRemoved(userId);