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.parseCreditValue; 20 21 import static com.android.server.tare.Modifier.COST_MODIFIER_CHARGING; 22 import static com.android.server.tare.Modifier.COST_MODIFIER_DEVICE_IDLE; 23 import static com.android.server.tare.Modifier.COST_MODIFIER_POWER_SAVE_MODE; 24 import static com.android.server.tare.Modifier.COST_MODIFIER_PROCESS_STATE; 25 import static com.android.server.tare.Modifier.NUM_COST_MODIFIERS; 26 import static com.android.server.tare.TareUtils.cakeToString; 27 28 import android.annotation.CallSuper; 29 import android.annotation.IntDef; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.content.ContentResolver; 33 import android.provider.DeviceConfig; 34 import android.provider.Settings; 35 import android.util.IndentingPrintWriter; 36 import android.util.KeyValueListParser; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 40 import java.lang.annotation.Retention; 41 import java.lang.annotation.RetentionPolicy; 42 43 /** 44 * An EconomicPolicy includes pricing information and daily ARC requirements and suggestions. 45 * Policies are defined per participating system service. This allows each service’s EconomicPolicy 46 * to be isolated while allowing the core economic system to scale across policies to achieve a 47 * logical system-wide value system. 48 */ 49 public abstract class EconomicPolicy { 50 private static final String TAG = "TARE-" + EconomicPolicy.class.getSimpleName(); 51 52 private static final int SHIFT_TYPE = 30; 53 static final int MASK_TYPE = 0b11 << SHIFT_TYPE; 54 static final int TYPE_REGULATION = 0 << SHIFT_TYPE; 55 static final int TYPE_ACTION = 1 << SHIFT_TYPE; 56 static final int TYPE_REWARD = 2 << SHIFT_TYPE; 57 58 private static final int SHIFT_POLICY = 28; 59 static final int MASK_POLICY = 0b11 << SHIFT_POLICY; 60 static final int ALL_POLICIES = MASK_POLICY; 61 // Reserve 0 for the base/common policy. 62 public static final int POLICY_ALARM = 1 << SHIFT_POLICY; 63 public static final int POLICY_JOB = 2 << SHIFT_POLICY; 64 65 static final int MASK_EVENT = -1 ^ (MASK_TYPE | MASK_POLICY); 66 67 static final int REGULATION_BASIC_INCOME = TYPE_REGULATION | 0; 68 static final int REGULATION_BIRTHRIGHT = TYPE_REGULATION | 1; 69 static final int REGULATION_WEALTH_RECLAMATION = TYPE_REGULATION | 2; 70 static final int REGULATION_PROMOTION = TYPE_REGULATION | 3; 71 static final int REGULATION_DEMOTION = TYPE_REGULATION | 4; 72 /** App is fully restricted from running in the background. */ 73 static final int REGULATION_BG_RESTRICTED = TYPE_REGULATION | 5; 74 static final int REGULATION_BG_UNRESTRICTED = TYPE_REGULATION | 6; 75 static final int REGULATION_FORCE_STOP = TYPE_REGULATION | 8; 76 77 static final int REWARD_NOTIFICATION_SEEN = TYPE_REWARD | 0; 78 static final int REWARD_NOTIFICATION_INTERACTION = TYPE_REWARD | 1; 79 static final int REWARD_TOP_ACTIVITY = TYPE_REWARD | 2; 80 static final int REWARD_WIDGET_INTERACTION = TYPE_REWARD | 3; 81 static final int REWARD_OTHER_USER_INTERACTION = TYPE_REWARD | 4; 82 83 @IntDef({ 84 AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE, 85 AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_EXACT, 86 AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_INEXACT_ALLOW_WHILE_IDLE, 87 AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_INEXACT, 88 AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_EXACT_ALLOW_WHILE_IDLE, 89 AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_EXACT, 90 AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_INEXACT_ALLOW_WHILE_IDLE, 91 AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_INEXACT, 92 AlarmManagerEconomicPolicy.ACTION_ALARM_CLOCK, 93 JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START, 94 JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING, 95 JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START, 96 JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING, 97 JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START, 98 JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING, 99 JobSchedulerEconomicPolicy.ACTION_JOB_LOW_START, 100 JobSchedulerEconomicPolicy.ACTION_JOB_LOW_RUNNING, 101 JobSchedulerEconomicPolicy.ACTION_JOB_MIN_START, 102 JobSchedulerEconomicPolicy.ACTION_JOB_MIN_RUNNING, 103 JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT, 104 }) 105 @Retention(RetentionPolicy.SOURCE) 106 public @interface AppAction { 107 } 108 109 @IntDef({ 110 TYPE_ACTION, 111 TYPE_REGULATION, 112 TYPE_REWARD, 113 }) 114 @Retention(RetentionPolicy.SOURCE) 115 public @interface EventType { 116 } 117 118 @IntDef({ 119 ALL_POLICIES, 120 POLICY_ALARM, 121 POLICY_JOB, 122 }) 123 @Retention(RetentionPolicy.SOURCE) 124 public @interface Policy { 125 } 126 127 @IntDef({ 128 REWARD_TOP_ACTIVITY, 129 REWARD_NOTIFICATION_SEEN, 130 REWARD_NOTIFICATION_INTERACTION, 131 REWARD_WIDGET_INTERACTION, 132 REWARD_OTHER_USER_INTERACTION, 133 JobSchedulerEconomicPolicy.REWARD_APP_INSTALL, 134 }) 135 @Retention(RetentionPolicy.SOURCE) 136 public @interface UtilityReward { 137 } 138 139 static class Action { 140 /** Unique id (including across policies) for this action. */ 141 public final int id; 142 /** 143 * How many ARCs the system says it takes to perform this action. 144 */ 145 public final long costToProduce; 146 /** 147 * The base price to perform this action. If this is 148 * less than the {@link #costToProduce}, then the system should not perform 149 * the action unless a modifier lowers the cost to produce. 150 */ 151 public final long basePrice; 152 /** 153 * Whether the remaining stock limit affects an app's ability to perform this action. 154 * If {@code false}, then the action can be performed, even if the cost is higher 155 * than the remaining stock. This does not affect checking against an app's balance. 156 */ 157 public final boolean respectsStockLimit; 158 Action(int id, long costToProduce, long basePrice)159 Action(int id, long costToProduce, long basePrice) { 160 this(id, costToProduce, basePrice, true); 161 } 162 Action(int id, long costToProduce, long basePrice, boolean respectsStockLimit)163 Action(int id, long costToProduce, long basePrice, boolean respectsStockLimit) { 164 this.id = id; 165 this.costToProduce = costToProduce; 166 this.basePrice = basePrice; 167 this.respectsStockLimit = respectsStockLimit; 168 } 169 } 170 171 static class Reward { 172 /** Unique id (including across policies) for this reward. */ 173 @UtilityReward 174 public final int id; 175 public final long instantReward; 176 /** Reward credited per second of ongoing activity. */ 177 public final long ongoingRewardPerSecond; 178 /** The maximum amount an app can earn from this reward within a 24 hour period. */ 179 public final long maxDailyReward; 180 Reward(int id, long instantReward, long ongoingReward, long maxDailyReward)181 Reward(int id, long instantReward, long ongoingReward, long maxDailyReward) { 182 this.id = id; 183 this.instantReward = instantReward; 184 this.ongoingRewardPerSecond = ongoingReward; 185 this.maxDailyReward = maxDailyReward; 186 } 187 } 188 189 static class Cost { 190 public final long costToProduce; 191 public final long price; 192 Cost(long costToProduce, long price)193 Cost(long costToProduce, long price) { 194 this.costToProduce = costToProduce; 195 this.price = price; 196 } 197 } 198 199 protected final InternalResourceService mIrs; 200 private static final Modifier[] COST_MODIFIER_BY_INDEX = new Modifier[NUM_COST_MODIFIERS]; 201 EconomicPolicy(@onNull InternalResourceService irs)202 EconomicPolicy(@NonNull InternalResourceService irs) { 203 mIrs = irs; 204 for (int mId : getCostModifiers()) { 205 initModifier(mId, irs); 206 } 207 } 208 209 @CallSuper setup(@onNull DeviceConfig.Properties properties)210 void setup(@NonNull DeviceConfig.Properties properties) { 211 for (int i = 0; i < NUM_COST_MODIFIERS; ++i) { 212 final Modifier modifier = COST_MODIFIER_BY_INDEX[i]; 213 if (modifier != null) { 214 modifier.setup(); 215 } 216 } 217 } 218 219 @CallSuper tearDown()220 void tearDown() { 221 for (int i = 0; i < NUM_COST_MODIFIERS; ++i) { 222 final Modifier modifier = COST_MODIFIER_BY_INDEX[i]; 223 if (modifier != null) { 224 modifier.tearDown(); 225 } 226 } 227 } 228 229 /** 230 * Returns the minimum suggested balance an app should have when the device is at 100% battery. 231 * This takes into account any exemptions the app may have. 232 */ getMinSatiatedBalance(int userId, @NonNull String pkgName)233 abstract long getMinSatiatedBalance(int userId, @NonNull String pkgName); 234 235 /** 236 * Returns the maximum balance an app should have when the device is at 100% battery. This 237 * exists to ensure that no single app accumulate all available resources and increases fairness 238 * for all apps. 239 */ getMaxSatiatedBalance(int userId, @NonNull String pkgName)240 abstract long getMaxSatiatedBalance(int userId, @NonNull String pkgName); 241 242 /** 243 * Returns the maximum number of cakes that should be consumed during a full 100% discharge 244 * cycle. This is the initial limit. The system may choose to increase the limit over time, 245 * but the increased limit should never exceed the value returned from 246 * {@link #getMaxSatiatedConsumptionLimit()}. 247 */ getInitialSatiatedConsumptionLimit()248 abstract long getInitialSatiatedConsumptionLimit(); 249 250 /** 251 * Returns the minimum number of cakes that should be available for consumption during a full 252 * 100% discharge cycle. 253 */ getMinSatiatedConsumptionLimit()254 abstract long getMinSatiatedConsumptionLimit(); 255 256 /** 257 * Returns the maximum number of cakes that should be available for consumption during a full 258 * 100% discharge cycle. 259 */ getMaxSatiatedConsumptionLimit()260 abstract long getMaxSatiatedConsumptionLimit(); 261 262 /** Return the set of modifiers that should apply to this policy's costs. */ 263 @NonNull getCostModifiers()264 abstract int[] getCostModifiers(); 265 266 @Nullable getAction(@ppAction int actionId)267 abstract Action getAction(@AppAction int actionId); 268 269 @Nullable getReward(@tilityReward int rewardId)270 abstract Reward getReward(@UtilityReward int rewardId); 271 dump(IndentingPrintWriter pw)272 void dump(IndentingPrintWriter pw) { 273 } 274 275 @NonNull getCostOfAction(int actionId, int userId, @NonNull String pkgName)276 final Cost getCostOfAction(int actionId, int userId, @NonNull String pkgName) { 277 final Action action = getAction(actionId); 278 if (action == null || mIrs.isVip(userId, pkgName)) { 279 return new Cost(0, 0); 280 } 281 long ctp = action.costToProduce; 282 long price = action.basePrice; 283 final int[] costModifiers = getCostModifiers(); 284 boolean useProcessStatePriceDeterminant = false; 285 for (int costModifier : costModifiers) { 286 if (costModifier == COST_MODIFIER_PROCESS_STATE) { 287 useProcessStatePriceDeterminant = true; 288 } else { 289 final Modifier modifier = getModifier(costModifier); 290 ctp = modifier.getModifiedCostToProduce(ctp); 291 price = modifier.getModifiedPrice(price); 292 } 293 } 294 // ProcessStateModifier needs to be done last. 295 if (useProcessStatePriceDeterminant) { 296 ProcessStateModifier processStateModifier = 297 (ProcessStateModifier) getModifier(COST_MODIFIER_PROCESS_STATE); 298 price = processStateModifier.getModifiedPrice(userId, pkgName, ctp, price); 299 } 300 return new Cost(ctp, price); 301 } 302 initModifier(@odifier.CostModifier final int modifierId, @NonNull InternalResourceService irs)303 private static void initModifier(@Modifier.CostModifier final int modifierId, 304 @NonNull InternalResourceService irs) { 305 if (modifierId < 0 || modifierId >= COST_MODIFIER_BY_INDEX.length) { 306 throw new IllegalArgumentException("Invalid modifier id " + modifierId); 307 } 308 Modifier modifier = COST_MODIFIER_BY_INDEX[modifierId]; 309 if (modifier == null) { 310 switch (modifierId) { 311 case COST_MODIFIER_CHARGING: 312 modifier = new ChargingModifier(irs); 313 break; 314 case COST_MODIFIER_DEVICE_IDLE: 315 modifier = new DeviceIdleModifier(irs); 316 break; 317 case COST_MODIFIER_POWER_SAVE_MODE: 318 modifier = new PowerSaveModeModifier(irs); 319 break; 320 case COST_MODIFIER_PROCESS_STATE: 321 modifier = new ProcessStateModifier(irs); 322 break; 323 default: 324 throw new IllegalArgumentException("Invalid modifier id " + modifierId); 325 } 326 COST_MODIFIER_BY_INDEX[modifierId] = modifier; 327 } 328 } 329 330 @NonNull getModifier(@odifier.CostModifier final int modifierId)331 private static Modifier getModifier(@Modifier.CostModifier final int modifierId) { 332 if (modifierId < 0 || modifierId >= COST_MODIFIER_BY_INDEX.length) { 333 throw new IllegalArgumentException("Invalid modifier id " + modifierId); 334 } 335 final Modifier modifier = COST_MODIFIER_BY_INDEX[modifierId]; 336 if (modifier == null) { 337 throw new IllegalStateException( 338 "Modifier #" + modifierId + " was never initialized"); 339 } 340 return modifier; 341 } 342 343 @EventType getEventType(int eventId)344 static int getEventType(int eventId) { 345 return eventId & MASK_TYPE; 346 } 347 isReward(int eventId)348 static boolean isReward(int eventId) { 349 return getEventType(eventId) == TYPE_REWARD; 350 } 351 352 @NonNull eventToString(int eventId)353 static String eventToString(int eventId) { 354 switch (eventId & MASK_TYPE) { 355 case TYPE_ACTION: 356 return actionToString(eventId); 357 358 case TYPE_REGULATION: 359 return regulationToString(eventId); 360 361 case TYPE_REWARD: 362 return rewardToString(eventId); 363 364 default: 365 return "UNKNOWN_EVENT:" + Integer.toHexString(eventId); 366 } 367 } 368 369 @NonNull actionToString(int eventId)370 static String actionToString(int eventId) { 371 switch (eventId & MASK_POLICY) { 372 case POLICY_ALARM: 373 switch (eventId) { 374 case AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE: 375 return "ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE"; 376 case AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_EXACT: 377 return "ALARM_WAKEUP_EXACT"; 378 case AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_INEXACT_ALLOW_WHILE_IDLE: 379 return "ALARM_WAKEUP_INEXACT_ALLOW_WHILE_IDLE"; 380 case AlarmManagerEconomicPolicy.ACTION_ALARM_WAKEUP_INEXACT: 381 return "ALARM_WAKEUP_INEXACT"; 382 case AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_EXACT_ALLOW_WHILE_IDLE: 383 return "ALARM_NONWAKEUP_EXACT_ALLOW_WHILE_IDLE"; 384 case AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_EXACT: 385 return "ALARM_NONWAKEUP_EXACT"; 386 case AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_INEXACT_ALLOW_WHILE_IDLE: 387 return "ALARM_NONWAKEUP_INEXACT_ALLOW_WHILE_IDLE"; 388 case AlarmManagerEconomicPolicy.ACTION_ALARM_NONWAKEUP_INEXACT: 389 return "ALARM_NONWAKEUP_INEXACT"; 390 case AlarmManagerEconomicPolicy.ACTION_ALARM_CLOCK: 391 return "ALARM_CLOCK"; 392 } 393 break; 394 395 case POLICY_JOB: 396 switch (eventId) { 397 case JobSchedulerEconomicPolicy.ACTION_JOB_MAX_START: 398 return "JOB_MAX_START"; 399 case JobSchedulerEconomicPolicy.ACTION_JOB_MAX_RUNNING: 400 return "JOB_MAX_RUNNING"; 401 case JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_START: 402 return "JOB_HIGH_START"; 403 case JobSchedulerEconomicPolicy.ACTION_JOB_HIGH_RUNNING: 404 return "JOB_HIGH_RUNNING"; 405 case JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_START: 406 return "JOB_DEFAULT_START"; 407 case JobSchedulerEconomicPolicy.ACTION_JOB_DEFAULT_RUNNING: 408 return "JOB_DEFAULT_RUNNING"; 409 case JobSchedulerEconomicPolicy.ACTION_JOB_LOW_START: 410 return "JOB_LOW_START"; 411 case JobSchedulerEconomicPolicy.ACTION_JOB_LOW_RUNNING: 412 return "JOB_LOW_RUNNING"; 413 case JobSchedulerEconomicPolicy.ACTION_JOB_MIN_START: 414 return "JOB_MIN_START"; 415 case JobSchedulerEconomicPolicy.ACTION_JOB_MIN_RUNNING: 416 return "JOB_MIN_RUNNING"; 417 case JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT: 418 return "JOB_TIMEOUT"; 419 } 420 break; 421 } 422 return "UNKNOWN_ACTION:" + Integer.toHexString(eventId); 423 } 424 425 @NonNull regulationToString(int eventId)426 static String regulationToString(int eventId) { 427 switch (eventId) { 428 case REGULATION_BASIC_INCOME: 429 return "BASIC_INCOME"; 430 case REGULATION_BIRTHRIGHT: 431 return "BIRTHRIGHT"; 432 case REGULATION_WEALTH_RECLAMATION: 433 return "WEALTH_RECLAMATION"; 434 case REGULATION_PROMOTION: 435 return "PROMOTION"; 436 case REGULATION_DEMOTION: 437 return "DEMOTION"; 438 case REGULATION_BG_RESTRICTED: 439 return "BG_RESTRICTED"; 440 case REGULATION_BG_UNRESTRICTED: 441 return "BG_UNRESTRICTED"; 442 case REGULATION_FORCE_STOP: 443 return "FORCE_STOP"; 444 } 445 return "UNKNOWN_REGULATION:" + Integer.toHexString(eventId); 446 } 447 448 @NonNull rewardToString(int eventId)449 static String rewardToString(int eventId) { 450 switch (eventId) { 451 case REWARD_TOP_ACTIVITY: 452 return "REWARD_TOP_ACTIVITY"; 453 case REWARD_NOTIFICATION_SEEN: 454 return "REWARD_NOTIFICATION_SEEN"; 455 case REWARD_NOTIFICATION_INTERACTION: 456 return "REWARD_NOTIFICATION_INTERACTION"; 457 case REWARD_WIDGET_INTERACTION: 458 return "REWARD_WIDGET_INTERACTION"; 459 case REWARD_OTHER_USER_INTERACTION: 460 return "REWARD_OTHER_USER_INTERACTION"; 461 case JobSchedulerEconomicPolicy.REWARD_APP_INSTALL: 462 return "REWARD_JOB_APP_INSTALL"; 463 } 464 return "UNKNOWN_REWARD:" + Integer.toHexString(eventId); 465 } 466 getConstantAsCake(@onNull KeyValueListParser parser, @Nullable DeviceConfig.Properties properties, String key, long defaultValCake)467 protected long getConstantAsCake(@NonNull KeyValueListParser parser, 468 @Nullable DeviceConfig.Properties properties, String key, long defaultValCake) { 469 return getConstantAsCake(parser, properties, key, defaultValCake, 0); 470 } 471 getConstantAsCake(@onNull KeyValueListParser parser, @Nullable DeviceConfig.Properties properties, String key, long defaultValCake, long minValCake)472 protected long getConstantAsCake(@NonNull KeyValueListParser parser, 473 @Nullable DeviceConfig.Properties properties, String key, long defaultValCake, 474 long minValCake) { 475 // Don't cross the streams! Mixing Settings/local user config changes with DeviceConfig 476 // config can cause issues since the scales may be different, so use one or the other. 477 if (parser.size() > 0) { 478 // User settings take precedence. Just stick with the Settings constants, even if there 479 // are invalid values. It's not worth the time to evaluate all the key/value pairs to 480 // make sure there are valid ones before deciding. 481 return Math.max(minValCake, 482 parseCreditValue(parser.getString(key, null), defaultValCake)); 483 } 484 if (properties != null) { 485 return Math.max(minValCake, 486 parseCreditValue(properties.getString(key, null), defaultValCake)); 487 } 488 return Math.max(minValCake, defaultValCake); 489 } 490 491 @VisibleForTesting 492 static class Injector { 493 @Nullable getSettingsGlobalString(@onNull ContentResolver resolver, @NonNull String name)494 String getSettingsGlobalString(@NonNull ContentResolver resolver, @NonNull String name) { 495 return Settings.Global.getString(resolver, name); 496 } 497 } 498 dumpActiveModifiers(IndentingPrintWriter pw)499 protected static void dumpActiveModifiers(IndentingPrintWriter pw) { 500 for (int i = 0; i < NUM_COST_MODIFIERS; ++i) { 501 pw.print("Modifier "); 502 pw.println(i); 503 pw.increaseIndent(); 504 505 Modifier modifier = COST_MODIFIER_BY_INDEX[i]; 506 if (modifier != null) { 507 modifier.dump(pw); 508 } else { 509 pw.println("NOT ACTIVE"); 510 } 511 512 pw.decreaseIndent(); 513 } 514 } 515 dumpAction(IndentingPrintWriter pw, @NonNull Action action)516 protected static void dumpAction(IndentingPrintWriter pw, @NonNull Action action) { 517 pw.print(actionToString(action.id)); 518 pw.print(": "); 519 pw.print("ctp="); 520 pw.print(cakeToString(action.costToProduce)); 521 pw.print(", basePrice="); 522 pw.print(cakeToString(action.basePrice)); 523 pw.println(); 524 } 525 dumpReward(IndentingPrintWriter pw, @NonNull Reward reward)526 protected static void dumpReward(IndentingPrintWriter pw, @NonNull Reward reward) { 527 pw.print(rewardToString(reward.id)); 528 pw.print(": "); 529 pw.print("instant="); 530 pw.print(cakeToString(reward.instantReward)); 531 pw.print(", ongoing/sec="); 532 pw.print(cakeToString(reward.ongoingRewardPerSecond)); 533 pw.print(", maxDaily="); 534 pw.print(cakeToString(reward.maxDailyReward)); 535 pw.println(); 536 } 537 } 538