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