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