/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.tare; import static android.app.tare.EconomyManager.ENABLED_MODE_OFF; import static android.app.tare.EconomyManager.ENABLED_MODE_ON; import static android.app.tare.EconomyManager.ENABLED_MODE_SHADOW; import static android.app.tare.EconomyManager.enabledModeToString; import static android.provider.Settings.Global.TARE_ALARM_MANAGER_CONSTANTS; import static android.provider.Settings.Global.TARE_JOB_SCHEDULER_CONSTANTS; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.server.tare.TareUtils.appToString; import static com.android.server.tare.TareUtils.cakeToString; import static com.android.server.tare.TareUtils.getCurrentTimeMillis; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AlarmManager; import android.app.AppOpsManager; import android.app.tare.EconomyManager; import android.app.tare.IEconomyManager; import android.app.usage.UsageEvents; import android.app.usage.UsageStatsManagerInternal; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.database.ContentObserver; import android.net.Uri; import android.os.BatteryManager; import android.os.BatteryManagerInternal; import android.os.Binder; import android.os.Handler; import android.os.IDeviceIdleController; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.SparseArrayMap; import android.util.SparseLongArray; import android.util.SparseSetArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; import com.android.internal.os.SomeArgs; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.pm.UserManagerInternal; import com.android.server.tare.EconomicPolicy.Cost; import com.android.server.tare.EconomyManagerInternal.TareStateChangeListener; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * Responsible for handling app's ARC count based on events, ensuring ARCs are credited when * appropriate, and reclaiming ARCs at the right times. The IRS deals with the high level details * while the {@link Agent} deals with the nitty-gritty details. * * Note on locking: Any function with the suffix 'Locked' needs to lock on {@link #mLock}. * * @hide */ public class InternalResourceService extends SystemService { 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; /** * The minimum amount of time an app must not have been used by the user before we start * periodically reclaiming ARCs from it. */ private static final long MIN_UNUSED_TIME_MS = 3 * DAY_IN_MILLIS; /** The amount of time to delay reclamation by after boot. */ private static final long RECLAMATION_STARTUP_DELAY_MS = 30_000L; /** * The amount of time after TARE has first been set up that a system installer will be allowed * expanded credit privileges. */ static final long INSTALLER_FIRST_SETUP_GRACE_PERIOD_MS = 7 * DAY_IN_MILLIS; /** * The amount of time to wait after TARE has first been set up before considering adjusting the * stock/consumption limit. */ private static final long STOCK_ADJUSTMENT_FIRST_SETUP_GRACE_PERIOD_MS = 5 * DAY_IN_MILLIS; /** * The battery level above which we may consider quantitative easing (increasing the consumption * limit). */ private static final int QUANTITATIVE_EASING_BATTERY_THRESHOLD = 50; /** * The battery level above which we may consider adjusting the desired stock level. */ private static final int STOCK_RECALCULATION_BATTERY_THRESHOLD = 80; /** * The amount of time to wait before considering recalculating the desired stock level. */ private static final long STOCK_RECALCULATION_DELAY_MS = 16 * HOUR_IN_MILLIS; /** * The minimum amount of time we must have background drain for before considering * recalculating the desired stock level. */ private static final long STOCK_RECALCULATION_MIN_DATA_DURATION_MS = 8 * HOUR_IN_MILLIS; private static final int PACKAGE_QUERY_FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_APEX | PackageManager.GET_PERMISSIONS; /** Global lock for all resource economy state. */ private final Object mLock = new Object(); private final Handler mHandler; private final BatteryManagerInternal mBatteryManagerInternal; private final PackageManager mPackageManager; private final PackageManagerInternal mPackageManagerInternal; private IAppOpsService mAppOpsService; private IDeviceIdleController mDeviceIdleController; private final Agent mAgent; private final Analyst mAnalyst; private final ConfigObserver mConfigObserver; private final EconomyManagerStub mEconomyManagerStub; private final Scribe mScribe; @GuardedBy("mLock") private CompleteEconomicPolicy mCompleteEconomicPolicy; @NonNull @GuardedBy("mLock") private final SparseArrayMap mPkgCache = new SparseArrayMap<>(); /** Cached mapping of UIDs (for all users) to a list of packages in the UID. */ @GuardedBy("mLock") private final SparseSetArray mUidToPackageCache = new SparseSetArray<>(); /** Cached mapping of userId+package to their UIDs (for all users) */ @GuardedBy("mPackageToUidCache") private final SparseArrayMap mPackageToUidCache = new SparseArrayMap<>(); @GuardedBy("mStateChangeListeners") private final SparseSetArray mStateChangeListeners = new SparseSetArray<>(); /** * List of packages that are fully restricted and shouldn't be allowed to run in the background. */ @GuardedBy("mLock") private final SparseSetArray mRestrictedApps = new SparseSetArray<>(); /** List of packages that are "exempted" from battery restrictions. */ // TODO(144864180): include userID @GuardedBy("mLock") private ArraySet mExemptedApps = new ArraySet<>(); @GuardedBy("mLock") private final SparseArrayMap mVipOverrides = new SparseArrayMap<>(); /** * Set of temporary Very Important Packages and when their VIP status ends, in the elapsed * realtime ({@link android.annotation.ElapsedRealtimeLong}) timebase. */ @GuardedBy("mLock") private final SparseArrayMap mTemporaryVips = new SparseArrayMap<>(); /** Set of apps each installer is responsible for installing. */ @GuardedBy("mLock") private final SparseArrayMap> mInstallers = new SparseArrayMap<>(); /** The package name of the wellbeing app. */ @GuardedBy("mLock") @Nullable private String mWellbeingPackage; private volatile boolean mHasBattery = true; @EconomyManager.EnabledMode private volatile int mEnabledMode; private volatile int mBootPhase; private volatile boolean mExemptListLoaded; // In the range [0,100] to represent 0% to 100% battery. @GuardedBy("mLock") private int mCurrentBatteryLevel; // TODO(250007395): make configurable per device (via config.xml) private final int mDefaultTargetBackgroundBatteryLifeHours; @GuardedBy("mLock") private int mTargetBackgroundBatteryLifeHours; private final IAppOpsCallback mApbListener = new IAppOpsCallback.Stub() { @Override public void opChanged(int op, int uid, String packageName) { boolean restricted = false; try { restricted = mAppOpsService.checkOperation( AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName) != AppOpsManager.MODE_ALLOWED; } catch (RemoteException e) { // Shouldn't happen } final int userId = UserHandle.getUserId(uid); synchronized (mLock) { if (restricted) { if (mRestrictedApps.add(userId, packageName)) { mAgent.onAppRestrictedLocked(userId, packageName); } } else if (mRestrictedApps.remove(UserHandle.getUserId(uid), packageName)) { mAgent.onAppUnrestrictedLocked(userId, packageName); } } } }; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Nullable private String getPackageName(Intent intent) { Uri uri = intent.getData(); return uri != null ? uri.getSchemeSpecificPart() : null; } @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { case Intent.ACTION_BATTERY_CHANGED: { final boolean hasBattery = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, mHasBattery); if (mHasBattery != hasBattery) { mHasBattery = hasBattery; mConfigObserver.updateEnabledStatus(); } } break; case Intent.ACTION_BATTERY_LEVEL_CHANGED: onBatteryLevelChanged(); break; case Intent.ACTION_PACKAGE_FULLY_REMOVED: { final String pkgName = getPackageName(intent); final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); onPackageRemoved(pkgUid, pkgName); } break; case Intent.ACTION_PACKAGE_ADDED: { if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { final String pkgName = getPackageName(intent); final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); onPackageAdded(pkgUid, pkgName); } } break; case Intent.ACTION_PACKAGE_RESTARTED: { final String pkgName = getPackageName(intent); final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); final int userId = UserHandle.getUserId(pkgUid); onPackageForceStopped(userId, pkgName); } break; case Intent.ACTION_USER_ADDED: { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); onUserAdded(userId); } break; case Intent.ACTION_USER_REMOVED: { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); onUserRemoved(userId); } break; case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED: onExemptionListChanged(); break; } } }; private final UsageStatsManagerInternal.UsageEventListener mSurveillanceAgent = new UsageStatsManagerInternal.UsageEventListener() { /** * Callback to inform listeners of a new event. */ @Override public void onUsageEvent(int userId, @NonNull UsageEvents.Event event) { mHandler.obtainMessage(MSG_PROCESS_USAGE_EVENT, userId, 0, event) .sendToTarget(); } }; private final AlarmManager.OnAlarmListener mUnusedWealthReclamationListener = new AlarmManager.OnAlarmListener() { @Override public void onAlarm() { synchronized (mLock) { mAgent.reclaimUnusedAssetsLocked( DEFAULT_UNUSED_RECLAMATION_PERCENTAGE, MIN_UNUSED_TIME_MS, false); mScribe.setLastReclamationTimeLocked(getCurrentTimeMillis()); scheduleUnusedWealthReclamationLocked(); } } }; private static final int MSG_NOTIFY_AFFORDABILITY_CHANGE_LISTENER = 0; private static final int MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT = 1; private static final int MSG_PROCESS_USAGE_EVENT = 2; private static final int MSG_NOTIFY_STATE_CHANGE_LISTENERS = 3; private static final int MSG_NOTIFY_STATE_CHANGE_LISTENER = 4; private static final int MSG_CLEAN_UP_TEMP_VIP_LIST = 5; private static final String ALARM_TAG_WEALTH_RECLAMATION = "*tare.reclamation*"; /** * Initializes the system service. *

* Subclasses must define a single argument constructor that accepts the context * and passes it to super. *

* * @param context The system server context. */ public InternalResourceService(Context context) { super(context); mHandler = new IrsHandler(TareHandlerThread.get().getLooper()); mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class); mPackageManager = context.getPackageManager(); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mEconomyManagerStub = new EconomyManagerStub(); mAnalyst = new Analyst(); mScribe = new Scribe(this, mAnalyst); mCompleteEconomicPolicy = new CompleteEconomicPolicy(this); mAgent = new Agent(this, mScribe, mAnalyst); mConfigObserver = new ConfigObserver(mHandler, context); mDefaultTargetBackgroundBatteryLifeHours = mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH) ? 100 // ~ 1.0%/hr : 40; // ~ 2.5%/hr mTargetBackgroundBatteryLifeHours = mDefaultTargetBackgroundBatteryLifeHours; publishLocalService(EconomyManagerInternal.class, new LocalService()); } @Override public void onStart() { publishBinderService(Context.RESOURCE_ECONOMY_SERVICE, mEconomyManagerStub); } @Override public void onBootPhase(int phase) { mBootPhase = phase; switch (phase) { case PHASE_SYSTEM_SERVICES_READY: mAppOpsService = IAppOpsService.Stub.asInterface( ServiceManager.getService(Context.APP_OPS_SERVICE)); mDeviceIdleController = IDeviceIdleController.Stub.asInterface( ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); mConfigObserver.start(); onBootPhaseSystemServicesReady(); break; case PHASE_THIRD_PARTY_APPS_CAN_START: onBootPhaseThirdPartyAppsCanStart(); break; case PHASE_BOOT_COMPLETED: onBootPhaseBootCompleted(); break; } } @NonNull Object getLock() { return mLock; } /** Returns the installed packages for all users. */ @NonNull @GuardedBy("mLock") CompleteEconomicPolicy getCompleteEconomicPolicyLocked() { 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) { return mPkgCache; } } /** Returns the installed packages for the specified user. */ @NonNull List getInstalledPackages(final int userId) { final List userPkgs = new ArrayList<>(); synchronized (mLock) { final int uIdx = mPkgCache.indexOfKey(userId); if (uIdx < 0) { return userPkgs; } for (int p = mPkgCache.numElementsForKeyAt(uIdx) - 1; p >= 0; --p) { final InstalledPackageInfo packageInfo = mPkgCache.valueAt(uIdx, p); userPkgs.add(packageInfo); } } return userPkgs; } @Nullable InstalledPackageInfo getInstalledPackageInfo(final int userId, @NonNull final String pkgName) { synchronized (mLock) { return mPkgCache.get(userId, pkgName); } } @GuardedBy("mLock") long getConsumptionLimitLocked() { return mCurrentBatteryLevel * mScribe.getSatiatedConsumptionLimitLocked() / 100; } @GuardedBy("mLock") long getMinBalanceLocked(final int userId, @NonNull final String pkgName) { return mCurrentBatteryLevel * mCompleteEconomicPolicy.getMinSatiatedBalance(userId, pkgName) / 100; } @GuardedBy("mLock") long getInitialSatiatedConsumptionLimitLocked() { return mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit(); } long getRealtimeSinceFirstSetupMs() { return mScribe.getRealtimeSinceFirstSetupMs(SystemClock.elapsedRealtime()); } int getUid(final int userId, @NonNull final String pkgName) { synchronized (mPackageToUidCache) { Integer uid = mPackageToUidCache.get(userId, pkgName); if (uid == null) { uid = mPackageManagerInternal.getPackageUid(pkgName, 0, userId); mPackageToUidCache.add(userId, pkgName, uid); } return uid; } } @EconomyManager.EnabledMode int getEnabledMode() { return mEnabledMode; } @EconomyManager.EnabledMode int getEnabledMode(int policyId) { synchronized (mLock) { // For now, treat enabled policies as using the same enabled mode as full TARE. // TODO: have enabled mode by policy if (mCompleteEconomicPolicy.isPolicyEnabled(policyId)) { return mEnabledMode; } return ENABLED_MODE_OFF; } } boolean isHeadlessSystemApp(final int userId, @NonNull String pkgName) { if (pkgName == null) { Slog.wtfStack(TAG, "isHeadlessSystemApp called with null package"); return false; } synchronized (mLock) { final InstalledPackageInfo ipo = getInstalledPackageInfo(userId, pkgName); if (ipo != null && ipo.isHeadlessSystemApp) { return true; } // The wellbeing app is pre-set on the device, not expected to be interacted with // much by the user, but can be expected to do work in the background on behalf of // the user. As such, it's a pseudo-headless system app, so treat it as a headless // system app. return pkgName.equals(mWellbeingPackage); } } boolean isPackageExempted(final int userId, @NonNull String pkgName) { synchronized (mLock) { return mExemptedApps.contains(pkgName); } } boolean isPackageRestricted(final int userId, @NonNull String pkgName) { synchronized (mLock) { return mRestrictedApps.contains(userId, pkgName); } } boolean isSystem(final int userId, @NonNull String pkgName) { if ("android".equals(pkgName)) { return true; } return UserHandle.isCore(getUid(userId, pkgName)); } boolean isVip(final int userId, @NonNull String pkgName) { return isVip(userId, pkgName, SystemClock.elapsedRealtime()); } boolean isVip(final int userId, @NonNull String pkgName, final long nowElapsed) { synchronized (mLock) { final Boolean override = mVipOverrides.get(userId, pkgName); if (override != null) { return override; } } if (isSystem(userId, pkgName)) { // The government, I mean the system, can create ARCs as it needs to in order to // operate. return true; } synchronized (mLock) { final Long expirationTimeElapsed = mTemporaryVips.get(userId, pkgName); if (expirationTimeElapsed != null) { return nowElapsed <= expirationTimeElapsed; } } return false; } void onBatteryLevelChanged() { synchronized (mLock) { final int newBatteryLevel = getCurrentBatteryLevel(); mAnalyst.noteBatteryLevelChange(newBatteryLevel); final boolean increased = newBatteryLevel > mCurrentBatteryLevel; if (increased) { if (newBatteryLevel >= STOCK_RECALCULATION_BATTERY_THRESHOLD) { maybeAdjustDesiredStockLevelLocked(); } mAgent.distributeBasicIncomeLocked(newBatteryLevel); } else if (newBatteryLevel == mCurrentBatteryLevel) { // The broadcast is also sent when the plug type changes... return; } mCurrentBatteryLevel = newBatteryLevel; adjustCreditSupplyLocked(increased); } } void onDeviceStateChanged() { synchronized (mLock) { mAgent.onDeviceStateChangedLocked(); } } void onExemptionListChanged() { final int[] userIds = LocalServices.getService(UserManagerInternal.class).getUserIds(); synchronized (mLock) { final ArraySet removed = mExemptedApps; final ArraySet added = new ArraySet<>(); try { mExemptedApps = new ArraySet<>(mDeviceIdleController.getFullPowerWhitelist()); mExemptListLoaded = true; } catch (RemoteException e) { // Shouldn't happen. } for (int i = mExemptedApps.size() - 1; i >= 0; --i) { final String pkg = mExemptedApps.valueAt(i); if (!removed.contains(pkg)) { added.add(pkg); } removed.remove(pkg); } for (int a = added.size() - 1; a >= 0; --a) { final String pkgName = added.valueAt(a); for (int userId : userIds) { // Since the exemption list doesn't specify user ID and we track by user ID, // we need to see if the app exists on the user before talking to the agent. // Otherwise, we may end up with invalid ledgers. final boolean appExists = getUid(userId, pkgName) >= 0; if (appExists) { mAgent.onAppExemptedLocked(userId, pkgName); } } } for (int r = removed.size() - 1; r >= 0; --r) { final String pkgName = removed.valueAt(r); for (int userId : userIds) { // Since the exemption list doesn't specify user ID and we track by user ID, // we need to see if the app exists on the user before talking to the agent. // Otherwise, we may end up with invalid ledgers. final boolean appExists = getUid(userId, pkgName) >= 0; if (appExists) { mAgent.onAppUnexemptedLocked(userId, pkgName); } } } } } void onPackageAdded(final int uid, @NonNull final String pkgName) { final int userId = UserHandle.getUserId(uid); final PackageInfo packageInfo; try { packageInfo = mPackageManager.getPackageInfoAsUser(pkgName, PACKAGE_QUERY_FLAGS, userId); } catch (PackageManager.NameNotFoundException e) { Slog.wtf(TAG, "PM couldn't find newly added package: " + pkgName, e); return; } synchronized (mPackageToUidCache) { mPackageToUidCache.add(userId, pkgName, uid); } synchronized (mLock) { final InstalledPackageInfo ipo = new InstalledPackageInfo(getContext(), userId, packageInfo); 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); if (ipo.installerPackageName != null) { mAgent.noteInstantaneousEventLocked(userId, ipo.installerPackageName, JobSchedulerEconomicPolicy.REWARD_APP_INSTALL, null); } } } void onPackageForceStopped(final int userId, @NonNull final String pkgName) { synchronized (mLock) { // Remove all credits if the user force stops the app. It will slowly regain them // in response to different events. mAgent.reclaimAllAssetsLocked(userId, pkgName, EconomicPolicy.REGULATION_FORCE_STOP); } } void onPackageRemoved(final int uid, @NonNull final String pkgName) { final int userId = UserHandle.getUserId(uid); synchronized (mPackageToUidCache) { mPackageToUidCache.delete(userId, pkgName); } synchronized (mLock) { mUidToPackageCache.remove(uid, pkgName); mVipOverrides.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); } } void onUidStateChanged(final int uid) { synchronized (mLock) { final ArraySet pkgNames = getPackagesForUidLocked(uid); if (pkgNames == null) { Slog.e(TAG, "Don't have packages for uid " + uid); } else { mAgent.onAppStatesChangedLocked(UserHandle.getUserId(uid), pkgNames); } } } void onUserAdded(final int userId) { synchronized (mLock) { final List pkgs = mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId); for (int i = pkgs.size() - 1; i >= 0; --i) { final InstalledPackageInfo ipo = new InstalledPackageInfo(getContext(), userId, pkgs.get(i)); final InstalledPackageInfo oldIpo = mPkgCache.add(userId, ipo.packageName, ipo); maybeUpdateInstallerStatusLocked(oldIpo, ipo); } mAgent.grantBirthrightsLocked(userId); final long nowElapsed = SystemClock.elapsedRealtime(); mScribe.setUserAddedTimeLocked(userId, nowElapsed); grantInstallersTemporaryVipStatusLocked(userId, nowElapsed, INSTALLER_FIRST_SETUP_GRACE_PERIOD_MS); } } void onUserRemoved(final int userId) { synchronized (mLock) { mVipOverrides.delete(userId); final int uIdx = mPkgCache.indexOfKey(userId); if (uIdx >= 0) { for (int p = mPkgCache.numElementsForKeyAt(uIdx) - 1; p >= 0; --p) { final InstalledPackageInfo pkgInfo = mPkgCache.valueAt(uIdx, p); mUidToPackageCache.remove(pkgInfo.uid); } } mInstallers.delete(userId); mPkgCache.delete(userId); mAgent.onUserRemovedLocked(userId); mScribe.onUserRemovedLocked(userId); } } /** * Try to increase the consumption limit if apps are reaching the current limit too quickly. */ @GuardedBy("mLock") void maybePerformQuantitativeEasingLocked() { if (mConfigObserver.ENABLE_TIP3) { maybeAdjustDesiredStockLevelLocked(); return; } if (getRealtimeSinceFirstSetupMs() < STOCK_ADJUSTMENT_FIRST_SETUP_GRACE_PERIOD_MS) { // Things can be very tumultuous soon after first setup. return; } // We don't need to increase the limit if the device runs out of consumable credits // when the battery is low. final long remainingConsumableCakes = mScribe.getRemainingConsumableCakesLocked(); if (mCurrentBatteryLevel <= QUANTITATIVE_EASING_BATTERY_THRESHOLD || remainingConsumableCakes > 0) { return; } final long currentConsumptionLimit = mScribe.getSatiatedConsumptionLimitLocked(); final long shortfall = (mCurrentBatteryLevel - QUANTITATIVE_EASING_BATTERY_THRESHOLD) * currentConsumptionLimit / 100; final long newConsumptionLimit = Math.min(currentConsumptionLimit + shortfall, mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()); if (newConsumptionLimit != currentConsumptionLimit) { Slog.i(TAG, "Increasing consumption limit from " + cakeToString(currentConsumptionLimit) + " to " + cakeToString(newConsumptionLimit)); mScribe.setConsumptionLimitLocked(newConsumptionLimit); adjustCreditSupplyLocked(/* allowIncrease */ true); } } /** * Adjust the consumption limit based on historical data and the target battery drain. */ @GuardedBy("mLock") void maybeAdjustDesiredStockLevelLocked() { if (!mConfigObserver.ENABLE_TIP3) { return; } if (getRealtimeSinceFirstSetupMs() < STOCK_ADJUSTMENT_FIRST_SETUP_GRACE_PERIOD_MS) { // Things can be very tumultuous soon after first setup. return; } // Don't adjust the limit too often or while the battery is low. final long now = getCurrentTimeMillis(); if ((now - mScribe.getLastStockRecalculationTimeLocked()) < STOCK_RECALCULATION_DELAY_MS || mCurrentBatteryLevel <= STOCK_RECALCULATION_BATTERY_THRESHOLD) { return; } // For now, use screen off battery drain as a proxy for background battery drain. // TODO: get more accurate background battery drain numbers final long totalScreenOffDurationMs = mAnalyst.getBatteryScreenOffDurationMs(); if (totalScreenOffDurationMs < STOCK_RECALCULATION_MIN_DATA_DURATION_MS) { return; } final long totalDischargeMah = mAnalyst.getBatteryScreenOffDischargeMah(); if (totalDischargeMah == 0) { Slog.i(TAG, "Total discharge was 0"); return; } final long batteryCapacityMah = mBatteryManagerInternal.getBatteryFullCharge() / 1000; final long estimatedLifeHours = batteryCapacityMah * totalScreenOffDurationMs / totalDischargeMah / HOUR_IN_MILLIS; final long percentageOfTarget = 100 * estimatedLifeHours / mTargetBackgroundBatteryLifeHours; if (DEBUG) { Slog.d(TAG, "maybeAdjustDesiredStockLevelLocked:" + " screenOffMs=" + totalScreenOffDurationMs + " dischargeMah=" + totalDischargeMah + " capacityMah=" + batteryCapacityMah + " estimatedLifeHours=" + estimatedLifeHours + " %ofTarget=" + percentageOfTarget); } final long currentConsumptionLimit = mScribe.getSatiatedConsumptionLimitLocked(); final long newConsumptionLimit; if (percentageOfTarget > 105) { // The stock is too low. We're doing pretty well. We can increase the stock slightly // to let apps do more work in the background. newConsumptionLimit = Math.min((long) (currentConsumptionLimit * 1.01), mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()); } else if (percentageOfTarget < 100) { // The stock is too high IMO. We're below the target. Decrease the stock to reduce // background work. newConsumptionLimit = Math.max((long) (currentConsumptionLimit * .98), mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit()); } else { // The stock is just right. return; } // TODO(250007191): calculate and log implied service level if (newConsumptionLimit != currentConsumptionLimit) { Slog.i(TAG, "Adjusting consumption limit from " + cakeToString(currentConsumptionLimit) + " to " + cakeToString(newConsumptionLimit) + " because drain was " + percentageOfTarget + "% of target"); mScribe.setConsumptionLimitLocked(newConsumptionLimit); adjustCreditSupplyLocked(/* allowIncrease */ true); mScribe.setLastStockRecalculationTimeLocked(now); } } void postAffordabilityChanged(final int userId, @NonNull final String pkgName, @NonNull Agent.ActionAffordabilityNote affordabilityNote) { if (DEBUG) { Slog.d(TAG, userId + ":" + pkgName + " affordability changed to " + affordabilityNote.isCurrentlyAffordable()); } final SomeArgs args = SomeArgs.obtain(); args.argi1 = userId; args.arg1 = pkgName; args.arg2 = affordabilityNote; mHandler.obtainMessage(MSG_NOTIFY_AFFORDABILITY_CHANGE_LISTENER, args).sendToTarget(); } @GuardedBy("mLock") private void adjustCreditSupplyLocked(boolean allowIncrease) { final long newLimit = getConsumptionLimitLocked(); final long remainingConsumableCakes = mScribe.getRemainingConsumableCakesLocked(); if (remainingConsumableCakes == newLimit) { return; } if (remainingConsumableCakes > newLimit) { mScribe.adjustRemainingConsumableCakesLocked(newLimit - remainingConsumableCakes); } else if (allowIncrease) { final double perc = mCurrentBatteryLevel / 100d; final long shortfall = newLimit - remainingConsumableCakes; mScribe.adjustRemainingConsumableCakesLocked((long) (perc * shortfall)); } mAgent.onCreditSupplyChanged(); } @GuardedBy("mLock") private void grantInstallersTemporaryVipStatusLocked(int userId, long nowElapsed, long grantDurationMs) { final long grantEndTimeElapsed = nowElapsed + grantDurationMs; final int uIdx = mPkgCache.indexOfKey(userId); if (uIdx < 0) { return; } for (int pIdx = mPkgCache.numElementsForKey(uIdx) - 1; pIdx >= 0; --pIdx) { final InstalledPackageInfo ipo = mPkgCache.valueAt(uIdx, pIdx); if (ipo.isSystemInstaller) { final Long currentGrantEndTimeElapsed = mTemporaryVips.get(userId, ipo.packageName); if (currentGrantEndTimeElapsed == null || currentGrantEndTimeElapsed < grantEndTimeElapsed) { mTemporaryVips.add(userId, ipo.packageName, grantEndTimeElapsed); } } } mHandler.sendEmptyMessageDelayed(MSG_CLEAN_UP_TEMP_VIP_LIST, grantDurationMs); } @GuardedBy("mLock") private void processUsageEventLocked(final int userId, @NonNull UsageEvents.Event event) { if (mEnabledMode == ENABLED_MODE_OFF) { return; } final String pkgName = event.getPackageName(); if (DEBUG) { Slog.d(TAG, "Processing event " + event.getEventType() + " (" + event.mInstanceId + ")" + " for " + appToString(userId, pkgName)); } final long nowElapsed = SystemClock.elapsedRealtime(); switch (event.getEventType()) { case UsageEvents.Event.ACTIVITY_RESUMED: mAgent.noteOngoingEventLocked(userId, pkgName, EconomicPolicy.REWARD_TOP_ACTIVITY, String.valueOf(event.mInstanceId), nowElapsed); break; case UsageEvents.Event.ACTIVITY_PAUSED: case UsageEvents.Event.ACTIVITY_STOPPED: case UsageEvents.Event.ACTIVITY_DESTROYED: final long now = getCurrentTimeMillis(); mAgent.stopOngoingActionLocked(userId, pkgName, EconomicPolicy.REWARD_TOP_ACTIVITY, String.valueOf(event.mInstanceId), nowElapsed, now); break; case UsageEvents.Event.USER_INTERACTION: case UsageEvents.Event.CHOOSER_ACTION: mAgent.noteInstantaneousEventLocked(userId, pkgName, EconomicPolicy.REWARD_OTHER_USER_INTERACTION, null); break; case UsageEvents.Event.NOTIFICATION_INTERRUPTION: case UsageEvents.Event.NOTIFICATION_SEEN: mAgent.noteInstantaneousEventLocked(userId, pkgName, EconomicPolicy.REWARD_NOTIFICATION_SEEN, null); break; } } @GuardedBy("mLock") private void scheduleUnusedWealthReclamationLocked() { final long now = getCurrentTimeMillis(); final long nextReclamationTime = Math.max(now + RECLAMATION_STARTUP_DELAY_MS, mScribe.getLastReclamationTimeLocked() + UNUSED_RECLAMATION_PERIOD_MS); 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, RECLAMATION_STARTUP_DELAY_MS); } }); } private int getCurrentBatteryLevel() { return mBatteryManagerInternal.getBatteryLevel(); } @Nullable @GuardedBy("mLock") private ArraySet getPackagesForUidLocked(final int uid) { ArraySet packages = mUidToPackageCache.get(uid); if (packages == null) { final String[] pkgs = mPackageManager.getPackagesForUid(uid); if (pkgs != null) { for (String pkg : pkgs) { mUidToPackageCache.add(uid, pkg); } packages = mUidToPackageCache.get(uid); } } return packages; } private boolean isTareSupported() { // TARE is presently designed for devices with batteries. Don't enable it on // battery-less devices for now. return mHasBattery; } @GuardedBy("mLock") private void loadInstalledPackageListLocked() { mPkgCache.clear(); final UserManagerInternal userManagerInternal = LocalServices.getService(UserManagerInternal.class); final int[] userIds = userManagerInternal.getUserIds(); for (int userId : userIds) { final List pkgs = mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId); for (int i = pkgs.size() - 1; i >= 0; --i) { final InstalledPackageInfo ipo = new InstalledPackageInfo(getContext(), userId, pkgs.get(i)); 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); filter.addAction(Intent.ACTION_BATTERY_LEVEL_CHANGED); filter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null); final IntentFilter pkgFilter = new IntentFilter(); pkgFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED); pkgFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED); pkgFilter.addDataScheme("package"); getContext() .registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, pkgFilter, null, null); final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED); userFilter.addAction(Intent.ACTION_USER_ADDED); getContext() .registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, userFilter, null, null); UsageStatsManagerInternal usmi = LocalServices.getService(UsageStatsManagerInternal.class); usmi.registerListener(mSurveillanceAgent); try { mAppOpsService .startWatchingMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, null, mApbListener); } catch (RemoteException e) { // shouldn't happen. } } /** Perform long-running and/or heavy setup work. This should be called off the main thread. */ private void setupHeavyWork() { if (mBootPhase < PHASE_THIRD_PARTY_APPS_CAN_START || mEnabledMode == ENABLED_MODE_OFF) { return; } synchronized (mLock) { mCompleteEconomicPolicy.setup(mConfigObserver.getAllDeviceConfigProperties()); loadInstalledPackageListLocked(); final SparseLongArray timeSinceUsersAdded; final boolean isFirstSetup = !mScribe.recordExists(); final long nowElapsed = SystemClock.elapsedRealtime(); if (isFirstSetup) { mAgent.grantBirthrightsLocked(); mScribe.setConsumptionLimitLocked( mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()); // Set the last reclamation time to now so we don't start reclaiming assets // too early. mScribe.setLastReclamationTimeLocked(getCurrentTimeMillis()); timeSinceUsersAdded = new SparseLongArray(); } else { mScribe.loadFromDiskLocked(); if (mScribe.getSatiatedConsumptionLimitLocked() < mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit() || mScribe.getSatiatedConsumptionLimitLocked() > mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()) { // Reset the consumption limit since several factors may have changed. mScribe.setConsumptionLimitLocked( mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()); } else { // Adjust the supply in case battery level changed while the device was off. adjustCreditSupplyLocked(true); } timeSinceUsersAdded = mScribe.getRealtimeSinceUsersAddedLocked(nowElapsed); } final int[] userIds = LocalServices.getService(UserManagerInternal.class).getUserIds(); for (int userId : userIds) { final long timeSinceUserAddedMs = timeSinceUsersAdded.get(userId, 0); // Temporarily mark installers as VIPs so they aren't subject to credit // limits and policies on first boot. if (timeSinceUserAddedMs < INSTALLER_FIRST_SETUP_GRACE_PERIOD_MS) { final long remainingGraceDurationMs = INSTALLER_FIRST_SETUP_GRACE_PERIOD_MS - timeSinceUserAddedMs; grantInstallersTemporaryVipStatusLocked(userId, nowElapsed, remainingGraceDurationMs); } } scheduleUnusedWealthReclamationLocked(); } } private void onBootPhaseSystemServicesReady() { if (mBootPhase < PHASE_SYSTEM_SERVICES_READY || mEnabledMode == ENABLED_MODE_OFF) { return; } synchronized (mLock) { registerListeners(); // As of Android UDC, users can't change the wellbeing package, so load it once // as soon as possible and don't bother trying to update it afterwards. mWellbeingPackage = mPackageManager.getWellbeingPackageName(); mCurrentBatteryLevel = getCurrentBatteryLevel(); // Get the current battery presence, if available. This would succeed if TARE is // toggled long after boot. final Intent batteryStatus = getContext().registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); if (batteryStatus != null) { final boolean hasBattery = batteryStatus.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); if (mHasBattery != hasBattery) { mHasBattery = hasBattery; mConfigObserver.updateEnabledStatus(); } } } } private void onBootPhaseThirdPartyAppsCanStart() { if (mBootPhase < PHASE_THIRD_PARTY_APPS_CAN_START || mEnabledMode == ENABLED_MODE_OFF) { return; } mHandler.post(this::setupHeavyWork); } private void onBootPhaseBootCompleted() { if (mBootPhase < PHASE_BOOT_COMPLETED || mEnabledMode == ENABLED_MODE_OFF) { return; } synchronized (mLock) { if (!mExemptListLoaded) { try { mExemptedApps = new ArraySet<>(mDeviceIdleController.getFullPowerWhitelist()); mExemptListLoaded = true; } catch (RemoteException e) { // Shouldn't happen. } } } } private void setupEverything() { if (mEnabledMode == ENABLED_MODE_OFF) { return; } if (mBootPhase >= PHASE_SYSTEM_SERVICES_READY) { onBootPhaseSystemServicesReady(); } if (mBootPhase >= PHASE_THIRD_PARTY_APPS_CAN_START) { onBootPhaseThirdPartyAppsCanStart(); } if (mBootPhase >= PHASE_BOOT_COMPLETED) { onBootPhaseBootCompleted(); } } private void tearDownEverything() { if (mEnabledMode != ENABLED_MODE_OFF) { return; } synchronized (mLock) { mAgent.tearDownLocked(); mAnalyst.tearDown(); mCompleteEconomicPolicy.tearDown(); mExemptedApps.clear(); mExemptListLoaded = false; 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.cancel(mUnusedWealthReclamationListener); } }); mPkgCache.clear(); mScribe.tearDownLocked(); mUidToPackageCache.clear(); getContext().unregisterReceiver(mBroadcastReceiver); UsageStatsManagerInternal usmi = LocalServices.getService(UsageStatsManagerInternal.class); usmi.unregisterListener(mSurveillanceAgent); try { mAppOpsService.stopWatchingMode(mApbListener); } catch (RemoteException e) { // shouldn't happen. } } synchronized (mPackageToUidCache) { mPackageToUidCache.clear(); } } private final class IrsHandler extends Handler { IrsHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_CLEAN_UP_TEMP_VIP_LIST: { removeMessages(MSG_CLEAN_UP_TEMP_VIP_LIST); synchronized (mLock) { final long nowElapsed = SystemClock.elapsedRealtime(); long earliestExpiration = Long.MAX_VALUE; for (int u = 0; u < mTemporaryVips.numMaps(); ++u) { final int userId = mTemporaryVips.keyAt(u); for (int p = mTemporaryVips.numElementsForKeyAt(u) - 1; p >= 0; --p) { final String pkgName = mTemporaryVips.keyAt(u, p); final Long expiration = mTemporaryVips.valueAt(u, p); if (expiration == null || expiration < nowElapsed) { mTemporaryVips.delete(userId, pkgName); } else { earliestExpiration = Math.min(earliestExpiration, expiration); } } } if (earliestExpiration < Long.MAX_VALUE) { sendEmptyMessageDelayed(MSG_CLEAN_UP_TEMP_VIP_LIST, earliestExpiration - nowElapsed); } } } break; case MSG_NOTIFY_AFFORDABILITY_CHANGE_LISTENER: { final SomeArgs args = (SomeArgs) msg.obj; final int userId = args.argi1; final String pkgName = (String) args.arg1; final Agent.ActionAffordabilityNote affordabilityNote = (Agent.ActionAffordabilityNote) args.arg2; final EconomyManagerInternal.AffordabilityChangeListener listener = affordabilityNote.getListener(); listener.onAffordabilityChanged(userId, pkgName, affordabilityNote.getActionBill(), affordabilityNote.isCurrentlyAffordable()); args.recycle(); } break; case MSG_NOTIFY_STATE_CHANGE_LISTENER: { final int policy = msg.arg1; final TareStateChangeListener listener = (TareStateChangeListener) msg.obj; listener.onTareEnabledModeChanged(getEnabledMode(policy)); } break; case MSG_NOTIFY_STATE_CHANGE_LISTENERS: { final int changedPolicies = msg.arg1; synchronized (mStateChangeListeners) { final int size = mStateChangeListeners.size(); for (int l = 0; l < size; ++l) { final int policy = mStateChangeListeners.keyAt(l); if ((policy & changedPolicies) == 0) { continue; } final ArraySet listeners = mStateChangeListeners.get(policy); final int enabledMode = getEnabledMode(policy); for (int p = listeners.size() - 1; p >= 0; --p) { final TareStateChangeListener listener = listeners.valueAt(p); listener.onTareEnabledModeChanged(enabledMode); } } } } break; case MSG_PROCESS_USAGE_EVENT: { final int userId = msg.arg1; final UsageEvents.Event event = (UsageEvents.Event) msg.obj; synchronized (mLock) { processUsageEventLocked(userId, event); } } break; case MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT: { removeMessages(MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT); synchronized (mLock) { scheduleUnusedWealthReclamationLocked(); } } break; } } } /** * Binder stub trampoline implementation */ final class EconomyManagerStub extends IEconomyManager.Stub { /** * "dumpsys" infrastructure */ @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return; boolean dumpAll = true; if (!ArrayUtils.isEmpty(args)) { String arg = args[0]; if ("-h".equals(arg) || "--help".equals(arg)) { dumpHelp(pw); return; } else if ("-a".equals(arg)) { // -a is passed when dumping a bug report. Bug reports have a time limit for // each service dump, so we can't dump everything. dumpAll = false; } else if (arg.length() > 0 && arg.charAt(0) == '-') { pw.println("Unknown option: " + arg); return; } } final long identityToken = Binder.clearCallingIdentity(); try { dumpInternal(new IndentingPrintWriter(pw, " "), dumpAll); } finally { Binder.restoreCallingIdentity(identityToken); } } @Override @EconomyManager.EnabledMode public int getEnabledMode() { return InternalResourceService.this.getEnabledMode(); } @Override public int handleShellCommand(@NonNull ParcelFileDescriptor in, @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, @NonNull String[] args) { return (new TareShellCommand(InternalResourceService.this)).exec( this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args); } } private final class LocalService implements EconomyManagerInternal { /** * Use an extremely large value to indicate that an app can pay for a bill indefinitely. * The value set here should be large/long enough that there's no reasonable expectation * of a device operating uninterrupted (or in the exact same state) for that period of time. * We intentionally don't use Long.MAX_VALUE to avoid potential overflow if a client * doesn't check the value and just immediately adds it to the current time. */ private static final long FOREVER_MS = 27 * 365 * 24 * HOUR_IN_MILLIS; @Override public void registerAffordabilityChangeListener(int userId, @NonNull String pkgName, @NonNull AffordabilityChangeListener listener, @NonNull ActionBill bill) { if (!isTareSupported() || isSystem(userId, pkgName)) { // The system's affordability never changes. return; } synchronized (mLock) { mAgent.registerAffordabilityChangeListenerLocked(userId, pkgName, listener, bill); } } @Override public void unregisterAffordabilityChangeListener(int userId, @NonNull String pkgName, @NonNull AffordabilityChangeListener listener, @NonNull ActionBill bill) { if (isSystem(userId, pkgName)) { // The system's affordability never changes. return; } synchronized (mLock) { mAgent.unregisterAffordabilityChangeListenerLocked(userId, pkgName, listener, bill); } } @Override public void registerTareStateChangeListener(@NonNull TareStateChangeListener listener, int policyId) { if (!isTareSupported()) { return; } synchronized (mStateChangeListeners) { if (mStateChangeListeners.add(policyId, listener)) { mHandler.obtainMessage(MSG_NOTIFY_STATE_CHANGE_LISTENER, policyId, 0, listener) .sendToTarget(); } } } @Override public void unregisterTareStateChangeListener(@NonNull TareStateChangeListener listener) { synchronized (mStateChangeListeners) { for (int i = mStateChangeListeners.size() - 1; i >= 0; --i) { final ArraySet listeners = mStateChangeListeners.get(mStateChangeListeners.keyAt(i)); listeners.remove(listener); } } } @Override public boolean canPayFor(int userId, @NonNull String pkgName, @NonNull ActionBill bill) { if (mEnabledMode == ENABLED_MODE_OFF) { return true; } if (isVip(userId, pkgName)) { // The government, I mean the system, can create ARCs as it needs to in order to // allow VIPs to operate. return true; } // TODO: take temp-allowlist into consideration long requiredBalance = 0; final List projectedActions = bill.getAnticipatedActions(); synchronized (mLock) { for (int i = 0; i < projectedActions.size(); ++i) { AnticipatedAction action = projectedActions.get(i); final Cost cost = mCompleteEconomicPolicy.getCostOfAction( action.actionId, userId, pkgName); requiredBalance += cost.price * action.numInstantaneousCalls + cost.price * (action.ongoingDurationMs / 1000); } return mAgent.getBalanceLocked(userId, pkgName) >= requiredBalance && mScribe.getRemainingConsumableCakesLocked() >= requiredBalance; } } @Override public long getMaxDurationMs(int userId, @NonNull String pkgName, @NonNull ActionBill bill) { if (mEnabledMode == ENABLED_MODE_OFF) { return FOREVER_MS; } if (isVip(userId, pkgName)) { return FOREVER_MS; } long totalCostPerSecond = 0; final List projectedActions = bill.getAnticipatedActions(); synchronized (mLock) { for (int i = 0; i < projectedActions.size(); ++i) { AnticipatedAction action = projectedActions.get(i); final Cost cost = mCompleteEconomicPolicy.getCostOfAction( action.actionId, userId, pkgName); totalCostPerSecond += cost.price; } if (totalCostPerSecond == 0) { return FOREVER_MS; } final long minBalance = Math.min( mAgent.getBalanceLocked(userId, pkgName), mScribe.getRemainingConsumableCakesLocked()); return minBalance * 1000 / totalCostPerSecond; } } @Override public int getEnabledMode() { return mEnabledMode; } @Override public int getEnabledMode(int policyId) { return InternalResourceService.this.getEnabledMode(policyId); } @Override public void noteInstantaneousEvent(int userId, @NonNull String pkgName, int eventId, @Nullable String tag) { if (mEnabledMode == ENABLED_MODE_OFF) { return; } synchronized (mLock) { mAgent.noteInstantaneousEventLocked(userId, pkgName, eventId, tag); } } @Override public void noteOngoingEventStarted(int userId, @NonNull String pkgName, int eventId, @Nullable String tag) { if (mEnabledMode == ENABLED_MODE_OFF) { return; } synchronized (mLock) { final long nowElapsed = SystemClock.elapsedRealtime(); mAgent.noteOngoingEventLocked(userId, pkgName, eventId, tag, nowElapsed); } } @Override public void noteOngoingEventStopped(int userId, @NonNull String pkgName, int eventId, @Nullable String tag) { if (mEnabledMode == ENABLED_MODE_OFF) { return; } final long nowElapsed = SystemClock.elapsedRealtime(); final long now = getCurrentTimeMillis(); synchronized (mLock) { mAgent.stopOngoingActionLocked(userId, pkgName, eventId, tag, nowElapsed, now); } } } private class ConfigObserver extends ContentObserver implements DeviceConfig.OnPropertiesChangedListener { private static final String KEY_ENABLE_TIP3 = "enable_tip3"; private static final String KEY_TARGET_BACKGROUND_BATTERY_LIFE_HOURS = "target_bg_battery_life_hrs"; private static final boolean DEFAULT_ENABLE_TIP3 = true; /** Use a target background battery drain rate to determine consumption limits. */ public boolean ENABLE_TIP3 = DEFAULT_ENABLE_TIP3; private final ContentResolver mContentResolver; ConfigObserver(Handler handler, Context context) { super(handler); mContentResolver = context.getContentResolver(); } public void start() { DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_TARE, TareHandlerThread.getExecutor(), this); mContentResolver.registerContentObserver( Settings.Global.getUriFor(Settings.Global.ENABLE_TARE), false, this); mContentResolver.registerContentObserver( Settings.Global.getUriFor(TARE_ALARM_MANAGER_CONSTANTS), false, this); mContentResolver.registerContentObserver( Settings.Global.getUriFor(TARE_JOB_SCHEDULER_CONSTANTS), false, this); onPropertiesChanged(getAllDeviceConfigProperties()); updateEnabledStatus(); } @NonNull DeviceConfig.Properties getAllDeviceConfigProperties() { // Don't want to cache the Properties object locally in case it ends up being large, // especially since it'll only be used once/infrequently (during setup or on a change). return DeviceConfig.getProperties(DeviceConfig.NAMESPACE_TARE); } @Override public void onChange(boolean selfChange, Uri uri) { if (uri.equals(Settings.Global.getUriFor(Settings.Global.ENABLE_TARE))) { updateEnabledStatus(); } else if (uri.equals(Settings.Global.getUriFor(TARE_ALARM_MANAGER_CONSTANTS)) || uri.equals(Settings.Global.getUriFor(TARE_JOB_SCHEDULER_CONSTANTS))) { updateEconomicPolicy(); } } @Override public void onPropertiesChanged(DeviceConfig.Properties properties) { boolean economicPolicyUpdated = false; synchronized (mLock) { for (String name : properties.getKeyset()) { if (name == null) { continue; } switch (name) { case EconomyManager.KEY_ENABLE_TARE_MODE: updateEnabledStatus(); break; case KEY_ENABLE_TIP3: ENABLE_TIP3 = properties.getBoolean(name, DEFAULT_ENABLE_TIP3); break; case KEY_TARGET_BACKGROUND_BATTERY_LIFE_HOURS: synchronized (mLock) { mTargetBackgroundBatteryLifeHours = properties.getInt(name, mDefaultTargetBackgroundBatteryLifeHours); maybeAdjustDesiredStockLevelLocked(); } break; default: if (!economicPolicyUpdated && (name.startsWith("am") || name.startsWith("js") || name.startsWith("enable_policy"))) { updateEconomicPolicy(); economicPolicyUpdated = true; } } } } } private void updateEnabledStatus() { // User setting should override DeviceConfig setting. final int tareEnabledModeDC = DeviceConfig.getInt(DeviceConfig.NAMESPACE_TARE, EconomyManager.KEY_ENABLE_TARE_MODE, EconomyManager.DEFAULT_ENABLE_TARE_MODE); final int tareEnabledModeConfig = isTareSupported() ? Settings.Global.getInt(mContentResolver, Settings.Global.ENABLE_TARE, tareEnabledModeDC) : ENABLED_MODE_OFF; final int enabledMode; if (tareEnabledModeConfig == ENABLED_MODE_OFF || tareEnabledModeConfig == ENABLED_MODE_ON || tareEnabledModeConfig == ENABLED_MODE_SHADOW) { // Config has a valid enabled mode. enabledMode = tareEnabledModeConfig; } else { enabledMode = EconomyManager.DEFAULT_ENABLE_TARE_MODE; } if (mEnabledMode != enabledMode) { // A full change where we've gone from OFF to {SHADOW or ON}, or vie versa. // With this transition, we'll have to set up or tear down. final boolean fullEnableChange = mEnabledMode == ENABLED_MODE_OFF || enabledMode == ENABLED_MODE_OFF; mEnabledMode = enabledMode; if (fullEnableChange) { if (mEnabledMode != ENABLED_MODE_OFF) { setupEverything(); } else { tearDownEverything(); } } mHandler.obtainMessage( MSG_NOTIFY_STATE_CHANGE_LISTENERS, EconomicPolicy.ALL_POLICIES, 0) .sendToTarget(); } } private void updateEconomicPolicy() { synchronized (mLock) { final long minLimit = mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit(); final long maxLimit = mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit(); final int oldEnabledPolicies = mCompleteEconomicPolicy.getEnabledPolicyIds(); mCompleteEconomicPolicy.tearDown(); mCompleteEconomicPolicy = new CompleteEconomicPolicy(InternalResourceService.this); if (mEnabledMode != ENABLED_MODE_OFF && mBootPhase >= PHASE_THIRD_PARTY_APPS_CAN_START) { mCompleteEconomicPolicy.setup(getAllDeviceConfigProperties()); if (minLimit != mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit() || maxLimit != mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()) { // Reset the consumption limit since several factors may have changed. mScribe.setConsumptionLimitLocked( mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()); } mAgent.onPricingChangedLocked(); final int newEnabledPolicies = mCompleteEconomicPolicy.getEnabledPolicyIds(); if (oldEnabledPolicies != newEnabledPolicies) { final int changedPolicies = oldEnabledPolicies ^ newEnabledPolicies; mHandler.obtainMessage( MSG_NOTIFY_STATE_CHANGE_LISTENERS, changedPolicies, 0) .sendToTarget(); } } } } } // Shell command infrastructure int executeClearVip(@NonNull PrintWriter pw) { synchronized (mLock) { final SparseSetArray changedPkgs = new SparseSetArray<>(); for (int u = mVipOverrides.numMaps() - 1; u >= 0; --u) { final int userId = mVipOverrides.keyAt(u); for (int p = mVipOverrides.numElementsForKeyAt(u) - 1; p >= 0; --p) { changedPkgs.add(userId, mVipOverrides.keyAt(u, p)); } } mVipOverrides.clear(); if (mEnabledMode != ENABLED_MODE_OFF) { mAgent.onVipStatusChangedLocked(changedPkgs); } } pw.println("Cleared all VIP statuses"); return TareShellCommand.COMMAND_SUCCESS; } int executeSetVip(@NonNull PrintWriter pw, int userId, @NonNull String pkgName, @Nullable Boolean newVipState) { final boolean changed; synchronized (mLock) { final boolean wasVip = isVip(userId, pkgName); if (newVipState == null) { mVipOverrides.delete(userId, pkgName); } else { mVipOverrides.add(userId, pkgName, newVipState); } changed = isVip(userId, pkgName) != wasVip; if (mEnabledMode != ENABLED_MODE_OFF && changed) { mAgent.onVipStatusChangedLocked(userId, pkgName); } } pw.println(appToString(userId, pkgName) + " VIP status set to " + newVipState + "." + " Final VIP state changed? " + changed); return TareShellCommand.COMMAND_SUCCESS; } // Dump infrastructure private static void dumpHelp(PrintWriter pw) { pw.println("Resource Economy (economy) dump options:"); pw.println(" [-h|--help] [package] ..."); pw.println(" -h | --help: print this help"); pw.println(" [package] is an optional package name to limit the output to."); } private void dumpInternal(final IndentingPrintWriter pw, final boolean dumpAll) { if (!isTareSupported()) { pw.print("Unsupported by device"); return; } synchronized (mLock) { pw.print("Enabled mode: "); pw.println(enabledModeToString(mEnabledMode)); pw.print("Current battery level: "); pw.println(mCurrentBatteryLevel); final long consumptionLimit = getConsumptionLimitLocked(); pw.print("Consumption limit (current/initial-satiated/current-satiated): "); pw.print(cakeToString(consumptionLimit)); pw.print("/"); pw.print(cakeToString(mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit())); pw.print("/"); pw.println(cakeToString(mScribe.getSatiatedConsumptionLimitLocked())); pw.print("Target bg battery life (hours): "); pw.print(mTargetBackgroundBatteryLifeHours); pw.print(" ("); pw.print(String.format("%.2f", 100f / mTargetBackgroundBatteryLifeHours)); pw.println("%/hr)"); final long remainingConsumable = mScribe.getRemainingConsumableCakesLocked(); pw.print("Goods remaining: "); pw.print(cakeToString(remainingConsumable)); pw.print(" ("); pw.print(String.format("%.2f", 100f * remainingConsumable / consumptionLimit)); pw.println("% of current limit)"); pw.print("Device wealth: "); pw.println(cakeToString(mScribe.getCakesInCirculationForLoggingLocked())); pw.println(); pw.print("Exempted apps", mExemptedApps); pw.println(); pw.println(); pw.print("Wellbeing app="); pw.println(mWellbeingPackage == null ? "None" : mWellbeingPackage); boolean printedVips = false; pw.println(); pw.print("VIPs:"); pw.increaseIndent(); for (int u = 0; u < mVipOverrides.numMaps(); ++u) { final int userId = mVipOverrides.keyAt(u); for (int p = 0; p < mVipOverrides.numElementsForKeyAt(u); ++p) { final String pkgName = mVipOverrides.keyAt(u, p); printedVips = true; pw.println(); pw.print(appToString(userId, pkgName)); pw.print("="); pw.print(mVipOverrides.valueAt(u, p)); } } if (printedVips) { pw.println(); } else { pw.print(" None"); } pw.decreaseIndent(); pw.println(); boolean printedTempVips = false; pw.println(); pw.print("Temp VIPs:"); pw.increaseIndent(); for (int u = 0; u < mTemporaryVips.numMaps(); ++u) { final int userId = mTemporaryVips.keyAt(u); for (int p = 0; p < mTemporaryVips.numElementsForKeyAt(u); ++p) { final String pkgName = mTemporaryVips.keyAt(u, p); printedTempVips = true; pw.println(); pw.print(appToString(userId, pkgName)); pw.print("="); pw.print(mTemporaryVips.valueAt(u, p)); } } if (printedTempVips) { pw.println(); } else { pw.print(" None"); } pw.decreaseIndent(); 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); pw.println(); mScribe.dumpLocked(pw, dumpAll); pw.println(); mAgent.dumpLocked(pw); pw.println(); mAnalyst.dump(pw); // Put this at the end since this may be a lot and we want to have the earlier // information easily accessible. boolean printedInterestingIpos = false; pw.println(); pw.print("Interesting apps:"); pw.increaseIndent(); for (int u = 0; u < mPkgCache.numMaps(); ++u) { for (int p = 0; p < mPkgCache.numElementsForKeyAt(u); ++p) { final InstalledPackageInfo ipo = mPkgCache.valueAt(u, p); // Printing out every single app will be too much. Only print apps that // have some interesting characteristic. final boolean isInteresting = ipo.hasCode && ipo.isHeadlessSystemApp && !UserHandle.isCore(ipo.uid); if (!isInteresting) { continue; } printedInterestingIpos = true; pw.println(); pw.print(ipo); } } if (printedInterestingIpos) { pw.println(); } else { pw.print(" None"); } pw.decreaseIndent(); } } }