• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.sample.oboe.manualtest;
18 
19 import android.app.Activity;
20 import android.content.Context;
21 import android.media.AudioDeviceInfo;
22 import android.media.AudioManager;
23 import android.os.Bundle;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.util.Log;
27 import android.view.View;
28 import android.view.WindowManager;
29 import android.widget.Button;
30 import android.widget.CheckBox;
31 import android.widget.Toast;
32 
33 import java.io.IOException;
34 import java.text.DateFormat;
35 import java.text.SimpleDateFormat;
36 import java.util.ArrayList;
37 import java.util.Calendar;
38 import java.util.Date;
39 
40 /**
41  * Base class for other Activities.
42  */
43 abstract class TestAudioActivity extends Activity {
44     public static final String TAG = "TestOboe";
45 
46     protected static final int FADER_PROGRESS_MAX = 1000;
47 
48     public static final int AUDIO_STATE_OPEN = 0;
49     public static final int AUDIO_STATE_STARTED = 1;
50     public static final int AUDIO_STATE_PAUSED = 2;
51     public static final int AUDIO_STATE_STOPPED = 3;
52     public static final int AUDIO_STATE_CLOSING = 4;
53     public static final int AUDIO_STATE_CLOSED = 5;
54 
55     public static final int COLOR_ACTIVE = 0xFFD0D0A0;
56     public static final int COLOR_IDLE = 0xFFD0D0D0;
57 
58     // Pass the activity index to native so it can know how to respond to the start and stop calls.
59     // WARNING - must match definitions in NativeAudioContext.h ActivityType
60     public static final int ACTIVITY_TEST_OUTPUT = 0;
61     public static final int ACTIVITY_TEST_INPUT = 1;
62     public static final int ACTIVITY_TAP_TO_TONE = 2;
63     public static final int ACTIVITY_RECORD_PLAY = 3;
64     public static final int ACTIVITY_ECHO = 4;
65     public static final int ACTIVITY_RT_LATENCY = 5;
66     public static final int ACTIVITY_GLITCHES = 6;
67     public static final int ACTIVITY_TEST_DISCONNECT = 7;
68 
69     private int mAudioState = AUDIO_STATE_CLOSED;
70     protected String audioManagerSampleRate;
71     protected int audioManagerFramesPerBurst;
72     protected ArrayList<StreamContext> mStreamContexts;
73     private Button mOpenButton;
74     private Button mStartButton;
75     private Button mPauseButton;
76     private Button mStopButton;
77     private Button mCloseButton;
78     private MyStreamSniffer mStreamSniffer;
79     private CheckBox mCallbackReturnStopBox;
80     private int mSampleRate;
81     private boolean mScoStarted;
82 
83     public static class StreamContext {
84         StreamConfigurationView configurationView;
85         AudioStreamTester tester;
86 
isInput()87         boolean isInput() {
88             return tester.getCurrentAudioStream().isInput();
89         }
90     }
91 
92     // Periodically query the status of the streams.
93     protected class MyStreamSniffer {
94         public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150;
95         public static final int SNIFFER_UPDATE_DELAY_MSEC = 300;
96 
97         private Handler mHandler;
98 
99         // Display status info for the stream.
100         private Runnable runnableCode = new Runnable() {
101             @Override
102             public void run() {
103                 boolean streamClosed = false;
104                 boolean gotViews = false;
105                 for (StreamContext streamContext : mStreamContexts) {
106                     AudioStreamBase.StreamStatus status = streamContext.tester.getCurrentAudioStream().getStreamStatus();
107                     if (streamContext.configurationView != null) {
108                         // Handler runs this on the main UI thread.
109                         int framesPerBurst = streamContext.tester.getCurrentAudioStream().getFramesPerBurst();
110                         status.framesPerCallback = getFramesPerCallback();
111                         final String msg = status.dump(framesPerBurst);
112                         streamContext.configurationView.setStatusText(msg);
113                         updateStreamDisplay();
114                         gotViews = true;
115                     }
116 
117                     streamClosed = streamClosed || (status.state >= 12);
118                 }
119 
120                 if (streamClosed) {
121                     onStreamClosed();
122                 } else {
123                     // Repeat this runnable code block again.
124                     if (gotViews) {
125                         mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
126                     }
127                 }
128             }
129         };
130 
startStreamSniffer()131         private void startStreamSniffer() {
132             stopStreamSniffer();
133             mHandler = new Handler(Looper.getMainLooper());
134             // Start the initial runnable task by posting through the handler
135             mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
136         }
137 
stopStreamSniffer()138         private void stopStreamSniffer() {
139             if (mHandler != null) {
140                 mHandler.removeCallbacks(runnableCode);
141             }
142         }
143 
144     }
145 
onStreamClosed()146     public void onStreamClosed() {
147     }
148 
inflateActivity()149     protected abstract void inflateActivity();
150 
updateStreamDisplay()151     void updateStreamDisplay() {
152     }
153 
154     @Override
onCreate(Bundle savedInstanceState)155     protected void onCreate(Bundle savedInstanceState) {
156         super.onCreate(savedInstanceState);
157         inflateActivity();
158         findAudioCommon();
159     }
160 
hideSettingsViews()161     public void hideSettingsViews() {
162         for (StreamContext streamContext : mStreamContexts) {
163             if (streamContext.configurationView != null) {
164                 streamContext.configurationView.hideSettingsView();
165             }
166         }
167     }
168 
169     @Override
onStart()170     protected void onStart() {
171         super.onStart();
172         resetConfiguration();
173     }
174 
resetConfiguration()175     protected void resetConfiguration() {
176     }
177 
178     @Override
onStop()179     protected void onStop() {
180         Log.i(TAG, "onStop() called so stopping audio =========================");
181         stopAudio();
182         closeAudio();
183         super.onStop();
184     }
185 
186     @Override
onDestroy()187     protected void onDestroy() {
188         mAudioState = AUDIO_STATE_CLOSED;
189         super.onDestroy();
190     }
191 
updateEnabledWidgets()192     protected void updateEnabledWidgets() {
193         if (mOpenButton != null) {
194             mOpenButton.setBackgroundColor(mAudioState == AUDIO_STATE_OPEN ? COLOR_ACTIVE : COLOR_IDLE);
195             mStartButton.setBackgroundColor(mAudioState == AUDIO_STATE_STARTED ? COLOR_ACTIVE : COLOR_IDLE);
196             mPauseButton.setBackgroundColor(mAudioState == AUDIO_STATE_PAUSED ? COLOR_ACTIVE : COLOR_IDLE);
197             mStopButton.setBackgroundColor(mAudioState == AUDIO_STATE_STOPPED ? COLOR_ACTIVE : COLOR_IDLE);
198             mCloseButton.setBackgroundColor(mAudioState == AUDIO_STATE_CLOSED ? COLOR_ACTIVE : COLOR_IDLE);
199         }
200         setConfigViewsEnabled(mAudioState == AUDIO_STATE_CLOSED);
201     }
202 
setConfigViewsEnabled(boolean b)203     private void setConfigViewsEnabled(boolean b) {
204         for (StreamContext streamContext : mStreamContexts) {
205             if (streamContext.configurationView != null) {
206                 streamContext.configurationView.setChildrenEnabled(b);
207             }
208         }
209     }
210 
isOutput()211     abstract boolean isOutput();
212 
clearStreamContexts()213     public void clearStreamContexts() {
214         mStreamContexts.clear();
215     }
216 
addOutputStreamContext()217     public StreamContext addOutputStreamContext() {
218         StreamContext streamContext = new StreamContext();
219         streamContext.tester = AudioOutputTester.getInstance();
220         streamContext.configurationView = (StreamConfigurationView)
221                 findViewById(R.id.outputStreamConfiguration);
222         if (streamContext.configurationView == null) {
223             streamContext.configurationView = (StreamConfigurationView)
224                     findViewById(R.id.streamConfiguration);
225         }
226         if (streamContext.configurationView != null) {
227             streamContext.configurationView.setOutput(true);
228             streamContext.configurationView.setRequestedConfiguration(streamContext.tester.requestedConfiguration);
229             streamContext.configurationView.setActualConfiguration(streamContext.tester.actualConfiguration);
230         }
231         mStreamContexts.add(streamContext);
232         return streamContext;
233     }
234 
235 
addAudioOutputTester()236     public AudioOutputTester addAudioOutputTester() {
237         StreamContext streamContext = addOutputStreamContext();
238         return (AudioOutputTester) streamContext.tester;
239     }
240 
addInputStreamContext()241     public StreamContext addInputStreamContext() {
242         StreamContext streamContext = new StreamContext();
243         streamContext.tester = AudioInputTester.getInstance();
244         streamContext.configurationView = (StreamConfigurationView)
245                 findViewById(R.id.inputStreamConfiguration);
246         if (streamContext.configurationView == null) {
247             streamContext.configurationView = (StreamConfigurationView)
248                     findViewById(R.id.streamConfiguration);
249         }
250         if (streamContext.configurationView != null) {
251             streamContext.configurationView.setOutput(false);
252             streamContext.configurationView.setRequestedConfiguration(streamContext.tester.requestedConfiguration);
253             streamContext.configurationView.setActualConfiguration(streamContext.tester.actualConfiguration);
254         }
255         streamContext.tester = AudioInputTester.getInstance();
256         mStreamContexts.add(streamContext);
257         return streamContext;
258     }
259 
addAudioInputTester()260     public AudioInputTester addAudioInputTester() {
261         StreamContext streamContext = addInputStreamContext();
262         return (AudioInputTester) streamContext.tester;
263     }
264 
updateStreamConfigurationViews()265     void updateStreamConfigurationViews() {
266         for (StreamContext streamContext : mStreamContexts) {
267             if (streamContext.configurationView != null) {
268                 streamContext.configurationView.updateDisplay();
269             }
270         }
271     }
272 
getFirstInputStreamContext()273     StreamContext getFirstInputStreamContext() {
274         for (StreamContext streamContext : mStreamContexts) {
275             if (streamContext.isInput())
276                 return streamContext;
277         }
278         return null;
279     }
280 
getFirstOutputStreamContext()281     StreamContext getFirstOutputStreamContext() {
282         for (StreamContext streamContext : mStreamContexts) {
283             if (!streamContext.isInput())
284                 return streamContext;
285         }
286         return null;
287     }
288 
findAudioCommon()289     protected void findAudioCommon() {
290         mOpenButton = (Button) findViewById(R.id.button_open);
291         if (mOpenButton != null) {
292             mStartButton = (Button) findViewById(R.id.button_start);
293             mPauseButton = (Button) findViewById(R.id.button_pause);
294             mStopButton = (Button) findViewById(R.id.button_stop);
295             mCloseButton = (Button) findViewById(R.id.button_close);
296         }
297         mStreamContexts = new ArrayList<StreamContext>();
298 
299         queryNativeAudioParameters();
300 
301         mCallbackReturnStopBox = (CheckBox) findViewById(R.id.callbackReturnStop);
302         if (mCallbackReturnStopBox != null) {
303             mCallbackReturnStopBox.setOnClickListener(new View.OnClickListener() {
304                 @Override
305                 public void onClick(View v) {
306                     OboeAudioStream.setCallbackReturnStop(mCallbackReturnStopBox.isChecked());
307                 }
308             });
309         }
310         OboeAudioStream.setCallbackReturnStop(false);
311 
312         mStreamSniffer = new MyStreamSniffer();
313     }
314 
queryNativeAudioParameters()315     private void queryNativeAudioParameters() {
316         AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
317         audioManagerSampleRate = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
318         String audioManagerFramesPerBurstText = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
319         audioManagerFramesPerBurst = Integer.parseInt(audioManagerFramesPerBurstText);
320     }
321 
setupEffects(int sessionId)322     abstract public void setupEffects(int sessionId);
323 
showErrorToast(String message)324     protected void showErrorToast(String message) {
325         showToast("Error: " + message);
326     }
327 
showToast(final String message)328     protected void showToast(final String message) {
329         runOnUiThread(new Runnable() {
330             @Override
331             public void run() {
332                 Toast.makeText(TestAudioActivity.this,
333                         message,
334                         Toast.LENGTH_SHORT).show();
335             }
336         });
337     }
338 
openAudio(View view)339     public void openAudio(View view) {
340         try {
341             openAudio();
342         } catch (Exception e) {
343             showErrorToast(e.getMessage());
344         }
345     }
346 
startAudio(View view)347     public void startAudio(View view) {
348         Log.i(TAG, "startAudio() called =======================================");
349         startAudio();
350         keepScreenOn(true);
351     }
352 
keepScreenOn(boolean on)353     protected void keepScreenOn(boolean on) {
354         if (on) {
355             getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
356         } else {
357             getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
358         }
359     }
360 
stopAudio(View view)361     public void stopAudio(View view) {
362         stopAudio();
363         keepScreenOn(false);
364     }
365 
pauseAudio(View view)366     public void pauseAudio(View view) {
367         pauseAudio();
368         keepScreenOn(false);
369     }
370 
closeAudio(View view)371     public void closeAudio(View view) {
372         closeAudio();
373     }
374 
getSampleRate()375     public int getSampleRate() {
376         return mSampleRate;
377     }
378 
openAudio()379     public void openAudio() throws IOException {
380         closeAudio();
381 
382         int sampleRate = 0;
383 
384         // Open output streams then open input streams.
385         // This is so that the capacity of input stream can be expanded to
386         // match the burst size of the output for full duplex.
387         for (StreamContext streamContext : mStreamContexts) {
388             if (!streamContext.isInput()) {
389                 openStreamContext(streamContext);
390                 int streamSampleRate = streamContext.tester.actualConfiguration.getSampleRate();
391                 if (sampleRate == 0) {
392                     sampleRate = streamSampleRate;
393                 }
394             }
395         }
396         for (StreamContext streamContext : mStreamContexts) {
397             if (streamContext.isInput()) {
398                 if (sampleRate != 0) {
399                     streamContext.tester.requestedConfiguration.setSampleRate(sampleRate);
400                 }
401                 openStreamContext(streamContext);
402             }
403         }
404         updateEnabledWidgets();
405         mStreamSniffer.startStreamSniffer();
406     }
407 
408     /**
409      * @param deviceId
410      * @return true if the device is TYPE_BLUETOOTH_SCO
411      */
isScoDevice(int deviceId)412     boolean isScoDevice(int deviceId) {
413         if (deviceId == 0) return false; // Unspecified
414         AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
415         final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
416         for (AudioDeviceInfo device : devices) {
417             if (device.getId() == deviceId) {
418                 return device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO;
419             }
420         }
421         return false;
422     }
423 
openStreamContext(StreamContext streamContext)424     private void openStreamContext(StreamContext streamContext) throws IOException {
425         StreamConfiguration requestedConfig = streamContext.tester.requestedConfiguration;
426         StreamConfiguration actualConfig = streamContext.tester.actualConfiguration;
427         requestedConfig.setFramesPerBurst(audioManagerFramesPerBurst);
428 
429         // Start Bluetooth SCO if needed.
430         if (isScoDevice(requestedConfig.getDeviceId()) && !mScoStarted) {
431             startBluetoothSco();
432             mScoStarted = true;
433         }
434 
435         streamContext.tester.open(); // OPEN the stream
436 
437         mSampleRate = actualConfig.getSampleRate();
438         mAudioState = AUDIO_STATE_OPEN;
439         int sessionId = actualConfig.getSessionId();
440         if (sessionId > 0) {
441             setupEffects(sessionId);
442         }
443         if (streamContext.configurationView != null) {
444             streamContext.configurationView.updateDisplay();
445         }
446     }
447 
448     // Native methods
startNative()449     private native int startNative();
pauseNative()450     private native int pauseNative();
stopNative()451     private native int stopNative();
setActivityType(int activityType)452     protected native void setActivityType(int activityType);
getFramesPerCallback()453     private native int getFramesPerCallback();
454 
startAudio()455     public void startAudio() {
456         int result = startNative();
457         if (result < 0) {
458             showErrorToast("Start failed with " + result);
459         } else {
460             for (StreamContext streamContext : mStreamContexts) {
461                 StreamConfigurationView configView = streamContext.configurationView;
462                 if (configView != null) {
463                     configView.updateDisplay();
464                 }
465             }
466             mAudioState = AUDIO_STATE_STARTED;
467             updateEnabledWidgets();
468         }
469     }
470 
toastPauseError(int result)471     protected void toastPauseError(int result) {
472         showErrorToast("Pause failed with " + result);
473     }
474 
pauseAudio()475     public void pauseAudio() {
476         int result = pauseNative();
477         if (result < 0) {
478             toastPauseError(result);
479         } else {
480             mAudioState = AUDIO_STATE_PAUSED;
481             updateEnabledWidgets();
482         }
483     }
484 
stopAudio()485     public void stopAudio() {
486         int result = stopNative();
487         if (result < 0) {
488             showErrorToast("Stop failed with " + result);
489         } else {
490             mAudioState = AUDIO_STATE_STOPPED;
491             updateEnabledWidgets();
492         }
493     }
494 
stopAudioQuiet()495     public void stopAudioQuiet() {
496         stopNative();
497         mAudioState = AUDIO_STATE_STOPPED;
498         updateEnabledWidgets();
499     }
500 
501     // Make synchronized so we don't close from two streams at the same time.
closeAudio()502     public synchronized void closeAudio() {
503         if (mAudioState >= AUDIO_STATE_CLOSING) {
504             Log.d(TAG, "closeAudio() already closing");
505             return;
506         }
507         mAudioState = AUDIO_STATE_CLOSING;
508 
509         mStreamSniffer.stopStreamSniffer();
510         // Close output streams first because legacy callbacks may still be active
511         // and an output stream may be calling the input stream.
512         for (StreamContext streamContext : mStreamContexts) {
513             if (!streamContext.isInput()) {
514                 streamContext.tester.close();
515             }
516         }
517         for (StreamContext streamContext : mStreamContexts) {
518             if (streamContext.isInput()) {
519                 streamContext.tester.close();
520             }
521         }
522 
523         if (mScoStarted) {
524             stopBluetoothSco();
525             mScoStarted = false;
526         }
527 
528         mAudioState = AUDIO_STATE_CLOSED;
529         updateEnabledWidgets();
530     }
531 
getTimestampString()532     String getTimestampString() {
533         DateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss");
534         Date now = Calendar.getInstance().getTime();
535         return df.format(now);
536     }
537 
startBluetoothSco()538     void startBluetoothSco() {
539         AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
540         myAudioMgr.startBluetoothSco();
541     }
542 
stopBluetoothSco()543     void stopBluetoothSco() {
544         AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
545         myAudioMgr.stopBluetoothSco();
546     }
547 }
548