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