1 /* 2 * Copyright (C) 2022 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.android.cts.verifier.audio; 18 19 import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode; 20 import static com.android.cts.verifier.TestListAdapter.setTestNameSuffix; 21 22 import android.app.AlertDialog; 23 import android.content.DialogInterface; 24 import android.media.AudioDescriptor; 25 import android.media.AudioDeviceCallback; 26 import android.media.AudioDeviceInfo; 27 import android.media.AudioHalVersionInfo; 28 import android.media.AudioManager; 29 import android.os.Bundle; 30 import android.util.Log; 31 import android.util.Pair; 32 import android.view.View; 33 import android.view.View.OnClickListener; 34 import android.widget.Button; 35 import android.widget.CheckBox; 36 import android.widget.TextView; 37 38 import com.android.compatibility.common.util.ResultType; 39 import com.android.compatibility.common.util.ResultUnit; 40 import com.android.cts.verifier.CtsVerifierReportLog; 41 import com.android.cts.verifier.PassFailButtons; 42 import com.android.cts.verifier.R; 43 44 import java.lang.reflect.Method; 45 import java.util.Collections; 46 import java.util.Map; 47 import java.util.Set; 48 import java.util.SortedMap; 49 import java.util.TreeMap; 50 51 /** 52 * AudioDescriptorActivity is used to test if the reported AudioDescriptor is valid and necessary. 53 * AudioDescriptor is introduced for the HAL to report the device capabilities that have formats 54 * unknown to Android. But it is also mandatory to report device capabilities using Android defined 55 * enums as long as it is possible so that the developers won't need to parse AudioDescriptor. 56 */ 57 public class AudioDescriptorActivity extends PassFailButtons.Activity { 58 private static final String TAG = "AudioDescriptorActivity"; 59 60 // ReportLog Schema 61 private static final String SECTION_AUDIODESCRIPTOR = "audio_descriptors_activity"; 62 private static final String KEY_CLAIMS_HDMI = "claims_hdmi"; 63 private static final String KEY_HAL_VERSION = "audio_hal_version"; 64 private static final String KEY_AUDIO_DESCRIPTOR = "audio_descriptor"; 65 66 private static final int EXTENSION_FORMAT_CODE = 15; 67 68 // Description of not used format extended codes can be found at 69 // https://en.wikipedia.org/wiki/Extended_Display_Identification_Data. 70 private static final Set<Integer> NOT_USED_FORMAT_EXTENDED_CODES = Set.of(1, 2, 3); 71 72 // Description of short audio descriptor can be found at 73 // https://en.wikipedia.org/wiki/Extended_Display_Identification_Data. 74 // The collection is sorted decreasingly by HAL version. 75 private static final SortedMap<AudioHalVersionInfo, HalFormats> ALL_HAL_FORMATS = 76 new TreeMap<>(Collections.reverseOrder()); 77 78 private AudioManager mAudioManager; 79 80 private Button mRunTestBtn; 81 82 private boolean mClaimsHDMI; 83 private AudioDeviceInfo mHDMIDeviceInfo; 84 85 private CheckBox mClaimsHDMICheckBox; 86 private TextView mHDMISupportLbl; 87 88 private OnBtnClickListener mClickListener = new OnBtnClickListener(); 89 90 TextView mTestStatusLbl; 91 92 boolean mIsValidHal; 93 String mHalVersionStr; 94 String mInvalidHalErrorMsg; 95 HalFormats mHalFormats; 96 97 AudioDescriptor mLastTestedAudioDescriptor; 98 99 @Override onCreate(Bundle savedInstceState)100 protected void onCreate(Bundle savedInstceState) { 101 super.onCreate(savedInstceState); 102 setContentView(R.layout.audio_descriptor); 103 104 mAudioManager = getSystemService(AudioManager.class); 105 mAudioManager.registerAudioDeviceCallback(new TestAudioDeviceCallback(), null); 106 107 mRunTestBtn = (Button) findViewById(R.id.audioDescriptorRunTestBtn); 108 mRunTestBtn.setOnClickListener(new OnClickListener() { 109 @Override 110 public void onClick(View view) { 111 runTest(); 112 } 113 }); 114 115 mClaimsHDMICheckBox = (CheckBox) findViewById(R.id.audioDescriptorHasHDMICheckBox); 116 mClaimsHDMICheckBox.setOnClickListener(mClickListener); 117 mHDMISupportLbl = (TextView) findViewById(R.id.audioDescriptorHDMISupportLbl); 118 mTestStatusLbl = (TextView) findViewById(R.id.audioDescriptorTestStatusLbl); 119 120 setInfoResources(R.string.audio_descriptor_test, R.string.audio_descriptor_test_info, -1); 121 setPassFailButtonClickListeners(); 122 clearTestResult(); 123 } 124 125 @Override requiresReportLog()126 public boolean requiresReportLog() { 127 return true; 128 } 129 130 @Override getReportFileName()131 public String getReportFileName() { 132 return PassFailButtons.AUDIO_TESTS_REPORT_LOG_NAME; 133 } 134 135 @Override getReportSectionName()136 public final String getReportSectionName() { 137 return setTestNameSuffix(sCurrentDisplayMode, SECTION_AUDIODESCRIPTOR); 138 } 139 140 @Override recordTestResults()141 public void recordTestResults() { 142 CtsVerifierReportLog reportLog = getReportLog(); 143 144 reportLog.addValue( 145 KEY_CLAIMS_HDMI, 146 mClaimsHDMI, 147 ResultType.NEUTRAL, 148 ResultUnit.NONE); 149 reportLog.addValue( 150 KEY_HAL_VERSION, 151 mHalVersionStr, 152 ResultType.NEUTRAL, 153 ResultUnit.NONE); 154 Log.i(TAG, "halVersion:" + mHalVersionStr); 155 reportLog.addValue( 156 KEY_AUDIO_DESCRIPTOR, 157 mLastTestedAudioDescriptor == null ? "" : mLastTestedAudioDescriptor.toString(), 158 ResultType.NEUTRAL, 159 ResultUnit.NONE); 160 Log.i(TAG, "desc:" + mLastTestedAudioDescriptor); 161 162 reportLog.submit(); 163 } 164 detectHalVersion()165 private void detectHalVersion() { 166 try { 167 AudioHalVersionInfo halVersion = mAudioManager.getHalVersion(); 168 if (halVersion == null) { 169 mIsValidHal = false; 170 mHalVersionStr = "InvalidHalVersion"; 171 mInvalidHalErrorMsg = 172 getResources() 173 .getString( 174 R.string.audio_descriptor_invalid_hal_version, 175 mHalVersionStr); 176 return; 177 } 178 mHalVersionStr = halVersion.toString(); 179 mHalFormats = getHalFormats(halVersion); 180 mIsValidHal = true; 181 mInvalidHalErrorMsg = ""; 182 } catch (Exception e) { 183 mIsValidHal = false; 184 mInvalidHalErrorMsg = getResources().getString( 185 R.string.audio_descriptor_cannot_get_hal_version); 186 } 187 } 188 detectHDMIDevice()189 private void detectHDMIDevice() { 190 mHDMIDeviceInfo = null; 191 AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL); 192 for (AudioDeviceInfo deviceInfo : deviceInfos) { 193 Log.i(TAG, " " + deviceInfo.getProductName() + " type:" + deviceInfo.getType()); 194 if (deviceInfo.isSink() && deviceInfo.getType() == AudioDeviceInfo.TYPE_HDMI) { 195 mHDMIDeviceInfo = deviceInfo; 196 break; 197 } 198 } 199 200 if (mHDMIDeviceInfo != null) { 201 mClaimsHDMICheckBox.setChecked(true); 202 mClaimsHDMI = true; 203 } 204 if (mClaimsHDMI) { 205 mHDMISupportLbl.setText( 206 mHDMIDeviceInfo == null ? R.string.audio_descriptor_hdmi_pending 207 : R.string.audio_descriptor_hdmi_connected); 208 } else { 209 mHDMISupportLbl.setText(R.string.audio_descriptor_hdmi_NA); 210 } 211 } 212 213 private class OnBtnClickListener implements OnClickListener { 214 @Override onClick(View v)215 public void onClick(View v) { 216 int id = v.getId(); 217 if (id == R.id.audioDescriptorHasHDMICheckBox) { 218 Log.i(TAG, "HDMI check box is clicked"); 219 if (mClaimsHDMICheckBox.isChecked()) { 220 AlertDialog.Builder builder = new AlertDialog.Builder( 221 v.getContext(), android.R.style.Theme_Material_Dialog_Alert); 222 builder.setTitle(R.string.audio_descriptor_hdmi_info_title); 223 builder.setMessage(R.string.audio_descriptor_hdmi_message); 224 builder.setPositiveButton(android.R.string.ok, 225 new DialogInterface.OnClickListener() { 226 public void onClick(DialogInterface dialog, int which) { 227 } 228 }); 229 builder.setIcon(android.R.drawable.ic_dialog_alert); 230 builder.show(); 231 232 mClaimsHDMI = true; 233 } else { 234 mClaimsHDMI = false; 235 } 236 detectHDMIDevice(); 237 clearTestResult(); 238 } 239 } 240 } 241 displayTestResult()242 private void displayTestResult() { 243 if (mClaimsHDMI && mHDMIDeviceInfo == null) { 244 mHDMISupportLbl.setText(R.string.audio_descriptor_hdmi_pending); 245 getPassButton().setEnabled(false); 246 mTestStatusLbl.setText(""); 247 return; 248 } 249 if (!mIsValidHal) { 250 getPassButton().setEnabled(false); 251 mTestStatusLbl.setText(mInvalidHalErrorMsg); 252 return; 253 } 254 Pair<Boolean, String> testResult = testAudioDescriptors(); 255 getPassButton().setEnabled(testResult.first); 256 mTestStatusLbl.setText(testResult.second); 257 } 258 testAudioDescriptors()259 private Pair<Boolean, String> testAudioDescriptors() { 260 AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL); 261 for (AudioDeviceInfo device : devices) { 262 for (AudioDescriptor descriptor : device.getAudioDescriptors()) { 263 mLastTestedAudioDescriptor = descriptor; 264 Pair<Boolean, String> ret = isAudioDescriptorValid(descriptor); 265 if (!ret.first) { 266 return ret; 267 } 268 } 269 } 270 return new Pair<>(true, getResources().getString(R.string.audio_descriptor_pass)); 271 } 272 isAudioDescriptorValid(AudioDescriptor descriptor)273 private Pair<Boolean, String> isAudioDescriptorValid(AudioDescriptor descriptor) { 274 if (descriptor.getStandard() == AudioDescriptor.STANDARD_NONE) { 275 return new Pair<>( 276 false, getResources().getString(R.string.audio_descriptor_standard_none)); 277 } 278 if (descriptor.getDescriptor() == null) { 279 return new Pair<>( 280 false, getResources().getString(R.string.audio_descriptor_is_null)); 281 } 282 switch (descriptor.getStandard()) { 283 case AudioDescriptor.STANDARD_EDID: 284 return verifyShortAudioDescriptor(descriptor.getDescriptor()); 285 default: 286 return new Pair<>(false, getResources().getString( 287 R.string.audio_descriptor_unrecognized_standard, descriptor.getStandard())); 288 } 289 } 290 291 /** 292 * Verify if short audio descriptor is valid and necessary. The length of short audio descriptor 293 * must be 3. Short audio descriptor is only needed when it can not be reported by Android 294 * defined enums. 295 * 296 * @param sad a byte array of short audio descriptor 297 * @return a pair where first object indicates if the short audio descriptor is valid and 298 * necessary and the second object is the error message. 299 */ verifyShortAudioDescriptor(byte[] sad)300 private Pair<Boolean, String> verifyShortAudioDescriptor(byte[] sad) { 301 if (sad.length != 3) { 302 return new Pair<>(false, getResources().getString( 303 R.string.audio_descriptor_length_error, sad.length)); 304 } 305 306 if (!mIsValidHal) { 307 return new Pair<>(false, mInvalidHalErrorMsg); 308 } 309 310 if (mHalFormats == null) { 311 Log.i(TAG, "No HAL formats found for v" + mHalVersionStr); 312 return new Pair<>(true, getResources().getString(R.string.audio_descriptor_pass)); 313 } 314 315 // Parse according CTA-861-G, section 7.5.2. 316 final int formatCode = (sad[0] >> 3) & 0xf; 317 if (mHalFormats.getFormatCodes().containsKey(formatCode)) { 318 return new Pair<>(false, getResources().getString( 319 R.string.audio_descriptor_format_code_should_not_be_reported, 320 formatCode, 321 mHalFormats.getFormatCodes().get(formatCode))); 322 } else if (formatCode == EXTENSION_FORMAT_CODE) { 323 final int formatExtendedCode = sad[2] >> 3; 324 if (mHalFormats.getExtendedFormatCodes().containsKey(formatExtendedCode)) { 325 return new Pair<>(false, getResources().getString( 326 R.string.audio_descriptor_format_extended_code_should_not_be_reported, 327 formatExtendedCode, 328 mHalFormats.getExtendedFormatCodes().get(formatExtendedCode))); 329 } else if (NOT_USED_FORMAT_EXTENDED_CODES.contains(formatExtendedCode)) { 330 return new Pair<>(false, getResources().getString( 331 R.string.audio_descriptor_format_extended_code_is_not_used, 332 formatExtendedCode)); 333 } 334 } 335 return new Pair<>(true, getResources().getString(R.string.audio_descriptor_pass)); 336 } 337 getHalFormats(AudioHalVersionInfo halVersion)338 private HalFormats getHalFormats(AudioHalVersionInfo halVersion) { 339 for (Map.Entry<AudioHalVersionInfo, HalFormats> entry : ALL_HAL_FORMATS.entrySet()) { 340 if (halVersion.compareTo(entry.getKey()) >= 0) { 341 return entry.getValue(); 342 } 343 } 344 return null; 345 } 346 runTest()347 private void runTest() { 348 if (!canGetHalVersion()) { 349 showNonSdkAccessibilityWarningDialog(this); 350 return; 351 } 352 353 // Fill all hal formats only when the hidden APIs are available. 354 fillAllHalFormats(); 355 356 detectHDMIDevice(); 357 if (mClaimsHDMI && mHDMIDeviceInfo == null) { 358 // Do not run the test if the HDMI is claimed but not connected. 359 mTestStatusLbl.setText(R.string.audio_descriptor_hdmi_claimed_but_not_connected); 360 return; 361 } 362 detectHalVersion(); 363 displayTestResult(); 364 } 365 clearTestResult()366 private void clearTestResult() { 367 getPassButton().setEnabled(false); 368 mTestStatusLbl.setText(""); 369 } 370 canGetHalVersion()371 private boolean canGetHalVersion() { 372 Method method = null; 373 try { 374 method = AudioManager.class.getMethod("getHalVersion"); 375 } catch (Exception e) { 376 // Ignore error here 377 return false; 378 } 379 return method != null; 380 } 381 fillAllHalFormats()382 private static void fillAllHalFormats() { 383 // Formats defined by audio HAL v7.0 can be found at 384 // hardware/interfaces/audio/7.0/config/audio_policy_configuration.xsd 385 ALL_HAL_FORMATS.clear(); 386 ALL_HAL_FORMATS.put( 387 AudioHalVersionInfo.HIDL_7_0, 388 new HalFormats( 389 Map.of( 390 2, "AC-3", 391 4, "MP3", 392 6, "AAC-LC", 393 11, "DTS-HD", 394 12, "Dolby TrueHD"), 395 Map.of( 396 7, "DRA", 397 // put(11, "MPEG-H"); MPEG-H is defined by Android but its 398 // capability can only be reported by short audio descriptor. 399 12, "AC-4"))); 400 } 401 402 static class HalFormats { 403 private final Map<Integer, String> mFormatCodes; 404 private final Map<Integer, String> mExtendedFormatCodes; 405 HalFormats(Map<Integer, String> formatCodes, Map<Integer, String> extendedFormatCodes)406 HalFormats(Map<Integer, String> formatCodes, 407 Map<Integer, String> extendedFormatCodes) { 408 mFormatCodes = formatCodes; 409 mExtendedFormatCodes = extendedFormatCodes; 410 } 411 getFormatCodes()412 Map<Integer, String> getFormatCodes() { 413 return mFormatCodes; 414 } 415 getExtendedFormatCodes()416 Map<Integer, String> getExtendedFormatCodes() { 417 return mExtendedFormatCodes; 418 } 419 } 420 421 private class TestAudioDeviceCallback extends AudioDeviceCallback { onAudioDevicesAdded(AudioDeviceInfo[] addedDevices)422 public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { 423 detectHDMIDevice(); 424 } 425 onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices)426 public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { 427 detectHDMIDevice(); 428 } 429 } 430 } 431