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