Home | History | Annotate | Download | only in loopback
      1 /*
      2  * Copyright (C) 2014 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 org.drrickorang.loopback;
     18 
     19 import android.Manifest;
     20 import android.app.Activity;
     21 import android.app.DialogFragment;
     22 import android.content.ComponentName;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.ServiceConnection;
     26 import android.content.pm.PackageManager;
     27 import android.database.Cursor;
     28 import android.graphics.Bitmap;
     29 import android.graphics.Canvas;
     30 import android.media.AudioManager;
     31 import android.net.Uri;
     32 import android.os.Build;
     33 import android.os.Bundle;
     34 import android.os.Handler;
     35 import android.os.IBinder;
     36 import android.os.Message;
     37 import android.os.ParcelFileDescriptor;
     38 import android.provider.MediaStore;
     39 import android.support.v4.app.ActivityCompat;
     40 import android.support.v4.content.ContextCompat;
     41 import android.text.format.DateFormat;
     42 import android.util.Log;
     43 import android.view.Gravity;
     44 import android.view.Menu;
     45 import android.view.MenuInflater;
     46 import android.view.MenuItem;
     47 import android.view.View;
     48 import android.view.ViewGroup;
     49 import android.widget.Button;
     50 import android.widget.LinearLayout;
     51 import android.widget.PopupWindow;
     52 import android.widget.SeekBar;
     53 import android.widget.TextView;
     54 import android.widget.Toast;
     55 
     56 import java.io.File;
     57 import java.io.FileDescriptor;
     58 import java.io.FileOutputStream;
     59 import java.util.Arrays;
     60 import java.util.Locale;
     61 
     62 
     63 /**
     64  * This is the main activity of the Loopback app. Two tests (latency test and buffer test) can be
     65  * initiated here. Note: buffer test and glitch detection is the same test, it's just that this test
     66  * has two parts of result.
     67  */
     68 
     69 public class LoopbackActivity extends Activity
     70         implements SaveFilesDialogFragment.NoticeDialogListener {
     71     private static final String TAG = "LoopbackActivity";
     72 
     73     private static final int SAVE_TO_WAVE_REQUEST = 42;
     74     private static final int SAVE_TO_PNG_REQUEST = 43;
     75     private static final int SAVE_TO_TXT_REQUEST = 44;
     76     private static final int SAVE_RECORDER_BUFFER_PERIOD_TO_TXT_REQUEST = 45;
     77     private static final int SAVE_PLAYER_BUFFER_PERIOD_TO_TXT_REQUEST = 46;
     78     private static final int SAVE_RECORDER_BUFFER_PERIOD_TO_PNG_REQUEST = 47;
     79     private static final int SAVE_PLAYER_BUFFER_PERIOD_TO_PNG_REQUEST = 48;
     80     private static final int SAVE_RECORDER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST = 49;
     81     private static final int SAVE_PLAYER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST = 50;
     82     private static final int SAVE_GLITCH_OCCURRENCES_TO_TEXT_REQUEST = 51;
     83     private static final int SAVE_GLITCH_AND_CALLBACK_HEATMAP_REQUEST = 52;
     84 
     85     private static final int SETTINGS_ACTIVITY_REQUEST = 54;
     86 
     87     private static final int THREAD_SLEEP_DURATION_MS = 200;
     88     private static final int PERMISSIONS_REQUEST_RECORD_AUDIO_LATENCY = 201;
     89     private static final int PERMISSIONS_REQUEST_RECORD_AUDIO_BUFFER = 202;
     90     private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE_RESULTS = 203;
     91     private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE_SCRIPT = 204;
     92     private static final int LATENCY_TEST_STARTED = 300;
     93     private static final int LATENCY_TEST_ENDED = 301;
     94     private static final int BUFFER_TEST_STARTED = 302;
     95     private static final int BUFFER_TEST_ENDED = 303;
     96     private static final int CALIBRATION_STARTED = 304;
     97     private static final int CALIBRATION_ENDED = 305;
     98 
     99     // 0-100 controls compression rate, currently ignore because PNG format is being used
    100     private static final int EXPORTED_IMAGE_QUALITY = 100;
    101 
    102     private static final int HISTOGRAM_EXPORT_WIDTH = 2000;
    103     private static final int HISTOGRAM_EXPORT_HEIGHT = 2000;
    104     private static final int HEATMAP_DRAW_WIDTH = 2560;
    105     private static final int HEATMAP_DRAW_HEIGHT = 1440;
    106     private static final int HEATMAP_EXPORT_DIVISOR = 2;
    107 
    108     LoopbackAudioThread  mAudioThread = null;
    109     NativeAudioThread    mNativeAudioThread = null;
    110     private Thread       mCalibrationThread;
    111     private WavePlotView mWavePlotView;
    112     private String       mTestStartTimeString = "IncorrectTime";  // The time the test begins
    113     private static final String FILE_SAVE_PATH = "file://mnt/sdcard/";
    114 
    115     private SeekBar  mBarMasterLevel; // drag the volume
    116     private TextView mTextInfo;
    117     private TextView mTextViewCurrentLevel;
    118     private TextView mTextViewResultSummary;
    119 
    120     private int          mTestType;
    121     private double []    mWaveData;    // this is where we store the data for the wave plot
    122     private Correlation  mCorrelation = new Correlation();
    123     private BufferPeriod mRecorderBufferPeriod = new BufferPeriod();
    124     private BufferPeriod mPlayerBufferPeriod = new BufferPeriod();
    125 
    126     // for native buffer period
    127     private int[]  mNativeRecorderBufferPeriodArray;
    128     private int    mNativeRecorderMaxBufferPeriod;
    129     private double mNativeRecorderStdDevBufferPeriod;
    130     private int[]  mNativePlayerBufferPeriodArray;
    131     private int    mNativePlayerMaxBufferPeriod;
    132     private double mNativePlayerStdDevBufferPeriod;
    133     private BufferCallbackTimes mRecorderCallbackTimes;
    134     private BufferCallbackTimes mPlayerCallbackTimes;
    135 
    136     private static final String INTENT_SAMPLING_FREQUENCY = "SF";
    137     private static final String INTENT_CHANNEL_INDEX = "CI";
    138     private static final String INTENT_FILENAME = "FileName";
    139     private static final String INTENT_RECORDER_BUFFER = "RecorderBuffer";
    140     private static final String INTENT_PLAYER_BUFFER = "PlayerBuffer";
    141     private static final String INTENT_AUDIO_THREAD = "AudioThread";
    142     private static final String INTENT_MIC_SOURCE = "MicSource";
    143     private static final String INTENT_PERFORMANCE_MODE = "PerformanceMode";
    144     private static final String INTENT_AUDIO_LEVEL = "AudioLevel";
    145     private static final String INTENT_IGNORE_FIRST_FRAMES = "IgnoreFirstFrames";
    146     private static final String INTENT_TEST_TYPE = "TestType";
    147     private static final String INTENT_BUFFER_TEST_DURATION = "BufferTestDuration";
    148     private static final String INTENT_NUMBER_LOAD_THREADS = "NumLoadThreads";
    149     private static final String INTENT_ENABLE_SYSTRACE = "CaptureSysTrace";
    150     private static final String INTENT_ENABLE_WAVCAPTURE = "CaptureWavs";
    151     private static final String INTENT_NUM_CAPTURES = "NumCaptures";
    152     private static final String INTENT_WAV_DURATION = "WavDuration";
    153 
    154     // for running the test using adb command
    155     private boolean mIntentRunning = false; // if it is running triggered by intent with parameters
    156     private String  mIntentFileName;
    157 
    158     // Note: these values should only be assigned in restartAudioSystem()
    159     private int   mAudioThreadType = Constant.UNKNOWN;
    160     private int   mMicSource;
    161     private int   mPerformanceMode;
    162     private int   mSamplingRate;
    163     private int   mChannelIndex;
    164     private int   mSoundLevel;
    165     private int   mPlayerBufferSizeInBytes;
    166     private int   mRecorderBufferSizeInBytes;
    167     private int   mIgnoreFirstFrames; // TODO: this only applies to native mode
    168     private CaptureHolder mCaptureHolder;
    169 
    170     // for buffer test
    171     private int[]   mGlitchesData;
    172     private boolean mGlitchingIntervalTooLong;
    173     private int     mFFTSamplingSize;
    174     private int     mFFTOverlapSamples;
    175     private long    mBufferTestStartTime;
    176     private int     mBufferTestElapsedSeconds;
    177     private int     mBufferTestDurationInSeconds;
    178     private int     mBufferTestWavePlotDurationInSeconds;
    179 
    180     // threads that load CPUs
    181     private LoadThread[]     mLoadThreads;
    182 
    183     // for getting the Service
    184     boolean mBound = false;
    185     private AudioTestService mAudioTestService;
    186     private final ServiceConnection mServiceConnection = new ServiceConnection() {
    187         public void onServiceConnected(ComponentName className, IBinder service) {
    188             mAudioTestService = ((AudioTestService.AudioTestBinder) service).getService();
    189             mBound = true;
    190         }
    191 
    192         public void onServiceDisconnected(ComponentName className) {
    193             mAudioTestService = null;
    194             mBound = false;
    195         }
    196     };
    197 
    198     private Handler mMessageHandler = new Handler() {
    199         public void handleMessage(Message msg) {
    200             super.handleMessage(msg);
    201             switch (msg.what) {
    202             case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED:
    203                 log("got message java latency test started!!");
    204                 showToast("Java Latency Test Started");
    205                 resetResults();
    206                 refreshState();
    207                 refreshPlots();
    208                 break;
    209             case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR:
    210                 log("got message java latency test rec can't start!!");
    211                 showToast("Java Latency Test Recording Error. Please try again");
    212                 refreshState();
    213                 stopAudioTestThreads();
    214                 mIntentRunning = false;
    215                 refreshSoundLevelBar();
    216                 break;
    217             case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
    218             case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE:
    219                 if (mAudioThread != null) {
    220                     mWaveData = mAudioThread.getWaveData();
    221                     mRecorderCallbackTimes = mRecorderBufferPeriod.getCallbackTimes();
    222                     mPlayerCallbackTimes = mPlayerBufferPeriod.getCallbackTimes();
    223                     mCorrelation.computeCorrelation(mWaveData, mSamplingRate);
    224                     log("got message java latency rec complete!!");
    225                     refreshPlots();
    226                     refreshState();
    227 
    228                     switch (msg.what) {
    229                     case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
    230                         showToast("Java Latency Test Stopped");
    231                         break;
    232                     case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE:
    233                         showToast("Java Latency Test Completed");
    234                         break;
    235                     }
    236 
    237                     stopAudioTestThreads();
    238                     if (mIntentRunning && mIntentFileName != null && mIntentFileName.length() > 0) {
    239                         saveAllTo(mIntentFileName);
    240                     }
    241                     mIntentRunning = false;
    242                 }
    243                 refreshSoundLevelBar();
    244                 break;
    245             case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED:
    246                 log("got message java buffer test rec started!!");
    247                 showToast("Java Buffer Test Started");
    248                 resetResults();
    249                 refreshState();
    250                 refreshPlots();
    251                 mBufferTestStartTime = System.currentTimeMillis();
    252                 break;
    253             case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR:
    254                 log("got message java buffer test rec can't start!!");
    255                 showToast("Java Buffer Test Recording Error. Please try again");
    256                 refreshState();
    257                 stopAudioTestThreads();
    258                 mIntentRunning = false;
    259                 refreshSoundLevelBar();
    260                 break;
    261             case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
    262             case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE:
    263                 if (mAudioThread != null) {
    264                     mWaveData = mAudioThread.getWaveData();
    265                     mGlitchesData = mAudioThread.getAllGlitches();
    266                     mGlitchingIntervalTooLong = mAudioThread.getGlitchingIntervalTooLong();
    267                     mFFTSamplingSize = mAudioThread.getFFTSamplingSize();
    268                     mFFTOverlapSamples = mAudioThread.getFFTOverlapSamples();
    269                     mRecorderCallbackTimes = mRecorderBufferPeriod.getCallbackTimes();
    270                     mPlayerCallbackTimes = mPlayerBufferPeriod.getCallbackTimes();
    271                     refreshPlots();  // only plot that last few seconds
    272                     refreshState();
    273                     //rounded up number of seconds elapsed
    274                     mBufferTestElapsedSeconds =
    275                             (int) ((System.currentTimeMillis() - mBufferTestStartTime +
    276                             Constant.MILLIS_PER_SECOND - 1) / Constant.MILLIS_PER_SECOND);
    277                     switch (msg.what) {
    278                     case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
    279                         showToast("Java Buffer Test Stopped");
    280                         break;
    281                     case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE:
    282                         showToast("Java Buffer Test Completed");
    283                         break;
    284                     }
    285                     if (getApp().isCaptureEnabled()) {
    286                         mCaptureHolder.stopLoopbackListenerScript();
    287                     }
    288                     stopAudioTestThreads();
    289                     if (mIntentRunning && mIntentFileName != null && mIntentFileName.length() > 0) {
    290                         saveAllTo(mIntentFileName);
    291                     }
    292                     mIntentRunning = false;
    293                 }
    294                 refreshSoundLevelBar();
    295                 break;
    296             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED:
    297                 log("got message native latency test rec started!!");
    298                 showToast("Native Latency Test Started");
    299                 resetResults();
    300                 refreshState();
    301                 refreshPlots();
    302                 break;
    303             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED:
    304                 log("got message native buffer test rec started!!");
    305                 showToast("Native Buffer Test Started");
    306                 resetResults();
    307                 refreshState();
    308                 refreshPlots();
    309                 mBufferTestStartTime = System.currentTimeMillis();
    310                 break;
    311             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR:
    312                 log("got message native latency test rec can't start!!");
    313                 showToast("Native Latency Test Recording Error. Please try again");
    314                 refreshState();
    315                 mIntentRunning = false;
    316                 refreshSoundLevelBar();
    317                 break;
    318             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR:
    319                 log("got message native buffer test rec can't start!!");
    320                 showToast("Native Buffer Test Recording Error. Please try again");
    321                 refreshState();
    322                 mIntentRunning = false;
    323                 refreshSoundLevelBar();
    324                 break;
    325             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
    326             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
    327             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE:
    328             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE:
    329             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE_ERRORS:
    330             case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE_ERRORS:
    331                     if (mNativeAudioThread != null) {
    332                     mGlitchesData = mNativeAudioThread.getNativeAllGlitches();
    333                     mGlitchingIntervalTooLong = mNativeAudioThread.getGlitchingIntervalTooLong();
    334                     mFFTSamplingSize = mNativeAudioThread.getNativeFFTSamplingSize();
    335                     mFFTOverlapSamples = mNativeAudioThread.getNativeFFTOverlapSamples();
    336                     mWaveData = mNativeAudioThread.getWaveData();
    337                     mNativeRecorderBufferPeriodArray = mNativeAudioThread.getRecorderBufferPeriod();
    338                     mNativeRecorderMaxBufferPeriod =
    339                             mNativeAudioThread.getRecorderMaxBufferPeriod();
    340                     mNativeRecorderStdDevBufferPeriod =
    341                             mNativeAudioThread.getRecorderStdDevBufferPeriod();
    342                     mNativePlayerBufferPeriodArray = mNativeAudioThread.getPlayerBufferPeriod();
    343                     mNativePlayerMaxBufferPeriod = mNativeAudioThread.getPlayerMaxBufferPeriod();
    344                     mNativePlayerStdDevBufferPeriod =
    345                             mNativeAudioThread.getPlayerStdDevBufferPeriod();
    346                     mRecorderCallbackTimes = mNativeAudioThread.getRecorderCallbackTimes();
    347                     mPlayerCallbackTimes = mNativeAudioThread.getPlayerCallbackTimes();
    348 
    349                     if (msg.what != NativeAudioThread.
    350                             LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE) {
    351                         mCorrelation.computeCorrelation(mWaveData, mSamplingRate);
    352                     }
    353 
    354                     log("got message native buffer test rec complete!!");
    355                     refreshPlots();
    356                     refreshState();
    357                     //rounded up number of seconds elapsed
    358                     mBufferTestElapsedSeconds =
    359                             (int) ((System.currentTimeMillis() - mBufferTestStartTime +
    360                                     Constant.MILLIS_PER_SECOND - 1) / Constant.MILLIS_PER_SECOND);
    361                     switch (msg.what) {
    362                         case NativeAudioThread.
    363                                 LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE_ERRORS:
    364                         case NativeAudioThread.
    365                                 LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE_ERRORS:
    366                         showToast("Native Test Completed with Fatal Errors");
    367                         break;
    368                         case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
    369                         case NativeAudioThread.
    370                                 LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
    371                         showToast("Native Test Stopped");
    372                         break;
    373                     default:
    374                         showToast("Native Test Completed");
    375                         break;
    376                     }
    377 
    378 
    379                     stopAudioTestThreads();
    380                     if (mIntentRunning && mIntentFileName != null && mIntentFileName.length() > 0) {
    381                         saveAllTo(mIntentFileName);
    382                     }
    383                     mIntentRunning = false;
    384 
    385                     if (getApp().isCaptureEnabled()) {
    386                         mCaptureHolder.stopLoopbackListenerScript();
    387                     }
    388                 }  // mNativeAudioThread != null
    389                 refreshSoundLevelBar();
    390                 break;
    391             default:
    392                 log("Got message:" + msg.what);
    393                 break;
    394             }
    395 
    396             // Control UI elements visibility specific to latency or buffer/glitch test
    397             switch (msg.what) {
    398                 // Latency test started
    399                 case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED:
    400                 case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED:
    401                     setTransportButtonsState(LATENCY_TEST_STARTED);
    402                     break;
    403 
    404                 // Latency test ended
    405                 case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE:
    406                 case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR:
    407                 case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
    408                 case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR:
    409                 case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE:
    410                 case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP:
    411                 case NativeAudioThread.
    412                         LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE_ERRORS:
    413                     setTransportButtonsState(LATENCY_TEST_ENDED);
    414                     break;
    415 
    416                 // Buffer test started
    417                 case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED:
    418                 case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED:
    419                     setTransportButtonsState(BUFFER_TEST_STARTED);
    420                     break;
    421 
    422                 // Buffer test ended
    423                 case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE:
    424                 case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR:
    425                 case LoopbackAudioThread.LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
    426                 case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR:
    427                 case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE:
    428                 case NativeAudioThread.LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP:
    429                 case NativeAudioThread.
    430                         LOOPBACK_NATIVE_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE_ERRORS:
    431                     setTransportButtonsState(BUFFER_TEST_ENDED);
    432                     break;
    433 
    434                 // Sound Calibration started
    435                 case CALIBRATION_STARTED:
    436                     setTransportButtonsState(CALIBRATION_STARTED);
    437                     break;
    438 
    439                 // Sound Calibration ended
    440                 case CALIBRATION_ENDED:
    441                     setTransportButtonsState(CALIBRATION_ENDED);
    442                     break;
    443             }
    444         }
    445     };
    446 
    447 
    448     @Override
    449     public void onCreate(Bundle savedInstanceState) {
    450         super.onCreate(savedInstanceState);
    451 
    452         // Set the layout for this activity. You can find it
    453         View view = getLayoutInflater().inflate(R.layout.main_activity, null);
    454         setContentView(view);
    455 
    456         // TODO: Write script to file at more appropriate time, from settings activity or intent
    457         // TODO: Respond to failure with more than just a toast
    458         if (hasWriteFilePermission()){
    459             boolean successfulWrite = AtraceScriptsWriter.writeScriptsToFile(this);
    460             if(!successfulWrite) {
    461                 showToast("Unable to write loopback_listener script to device");
    462             }
    463         } else {
    464             requestWriteFilePermission(PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE_SCRIPT);
    465         }
    466 
    467 
    468         mTextInfo = (TextView) findViewById(R.id.textInfo);
    469         mBarMasterLevel = (SeekBar) findViewById(R.id.BarMasterLevel);
    470 
    471         AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    472         int maxVolume = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
    473         mBarMasterLevel.setMax(maxVolume);
    474 
    475         mBarMasterLevel.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    476             @Override
    477             public void onStopTrackingTouch(SeekBar seekBar) {
    478             }
    479 
    480             @Override
    481             public void onStartTrackingTouch(SeekBar seekBar) {
    482             }
    483 
    484             @Override
    485             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    486                 AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    487                 am.setStreamVolume(AudioManager.STREAM_MUSIC,
    488                         progress, 0);
    489                 refreshSoundLevelBar();
    490                 log("Changed stream volume to: " + progress);
    491             }
    492         });
    493         mWavePlotView = (WavePlotView) findViewById(R.id.viewWavePlot);
    494 
    495         mTextViewCurrentLevel = (TextView) findViewById(R.id.textViewCurrentLevel);
    496         mTextViewCurrentLevel.setTextSize(15);
    497 
    498         mTextViewResultSummary = (TextView) findViewById(R.id.resultSummary);
    499         refreshSoundLevelBar();
    500 
    501         if(savedInstanceState != null) {
    502             restoreInstanceState(savedInstanceState);
    503         }
    504 
    505         if (!hasRecordAudioPermission()) {
    506             requestRecordAudioPermission(PERMISSIONS_REQUEST_RECORD_AUDIO_LATENCY);
    507         }
    508 
    509         applyIntent(getIntent());
    510     }
    511 
    512     @Override
    513     protected void onStart() {
    514         super.onStart();
    515         Intent audioTestIntent = new Intent(this, AudioTestService.class);
    516         startService(audioTestIntent);
    517         boolean bound = bindService(audioTestIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
    518         if (bound) {
    519             log("Successfully bound to service!");
    520         }
    521         else {
    522             log("Failed to bind service!");
    523         }
    524     }
    525 
    526 
    527     @Override
    528     protected void onStop() {
    529         super.onStop();
    530         log("Activity on stop!");
    531         // Unbind from the service
    532         if (mBound) {
    533             unbindService(mServiceConnection);
    534             mBound = false;
    535         }
    536     }
    537 
    538     @Override
    539     public void onNewIntent(Intent intent) {
    540         log("On New Intent called!");
    541         applyIntent(intent);
    542     }
    543 
    544 
    545     /**
    546      * This method will be called whenever the test starts running (either by operating on the
    547      * device or by adb command). In the case where the test is started through adb command,
    548      * adb parameters will be read into intermediate variables.
    549      */
    550     private void applyIntent(Intent intent) {
    551         Bundle b = intent.getExtras();
    552         if (b != null && !mIntentRunning) {
    553             // adb shell am start -n org.drrickorang.loopback/.LoopbackActivity
    554             // --ei SF 48000 --es FileName test1 --ei RecorderBuffer 512 --ei PlayerBuffer 512
    555             // --ei AudioThread 1 --ei MicSource 3 --ei AudioLevel 12
    556             // --ei TestType 223 --ei BufferTestDuration 60 --ei NumLoadThreads 4
    557             // --ei CI -1 --ez CaptureSysTrace true --ez CaptureWavs false --ei NumCaptures 5
    558             // --ei WavDuration 15
    559 
    560             // Note: for native mode, player and recorder buffer sizes are the same, and can only be
    561             // set through player buffer size
    562 
    563             boolean hasRecordAudioPermission = hasRecordAudioPermission();
    564             boolean hasWriteFilePermission = hasWriteFilePermission();
    565             if (!hasRecordAudioPermission || !hasWriteFilePermission) {
    566                 if (!hasRecordAudioPermission) {
    567                     log("Missing Permission: RECORD_AUDIO");
    568                 }
    569 
    570                 if (!hasWriteFilePermission) {
    571                     log("Missing Permission: WRITE_EXTERNAL_STORAGE");
    572                 }
    573 
    574                 return;
    575             }
    576 
    577             if (b.containsKey(INTENT_BUFFER_TEST_DURATION)) {
    578                 getApp().setBufferTestDuration(b.getInt(INTENT_BUFFER_TEST_DURATION));
    579                 mIntentRunning = true;
    580             }
    581 
    582             if (b.containsKey(INTENT_SAMPLING_FREQUENCY)) {
    583                 getApp().setSamplingRate(b.getInt(INTENT_SAMPLING_FREQUENCY));
    584                 mIntentRunning = true;
    585             }
    586 
    587             if (b.containsKey(INTENT_CHANNEL_INDEX)) {
    588                 getApp().setChannelIndex(b.getInt(INTENT_CHANNEL_INDEX));
    589                 mChannelIndex = b.getInt(INTENT_CHANNEL_INDEX);
    590                 mIntentRunning = true;
    591             }
    592 
    593             if (b.containsKey(INTENT_FILENAME)) {
    594                 mIntentFileName = b.getString(INTENT_FILENAME);
    595                 mIntentRunning = true;
    596             }
    597 
    598             if (b.containsKey(INTENT_RECORDER_BUFFER)) {
    599                 getApp().setRecorderBufferSizeInBytes(
    600                         b.getInt(INTENT_RECORDER_BUFFER) * Constant.BYTES_PER_FRAME);
    601                 mIntentRunning = true;
    602             }
    603 
    604             if (b.containsKey(INTENT_PLAYER_BUFFER)) {
    605                 getApp().setPlayerBufferSizeInBytes(
    606                         b.getInt(INTENT_PLAYER_BUFFER) * Constant.BYTES_PER_FRAME);
    607                 mIntentRunning = true;
    608             }
    609 
    610             if (b.containsKey(INTENT_AUDIO_THREAD)) {
    611                 getApp().setAudioThreadType(b.getInt(INTENT_AUDIO_THREAD));
    612                 mIntentRunning = true;
    613             }
    614 
    615             if (b.containsKey(INTENT_MIC_SOURCE)) {
    616                 getApp().setMicSource(b.getInt(INTENT_MIC_SOURCE));
    617                 mIntentRunning = true;
    618             }
    619 
    620             if (b.containsKey(INTENT_PERFORMANCE_MODE)) {
    621                 getApp().setPerformanceMode(b.getInt(INTENT_PERFORMANCE_MODE));
    622                 mIntentRunning = true;
    623             }
    624 
    625             if (b.containsKey(INTENT_IGNORE_FIRST_FRAMES)) {
    626                 getApp().setIgnoreFirstFrames(b.getInt(INTENT_IGNORE_FIRST_FRAMES));
    627                 mIntentRunning = true;
    628             }
    629 
    630             if (b.containsKey(INTENT_AUDIO_LEVEL)) {
    631                 int audioLevel = b.getInt(INTENT_AUDIO_LEVEL);
    632                 if (audioLevel >= 0) {
    633                     AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    634                     am.setStreamVolume(AudioManager.STREAM_MUSIC, audioLevel, 0);
    635                     getApp().setSoundLevelCalibrationEnabled(false);
    636                 } else { // AudioLevel of -1 means automatically calibrate
    637                     getApp().setSoundLevelCalibrationEnabled(true);
    638                 }
    639                 mIntentRunning = true;
    640             }
    641 
    642             if (b.containsKey(INTENT_NUMBER_LOAD_THREADS)) {
    643                 getApp().setNumberOfLoadThreads(b.getInt(INTENT_NUMBER_LOAD_THREADS));
    644                 mIntentRunning = true;
    645             }
    646 
    647             if (b.containsKey(INTENT_ENABLE_SYSTRACE)) {
    648                 getApp().setCaptureSysTraceEnabled(b.getBoolean(INTENT_ENABLE_SYSTRACE));
    649                 mIntentRunning = true;
    650             }
    651 
    652             if (b.containsKey(INTENT_ENABLE_WAVCAPTURE)) {
    653                 getApp().setCaptureWavsEnabled(b.getBoolean(INTENT_ENABLE_WAVCAPTURE));
    654                 mIntentRunning = true;
    655             }
    656 
    657             if (b.containsKey(INTENT_NUM_CAPTURES)) {
    658                 getApp().setNumberOfCaptures(b.getInt(INTENT_NUM_CAPTURES));
    659                 mIntentRunning = true;
    660             }
    661 
    662             if (b.containsKey(INTENT_WAV_DURATION)) {
    663                 getApp().setBufferTestWavePlotDuration(b.getInt(INTENT_WAV_DURATION));
    664                 mIntentRunning = true;
    665             }
    666 
    667             if (mIntentRunning || b.containsKey(INTENT_TEST_TYPE)) {
    668                 // run tests with provided or default parameters
    669                 refreshState();
    670 
    671                 // if no test is specified then Latency Test will be run
    672                 int testType = b.getInt(INTENT_TEST_TYPE,
    673                         Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY);
    674                 switch (testType) {
    675                     case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
    676                         startBufferTest();
    677                         break;
    678                     case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_CALIBRATION:
    679                         doCalibration();
    680                         break;
    681                     case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
    682                     default:
    683                         startLatencyTest();
    684                         break;
    685                 }
    686             }
    687 
    688         } else {
    689             if (mIntentRunning && b != null) {
    690                 log("Test already in progress");
    691                 showToast("Test already in progress");
    692             }
    693         }
    694     }
    695 
    696 
    697     /** Stop all currently running threads that are related to audio test. */
    698     private void stopAudioTestThreads() {
    699         log("stopping audio threads");
    700         if (mAudioThread != null) {
    701             try {
    702                 mAudioThread.finish();
    703                 mAudioThread.join(Constant.JOIN_WAIT_TIME_MS);
    704             } catch (InterruptedException e) {
    705                 e.printStackTrace();
    706             }
    707             mAudioThread = null;
    708         }
    709 
    710         if (mNativeAudioThread != null) {
    711             try {
    712                 mNativeAudioThread.finish();
    713                 mNativeAudioThread.join(Constant.JOIN_WAIT_TIME_MS);
    714             } catch (InterruptedException e) {
    715                 e.printStackTrace();
    716             }
    717             mNativeAudioThread = null;
    718         }
    719 
    720         stopLoadThreads();
    721         System.gc();
    722     }
    723 
    724 
    725     public void onDestroy() {
    726         stopAudioTestThreads();
    727         super.onDestroy();
    728         stopService(new Intent(this, AudioTestService.class));
    729     }
    730 
    731 
    732     @Override
    733     protected void onResume() {
    734         super.onResume();
    735         log("on resume called");
    736     }
    737 
    738 
    739     @Override
    740     protected void onPause() {
    741         super.onPause();
    742     }
    743 
    744     @Override
    745     public boolean onCreateOptionsMenu(Menu menu){
    746         MenuInflater inflater = getMenuInflater();
    747         inflater.inflate(R.menu.tool_bar_menu, menu);
    748         return true;
    749     }
    750 
    751     @Override
    752     public boolean onOptionsItemSelected(MenuItem item) {
    753         // Respond to user selecting action bar buttons
    754         switch (item.getItemId()) {
    755             case R.id.action_help:
    756                 if (!isBusy()) {
    757                     // Launch about Activity
    758                     Intent aboutIntent = new Intent(this, AboutActivity.class);
    759                     startActivity(aboutIntent);
    760                 } else {
    761                     showToast("Test in progress... please wait");
    762                 }
    763 
    764                 return true;
    765 
    766             case R.id.action_settings:
    767                 if (!isBusy()) {
    768                     // Launch settings activity
    769                     Intent mySettingsIntent = new Intent(this, SettingsActivity.class);
    770                     startActivityForResult(mySettingsIntent, SETTINGS_ACTIVITY_REQUEST);
    771                 } else {
    772                     showToast("Test in progress... please wait");
    773                 }
    774                 return true;
    775         }
    776 
    777         return super.onOptionsItemSelected(item);
    778     }
    779 
    780 
    781     /** Check if the app is busy (running test). */
    782     public boolean isBusy() {
    783         boolean busy = false;
    784 
    785         if (mAudioThread != null && mAudioThread.mIsRunning) {
    786             busy = true;
    787         }
    788 
    789         if (mNativeAudioThread != null && mNativeAudioThread.mIsRunning) {
    790             busy = true;
    791         }
    792 
    793         return busy;
    794     }
    795 
    796 
    797     /** Create a new audio thread according to the settings. */
    798     private void restartAudioSystem() {
    799         log("restart audio system...");
    800 
    801         int sessionId = 0; /* FIXME runtime test for am.generateAudioSessionId() in API 21 */
    802 
    803         mAudioThreadType = getApp().getAudioThreadType();
    804         mSamplingRate = getApp().getSamplingRate();
    805         mChannelIndex = getApp().getChannelIndex();
    806         mPlayerBufferSizeInBytes = getApp().getPlayerBufferSizeInBytes();
    807         mRecorderBufferSizeInBytes = getApp().getRecorderBufferSizeInBytes();
    808         mTestStartTimeString = (String) DateFormat.format("MMddkkmmss",
    809                 System.currentTimeMillis());
    810         mMicSource = getApp().getMicSource();
    811         mPerformanceMode = getApp().getPerformanceMode();
    812         mIgnoreFirstFrames = getApp().getIgnoreFirstFrames();
    813         AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    814         mSoundLevel = am.getStreamVolume(AudioManager.STREAM_MUSIC);
    815         mBufferTestDurationInSeconds = getApp().getBufferTestDuration();
    816         mBufferTestWavePlotDurationInSeconds = getApp().getBufferTestWavePlotDuration();
    817 
    818         mCaptureHolder = new CaptureHolder(getApp().getNumStateCaptures(),
    819                 getFileNamePrefix(), getApp().isCaptureWavSnippetsEnabled(),
    820                 getApp().isCaptureSysTraceEnabled(), getApp().isCaptureBugreportEnabled(),
    821                 this, mSamplingRate);
    822 
    823         log(" current sampling rate: " + mSamplingRate);
    824         stopAudioTestThreads();
    825 
    826         // select java or native audio thread
    827         int micSourceMapped;
    828         switch (mAudioThreadType) {
    829         case Constant.AUDIO_THREAD_TYPE_JAVA:
    830             micSourceMapped = getApp().mapMicSource(Constant.AUDIO_THREAD_TYPE_JAVA, mMicSource);
    831 
    832             int expectedRecorderBufferPeriod = Math.round(
    833                     (float) (mRecorderBufferSizeInBytes * Constant.MILLIS_PER_SECOND)
    834                             / (Constant.BYTES_PER_FRAME * mSamplingRate));
    835             mRecorderBufferPeriod.prepareMemberObjects(
    836                     Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND * mBufferTestDurationInSeconds,
    837                     expectedRecorderBufferPeriod, mCaptureHolder);
    838 
    839             int expectedPlayerBufferPeriod = Math.round(
    840                     (float) (mPlayerBufferSizeInBytes * Constant.MILLIS_PER_SECOND)
    841                             / (Constant.BYTES_PER_FRAME * mSamplingRate));
    842             mPlayerBufferPeriod.prepareMemberObjects(
    843                     Constant.MAX_RECORDED_LATE_CALLBACKS_PER_SECOND * mBufferTestDurationInSeconds,
    844                     expectedPlayerBufferPeriod, mCaptureHolder);
    845 
    846             mAudioThread = new LoopbackAudioThread(mSamplingRate, mPlayerBufferSizeInBytes,
    847                           mRecorderBufferSizeInBytes, micSourceMapped, /* no performance mode */ mRecorderBufferPeriod,
    848                           mPlayerBufferPeriod, mTestType, mBufferTestDurationInSeconds,
    849                           mBufferTestWavePlotDurationInSeconds, getApplicationContext(),
    850                           mChannelIndex, mCaptureHolder);
    851             mAudioThread.setMessageHandler(mMessageHandler);
    852             mAudioThread.mSessionId = sessionId;
    853             mAudioThread.start();
    854             break;
    855         case Constant.AUDIO_THREAD_TYPE_NATIVE:
    856             micSourceMapped = getApp().mapMicSource(Constant.AUDIO_THREAD_TYPE_NATIVE, mMicSource);
    857             int performanceModeMapped = getApp().mapPerformanceMode(mPerformanceMode);
    858             // Note: mRecorderBufferSizeInBytes will not actually be used, since recorder buffer
    859             // size = player buffer size in native mode
    860             mNativeAudioThread = new NativeAudioThread(mSamplingRate, mPlayerBufferSizeInBytes,
    861                                 mRecorderBufferSizeInBytes, micSourceMapped, performanceModeMapped, mTestType,
    862                                 mBufferTestDurationInSeconds, mBufferTestWavePlotDurationInSeconds,
    863                                 mIgnoreFirstFrames, mCaptureHolder);
    864             mNativeAudioThread.setMessageHandler(mMessageHandler);
    865             mNativeAudioThread.mSessionId = sessionId;
    866             mNativeAudioThread.start();
    867             break;
    868         }
    869 
    870         startLoadThreads();
    871 
    872         mMessageHandler.post(new Runnable() {
    873             @Override
    874             public void run() {
    875                 refreshState();
    876             }
    877         });
    878     }
    879 
    880 
    881     /** Start all LoadThread. */
    882     private void startLoadThreads() {
    883 
    884         if (getApp().getNumberOfLoadThreads() > 0) {
    885 
    886             mLoadThreads = new LoadThread[getApp().getNumberOfLoadThreads()];
    887 
    888             for (int i = 0; i < mLoadThreads.length; i++) {
    889                 mLoadThreads[i] = new LoadThread("Loopback_LoadThread_" + i);
    890                 mLoadThreads[i].start();
    891             }
    892         }
    893     }
    894 
    895 
    896     /** Stop all LoadThread. */
    897     private void stopLoadThreads() {
    898         log("stopping load threads");
    899         if (mLoadThreads != null) {
    900             for (int i = 0; i < mLoadThreads.length; i++) {
    901                 if (mLoadThreads[i] != null) {
    902                     try {
    903                         mLoadThreads[i].requestStop();
    904                         mLoadThreads[i].join(Constant.JOIN_WAIT_TIME_MS);
    905                     } catch (InterruptedException e) {
    906                         e.printStackTrace();
    907                     }
    908                     mLoadThreads[i] = null;
    909                 }
    910             }
    911         }
    912     }
    913 
    914 
    915     private void resetBufferPeriodRecord(BufferPeriod recorderBufferPeriod,
    916                                          BufferPeriod playerBufferPeriod) {
    917         recorderBufferPeriod.resetRecord();
    918         playerBufferPeriod.resetRecord();
    919     }
    920 
    921 
    922     private void setTransportButtonsState(int state){
    923         Button latencyStart = (Button) findViewById(R.id.buttonStartLatencyTest);
    924         Button bufferStart = (Button) findViewById(R.id.buttonStartBufferTest);
    925         Button calibrationStart = (Button) findViewById(R.id.buttonCalibrateSoundLevel);
    926 
    927         switch (state) {
    928             case LATENCY_TEST_STARTED:
    929                 findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.INVISIBLE);
    930                 mTextViewResultSummary.setText("");
    931                 findViewById(R.id.glitchReportPanel).setVisibility(View.INVISIBLE);
    932                 latencyStart.setCompoundDrawablesWithIntrinsicBounds(
    933                         R.drawable.ic_stop, 0, 0, 0);
    934                 bufferStart.setEnabled(false);
    935                 calibrationStart.setEnabled(false);
    936                 break;
    937 
    938             case LATENCY_TEST_ENDED:
    939                 findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.VISIBLE);
    940                 latencyStart.setCompoundDrawablesWithIntrinsicBounds(
    941                         R.drawable.ic_play_arrow, 0, 0, 0);
    942                 bufferStart.setEnabled(true);
    943                 calibrationStart.setEnabled(true);
    944                 break;
    945 
    946             case BUFFER_TEST_STARTED:
    947                 findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.INVISIBLE);
    948                 mTextViewResultSummary.setText("");
    949                 findViewById(R.id.glitchReportPanel).setVisibility(View.INVISIBLE);
    950                 bufferStart.setCompoundDrawablesWithIntrinsicBounds(
    951                         R.drawable.ic_stop, 0, 0, 0);
    952                 latencyStart.setEnabled(false);
    953                 calibrationStart.setEnabled(false);
    954                 break;
    955 
    956             case BUFFER_TEST_ENDED:
    957                 findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.VISIBLE);
    958                 findViewById(R.id.resultSummary).setVisibility(View.VISIBLE);
    959                 findViewById(R.id.glitchReportPanel).setVisibility(View.VISIBLE);
    960                 bufferStart.setCompoundDrawablesWithIntrinsicBounds(
    961                         R.drawable.ic_play_arrow, 0, 0, 0);
    962                 latencyStart.setEnabled(true);
    963                 calibrationStart.setEnabled(true);
    964                 break;
    965 
    966             case CALIBRATION_STARTED:
    967                 findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.INVISIBLE);
    968                 findViewById(R.id.resultSummary).setVisibility(View.INVISIBLE);
    969                 findViewById(R.id.glitchReportPanel).setVisibility(View.INVISIBLE);
    970                 bufferStart.setCompoundDrawablesWithIntrinsicBounds(
    971                         R.drawable.ic_stop, 0, 0, 0);
    972                 latencyStart.setEnabled(false);
    973                 bufferStart.setEnabled(false);
    974                 calibrationStart.setEnabled(false);
    975                 break;
    976 
    977             case CALIBRATION_ENDED:
    978                 findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.VISIBLE);
    979                 findViewById(R.id.resultSummary).setVisibility(View.VISIBLE);
    980                 findViewById(R.id.glitchReportPanel).setVisibility(View.VISIBLE);
    981                 bufferStart.setCompoundDrawablesWithIntrinsicBounds(
    982                         R.drawable.ic_play_arrow, 0, 0, 0);
    983                 latencyStart.setEnabled(true);
    984                 bufferStart.setEnabled(true);
    985                 calibrationStart.setEnabled(true);
    986                 break;
    987         }
    988     }
    989 
    990     private void doCalibrationIfEnabled(final Runnable onComplete) {
    991         if (getApp().isSoundLevelCalibrationEnabled()) {
    992             doCalibration(onComplete);
    993         } else {
    994             if (onComplete != null) {
    995                 onComplete.run();
    996             }
    997         }
    998     }
    999 
   1000     private void doCalibration() {
   1001         doCalibration(null);
   1002     }
   1003 
   1004     private void doCalibration(final Runnable onComplete) {
   1005         if (isBusy()) {
   1006             showToast("Test in progress... please wait");
   1007             return;
   1008         }
   1009 
   1010         if (!hasRecordAudioPermission()) {
   1011             requestRecordAudioPermission(PERMISSIONS_REQUEST_RECORD_AUDIO_LATENCY);
   1012             // Returning, otherwise we don't know if user accepted or rejected.
   1013             return;
   1014         }
   1015 
   1016         showToast("Calibrating sound level...");
   1017         final SoundLevelCalibration calibration =
   1018                 new SoundLevelCalibration(getApp().getSamplingRate(),
   1019                         getApp().getPlayerBufferSizeInBytes(),
   1020                         getApp().getRecorderBufferSizeInBytes(),
   1021                         getApp().getMicSource(), getApp().getPerformanceMode(), this);
   1022 
   1023         calibration.setChangeListener(new SoundLevelCalibration.SoundLevelChangeListener() {
   1024             @Override
   1025             void onChange(int newLevel) {
   1026                 refreshSoundLevelBar();
   1027             }
   1028         });
   1029 
   1030         mCalibrationThread = new Thread(new Runnable() {
   1031             @Override
   1032             public void run() {
   1033                 calibration.calibrate();
   1034                 showToast("Calibration complete");
   1035                 if (onComplete != null) {
   1036                     onComplete.run();
   1037                 }
   1038             }
   1039         });
   1040 
   1041         mCalibrationThread.start();
   1042     }
   1043 
   1044     /** Start the latency test. */
   1045     public void onButtonLatencyTest(View view) throws InterruptedException {
   1046         if (isBusy()) {
   1047             stopTests();
   1048             return;
   1049         }
   1050 
   1051         // Ensure we have RECORD_AUDIO permissions
   1052         // On Android M (API 23) we must request dangerous permissions each time we use them
   1053         if (hasRecordAudioPermission()) {
   1054             startLatencyTest();
   1055         } else {
   1056             requestRecordAudioPermission(PERMISSIONS_REQUEST_RECORD_AUDIO_LATENCY);
   1057         }
   1058     }
   1059 
   1060     private void startLatencyTest() {
   1061         if (isBusy()) {
   1062             showToast("Test in progress... please wait");
   1063             return;
   1064         }
   1065 
   1066         doCalibrationIfEnabled(latencyTestRunnable);
   1067     }
   1068 
   1069     private Runnable latencyTestRunnable = new Runnable() {
   1070         @Override
   1071         public void run() {
   1072             if (isBusy()) {
   1073                 showToast("Test in progress... please wait");
   1074                 return;
   1075             }
   1076 
   1077             mBarMasterLevel.post(new Runnable() {
   1078                 @Override
   1079                 public void run() {
   1080                     mBarMasterLevel.setEnabled(false);
   1081                 }
   1082             });
   1083             resetBufferPeriodRecord(mRecorderBufferPeriod, mPlayerBufferPeriod);
   1084 
   1085             mTestType = Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY;
   1086             restartAudioSystem();
   1087             try {
   1088                 Thread.sleep(THREAD_SLEEP_DURATION_MS);
   1089             } catch (InterruptedException e) {
   1090                 e.printStackTrace();
   1091             }
   1092 
   1093             switch (mAudioThreadType) {
   1094                 case Constant.AUDIO_THREAD_TYPE_JAVA:
   1095                     if (mAudioThread != null) {
   1096                         mAudioThread.runTest();
   1097                     }
   1098                     break;
   1099                 case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1100                     if (mNativeAudioThread != null) {
   1101                         mNativeAudioThread.runTest();
   1102                     }
   1103                     break;
   1104             }
   1105         }
   1106     };
   1107 
   1108 
   1109     /** Start the Buffer (Glitch Detection) Test. */
   1110     public void onButtonBufferTest(View view) throws InterruptedException {
   1111         if (isBusy()) {
   1112             stopTests();
   1113             return;
   1114         }
   1115 
   1116         if (hasRecordAudioPermission()) {
   1117             startBufferTest();
   1118         } else {
   1119             requestRecordAudioPermission(PERMISSIONS_REQUEST_RECORD_AUDIO_BUFFER);
   1120         }
   1121     }
   1122 
   1123 
   1124     private void startBufferTest() {
   1125 
   1126         if (!isBusy()) {
   1127             mBarMasterLevel.setEnabled(false);
   1128             resetBufferPeriodRecord(mRecorderBufferPeriod, mPlayerBufferPeriod);
   1129             mTestType = Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD;
   1130             restartAudioSystem();   // in this function a audio thread is created
   1131             try {
   1132                 Thread.sleep(THREAD_SLEEP_DURATION_MS);
   1133             } catch (InterruptedException e) {
   1134                 e.printStackTrace();
   1135             }
   1136 
   1137             switch (mAudioThreadType) {
   1138             case Constant.AUDIO_THREAD_TYPE_JAVA:
   1139                 if (mAudioThread != null) {
   1140                     mAudioThread.runBufferTest();
   1141                 }
   1142                 break;
   1143             case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1144                 if (mNativeAudioThread != null) {
   1145                     mNativeAudioThread.runBufferTest();
   1146                 }
   1147                 break;
   1148             }
   1149         } else {
   1150             int duration = 0;
   1151             switch (mAudioThreadType) {
   1152             case Constant.AUDIO_THREAD_TYPE_JAVA:
   1153                 duration = mAudioThread.getDurationInSeconds();
   1154                 break;
   1155             case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1156                 duration = mNativeAudioThread.getDurationInSeconds();
   1157                 break;
   1158             }
   1159             showToast("Long-run Test in progress, in total should take " +
   1160                     Integer.toString(duration) + "s, please wait");
   1161         }
   1162     }
   1163 
   1164 
   1165     /** Stop the ongoing test. */
   1166     public void stopTests() throws InterruptedException {
   1167         if (mAudioThread != null) {
   1168             mAudioThread.requestStopTest();
   1169         }
   1170 
   1171         if (mNativeAudioThread != null) {
   1172             mNativeAudioThread.requestStopTest();
   1173         }
   1174     }
   1175 
   1176     public void onButtonCalibrateSoundLevel(final View view) {
   1177         Message m = Message.obtain();
   1178         m.what = CALIBRATION_STARTED;
   1179         mMessageHandler.sendMessage(m);
   1180         Runnable onComplete = new Runnable() {
   1181             @Override
   1182             public void run() {
   1183                 Message m = Message.obtain();
   1184                 m.what = CALIBRATION_ENDED;
   1185                 mMessageHandler.sendMessage(m);
   1186             }
   1187         };
   1188         doCalibration(onComplete);
   1189     }
   1190 
   1191     /***
   1192      * Show dialog to choose to save files with filename dialog or not
   1193      */
   1194     public void onButtonSave(View view) {
   1195         if (!isBusy()) {
   1196             DialogFragment newFragment = new SaveFilesDialogFragment();
   1197             newFragment.show(getFragmentManager(), "saveFiles");
   1198         } else {
   1199             showToast("Test in progress... please wait");
   1200         }
   1201     }
   1202 
   1203     /**
   1204      * Save five files: one .png file for a screenshot on the main activity, one .wav file for
   1205      * the plot displayed on the main activity, one .txt file for storing various test results, one
   1206      * .txt file for storing recorder buffer period data, and one .txt file for storing player
   1207      * buffer period data.
   1208      */
   1209     private void SaveFilesWithDialog() {
   1210 
   1211         String fileName = "loopback_" + mTestStartTimeString;
   1212 
   1213         //Launch filename choosing activities if available, otherwise save without prompting
   1214         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
   1215             launchFileNameChoosingActivity("text/plain", fileName, ".txt", SAVE_TO_TXT_REQUEST);
   1216             launchFileNameChoosingActivity("image/png", fileName, ".png", SAVE_TO_PNG_REQUEST);
   1217             launchFileNameChoosingActivity("audio/wav", fileName, ".wav", SAVE_TO_WAVE_REQUEST);
   1218             launchFileNameChoosingActivity("text/plain", fileName, "_recorderBufferPeriod.txt",
   1219                     SAVE_RECORDER_BUFFER_PERIOD_TO_TXT_REQUEST);
   1220             launchFileNameChoosingActivity("text/plain", fileName, "_recorderBufferPeriodTimes.txt",
   1221                     SAVE_RECORDER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST);
   1222             launchFileNameChoosingActivity("image/png", fileName, "_recorderBufferPeriod.png",
   1223                     SAVE_RECORDER_BUFFER_PERIOD_TO_PNG_REQUEST);
   1224             launchFileNameChoosingActivity("text/plain", fileName, "_playerBufferPeriod.txt",
   1225                     SAVE_PLAYER_BUFFER_PERIOD_TO_TXT_REQUEST);
   1226             launchFileNameChoosingActivity("text/plain", fileName, "_playerBufferPeriodTimes.txt",
   1227                     SAVE_PLAYER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST);
   1228             launchFileNameChoosingActivity("image/png", fileName, "_playerBufferPeriod.png",
   1229                     SAVE_PLAYER_BUFFER_PERIOD_TO_PNG_REQUEST);
   1230 
   1231             if (mGlitchesData != null) {
   1232                 launchFileNameChoosingActivity("text/plain", fileName, "_glitchMillis.txt",
   1233                         SAVE_GLITCH_OCCURRENCES_TO_TEXT_REQUEST);
   1234                 launchFileNameChoosingActivity("image/png", fileName, "_heatMap.png",
   1235                         SAVE_GLITCH_AND_CALLBACK_HEATMAP_REQUEST);
   1236             }
   1237         } else {
   1238             saveAllTo(fileName);
   1239         }
   1240     }
   1241 
   1242     /**
   1243      * Launches an activity for choosing the filename of the file to be saved
   1244      */
   1245     public void launchFileNameChoosingActivity(String type, String fileName, String suffix,
   1246                                                int RequestCode) {
   1247         Intent FilenameIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
   1248         FilenameIntent.addCategory(Intent.CATEGORY_OPENABLE);
   1249         FilenameIntent.setType(type);
   1250         FilenameIntent.putExtra(Intent.EXTRA_TITLE, fileName + suffix);
   1251         startActivityForResult(FilenameIntent, RequestCode);
   1252     }
   1253 
   1254     private String getFileNamePrefix(){
   1255         if (mIntentFileName != null && !mIntentFileName.isEmpty()) {
   1256             return mIntentFileName;
   1257         } else {
   1258             return "loopback_" + mTestStartTimeString;
   1259         }
   1260     }
   1261 
   1262     /** See the documentation on onButtonSave() */
   1263     public void saveAllTo(String fileName) {
   1264 
   1265         if (!hasWriteFilePermission()) {
   1266             requestWriteFilePermission(PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE_RESULTS);
   1267             return;
   1268         }
   1269 
   1270         showToast("Saving files to: " + fileName + ".(wav,png,txt)");
   1271 
   1272         //save to a given uri... local file?
   1273         saveToWaveFile(Uri.parse(FILE_SAVE_PATH + fileName + ".wav"));
   1274 
   1275         saveScreenShot(Uri.parse(FILE_SAVE_PATH + fileName + ".png"));
   1276 
   1277         saveTextToFile(Uri.parse(FILE_SAVE_PATH + fileName + ".txt"), getReport().toString());
   1278 
   1279         int[] bufferPeriodArray = null;
   1280         int maxBufferPeriod = Constant.UNKNOWN;
   1281         switch (mAudioThreadType) {
   1282         case Constant.AUDIO_THREAD_TYPE_JAVA:
   1283             bufferPeriodArray = mRecorderBufferPeriod.getBufferPeriodArray();
   1284             maxBufferPeriod = mRecorderBufferPeriod.getMaxBufferPeriod();
   1285             break;
   1286         case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1287             bufferPeriodArray = mNativeRecorderBufferPeriodArray;
   1288             maxBufferPeriod = mNativeRecorderMaxBufferPeriod;
   1289             break;
   1290         }
   1291         saveBufferPeriod(Uri.parse(FILE_SAVE_PATH + fileName + "_recorderBufferPeriod.txt"),
   1292                 bufferPeriodArray, maxBufferPeriod);
   1293         saveHistogram(Uri.parse(FILE_SAVE_PATH + fileName + "_recorderBufferPeriod.png"),
   1294                 bufferPeriodArray, maxBufferPeriod);
   1295         saveTextToFile(Uri.parse(FILE_SAVE_PATH + fileName + "_recorderBufferPeriodTimes.txt"),
   1296                 mRecorderCallbackTimes.toString());
   1297 
   1298         bufferPeriodArray = null;
   1299         maxBufferPeriod = Constant.UNKNOWN;
   1300         switch (mAudioThreadType) {
   1301         case Constant.AUDIO_THREAD_TYPE_JAVA:
   1302             bufferPeriodArray = mPlayerBufferPeriod.getBufferPeriodArray();
   1303             maxBufferPeriod = mPlayerBufferPeriod.getMaxBufferPeriod();
   1304             break;
   1305         case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1306             bufferPeriodArray = mNativePlayerBufferPeriodArray;
   1307             maxBufferPeriod = mNativePlayerMaxBufferPeriod;
   1308             break;
   1309         }
   1310         saveBufferPeriod(Uri.parse(FILE_SAVE_PATH + fileName + "_playerBufferPeriod.txt")
   1311                 , bufferPeriodArray, maxBufferPeriod);
   1312         saveHistogram(Uri.parse(FILE_SAVE_PATH + fileName + "_playerBufferPeriod.png"),
   1313                 bufferPeriodArray, maxBufferPeriod);
   1314         saveTextToFile(Uri.parse(FILE_SAVE_PATH + fileName + "_playerBufferPeriodTimes.txt"),
   1315                 mPlayerCallbackTimes.toString());
   1316 
   1317         if (mGlitchesData != null) {
   1318             saveGlitchOccurrences(Uri.parse(FILE_SAVE_PATH + fileName + "_glitchMillis.txt"),
   1319                     mGlitchesData);
   1320             saveHeatMap(Uri.parse(FILE_SAVE_PATH + fileName + "_heatMap.png"),
   1321                     mRecorderCallbackTimes, mPlayerCallbackTimes,
   1322                     GlitchesStringBuilder.getGlitchMilliseconds(mFFTSamplingSize,
   1323                             mFFTOverlapSamples, mGlitchesData, mSamplingRate),
   1324                     mGlitchingIntervalTooLong, mBufferTestElapsedSeconds, fileName);
   1325         }
   1326 
   1327     }
   1328 
   1329 
   1330     @Override
   1331     public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
   1332         log("ActivityResult request: " + requestCode + "  result:" + resultCode);
   1333 
   1334         if (resultCode == Activity.RESULT_OK) {
   1335             switch (requestCode) {
   1336             case SAVE_TO_WAVE_REQUEST:
   1337                 log("got SAVE TO WAV intent back!");
   1338                 if (resultData != null) {
   1339                     saveToWaveFile(resultData.getData());
   1340                 }
   1341                 break;
   1342             case SAVE_TO_PNG_REQUEST:
   1343                 log("got SAVE TO PNG intent back!");
   1344                 if (resultData != null) {
   1345                     saveScreenShot(resultData.getData());
   1346                 }
   1347                 break;
   1348             case SAVE_TO_TXT_REQUEST:
   1349                 if (resultData != null) {
   1350                     saveTextToFile(resultData.getData(), getReport().toString());
   1351                 }
   1352                 break;
   1353             case SAVE_RECORDER_BUFFER_PERIOD_TO_TXT_REQUEST:
   1354                 if (resultData != null) {
   1355                     int[] bufferPeriodArray = null;
   1356                     int maxBufferPeriod = Constant.UNKNOWN;
   1357                     switch (mAudioThreadType) {
   1358                     case Constant.AUDIO_THREAD_TYPE_JAVA:
   1359                         bufferPeriodArray = mRecorderBufferPeriod.getBufferPeriodArray();
   1360                         maxBufferPeriod = mRecorderBufferPeriod.getMaxBufferPeriod();
   1361                         break;
   1362                     case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1363                         bufferPeriodArray = mNativeRecorderBufferPeriodArray;
   1364                         maxBufferPeriod = mNativeRecorderMaxBufferPeriod;
   1365                         break;
   1366                     }
   1367                     saveBufferPeriod(resultData.getData(), bufferPeriodArray, maxBufferPeriod);
   1368                 }
   1369                 break;
   1370             case SAVE_PLAYER_BUFFER_PERIOD_TO_TXT_REQUEST:
   1371                 if (resultData != null) {
   1372                     int[] bufferPeriodArray = null;
   1373                     int maxBufferPeriod = Constant.UNKNOWN;
   1374                     switch (mAudioThreadType) {
   1375                     case Constant.AUDIO_THREAD_TYPE_JAVA:
   1376                         bufferPeriodArray = mPlayerBufferPeriod.getBufferPeriodArray();
   1377                         maxBufferPeriod = mPlayerBufferPeriod.getMaxBufferPeriod();
   1378                         break;
   1379                     case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1380                         bufferPeriodArray = mNativePlayerBufferPeriodArray;
   1381                         maxBufferPeriod = mNativePlayerMaxBufferPeriod;
   1382                         break;
   1383                     }
   1384                     saveBufferPeriod(resultData.getData(), bufferPeriodArray, maxBufferPeriod);
   1385                 }
   1386                 break;
   1387             case SAVE_RECORDER_BUFFER_PERIOD_TO_PNG_REQUEST:
   1388                 if (resultData != null) {
   1389                     int[] bufferPeriodArray = null;
   1390                     int maxBufferPeriod = Constant.UNKNOWN;
   1391                     switch (mAudioThreadType) {
   1392                         case Constant.AUDIO_THREAD_TYPE_JAVA:
   1393                             bufferPeriodArray = mRecorderBufferPeriod.getBufferPeriodArray();
   1394                             maxBufferPeriod = mRecorderBufferPeriod.getMaxBufferPeriod();
   1395                             break;
   1396                         case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1397                             bufferPeriodArray = mNativeRecorderBufferPeriodArray;
   1398                             maxBufferPeriod = mNativeRecorderMaxBufferPeriod;
   1399                             break;
   1400                     }
   1401                     saveHistogram(resultData.getData(), bufferPeriodArray, maxBufferPeriod);
   1402                 }
   1403                 break;
   1404             case SAVE_PLAYER_BUFFER_PERIOD_TO_PNG_REQUEST:
   1405                 if (resultData != null) {
   1406                     int[] bufferPeriodArray = null;
   1407                     int maxBufferPeriod = Constant.UNKNOWN;
   1408                     switch (mAudioThreadType) {
   1409                         case Constant.AUDIO_THREAD_TYPE_JAVA:
   1410                             bufferPeriodArray = mPlayerBufferPeriod.getBufferPeriodArray();
   1411                             maxBufferPeriod = mPlayerBufferPeriod.getMaxBufferPeriod();
   1412                             break;
   1413                         case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1414                             bufferPeriodArray = mNativePlayerBufferPeriodArray;
   1415                             maxBufferPeriod = mNativePlayerMaxBufferPeriod;
   1416                             break;
   1417                     }
   1418                     saveHistogram(resultData.getData(), bufferPeriodArray, maxBufferPeriod);
   1419                 }
   1420                 break;
   1421             case SAVE_PLAYER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST:
   1422                 if (resultData != null) {
   1423                     saveTextToFile(resultData.getData(),
   1424                             mPlayerCallbackTimes.toString());
   1425                 }
   1426                 break;
   1427             case SAVE_RECORDER_BUFFER_PERIOD_TIMES_TO_TXT_REQUEST:
   1428                 if (resultData != null) {
   1429                     saveTextToFile(resultData.getData(),
   1430                             mRecorderCallbackTimes.toString());
   1431                 }
   1432                 break;
   1433             case SAVE_GLITCH_OCCURRENCES_TO_TEXT_REQUEST:
   1434                 if (resultData != null) {
   1435                     saveGlitchOccurrences(resultData.getData(), mGlitchesData);
   1436                 }
   1437                 break;
   1438             case SAVE_GLITCH_AND_CALLBACK_HEATMAP_REQUEST:
   1439                 if (resultData != null && mGlitchesData != null && mRecorderCallbackTimes != null
   1440                         & mPlayerCallbackTimes != null){
   1441                     saveHeatMap(resultData.getData(), mRecorderCallbackTimes, mPlayerCallbackTimes,
   1442                             GlitchesStringBuilder.getGlitchMilliseconds(mFFTSamplingSize,
   1443                                     mFFTOverlapSamples, mGlitchesData, mSamplingRate),
   1444                             mGlitchingIntervalTooLong, mBufferTestElapsedSeconds,
   1445                             resultData.getData().toString());
   1446                 }
   1447             case SETTINGS_ACTIVITY_REQUEST:
   1448                 log("return from new settings!");
   1449 
   1450                 break;
   1451             }
   1452         }
   1453     }
   1454 
   1455 
   1456     /**
   1457      * Refresh the sound level bar on the main activity to reflect the current sound level
   1458      * of the system.
   1459      */
   1460     private void refreshSoundLevelBar() {
   1461         mBarMasterLevel.setEnabled(true);
   1462         AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
   1463         int currentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
   1464         mBarMasterLevel.setProgress(currentVolume);
   1465 
   1466         mTextViewCurrentLevel.setText(String.format("Current Sound Level: %d/%d", currentVolume,
   1467                 mBarMasterLevel.getMax()));
   1468     }
   1469 
   1470 
   1471     /** Reset all results gathered from previous round of test (if any). */
   1472     private void resetResults() {
   1473         mCorrelation.invalidate();
   1474         mNativeRecorderBufferPeriodArray = null;
   1475         mNativePlayerBufferPeriodArray = null;
   1476         mPlayerCallbackTimes = null;
   1477         mRecorderCallbackTimes = null;
   1478         mGlitchesData = null;
   1479         mWaveData = null;
   1480     }
   1481 
   1482 
   1483     /** Get the file path from uri. Doesn't work for all devices. */
   1484     private String getPath(Uri uri) {
   1485         String[] projection = {MediaStore.Images.Media.DATA};
   1486         Cursor cursor1 = getContentResolver().query(uri, projection, null, null, null);
   1487         if (cursor1 == null) {
   1488             return uri.getPath();
   1489         }
   1490 
   1491         int ColumnIndex = cursor1.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
   1492         cursor1.moveToFirst();
   1493         String path = cursor1.getString(ColumnIndex);
   1494         cursor1.close();
   1495         return path;
   1496     }
   1497 
   1498 
   1499     /** Zoom out the plot to its full size. */
   1500     public void onButtonZoomOutFull(View view) {
   1501         double fullZoomOut = mWavePlotView.getMaxZoomOut();
   1502         mWavePlotView.setZoom(fullZoomOut);
   1503         mWavePlotView.refreshGraph();
   1504     }
   1505 
   1506 
   1507     /** Zoom out the plot. */
   1508     public void onButtonZoomOut(View view) {
   1509         double zoom = mWavePlotView.getZoom();
   1510         zoom = 2.0 * zoom;
   1511         mWavePlotView.setZoom(zoom);
   1512         mWavePlotView.refreshGraph();
   1513     }
   1514 
   1515 
   1516     /** Zoom in the plot. */
   1517     public void onButtonZoomIn(View view) {
   1518         double zoom = mWavePlotView.getZoom();
   1519         zoom = zoom / 2.0;
   1520         mWavePlotView.setZoom(zoom);
   1521         mWavePlotView.refreshGraph();
   1522     }
   1523 
   1524 
   1525     /** Go to RecorderBufferPeriodActivity */
   1526     public void onButtonRecorderBufferPeriod(View view) {
   1527         if (!isBusy()) {
   1528             Intent RecorderBufferPeriodIntent = new Intent(this,
   1529                                                 RecorderBufferPeriodActivity.class);
   1530             int recorderBufferSizeInFrames = mRecorderBufferSizeInBytes / Constant.BYTES_PER_FRAME;
   1531             log("recorderBufferSizeInFrames:" + recorderBufferSizeInFrames);
   1532 
   1533             switch (mAudioThreadType) {
   1534             case Constant.AUDIO_THREAD_TYPE_JAVA:
   1535                 RecorderBufferPeriodIntent.putExtra("recorderBufferPeriodArray",
   1536                         mRecorderBufferPeriod.getBufferPeriodArray());
   1537                 RecorderBufferPeriodIntent.putExtra("recorderBufferPeriodMax",
   1538                         mRecorderBufferPeriod.getMaxBufferPeriod());
   1539                 break;
   1540             case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1541                 RecorderBufferPeriodIntent.putExtra("recorderBufferPeriodArray",
   1542                         mNativeRecorderBufferPeriodArray);
   1543                 RecorderBufferPeriodIntent.putExtra("recorderBufferPeriodMax",
   1544                         mNativeRecorderMaxBufferPeriod);
   1545                 break;
   1546             }
   1547 
   1548             RecorderBufferPeriodIntent.putExtra("recorderBufferSize", recorderBufferSizeInFrames);
   1549             RecorderBufferPeriodIntent.putExtra("samplingRate", mSamplingRate);
   1550             startActivity(RecorderBufferPeriodIntent);
   1551         } else
   1552             showToast("Test in progress... please wait");
   1553     }
   1554 
   1555 
   1556     /** Go to PlayerBufferPeriodActivity */
   1557     public void onButtonPlayerBufferPeriod(View view) {
   1558         if (!isBusy()) {
   1559             Intent PlayerBufferPeriodIntent = new Intent(this, PlayerBufferPeriodActivity.class);
   1560             int playerBufferSizeInFrames = mPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME;
   1561 
   1562             switch (mAudioThreadType) {
   1563             case Constant.AUDIO_THREAD_TYPE_JAVA:
   1564                 PlayerBufferPeriodIntent.putExtra("playerBufferPeriodArray",
   1565                         mPlayerBufferPeriod.getBufferPeriodArray());
   1566                 PlayerBufferPeriodIntent.putExtra("playerBufferPeriodMax",
   1567                         mPlayerBufferPeriod.getMaxBufferPeriod());
   1568                 break;
   1569             case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1570                 PlayerBufferPeriodIntent.putExtra("playerBufferPeriodArray",
   1571                         mNativePlayerBufferPeriodArray);
   1572                 PlayerBufferPeriodIntent.putExtra("playerBufferPeriodMax",
   1573                         mNativePlayerMaxBufferPeriod);
   1574                 break;
   1575             }
   1576 
   1577             PlayerBufferPeriodIntent.putExtra("playerBufferSize", playerBufferSizeInFrames);
   1578             PlayerBufferPeriodIntent.putExtra("samplingRate", mSamplingRate);
   1579             startActivity(PlayerBufferPeriodIntent);
   1580         } else
   1581             showToast("Test in progress... please wait");
   1582     }
   1583 
   1584 
   1585     /** Display pop up window of recorded glitches */
   1586     public void onButtonGlitches(View view) {
   1587         if (!isBusy()) {
   1588             if (mGlitchesData != null) {
   1589                 // Create a PopUpWindow with scrollable TextView
   1590                 View puLayout = this.getLayoutInflater().inflate(R.layout.report_window, null);
   1591                 PopupWindow popUp = new PopupWindow(puLayout, ViewGroup.LayoutParams.MATCH_PARENT,
   1592                         ViewGroup.LayoutParams.MATCH_PARENT, true);
   1593 
   1594                 // Generate report of glitch intervals and set pop up window text
   1595                 TextView GlitchText =
   1596                         (TextView) popUp.getContentView().findViewById(R.id.ReportInfo);
   1597                 GlitchText.setText(GlitchesStringBuilder.getGlitchString(mFFTSamplingSize,
   1598                         mFFTOverlapSamples, mGlitchesData, mSamplingRate,
   1599                         mGlitchingIntervalTooLong, estimateNumberOfGlitches(mGlitchesData)));
   1600 
   1601                 // display pop up window, dismissible with back button
   1602                 popUp.showAtLocation(findViewById(R.id.linearLayoutMain), Gravity.TOP, 0, 0);
   1603             } else {
   1604                 showToast("Please run the buffer test to get data");
   1605             }
   1606 
   1607         } else {
   1608             showToast("Test in progress... please wait");
   1609         }
   1610     }
   1611 
   1612     /** Display pop up window of recorded metrics and system information */
   1613     public void onButtonReport(View view) {
   1614         if (!isBusy()) {
   1615             if ((mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD
   1616                     && mGlitchesData != null)
   1617                     || (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY
   1618                     && mCorrelation.isValid())) {
   1619                 // Create a PopUpWindow with scrollable TextView
   1620                 View puLayout = this.getLayoutInflater().inflate(R.layout.report_window, null);
   1621                 PopupWindow popUp = new PopupWindow(puLayout, ViewGroup.LayoutParams.MATCH_PARENT,
   1622                         ViewGroup.LayoutParams.MATCH_PARENT, true);
   1623 
   1624                 // Generate report of glitch intervals and set pop up window text
   1625                 TextView reportText =
   1626                         (TextView) popUp.getContentView().findViewById(R.id.ReportInfo);
   1627                 reportText.setText(getReport().toString());
   1628 
   1629                 // display pop up window, dismissible with back button
   1630                 popUp.showAtLocation(findViewById(R.id.linearLayoutMain), Gravity.TOP, 0, 0);
   1631             } else {
   1632                 showToast("Please run the tests to get data");
   1633             }
   1634 
   1635         } else {
   1636             showToast("Test in progress... please wait");
   1637         }
   1638     }
   1639 
   1640     /** Display pop up window of recorded metrics and system information */
   1641     public void onButtonHeatMap(View view) {
   1642         if (!isBusy()) {
   1643             if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD
   1644                     && mGlitchesData != null && mRecorderCallbackTimes != null
   1645                     && mRecorderCallbackTimes != null) {
   1646 
   1647                 // Create a PopUpWindow with heatMap custom view
   1648                 View puLayout = this.getLayoutInflater().inflate(R.layout.heatmap_window, null);
   1649                 PopupWindow popUp = new PopupWindow(puLayout, ViewGroup.LayoutParams.MATCH_PARENT,
   1650                         ViewGroup.LayoutParams.MATCH_PARENT, true);
   1651 
   1652                 ((LinearLayout) popUp.getContentView()).addView(
   1653                         new GlitchAndCallbackHeatMapView(this, mRecorderCallbackTimes,
   1654                                 mPlayerCallbackTimes,
   1655                                 GlitchesStringBuilder.getGlitchMilliseconds(mFFTSamplingSize,
   1656                                         mFFTOverlapSamples, mGlitchesData, mSamplingRate),
   1657                                 mGlitchingIntervalTooLong, mBufferTestElapsedSeconds,
   1658                                 getResources().getString(R.string.heatTitle)));
   1659 
   1660                 popUp.showAtLocation(findViewById(R.id.linearLayoutMain), Gravity.TOP, 0, 0);
   1661 
   1662             } else {
   1663                 showToast("Please run the tests to get data");
   1664             }
   1665 
   1666         } else {
   1667             showToast("Test in progress... please wait");
   1668         }
   1669     }
   1670 
   1671     /** Redraw the plot according to mWaveData */
   1672     void refreshPlots() {
   1673         mWavePlotView.setData(mWaveData, mSamplingRate);
   1674         mWavePlotView.redraw();
   1675     }
   1676 
   1677     /** Refresh the text on the main activity that shows the app states and audio settings. */
   1678     void refreshState() {
   1679         log("refreshState!");
   1680         refreshSoundLevelBar();
   1681 
   1682         // get info
   1683         int playerFrames = mPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME;
   1684         int recorderFrames = mRecorderBufferSizeInBytes / Constant.BYTES_PER_FRAME;
   1685         StringBuilder s = new StringBuilder(200);
   1686 
   1687         s.append("Settings from most recent run (at ");
   1688         s.append(mTestStartTimeString);
   1689         s.append("):\n");
   1690 
   1691         s.append("SR: ").append(mSamplingRate).append(" Hz");
   1692         s.append(" ChannelIndex: ").append(mChannelIndex < 0 ? "MONO" : mChannelIndex);
   1693         switch (mAudioThreadType) {
   1694         case Constant.AUDIO_THREAD_TYPE_JAVA:
   1695             s.append(" Play Frames: " ).append(playerFrames);
   1696             s.append(" Record Frames: ").append(recorderFrames);
   1697             s.append(" Audio: JAVA");
   1698             break;
   1699         case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1700             s.append(" Frames: ").append(playerFrames);
   1701             s.append(" Audio: NATIVE");
   1702             break;
   1703         }
   1704 
   1705         // mic source
   1706         String micSourceName = getApp().getMicSourceString(mMicSource);
   1707         if (micSourceName != null) {
   1708             s.append(" Mic: ").append(micSourceName);
   1709         }
   1710 
   1711         // performance mode
   1712         String performanceModeName = getApp().getPerformanceModeString(mPerformanceMode);
   1713         if (performanceModeName != null) {
   1714             s.append(" Performance Mode: ").append(performanceModeName);
   1715         }
   1716 
   1717         // sound level at start of test
   1718         s.append(" Sound Level: ").append(mSoundLevel).append("/").append(mBarMasterLevel.getMax());
   1719 
   1720         // load threads
   1721         s.append(" Simulated Load Threads: ").append(getApp().getNumberOfLoadThreads());
   1722 
   1723         // Show short summary of results, round trip latency or number of glitches
   1724         if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY) {
   1725             if (mIgnoreFirstFrames > 0) {
   1726                 s.append(" First Frames Ignored: ").append(mIgnoreFirstFrames);
   1727             }
   1728             if (mCorrelation.isValid()) {
   1729                 mTextViewResultSummary.setText(String.format("Latency: %.2f ms Confidence: %.2f" +
   1730                                 " Average = %.4f RMS = %.4f",
   1731                         mCorrelation.mEstimatedLatencyMs, mCorrelation.mEstimatedLatencyConfidence,
   1732                         mCorrelation.mAverage, mCorrelation.mRms));
   1733             }
   1734         } else if (mTestType == Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD &&
   1735                 mGlitchesData != null) {
   1736             // show buffer test duration
   1737             s.append("\nBuffer Test Duration: ").append(mBufferTestDurationInSeconds).append(" s");
   1738 
   1739             // show buffer test wave plot duration
   1740             s.append("   Buffer Test Wave Plot Duration: last ");
   1741             s.append(mBufferTestWavePlotDurationInSeconds);
   1742             s.append(" s");
   1743 
   1744             mTextViewResultSummary.setText(getResources().getString(R.string.numGlitches) + " " +
   1745                     estimateNumberOfGlitches(mGlitchesData));
   1746         } else {
   1747             mTextViewResultSummary.setText("");
   1748         }
   1749 
   1750         String info = getApp().getSystemInfo();
   1751         s.append(" ").append(info);
   1752 
   1753         mTextInfo.setText(s.toString());
   1754     }
   1755 
   1756 
   1757     private static void log(String msg) {
   1758         Log.v(TAG, msg);
   1759     }
   1760 
   1761 
   1762     public void showToast(final String msg) {
   1763         // Make sure UI manipulations are only done on the UI thread
   1764         LoopbackActivity.this.runOnUiThread(new Runnable() {
   1765             public void run() {
   1766                 Toast toast = Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG);
   1767                 toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL, 10, 10);
   1768                 toast.show();
   1769             }
   1770         });
   1771     }
   1772 
   1773 
   1774     /** Get the application that runs this activity. Wrapper for getApplication(). */
   1775     private LoopbackApplication getApp() {
   1776         return (LoopbackApplication) this.getApplication();
   1777     }
   1778 
   1779 
   1780     /** Save a .wav file of the wave plot on the main activity. */
   1781     void saveToWaveFile(Uri uri) {
   1782         if (mWaveData != null && mWaveData.length > 0) {
   1783             AudioFileOutput audioFileOutput = new AudioFileOutput(getApplicationContext(), uri,
   1784                                                                   mSamplingRate);
   1785             boolean status = audioFileOutput.writeData(mWaveData);
   1786             if (status) {
   1787                 String wavFileAbsolutePath = getPath(uri);
   1788                 // for some devices getPath fails
   1789                 if (wavFileAbsolutePath != null) {
   1790                     File file = new File(wavFileAbsolutePath);
   1791                     wavFileAbsolutePath = file.getAbsolutePath();
   1792                 } else {
   1793                     wavFileAbsolutePath = "";
   1794                 }
   1795                 showToast("Finished exporting wave File " + wavFileAbsolutePath);
   1796             } else {
   1797                 showToast("Something failed saving wave file");
   1798             }
   1799 
   1800         }
   1801     }
   1802 
   1803 
   1804     /** Save a screenshot of the main activity. */
   1805     void saveScreenShot(Uri uri) {
   1806         ParcelFileDescriptor parcelFileDescriptor = null;
   1807         FileOutputStream outputStream;
   1808         try {
   1809             parcelFileDescriptor = getApplicationContext().getContentResolver().
   1810                                    openFileDescriptor(uri, "w");
   1811 
   1812             FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
   1813             outputStream = new FileOutputStream(fileDescriptor);
   1814 
   1815             log("Done creating output stream");
   1816 
   1817             LinearLayout LL = (LinearLayout) findViewById(R.id.linearLayoutMain);
   1818 
   1819             View v = LL.getRootView();
   1820             v.setDrawingCacheEnabled(true);
   1821             Bitmap b = v.getDrawingCache();
   1822 
   1823             //save
   1824             b.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
   1825             parcelFileDescriptor.close();
   1826             v.setDrawingCacheEnabled(false);
   1827         } catch (Exception e) {
   1828             log("Failed to open png file " + e);
   1829         } finally {
   1830             try {
   1831                 if (parcelFileDescriptor != null) {
   1832                     parcelFileDescriptor.close();
   1833                 }
   1834             } catch (Exception e) {
   1835                 e.printStackTrace();
   1836                 log("Error closing ParcelFile Descriptor");
   1837             }
   1838         }
   1839     }
   1840 
   1841     private void saveHistogram(Uri uri, int[] bufferPeriodArray, int maxBufferPeriod) {
   1842         // Create and histogram view bitmap
   1843         HistogramView recordHisto = new HistogramView(this,null);
   1844         recordHisto.setBufferPeriodArray(bufferPeriodArray);
   1845         recordHisto.setMaxBufferPeriod(maxBufferPeriod);
   1846 
   1847         // Draw histogram on bitmap canvas
   1848         Bitmap histoBmp = Bitmap.createBitmap(HISTOGRAM_EXPORT_WIDTH,
   1849                 HISTOGRAM_EXPORT_HEIGHT, Bitmap.Config.ARGB_8888); // creates a MUTABLE bitmap
   1850         recordHisto.fillCanvas(new Canvas(histoBmp), histoBmp.getWidth(), histoBmp.getHeight());
   1851 
   1852         saveImage(uri, histoBmp);
   1853     }
   1854 
   1855     private void saveHeatMap(Uri uri, BufferCallbackTimes recorderCallbackTimes,
   1856                              BufferCallbackTimes playerCallbackTimes, int[] glitchMilliseconds,
   1857                              boolean glitchesExceeded, int duration, String title) {
   1858         Bitmap heatBmp = Bitmap.createBitmap(HEATMAP_DRAW_WIDTH, HEATMAP_DRAW_HEIGHT,
   1859                 Bitmap.Config.ARGB_8888);
   1860         GlitchAndCallbackHeatMapView.fillCanvas(new Canvas(heatBmp), recorderCallbackTimes,
   1861                 playerCallbackTimes, glitchMilliseconds, glitchesExceeded, duration,
   1862                 title);
   1863         saveImage(uri, Bitmap.createScaledBitmap(heatBmp,
   1864                 HEATMAP_DRAW_WIDTH / HEATMAP_EXPORT_DIVISOR,
   1865                 HEATMAP_DRAW_HEIGHT / HEATMAP_EXPORT_DIVISOR, false));
   1866     }
   1867 
   1868     /** Save an image to file. */
   1869     private void saveImage(Uri uri, Bitmap bmp) {
   1870         ParcelFileDescriptor parcelFileDescriptor = null;
   1871         FileOutputStream outputStream;
   1872         try {
   1873             parcelFileDescriptor = getApplicationContext().getContentResolver().
   1874                     openFileDescriptor(uri, "w");
   1875 
   1876             FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
   1877             outputStream = new FileOutputStream(fileDescriptor);
   1878 
   1879             log("Done creating output stream");
   1880 
   1881             // Save compressed bitmap to file
   1882             bmp.compress(Bitmap.CompressFormat.PNG, EXPORTED_IMAGE_QUALITY, outputStream);
   1883             parcelFileDescriptor.close();
   1884         } catch (Exception e) {
   1885             log("Failed to open png file " + e);
   1886         } finally {
   1887             try {
   1888                 if (parcelFileDescriptor != null) {
   1889                     parcelFileDescriptor.close();
   1890                 }
   1891             } catch (Exception e) {
   1892                 e.printStackTrace();
   1893                 log("Error closing ParcelFile Descriptor");
   1894             }
   1895         }
   1896     }
   1897 
   1898 
   1899     /**
   1900      * Save a .txt file of the given buffer period's data.
   1901      * First column is time, second column is count.
   1902      */
   1903     void saveBufferPeriod(Uri uri, int[] bufferPeriodArray, int maxBufferPeriod) {
   1904         ParcelFileDescriptor parcelFileDescriptor = null;
   1905         FileOutputStream outputStream;
   1906         if (bufferPeriodArray != null) {
   1907             try {
   1908                 parcelFileDescriptor = getApplicationContext().getContentResolver().
   1909                         openFileDescriptor(uri, "w");
   1910 
   1911                 FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
   1912                 outputStream = new FileOutputStream(fileDescriptor);
   1913                 log("Done creating output stream for saving buffer period");
   1914 
   1915                 int usefulDataRange = Math.min(maxBufferPeriod + 1, bufferPeriodArray.length);
   1916                 int[] usefulBufferData = Arrays.copyOfRange(bufferPeriodArray, 0, usefulDataRange);
   1917 
   1918                 String endline = "\n";
   1919                 String delimiter = ",";
   1920                 StringBuilder sb = new StringBuilder();
   1921                 for (int i = 0; i < usefulBufferData.length; i++) {
   1922                     sb.append(i + delimiter + usefulBufferData[i] + endline);
   1923                 }
   1924 
   1925                 outputStream.write(sb.toString().getBytes());
   1926 
   1927             } catch (Exception e) {
   1928                 log("Failed to open text file " + e);
   1929             } finally {
   1930                 try {
   1931                     if (parcelFileDescriptor != null) {
   1932                         parcelFileDescriptor.close();
   1933                     }
   1934                 } catch (Exception e) {
   1935                     e.printStackTrace();
   1936                     log("Error closing ParcelFile Descriptor");
   1937                 }
   1938             }
   1939         }
   1940 
   1941     }
   1942 
   1943     /** Save a .txt file of various test results. */
   1944     void saveTextToFile(Uri uri, String outputText) {
   1945         ParcelFileDescriptor parcelFileDescriptor = null;
   1946         FileOutputStream outputStream;
   1947         try {
   1948             parcelFileDescriptor = getApplicationContext().getContentResolver().
   1949                                    openFileDescriptor(uri, "w");
   1950 
   1951             FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
   1952             outputStream = new FileOutputStream(fileDescriptor);
   1953             log("Done creating output stream");
   1954 
   1955             outputStream.write(outputText.getBytes());
   1956             parcelFileDescriptor.close();
   1957         } catch (Exception e) {
   1958             log("Failed to open text file " + e);
   1959         } finally {
   1960             try {
   1961                 if (parcelFileDescriptor != null) {
   1962                     parcelFileDescriptor.close();
   1963                 }
   1964             } catch (Exception e) {
   1965                 e.printStackTrace();
   1966                 log("Error closing ParcelFile Descriptor");
   1967             }
   1968         }
   1969     }
   1970 
   1971     private StringBuilder getReport() {
   1972         String endline = "\n";
   1973         final int stringLength = 300;
   1974         StringBuilder sb = new StringBuilder(stringLength);
   1975         sb.append("DateTime = " + mTestStartTimeString + endline);
   1976         sb.append(INTENT_SAMPLING_FREQUENCY + " = " + mSamplingRate + endline);
   1977         sb.append(INTENT_CHANNEL_INDEX + " = " + mChannelIndex + endline);
   1978         sb.append(INTENT_RECORDER_BUFFER + " = " + mRecorderBufferSizeInBytes /
   1979                 Constant.BYTES_PER_FRAME + endline);
   1980         sb.append(INTENT_PLAYER_BUFFER + " = " + mPlayerBufferSizeInBytes /
   1981                 Constant.BYTES_PER_FRAME + endline);
   1982         sb.append(INTENT_AUDIO_THREAD + " = " + mAudioThreadType + endline);
   1983 
   1984         String audioType = "unknown";
   1985         switch (mAudioThreadType) {
   1986             case Constant.AUDIO_THREAD_TYPE_JAVA:
   1987                 audioType = "JAVA";
   1988                 break;
   1989             case Constant.AUDIO_THREAD_TYPE_NATIVE:
   1990                 audioType = "NATIVE";
   1991                 break;
   1992         }
   1993         sb.append(INTENT_AUDIO_THREAD + "_String = " + audioType + endline);
   1994 
   1995         sb.append(INTENT_MIC_SOURCE + " = " + mMicSource + endline);
   1996         sb.append(INTENT_MIC_SOURCE + "_String = " + getApp().getMicSourceString(mMicSource)
   1997                 + endline);
   1998         sb.append(INTENT_AUDIO_LEVEL + " = " + mSoundLevel + endline);
   1999 
   2000         switch (mTestType) {
   2001             case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
   2002                 sb.append(INTENT_IGNORE_FIRST_FRAMES + " = " + mIgnoreFirstFrames + endline);
   2003                 if (mCorrelation.isValid()) {
   2004                     sb.append(String.format("LatencyMs = %.2f", mCorrelation.mEstimatedLatencyMs)
   2005                             + endline);
   2006                 } else {
   2007                     sb.append(String.format("LatencyMs = unknown") + endline);
   2008                 }
   2009 
   2010                 sb.append(String.format("LatencyConfidence = %.2f",
   2011                         mCorrelation.mEstimatedLatencyConfidence) + endline);
   2012 
   2013                 sb.append(String.format("Average = %.4f", mCorrelation.mAverage) + endline);
   2014                 sb.append(String.format("RMS = %.4f", mCorrelation.mRms) + endline);
   2015                 break;
   2016             case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
   2017                 sb.append("Buffer Test Duration (s) = " + mBufferTestDurationInSeconds + endline);
   2018 
   2019                 // report recorder results
   2020                 int[] recorderBufferData = null;
   2021                 int recorderBufferDataMax = 0;
   2022                 double recorderBufferDataStdDev = 0.0;
   2023                 switch (mAudioThreadType) {
   2024                     case Constant.AUDIO_THREAD_TYPE_JAVA:
   2025                         recorderBufferData = mRecorderBufferPeriod.getBufferPeriodArray();
   2026                         recorderBufferDataMax = mRecorderBufferPeriod.getMaxBufferPeriod();
   2027                         recorderBufferDataStdDev = mRecorderBufferPeriod.getStdDevBufferPeriod();
   2028                         break;
   2029                     case Constant.AUDIO_THREAD_TYPE_NATIVE:
   2030                         recorderBufferData = mNativeRecorderBufferPeriodArray;
   2031                         recorderBufferDataMax = mNativeRecorderMaxBufferPeriod;
   2032                         recorderBufferDataStdDev = mNativeRecorderStdDevBufferPeriod;
   2033                         break;
   2034                 }
   2035                 // report expected recorder buffer period
   2036                 if (recorderBufferData != null) {
   2037                     // this is the range of data that actually has values
   2038                     int usefulDataRange = Math.min(recorderBufferDataMax + 1,
   2039                             recorderBufferData.length);
   2040                     int[] usefulBufferData = Arrays.copyOfRange(recorderBufferData, 0,
   2041                             usefulDataRange);
   2042                     PerformanceMeasurement measurement = new PerformanceMeasurement(
   2043                             mRecorderCallbackTimes.getExpectedBufferPeriod(), usefulBufferData);
   2044                     float recorderPercentAtExpected =
   2045                             measurement.percentBufferPeriodsAtExpected();
   2046                     double benchmark = measurement.computeWeightedBenchmark();
   2047                     int outliers = measurement.countOutliers();
   2048                     sb.append("Expected Recorder Buffer Period (ms) = " +
   2049                             mRecorderCallbackTimes.getExpectedBufferPeriod() + endline);
   2050                     sb.append("Recorder Buffer Periods At Expected = " +
   2051                             String.format("%.5f%%", recorderPercentAtExpected * 100) + endline);
   2052 
   2053                     sb.append("Recorder Buffer Period Std Dev = "
   2054                             + String.format(Locale.US, "%.5f ms", recorderBufferDataStdDev)
   2055                             + endline);
   2056 
   2057                     // output thousandths of a percent not at expected buffer period
   2058                     sb.append("kth% Late Recorder Buffer Callbacks = "
   2059                             + String.format("%.5f", (1 - recorderPercentAtExpected) * 100000)
   2060                             + endline);
   2061                     sb.append("Recorder Benchmark = " + benchmark + endline);
   2062                     sb.append("Recorder Number of Outliers = " + outliers + endline);
   2063                 } else {
   2064                     sb.append("Cannot Find Recorder Buffer Period Data!" + endline);
   2065                 }
   2066 
   2067                 // report player results
   2068                 int[] playerBufferData = null;
   2069                 int playerBufferDataMax = 0;
   2070                 double playerBufferDataStdDev = 0.0;
   2071                 switch (mAudioThreadType) {
   2072                     case Constant.AUDIO_THREAD_TYPE_JAVA:
   2073                         playerBufferData = mPlayerBufferPeriod.getBufferPeriodArray();
   2074                         playerBufferDataMax = mPlayerBufferPeriod.getMaxBufferPeriod();
   2075                         playerBufferDataStdDev = mPlayerBufferPeriod.getStdDevBufferPeriod();
   2076                         break;
   2077                     case Constant.AUDIO_THREAD_TYPE_NATIVE:
   2078                         playerBufferData = mNativePlayerBufferPeriodArray;
   2079                         playerBufferDataMax = mNativePlayerMaxBufferPeriod;
   2080                         playerBufferDataStdDev = mNativePlayerStdDevBufferPeriod;
   2081                         break;
   2082                 }
   2083                 // report expected player buffer period
   2084                 sb.append("Expected Player Buffer Period (ms) = " +
   2085                         mPlayerCallbackTimes.getExpectedBufferPeriod() + endline);
   2086                 if (playerBufferData != null) {
   2087                     // this is the range of data that actually has values
   2088                     int usefulDataRange = Math.min(playerBufferDataMax + 1,
   2089                             playerBufferData.length);
   2090                     int[] usefulBufferData = Arrays.copyOfRange(playerBufferData, 0,
   2091                             usefulDataRange);
   2092                     PerformanceMeasurement measurement = new PerformanceMeasurement(
   2093                             mPlayerCallbackTimes.getExpectedBufferPeriod(), usefulBufferData);
   2094                     float playerPercentAtExpected = measurement.percentBufferPeriodsAtExpected();
   2095                     double benchmark = measurement.computeWeightedBenchmark();
   2096                     int outliers = measurement.countOutliers();
   2097                     sb.append("Player Buffer Periods At Expected = "
   2098                             + String.format("%.5f%%", playerPercentAtExpected * 100) + endline);
   2099 
   2100                     sb.append("Player Buffer Period Std Dev = "
   2101                             + String.format(Locale.US, "%.5f ms", playerBufferDataStdDev)
   2102                             + endline);
   2103 
   2104                     // output thousandths of a percent not at expected buffer period
   2105                     sb.append("kth% Late Player Buffer Callbacks = "
   2106                             + String.format("%.5f", (1 - playerPercentAtExpected) * 100000)
   2107                             + endline);
   2108                     sb.append("Player Benchmark = " + benchmark + endline);
   2109                     sb.append("Player Number of Outliers = " + outliers + endline);
   2110 
   2111                 } else {
   2112                     sb.append("Cannot Find Player Buffer Period Data!" + endline);
   2113                 }
   2114                 // report glitches per hour
   2115                 int numberOfGlitches = estimateNumberOfGlitches(mGlitchesData);
   2116                 float testDurationInHours = mBufferTestElapsedSeconds
   2117                         / (float) Constant.SECONDS_PER_HOUR;
   2118 
   2119                 // Report Glitches Per Hour if sufficient data available, ie at least half an hour
   2120                 if (testDurationInHours >= .5) {
   2121                     int glitchesPerHour = (int) Math.ceil(numberOfGlitches/testDurationInHours);
   2122                     sb.append("Glitches Per Hour = " + glitchesPerHour + endline);
   2123                 }
   2124                 sb.append("Total Number of Glitches = " + numberOfGlitches + endline);
   2125 
   2126                 // report if the total glitching interval is too long
   2127                 sb.append("Total glitching interval too long =  " +
   2128                         mGlitchingIntervalTooLong);
   2129 
   2130                 sb.append("\nLate Player Callbacks = ");
   2131                 sb.append(mPlayerCallbackTimes.getNumLateOrEarlyCallbacks());
   2132                 sb.append("\nLate Player Callbacks Exceeded Capacity = ");
   2133                 sb.append(mPlayerCallbackTimes.isCapacityExceeded());
   2134                 sb.append("\nLate Recorder Callbacks = ");
   2135                 sb.append(mRecorderCallbackTimes.getNumLateOrEarlyCallbacks());
   2136                 sb.append("\nLate Recorder Callbacks Exceeded Capacity = ");
   2137                 sb.append(mRecorderCallbackTimes.isCapacityExceeded());
   2138                 sb.append("\n");
   2139         }
   2140 
   2141 
   2142         String info = getApp().getSystemInfo();
   2143         sb.append("SystemInfo = " + info + endline);
   2144 
   2145         return sb;
   2146     }
   2147 
   2148     /** Save a .txt file of of glitch occurrences in ms from beginning of test. */
   2149     private void saveGlitchOccurrences(Uri uri, int[] glitchesData) {
   2150         ParcelFileDescriptor parcelFileDescriptor = null;
   2151         FileOutputStream outputStream;
   2152         try {
   2153             parcelFileDescriptor = getApplicationContext().getContentResolver().
   2154                     openFileDescriptor(uri, "w");
   2155 
   2156             FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
   2157             outputStream = new FileOutputStream(fileDescriptor);
   2158 
   2159             log("Done creating output stream");
   2160 
   2161             outputStream.write(GlitchesStringBuilder.getGlitchStringForFile(mFFTSamplingSize,
   2162                     mFFTOverlapSamples, glitchesData, mSamplingRate).getBytes());
   2163         } catch (Exception e) {
   2164             log("Failed to open text file " + e);
   2165         } finally {
   2166             try {
   2167                 if (parcelFileDescriptor != null) {
   2168                     parcelFileDescriptor.close();
   2169                 }
   2170             } catch (Exception e) {
   2171                 e.printStackTrace();
   2172                 log("Error closing ParcelFile Descriptor");
   2173             }
   2174         }
   2175     }
   2176 
   2177     /**
   2178      * Estimate the number of glitches. This version of estimation will count two consecutive
   2179      * glitching intervals as one glitch. This is because two time intervals are partly overlapped.
   2180      * Note: If the total glitching intervals exceed the length of glitchesData, this estimation
   2181      * becomes incomplete. However, whether or not the total glitching interval is too long will
   2182      * also be indicated, and in the case it's true, we know something went wrong.
   2183      */
   2184     private static int estimateNumberOfGlitches(int[] glitchesData) {
   2185         final int discard = 10; // don't count glitches occurring at the first few FFT interval
   2186         boolean isPreviousGlitch = false; // is there a glitch in previous interval or not
   2187         int previousFFTInterval = -1;
   2188         int count = 0;
   2189         // if there are three consecutive glitches, the first two will be counted as one,
   2190         // the third will be counted as another one
   2191         for (int i = 0; i < glitchesData.length; i++) {
   2192             if (glitchesData[i] > discard) {
   2193                 if (glitchesData[i] == previousFFTInterval + 1 && isPreviousGlitch) {
   2194                     isPreviousGlitch = false;
   2195                     previousFFTInterval = glitchesData[i];
   2196                 } else {
   2197                     isPreviousGlitch = true;
   2198                     previousFFTInterval = glitchesData[i];
   2199                     count += 1;
   2200                 }
   2201             }
   2202 
   2203         }
   2204 
   2205         return count;
   2206     }
   2207 
   2208 
   2209     /**
   2210      * Estimate the number of glitches. This version of estimation will count the whole consecutive
   2211      * intervals as one glitch. This version is not currently used.
   2212      * Note: If the total glitching intervals exceed the length of glitchesData, this estimation
   2213      * becomes incomplete. However, whether or not the total glitching interval is too long will
   2214      * also be indicated, and in the case it's true, we know something went wrong.
   2215      */
   2216     private static int estimateNumberOfGlitches2(int[] glitchesData) {
   2217         final int discard = 10; // don't count glitches occurring at the first few FFT interval
   2218         int previousFFTInterval = -1;
   2219         int count = 0;
   2220         for (int i = 0; i < glitchesData.length; i++) {
   2221             if (glitchesData[i] > discard) {
   2222                 if (glitchesData[i] != previousFFTInterval + 1) {
   2223                     count += 1;
   2224                 }
   2225                 previousFFTInterval = glitchesData[i];
   2226             }
   2227         }
   2228         return count;
   2229     }
   2230 
   2231     /**
   2232      * Check whether we have the RECORD_AUDIO permission
   2233      * @return true if we do
   2234      */
   2235     private boolean hasRecordAudioPermission(){
   2236         boolean hasPermission = (ContextCompat.checkSelfPermission(this,
   2237                 Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED);
   2238 
   2239         log("Has RECORD_AUDIO permission? " + hasPermission);
   2240         return hasPermission;
   2241     }
   2242 
   2243     /**
   2244      * Requests the RECORD_AUDIO permission from the user
   2245      */
   2246     private void requestRecordAudioPermission(int requestCode){
   2247 
   2248         String requiredPermission = Manifest.permission.RECORD_AUDIO;
   2249 
   2250         // If the user previously denied this permission then show a message explaining why
   2251         // this permission is needed
   2252         if (ActivityCompat.shouldShowRequestPermissionRationale(this,
   2253                 requiredPermission)) {
   2254 
   2255             showToast("This app needs to record audio through the microphone to test the device's "+
   2256                     "performance");
   2257         }
   2258 
   2259         // request the permission.
   2260         ActivityCompat.requestPermissions(this, new String[]{requiredPermission}, requestCode);
   2261     }
   2262 
   2263     @Override
   2264     public void onRequestPermissionsResult(int requestCode,
   2265                                            String permissions[], int[] grantResults) {
   2266 
   2267         // Save all files or run requested test after being granted permissions
   2268         if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
   2269             if (requestCode == PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE_RESULTS ) {
   2270                 saveAllTo(getFileNamePrefix());
   2271             } else if (requestCode == PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE_SCRIPT ) {
   2272                 AtraceScriptsWriter.writeScriptsToFile(this);
   2273             } else if (requestCode == PERMISSIONS_REQUEST_RECORD_AUDIO_BUFFER) {
   2274                 startBufferTest();
   2275             } else if (requestCode == PERMISSIONS_REQUEST_RECORD_AUDIO_LATENCY) {
   2276                 startLatencyTest();
   2277             }
   2278         }
   2279     }
   2280 
   2281     /**
   2282      * Check whether we have the WRITE_EXTERNAL_STORAGE permission
   2283      *
   2284      * @return true if we do
   2285      */
   2286     private boolean hasWriteFilePermission() {
   2287         boolean hasPermission = (ContextCompat.checkSelfPermission(this,
   2288                 Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
   2289 
   2290         log("Has WRITE_EXTERNAL_STORAGE? " + hasPermission);
   2291         return hasPermission;
   2292     }
   2293 
   2294     /**
   2295      * Requests the WRITE_EXTERNAL_STORAGE permission from the user
   2296      */
   2297     private void requestWriteFilePermission(int requestCode) {
   2298 
   2299         String requiredPermission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
   2300 
   2301         // request the permission.
   2302         ActivityCompat.requestPermissions(this, new String[]{requiredPermission}, requestCode);
   2303     }
   2304 
   2305     /**
   2306      * Receive results from save files DialogAlert and either save all files directly
   2307      * or use filename dialog
   2308      */
   2309     @Override
   2310     public void onSaveDialogSelect(DialogFragment dialog, boolean saveWithoutDialog) {
   2311         if (saveWithoutDialog) {
   2312             saveAllTo("loopback_" + mTestStartTimeString);
   2313         } else {
   2314             SaveFilesWithDialog();
   2315         }
   2316     }
   2317 
   2318     private void restoreInstanceState(Bundle in) {
   2319         mWaveData = in.getDoubleArray("mWaveData");
   2320 
   2321         mTestType = in.getInt("mTestType");
   2322         mMicSource = in.getInt("mMicSource");
   2323         mAudioThreadType = in.getInt("mAudioThreadType");
   2324         mSamplingRate = in.getInt("mSamplingRate");
   2325         mChannelIndex = in.getInt("mChannelIndex");
   2326         mSoundLevel = in.getInt("mSoundLevel");
   2327         mPlayerBufferSizeInBytes = in.getInt("mPlayerBufferSizeInBytes");
   2328         mRecorderBufferSizeInBytes = in.getInt("mRecorderBufferSizeInBytes");
   2329 
   2330         mTestStartTimeString = in.getString("mTestStartTimeString");
   2331 
   2332         mGlitchesData = in.getIntArray("mGlitchesData");
   2333         if(mGlitchesData != null) {
   2334             mGlitchingIntervalTooLong = in.getBoolean("mGlitchingIntervalTooLong");
   2335             mFFTSamplingSize = in.getInt("mFFTSamplingSize");
   2336             mFFTOverlapSamples = in.getInt("mFFTOverlapSamples");
   2337             mBufferTestStartTime = in.getLong("mBufferTestStartTime");
   2338             mBufferTestElapsedSeconds = in.getInt("mBufferTestElapsedSeconds");
   2339             mBufferTestDurationInSeconds = in.getInt("mBufferTestDurationInSeconds");
   2340             mBufferTestWavePlotDurationInSeconds =
   2341                     in.getInt("mBufferTestWavePlotDurationInSeconds");
   2342 
   2343             findViewById(R.id.glitchReportPanel).setVisibility(View.VISIBLE);
   2344         }
   2345 
   2346         if(mWaveData != null) {
   2347             mCorrelation = in.getParcelable("mCorrelation");
   2348             mPlayerBufferPeriod = in.getParcelable("mPlayerBufferPeriod");
   2349             mRecorderBufferPeriod = in.getParcelable("mRecorderBufferPeriod");
   2350             mPlayerCallbackTimes = in.getParcelable("mPlayerCallbackTimes");
   2351             mRecorderCallbackTimes = in.getParcelable("mRecorderCallbackTimes");
   2352 
   2353             mNativePlayerBufferPeriodArray = in.getIntArray("mNativePlayerBufferPeriodArray");
   2354             mNativePlayerMaxBufferPeriod = in.getInt("mNativePlayerMaxBufferPeriod");
   2355             mNativeRecorderBufferPeriodArray = in.getIntArray("mNativeRecorderBufferPeriodArray");
   2356             mNativeRecorderMaxBufferPeriod = in.getInt("mNativeRecorderMaxBufferPeriod");
   2357 
   2358             mWavePlotView.setData(mWaveData, mSamplingRate);
   2359             refreshState();
   2360             findViewById(R.id.zoomAndSaveControlPanel).setVisibility(View.VISIBLE);
   2361             findViewById(R.id.resultSummary).setVisibility(View.VISIBLE);
   2362         }
   2363     }
   2364 
   2365     @Override
   2366     protected void onSaveInstanceState(Bundle out) {
   2367         super.onSaveInstanceState(out);
   2368         // TODO: keep larger pieces of data in a fragment to speed up response to rotation
   2369         out.putDoubleArray("mWaveData", mWaveData);
   2370 
   2371         out.putInt("mTestType", mTestType);
   2372         out.putInt("mMicSource", mMicSource);
   2373         out.putInt("mAudioThreadType", mAudioThreadType);
   2374         out.putInt("mSamplingRate", mSamplingRate);
   2375         out.putInt("mChannelIndex", mChannelIndex);
   2376         out.putInt("mSoundLevel", mSoundLevel);
   2377         out.putInt("mPlayerBufferSizeInBytes", mPlayerBufferSizeInBytes);
   2378         out.putInt("mRecorderBufferSizeInBytes", mRecorderBufferSizeInBytes);
   2379         out.putString("mTestStartTimeString", mTestStartTimeString);
   2380 
   2381         out.putParcelable("mCorrelation", mCorrelation);
   2382         out.putParcelable("mPlayerBufferPeriod", mPlayerBufferPeriod);
   2383         out.putParcelable("mRecorderBufferPeriod", mRecorderBufferPeriod);
   2384         out.putParcelable("mPlayerCallbackTimes", mPlayerCallbackTimes);
   2385         out.putParcelable("mRecorderCallbackTimes", mRecorderCallbackTimes);
   2386 
   2387         out.putIntArray("mNativePlayerBufferPeriodArray", mNativePlayerBufferPeriodArray);
   2388         out.putInt("mNativePlayerMaxBufferPeriod", mNativePlayerMaxBufferPeriod);
   2389         out.putIntArray("mNativeRecorderBufferPeriodArray", mNativeRecorderBufferPeriodArray);
   2390         out.putInt("mNativeRecorderMaxBufferPeriod", mNativeRecorderMaxBufferPeriod);
   2391 
   2392         // buffer test values
   2393         out.putIntArray("mGlitchesData", mGlitchesData);
   2394         out.putBoolean("mGlitchingIntervalTooLong", mGlitchingIntervalTooLong);
   2395         out.putInt("mFFTSamplingSize", mFFTSamplingSize);
   2396         out.putInt("mFFTOverlapSamples", mFFTOverlapSamples);
   2397         out.putLong("mBufferTestStartTime", mBufferTestStartTime);
   2398         out.putInt("mBufferTestElapsedSeconds", mBufferTestElapsedSeconds);
   2399         out.putInt("mBufferTestDurationInSeconds", mBufferTestDurationInSeconds);
   2400         out.putInt("mBufferTestWavePlotDurationInSeconds", mBufferTestWavePlotDurationInSeconds);
   2401     }
   2402 }
   2403