blob: dcc324deaac61163a165d17132113ebf564e56a4 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.tare;
import static android.app.tare.EconomyManager.ENABLED_MODE_OFF;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static com.android.server.tare.EconomicPolicy.REGULATION_BASIC_INCOME;
import static com.android.server.tare.EconomicPolicy.REGULATION_BG_RESTRICTED;
import static com.android.server.tare.EconomicPolicy.REGULATION_BG_UNRESTRICTED;
import static com.android.server.tare.EconomicPolicy.REGULATION_BIRTHRIGHT;
import static com.android.server.tare.EconomicPolicy.REGULATION_DEMOTION;
import static com.android.server.tare.EconomicPolicy.REGULATION_PROMOTION;
import static com.android.server.tare.EconomicPolicy.REGULATION_WEALTH_RECLAMATION;
import static com.android.server.tare.EconomicPolicy.TYPE_ACTION;
import static com.android.server.tare.EconomicPolicy.TYPE_REWARD;
import static com.android.server.tare.EconomicPolicy.eventToString;
import static com.android.server.tare.EconomicPolicy.getEventType;
import static com.android.server.tare.TareUtils.appToString;
import static com.android.server.tare.TareUtils.cakeToString;
import static com.android.server.tare.TareUtils.getCurrentTimeMillis;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.UserPackage;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArrayMap;
import android.util.SparseSetArray;
import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.usage.AppStandbyInternal;
import com.android.server.utils.AlarmQueue;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Other half of the IRS. The agent handles the nitty gritty details, interacting directly with
* ledgers, carrying out specific events such as wealth reclamation, granting initial balances or
* replenishing balances, and tracking ongoing events.
*/
class Agent {
private static final String TAG = "TARE-" + Agent.class.getSimpleName();
private static final boolean DEBUG = InternalResourceService.DEBUG
|| Log.isLoggable(TAG, Log.DEBUG);
private static final String ALARM_TAG_AFFORDABILITY_CHECK = "*tare.affordability_check*";
private final Object mLock;
private final Handler mHandler;
private final Analyst mAnalyst;
private final InternalResourceService mIrs;
private final Scribe mScribe;
private final AppStandbyInternal mAppStandbyInternal;
@GuardedBy("mLock")
private final SparseArrayMap<String, SparseArrayMap<String, OngoingEvent>>
mCurrentOngoingEvents = new SparseArrayMap<>();
/**
* Set of {@link ActionAffordabilityNote ActionAffordabilityNotes} keyed by userId-pkgName.
*
* Note: it would be nice/better to sort by base price since that doesn't change and simply
* look at the change in the "insertion" of what would be affordable, but since CTP
* is factored into the final price, the sorting order (by modified price) could be different
* and that method wouldn't work >:(
*/
@GuardedBy("mLock")
private final SparseArrayMap<String, ArraySet<ActionAffordabilityNote>>
mActionAffordabilityNotes = new SparseArrayMap<>();
/**
* Queue to track and manage when apps will cross the closest affordability threshold (in
* both directions).
*/
@GuardedBy("mLock")
private final BalanceThresholdAlarmQueue mBalanceThresholdAlarmQueue;
/**
* Check the affordability notes of all apps.
*/
private static final int MSG_CHECK_ALL_AFFORDABILITY = 0;
/**
* Check the affordability notes of a single app.
*/
private static final int MSG_CHECK_INDIVIDUAL_AFFORDABILITY = 1;
Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst) {
mLock = irs.getLock();
mIrs = irs;
mScribe = scribe;
mAnalyst = analyst;
mHandler = new AgentHandler(TareHandlerThread.get().getLooper());
mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue(
mIrs.getContext(), TareHandlerThread.get().getLooper());
}
private class TotalDeltaCalculator implements Consumer<OngoingEvent> {
private Ledger mLedger;
private long mNowElapsed;
private long mNow;
private long mTotal;
void reset(@NonNull Ledger ledger, long nowElapsed, long now) {
mLedger = ledger;
mNowElapsed = nowElapsed;
mNow = now;
mTotal = 0;
}
@Override
public void accept(OngoingEvent ongoingEvent) {
mTotal += getActualDeltaLocked(ongoingEvent, mLedger, mNowElapsed, mNow).price;
}
}
@GuardedBy("mLock")
private final TotalDeltaCalculator mTotalDeltaCalculator = new TotalDeltaCalculator();
/** Get an app's current balance, factoring in any currently ongoing events. */
@GuardedBy("mLock")
long getBalanceLocked(final int userId, @NonNull final String pkgName) {
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
long balance = ledger.getCurrentBalance();
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents != null) {
final long nowElapsed = SystemClock.elapsedRealtime();
final long now = getCurrentTimeMillis();
mTotalDeltaCalculator.reset(ledger, nowElapsed, now);
ongoingEvents.forEach(mTotalDeltaCalculator);
balance += mTotalDeltaCalculator.mTotal;
}
return balance;
}
@GuardedBy("mLock")
private boolean isAffordableLocked(long balance, long price, long stockLimitHonoringCtp) {
return balance >= price
&& mScribe.getRemainingConsumableCakesLocked() >= stockLimitHonoringCtp;
}
@GuardedBy("mLock")
void noteInstantaneousEventLocked(final int userId, @NonNull final String pkgName,
final int eventId, @Nullable String tag) {
if (mIrs.isSystem(userId, pkgName)) {
// Events are free for the system. Don't bother recording them.
return;
}
final long now = getCurrentTimeMillis();
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
final int eventType = getEventType(eventId);
switch (eventType) {
case TYPE_ACTION:
final EconomicPolicy.Cost actionCost =
economicPolicy.getCostOfAction(eventId, userId, pkgName);
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, eventId, tag,
-actionCost.price, actionCost.costToProduce),
true);
break;
case TYPE_REWARD:
final EconomicPolicy.Reward reward = economicPolicy.getReward(eventId);
if (reward != null) {
final long rewardSum = ledger.get24HourSum(eventId, now);
final long rewardVal = Math.max(0,
Math.min(reward.maxDailyReward - rewardSum, reward.instantReward));
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, eventId, tag, rewardVal, 0), true);
}
break;
default:
Slog.w(TAG, "Unsupported event type: " + eventType);
}
scheduleBalanceCheckLocked(userId, pkgName);
}
@GuardedBy("mLock")
void noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId,
@Nullable String tag, final long startElapsed) {
noteOngoingEventLocked(userId, pkgName, eventId, tag, startElapsed, true);
}
@GuardedBy("mLock")
private void noteOngoingEventLocked(final int userId, @NonNull final String pkgName,
final int eventId, @Nullable String tag, final long startElapsed,
final boolean updateBalanceCheck) {
if (mIrs.isSystem(userId, pkgName)) {
// Events are free for the system. Don't bother recording them.
return;
}
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents == null) {
ongoingEvents = new SparseArrayMap<>();
mCurrentOngoingEvents.add(userId, pkgName, ongoingEvents);
}
OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag);
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
final int eventType = getEventType(eventId);
switch (eventType) {
case TYPE_ACTION:
final EconomicPolicy.Cost actionCost =
economicPolicy.getCostOfAction(eventId, userId, pkgName);
if (ongoingEvent == null) {
ongoingEvents.add(eventId, tag,
new OngoingEvent(eventId, tag, startElapsed, actionCost));
} else {
ongoingEvent.refCount++;
}
break;
case TYPE_REWARD:
final EconomicPolicy.Reward reward = economicPolicy.getReward(eventId);
if (reward != null) {
if (ongoingEvent == null) {
ongoingEvents.add(eventId, tag, new OngoingEvent(
eventId, tag, startElapsed, reward));
} else {
ongoingEvent.refCount++;
}
}
break;
default:
Slog.w(TAG, "Unsupported event type: " + eventType);
}
if (updateBalanceCheck) {
scheduleBalanceCheckLocked(userId, pkgName);
}
}
@GuardedBy("mLock")
void onDeviceStateChangedLocked() {
onPricingChangedLocked();
}
@GuardedBy("mLock")
void onPricingChangedLocked() {
onAnythingChangedLocked(true);
}
@GuardedBy("mLock")
void onAppStatesChangedLocked(final int userId, @NonNull ArraySet<String> pkgNames) {
final long now = getCurrentTimeMillis();
final long nowElapsed = SystemClock.elapsedRealtime();
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
for (int i = 0; i < pkgNames.size(); ++i) {
final String pkgName = pkgNames.valueAt(i);
final boolean isVip = mIrs.isVip(userId, pkgName, nowElapsed);
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents != null) {
mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed);
ongoingEvents.forEach(mOngoingEventUpdater);
final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
mActionAffordabilityNotes.get(userId, pkgName);
if (actionAffordabilityNotes != null) {
final int size = actionAffordabilityNotes.size();
final long newBalance =
mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance();
for (int n = 0; n < size; ++n) {
final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
note.recalculateCosts(economicPolicy, userId, pkgName);
final boolean isAffordable = isVip
|| isAffordableLocked(newBalance,
note.getCachedModifiedPrice(),
note.getStockLimitHonoringCtp());
if (note.isCurrentlyAffordable() != isAffordable) {
note.setNewAffordability(isAffordable);
mIrs.postAffordabilityChanged(userId, pkgName, note);
}
}
}
scheduleBalanceCheckLocked(userId, pkgName);
}
}
}
@GuardedBy("mLock")
void onVipStatusChangedLocked(final int userId, @NonNull String pkgName) {
final long now = getCurrentTimeMillis();
final long nowElapsed = SystemClock.elapsedRealtime();
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
final boolean isVip = mIrs.isVip(userId, pkgName, nowElapsed);
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents != null) {
mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed);
ongoingEvents.forEach(mOngoingEventUpdater);
}
final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
mActionAffordabilityNotes.get(userId, pkgName);
if (actionAffordabilityNotes != null) {
final int size = actionAffordabilityNotes.size();
final long newBalance =
mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance();
for (int n = 0; n < size; ++n) {
final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
note.recalculateCosts(economicPolicy, userId, pkgName);
final boolean isAffordable = isVip
|| isAffordableLocked(newBalance,
note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp());
if (note.isCurrentlyAffordable() != isAffordable) {
note.setNewAffordability(isAffordable);
mIrs.postAffordabilityChanged(userId, pkgName, note);
}
}
}
scheduleBalanceCheckLocked(userId, pkgName);
}
@GuardedBy("mLock")
void onVipStatusChangedLocked(@NonNull SparseSetArray<String> pkgs) {
for (int u = pkgs.size() - 1; u >= 0; --u) {
final int userId = pkgs.keyAt(u);
for (int p = pkgs.sizeAt(u) - 1; p >= 0; --p) {
onVipStatusChangedLocked(userId, pkgs.valueAt(u, p));
}
}
}
@GuardedBy("mLock")
private void onAnythingChangedLocked(final boolean updateOngoingEvents) {
final long now = getCurrentTimeMillis();
final long nowElapsed = SystemClock.elapsedRealtime();
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
for (int uIdx = mCurrentOngoingEvents.numMaps() - 1; uIdx >= 0; --uIdx) {
final int userId = mCurrentOngoingEvents.keyAt(uIdx);
for (int pIdx = mCurrentOngoingEvents.numElementsForKey(userId) - 1; pIdx >= 0;
--pIdx) {
final String pkgName = mCurrentOngoingEvents.keyAt(uIdx, pIdx);
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.valueAt(uIdx, pIdx);
if (ongoingEvents != null) {
if (updateOngoingEvents) {
mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed);
ongoingEvents.forEach(mOngoingEventUpdater);
}
scheduleBalanceCheckLocked(userId, pkgName);
}
}
}
for (int uIdx = mActionAffordabilityNotes.numMaps() - 1; uIdx >= 0; --uIdx) {
final int userId = mActionAffordabilityNotes.keyAt(uIdx);
for (int pIdx = mActionAffordabilityNotes.numElementsForKey(userId) - 1; pIdx >= 0;
--pIdx) {
final String pkgName = mActionAffordabilityNotes.keyAt(uIdx, pIdx);
final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
mActionAffordabilityNotes.valueAt(uIdx, pIdx);
if (actionAffordabilityNotes != null) {
final int size = actionAffordabilityNotes.size();
final long newBalance = getBalanceLocked(userId, pkgName);
final boolean isVip = mIrs.isVip(userId, pkgName, nowElapsed);
for (int n = 0; n < size; ++n) {
final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n);
note.recalculateCosts(economicPolicy, userId, pkgName);
final boolean isAffordable = isVip
|| isAffordableLocked(newBalance,
note.getCachedModifiedPrice(),
note.getStockLimitHonoringCtp());
if (note.isCurrentlyAffordable() != isAffordable) {
note.setNewAffordability(isAffordable);
mIrs.postAffordabilityChanged(userId, pkgName, note);
}
}
}
}
}
}
@GuardedBy("mLock")
void stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId,
@Nullable String tag, final long nowElapsed, final long now) {
stopOngoingActionLocked(userId, pkgName, eventId, tag, nowElapsed, now, true, true);
}
/**
* @param updateBalanceCheck Whether to reschedule the affordability/balance
* check alarm.
* @param notifyOnAffordabilityChange Whether to evaluate the app's ability to afford
* registered bills and notify listeners about any changes.
*/
@GuardedBy("mLock")
private void 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) {
if (mIrs.isSystem(userId, pkgName)) {
// Events are free for the system. Don't bother recording them.
return;
}
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents == null) {
// This may occur if TARE goes from disabled to enabled while an event is already
// occurring.
Slog.w(TAG, "No ongoing transactions for " + appToString(userId, pkgName));
return;
}
final OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag);
if (ongoingEvent == null) {
// This may occur if TARE goes from disabled to enabled while an event is already
// occurring.
Slog.w(TAG, "Nonexistent ongoing transaction "
+ eventToString(eventId) + (tag == null ? "" : ":" + tag)
+ " for " + appToString(userId, pkgName) + " ended");
return;
}
ongoingEvent.refCount--;
if (ongoingEvent.refCount <= 0) {
final long startElapsed = ongoingEvent.startTimeElapsed;
final long startTime = now - (nowElapsed - startElapsed);
final EconomicPolicy.Cost actualDelta =
getActualDeltaLocked(ongoingEvent, ledger, nowElapsed, now);
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(startTime, now, eventId, tag, actualDelta.price,
actualDelta.costToProduce),
notifyOnAffordabilityChange);
ongoingEvents.delete(eventId, tag);
}
if (updateBalanceCheck) {
scheduleBalanceCheckLocked(userId, pkgName);
}
}
@GuardedBy("mLock")
@NonNull
private EconomicPolicy.Cost getActualDeltaLocked(@NonNull OngoingEvent ongoingEvent,
@NonNull Ledger ledger, long nowElapsed, long now) {
final long startElapsed = ongoingEvent.startTimeElapsed;
final long durationSecs = (nowElapsed - startElapsed) / 1000;
final long computedDelta = durationSecs * ongoingEvent.getDeltaPerSec();
if (ongoingEvent.reward == null) {
return new EconomicPolicy.Cost(
durationSecs * ongoingEvent.getCtpPerSec(), computedDelta);
}
final long rewardSum = ledger.get24HourSum(ongoingEvent.eventId, now);
return new EconomicPolicy.Cost(0,
Math.max(0,
Math.min(ongoingEvent.reward.maxDailyReward - rewardSum, computedDelta)));
}
@VisibleForTesting
@GuardedBy("mLock")
void recordTransactionLocked(final int userId, @NonNull final String pkgName,
@NonNull Ledger ledger, @NonNull Ledger.Transaction transaction,
final boolean notifyOnAffordabilityChange) {
if (!DEBUG && transaction.delta == 0) {
// Skip recording transactions with a delta of 0 to save on space.
return;
}
if (mIrs.isSystem(userId, pkgName)) {
Slog.wtfStack(TAG,
"Tried to adjust system balance for " + appToString(userId, pkgName));
return;
}
final boolean isVip = mIrs.isVip(userId, pkgName);
if (isVip) {
// This could happen if the app was made a VIP after it started performing actions.
// Continue recording the transaction for debugging purposes, but don't let it change
// any numbers.
transaction = new Ledger.Transaction(
transaction.startTimeMs, transaction.endTimeMs,
transaction.eventId, transaction.tag, 0 /* delta */, transaction.ctp);
}
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
final long originalBalance = ledger.getCurrentBalance();
final long maxBalance = economicPolicy.getMaxSatiatedBalance(userId, pkgName);
if (transaction.delta > 0
&& originalBalance + transaction.delta > maxBalance) {
// Set lower bound at 0 so we don't accidentally take away credits when we were trying
// to _give_ the app credits.
final long newDelta = Math.max(0, maxBalance - originalBalance);
Slog.i(TAG, "Would result in becoming too rich. Decreasing transaction "
+ eventToString(transaction.eventId)
+ (transaction.tag == null ? "" : ":" + transaction.tag)
+ " for " + appToString(userId, pkgName)
+ " by " + cakeToString(transaction.delta - newDelta));
transaction = new Ledger.Transaction(
transaction.startTimeMs, transaction.endTimeMs,
transaction.eventId, transaction.tag, newDelta, transaction.ctp);
}
ledger.recordTransaction(transaction);
mScribe.adjustRemainingConsumableCakesLocked(-transaction.ctp);
mAnalyst.noteTransaction(transaction);
if (transaction.delta != 0 && notifyOnAffordabilityChange) {
final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
mActionAffordabilityNotes.get(userId, pkgName);
if (actionAffordabilityNotes != null) {
final long newBalance = ledger.getCurrentBalance();
for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
final boolean isAffordable = isVip
|| isAffordableLocked(newBalance,
note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp());
if (note.isCurrentlyAffordable() != isAffordable) {
note.setNewAffordability(isAffordable);
mIrs.postAffordabilityChanged(userId, pkgName, note);
}
}
}
}
if (transaction.ctp != 0) {
mHandler.sendEmptyMessage(MSG_CHECK_ALL_AFFORDABILITY);
mIrs.maybePerformQuantitativeEasingLocked();
}
}
@GuardedBy("mLock")
void reclaimAllAssetsLocked(final int userId, @NonNull final String pkgName, int regulationId) {
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
final long curBalance = ledger.getCurrentBalance();
if (curBalance <= 0) {
return;
}
if (DEBUG) {
Slog.i(TAG, "Reclaiming " + cakeToString(curBalance)
+ " from " + appToString(userId, pkgName)
+ " because of " + eventToString(regulationId));
}
final long now = getCurrentTimeMillis();
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, regulationId, null, -curBalance, 0),
true);
}
/**
* Reclaim a percentage of unused ARCs from every app that hasn't been used recently. The
* reclamation will not reduce an app's balance below its minimum balance as dictated by
* {@code scaleMinBalance}.
*
* @param percentage A value between 0 and 1 to indicate how much of the unused balance
* should be reclaimed.
* @param minUnusedTimeMs The minimum amount of time (in milliseconds) that must have
* transpired since the last user usage event before we will consider
* reclaiming ARCs from the app.
* @param scaleMinBalance Whether or not to use the scaled minimum app balance. If false,
* this will use the constant min balance floor given by
* {@link EconomicPolicy#getMinSatiatedBalance(int, String)}. If true,
* this will use the scaled balance given by
* {@link InternalResourceService#getMinBalanceLocked(int, String)}.
*/
@GuardedBy("mLock")
void reclaimUnusedAssetsLocked(double percentage, long minUnusedTimeMs,
boolean scaleMinBalance) {
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
final SparseArrayMap<String, Ledger> ledgers = mScribe.getLedgersLocked();
final long now = getCurrentTimeMillis();
for (int u = 0; u < ledgers.numMaps(); ++u) {
final int userId = ledgers.keyAt(u);
for (int p = 0; p < ledgers.numElementsForKey(userId); ++p) {
final Ledger ledger = ledgers.valueAt(u, p);
final long curBalance = ledger.getCurrentBalance();
if (curBalance <= 0) {
continue;
}
final String pkgName = ledgers.keyAt(u, p);
// AppStandby only counts elapsed time for things like this
// TODO: should we use clock time instead?
final long timeSinceLastUsedMs =
mAppStandbyInternal.getTimeSinceLastUsedByUser(pkgName, userId);
if (timeSinceLastUsedMs >= minUnusedTimeMs) {
final long minBalance;
if (!scaleMinBalance) {
// Use a constant floor instead of the scaled floor from the IRS.
minBalance = economicPolicy.getMinSatiatedBalance(userId, pkgName);
} else {
minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
}
long toReclaim = (long) (curBalance * percentage);
if (curBalance - toReclaim < minBalance) {
toReclaim = curBalance - minBalance;
}
if (toReclaim > 0) {
if (DEBUG) {
Slog.i(TAG, "Reclaiming unused wealth! Taking "
+ cakeToString(toReclaim)
+ " from " + appToString(userId, pkgName));
}
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, REGULATION_WEALTH_RECLAMATION,
null, -toReclaim, 0),
true);
}
}
}
}
}
/**
* Reclaim a percentage of unused ARCs from an app that was just removed from an exemption list.
* The amount reclaimed will depend on how recently the app was used. The reclamation will not
* reduce an app's balance below its current minimum balance.
*/
@GuardedBy("mLock")
void onAppUnexemptedLocked(final int userId, @NonNull final String pkgName) {
final long curBalance = getBalanceLocked(userId, pkgName);
final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
if (curBalance <= minBalance) {
return;
}
// AppStandby only counts elapsed time for things like this
// TODO: should we use clock time instead?
final long timeSinceLastUsedMs =
mAppStandbyInternal.getTimeSinceLastUsedByUser(pkgName, userId);
// The app is no longer exempted. We should take away some of credits so it's more in line
// with other non-exempt apps. However, don't take away as many credits if the app was used
// recently.
final double percentageToReclaim;
if (timeSinceLastUsedMs < DAY_IN_MILLIS) {
percentageToReclaim = .25;
} else if (timeSinceLastUsedMs < 2 * DAY_IN_MILLIS) {
percentageToReclaim = .5;
} else if (timeSinceLastUsedMs < 3 * DAY_IN_MILLIS) {
percentageToReclaim = .75;
} else {
percentageToReclaim = 1;
}
final long overage = curBalance - minBalance;
final long toReclaim = (long) (overage * percentageToReclaim);
if (toReclaim > 0) {
if (DEBUG) {
Slog.i(TAG, "Reclaiming bonus wealth! Taking " + toReclaim
+ " from " + appToString(userId, pkgName));
}
final long now = getCurrentTimeMillis();
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, REGULATION_DEMOTION, null, -toReclaim, 0),
true);
}
}
/**
* Reclaim all ARCs from an app that was just restricted.
*/
@GuardedBy("mLock")
void onAppRestrictedLocked(final int userId, @NonNull final String pkgName) {
reclaimAllAssetsLocked(userId, pkgName, REGULATION_BG_RESTRICTED);
}
/**
* Give an app that was just unrestricted some ARCs.
*/
@GuardedBy("mLock")
void onAppUnrestrictedLocked(final int userId, @NonNull final String pkgName) {
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
if (ledger.getCurrentBalance() > 0) {
Slog.wtf(TAG, "App " + pkgName + " had credits while it was restricted");
// App already got credits somehow. Move along.
return;
}
final long now = getCurrentTimeMillis();
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, REGULATION_BG_UNRESTRICTED, null,
mIrs.getMinBalanceLocked(userId, pkgName), 0), true);
}
/** Returns true if an app should be given credits in the general distributions. */
private boolean shouldGiveCredits(@NonNull InstalledPackageInfo packageInfo) {
// Skip apps that wouldn't be doing any work. Giving them ARCs would be wasteful.
if (!packageInfo.hasCode) {
return false;
}
final int userId = UserHandle.getUserId(packageInfo.uid);
// No point allocating ARCs to the system. It can do whatever it wants.
return !mIrs.isSystem(userId, packageInfo.packageName)
&& !mIrs.isPackageRestricted(userId, packageInfo.packageName);
}
void onCreditSupplyChanged() {
mHandler.sendEmptyMessage(MSG_CHECK_ALL_AFFORDABILITY);
}
@GuardedBy("mLock")
void distributeBasicIncomeLocked(int batteryLevel) {
final SparseArrayMap<String, InstalledPackageInfo> pkgs = mIrs.getInstalledPackages();
final long now = getCurrentTimeMillis();
for (int uIdx = pkgs.numMaps() - 1; uIdx >= 0; --uIdx) {
final int userId = pkgs.keyAt(uIdx);
for (int pIdx = pkgs.numElementsForKeyAt(uIdx) - 1; pIdx >= 0; --pIdx) {
final InstalledPackageInfo pkgInfo = pkgs.valueAt(uIdx, pIdx);
if (!shouldGiveCredits(pkgInfo)) {
continue;
}
final String pkgName = pkgInfo.packageName;
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
final double perc = batteryLevel / 100d;
// TODO: maybe don't give credits to bankrupt apps until battery level >= 50%
final long shortfall = minBalance - ledger.getCurrentBalance();
if (shortfall > 0) {
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, REGULATION_BASIC_INCOME,
null, (long) (perc * shortfall), 0), true);
}
}
}
}
/** Give each app an initial balance. */
@GuardedBy("mLock")
void grantBirthrightsLocked() {
UserManagerInternal userManagerInternal =
LocalServices.getService(UserManagerInternal.class);
final int[] userIds = userManagerInternal.getUserIds();
for (int userId : userIds) {
grantBirthrightsLocked(userId);
}
}
@GuardedBy("mLock")
void grantBirthrightsLocked(final int userId) {
final List<InstalledPackageInfo> pkgs = mIrs.getInstalledPackages(userId);
final long now = getCurrentTimeMillis();
for (int i = 0; i < pkgs.size(); ++i) {
final InstalledPackageInfo packageInfo = pkgs.get(i);
if (!shouldGiveCredits(packageInfo)) {
continue;
}
final String pkgName = packageInfo.packageName;
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
if (ledger.getCurrentBalance() > 0) {
// App already got credits somehow. Move along.
Slog.wtf(TAG, "App " + pkgName + " had credits before economy was set up");
continue;
}
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, REGULATION_BIRTHRIGHT, null,
mIrs.getMinBalanceLocked(userId, pkgName), 0),
true);
}
}
@GuardedBy("mLock")
void grantBirthrightLocked(final int userId, @NonNull final String pkgName) {
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
if (ledger.getCurrentBalance() > 0) {
Slog.wtf(TAG, "App " + pkgName + " had credits as soon as it was installed");
// App already got credits somehow. Move along.
return;
}
final long now = getCurrentTimeMillis();
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, REGULATION_BIRTHRIGHT, null,
mIrs.getMinBalanceLocked(userId, pkgName), 0), true);
}
@GuardedBy("mLock")
void onAppExemptedLocked(final int userId, @NonNull final String pkgName) {
final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName);
final long missing = minBalance - getBalanceLocked(userId, pkgName);
if (missing <= 0) {
return;
}
final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
final long now = getCurrentTimeMillis();
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(now, now, REGULATION_PROMOTION, null, missing, 0), true);
}
@GuardedBy("mLock")
void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) {
mScribe.discardLedgerLocked(userId, pkgName);
mCurrentOngoingEvents.delete(userId, pkgName);
mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
@GuardedBy("mLock")
void onUserRemovedLocked(final int userId) {
mCurrentOngoingEvents.delete(userId);
mBalanceThresholdAlarmQueue.removeAlarmsForUserId(userId);
}
@VisibleForTesting
static class TrendCalculator implements Consumer<OngoingEvent> {
static final long WILL_NOT_CROSS_THRESHOLD = -1;
private long mCurBalance;
private long mRemainingConsumableCredits;
/**
* The maximum change in credits per second towards the upper threshold
* {@link #mUpperThreshold}. A value of 0 means the current ongoing events will never
* result in the app crossing the upper threshold.
*/
private long mMaxDeltaPerSecToUpperThreshold;
/**
* The maximum change in credits per second towards the lower threshold
* {@link #mLowerThreshold}. A value of 0 means the current ongoing events will never
* result in the app crossing the lower threshold.
*/
private long mMaxDeltaPerSecToLowerThreshold;
/**
* The maximum change in credits per second towards the highest CTP threshold below the
* remaining consumable credits (cached in {@link #mCtpThreshold}). A value of 0 means
* the current ongoing events will never result in the app crossing the lower threshold.
*/
private long mMaxDeltaPerSecToCtpThreshold;
private long mUpperThreshold;
private long mLowerThreshold;
private long mCtpThreshold;
void reset(long curBalance, long remainingConsumableCredits,
@Nullable ArraySet<ActionAffordabilityNote> actionAffordabilityNotes) {
mCurBalance = curBalance;
mRemainingConsumableCredits = remainingConsumableCredits;
mMaxDeltaPerSecToUpperThreshold = mMaxDeltaPerSecToLowerThreshold = 0;
mMaxDeltaPerSecToCtpThreshold = 0;
mUpperThreshold = Long.MIN_VALUE;
mLowerThreshold = Long.MAX_VALUE;
mCtpThreshold = 0;
if (actionAffordabilityNotes != null) {
for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i);
final long price = note.getCachedModifiedPrice();
if (price <= mCurBalance) {
mLowerThreshold = (mLowerThreshold == Long.MAX_VALUE)
? price : Math.max(mLowerThreshold, price);
} else {
mUpperThreshold = (mUpperThreshold == Long.MIN_VALUE)
? price : Math.min(mUpperThreshold, price);
}
final long ctp = note.getStockLimitHonoringCtp();
if (ctp <= mRemainingConsumableCredits) {
mCtpThreshold = Math.max(mCtpThreshold, ctp);
}
}
}
}
/**
* Returns the amount of time (in millisecond) it will take for the app to cross the next
* lowest action affordability note (compared to its current balance) based on current
* ongoing events.
* Returns {@link #WILL_NOT_CROSS_THRESHOLD} if the app will never cross the lowest
* threshold.
*/
long getTimeToCrossLowerThresholdMs() {
if (mMaxDeltaPerSecToLowerThreshold == 0 && mMaxDeltaPerSecToCtpThreshold == 0) {
// Will never cross lower threshold based on current events.
return WILL_NOT_CROSS_THRESHOLD;
}
long minSeconds = Long.MAX_VALUE;
if (mMaxDeltaPerSecToLowerThreshold != 0) {
// deltaPerSec is a negative value, so do threshold-balance to cancel out the
// negative.
minSeconds = (mLowerThreshold - mCurBalance) / mMaxDeltaPerSecToLowerThreshold;
}
if (mMaxDeltaPerSecToCtpThreshold != 0) {
minSeconds = Math.min(minSeconds,
// deltaPerSec is a negative value, so do threshold-balance to cancel
// out the negative.
(mCtpThreshold - mRemainingConsumableCredits)
/ mMaxDeltaPerSecToCtpThreshold);
}
return minSeconds * 1000;
}
/**
* Returns the amount of time (in millisecond) it will take for the app to cross the next
* highest action affordability note (compared to its current balance) based on current
* ongoing events.
* Returns {@link #WILL_NOT_CROSS_THRESHOLD} if the app will never cross the upper
* threshold.
*/
long getTimeToCrossUpperThresholdMs() {
if (mMaxDeltaPerSecToUpperThreshold == 0) {
// Will never cross upper threshold based on current events.
return WILL_NOT_CROSS_THRESHOLD;
}
final long minSeconds =
(mUpperThreshold - mCurBalance) / mMaxDeltaPerSecToUpperThreshold;
return minSeconds * 1000;
}
@Override
public void accept(OngoingEvent ongoingEvent) {
final long deltaPerSec = ongoingEvent.getDeltaPerSec();
if (mCurBalance >= mLowerThreshold && deltaPerSec < 0) {
mMaxDeltaPerSecToLowerThreshold += deltaPerSec;
} else if (mCurBalance < mUpperThreshold && deltaPerSec > 0) {
mMaxDeltaPerSecToUpperThreshold += deltaPerSec;
}
final long ctpPerSec = ongoingEvent.getCtpPerSec();
if (mRemainingConsumableCredits >= mCtpThreshold && deltaPerSec < 0) {
mMaxDeltaPerSecToCtpThreshold -= ctpPerSec;
}
}
}
@GuardedBy("mLock")
private final TrendCalculator mTrendCalculator = new TrendCalculator();
@GuardedBy("mLock")
private void scheduleBalanceCheckLocked(final int userId, @NonNull final String pkgName) {
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents == null || mIrs.isVip(userId, pkgName)) {
// No ongoing transactions. No reason to schedule
mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
return;
}
mTrendCalculator.reset(getBalanceLocked(userId, pkgName),
mScribe.getRemainingConsumableCakesLocked(),
mActionAffordabilityNotes.get(userId, pkgName));
ongoingEvents.forEach(mTrendCalculator);
final long lowerTimeMs = mTrendCalculator.getTimeToCrossLowerThresholdMs();
final long upperTimeMs = mTrendCalculator.getTimeToCrossUpperThresholdMs();
final long timeToThresholdMs;
if (lowerTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
if (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
// Will never cross a threshold based on current events.
mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
return;
}
timeToThresholdMs = upperTimeMs;
} else {
timeToThresholdMs = (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD)
? lowerTimeMs : Math.min(lowerTimeMs, upperTimeMs);
}
mBalanceThresholdAlarmQueue.addAlarm(UserPackage.of(userId, pkgName),
SystemClock.elapsedRealtime() + timeToThresholdMs);
}
@GuardedBy("mLock")
void tearDownLocked() {
mCurrentOngoingEvents.clear();
mBalanceThresholdAlarmQueue.removeAllAlarms();
}
@VisibleForTesting
static class OngoingEvent {
public final long startTimeElapsed;
public final int eventId;
@Nullable
public final String tag;
@Nullable
public final EconomicPolicy.Reward reward;
@Nullable
public final EconomicPolicy.Cost actionCost;
public int refCount;
OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed,
@NonNull EconomicPolicy.Reward reward) {
this.startTimeElapsed = startTimeElapsed;
this.eventId = eventId;
this.tag = tag;
this.reward = reward;
this.actionCost = null;
refCount = 1;
}
OngoingEvent(int eventId, @Nullable String tag, long startTimeElapsed,
@NonNull EconomicPolicy.Cost actionCost) {
this.startTimeElapsed = startTimeElapsed;
this.eventId = eventId;
this.tag = tag;
this.reward = null;
this.actionCost = actionCost;
refCount = 1;
}
long getDeltaPerSec() {
if (actionCost != null) {
return -actionCost.price;
}
if (reward != null) {
return reward.ongoingRewardPerSecond;
}
Slog.wtfStack(TAG, "No action or reward in ongoing event?!??!");
return 0;
}
long getCtpPerSec() {
if (actionCost != null) {
return actionCost.costToProduce;
}
return 0;
}
}
private class OngoingEventUpdater implements Consumer<OngoingEvent> {
private int mUserId;
private String mPkgName;
private long mNow;
private long mNowElapsed;
private void reset(int userId, String pkgName, long now, long nowElapsed) {
mUserId = userId;
mPkgName = pkgName;
mNow = now;
mNowElapsed = nowElapsed;
}
@Override
public void accept(OngoingEvent ongoingEvent) {
// Disable balance check & affordability notifications here because
// we're in the middle of updating ongoing action costs/prices and
// sending out notifications or rescheduling the balance check alarm
// would be a waste since we'll have to redo them again after all of
// our internal state is updated.
final boolean updateBalanceCheck = false;
stopOngoingActionLocked(mUserId, mPkgName, ongoingEvent.eventId, ongoingEvent.tag,
mNowElapsed, mNow, updateBalanceCheck, /* notifyOnAffordabilityChange */ false);
noteOngoingEventLocked(mUserId, mPkgName, ongoingEvent.eventId, ongoingEvent.tag,
mNowElapsed, updateBalanceCheck);
}
}
private final OngoingEventUpdater mOngoingEventUpdater = new OngoingEventUpdater();
/** Track when apps will cross the closest affordability threshold (in both directions). */
private class BalanceThresholdAlarmQueue extends AlarmQueue<UserPackage> {
private BalanceThresholdAlarmQueue(Context context, Looper looper) {
super(context, looper, ALARM_TAG_AFFORDABILITY_CHECK, "Affordability check", true,
15_000L);
}
@Override
protected boolean isForUser(@NonNull UserPackage key, int userId) {
return key.userId == userId;
}
@Override
protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
for (int i = 0; i < expired.size(); ++i) {
UserPackage p = expired.valueAt(i);
mHandler.obtainMessage(
MSG_CHECK_INDIVIDUAL_AFFORDABILITY, p.userId, 0, p.packageName)
.sendToTarget();
}
}
}
@GuardedBy("mLock")
public void registerAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName,
@NonNull EconomyManagerInternal.AffordabilityChangeListener listener,
@NonNull EconomyManagerInternal.ActionBill bill) {
ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
mActionAffordabilityNotes.get(userId, pkgName);
if (actionAffordabilityNotes == null) {
actionAffordabilityNotes = new ArraySet<>();
mActionAffordabilityNotes.add(userId, pkgName, actionAffordabilityNotes);
}
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
final ActionAffordabilityNote note =
new ActionAffordabilityNote(bill, listener, economicPolicy);
if (actionAffordabilityNotes.add(note)) {
if (mIrs.getEnabledMode() == ENABLED_MODE_OFF) {
// When TARE isn't enabled, we always say something is affordable. We also don't
// want to silently drop affordability change listeners in case TARE becomes enabled
// because then clients will be in an ambiguous state.
note.setNewAffordability(true);
return;
}
final boolean isVip = mIrs.isVip(userId, pkgName);
note.recalculateCosts(economicPolicy, userId, pkgName);
note.setNewAffordability(isVip
|| isAffordableLocked(getBalanceLocked(userId, pkgName),
note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp()));
mIrs.postAffordabilityChanged(userId, pkgName, note);
// Update ongoing alarm
scheduleBalanceCheckLocked(userId, pkgName);
}
}
@GuardedBy("mLock")
public void unregisterAffordabilityChangeListenerLocked(int userId, @NonNull String pkgName,
@NonNull EconomyManagerInternal.AffordabilityChangeListener listener,
@NonNull EconomyManagerInternal.ActionBill bill) {
final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
mActionAffordabilityNotes.get(userId, pkgName);
if (actionAffordabilityNotes != null) {
final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked();
final ActionAffordabilityNote note =
new ActionAffordabilityNote(bill, listener, economicPolicy);
if (actionAffordabilityNotes.remove(note)) {
// Update ongoing alarm
scheduleBalanceCheckLocked(userId, pkgName);
}
}
}
static final class ActionAffordabilityNote {
private final EconomyManagerInternal.ActionBill mActionBill;
private final EconomyManagerInternal.AffordabilityChangeListener mListener;
private long mStockLimitHonoringCtp;
private long mModifiedPrice;
private boolean mIsAffordable;
@VisibleForTesting
ActionAffordabilityNote(@NonNull EconomyManagerInternal.ActionBill bill,
@NonNull EconomyManagerInternal.AffordabilityChangeListener listener,
@NonNull EconomicPolicy economicPolicy) {
mActionBill = bill;
final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions =
bill.getAnticipatedActions();
for (int i = 0; i < anticipatedActions.size(); ++i) {
final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i);
final EconomicPolicy.Action action = economicPolicy.getAction(aa.actionId);
if (action == null) {
if ((aa.actionId & EconomicPolicy.ALL_POLICIES) == 0) {
throw new IllegalArgumentException("Invalid action id: " + aa.actionId);
} else {
Slog.w(TAG, "Tracking disabled policy's action? " + aa.actionId);
}
}
}
mListener = listener;
}
@NonNull
EconomyManagerInternal.ActionBill getActionBill() {
return mActionBill;
}
@NonNull
EconomyManagerInternal.AffordabilityChangeListener getListener() {
return mListener;
}
private long getCachedModifiedPrice() {
return mModifiedPrice;
}
/** Returns the cumulative CTP of actions in this note that respect the stock limit. */
private long getStockLimitHonoringCtp() {
return mStockLimitHonoringCtp;
}
@VisibleForTesting
void recalculateCosts(@NonNull EconomicPolicy economicPolicy,
int userId, @NonNull String pkgName) {
long modifiedPrice = 0;
long stockLimitHonoringCtp = 0;
final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions =
mActionBill.getAnticipatedActions();
for (int i = 0; i < anticipatedActions.size(); ++i) {
final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i);
final EconomicPolicy.Action action = economicPolicy.getAction(aa.actionId);
final EconomicPolicy.Cost actionCost =
economicPolicy.getCostOfAction(aa.actionId, userId, pkgName);
modifiedPrice += actionCost.price * aa.numInstantaneousCalls
+ actionCost.price * (aa.ongoingDurationMs / 1000);
if (action.respectsStockLimit) {
stockLimitHonoringCtp +=
actionCost.costToProduce * aa.numInstantaneousCalls
+ actionCost.costToProduce * (aa.ongoingDurationMs / 1000);
}
}
mModifiedPrice = modifiedPrice;
mStockLimitHonoringCtp = stockLimitHonoringCtp;
}
boolean isCurrentlyAffordable() {
return mIsAffordable;
}
private void setNewAffordability(boolean isAffordable) {
mIsAffordable = isAffordable;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ActionAffordabilityNote)) return false;
ActionAffordabilityNote other = (ActionAffordabilityNote) o;
return mActionBill.equals(other.mActionBill)
&& mListener.equals(other.mListener);
}
@Override
public int hashCode() {
int hash = 0;
hash = 31 * hash + Objects.hash(mListener);
hash = 31 * hash + mActionBill.hashCode();
return hash;
}
}
private final class AgentHandler extends Handler {
AgentHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_CHECK_ALL_AFFORDABILITY: {
synchronized (mLock) {
removeMessages(MSG_CHECK_ALL_AFFORDABILITY);
onAnythingChangedLocked(false);
}
}
break;
case MSG_CHECK_INDIVIDUAL_AFFORDABILITY: {
final int userId = msg.arg1;
final String pkgName = (String) msg.obj;
synchronized (mLock) {
final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
mActionAffordabilityNotes.get(userId, pkgName);
if (actionAffordabilityNotes != null
&& actionAffordabilityNotes.size() > 0) {
final long newBalance = getBalanceLocked(userId, pkgName);
final boolean isVip = mIrs.isVip(userId, pkgName);
for (int i = 0; i < actionAffordabilityNotes.size(); ++i) {
final ActionAffordabilityNote note =
actionAffordabilityNotes.valueAt(i);
final boolean isAffordable = isVip || isAffordableLocked(
newBalance, note.getCachedModifiedPrice(),
note.getStockLimitHonoringCtp());
if (note.isCurrentlyAffordable() != isAffordable) {
note.setNewAffordability(isAffordable);
mIrs.postAffordabilityChanged(userId, pkgName, note);
}
}
}
scheduleBalanceCheckLocked(userId, pkgName);
}
}
break;
}
}
}
@GuardedBy("mLock")
void dumpLocked(IndentingPrintWriter pw) {
mBalanceThresholdAlarmQueue.dump(pw);
pw.println();
pw.println("Ongoing events:");
pw.increaseIndent();
boolean printedEvents = false;
final long nowElapsed = SystemClock.elapsedRealtime();
for (int u = mCurrentOngoingEvents.numMaps() - 1; u >= 0; --u) {
final int userId = mCurrentOngoingEvents.keyAt(u);
for (int p = mCurrentOngoingEvents.numElementsForKey(userId) - 1; p >= 0; --p) {
final String pkgName = mCurrentOngoingEvents.keyAt(u, p);
final SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
boolean printedApp = false;
for (int e = ongoingEvents.numMaps() - 1; e >= 0; --e) {
final int eventId = ongoingEvents.keyAt(e);
for (int t = ongoingEvents.numElementsForKey(eventId) - 1; t >= 0; --t) {
if (!printedApp) {
printedApp = true;
pw.println(appToString(userId, pkgName));
pw.increaseIndent();
}
printedEvents = true;
OngoingEvent ongoingEvent = ongoingEvents.valueAt(e, t);
pw.print(EconomicPolicy.eventToString(ongoingEvent.eventId));
if (ongoingEvent.tag != null) {
pw.print("(");
pw.print(ongoingEvent.tag);
pw.print(")");
}
pw.print(" runtime=");
TimeUtils.formatDuration(nowElapsed - ongoingEvent.startTimeElapsed, pw);
pw.print(" delta/sec=");
pw.print(cakeToString(ongoingEvent.getDeltaPerSec()));
final long ctp = ongoingEvent.getCtpPerSec();
if (ctp != 0) {
pw.print(" ctp/sec=");
pw.print(cakeToString(ongoingEvent.getCtpPerSec()));
}
pw.print(" refCount=");
pw.print(ongoingEvent.refCount);
pw.println();
}
}
if (printedApp) {
pw.decreaseIndent();
}
}
}
if (!printedEvents) {
pw.print("N/A");
}
pw.decreaseIndent();
}
}