1 /*
2  * Copyright (C) 2017 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 package com.android.statsd.loadtest;
17 
18 import android.annotation.Nullable;
19 import android.app.Activity;
20 import android.app.AlarmManager;
21 import android.app.PendingIntent;
22 import android.app.StatsManager;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.graphics.Color;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.IStatsManager;
30 import android.os.PowerManager;
31 import android.os.PowerManager.WakeLock;
32 import android.os.ServiceManager;
33 import android.os.SystemClock;
34 import android.text.Editable;
35 import android.text.TextWatcher;
36 import android.util.Log;
37 import android.util.StatsLog;
38 import android.view.View;
39 import android.view.inputmethod.InputMethodManager;
40 import android.view.MotionEvent;
41 import android.view.View.OnFocusChangeListener;
42 import android.widget.AdapterView;
43 import android.widget.ArrayAdapter;
44 import android.widget.Button;
45 import android.widget.CheckBox;
46 import android.widget.EditText;
47 import android.widget.Spinner;
48 import android.widget.TextView;
49 import android.widget.Toast;
50 
51 import com.android.os.StatsLog.ConfigMetricsReport;
52 import com.android.os.StatsLog.ConfigMetricsReportList;
53 import com.android.os.StatsLog.StatsdStatsReport;
54 import com.android.internal.os.StatsdConfigProto.TimeUnit;
55 
56 import java.util.ArrayList;
57 import java.util.HashMap;
58 import java.util.List;
59 import java.util.Map;
60 
61 /**
62  * Runs a load test for statsd.
63  * How it works:
64  * <ul>
65  * <li> Sets up and pushes a custom config with metrics that exercise a large swath of code paths.
66  * <li> Periodically logs certain atoms into logd.
67  * <li> Impact on battery can be printed to logcat, or a bug report can be filed and analyzed
68  * in battery Historian.
69  * </ul>
70  * The load depends on how demanding the config is, as well as how frequently atoms are pushsed
71  * to logd. Those are all controlled by 4 adjustable parameters:
72  * <ul>
73  * <li> The 'replication' parameter artificially multiplies the number of metrics in the config.
74  * <li> The bucket size controls the time-bucketing the aggregate metrics.
75  * <li> The period parameter controls how frequently atoms are pushed to logd.
76  * <li> The 'burst' parameter controls how many atoms are pushed at the same time (per period).
77  * </ul>
78  */
79 public class LoadtestActivity extends Activity implements AdapterView.OnItemSelectedListener {
80 
81     private static final String TAG = "loadtest.LoadtestActivity";
82     public static final String TYPE = "type";
83     private static final String PUSH_ALARM = "push_alarm";
84     public static final String PERF_ALARM = "perf_alarm";
85     private static final String SET_REPLICATION = "set_replication";
86     private static final String REPLICATION = "replication";
87     private static final String START = "start";
88     private static final String STOP = "stop";
89     private static final Map<String, TimeUnit> TIME_UNIT_MAP = initializeTimeUnitMap();
90     private static final List<String> TIME_UNIT_LABELS = initializeTimeUnitLabels();
91 
92     public final static class PusherAlarmReceiver extends BroadcastReceiver {
93         @Override
onReceive(Context context, Intent intent)94         public void onReceive(Context context, Intent intent) {
95             Intent activityIntent = new Intent(context, LoadtestActivity.class);
96             activityIntent.putExtra(TYPE, PUSH_ALARM);
97             context.startActivity(activityIntent);
98         }
99     }
100 
101     public final static class StopperAlarmReceiver extends BroadcastReceiver {
102         @Override
onReceive(Context context, Intent intent)103         public void onReceive(Context context, Intent intent) {
104             Intent activityIntent = new Intent(context, LoadtestActivity.class);
105             activityIntent.putExtra(TYPE, STOP);
106             context.startActivity(activityIntent);
107         }
108     }
109 
initializeTimeUnitMap()110     private static Map<String, TimeUnit> initializeTimeUnitMap() {
111         Map<String, TimeUnit> labels = new HashMap();
112         labels.put("1m", TimeUnit.ONE_MINUTE);
113         labels.put("5m", TimeUnit.FIVE_MINUTES);
114         labels.put("10m", TimeUnit.TEN_MINUTES);
115         labels.put("30m", TimeUnit.THIRTY_MINUTES);
116         labels.put("1h", TimeUnit.ONE_HOUR);
117         labels.put("3h", TimeUnit.THREE_HOURS);
118         labels.put("6h", TimeUnit.SIX_HOURS);
119         labels.put("12h", TimeUnit.TWELVE_HOURS);
120         labels.put("1d", TimeUnit.ONE_DAY);
121         labels.put("1s", TimeUnit.CTS);
122         return labels;
123     }
124 
initializeTimeUnitLabels()125     private static List<String> initializeTimeUnitLabels() {
126         List<String> labels = new ArrayList();
127         labels.add("1s");
128         labels.add("1m");
129         labels.add("5m");
130         labels.add("10m");
131         labels.add("30m");
132         labels.add("1h");
133         labels.add("3h");
134         labels.add("6h");
135         labels.add("12h");
136         labels.add("1d");
137         return labels;
138     }
139 
140     private AlarmManager mAlarmMgr;
141 
142     /**
143      * Used to periodically log atoms to logd.
144      */
145     private PendingIntent mPushPendingIntent;
146 
147     /**
148      * Used to end the loadtest.
149      */
150     private PendingIntent mStopPendingIntent;
151 
152     private Button mStartStop;
153     private EditText mReplicationText;
154     private Spinner mBucketSpinner;
155     private EditText mPeriodText;
156     private EditText mBurstText;
157     private EditText mDurationText;
158     private TextView mReportText;
159     private CheckBox mPlaceboCheckBox;
160     private CheckBox mCountMetricCheckBox;
161     private CheckBox mDurationMetricCheckBox;
162     private CheckBox mEventMetricCheckBox;
163     private CheckBox mValueMetricCheckBox;
164     private CheckBox mGaugeMetricCheckBox;
165 
166     /**
167      * When the load test started.
168      */
169     private long mStartedTimeMillis;
170 
171     /**
172      * For measuring perf data.
173      */
174     private PerfData mPerfData;
175 
176     /**
177      * For communicating with statsd.
178      */
179     private StatsManager mStatsManager;
180 
181     private PowerManager mPowerManager;
182     private WakeLock mWakeLock;
183 
184     /**
185      * If true, we only measure the effect of the loadtest infrastructure. No atom are pushed and
186      * the configuration is empty.
187      */
188     private boolean mPlacebo;
189 
190     /**
191      * Whether to include CountMetric in the config.
192      */
193     private boolean mIncludeCountMetric;
194 
195     /**
196      * Whether to include DurationMetric in the config.
197      */
198     private boolean mIncludeDurationMetric;
199 
200     /**
201      * Whether to include EventMetric in the config.
202      */
203     private boolean mIncludeEventMetric;
204 
205     /**
206      * Whether to include ValueMetric in the config.
207      */
208     private boolean mIncludeValueMetric;
209 
210     /**
211      * Whether to include GaugeMetric in the config.
212      */
213     private boolean mIncludeGaugeMetric;
214 
215     /**
216      * The burst size.
217      */
218     private int mBurst;
219 
220     /**
221      * The metrics replication.
222      */
223     private int mReplication;
224 
225     /**
226      * The period, in seconds, at which batches of atoms are pushed.
227      */
228     private long mPeriodSecs;
229 
230     /**
231      * The bucket size, in minutes, for aggregate metrics.
232      */
233     private TimeUnit mBucket;
234 
235     /**
236      * The duration, in minutes, of the loadtest.
237      */
238     private long mDurationMins;
239 
240     /**
241      * Whether the loadtest has started.
242      */
243     private boolean mStarted = false;
244 
245     /**
246      * Orchestrates the logging of pushed events into logd.
247      */
248     private SequencePusher mPusher;
249 
250     /**
251      * Generates statsd configs.
252      */
253     private ConfigFactory mFactory;
254 
255     /**
256      * For intra-minute periods.
257      */
258     private final Handler mHandler = new Handler();
259 
260     /**
261      * Number of metrics in the current config.
262      */
263     private int mNumMetrics;
264 
265     @Override
onCreate(Bundle savedInstanceState)266     protected void onCreate(Bundle savedInstanceState) {
267         super.onCreate(savedInstanceState);
268 
269         Log.d(TAG, "Starting loadtest Activity");
270 
271         setContentView(R.layout.activity_loadtest);
272         mReportText = (TextView) findViewById(R.id.report_text);
273         initBurst();
274         initReplication();
275         initBucket();
276         initPeriod();
277         initDuration();
278         initPlacebo();
279         initMetricWhitelist();
280 
281         // Hide the keyboard outside edit texts.
282         findViewById(R.id.outside).setOnTouchListener(new View.OnTouchListener() {
283             @Override
284             public boolean onTouch(View v, MotionEvent event) {
285                 InputMethodManager imm =
286                         (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
287                 if (getCurrentFocus() != null) {
288                     imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
289                 }
290                 return true;
291             }
292         });
293 
294         mStartStop = findViewById(R.id.start_stop);
295         mStartStop.setOnClickListener(new View.OnClickListener() {
296             @Override
297             public void onClick(View view) {
298                 if (mStarted) {
299                     stopLoadtest();
300                 } else {
301                     startLoadtest();
302                 }
303             }
304         });
305 
306         mAlarmMgr = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
307         mStatsManager = (StatsManager) getSystemService("stats");
308         mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
309         mFactory = new ConfigFactory(this);
310         stopLoadtest();
311         mReportText.setText("");
312     }
313 
314     @Override
onNewIntent(Intent intent)315     public void onNewIntent(Intent intent) {
316         String type = intent.getStringExtra(TYPE);
317         if (type == null) {
318             return;
319         }
320         switch (type) {
321             case PERF_ALARM:
322                 onPerfAlarm();
323                 break;
324             case PUSH_ALARM:
325                 onAlarm();
326                 break;
327             case SET_REPLICATION:
328                 if (intent.hasExtra(REPLICATION)) {
329                     setReplication(intent.getIntExtra(REPLICATION, 0));
330                 }
331                 break;
332             case START:
333                 startLoadtest();
334                 break;
335             case STOP:
336                 stopLoadtest();
337                 break;
338             default:
339                 throw new IllegalArgumentException("Unknown type: " + type);
340         }
341     }
342 
343     @Override
onDestroy()344     public void onDestroy() {
345         Log.d(TAG, "Destroying");
346         mPerfData.onDestroy();
347         stopLoadtest();
348         clearConfigs();
349         super.onDestroy();
350     }
351 
352     @Nullable
getMetadata()353     public StatsdStatsReport getMetadata() {
354         if (!statsdRunning()) {
355             return null;
356         }
357         if (mStatsManager != null) {
358             byte[] data;
359             try {
360                 data = mStatsManager.getStatsMetadata();
361             } catch (StatsManager.StatsUnavailableException e) {
362                 Log.e(TAG, "Failed to get data from statsd", e);
363                 return null;
364             }
365             if (data != null) {
366                 StatsdStatsReport report = null;
367                 boolean good = false;
368                 try {
369                     return StatsdStatsReport.parseFrom(data);
370                 } catch (com.google.protobuf.InvalidProtocolBufferException e) {
371                     Log.d(TAG, "Bad StatsdStatsReport");
372                 }
373             }
374         }
375         return null;
376     }
377 
378     @Nullable
getData()379     public List<ConfigMetricsReport> getData() {
380         if (!statsdRunning()) {
381             return null;
382         }
383         if (mStatsManager != null) {
384             byte[] data;
385             try {
386                 data = mStatsManager.getReports(ConfigFactory.CONFIG_ID);
387             } catch (StatsManager.StatsUnavailableException e) {
388                 Log.e(TAG, "Failed to get data from statsd", e);
389                 return null;
390             }
391             if (data != null) {
392                 ConfigMetricsReportList reports = null;
393                 try {
394                     reports = ConfigMetricsReportList.parseFrom(data);
395                     Log.d(TAG, "Num reports: " + reports.getReportsCount());
396                     StringBuilder sb = new StringBuilder();
397                     DisplayProtoUtils.displayLogReport(sb, reports);
398                     Log.d(TAG, sb.toString());
399                 } catch (com.google.protobuf.InvalidProtocolBufferException e) {
400                     Log.d(TAG, "Invalid data");
401                 }
402                 if (reports != null) {
403                     return reports.getReportsList();
404                 }
405             }
406         }
407         return null;
408     }
409 
410     @Override
onItemSelected(AdapterView<?> parent, View view, int position, long id)411     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
412         String item = parent.getItemAtPosition(position).toString();
413 
414         mBucket = TIME_UNIT_MAP.get(item);
415     }
416 
417     @Override
onNothingSelected(AdapterView<?> parent)418     public void onNothingSelected(AdapterView<?> parent) {
419         // Another interface callback
420     }
421 
onPerfAlarm()422     private void onPerfAlarm() {
423         if (mPerfData != null) {
424             mPerfData.onAlarm(this);
425         }
426         // Piggy-back on that alarm to show the elapsed time.
427         long elapsedTimeMins = (long) Math.floor(
428                 (SystemClock.elapsedRealtime() - mStartedTimeMillis) / 60 / 1000);
429         mReportText.setText("Loadtest in progress.\n"
430                 + "num metrics =" + mNumMetrics
431                 + "\nElapsed time = " + elapsedTimeMins + " min(s)");
432     }
433 
onAlarm()434     private void onAlarm() {
435         Log.d(TAG, "ON ALARM");
436 
437         // Set the next task.
438         scheduleNext();
439 
440         // Do the work.
441         mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "StatsdLoadTest");
442         mWakeLock.acquire();
443         if (mPusher != null) {
444             mPusher.next();
445         }
446         mWakeLock.release();
447         mWakeLock = null;
448     }
449 
450     /**
451      * Schedules the next cycle of pushing atoms into logd.
452      */
scheduleNext()453     private void scheduleNext() {
454         Intent intent = new Intent(this, PusherAlarmReceiver.class);
455         intent.putExtra(TYPE, PUSH_ALARM);
456         mPushPendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
457         long nextTime = SystemClock.elapsedRealtime() + mPeriodSecs * 1000;
458         mAlarmMgr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTime, mPushPendingIntent);
459     }
460 
startLoadtest()461     private synchronized void startLoadtest() {
462         if (mStarted) {
463             return;
464         }
465 
466         // Clean up the state.
467         stopLoadtest();
468 
469         // Prepare to push a sequence of atoms to logd.
470         mPusher = new SequencePusher(mBurst, mPlacebo);
471 
472         // Create a config and push it to statsd.
473         if (!setConfig(mFactory.getConfig(mReplication, mBucket, mPlacebo,
474                 mIncludeCountMetric, mIncludeDurationMetric, mIncludeEventMetric,
475                 mIncludeValueMetric, mIncludeGaugeMetric))) {
476             return;
477         }
478 
479         // Remember to stop in the future.
480         Intent intent = new Intent(this, StopperAlarmReceiver.class);
481         intent.putExtra(TYPE, STOP);
482         mStopPendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
483         long nextTime = SystemClock.elapsedRealtime() + mDurationMins * 60 * 1000;
484         mAlarmMgr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTime, mStopPendingIntent);
485 
486         // Log atoms.
487         scheduleNext();
488 
489         // Start tracking performance.
490         mPerfData = new PerfData(this, mPlacebo, mReplication, mBucket, mPeriodSecs, mBurst,
491                 mIncludeCountMetric, mIncludeDurationMetric, mIncludeEventMetric, mIncludeValueMetric,
492                 mIncludeGaugeMetric);
493         mPerfData.startRecording(this);
494 
495         mReportText.setText("Loadtest in progress.\nnum metrics =" + mNumMetrics);
496         mStartedTimeMillis = SystemClock.elapsedRealtime();
497 
498         updateStarted(true);
499     }
500 
stopLoadtest()501     private synchronized void stopLoadtest() {
502         if (mPushPendingIntent != null) {
503             Log.d(TAG, "Canceling pre-existing push alarm");
504             mAlarmMgr.cancel(mPushPendingIntent);
505             mPushPendingIntent = null;
506         }
507         if (mStopPendingIntent != null) {
508             Log.d(TAG, "Canceling pre-existing stop alarm");
509             mAlarmMgr.cancel(mStopPendingIntent);
510             mStopPendingIntent = null;
511         }
512         if (mWakeLock != null) {
513             mWakeLock.release();
514             mWakeLock = null;
515         }
516         if (mPerfData != null) {
517             mPerfData.stopRecording(this);
518             mPerfData.onDestroy();
519             mPerfData = null;
520         }
521 
522         // Obtain the latest data and display it.
523         getData();
524 
525         long elapsedTimeMins = (long) Math.floor(
526                 (SystemClock.elapsedRealtime() - mStartedTimeMillis) / 60 / 1000);
527         mReportText.setText("Loadtest ended. Elapsed time = " + elapsedTimeMins + " min(s)");
528         clearConfigs();
529         updateStarted(false);
530     }
531 
updateStarted(boolean started)532     private synchronized void updateStarted(boolean started) {
533         mStarted = started;
534         mStartStop.setBackgroundColor(started ?
535                 Color.parseColor("#FFFF0000") : Color.parseColor("#FF00FF00"));
536         mStartStop.setText(started ? getString(R.string.stop) : getString(R.string.start));
537         updateControlsEnabled();
538     }
539 
updateControlsEnabled()540     private void updateControlsEnabled() {
541         mBurstText.setEnabled(!mPlacebo && !mStarted);
542         mReplicationText.setEnabled(!mPlacebo && !mStarted);
543         mPeriodText.setEnabled(!mStarted);
544         mBucketSpinner.setEnabled(!mPlacebo && !mStarted);
545         mDurationText.setEnabled(!mStarted);
546         mPlaceboCheckBox.setEnabled(!mStarted);
547 
548         boolean enabled = !mStarted && !mPlaceboCheckBox.isChecked();
549         mCountMetricCheckBox.setEnabled(enabled);
550         mDurationMetricCheckBox.setEnabled(enabled);
551         mEventMetricCheckBox.setEnabled(enabled);
552         mValueMetricCheckBox.setEnabled(enabled);
553         mGaugeMetricCheckBox.setEnabled(enabled);
554     }
555 
statsdRunning()556     private boolean statsdRunning() {
557         if (IStatsManager.Stub.asInterface(ServiceManager.getService("stats")) == null) {
558             Log.d(TAG, "Statsd not running");
559             Toast.makeText(LoadtestActivity.this, "Statsd NOT running!", Toast.LENGTH_LONG).show();
560             return false;
561         }
562         return true;
563     }
564 
sanitizeInt(int val, int min, int max)565     private int sanitizeInt(int val, int min, int max) {
566         if (val > max) {
567             val = max;
568         } else if (val < min) {
569             val = min;
570         }
571         return val;
572     }
573 
clearConfigs()574     private void clearConfigs() {
575         // TODO: Clear all configs instead of specific ones.
576         if (mStatsManager != null) {
577             if (mStarted) {
578                 try {
579                     mStatsManager.removeConfig(ConfigFactory.CONFIG_ID);
580                     Log.d(TAG, "Removed loadtest statsd configs.");
581                 } catch (StatsManager.StatsUnavailableException e) {
582                     Log.e(TAG, "Failed to remove loadtest configs.", e);
583                 }
584             }
585         }
586     }
587 
setConfig(ConfigFactory.ConfigMetadata configData)588     private boolean setConfig(ConfigFactory.ConfigMetadata configData) {
589         if (mStatsManager != null) {
590             try {
591                 mStatsManager.addConfig(ConfigFactory.CONFIG_ID, configData.bytes);
592                 mNumMetrics = configData.numMetrics;
593                 Log.d(TAG, "Config pushed to statsd");
594                 return true;
595             } catch (StatsManager.StatsUnavailableException | IllegalArgumentException e) {
596                 Log.e(TAG, "Failed to push config to statsd", e);
597             }
598         }
599         return false;
600     }
601 
setReplication(int replication)602     private synchronized void setReplication(int replication) {
603         if (mStarted) {
604             return;
605         }
606         mReplicationText.setText("" + replication);
607     }
608 
setPeriodSecs(long periodSecs)609     private synchronized void setPeriodSecs(long periodSecs) {
610         mPeriodSecs = periodSecs;
611     }
612 
setBurst(int burst)613     private synchronized void setBurst(int burst) {
614         mBurst = burst;
615     }
616 
setDurationMins(long durationMins)617     private synchronized void setDurationMins(long durationMins) {
618         mDurationMins = durationMins;
619     }
620 
621 
handleFocus(EditText editText)622     private void handleFocus(EditText editText) {
623       /*
624         editText.setOnFocusChangeListener(new OnFocusChangeListener() {
625             @Override
626             public void onFocusChange(View v, boolean hasFocus) {
627                 if (!hasFocus && editText.getText().toString().isEmpty()) {
628                     editText.setText("-1", TextView.BufferType.EDITABLE);
629                 }
630             }
631         });
632       */
633     }
634 
initBurst()635     private void initBurst() {
636         mBurst = getResources().getInteger(R.integer.burst_default);
637         mBurstText = (EditText) findViewById(R.id.burst);
638         mBurstText.addTextChangedListener(new NumericalWatcher(mBurstText, 0, 1000) {
639             @Override
640             public void onNewValue(int newValue) {
641                 setBurst(newValue);
642             }
643         });
644         handleFocus(mBurstText);
645     }
646 
initReplication()647     private void initReplication() {
648         mReplication = getResources().getInteger(R.integer.replication_default);
649         mReplicationText = (EditText) findViewById(R.id.replication);
650         mReplicationText.addTextChangedListener(new NumericalWatcher(mReplicationText, 1, 4096) {
651             @Override
652             public void onNewValue(int newValue) {
653                 mReplication = newValue;
654             }
655         });
656         handleFocus(mReplicationText);
657     }
658 
initBucket()659     private void initBucket() {
660         String defaultValue = getResources().getString(R.string.bucket_default);
661         mBucket = TimeUnit.valueOf(defaultValue);
662         mBucketSpinner = (Spinner) findViewById(R.id.bucket_spinner);
663 
664         ArrayAdapter<String> dataAdapter = new ArrayAdapter<String>(
665                 this, R.layout.spinner_item, TIME_UNIT_LABELS);
666 
667         mBucketSpinner.setAdapter(dataAdapter);
668         mBucketSpinner.setOnItemSelectedListener(this);
669 
670         for (String label : TIME_UNIT_MAP.keySet()) {
671             if (defaultValue.equals(TIME_UNIT_MAP.get(label).toString())) {
672                 mBucketSpinner.setSelection(dataAdapter.getPosition(label));
673             }
674         }
675     }
676 
initPeriod()677     private void initPeriod() {
678         mPeriodSecs = getResources().getInteger(R.integer.period_default);
679         mPeriodText = (EditText) findViewById(R.id.period);
680         mPeriodText.addTextChangedListener(new NumericalWatcher(mPeriodText, 1, 60) {
681             @Override
682             public void onNewValue(int newValue) {
683                 setPeriodSecs(newValue);
684             }
685         });
686         handleFocus(mPeriodText);
687     }
688 
initDuration()689     private void initDuration() {
690         mDurationMins = getResources().getInteger(R.integer.duration_default);
691         mDurationText = (EditText) findViewById(R.id.duration);
692         mDurationText.addTextChangedListener(new NumericalWatcher(mDurationText, 1, 24 * 60) {
693             @Override
694             public void onNewValue(int newValue) {
695                 setDurationMins(newValue);
696             }
697         });
698         handleFocus(mDurationText);
699     }
700 
initPlacebo()701     private void initPlacebo() {
702         mPlaceboCheckBox = findViewById(R.id.placebo);
703         mPlacebo = false;
704         mPlaceboCheckBox.setOnClickListener(new View.OnClickListener() {
705             @Override
706             public void onClick(View view) {
707                 mPlacebo = mPlaceboCheckBox.isChecked();
708                 updateControlsEnabled();
709             }
710         });
711     }
712 
initMetricWhitelist()713     private void initMetricWhitelist() {
714         mCountMetricCheckBox = findViewById(R.id.include_count);
715         mCountMetricCheckBox.setOnClickListener(new View.OnClickListener() {
716             @Override
717             public void onClick(View view) {
718                 mIncludeCountMetric = mCountMetricCheckBox.isChecked();
719             }
720         });
721         mDurationMetricCheckBox = findViewById(R.id.include_duration);
722         mDurationMetricCheckBox.setOnClickListener(new View.OnClickListener() {
723             @Override
724             public void onClick(View view) {
725                 mIncludeDurationMetric = mDurationMetricCheckBox.isChecked();
726             }
727         });
728         mEventMetricCheckBox = findViewById(R.id.include_event);
729         mEventMetricCheckBox.setOnClickListener(new View.OnClickListener() {
730             @Override
731             public void onClick(View view) {
732                 mIncludeEventMetric = mEventMetricCheckBox.isChecked();
733             }
734         });
735         mValueMetricCheckBox = findViewById(R.id.include_value);
736         mValueMetricCheckBox.setOnClickListener(new View.OnClickListener() {
737             @Override
738             public void onClick(View view) {
739                 mIncludeValueMetric = mValueMetricCheckBox.isChecked();
740             }
741         });
742         mGaugeMetricCheckBox = findViewById(R.id.include_gauge);
743         mGaugeMetricCheckBox.setOnClickListener(new View.OnClickListener() {
744             @Override
745             public void onClick(View view) {
746                 mIncludeGaugeMetric = mGaugeMetricCheckBox.isChecked();
747             }
748         });
749 
750         mIncludeCountMetric = mCountMetricCheckBox.isChecked();
751         mIncludeDurationMetric = mDurationMetricCheckBox.isChecked();
752         mIncludeEventMetric = mEventMetricCheckBox.isChecked();
753         mIncludeValueMetric = mValueMetricCheckBox.isChecked();
754         mIncludeGaugeMetric = mGaugeMetricCheckBox.isChecked();
755     }
756 }
757