1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.chromium.latency.walt; 18 19 import android.Manifest; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.graphics.Color; 25 import android.media.AudioManager; 26 import android.os.Bundle; 27 import android.support.v4.app.Fragment; 28 import android.support.v4.content.ContextCompat; 29 import android.text.method.ScrollingMovementMethod; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.ArrayAdapter; 34 import android.widget.Spinner; 35 import android.widget.TextView; 36 37 import com.github.mikephil.charting.charts.LineChart; 38 import com.github.mikephil.charting.components.Description; 39 import com.github.mikephil.charting.components.LimitLine; 40 import com.github.mikephil.charting.data.Entry; 41 import com.github.mikephil.charting.data.LineData; 42 import com.github.mikephil.charting.data.LineDataSet; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Locale; 47 48 import static org.chromium.latency.walt.Utils.getIntPreference; 49 50 /** 51 * A simple {@link Fragment} subclass. 52 */ 53 public class AudioFragment extends Fragment implements View.OnClickListener, 54 BaseTest.TestStateListener { 55 56 enum AudioTestType { 57 CONTINUOUS_PLAYBACK, 58 CONTINUOUS_RECORDING, 59 COLD_PLAYBACK, 60 COLD_RECORDING, 61 DISPLAY_WAVEFORM 62 } 63 64 private SimpleLogger logger; 65 private TextView textView; 66 private AudioTest audioTest; 67 private View startButton; 68 private View stopButton; 69 private Spinner modeSpinner; 70 private LineChart chart; 71 private HistogramChart latencyChart; 72 private View chartLayout; 73 74 private static final int PERMISSION_REQUEST_RECORD_AUDIO = 1; 75 AudioFragment()76 public AudioFragment() { 77 // Required empty public constructor 78 } 79 80 81 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)82 public View onCreateView(LayoutInflater inflater, ViewGroup container, 83 Bundle savedInstanceState) { 84 85 logger = SimpleLogger.getInstance(getContext()); 86 87 audioTest = new AudioTest(getActivity()); 88 audioTest.setTestStateListener(this); 89 90 // Inflate the layout for this fragment 91 View view = inflater.inflate(R.layout.fragment_audio, container, false); 92 textView = (TextView) view.findViewById(R.id.txt_box_audio); 93 textView.setMovementMethod(new ScrollingMovementMethod()); 94 startButton = view.findViewById(R.id.button_start_audio); 95 stopButton = view.findViewById(R.id.button_stop_audio); 96 chartLayout = view.findViewById(R.id.chart_layout); 97 chart = (LineChart) view.findViewById(R.id.chart); 98 latencyChart = (HistogramChart) view.findViewById(R.id.latency_chart); 99 100 view.findViewById(R.id.button_close_chart).setOnClickListener(this); 101 enableButtons(); 102 103 // Configure the audio mode spinner 104 modeSpinner = (Spinner) view.findViewById(R.id.spinner_audio_mode); 105 ArrayAdapter<CharSequence> modeAdapter = ArrayAdapter.createFromResource(getContext(), 106 R.array.audio_mode_array, android.R.layout.simple_spinner_item); 107 modeAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item); 108 modeSpinner.setAdapter(modeAdapter); 109 110 return view; 111 } 112 113 @Override onResume()114 public void onResume() { 115 super.onResume(); 116 117 // Register this fragment class as the listener for some button clicks 118 startButton.setOnClickListener(this); 119 stopButton.setOnClickListener(this); 120 121 textView.setText(logger.getLogText()); 122 logger.registerReceiver(logReceiver); 123 } 124 125 @Override onPause()126 public void onPause() { 127 logger.unregisterReceiver(logReceiver); 128 super.onPause(); 129 } 130 131 @Override onDestroy()132 public void onDestroy() { 133 super.onDestroy(); 134 audioTest.teardown(); 135 } 136 137 @Override onClick(View v)138 public void onClick(View v) { 139 switch (v.getId()) { 140 case R.id.button_start_audio: 141 chartLayout.setVisibility(View.GONE); 142 disableButtons(); 143 AudioTestType testType = getSelectedTestType(); 144 switch (testType) { 145 case CONTINUOUS_PLAYBACK: 146 case CONTINUOUS_RECORDING: 147 case DISPLAY_WAVEFORM: 148 audioTest.setAudioMode(AudioTest.AudioMode.CONTINUOUS); 149 audioTest.setPeriod(AudioTest.CONTINUOUS_TEST_PERIOD); 150 break; 151 case COLD_PLAYBACK: 152 case COLD_RECORDING: 153 audioTest.setAudioMode(AudioTest.AudioMode.CONTINUOUS); 154 audioTest.setPeriod(AudioTest.COLD_TEST_PERIOD); 155 break; 156 } 157 if (testType == AudioTestType.DISPLAY_WAVEFORM) { 158 // Only need to record 1 beep to display wave 159 audioTest.setRecordingRepetitions(1); 160 } else { 161 audioTest.setRecordingRepetitions( 162 getIntPreference(getContext(), R.string.preference_audio_in_reps, 5)); 163 } 164 if (testType == AudioTestType.CONTINUOUS_PLAYBACK || 165 testType == AudioTestType.COLD_PLAYBACK || 166 testType == AudioTestType.CONTINUOUS_RECORDING || 167 testType == AudioTestType.COLD_RECORDING) { 168 latencyChart.setVisibility(View.VISIBLE); 169 latencyChart.clearData(); 170 latencyChart.setLegendEnabled(false); 171 final String description = 172 getResources().getStringArray(R.array.audio_mode_array)[ 173 modeSpinner.getSelectedItemPosition()] + " [ms]"; 174 latencyChart.setDescription(description); 175 } 176 switch (testType) { 177 case CONTINUOUS_RECORDING: 178 case COLD_RECORDING: 179 case DISPLAY_WAVEFORM: 180 attemptRecordingTest(); 181 break; 182 case CONTINUOUS_PLAYBACK: 183 case COLD_PLAYBACK: 184 // Set media volume to max 185 AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); 186 am.setStreamVolume(AudioManager.STREAM_MUSIC, am.getStreamMaxVolume(AudioManager.STREAM_MUSIC), 0); 187 audioTest.beginPlaybackMeasurement(); 188 break; 189 } 190 break; 191 case R.id.button_stop_audio: 192 audioTest.stopTest(); 193 break; 194 case R.id.button_close_chart: 195 chartLayout.setVisibility(View.GONE); 196 break; 197 } 198 } 199 getSelectedTestType()200 private AudioTestType getSelectedTestType() { 201 return AudioTestType.values()[modeSpinner.getSelectedItemPosition()]; 202 } 203 204 private BroadcastReceiver logReceiver = new BroadcastReceiver() { 205 @Override 206 public void onReceive(Context context, Intent intent) { 207 String msg = intent.getStringExtra("message"); 208 textView.append(msg + "\n"); 209 } 210 }; 211 attemptRecordingTest()212 private void attemptRecordingTest() { 213 // first see if we already have permission to record audio 214 int currentPermission = ContextCompat.checkSelfPermission(this.getContext(), 215 Manifest.permission.RECORD_AUDIO); 216 if (currentPermission == PackageManager.PERMISSION_GRANTED) { 217 disableButtons(); 218 audioTest.beginRecordingMeasurement(); 219 } else { 220 requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, 221 PERMISSION_REQUEST_RECORD_AUDIO); 222 } 223 } 224 225 @Override onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)226 public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 227 switch (requestCode) { 228 case PERMISSION_REQUEST_RECORD_AUDIO: 229 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 230 disableButtons(); 231 audioTest.beginRecordingMeasurement(); 232 } else { 233 logger.log("Could not get permission to record audio"); 234 } 235 return; 236 } 237 } 238 239 @Override onTestStopped()240 public void onTestStopped() { 241 if (getSelectedTestType() == AudioTestType.DISPLAY_WAVEFORM) { 242 drawWaveformChart(); 243 } else { 244 if (!audioTest.deltas_mic.isEmpty()) { 245 latencyChart.setLegendEnabled(true); 246 latencyChart.setLabel(String.format(Locale.US, "Median=%.1f ms", Utils.median(audioTest.deltas_mic))); 247 } else if (!audioTest.deltas_queue2wire.isEmpty()) { 248 latencyChart.setLegendEnabled(true); 249 latencyChart.setLabel(String.format(Locale.US, "Median=%.1f ms", Utils.median(audioTest.deltas_queue2wire))); 250 } 251 } 252 LogUploader.uploadIfAutoEnabled(getContext()); 253 enableButtons(); 254 } 255 256 @Override onTestStoppedWithError()257 public void onTestStoppedWithError() { 258 enableButtons(); 259 latencyChart.setVisibility(View.GONE); 260 } 261 262 @Override onTestPartialResult(double value)263 public void onTestPartialResult(double value) { 264 latencyChart.addEntry(value); 265 } 266 drawWaveformChart()267 private void drawWaveformChart() { 268 final short[] wave = AudioTest.getRecordedWave(); 269 List<Entry> entries = new ArrayList<>(); 270 int frameRate = audioTest.getOptimalFrameRate(); 271 for (int i = 0; i < wave.length; i++) { 272 float timeStamp = (float) i / frameRate * 1000f; 273 entries.add(new Entry(timeStamp, (float) wave[i])); 274 } 275 LineDataSet dataSet = new LineDataSet(entries, "Waveform"); 276 dataSet.setColor(Color.BLACK); 277 dataSet.setValueTextColor(Color.BLACK); 278 dataSet.setCircleColor(ContextCompat.getColor(getContext(), R.color.DarkGreen)); 279 dataSet.setCircleRadius(1.5f); 280 dataSet.setCircleColorHole(Color.DKGRAY); 281 LineData lineData = new LineData(dataSet); 282 chart.setData(lineData); 283 284 LimitLine line = new LimitLine(audioTest.getThreshold(), "Threshold"); 285 line.setLineColor(Color.RED); 286 line.setLabelPosition(LimitLine.LimitLabelPosition.LEFT_TOP); 287 line.setLineWidth(2f); 288 line.setTextColor(Color.DKGRAY); 289 line.setTextSize(10f); 290 chart.getAxisLeft().addLimitLine(line); 291 292 final Description desc = new Description(); 293 desc.setText("Wave [digital level -32768 to +32767] vs. Time [ms]"); 294 desc.setTextSize(12f); 295 chart.setDescription(desc); 296 chart.getLegend().setEnabled(false); 297 chart.invalidate(); 298 chartLayout.setVisibility(View.VISIBLE); 299 } 300 disableButtons()301 private void disableButtons() { 302 startButton.setEnabled(false); 303 stopButton.setEnabled(true); 304 } 305 enableButtons()306 private void enableButtons() { 307 startButton.setEnabled(true); 308 stopButton.setEnabled(false); 309 } 310 } 311