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
17package org.drrickorang.loopback;
18
19import android.Manifest;
20import android.app.Activity;
21import android.app.DialogFragment;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.ServiceConnection;
26import android.content.pm.PackageManager;
27import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.graphics.Canvas;
30import android.media.AudioManager;
31import android.net.Uri;
32import android.os.Build;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.IBinder;
36import android.os.Message;
37import android.os.ParcelFileDescriptor;
38import android.provider.MediaStore;
39import android.support.v4.app.ActivityCompat;
40import android.support.v4.content.ContextCompat;
41import android.text.format.DateFormat;
42import android.util.Log;
43import android.view.Gravity;
44import android.view.Menu;
45import android.view.MenuInflater;
46import android.view.MenuItem;
47import android.view.View;
48import android.view.ViewGroup;
49import android.widget.Button;
50import android.widget.LinearLayout;
51import android.widget.PopupWindow;
52import android.widget.SeekBar;
53import android.widget.TextView;
54import android.widget.Toast;
55
56import java.io.File;
57import java.io.FileDescriptor;
58import java.io.FileOutputStream;
59import java.util.Arrays;
60import 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
69public 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