| /* |
| * 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(); |
| } |
| } |