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