Home | History | Annotate | Download | only in loadtest
      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
     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
    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 
    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 
    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
    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
    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
    344     public void onDestroy() {
    345         Log.d(TAG, "Destroying");
    346         mPerfData.onDestroy();
    347         stopLoadtest();
    348         clearConfigs();
    349         super.onDestroy();
    350     }
    351 
    352     @Nullable
    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
    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
    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
    418     public void onNothingSelected(AdapterView<?> parent) {
    419         // Another interface callback
    420     }
    421 
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    602     private synchronized void setReplication(int replication) {
    603         if (mStarted) {
    604             return;
    605         }
    606         mReplicationText.setText("" + replication);
    607     }
    608 
    609     private synchronized void setPeriodSecs(long periodSecs) {
    610         mPeriodSecs = periodSecs;
    611     }
    612 
    613     private synchronized void setBurst(int burst) {
    614         mBurst = burst;
    615     }
    616 
    617     private synchronized void setDurationMins(long durationMins) {
    618         mDurationMins = durationMins;
    619     }
    620 
    621 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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