blob: 30bc5fcabfc4925ae7fe527307b9eeede80035a9 [file] [log] [blame]
/*
* Copyright (C) 2018 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 com.android.cts.verifier.audio;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R; // needed to access resource in CTSVerifier project namespace.
import java.util.List;
public class ProAudioActivity
extends PassFailButtons.Activity
implements View.OnClickListener {
private static final String TAG = ProAudioActivity.class.getName();
private static final boolean DEBUG = false;
// Flags
private boolean mClaimsLowLatencyAudio; // CDD ProAudio section C-1-1
private boolean mClaimsMIDI; // CDD ProAudio section C-1-4
private boolean mClaimsUSBHostMode; // CDD ProAudio section C-1-3
private boolean mClaimsUSBPeripheralMode; // CDD ProAudio section C-1-3
private boolean mClaimsHDMI; // CDD ProAudio section C-1-3
// Values
private static final double LATENCY_MS_LIMIT = 20.0; // CDD ProAudio section C-1-2
private double mRoundTripLatency;
private static final double CONFIDENCE_LIMIT = 0.75; // TBD
private double mRoundTripConfidence;
// Peripheral(s)
AudioManager mAudioManager;
private boolean mIsPeripheralAttached; // CDD ProAudio section C-1-3
private AudioDeviceInfo mOutputDevInfo;
private AudioDeviceInfo mInputDevInfo;
private AudioDeviceInfo mHDMIDeviceInfo;
// Widgets
TextView mInputDeviceTxt;
TextView mOutputDeviceTxt;
TextView mRoundTripLatencyTxt;
TextView mRoundTripConfidenceTxt;
TextView mHDMISupportLbl;
CheckBox mClaimsHDMICheckBox;
public ProAudioActivity() {
super();
}
private boolean claimsLowLatencyAudio() {
// CDD Section C-1-1: android.hardware.audio.low_latency
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
}
private boolean claimsMIDI() {
// CDD Section C-1-4: android.software.midi
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI);
}
private boolean claimsUSBHostMode() {
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_USB_HOST);
}
private boolean claimsUSBPeripheralMode() {
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_USB_ACCESSORY);
}
private void showConnectedAudioPeripheral() {
mInputDeviceTxt.setText(
mInputDevInfo != null ? mInputDevInfo.getProductName().toString()
: "");
mOutputDeviceTxt.setText(
mOutputDevInfo != null ? mOutputDevInfo.getProductName().toString()
: "");
}
// HDMI Stuff
private boolean isHDMIValid() {
if (mHDMIDeviceInfo == null) {
return false;
}
// MUST support output in stereo and eight channels...
boolean has2Chans = false;
boolean has8Chans = false;
int[] channelCounts = mHDMIDeviceInfo.getChannelCounts();
for (int count : channelCounts) {
if (count == 2) {
has2Chans = true;
} else if (count == 8) {
has8Chans = true;
}
}
if (!has2Chans || !has8Chans) {
return false;
}
// at 20-bit or 24-bit depth
boolean hasFloatEncoding = false;
int[] encodings = mHDMIDeviceInfo.getEncodings();
for (int encoding : encodings) {
if (encoding == AudioFormat.ENCODING_PCM_FLOAT) {
hasFloatEncoding = true;
break;
}
}
if (!hasFloatEncoding) {
return false;
}
// and 192 kHz
boolean has192K = false;
int[] sampleRates = mHDMIDeviceInfo.getSampleRates();
for (int rate : sampleRates) {
if (rate >= 192000) {
has192K = true;
}
}
if (!has192K) {
return false;
}
// without bit-depth loss or resampling (hmmmmm....).
return true;
}
private void calculatePass() {
boolean hasPassed =
mClaimsLowLatencyAudio && mClaimsMIDI &&
mClaimsUSBHostMode && mClaimsUSBPeripheralMode &&
(!mClaimsHDMI || isHDMIValid()) &&
mOutputDevInfo != null && mInputDevInfo != null &&
mRoundTripLatency != 0.0 && mRoundTripLatency <= LATENCY_MS_LIMIT &&
mRoundTripConfidence >= CONFIDENCE_LIMIT;
getPassButton().setEnabled(hasPassed);
}
//
// Loopback App Stuff
//
private final static String LOOPBACK_PACKAGE_NAME = "org.drrickorang.loopback";
// Test Intents
// From Loopback App LoobackActivity.java
private static final String INTENT_TEST_TYPE = "TestType";
// from Loopback App Constant.java
public static final int LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY = 222;
public boolean isLoopbackAppInstalled() {
try {
getPackageManager().getPackageInfo(
LOOPBACK_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
return true;
} catch (PackageManager.NameNotFoundException e) {
// This indicates that the specified app (Loopback in this case) is NOT installed
// fall through...
}
return false;
}
// arbitrary request code
private static final int LATENCY_RESULTS_REQUEST_CODE = 1;
private static final String KEY_CTSINVOCATION = "CTS-Test";
private static final String KEY_ROUND_TRIP_TIME = "RoundTripTime";
private static final String KEY_ROUND_TRIP_CONFIDENCE = "Confidence";
// We may need to iterate and average round-trip measurements
// So add this plumbing though NOT USED.
private static final String KEY_NUMITERATIONS = "NumIterations";
private static final int NUM_ROUNDTRIPITERATIONS = 3;
private void runRoundTripTest() {
if (!isLoopbackAppInstalled()) {
Toast.makeText(this, "Loopback App not installed", Toast.LENGTH_SHORT).show();
return;
}
if (!mIsPeripheralAttached) {
Toast.makeText(this, "Please connect a USB audio peripheral with loopback cables" +
" before running the latency test.",
Toast.LENGTH_SHORT).show();
return;
}
mRoundTripLatency = 0.0;
mRoundTripConfidence = 0.0;
Intent intent = new Intent(Intent.CATEGORY_LAUNCHER);
intent.setComponent(
new ComponentName(LOOPBACK_PACKAGE_NAME,LOOPBACK_PACKAGE_NAME + ".LoopbackActivity"));
intent.putExtra(KEY_CTSINVOCATION, "CTS-Verifier Invocation");
intent.putExtra(INTENT_TEST_TYPE, LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY);
intent.putExtra(KEY_NUMITERATIONS, NUM_ROUNDTRIPITERATIONS);
startActivityForResult(intent, LATENCY_RESULTS_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Check which request we're responding to
if (resultCode == RESULT_OK) {
Toast.makeText(this, "Round Trip Test Complete.", Toast.LENGTH_SHORT).show();
if (requestCode == LATENCY_RESULTS_REQUEST_CODE) {
Bundle extras = data != null ? data.getExtras() : null;
if (extras != null) {
mRoundTripLatency = extras.getDouble(KEY_ROUND_TRIP_TIME);
mRoundTripLatencyTxt.setText(String.format("%.2f ms", mRoundTripLatency));
mRoundTripConfidence = extras.getDouble(KEY_ROUND_TRIP_CONFIDENCE);
mRoundTripConfidenceTxt.setText(String.format("%.2f", mRoundTripConfidence));
}
}
calculatePass();
} else {
Toast.makeText(this, "Round Trip Test Canceled.", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.pro_audio);
mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
mAudioManager.registerAudioDeviceCallback(new ConnectListener(), new Handler());
setPassFailButtonClickListeners();
setInfoResources(R.string.proaudio_test, R.string.proaudio_info, -1);
mClaimsLowLatencyAudio = claimsLowLatencyAudio();
((TextView)findViewById(R.id.proAudioHasLLALbl)).setText("" + mClaimsLowLatencyAudio);
mClaimsMIDI = claimsMIDI();
((TextView)findViewById(R.id.proAudioHasMIDILbl)).setText("" + mClaimsMIDI);
mClaimsUSBHostMode = claimsUSBHostMode();
((TextView)findViewById(R.id.proAudioMidiHasUSBHostLbl)).setText("" + mClaimsUSBHostMode);
mClaimsUSBPeripheralMode = claimsUSBPeripheralMode();
((TextView)findViewById(
R.id.proAudioMidiHasUSBPeripheralLbl)).setText("" + mClaimsUSBPeripheralMode);
// Connected Device
mInputDeviceTxt = ((TextView)findViewById(R.id.proAudioInputLbl));
mOutputDeviceTxt = ((TextView)findViewById(R.id.proAudioOutputLbl));
// Round-trip Latency
mRoundTripLatencyTxt = (TextView)findViewById(R.id.proAudioRoundTripLbl);
mRoundTripConfidenceTxt = (TextView)findViewById(R.id.proAudioConfidenceLbl);
((Button)findViewById(R.id.proAudio_runRoundtripBtn)).setOnClickListener(this);
// HDMI
mHDMISupportLbl = (TextView)findViewById(R.id.proAudioHDMISupportLbl);
mClaimsHDMICheckBox = (CheckBox)findViewById(R.id.proAudioHasHDMICheckBox);
mClaimsHDMICheckBox.setOnClickListener(this);
calculatePass();
}
private void scanPeripheralList(AudioDeviceInfo[] devices) {
// CDD Section C-1-3: USB port, host-mode support
// Can't just use the first record because then we will only get
// Source OR sink, not both even on devices that are both.
mOutputDevInfo = null;
mInputDevInfo = null;
// Any valid peripherals
// Do we leave in the Headset test to support a USB-Dongle?
for (AudioDeviceInfo devInfo : devices) {
if (devInfo.getType() == AudioDeviceInfo.TYPE_USB_DEVICE || // USB Peripheral
devInfo.getType() == AudioDeviceInfo.TYPE_USB_HEADSET || // USB dongle+LBPlug
devInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET || // Loopback Plug?
devInfo.getType() == AudioDeviceInfo.TYPE_AUX_LINE) { // Aux-cable loopback?
if (devInfo.isSink()) {
mOutputDevInfo = devInfo;
}
if (devInfo.isSource()) {
mInputDevInfo = devInfo;
}
} else if (devInfo.isSink() && devInfo.getType() == AudioDeviceInfo.TYPE_HDMI) {
mHDMIDeviceInfo = devInfo;
}
}
mIsPeripheralAttached = mOutputDevInfo != null || mInputDevInfo != null;
showConnectedAudioPeripheral();
if (mHDMIDeviceInfo != null) {
mClaimsHDMICheckBox.setChecked(true);
mHDMISupportLbl.setText(getResources().getString(
isHDMIValid() ? R.string.pass_button_text : R.string.fail_button_text));
}
mHDMISupportLbl.setText(getResources().getString(R.string.audio_proaudio_NA));
calculatePass();
}
private class ConnectListener extends AudioDeviceCallback {
/*package*/ ConnectListener() {}
//
// AudioDevicesManager.OnDeviceConnectionListener
//
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
}
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.proAudio_runRoundtripBtn:
runRoundTripTest();
break;
case R.id.proAudioHasHDMICheckBox:
if (mClaimsHDMICheckBox.isChecked()) {
AlertDialog.Builder builder =
new AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert);
builder.setTitle(getResources().getString(R.string.proaudio_hdmi_infotitle));
builder.setMessage(getResources().getString(R.string.proaudio_hdmi_message));
builder.setPositiveButton(android.R.string.yes,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {}
});
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.show();
mClaimsHDMI = true;
mHDMISupportLbl.setText(getResources().getString(R.string.audio_proaudio_pending));
} else {
mClaimsHDMI = false;
mHDMISupportLbl.setText(getResources().getString(R.string.audio_proaudio_NA));
}
break;
}
}
}