1 /*
2  * Copyright 2019 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.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.os.Bundle;
24 import android.view.View;
25 import android.widget.Button;
26 import android.widget.TextView;
27 
28 import java.io.IOException;
29 
30 /**
31  * Guide the user through a series of tests plugging in and unplugging a headset.
32  * Print a summary at the end of any failures.
33  */
34 public class TestDisconnectActivity extends TestAudioActivity {
35 
36     private static final String TEXT_SKIP = "SKIP";
37     private static final String TEXT_PASS = "PASS";
38     private static final String TEXT_FAIL = "FAIL !!!!";
39     public static final int POLL_DURATION_MILLIS = 50;
40     public static final int SETTLING_TIME_MILLIS = 600;
41     public static final int TIME_TO_FAILURE_MILLIS = 3000;
42 
43     private TextView     mInstructionsTextView;
44     private TextView     mStatusTextView;
45     private TextView     mPlugTextView;
46 
47     private volatile boolean mTestFailed;
48     private volatile boolean mSkipTest;
49     private volatile int mPlugCount;
50     private BroadcastReceiver mPluginReceiver = new PluginBroadcastReceiver();
51     private Button       mFailButton;
52     private Button       mSkipButton;
53 
54     protected AutomatedTestRunner mAutomatedTestRunner;
55 
56     // Receive a broadcast Intent when a headset is plugged in or unplugged.
57     // Display a count on screen.
58     public class PluginBroadcastReceiver extends BroadcastReceiver {
59         @Override
onReceive(Context context, Intent intent)60         public void onReceive(Context context, Intent intent) {
61             mPlugCount++;
62             runOnUiThread(new Runnable() {
63                 @Override
64                 public void run() {
65                     String message = "Intent.HEADSET_PLUG #" + mPlugCount;
66                     mPlugTextView.setText(message);
67                 }
68             });
69         }
70     }
71 
72     @Override
inflateActivity()73     protected void inflateActivity() {
74         setContentView(R.layout.activity_test_disconnect);
75     }
76 
77     @Override
onCreate(Bundle savedInstanceState)78     protected void onCreate(Bundle savedInstanceState) {
79         super.onCreate(savedInstanceState);
80 
81         mAutomatedTestRunner = findViewById(R.id.auto_test_runner);
82         mAutomatedTestRunner.setActivity(this);
83 
84         mInstructionsTextView = (TextView) findViewById(R.id.text_instructions);
85         mStatusTextView = (TextView) findViewById(R.id.text_status);
86         mPlugTextView = (TextView) findViewById(R.id.text_plug_events);
87 
88         mFailButton = (Button) findViewById(R.id.button_fail);
89         mSkipButton = (Button) findViewById(R.id.button_skip);
90         updateFailSkipButton(false);
91     }
92 
93     @Override
getTestName()94     public String getTestName() {
95         return "Disconnect";
96     }
97 
getActivityType()98     int getActivityType() {
99         return ACTIVITY_TEST_DISCONNECT;
100     }
101 
102     @Override
isOutput()103     boolean isOutput() {
104         return true;
105     }
106 
107     @Override
setupEffects(int sessionId)108     public void setupEffects(int sessionId) {
109     }
110 
updateFailSkipButton(final boolean running)111     private void updateFailSkipButton(final boolean running) {
112         runOnUiThread(new Runnable() {
113             @Override
114             public void run() {
115                 mFailButton.setEnabled(running);
116                 mSkipButton.setEnabled(running);
117             }
118         });
119     }
120 
121     // Write to status and command view
setInstructionsText(final String text)122     private void setInstructionsText(final String text) {
123         runOnUiThread(new Runnable() {
124             @Override
125             public void run() {
126                 mInstructionsTextView.setText(text);
127             }
128         });
129     }
130 
131     // Write to status and command view
setStatusText(final String text)132     private void setStatusText(final String text) {
133         runOnUiThread(new Runnable() {
134             @Override
135             public void run() {
136                 mStatusTextView.setText(text);
137             }
138         });
139     }
140 
141     @Override
onResume()142     public void onResume() {
143         super.onResume();
144         IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
145         this.registerReceiver(mPluginReceiver, filter);
146     }
147 
148     @Override
onPause()149     public void onPause() {
150         this.unregisterReceiver(mPluginReceiver);
151         super.onPause();
152     }
153 
154     // This should only be called from UI events such as onStop or a button press.
155     @Override
onStopTest()156     public void onStopTest() {
157         mAutomatedTestRunner.stopTest();
158     }
159 
startAudioTest()160     public void startAudioTest() throws IOException {
161         startAudio();
162     }
163 
stopAudioTest()164     public void stopAudioTest() {
165         stopAudioQuiet();
166         closeAudio();
167     }
168 
onCancel(View view)169     public void onCancel(View view) {
170         stopAudioTest();
171         mAutomatedTestRunner.onTestFinished();
172     }
173 
174     // Called on UI thread
onStopAudioTest(View view)175     public void onStopAudioTest(View view) {
176         stopAudioTest();
177         mAutomatedTestRunner.onTestFinished();
178         keepScreenOn(false);
179     }
180 
onFailTest(View view)181     public void onFailTest(View view) {
182         mTestFailed = true;
183     }
184 
onSkipTest(View view)185     public void onSkipTest(View view) {
186         mSkipTest = true;
187     }
188 
getConfigText(StreamConfiguration config)189     private String getConfigText(StreamConfiguration config) {
190         return ((config.getDirection() == StreamConfiguration.DIRECTION_OUTPUT) ? "OUT" : "IN")
191                 + ", Perf = " + StreamConfiguration.convertPerformanceModeToText(
192                 config.getPerformanceMode())
193                 + ", " + StreamConfiguration.convertSharingModeToText(config.getSharingMode())
194                 + ", " + config.getSampleRate();
195     }
196 
log(String text)197     private void log(String text) {
198         mAutomatedTestRunner.log(text);
199     }
200 
appendFailedSummary(String text)201     private void appendFailedSummary(String text) {
202         mAutomatedTestRunner.appendFailedSummary(text);
203     }
204 
testConfiguration(boolean isInput, int perfMode, int sharingMode, int sampleRate, boolean requestPlugin)205     private void testConfiguration(boolean isInput,
206                                    int perfMode,
207                                    int sharingMode,
208                                    int sampleRate,
209                                    boolean requestPlugin) throws InterruptedException {
210         String actualConfigText = "none";
211         mSkipTest = false;
212 
213         AudioInputTester    mAudioInTester = null;
214         AudioOutputTester   mAudioOutTester = null;
215 
216         clearStreamContexts();
217 
218         if (isInput) {
219             mAudioInTester = addAudioInputTester();
220         } else {
221             mAudioOutTester = addAudioOutputTester();
222         }
223 
224         // Configure settings
225         StreamConfiguration requestedConfig = (isInput)
226                 ? mAudioInTester.requestedConfiguration
227                 : mAudioOutTester.requestedConfiguration;
228         StreamConfiguration actualConfig = (isInput)
229                 ? mAudioInTester.actualConfiguration
230                 : mAudioOutTester.actualConfiguration;
231 
232         requestedConfig.reset();
233         requestedConfig.setPerformanceMode(perfMode);
234         requestedConfig.setSharingMode(sharingMode);
235         requestedConfig.setSampleRate(sampleRate);
236         if (sampleRate != 0) {
237             requestedConfig.setRateConversionQuality(StreamConfiguration.RATE_CONVERSION_QUALITY_MEDIUM);
238         }
239 
240         log("========================== #" + mAutomatedTestRunner.getTestCount());
241         log("Requested:");
242         log(getConfigText(requestedConfig));
243 
244         // Give previous stream time to close and release resources. Avoid race conditions.
245         Thread.sleep(SETTLING_TIME_MILLIS);
246         if (!mAutomatedTestRunner.isThreadEnabled()) return;
247         boolean openFailed = false;
248         AudioStreamBase stream = null;
249         try {
250             openAudio();
251             log("Actual:");
252             actualConfigText = getConfigText(actualConfig)
253                     + ", " + (actualConfig.isMMap() ? "MMAP" : "Legacy");
254             log(actualConfigText);
255 
256             stream = (isInput)
257                     ? mAudioInTester.getCurrentAudioStream()
258                     : mAudioOutTester.getCurrentAudioStream();
259         } catch (IOException e) {
260             openFailed = true;
261             log(e.getMessage());
262         }
263 
264         // The test is only worth running if we got the configuration we requested.
265         boolean valid = true;
266         if (!openFailed) {
267             if(actualConfig.getSharingMode() != sharingMode) {
268                 log("did not get requested sharing mode");
269                 valid = false;
270             }
271             if (actualConfig.getPerformanceMode() != perfMode) {
272                 log("did not get requested performance mode");
273                 valid = false;
274             }
275             if (actualConfig.getNativeApi() == StreamConfiguration.NATIVE_API_OPENSLES) {
276                 log("OpenSL ES does not support automatic disconnect");
277                 valid = false;
278             }
279         }
280 
281         if (!openFailed && valid) {
282             try {
283                 startAudioTest();
284             } catch (IOException e) {
285                 e.printStackTrace();
286                 valid = false;
287                 log(e.getMessage());
288             }
289         }
290 
291         int oldPlugCount = mPlugCount;
292         if (!openFailed && valid) {
293             mTestFailed = false;
294             updateFailSkipButton(true);
295             // poll until stream started
296             while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest &&
297                     stream.getState() == StreamConfiguration.STREAM_STATE_STARTING) {
298                 Thread.sleep(POLL_DURATION_MILLIS);
299             }
300             String message = (requestPlugin ? "Plug IN" : "UNplug") + " headset now!";
301             setStatusText("Testing:\n" + actualConfigText);
302             setInstructionsText(message);
303             int timeoutCount = 0;
304             // Wait for Java plug count to change or stream to disconnect.
305             while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest &&
306                     stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) {
307                 Thread.sleep(POLL_DURATION_MILLIS);
308                 if (mPlugCount > oldPlugCount) {
309                     timeoutCount = TIME_TO_FAILURE_MILLIS / POLL_DURATION_MILLIS;
310                     break;
311                 }
312             }
313             // Wait for timeout or stream to disconnect.
314             while (!mTestFailed && mAutomatedTestRunner.isThreadEnabled() && !mSkipTest && (timeoutCount > 0) &&
315                     stream.getState() == StreamConfiguration.STREAM_STATE_STARTED) {
316                 Thread.sleep(POLL_DURATION_MILLIS);
317                 timeoutCount--;
318                 if (timeoutCount == 0) {
319                     mTestFailed = true;
320                 } else {
321                     setStatusText("Plug detected by Java.\nCounting down to Oboe failure: " + timeoutCount);
322                 }
323             }
324             if (!mTestFailed) {
325                 int error = stream.getLastErrorCallbackResult();
326                 if (error != StreamConfiguration.ERROR_DISCONNECTED) {
327                     log("onEerrorCallback error = " + error
328                             + ", expected " + StreamConfiguration.ERROR_DISCONNECTED);
329                     mTestFailed = true;
330                 }
331             }
332             setStatusText(mTestFailed ? "Failed" : "Passed - detected");
333         }
334         updateFailSkipButton(false);
335         setInstructionsText("Wait...");
336 
337         if (!openFailed) {
338             stopAudioTest();
339         }
340 
341         if (mSkipTest) valid = false;
342 
343         if (valid) {
344             if (openFailed) {
345                 appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n");
346                 appendFailedSummary(getConfigText(requestedConfig) + "\n");
347                 appendFailedSummary("Open failed!\n");
348                 mAutomatedTestRunner.incrementFailCount();
349             } else {
350                 log("Result:");
351                 boolean passed = !mTestFailed;
352                 String resultText = requestPlugin ? "plugIN" : "UNplug";
353                 resultText += ", " + (passed ? TEXT_PASS : TEXT_FAIL);
354                 log(resultText);
355                 if (!passed) {
356                     appendFailedSummary("------ #" + mAutomatedTestRunner.getTestCount() + "\n");
357                     appendFailedSummary("  " + actualConfigText + "\n");
358                     appendFailedSummary("    " + resultText + "\n");
359                     mAutomatedTestRunner.incrementFailCount();
360                 } else {
361                     mAutomatedTestRunner.incrementPassCount();
362                 }
363             }
364         } else {
365             log(TEXT_SKIP);
366         }
367         // Give hardware time to settle between tests.
368         Thread.sleep(1000);
369         mAutomatedTestRunner.incrementTestCount();
370     }
371 
testConfiguration(boolean isInput, int performanceMode, int sharingMode, int sampleRate)372     private void testConfiguration(boolean isInput, int performanceMode,
373                                    int sharingMode, int sampleRate) throws InterruptedException {
374         boolean requestPlugin = true; // plug IN
375         testConfiguration(isInput, performanceMode, sharingMode, sampleRate, requestPlugin);
376         requestPlugin = false; // UNplug
377         testConfiguration(isInput, performanceMode, sharingMode, sampleRate, requestPlugin);
378     }
379 
testConfiguration(boolean isInput, int performanceMode, int sharingMode)380     private void testConfiguration(boolean isInput, int performanceMode,
381                                    int sharingMode) throws InterruptedException {
382         final int sampleRate = 0;
383         testConfiguration(isInput, performanceMode, sharingMode, sampleRate);
384     }
385 
testConfiguration(int performanceMode, int sharingMode)386     private void testConfiguration(int performanceMode,
387                                    int sharingMode) throws InterruptedException {
388         testConfiguration(false, performanceMode, sharingMode);
389         testConfiguration(true, performanceMode, sharingMode);
390     }
391 
392     @Override
runTest()393     public void runTest() {
394         mPlugCount = 0;
395         // Try several different configurations.
396         try {
397             testConfiguration(false, StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
398                     StreamConfiguration.SHARING_MODE_EXCLUSIVE, 44100);
399             testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
400                     StreamConfiguration.SHARING_MODE_EXCLUSIVE);
401             testConfiguration(StreamConfiguration.PERFORMANCE_MODE_LOW_LATENCY,
402                     StreamConfiguration.SHARING_MODE_SHARED);
403             testConfiguration(StreamConfiguration.PERFORMANCE_MODE_NONE,
404                     StreamConfiguration.SHARING_MODE_SHARED);
405         } catch (InterruptedException e) {
406             log(e.getMessage());
407             showErrorToast(e.getMessage());
408         } finally {
409             setInstructionsText("Test completed.");
410             updateFailSkipButton(false);
411         }
412     }
413 }
414