<NME> AudioFragment.java
<BEF> /*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.chromium.latency.walt;

import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;

import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.Description;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import static org.chromium.latency.walt.Utils.getIntPreference;

/**
 * A simple {@link Fragment} subclass.
 */
public class AudioFragment extends Fragment implements View.OnClickListener,
        BaseTest.TestStateListener {

    enum AudioTestType {
        CONTINUOUS_PLAYBACK,
        CONTINUOUS_RECORDING,
        COLD_PLAYBACK,
        COLD_RECORDING,
        DISPLAY_WAVEFORM
    }

    private SimpleLogger logger;
    private TextView textView;
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_audio, container, false);
        textView = (TextView) view.findViewById(R.id.txt_box_audio);
        startPlaybackButton = view.findViewById(R.id.button_start_audio_play);
        startRecordingButton = view.findViewById(R.id.button_start_audio_rec);


    private static final int PERMISSION_REQUEST_RECORD_AUDIO = 1;

    public AudioFragment() {
        // Required empty public constructor
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        logger = SimpleLogger.getInstance(getContext());

        audioTest = new AudioTest(getActivity());
        audioTest.setTestStateListener(this);

        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_audio, container, false);
        textView = (TextView) view.findViewById(R.id.txt_box_audio);
        textView.setMovementMethod(new ScrollingMovementMethod());
        startButton = view.findViewById(R.id.button_start_audio);
        stopButton = view.findViewById(R.id.button_stop_audio);
        chartLayout = view.findViewById(R.id.chart_layout);
        chart = (LineChart) view.findViewById(R.id.chart);
        latencyChart = (HistogramChart) view.findViewById(R.id.latency_chart);

        view.findViewById(R.id.button_close_chart).setOnClickListener(this);
        enableButtons();

        // Configure the audio mode spinner
        modeSpinner = (Spinner) view.findViewById(R.id.spinner_audio_mode);
        ArrayAdapter<CharSequence> modeAdapter = ArrayAdapter.createFromResource(getContext(),
                R.array.audio_mode_array, android.R.layout.simple_spinner_item);
        modeAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item);
        modeSpinner.setAdapter(modeAdapter);

        return view;
    }

    @Override
    public void onResume() {
        super.onResume();

        // Register this fragment class as the listener for some button clicks
        startButton.setOnClickListener(this);
        stopButton.setOnClickListener(this);

        textView.setText(logger.getLogText());
        logger.registerReceiver(logReceiver);
    }

    @Override
    public void onPause() {
        logger.unregisterReceiver(logReceiver);
        super.onPause();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        audioTest.teardown();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button_start_audio:
                chartLayout.setVisibility(View.GONE);
                disableButtons();
                AudioTestType testType = getSelectedTestType();
                switch (testType) {
                    case CONTINUOUS_PLAYBACK:
                    case CONTINUOUS_RECORDING:
                    case DISPLAY_WAVEFORM:
                        audioTest.setAudioMode(AudioTest.AudioMode.CONTINUOUS);
                        audioTest.setPeriod(AudioTest.CONTINUOUS_TEST_PERIOD);
                        break;
                    case COLD_PLAYBACK:
                    case COLD_RECORDING:
                        audioTest.setAudioMode(AudioTest.AudioMode.CONTINUOUS);
                        audioTest.setPeriod(AudioTest.COLD_TEST_PERIOD);
                        break;
                }
                if (testType == AudioTestType.DISPLAY_WAVEFORM) {
                    // Only need to record 1 beep to display wave
                    audioTest.setRecordingRepetitions(1);
                } else {
                    audioTest.setRecordingRepetitions(
                            getIntPreference(getContext(), R.string.preference_audio_in_reps, 5));
                }
                if (testType == AudioTestType.CONTINUOUS_PLAYBACK ||
                        testType == AudioTestType.COLD_PLAYBACK ||
                        testType == AudioTestType.CONTINUOUS_RECORDING ||
                        testType == AudioTestType.COLD_RECORDING) {
                    latencyChart.setVisibility(View.VISIBLE);
                    latencyChart.clearData();
                    latencyChart.setLegendEnabled(false);
                    final String description =
                            getResources().getStringArray(R.array.audio_mode_array)[
                                    modeSpinner.getSelectedItemPosition()] + " [ms]";
                    latencyChart.setDescription(description);
                }
                switch (testType) {
                    case CONTINUOUS_RECORDING:
                    case COLD_RECORDING:
                    case DISPLAY_WAVEFORM:
                        attemptRecordingTest();
                        break;
                    case CONTINUOUS_PLAYBACK:
                    case COLD_PLAYBACK:
                        // Set media volume to max
                        AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
                        am.setStreamVolume(AudioManager.STREAM_MUSIC, am.getStreamMaxVolume(AudioManager.STREAM_MUSIC), 0);
                        audioTest.beginPlaybackMeasurement();
                        break;
                }
                break;
            case R.id.button_stop_audio:
                audioTest.stopTest();
                break;
            case R.id.button_close_chart:
                chartLayout.setVisibility(View.GONE);
                break;
        }
    }

    private AudioTestType getSelectedTestType() {
        return AudioTestType.values()[modeSpinner.getSelectedItemPosition()];
    }

    private BroadcastReceiver logReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String msg = intent.getStringExtra("message");
            textView.append(msg + "\n");
        }
    };

    private void attemptRecordingTest() {
        // first see if we already have permission to record audio
        int currentPermission = ContextCompat.checkSelfPermission(this.getContext(),
                Manifest.permission.RECORD_AUDIO);
        if (currentPermission == PackageManager.PERMISSION_GRANTED) {
            disableButtons();
            audioTest.beginRecordingMeasurement();
        } else {
            requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO},
                    PERMISSION_REQUEST_RECORD_AUDIO);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_RECORD_AUDIO:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    disableButtons();
                    audioTest.beginRecordingMeasurement();
                } else {
                    logger.log("Could not get permission to record audio");
                }
                return;
        }
    }

    @Override
    public void onTestStopped() {
        if (getSelectedTestType() == AudioTestType.DISPLAY_WAVEFORM) {
            drawWaveformChart();
        } else {
            if (!audioTest.deltas_mic.isEmpty()) {
                latencyChart.setLegendEnabled(true);
                latencyChart.setLabel(String.format(Locale.US, "Median=%.1f ms", Utils.median(audioTest.deltas_mic)));
            } else if (!audioTest.deltas_queue2wire.isEmpty()) {
                latencyChart.setLegendEnabled(true);
                latencyChart.setLabel(String.format(Locale.US, "Median=%.1f ms", Utils.median(audioTest.deltas_queue2wire)));
            }
        }
        LogUploader.uploadIfAutoEnabled(getContext());
        enableButtons();
    }

    @Override
    public void onTestStoppedWithError() {
        enableButtons();
        latencyChart.setVisibility(View.GONE);
    }

    @Override
    public void onTestPartialResult(double value) {
        latencyChart.addEntry(value);
    }

    private void drawWaveformChart() {
        final short[] wave = AudioTest.getRecordedWave();
        List<Entry> entries = new ArrayList<>();
        int frameRate = audioTest.getOptimalFrameRate();
        for (int i = 0; i < wave.length; i++) {
            float timeStamp = (float) i / frameRate * 1000f;
            entries.add(new Entry(timeStamp, (float) wave[i]));
        }
        LineDataSet dataSet = new LineDataSet(entries, "Waveform");
        dataSet.setColor(Color.BLACK);
        dataSet.setValueTextColor(Color.BLACK);
        dataSet.setCircleColor(ContextCompat.getColor(getContext(), R.color.DarkGreen));
        dataSet.setCircleRadius(1.5f);
        dataSet.setCircleColorHole(Color.DKGRAY);
        LineData lineData = new LineData(dataSet);
        chart.setData(lineData);

        LimitLine line = new LimitLine(audioTest.getThreshold(), "Threshold");
        line.setLineColor(Color.RED);
        line.setLabelPosition(LimitLine.LimitLabelPosition.LEFT_TOP);
        line.setLineWidth(2f);
        line.setTextColor(Color.DKGRAY);
        line.setTextSize(10f);
        chart.getAxisLeft().addLimitLine(line);

        final Description desc = new Description();
        desc.setText("Wave [digital level -32768 to +32767] vs. Time [ms]");
        desc.setTextSize(12f);
        chart.setDescription(desc);
        chart.getLegend().setEnabled(false);
        chart.invalidate();
        chartLayout.setVisibility(View.VISIBLE);
    }

    private void disableButtons() {
        startButton.setEnabled(false);
        stopButton.setEnabled(true);
    }

    private void enableButtons() {
        startButton.setEnabled(true);
        stopButton.setEnabled(false);
    }
}

<MSG> Allow scrolling the log in audio test

Fixes #54

<DFF> @@ -25,6 +25,7 @@ import android.media.AudioManager;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.content.ContextCompat;
+import android.text.method.ScrollingMovementMethod;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -65,6 +66,7 @@ public class AudioFragment extends Fragment implements View.OnClickListener,
         // Inflate the layout for this fragment
         View view = inflater.inflate(R.layout.fragment_audio, container, false);
         textView = (TextView) view.findViewById(R.id.txt_box_audio);
+        textView.setMovementMethod(new ScrollingMovementMethod());
         startPlaybackButton = view.findViewById(R.id.button_start_audio_play);
         startRecordingButton = view.findViewById(R.id.button_start_audio_rec);
 

Allow scrolling the log in audio test