blob: a4043dd8ba78955152315ebfd035721c8cc1445b [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.parseCreditValue;
import static com.android.server.tare.Modifier.COST_MODIFIER_CHARGING;
import static com.android.server.tare.Modifier.COST_MODIFIER_DEVICE_IDLE;
import static com.android.server.tare.Modifier.COST_MODIFIER_POWER_SAVE_MODE;
import static com.android.server.tare.Modifier.COST_MODIFIER_PROCESS_STATE;
import static com.android.server.tare.Modifier.NUM_COST_MODIFIERS;
import static com.android.server.tare.TareUtils.cakeToString;
import android.annotation.CallSuper;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.KeyValueListParser;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* An EconomicPolicy includes pricing information and daily ARC requirements and suggestions.
* Policies are defined per participating system service. This allows each service’s EconomicPolicy
* to be isolated while allowing the core economic system to scale across policies to achieve a
* logical system-wide value system.
*/
public abstract class EconomicPolicy {
private static final String TAG = "TARE-" + EconomicPolicy.class.getSimpleName();
private static final int SHIFT_TYPE = 30;
static final int MASK_TYPE = 0b11 << SHIFT_TYPE;
static final int TYPE_REGULATION = 0 << SHIFT_TYPE;
static final int TYPE_ACTION = 1 << SHIFT_TYPE;
static final int TYPE_REWARD = 2 << SHIFT_TYPE;
private static final int SHIFT_POLICY = 28;
static final int MASK_POLICY = 0b11 << SHIFT_POLICY;
static final int ALL_POLICIES = MASK_POLICY;
// Reserve 0 for the base/common policy.
public static final int POLICY_ALARM = 1 << SHIFT_POLICY;
public static final int POLICY_JOB = 2 << SHIFT_POLICY;
static final int MASK_EVENT = -1 ^ (MASK_TYPE | MASK_POLICY);
static final int REGULATION_BASIC_INCOME = TYPE_REGULATION | 0;
static final int REGULATION_BIRTHRIGHT = TYPE_REGULATION | 1;
static final int REGULATION_WEALTH_RECLAMATION = TYPE_REGULATION | 2;
static final int REGULATION_PROMOTION = TYPE_REGULATION | 3;
static final int REGULATION_DEMOTION = TYPE_REGULATION | 4;
/** App is fully restricted from running in the background. */
static final int REGULATION_BG_RESTRICTED = TYPE_REGULATION | 5;
static final int REGULATION_BG_UNRESTRICTED = TYPE_REGULATION | 6;
static final int REGULATION_FORCE_STOP = TYPE_REGULATION | 8;
static final int REWARD_NOTIFICATION_SEEN = TYPE_REWARD | 0;
static final int REWARD_NOTIFICATION_INTERACTION = TYPE_REWARD | 1;
static final int REWARD_TOP_ACTIVITY = TYPE_REWARD | 2;
static final int REWARD_WIDGET_INTERACTION = TYPE_REWARD | 3;
static final int REWARD_OTHER_USER_INTERACTION = TYPE_REWARD | 4;
@IntDef({
AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE,
AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_EXACT,
AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_INEXACT_ALLOW_WHILE_IDLE,
AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_INEXACT,
AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_EXACT_ALLOW_WHILE_IDLE,
AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_EXACT,
AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_INEXACT_ALLOW_WHILE_IDLE,
AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_INEXACT,
AlarmManagerEconomicPolicy.ACTION_ALARM_CLOCK,
JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START,
JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING,
JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START,
JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING,
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START,
JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING,
JobSchedulerEconomicPolicy.ACTION_JOB_LOW_START,
JobSchedulerEconomicPolicy.ACTION_JOB_LOW_RUNNING,
JobSchedulerEconomicPolicy.ACTION_JOB_MIN_START,
JobSchedulerEconomicPolicy.ACTION_JOB_MIN_RUNNING,
JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AppAction {
}
@IntDef({
TYPE_ACTION,
TYPE_REGULATION,
TYPE_REWARD,
})
@Retention(RetentionPolicy.SOURCE)
public @interface EventType {
}
@IntDef({
ALL_POLICIES,
POLICY_ALARM,
POLICY_JOB,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Policy {
}
@IntDef({
REWARD_TOP_ACTIVITY,
REWARD_NOTIFICATION_SEEN,
REWARD_NOTIFICATION_INTERACTION,
REWARD_WIDGET_INTERACTION,
REWARD_OTHER_USER_INTERACTION,
JobSchedulerEconomicPolicy.REWARD_APP_INSTALL,
})
@Retention(RetentionPolicy.SOURCE)
public @interface UtilityReward {
}
static class Action {
/** Unique id (including across policies) for this action. */
public final int id;
/**
* How many ARCs the system says it takes to perform this action.
*/
public final long costToProduce;
/**
* The base price to perform this action. If this is
* less than the {@link #costToProduce}, then the system should not perform
* the action unless a modifier lowers the cost to produce.
*/
public final long basePrice;
/**
* Whether the remaining stock limit affects an app's ability to perform this action.
* If {@code false}, then the action can be performed, even if the cost is higher
* than the remaining stock. This does not affect checking against an app's balance.
*/
public final boolean respectsStockLimit;
Action(int id, long costToProduce, long basePrice) {
this(id, costToProduce, basePrice, true);
}
Action(int id, long costToProduce, long basePrice, boolean respectsStockLimit) {
this.id = id;
this.costToProduce = costToProduce;
this.basePrice = basePrice;
this.respectsStockLimit = respectsStockLimit;
}
}
static class Reward {
/** Unique id (including across policies) for this reward. */
@UtilityReward
public final int id;
public final long instantReward;
/** Reward credited per second of ongoing activity. */
public final long ongoingRewardPerSecond;
/** The maximum amount an app can earn from this reward within a 24 hour period. */
public final long maxDailyReward;
Reward(int id, long instantReward, long ongoingReward, long maxDailyReward) {
this.id = id;
this.instantReward = instantReward;
this.ongoingRewardPerSecond = ongoingReward;
this.maxDailyReward = maxDailyReward;
}
}
static class Cost {
public final long costToProduce;
public final long price;
Cost(long costToProduce, long price) {
this.costToProduce = costToProduce;
this.price = price;
}
}
protected final InternalResourceService mIrs;
private static final Modifier[] COST_MODIFIER_BY_INDEX = new Modifier[NUM_COST_MODIFIERS];
EconomicPolicy(@NonNull InternalResourceService irs) {
mIrs = irs;
for (int mId : getCostModifiers()) {
initModifier(mId, irs);
}
}
@CallSuper
void setup(@NonNull DeviceConfig.Properties properties) {
for (int i = 0; i < NUM_COST_MODIFIERS; ++i) {
final Modifier modifier = COST_MODIFIER_BY_INDEX[i];
if (modifier != null) {
modifier.setup();
}
}
}
@CallSuper
void tearDown() {
for (int i = 0; i < NUM_COST_MODIFIERS; ++i) {
final Modifier modifier = COST_MODIFIER_BY_INDEX[i];
if (modifier != null) {
modifier.tearDown();
}
}
}
/**
* Returns the minimum suggested balance an app should have when the device is at 100% battery.
* This takes into account any exemptions the app may have.
*/
abstract long getMinSatiatedBalance(int userId, @NonNull String pkgName);
/**
* Returns the maximum balance an app should have when the device is at 100% battery. This
* exists to ensure that no single app accumulate all available resources and increases fairness
* for all apps.
*/
abstract long getMaxSatiatedBalance(int userId, @NonNull String pkgName);
/**
* Returns the maximum number of cakes that should be consumed during a full 100% discharge
* cycle. This is the initial limit. The system may choose to increase the limit over time,
* but the increased limit should never exceed the value returned from
* {@link #getMaxSatiatedConsumptionLimit()}.
*/
abstract long getInitialSatiatedConsumptionLimit();
/**
* Returns the minimum number of cakes that should be available for consumption during a full
* 100% discharge cycle.
*/
abstract long getMinSatiatedConsumptionLimit();
/**
* Returns the maximum number of cakes that should be available for consumption during a full
* 100% discharge cycle.
*/
abstract long getMaxSatiatedConsumptionLimit();
/** Return the set of modifiers that should apply to this policy's costs. */
@NonNull
abstract int[] getCostModifiers();
@Nullable
abstract Action getAction(@AppAction int actionId);
@Nullable
abstract Reward getReward(@UtilityReward int rewardId);
void dump(IndentingPrintWriter pw) {
}
@NonNull
final Cost getCostOfAction(int actionId, int userId, @NonNull String pkgName) {
final Action action = getAction(actionId);
if (action == null || mIrs.isVip(userId, pkgName)) {
return new Cost(0, 0);
}
long ctp = action.costToProduce;
long price = action.basePrice;
final int[] costModifiers = getCostModifiers();
boolean useProcessStatePriceDeterminant = false;
for (int costModifier : costModifiers) {
if (costModifier == COST_MODIFIER_PROCESS_STATE) {
useProcessStatePriceDeterminant = true;
} else {
final Modifier modifier = getModifier(costModifier);
ctp = modifier.getModifiedCostToProduce(ctp);
price = modifier.getModifiedPrice(price);
}
}
// ProcessStateModifier needs to be done last.
if (useProcessStatePriceDeterminant) {
ProcessStateModifier processStateModifier =
(ProcessStateModifier) getModifier(COST_MODIFIER_PROCESS_STATE);
price = processStateModifier.getModifiedPrice(userId, pkgName, ctp, price);
}
return new Cost(ctp, price);
}
private static void initModifier(@Modifier.CostModifier final int modifierId,
@NonNull InternalResourceService irs) {
if (modifierId < 0 || modifierId >= COST_MODIFIER_BY_INDEX.length) {
throw new IllegalArgumentException("Invalid modifier id " + modifierId);
}
Modifier modifier = COST_MODIFIER_BY_INDEX[modifierId];
if (modifier == null) {
switch (modifierId) {
case COST_MODIFIER_CHARGING:
modifier = new ChargingModifier(irs);
break;
case COST_MODIFIER_DEVICE_IDLE:
modifier = new DeviceIdleModifier(irs);
break;
case COST_MODIFIER_POWER_SAVE_MODE:
modifier = new PowerSaveModeModifier(irs);
break;
case COST_MODIFIER_PROCESS_STATE:
modifier = new ProcessStateModifier(irs);
break;
default:
throw new IllegalArgumentException("Invalid modifier id " + modifierId);
}
COST_MODIFIER_BY_INDEX[modifierId] = modifier;
}
}
@NonNull
private static Modifier getModifier(@Modifier.CostModifier final int modifierId) {
if (modifierId < 0 || modifierId >= COST_MODIFIER_BY_INDEX.length) {
throw new IllegalArgumentException("Invalid modifier id " + modifierId);
}
final Modifier modifier = COST_MODIFIER_BY_INDEX[modifierId];
if (modifier == null) {
throw new IllegalStateException(
"Modifier #" + modifierId + " was never initialized");
}
return modifier;
}
@EventType
static int getEventType(int eventId) {
return eventId & MASK_TYPE;
}
static boolean isReward(int eventId) {
return getEventType(eventId) == TYPE_REWARD;
}
@NonNull
static String eventToString(int eventId) {
switch (eventId & MASK_TYPE) {
case TYPE_ACTION:
return actionToString(eventId);
case TYPE_REGULATION:
return regulationToString(eventId);
case TYPE_REWARD:
return rewardToString(eventId);
default:
return "UNKNOWN_EVENT:" + Integer.toHexString(eventId);
}
}
@NonNull
static String actionToString(int eventId) {
switch (eventId & MASK_POLICY) {
case POLICY_ALARM:
switch (eventId) {
case AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE:
return "ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE";
case AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_EXACT:
return "ALARM_WAKEUP_EXACT";
case AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_INEXACT_ALLOW_WHILE_IDLE:
return "ALARM_WAKEUP_INEXACT_ALLOW_WHILE_IDLE";
case AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_INEXACT:
return "ALARM_WAKEUP_INEXACT";
case AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_EXACT_ALLOW_WHILE_IDLE:
return "ALARM_NONWAKEUP_EXACT_ALLOW_WHILE_IDLE";
case AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_EXACT:
return "ALARM_NONWAKEUP_EXACT";
case AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_INEXACT_ALLOW_WHILE_IDLE:
return "ALARM_NONWAKEUP_INEXACT_ALLOW_WHILE_IDLE";
case AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_INEXACT:
return "ALARM_NONWAKEUP_INEXACT";
case AlarmManagerEconomicPolicy.ACTION_ALARM_CLOCK:
return "ALARM_CLOCK";
}
break;
case POLICY_JOB:
switch (eventId) {
case JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START:
return "JOB_MAX_START";
case JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING:
return "JOB_MAX_RUNNING";
case JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START:
return "JOB_HIGH_START";
case JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING:
return "JOB_HIGH_RUNNING";
case JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START:
return "JOB_DEFAULT_START";
case JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING:
return "JOB_DEFAULT_RUNNING";
case JobSchedulerEconomicPolicy.ACTION_JOB_LOW_START:
return "JOB_LOW_START";
case JobSchedulerEconomicPolicy.ACTION_JOB_LOW_RUNNING:
return "JOB_LOW_RUNNING";
case JobSchedulerEconomicPolicy.ACTION_JOB_MIN_START:
return "JOB_MIN_START";
case JobSchedulerEconomicPolicy.ACTION_JOB_MIN_RUNNING:
return "JOB_MIN_RUNNING";
case JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT:
return "JOB_TIMEOUT";
}
break;
}
return "UNKNOWN_ACTION:" + Integer.toHexString(eventId);
}
@NonNull
static String regulationToString(int eventId) {
switch (eventId) {
case REGULATION_BASIC_INCOME:
return "BASIC_INCOME";
case REGULATION_BIRTHRIGHT:
return "BIRTHRIGHT";
case REGULATION_WEALTH_RECLAMATION:
return "WEALTH_RECLAMATION";
case REGULATION_PROMOTION:
return "PROMOTION";
case REGULATION_DEMOTION:
return "DEMOTION";
case REGULATION_BG_RESTRICTED:
return "BG_RESTRICTED";
case REGULATION_BG_UNRESTRICTED:
return "BG_UNRESTRICTED";
case REGULATION_FORCE_STOP:
return "FORCE_STOP";
}
return "UNKNOWN_REGULATION:" + Integer.toHexString(eventId);
}
@NonNull
static String rewardToString(int eventId) {
switch (eventId) {
case REWARD_TOP_ACTIVITY:
return "REWARD_TOP_ACTIVITY";
case REWARD_NOTIFICATION_SEEN:
return "REWARD_NOTIFICATION_SEEN";
case REWARD_NOTIFICATION_INTERACTION:
return "REWARD_NOTIFICATION_INTERACTION";
case REWARD_WIDGET_INTERACTION:
return "REWARD_WIDGET_INTERACTION";
case REWARD_OTHER_USER_INTERACTION:
return "REWARD_OTHER_USER_INTERACTION";
case JobSchedulerEconomicPolicy.REWARD_APP_INSTALL:
return "REWARD_JOB_APP_INSTALL";
}
return "UNKNOWN_REWARD:" + Integer.toHexString(eventId);
}
protected long getConstantAsCake(@NonNull KeyValueListParser parser,
@Nullable DeviceConfig.Properties properties, String key, long defaultValCake) {
return getConstantAsCake(parser, properties, key, defaultValCake, 0);
}
protected long getConstantAsCake(@NonNull KeyValueListParser parser,
@Nullable DeviceConfig.Properties properties, String key, long defaultValCake,
long minValCake) {
// Don't cross the streams! Mixing Settings/local user config changes with DeviceConfig
// config can cause issues since the scales may be different, so use one or the other.
if (parser.size() > 0) {
// User settings take precedence. Just stick with the Settings constants, even if there
// are invalid values. It's not worth the time to evaluate all the key/value pairs to
// make sure there are valid ones before deciding.
return Math.max(minValCake,
parseCreditValue(parser.getString(key, null), defaultValCake));
}
if (properties != null) {
return Math.max(minValCake,
parseCreditValue(properties.getString(key, null), defaultValCake));
}
return Math.max(minValCake, defaultValCake);
}
@VisibleForTesting
static class Injector {
@Nullable
String getSettingsGlobalString(@NonNull ContentResolver resolver, @NonNull String name) {
return Settings.Global.getString(resolver, name);
}
}
protected static void dumpActiveModifiers(IndentingPrintWriter pw) {
for (int i = 0; i < NUM_COST_MODIFIERS; ++i) {
pw.print("Modifier ");
pw.println(i);
pw.increaseIndent();
Modifier modifier = COST_MODIFIER_BY_INDEX[i];
if (modifier != null) {
modifier.dump(pw);
} else {
pw.println("NOT ACTIVE");
}
pw.decreaseIndent();
}
}
protected static void dumpAction(IndentingPrintWriter pw, @NonNull Action action) {
pw.print(actionToString(action.id));
pw.print(": ");
pw.print("ctp=");
pw.print(cakeToString(action.costToProduce));
pw.print(", basePrice=");
pw.print(cakeToString(action.basePrice));
pw.println();
}
protected static void dumpReward(IndentingPrintWriter pw, @NonNull Reward reward) {
pw.print(rewardToString(reward.id));
pw.print(": ");
pw.print("instant=");
pw.print(cakeToString(reward.instantReward));
pw.print(", ongoing/sec=");
pw.print(cakeToString(reward.ongoingRewardPerSecond));
pw.print(", maxDaily=");
pw.print(cakeToString(reward.maxDailyReward));
pw.println();
}
}