1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.tare; 18 19 import static android.text.format.DateUtils.DAY_IN_MILLIS; 20 21 import static com.android.server.tare.EconomicPolicy.REGULATION_BASIC_INCOME; 22 import static com.android.server.tare.EconomicPolicy.REGULATION_BIRTHRIGHT; 23 import static com.android.server.tare.EconomicPolicy.REGULATION_DEMOTION; 24 import static com.android.server.tare.EconomicPolicy.REGULATION_PROMOTION; 25 import static com.android.server.tare.EconomicPolicy.REGULATION_WEALTH_RECLAMATION; 26 import static com.android.server.tare.EconomicPolicy.TYPE_ACTION; 27 import static com.android.server.tare.EconomicPolicy.TYPE_REWARD; 28 import static com.android.server.tare.EconomicPolicy.eventToString; 29 import static com.android.server.tare.EconomicPolicy.getEventType; 30 import static com.android.server.tare.TareUtils.appToString; 31 import static com.android.server.tare.TareUtils.cakeToString; 32 import static com.android.server.tare.TareUtils.getCurrentTimeMillis; 33 34 import android.annotation.NonNull; 35 import android.annotation.Nullable; 36 import android.content.Context; 37 import android.content.pm.ApplicationInfo; 38 import android.content.pm.PackageInfo; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.os.Message; 42 import android.os.SystemClock; 43 import android.os.UserHandle; 44 import android.util.ArraySet; 45 import android.util.IndentingPrintWriter; 46 import android.util.Log; 47 import android.util.Slog; 48 import android.util.SparseArrayMap; 49 import android.util.TimeUtils; 50 51 import com.android.internal.annotations.GuardedBy; 52 import com.android.internal.annotations.VisibleForTesting; 53 import com.android.server.LocalServices; 54 import com.android.server.pm.UserManagerInternal; 55 import com.android.server.usage.AppStandbyInternal; 56 import com.android.server.utils.AlarmQueue; 57 58 import java.util.List; 59 import java.util.Objects; 60 import java.util.function.Consumer; 61 62 /** 63 * Other half of the IRS. The agent handles the nitty gritty details, interacting directly with 64 * ledgers, carrying out specific events such as wealth reclamation, granting initial balances or 65 * replenishing balances, and tracking ongoing events. 66 */ 67 class Agent { 68 private static final String TAG = "TARE-" + Agent.class.getSimpleName(); 69 private static final boolean DEBUG = InternalResourceService.DEBUG 70 || Log.isLoggable(TAG, Log.DEBUG); 71 72 private static final String ALARM_TAG_AFFORDABILITY_CHECK = "*tare.affordability_check*"; 73 74 private final Object mLock; 75 private final Handler mHandler; 76 private final Analyst mAnalyst; 77 private final InternalResourceService mIrs; 78 private final Scribe mScribe; 79 80 private final AppStandbyInternal mAppStandbyInternal; 81 82 @GuardedBy("mLock") 83 private final SparseArrayMap<String, SparseArrayMap<String, OngoingEvent>> 84 mCurrentOngoingEvents = new SparseArrayMap<>(); 85 86 /** 87 * Set of {@link ActionAffordabilityNote ActionAffordabilityNotes} keyed by userId-pkgName. 88 * 89 * Note: it would be nice/better to sort by base price since that doesn't change and simply 90 * look at the change in the "insertion" of what would be affordable, but since CTP 91 * is factored into the final price, the sorting order (by modified price) could be different 92 * and that method wouldn't work >:( 93 */ 94 @GuardedBy("mLock") 95 private final SparseArrayMap<String, ArraySet<ActionAffordabilityNote>> 96 mActionAffordabilityNotes = new SparseArrayMap<>(); 97 98 /** 99 * Queue to track and manage when apps will cross the closest affordability threshold (in 100 * both directions). 101 */ 102 @GuardedBy("mLock") 103 private final BalanceThresholdAlarmQueue mBalanceThresholdAlarmQueue; 104 105 /** 106 * Check the affordability notes of all apps. 107 */ 108 private static final int MSG_CHECK_ALL_AFFORDABILITY = 0; 109 /** 110 * Check the affordability notes of a single app. 111 */ 112 private static final int MSG_CHECK_INDIVIDUAL_AFFORDABILITY = 1; 113 Agent(@onNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst)114 Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst) { 115 mLock = irs.getLock(); 116 mIrs = irs; 117 mScribe = scribe; 118 mAnalyst = analyst; 119 mHandler = new AgentHandler(TareHandlerThread.get().getLooper()); 120 mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); 121 mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue( 122 mIrs.getContext(), TareHandlerThread.get().getLooper()); 123 } 124 125 private class TotalDeltaCalculator implements Consumer<OngoingEvent> { 126 private Ledger mLedger; 127 private long mNowElapsed; 128 private long mNow; 129 private long mTotal; 130 reset(@onNull Ledger ledger, long nowElapsed, long now)131 void reset(@NonNull Ledger ledger, long nowElapsed, long now) { 132 mLedger = ledger; 133 mNowElapsed = nowElapsed; 134 mNow = now; 135 mTotal = 0; 136 } 137 138 @Override accept(OngoingEvent ongoingEvent)139 public void accept(OngoingEvent ongoingEvent) { 140 mTotal += getActualDeltaLocked(ongoingEvent, mLedger, mNowElapsed, mNow).price; 141 } 142 } 143 144 @GuardedBy("mLock") 145 private final TotalDeltaCalculator mTotalDeltaCalculator = new TotalDeltaCalculator(); 146 147 /** Get an app's current balance, factoring in any currently ongoing events. */ 148 @GuardedBy("mLock") getBalanceLocked(final int userId, @NonNull final String pkgName)149 long getBalanceLocked(final int userId, @NonNull final String pkgName) { 150 final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); 151 long balance = ledger.getCurrentBalance(); 152 SparseArrayMap<String, OngoingEvent> ongoingEvents = 153 mCurrentOngoingEvents.get(userId, pkgName); 154 if (ongoingEvents != null) { 155 final long nowElapsed = SystemClock.elapsedRealtime(); 156 final long now = getCurrentTimeMillis(); 157 mTotalDeltaCalculator.reset(ledger, nowElapsed, now); 158 ongoingEvents.forEach(mTotalDeltaCalculator); 159 balance += mTotalDeltaCalculator.mTotal; 160 } 161 return balance; 162 } 163 164 @GuardedBy("mLock") isAffordableLocked(long balance, long price, long ctp)165 private boolean isAffordableLocked(long balance, long price, long ctp) { 166 return balance >= price && mScribe.getRemainingConsumableCakesLocked() >= ctp; 167 } 168 169 @GuardedBy("mLock") noteInstantaneousEventLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag)170 void noteInstantaneousEventLocked(final int userId, @NonNull final String pkgName, 171 final int eventId, @Nullable String tag) { 172 if (mIrs.isSystem(userId, pkgName)) { 173 // Events are free for the system. Don't bother recording them. 174 return; 175 } 176 177 final long now = getCurrentTimeMillis(); 178 final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); 179 final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); 180 181 final int eventType = getEventType(eventId); 182 switch (eventType) { 183 case TYPE_ACTION: 184 final EconomicPolicy.Cost actionCost = 185 economicPolicy.getCostOfAction(eventId, userId, pkgName); 186 187 recordTransactionLocked(userId, pkgName, ledger, 188 new Ledger.Transaction(now, now, eventId, tag, 189 -actionCost.price, actionCost.costToProduce), 190 true); 191 break; 192 193 case TYPE_REWARD: 194 final EconomicPolicy.Reward reward = economicPolicy.getReward(eventId); 195 if (reward != null) { 196 final long rewardSum = ledger.get24HourSum(eventId, now); 197 final long rewardVal = Math.max(0, 198 Math.min(reward.maxDailyReward - rewardSum, reward.instantReward)); 199 recordTransactionLocked(userId, pkgName, ledger, 200 new Ledger.Transaction(now, now, eventId, tag, rewardVal, 0), true); 201 } 202 break; 203 204 default: 205 Slog.w(TAG, "Unsupported event type: " + eventType); 206 } 207 scheduleBalanceCheckLocked(userId, pkgName); 208 } 209 210 @GuardedBy("mLock") noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag, final long startElapsed)211 void noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId, 212 @Nullable String tag, final long startElapsed) { 213 noteOngoingEventLocked(userId, pkgName, eventId, tag, startElapsed, true); 214 } 215 216 @GuardedBy("mLock") noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag, final long startElapsed, final boolean updateBalanceCheck)217 private void noteOngoingEventLocked(final int userId, @NonNull final String pkgName, 218 final int eventId, @Nullable String tag, final long startElapsed, 219 final boolean updateBalanceCheck) { 220 if (mIrs.isSystem(userId, pkgName)) { 221 // Events are free for the system. Don't bother recording them. 222 return; 223 } 224 225 SparseArrayMap<String, OngoingEvent> ongoingEvents = 226 mCurrentOngoingEvents.get(userId, pkgName); 227 if (ongoingEvents == null) { 228 ongoingEvents = new SparseArrayMap<>(); 229 mCurrentOngoingEvents.add(userId, pkgName, ongoingEvents); 230 } 231 OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag); 232 233 final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); 234 final int eventType = getEventType(eventId); 235 switch (eventType) { 236 case TYPE_ACTION: 237 final EconomicPolicy.Cost actionCost = 238 economicPolicy.getCostOfAction(eventId, userId, pkgName); 239 240 if (ongoingEvent == null) { 241 ongoingEvents.add(eventId, tag, 242 new OngoingEvent(eventId, tag, startElapsed, actionCost)); 243 } else { 244 ongoingEvent.refCount++; 245 } 246 break; 247 248 case TYPE_REWARD: 249 final EconomicPolicy.Reward reward = economicPolicy.getReward(eventId); 250 if (reward != null) { 251 if (ongoingEvent == null) { 252 ongoingEvents.add(eventId, tag, new OngoingEvent( 253 eventId, tag, startElapsed, reward)); 254 } else { 255 ongoingEvent.refCount++; 256 } 257 } 258 break; 259 260 default: 261 Slog.w(TAG, "Unsupported event type: " + eventType); 262 } 263 264 if (updateBalanceCheck) { 265 scheduleBalanceCheckLocked(userId, pkgName); 266 } 267 } 268 269 @GuardedBy("mLock") onDeviceStateChangedLocked()270 void onDeviceStateChangedLocked() { 271 onPricingChangedLocked(); 272 } 273 274 @GuardedBy("mLock") onPricingChangedLocked()275 void onPricingChangedLocked() { 276 onAnythingChangedLocked(true); 277 } 278 279 @GuardedBy("mLock") onAppStatesChangedLocked(final int userId, @NonNull ArraySet<String> pkgNames)280 void onAppStatesChangedLocked(final int userId, @NonNull ArraySet<String> pkgNames) { 281 final long now = getCurrentTimeMillis(); 282 final long nowElapsed = SystemClock.elapsedRealtime(); 283 final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); 284 285 for (int i = 0; i < pkgNames.size(); ++i) { 286 final String pkgName = pkgNames.valueAt(i); 287 SparseArrayMap<String, OngoingEvent> ongoingEvents = 288 mCurrentOngoingEvents.get(userId, pkgName); 289 if (ongoingEvents != null) { 290 mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed); 291 ongoingEvents.forEach(mOngoingEventUpdater); 292 final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes = 293 mActionAffordabilityNotes.get(userId, pkgName); 294 if (actionAffordabilityNotes != null) { 295 final int size = actionAffordabilityNotes.size(); 296 final long newBalance = 297 mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance(); 298 for (int n = 0; n < size; ++n) { 299 final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n); 300 note.recalculateCosts(economicPolicy, userId, pkgName); 301 final boolean isAffordable = 302 isAffordableLocked(newBalance, 303 note.getCachedModifiedPrice(), note.getCtp()); 304 if (note.isCurrentlyAffordable() != isAffordable) { 305 note.setNewAffordability(isAffordable); 306 mIrs.postAffordabilityChanged(userId, pkgName, note); 307 } 308 } 309 } 310 scheduleBalanceCheckLocked(userId, pkgName); 311 } 312 } 313 } 314 315 @GuardedBy("mLock") onAnythingChangedLocked(final boolean updateOngoingEvents)316 private void onAnythingChangedLocked(final boolean updateOngoingEvents) { 317 final long now = getCurrentTimeMillis(); 318 final long nowElapsed = SystemClock.elapsedRealtime(); 319 final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); 320 321 for (int uIdx = mCurrentOngoingEvents.numMaps() - 1; uIdx >= 0; --uIdx) { 322 final int userId = mCurrentOngoingEvents.keyAt(uIdx); 323 324 for (int pIdx = mCurrentOngoingEvents.numElementsForKey(userId) - 1; pIdx >= 0; 325 --pIdx) { 326 final String pkgName = mCurrentOngoingEvents.keyAt(uIdx, pIdx); 327 328 SparseArrayMap<String, OngoingEvent> ongoingEvents = 329 mCurrentOngoingEvents.valueAt(uIdx, pIdx); 330 if (ongoingEvents != null) { 331 if (updateOngoingEvents) { 332 mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed); 333 ongoingEvents.forEach(mOngoingEventUpdater); 334 } 335 scheduleBalanceCheckLocked(userId, pkgName); 336 } 337 } 338 } 339 for (int uIdx = mActionAffordabilityNotes.numMaps() - 1; uIdx >= 0; --uIdx) { 340 final int userId = mActionAffordabilityNotes.keyAt(uIdx); 341 342 for (int pIdx = mActionAffordabilityNotes.numElementsForKey(userId) - 1; pIdx >= 0; 343 --pIdx) { 344 final String pkgName = mActionAffordabilityNotes.keyAt(uIdx, pIdx); 345 346 final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes = 347 mActionAffordabilityNotes.valueAt(uIdx, pIdx); 348 349 if (actionAffordabilityNotes != null) { 350 final int size = actionAffordabilityNotes.size(); 351 final long newBalance = getBalanceLocked(userId, pkgName); 352 for (int n = 0; n < size; ++n) { 353 final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n); 354 note.recalculateCosts(economicPolicy, userId, pkgName); 355 final boolean isAffordable = 356 isAffordableLocked(newBalance, 357 note.getCachedModifiedPrice(), note.getCtp()); 358 if (note.isCurrentlyAffordable() != isAffordable) { 359 note.setNewAffordability(isAffordable); 360 mIrs.postAffordabilityChanged(userId, pkgName, note); 361 } 362 } 363 } 364 } 365 } 366 } 367 368 @GuardedBy("mLock") stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag, final long nowElapsed, final long now)369 void stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId, 370 @Nullable String tag, final long nowElapsed, final long now) { 371 stopOngoingActionLocked(userId, pkgName, eventId, tag, nowElapsed, now, true, true); 372 } 373 374 /** 375 * @param updateBalanceCheck Whether to reschedule the affordability/balance 376 * check alarm. 377 * @param notifyOnAffordabilityChange Whether to evaluate the app's ability to afford 378 * registered bills and notify listeners about any changes. 379 */ 380 @GuardedBy("mLock") stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId, @Nullable String tag, final long nowElapsed, final long now, final boolean updateBalanceCheck, final boolean notifyOnAffordabilityChange)381 private void stopOngoingActionLocked(final int userId, @NonNull final String pkgName, 382 final int eventId, @Nullable String tag, final long nowElapsed, final long now, 383 final boolean updateBalanceCheck, final boolean notifyOnAffordabilityChange) { 384 if (mIrs.isSystem(userId, pkgName)) { 385 // Events are free for the system. Don't bother recording them. 386 return; 387 } 388 389 final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); 390 391 SparseArrayMap<String, OngoingEvent> ongoingEvents = 392 mCurrentOngoingEvents.get(userId, pkgName); 393 if (ongoingEvents == null) { 394 // This may occur if TARE goes from disabled to enabled while an event is already 395 // occurring. 396 Slog.w(TAG, "No ongoing transactions for " + appToString(userId, pkgName)); 397 return; 398 } 399 final OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag); 400 if (ongoingEvent == null) { 401 // This may occur if TARE goes from disabled to enabled while an event is already 402 // occurring. 403 Slog.w(TAG, "Nonexistent ongoing transaction " 404 + eventToString(eventId) + (tag == null ? "" : ":" + tag) 405 + " for " + appToString(userId, pkgName) + " ended"); 406 return; 407 } 408 ongoingEvent.refCount--; 409 if (ongoingEvent.refCount <= 0) { 410 final long startElapsed = ongoingEvent.startTimeElapsed; 411 final long startTime = now - (nowElapsed - startElapsed); 412 final EconomicPolicy.Cost actualDelta = 413 getActualDeltaLocked(ongoingEvent, ledger, nowElapsed, now); 414 recordTransactionLocked(userId, pkgName, ledger, 415 new Ledger.Transaction(startTime, now, eventId, tag, actualDelta.price, 416 actualDelta.costToProduce), 417 notifyOnAffordabilityChange); 418 419 ongoingEvents.delete(eventId, tag); 420 } 421 if (updateBalanceCheck) { 422 scheduleBalanceCheckLocked(userId, pkgName); 423 } 424 } 425 426 @GuardedBy("mLock") 427 @NonNull getActualDeltaLocked(@onNull OngoingEvent ongoingEvent, @NonNull Ledger ledger, long nowElapsed, long now)428 private EconomicPolicy.Cost getActualDeltaLocked(@NonNull OngoingEvent ongoingEvent, 429 @NonNull Ledger ledger, long nowElapsed, long now) { 430 final long startElapsed = ongoingEvent.startTimeElapsed; 431 final long durationSecs = (nowElapsed - startElapsed) / 1000; 432 final long computedDelta = durationSecs * ongoingEvent.getDeltaPerSec(); 433 if (ongoingEvent.reward == null) { 434 return new EconomicPolicy.Cost( 435 durationSecs * ongoingEvent.getCtpPerSec(), computedDelta); 436 } 437 final long rewardSum = ledger.get24HourSum(ongoingEvent.eventId, now); 438 return new EconomicPolicy.Cost(0, 439 Math.max(0, 440 Math.min(ongoingEvent.reward.maxDailyReward - rewardSum, computedDelta))); 441 } 442 443 @VisibleForTesting 444 @GuardedBy("mLock") recordTransactionLocked(final int userId, @NonNull final String pkgName, @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction, final boolean notifyOnAffordabilityChange)445 void recordTransactionLocked(final int userId, @NonNull final String pkgName, 446 @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction, 447 final boolean notifyOnAffordabilityChange) { 448 if (!DEBUG && transaction.delta == 0) { 449 // Skip recording transactions with a delta of 0 to save on space. 450 return; 451 } 452 if (mIrs.isSystem(userId, pkgName)) { 453 Slog.wtfStack(TAG, 454 "Tried to adjust system balance for " + appToString(userId, pkgName)); 455 return; 456 } 457 final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); 458 final long originalBalance = ledger.getCurrentBalance(); 459 if (transaction.delta > 0 460 && originalBalance + transaction.delta > economicPolicy.getMaxSatiatedBalance()) { 461 // Set lower bound at 0 so we don't accidentally take away credits when we were trying 462 // to _give_ the app credits. 463 final long newDelta = 464 Math.max(0, economicPolicy.getMaxSatiatedBalance() - originalBalance); 465 Slog.i(TAG, "Would result in becoming too rich. Decreasing transaction " 466 + eventToString(transaction.eventId) 467 + (transaction.tag == null ? "" : ":" + transaction.tag) 468 + " for " + appToString(userId, pkgName) 469 + " by " + cakeToString(transaction.delta - newDelta)); 470 transaction = new Ledger.Transaction( 471 transaction.startTimeMs, transaction.endTimeMs, 472 transaction.eventId, transaction.tag, newDelta, transaction.ctp); 473 } 474 ledger.recordTransaction(transaction); 475 mScribe.adjustRemainingConsumableCakesLocked(-transaction.ctp); 476 mAnalyst.noteTransaction(transaction); 477 if (transaction.delta != 0 && notifyOnAffordabilityChange) { 478 final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes = 479 mActionAffordabilityNotes.get(userId, pkgName); 480 if (actionAffordabilityNotes != null) { 481 final long newBalance = ledger.getCurrentBalance(); 482 for (int i = 0; i < actionAffordabilityNotes.size(); ++i) { 483 final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i); 484 final boolean isAffordable = 485 isAffordableLocked(newBalance, 486 note.getCachedModifiedPrice(), note.getCtp()); 487 if (note.isCurrentlyAffordable() != isAffordable) { 488 note.setNewAffordability(isAffordable); 489 mIrs.postAffordabilityChanged(userId, pkgName, note); 490 } 491 } 492 } 493 } 494 if (transaction.ctp != 0) { 495 mHandler.sendEmptyMessage(MSG_CHECK_ALL_AFFORDABILITY); 496 mIrs.maybePerformQuantitativeEasingLocked(); 497 } 498 } 499 500 /** 501 * Reclaim a percentage of unused ARCs from every app that hasn't been used recently. The 502 * reclamation will not reduce an app's balance below its minimum balance as dictated by 503 * {@code scaleMinBalance}. 504 * 505 * @param percentage A value between 0 and 1 to indicate how much of the unused balance 506 * should be reclaimed. 507 * @param minUnusedTimeMs The minimum amount of time (in milliseconds) that must have 508 * transpired since the last user usage event before we will consider 509 * reclaiming ARCs from the app. 510 * @param scaleMinBalance Whether or not to used the scaled minimum app balance. If false, 511 * this will use the constant min balance floor given by 512 * {@link EconomicPolicy#getMinSatiatedBalance(int, String)}. If true, 513 * this will use the scaled balance given by 514 * {@link InternalResourceService#getMinBalanceLocked(int, String)}. 515 */ 516 @GuardedBy("mLock") reclaimUnusedAssetsLocked(double percentage, long minUnusedTimeMs, boolean scaleMinBalance)517 void reclaimUnusedAssetsLocked(double percentage, long minUnusedTimeMs, 518 boolean scaleMinBalance) { 519 final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); 520 final SparseArrayMap<String, Ledger> ledgers = mScribe.getLedgersLocked(); 521 final long now = getCurrentTimeMillis(); 522 for (int u = 0; u < ledgers.numMaps(); ++u) { 523 final int userId = ledgers.keyAt(u); 524 for (int p = 0; p < ledgers.numElementsForKey(userId); ++p) { 525 final Ledger ledger = ledgers.valueAt(u, p); 526 final long curBalance = ledger.getCurrentBalance(); 527 if (curBalance <= 0) { 528 continue; 529 } 530 final String pkgName = ledgers.keyAt(u, p); 531 // AppStandby only counts elapsed time for things like this 532 // TODO: should we use clock time instead? 533 final long timeSinceLastUsedMs = 534 mAppStandbyInternal.getTimeSinceLastUsedByUser(pkgName, userId); 535 if (timeSinceLastUsedMs >= minUnusedTimeMs) { 536 final long minBalance; 537 if (!scaleMinBalance) { 538 // Use a constant floor instead of the scaled floor from the IRS. 539 minBalance = economicPolicy.getMinSatiatedBalance(userId, pkgName); 540 } else { 541 minBalance = mIrs.getMinBalanceLocked(userId, pkgName); 542 } 543 long toReclaim = (long) (curBalance * percentage); 544 if (curBalance - toReclaim < minBalance) { 545 toReclaim = curBalance - minBalance; 546 } 547 if (toReclaim > 0) { 548 if (DEBUG) { 549 Slog.i(TAG, "Reclaiming unused wealth! Taking " + toReclaim 550 + " from " + appToString(userId, pkgName)); 551 } 552 553 recordTransactionLocked(userId, pkgName, ledger, 554 new Ledger.Transaction(now, now, REGULATION_WEALTH_RECLAMATION, 555 null, -toReclaim, 0), 556 true); 557 } 558 } 559 } 560 } 561 } 562 563 /** 564 * Reclaim a percentage of unused ARCs from an app that was just removed from an exemption list. 565 * The amount reclaimed will depend on how recently the app was used. The reclamation will not 566 * reduce an app's balance below its current minimum balance. 567 */ 568 @GuardedBy("mLock") onAppUnexemptedLocked(final int userId, @NonNull final String pkgName)569 void onAppUnexemptedLocked(final int userId, @NonNull final String pkgName) { 570 final long curBalance = getBalanceLocked(userId, pkgName); 571 final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName); 572 if (curBalance <= minBalance) { 573 return; 574 } 575 // AppStandby only counts elapsed time for things like this 576 // TODO: should we use clock time instead? 577 final long timeSinceLastUsedMs = 578 mAppStandbyInternal.getTimeSinceLastUsedByUser(pkgName, userId); 579 // The app is no longer exempted. We should take away some of credits so it's more in line 580 // with other non-exempt apps. However, don't take away as many credits if the app was used 581 // recently. 582 final double percentageToReclaim; 583 if (timeSinceLastUsedMs < DAY_IN_MILLIS) { 584 percentageToReclaim = .25; 585 } else if (timeSinceLastUsedMs < 2 * DAY_IN_MILLIS) { 586 percentageToReclaim = .5; 587 } else if (timeSinceLastUsedMs < 3 * DAY_IN_MILLIS) { 588 percentageToReclaim = .75; 589 } else { 590 percentageToReclaim = 1; 591 } 592 final long overage = curBalance - minBalance; 593 final long toReclaim = (long) (overage * percentageToReclaim); 594 if (toReclaim > 0) { 595 if (DEBUG) { 596 Slog.i(TAG, "Reclaiming bonus wealth! Taking " + toReclaim 597 + " from " + appToString(userId, pkgName)); 598 } 599 600 final long now = getCurrentTimeMillis(); 601 final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); 602 recordTransactionLocked(userId, pkgName, ledger, 603 new Ledger.Transaction(now, now, REGULATION_DEMOTION, null, -toReclaim, 0), 604 true); 605 } 606 } 607 608 /** Returns true if an app should be given credits in the general distributions. */ shouldGiveCredits(@onNull PackageInfo packageInfo)609 private boolean shouldGiveCredits(@NonNull PackageInfo packageInfo) { 610 final ApplicationInfo applicationInfo = packageInfo.applicationInfo; 611 // Skip apps that wouldn't be doing any work. Giving them ARCs would be wasteful. 612 if (applicationInfo == null || !applicationInfo.hasCode()) { 613 return false; 614 } 615 final int userId = UserHandle.getUserId(packageInfo.applicationInfo.uid); 616 // No point allocating ARCs to the system. It can do whatever it wants. 617 return !mIrs.isSystem(userId, packageInfo.packageName); 618 } 619 onCreditSupplyChanged()620 void onCreditSupplyChanged() { 621 mHandler.sendEmptyMessage(MSG_CHECK_ALL_AFFORDABILITY); 622 } 623 624 @GuardedBy("mLock") distributeBasicIncomeLocked(int batteryLevel)625 void distributeBasicIncomeLocked(int batteryLevel) { 626 List<PackageInfo> pkgs = mIrs.getInstalledPackages(); 627 628 final long now = getCurrentTimeMillis(); 629 for (int i = 0; i < pkgs.size(); ++i) { 630 final PackageInfo pkgInfo = pkgs.get(i); 631 if (!shouldGiveCredits(pkgInfo)) { 632 continue; 633 } 634 final int userId = UserHandle.getUserId(pkgInfo.applicationInfo.uid); 635 final String pkgName = pkgInfo.packageName; 636 final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); 637 final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName); 638 final double perc = batteryLevel / 100d; 639 // TODO: maybe don't give credits to bankrupt apps until battery level >= 50% 640 final long shortfall = minBalance - ledger.getCurrentBalance(); 641 if (shortfall > 0) { 642 recordTransactionLocked(userId, pkgName, ledger, 643 new Ledger.Transaction(now, now, REGULATION_BASIC_INCOME, 644 null, (long) (perc * shortfall), 0), true); 645 } 646 } 647 } 648 649 /** Give each app an initial balance. */ 650 @GuardedBy("mLock") grantBirthrightsLocked()651 void grantBirthrightsLocked() { 652 UserManagerInternal userManagerInternal = 653 LocalServices.getService(UserManagerInternal.class); 654 final int[] userIds = userManagerInternal.getUserIds(); 655 for (int userId : userIds) { 656 grantBirthrightsLocked(userId); 657 } 658 } 659 660 @GuardedBy("mLock") grantBirthrightsLocked(final int userId)661 void grantBirthrightsLocked(final int userId) { 662 final List<PackageInfo> pkgs = mIrs.getInstalledPackages(userId); 663 final long now = getCurrentTimeMillis(); 664 665 for (int i = 0; i < pkgs.size(); ++i) { 666 final PackageInfo packageInfo = pkgs.get(i); 667 if (!shouldGiveCredits(packageInfo)) { 668 continue; 669 } 670 final String pkgName = packageInfo.packageName; 671 final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); 672 if (ledger.getCurrentBalance() > 0) { 673 // App already got credits somehow. Move along. 674 Slog.wtf(TAG, "App " + pkgName + " had credits before economy was set up"); 675 continue; 676 } 677 678 recordTransactionLocked(userId, pkgName, ledger, 679 new Ledger.Transaction(now, now, REGULATION_BIRTHRIGHT, null, 680 mIrs.getMinBalanceLocked(userId, pkgName), 0), 681 true); 682 } 683 } 684 685 @GuardedBy("mLock") grantBirthrightLocked(final int userId, @NonNull final String pkgName)686 void grantBirthrightLocked(final int userId, @NonNull final String pkgName) { 687 final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); 688 if (ledger.getCurrentBalance() > 0) { 689 Slog.wtf(TAG, "App " + pkgName + " had credits as soon as it was installed"); 690 // App already got credits somehow. Move along. 691 return; 692 } 693 694 final long now = getCurrentTimeMillis(); 695 696 recordTransactionLocked(userId, pkgName, ledger, 697 new Ledger.Transaction(now, now, REGULATION_BIRTHRIGHT, null, 698 mIrs.getMinBalanceLocked(userId, pkgName), 0), true); 699 } 700 701 @GuardedBy("mLock") onAppExemptedLocked(final int userId, @NonNull final String pkgName)702 void onAppExemptedLocked(final int userId, @NonNull final String pkgName) { 703 final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName); 704 final long missing = minBalance - getBalanceLocked(userId, pkgName); 705 if (missing <= 0) { 706 return; 707 } 708 709 final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); 710 final long now = getCurrentTimeMillis(); 711 712 recordTransactionLocked(userId, pkgName, ledger, 713 new Ledger.Transaction(now, now, REGULATION_PROMOTION, null, missing, 0), true); 714 } 715 716 @GuardedBy("mLock") onPackageRemovedLocked(final int userId, @NonNull final String pkgName)717 void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) { 718 reclaimAssetsLocked(userId, pkgName); 719 mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName)); 720 } 721 722 /** 723 * Reclaims any ARCs granted to the app, making them available to other apps. Also deletes the 724 * app's ledger and stops any ongoing event tracking. 725 */ 726 @GuardedBy("mLock") reclaimAssetsLocked(final int userId, @NonNull final String pkgName)727 private void reclaimAssetsLocked(final int userId, @NonNull final String pkgName) { 728 final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); 729 if (ledger.getCurrentBalance() != 0) { 730 mScribe.adjustRemainingConsumableCakesLocked(-ledger.getCurrentBalance()); 731 } 732 mScribe.discardLedgerLocked(userId, pkgName); 733 mCurrentOngoingEvents.delete(userId, pkgName); 734 } 735 736 @GuardedBy("mLock") onUserRemovedLocked(final int userId, @NonNull final List<String> pkgNames)737 void onUserRemovedLocked(final int userId, @NonNull final List<String> pkgNames) { 738 reclaimAssetsLocked(userId, pkgNames); 739 mBalanceThresholdAlarmQueue.removeAlarmsForUserId(userId); 740 } 741 742 @GuardedBy("mLock") reclaimAssetsLocked(final int userId, @NonNull final List<String> pkgNames)743 private void reclaimAssetsLocked(final int userId, @NonNull final List<String> pkgNames) { 744 for (int i = 0; i < pkgNames.size(); ++i) { 745 reclaimAssetsLocked(userId, pkgNames.get(i)); 746 } 747 } 748 749 @VisibleForTesting 750 static class TrendCalculator implements Consumer<OngoingEvent> { 751 static final long WILL_NOT_CROSS_THRESHOLD = -1; 752 753 private long mCurBalance; 754 private long mRemainingConsumableCredits; 755 /** 756 * The maximum change in credits per second towards the upper threshold 757 * {@link #mUpperThreshold}. A value of 0 means the current ongoing events will never 758 * result in the app crossing the upper threshold. 759 */ 760 private long mMaxDeltaPerSecToUpperThreshold; 761 /** 762 * The maximum change in credits per second towards the lower threshold 763 * {@link #mLowerThreshold}. A value of 0 means the current ongoing events will never 764 * result in the app crossing the lower threshold. 765 */ 766 private long mMaxDeltaPerSecToLowerThreshold; 767 /** 768 * The maximum change in credits per second towards the highest CTP threshold below the 769 * remaining consumable credits (cached in {@link #mCtpThreshold}). A value of 0 means 770 * the current ongoing events will never result in the app crossing the lower threshold. 771 */ 772 private long mMaxDeltaPerSecToCtpThreshold; 773 private long mUpperThreshold; 774 private long mLowerThreshold; 775 private long mCtpThreshold; 776 reset(long curBalance, long remainingConsumableCredits, @Nullable ArraySet<ActionAffordabilityNote> actionAffordabilityNotes)777 void reset(long curBalance, long remainingConsumableCredits, 778 @Nullable ArraySet<ActionAffordabilityNote> actionAffordabilityNotes) { 779 mCurBalance = curBalance; 780 mRemainingConsumableCredits = remainingConsumableCredits; 781 mMaxDeltaPerSecToUpperThreshold = mMaxDeltaPerSecToLowerThreshold = 0; 782 mMaxDeltaPerSecToCtpThreshold = 0; 783 mUpperThreshold = Long.MIN_VALUE; 784 mLowerThreshold = Long.MAX_VALUE; 785 mCtpThreshold = 0; 786 if (actionAffordabilityNotes != null) { 787 for (int i = 0; i < actionAffordabilityNotes.size(); ++i) { 788 final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i); 789 final long price = note.getCachedModifiedPrice(); 790 if (price <= mCurBalance) { 791 mLowerThreshold = (mLowerThreshold == Long.MAX_VALUE) 792 ? price : Math.max(mLowerThreshold, price); 793 } else { 794 mUpperThreshold = (mUpperThreshold == Long.MIN_VALUE) 795 ? price : Math.min(mUpperThreshold, price); 796 } 797 final long ctp = note.getCtp(); 798 if (ctp <= mRemainingConsumableCredits) { 799 mCtpThreshold = Math.max(mCtpThreshold, ctp); 800 } 801 } 802 } 803 } 804 805 /** 806 * Returns the amount of time (in millisecond) it will take for the app to cross the next 807 * lowest action affordability note (compared to its current balance) based on current 808 * ongoing events. 809 * Returns {@link #WILL_NOT_CROSS_THRESHOLD} if the app will never cross the lowest 810 * threshold. 811 */ getTimeToCrossLowerThresholdMs()812 long getTimeToCrossLowerThresholdMs() { 813 if (mMaxDeltaPerSecToLowerThreshold == 0 && mMaxDeltaPerSecToCtpThreshold == 0) { 814 // Will never cross lower threshold based on current events. 815 return WILL_NOT_CROSS_THRESHOLD; 816 } 817 long minSeconds = Long.MAX_VALUE; 818 if (mMaxDeltaPerSecToLowerThreshold != 0) { 819 // deltaPerSec is a negative value, so do threshold-balance to cancel out the 820 // negative. 821 minSeconds = (mLowerThreshold - mCurBalance) / mMaxDeltaPerSecToLowerThreshold; 822 } 823 if (mMaxDeltaPerSecToCtpThreshold != 0) { 824 minSeconds = Math.min(minSeconds, 825 // deltaPerSec is a negative value, so do threshold-balance to cancel 826 // out the negative. 827 (mCtpThreshold - mRemainingConsumableCredits) 828 / mMaxDeltaPerSecToCtpThreshold); 829 } 830 return minSeconds * 1000; 831 } 832 833 /** 834 * Returns the amount of time (in millisecond) it will take for the app to cross the next 835 * highest action affordability note (compared to its current balance) based on current 836 * ongoing events. 837 * Returns {@link #WILL_NOT_CROSS_THRESHOLD} if the app will never cross the upper 838 * threshold. 839 */ getTimeToCrossUpperThresholdMs()840 long getTimeToCrossUpperThresholdMs() { 841 if (mMaxDeltaPerSecToUpperThreshold == 0) { 842 // Will never cross upper threshold based on current events. 843 return WILL_NOT_CROSS_THRESHOLD; 844 } 845 final long minSeconds = 846 (mUpperThreshold - mCurBalance) / mMaxDeltaPerSecToUpperThreshold; 847 return minSeconds * 1000; 848 } 849 850 @Override accept(OngoingEvent ongoingEvent)851 public void accept(OngoingEvent ongoingEvent) { 852 final long deltaPerSec = ongoingEvent.getDeltaPerSec(); 853 if (mCurBalance >= mLowerThreshold && deltaPerSec < 0) { 854 mMaxDeltaPerSecToLowerThreshold += deltaPerSec; 855 } else if (mCurBalance < mUpperThreshold && deltaPerSec > 0) { 856 mMaxDeltaPerSecToUpperThreshold += deltaPerSec; 857 } 858 final long ctpPerSec = ongoingEvent.getCtpPerSec(); 859 if (mRemainingConsumableCredits >= mCtpThreshold && deltaPerSec < 0) { 860 mMaxDeltaPerSecToCtpThreshold -= ctpPerSec; 861 } 862 } 863 } 864 865 @GuardedBy("mLock") 866 private final TrendCalculator mTrendCalculator = new TrendCalculator(); 867 868 @GuardedBy("mLock") scheduleBalanceCheckLocked(final int userId, @NonNull final String pkgName)869 private void scheduleBalanceCheckLocked(final int userId, @NonNull final String pkgName) { 870 SparseArrayMap<String, OngoingEvent> ongoingEvents = 871 mCurrentOngoingEvents.get(userId, pkgName); 872 if (ongoingEvents == null) { 873 // No ongoing transactions. No reason to schedule 874 mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName)); 875 return; 876 } 877 mTrendCalculator.reset(getBalanceLocked(userId, pkgName), 878 mScribe.getRemainingConsumableCakesLocked(), 879 mActionAffordabilityNotes.get(userId, pkgName)); 880 ongoingEvents.forEach(mTrendCalculator); 881 final long lowerTimeMs = mTrendCalculator.getTimeToCrossLowerThresholdMs(); 882 final long upperTimeMs = mTrendCalculator.getTimeToCrossUpperThresholdMs(); 883 final long timeToThresholdMs; 884 if (lowerTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) { 885 if (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) { 886 // Will never cross a threshold based on current events. 887 mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName)); 888 return; 889 } 890 timeToThresholdMs = upperTimeMs; 891 } else { 892 timeToThresholdMs = (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) 893 ? lowerTimeMs : Math.min(lowerTimeMs, upperTimeMs); 894 } 895 mBalanceThresholdAlarmQueue.addAlarm(new Package(userId, pkgName), 896 SystemClock.elapsedRealtime() + timeToThresholdMs); 897 } 898 899 @GuardedBy("mLock") tearDownLocked()900 void tearDownLocked() { 901 mCurrentOngoingEvents.clear(); 902 mBalanceThresholdAlarmQueue.removeAllAlarms(); 903 } 904 905 @VisibleForTesting 906 static class OngoingEvent { 907 public final long startTimeElapsed; 908 public final int eventId; 909 @Nullable 910 public final String tag; 911 @Nullable 912 public final EconomicPolicy.Reward reward; 913 @Nullable 914 public final EconomicPolicy.Cost actionCost; 915 public int refCount; 916 OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed, @NonNull EconomicPolicy.Reward reward)917 OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed, 918 @NonNull EconomicPolicy.Reward reward) { 919 this.startTimeElapsed = startTimeElapsed; 920 this.eventId = eventId; 921 this.tag = tag; 922 this.reward = reward; 923 this.actionCost = null; 924 refCount = 1; 925 } 926 OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed, @NonNull EconomicPolicy.Cost actionCost)927 OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed, 928 @NonNull EconomicPolicy.Cost actionCost) { 929 this.startTimeElapsed = startTimeElapsed; 930 this.eventId = eventId; 931 this.tag = tag; 932 this.reward = null; 933 this.actionCost = actionCost; 934 refCount = 1; 935 } 936 getDeltaPerSec()937 long getDeltaPerSec() { 938 if (actionCost != null) { 939 return -actionCost.price; 940 } 941 if (reward != null) { 942 return reward.ongoingRewardPerSecond; 943 } 944 Slog.wtfStack(TAG, "No action or reward in ongoing event?!??!"); 945 return 0; 946 } 947 getCtpPerSec()948 long getCtpPerSec() { 949 if (actionCost != null) { 950 return actionCost.costToProduce; 951 } 952 return 0; 953 } 954 } 955 956 private class OngoingEventUpdater implements Consumer<OngoingEvent> { 957 private int mUserId; 958 private String mPkgName; 959 private long mNow; 960 private long mNowElapsed; 961 reset(int userId, String pkgName, long now, long nowElapsed)962 private void reset(int userId, String pkgName, long now, long nowElapsed) { 963 mUserId = userId; 964 mPkgName = pkgName; 965 mNow = now; 966 mNowElapsed = nowElapsed; 967 } 968 969 @Override accept(OngoingEvent ongoingEvent)970 public void accept(OngoingEvent ongoingEvent) { 971 // Disable balance check & affordability notifications here because 972 // we're in the middle of updating ongoing action costs/prices and 973 // sending out notifications or rescheduling the balance check alarm 974 // would be a waste since we'll have to redo them again after all of 975 // our internal state is updated. 976 final boolean updateBalanceCheck = false; 977 stopOngoingActionLocked(mUserId, mPkgName, ongoingEvent.eventId, ongoingEvent.tag, 978 mNowElapsed, mNow, updateBalanceCheck, /* notifyOnAffordabilityChange */ false); 979 noteOngoingEventLocked(mUserId, mPkgName, ongoingEvent.eventId, ongoingEvent.tag, 980 mNowElapsed, updateBalanceCheck); 981 } 982 } 983 984 private final OngoingEventUpdater mOngoingEventUpdater = new OngoingEventUpdater(); 985 986 private static final class Package { 987 public final String packageName; 988 public final int userId; 989 Package(int userId, String packageName)990 Package(int userId, String packageName) { 991 this.userId = userId; 992 this.packageName = packageName; 993 } 994 995 @Override toString()996 public String toString() { 997 return appToString(userId, packageName); 998 } 999 1000 @Override equals(Object obj)1001 public boolean equals(Object obj) { 1002 if (obj == null) { 1003 return false; 1004 } 1005 if (this == obj) { 1006 return true; 1007 } 1008 if (obj instanceof Package) { 1009 Package other = (Package) obj; 1010 return userId == other.userId && Objects.equals(packageName, other.packageName); 1011 } 1012 return false; 1013 } 1014 1015 @Override hashCode()1016 public int hashCode() { 1017 return packageName.hashCode() + userId; 1018 } 1019 } 1020 1021 /** Track when apps will cross the closest affordability threshold (in both directions). */ 1022 private class BalanceThresholdAlarmQueue extends AlarmQueue<Package> { BalanceThresholdAlarmQueue(Context context, Looper looper)1023 private BalanceThresholdAlarmQueue(Context context, Looper looper) { 1024 super(context, looper, ALARM_TAG_AFFORDABILITY_CHECK, "Affordability check", true, 1025 15_000L); 1026 } 1027 1028 @Override isForUser(@onNull Package key, int userId)1029 protected boolean isForUser(@NonNull Package key, int userId) { 1030 return key.userId == userId; 1031 } 1032 1033 @Override processExpiredAlarms(@onNull ArraySet<Package> expired)1034 protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) { 1035 for (int i = 0; i < expired.size(); ++i) { 1036 Package p = expired.valueAt(i); 1037 mHandler.obtainMessage( 1038 MSG_CHECK_INDIVIDUAL_AFFORDABILITY, p.userId, 0, p.packageName) 1039 .sendToTarget(); 1040 } 1041 } 1042 } 1043 1044 @GuardedBy("mLock") registerAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName, @NonNull EconomyManagerInternal.AffordabilityChangeListener listener, @NonNull EconomyManagerInternal.ActionBill bill)1045 public void registerAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName, 1046 @NonNull EconomyManagerInternal.AffordabilityChangeListener listener, 1047 @NonNull EconomyManagerInternal.ActionBill bill) { 1048 ArraySet<ActionAffordabilityNote> actionAffordabilityNotes = 1049 mActionAffordabilityNotes.get(userId, pkgName); 1050 if (actionAffordabilityNotes == null) { 1051 actionAffordabilityNotes = new ArraySet<>(); 1052 mActionAffordabilityNotes.add(userId, pkgName, actionAffordabilityNotes); 1053 } 1054 final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); 1055 final ActionAffordabilityNote note = 1056 new ActionAffordabilityNote(bill, listener, economicPolicy); 1057 if (actionAffordabilityNotes.add(note)) { 1058 if (!mIrs.isEnabled()) { 1059 // When TARE isn't enabled, we always say something is affordable. We also don't 1060 // want to silently drop affordability change listeners in case TARE becomes enabled 1061 // because then clients will be in an ambiguous state. 1062 note.setNewAffordability(true); 1063 return; 1064 } 1065 note.recalculateCosts(economicPolicy, userId, pkgName); 1066 note.setNewAffordability( 1067 isAffordableLocked(getBalanceLocked(userId, pkgName), 1068 note.getCachedModifiedPrice(), note.getCtp())); 1069 mIrs.postAffordabilityChanged(userId, pkgName, note); 1070 // Update ongoing alarm 1071 scheduleBalanceCheckLocked(userId, pkgName); 1072 } 1073 } 1074 1075 @GuardedBy("mLock") unregisterAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName, @NonNull EconomyManagerInternal.AffordabilityChangeListener listener, @NonNull EconomyManagerInternal.ActionBill bill)1076 public void unregisterAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName, 1077 @NonNull EconomyManagerInternal.AffordabilityChangeListener listener, 1078 @NonNull EconomyManagerInternal.ActionBill bill) { 1079 final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes = 1080 mActionAffordabilityNotes.get(userId, pkgName); 1081 if (actionAffordabilityNotes != null) { 1082 final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); 1083 final ActionAffordabilityNote note = 1084 new ActionAffordabilityNote(bill, listener, economicPolicy); 1085 if (actionAffordabilityNotes.remove(note)) { 1086 // Update ongoing alarm 1087 scheduleBalanceCheckLocked(userId, pkgName); 1088 } 1089 } 1090 } 1091 1092 static final class ActionAffordabilityNote { 1093 private final EconomyManagerInternal.ActionBill mActionBill; 1094 private final EconomyManagerInternal.AffordabilityChangeListener mListener; 1095 private long mCtp; 1096 private long mModifiedPrice; 1097 private boolean mIsAffordable; 1098 1099 @VisibleForTesting ActionAffordabilityNote(@onNull EconomyManagerInternal.ActionBill bill, @NonNull EconomyManagerInternal.AffordabilityChangeListener listener, @NonNull EconomicPolicy economicPolicy)1100 ActionAffordabilityNote(@NonNull EconomyManagerInternal.ActionBill bill, 1101 @NonNull EconomyManagerInternal.AffordabilityChangeListener listener, 1102 @NonNull EconomicPolicy economicPolicy) { 1103 mActionBill = bill; 1104 final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions = 1105 bill.getAnticipatedActions(); 1106 for (int i = 0; i < anticipatedActions.size(); ++i) { 1107 final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i); 1108 final EconomicPolicy.Action action = economicPolicy.getAction(aa.actionId); 1109 if (action == null) { 1110 throw new IllegalArgumentException("Invalid action id: " + aa.actionId); 1111 } 1112 } 1113 mListener = listener; 1114 } 1115 1116 @NonNull getActionBill()1117 EconomyManagerInternal.ActionBill getActionBill() { 1118 return mActionBill; 1119 } 1120 1121 @NonNull getListener()1122 EconomyManagerInternal.AffordabilityChangeListener getListener() { 1123 return mListener; 1124 } 1125 getCachedModifiedPrice()1126 private long getCachedModifiedPrice() { 1127 return mModifiedPrice; 1128 } 1129 getCtp()1130 private long getCtp() { 1131 return mCtp; 1132 } 1133 1134 @VisibleForTesting recalculateCosts(@onNull EconomicPolicy economicPolicy, int userId, @NonNull String pkgName)1135 void recalculateCosts(@NonNull EconomicPolicy economicPolicy, 1136 int userId, @NonNull String pkgName) { 1137 long modifiedPrice = 0; 1138 long ctp = 0; 1139 final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions = 1140 mActionBill.getAnticipatedActions(); 1141 for (int i = 0; i < anticipatedActions.size(); ++i) { 1142 final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i); 1143 1144 final EconomicPolicy.Cost actionCost = 1145 economicPolicy.getCostOfAction(aa.actionId, userId, pkgName); 1146 modifiedPrice += actionCost.price * aa.numInstantaneousCalls 1147 + actionCost.price * (aa.ongoingDurationMs / 1000); 1148 ctp += actionCost.costToProduce * aa.numInstantaneousCalls 1149 + actionCost.costToProduce * (aa.ongoingDurationMs / 1000); 1150 } 1151 mModifiedPrice = modifiedPrice; 1152 mCtp = ctp; 1153 } 1154 isCurrentlyAffordable()1155 boolean isCurrentlyAffordable() { 1156 return mIsAffordable; 1157 } 1158 setNewAffordability(boolean isAffordable)1159 private void setNewAffordability(boolean isAffordable) { 1160 mIsAffordable = isAffordable; 1161 } 1162 1163 @Override equals(Object o)1164 public boolean equals(Object o) { 1165 if (this == o) return true; 1166 if (!(o instanceof ActionAffordabilityNote)) return false; 1167 ActionAffordabilityNote other = (ActionAffordabilityNote) o; 1168 return mActionBill.equals(other.mActionBill) 1169 && mListener.equals(other.mListener); 1170 } 1171 1172 @Override hashCode()1173 public int hashCode() { 1174 int hash = 0; 1175 hash = 31 * hash + Objects.hash(mListener); 1176 hash = 31 * hash + mActionBill.hashCode(); 1177 return hash; 1178 } 1179 } 1180 1181 private final class AgentHandler extends Handler { AgentHandler(Looper looper)1182 AgentHandler(Looper looper) { 1183 super(looper); 1184 } 1185 1186 @Override handleMessage(Message msg)1187 public void handleMessage(Message msg) { 1188 switch (msg.what) { 1189 case MSG_CHECK_ALL_AFFORDABILITY: { 1190 synchronized (mLock) { 1191 removeMessages(MSG_CHECK_ALL_AFFORDABILITY); 1192 onAnythingChangedLocked(false); 1193 } 1194 } 1195 break; 1196 1197 case MSG_CHECK_INDIVIDUAL_AFFORDABILITY: { 1198 final int userId = msg.arg1; 1199 final String pkgName = (String) msg.obj; 1200 synchronized (mLock) { 1201 final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes = 1202 mActionAffordabilityNotes.get(userId, pkgName); 1203 if (actionAffordabilityNotes != null 1204 && actionAffordabilityNotes.size() > 0) { 1205 final long newBalance = getBalanceLocked(userId, pkgName); 1206 1207 for (int i = 0; i < actionAffordabilityNotes.size(); ++i) { 1208 final ActionAffordabilityNote note = 1209 actionAffordabilityNotes.valueAt(i); 1210 final boolean isAffordable = isAffordableLocked( 1211 newBalance, note.getCachedModifiedPrice(), note.getCtp()); 1212 if (note.isCurrentlyAffordable() != isAffordable) { 1213 note.setNewAffordability(isAffordable); 1214 mIrs.postAffordabilityChanged(userId, pkgName, note); 1215 } 1216 } 1217 } 1218 scheduleBalanceCheckLocked(userId, pkgName); 1219 } 1220 } 1221 break; 1222 } 1223 } 1224 } 1225 1226 @GuardedBy("mLock") dumpLocked(IndentingPrintWriter pw)1227 void dumpLocked(IndentingPrintWriter pw) { 1228 pw.println(); 1229 mBalanceThresholdAlarmQueue.dump(pw); 1230 1231 pw.println(); 1232 pw.println("Ongoing events:"); 1233 pw.increaseIndent(); 1234 boolean printedEvents = false; 1235 final long nowElapsed = SystemClock.elapsedRealtime(); 1236 for (int u = mCurrentOngoingEvents.numMaps() - 1; u >= 0; --u) { 1237 final int userId = mCurrentOngoingEvents.keyAt(u); 1238 for (int p = mCurrentOngoingEvents.numElementsForKey(userId) - 1; p >= 0; --p) { 1239 final String pkgName = mCurrentOngoingEvents.keyAt(u, p); 1240 final SparseArrayMap<String, OngoingEvent> ongoingEvents = 1241 mCurrentOngoingEvents.get(userId, pkgName); 1242 1243 boolean printedApp = false; 1244 1245 for (int e = ongoingEvents.numMaps() - 1; e >= 0; --e) { 1246 final int eventId = ongoingEvents.keyAt(e); 1247 for (int t = ongoingEvents.numElementsForKey(eventId) - 1; t >= 0; --t) { 1248 if (!printedApp) { 1249 printedApp = true; 1250 pw.println(appToString(userId, pkgName)); 1251 pw.increaseIndent(); 1252 } 1253 printedEvents = true; 1254 1255 OngoingEvent ongoingEvent = ongoingEvents.valueAt(e, t); 1256 1257 pw.print(EconomicPolicy.eventToString(ongoingEvent.eventId)); 1258 if (ongoingEvent.tag != null) { 1259 pw.print("("); 1260 pw.print(ongoingEvent.tag); 1261 pw.print(")"); 1262 } 1263 pw.print(" runtime="); 1264 TimeUtils.formatDuration(nowElapsed - ongoingEvent.startTimeElapsed, pw); 1265 pw.print(" delta/sec="); 1266 pw.print(cakeToString(ongoingEvent.getDeltaPerSec())); 1267 final long ctp = ongoingEvent.getCtpPerSec(); 1268 if (ctp != 0) { 1269 pw.print(" ctp/sec="); 1270 pw.print(cakeToString(ongoingEvent.getCtpPerSec())); 1271 } 1272 pw.print(" refCount="); 1273 pw.print(ongoingEvent.refCount); 1274 pw.println(); 1275 } 1276 } 1277 1278 if (printedApp) { 1279 pw.decreaseIndent(); 1280 } 1281 } 1282 } 1283 if (!printedEvents) { 1284 pw.print("N/A"); 1285 } 1286 pw.decreaseIndent(); 1287 } 1288 } 1289