1 /*
2  * Copyright (C) 2023 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 android.media.audio.cts;
18 
19 import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM;
20 import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
21 import static android.media.AudioAttributes.USAGE_MEDIA;
22 import static android.media.AudioManager.CSD_WARNING_MOMENTARY_EXPOSURE;
23 import static android.media.AudioManager.STREAM_MUSIC;
24 import static android.media.cts.AudioHelper.hasAudioSilentProperty;
25 
26 import android.Manifest;
27 import android.content.Context;
28 import android.content.pm.PackageManager;
29 import android.media.AudioAttributes;
30 import android.media.AudioManager;
31 import android.media.IVolumeController;
32 import android.media.SoundPool;
33 import android.os.RemoteException;
34 import android.platform.test.annotations.AppModeSdkSandbox;
35 import android.util.Log;
36 
37 import com.android.compatibility.common.util.CtsAndroidTestCase;
38 import com.android.compatibility.common.util.NonMainlineTest;
39 
40 import java.util.concurrent.atomic.AtomicInteger;
41 
42 @NonMainlineTest
43 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
44 public class SoundDoseHelperTest extends CtsAndroidTestCase {
45     private static final String TAG = "SoundDoseHelperTest";
46 
47     private static final int TEST_TIMING_TOLERANCE_MS = 100;
48     private static final int TEST_TIMEOUT_SOUNDPOOL_LOAD_MS = 3000;
49     private static final int TEST_MAX_TIME_EXPOSURE_WARNING_MS = 2500;
50 
51     private static final float DEFAULT_RS2_VALUE = 100.f;
52     private static final float MIN_RS2_VALUE = 80.f;
53     private static final float[] CUSTOM_VALID_RS2 = {80.f, 90.f, 100.f};
54     private static final float[] CUSTOM_INVALID_RS2 = {79.9f, 100.1f};
55 
56     private static final float CSD_VALUE_100PERC = 1.0f;
57 
58     private static final AudioAttributes ATTRIBUTES = new AudioAttributes.Builder()
59             .setUsage(USAGE_MEDIA)
60             .setContentType(CONTENT_TYPE_MUSIC)
61             .setAllowedCapturePolicy(ALLOW_CAPTURE_BY_SYSTEM)
62             .build();
63 
64     private final AtomicInteger mDisplayedCsdWarningTimes = new AtomicInteger(0);
65 
66     private Context mContext;
67 
68     private final IVolumeController mVolumeController = new IVolumeController.Stub() {
69         @Override
70         public void displaySafeVolumeWarning(int flags) throws RemoteException {
71             // do nothing
72         }
73 
74         @Override
75         public void volumeChanged(int streamType, int flags) throws RemoteException {
76             // do nothing
77         }
78 
79         @Override
80         public void masterMuteChanged(int flags) throws RemoteException {
81             // do nothing
82         }
83 
84         @Override
85         public void setLayoutDirection(int layoutDirection) throws RemoteException {
86             // do nothing
87         }
88 
89         @Override
90         public void dismiss() throws RemoteException {
91             // do nothing
92         }
93 
94         @Override
95         public void setA11yMode(int mode) throws RemoteException {
96             // do nothing
97         }
98 
99         @Override
100         public void displayCsdWarning(int warning, int displayDurationMs) throws RemoteException {
101             if (warning == CSD_WARNING_MOMENTARY_EXPOSURE) {
102                 mDisplayedCsdWarningTimes.incrementAndGet();
103             }
104         }
105     };
106 
107     @Override
setUp()108     protected void setUp() throws Exception {
109         super.setUp();
110         mContext = getContext();
111         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
112                 Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED,
113                 Manifest.permission.STATUS_BAR_SERVICE);
114     }
115 
116     @Override
tearDown()117     protected void tearDown() throws Exception {
118         super.tearDown();
119         getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
120     }
121 
testGetSetRs2Value()122     public void testGetSetRs2Value() throws Exception {
123         final AudioManager am = new AudioManager(mContext);
124         if (!platformSupportsSoundDose("testGetSetRs2Value", am)) {
125             return;
126         }
127 
128         float prevRS2Value = am.getRs2Value();
129 
130         for (float rs2Value : CUSTOM_INVALID_RS2) {
131             am.setRs2Value(rs2Value);
132             Thread.sleep(TEST_TIMING_TOLERANCE_MS);  // waiting for RS2 to propagate
133             assertEquals(DEFAULT_RS2_VALUE, am.getRs2Value());
134         }
135 
136         for (float rs2Value : CUSTOM_VALID_RS2) {
137             am.setRs2Value(rs2Value);
138             Thread.sleep(TEST_TIMING_TOLERANCE_MS);  // waiting for RS2 to propagate
139             assertEquals(rs2Value, am.getRs2Value());
140         }
141 
142         // Restore the RS2 value
143         am.setRs2Value(prevRS2Value);
144     }
145 
testGetSetCsd()146     public void testGetSetCsd() throws Exception {
147         final AudioManager am = new AudioManager(mContext);
148         if (!platformSupportsSoundDose("testGetSetCsd", am)) {
149             return;
150         }
151 
152         am.setCsd(CSD_VALUE_100PERC);
153         Thread.sleep(TEST_TIMING_TOLERANCE_MS);  // waiting for CSD to propagate
154         assertEquals(CSD_VALUE_100PERC, am.getCsd());
155     }
156 
testFrameworkMomentaryExposure()157     public void testFrameworkMomentaryExposure() throws Exception {
158         final AudioManager am = new AudioManager(mContext);
159         if (!platformSupportsSoundDose("testFrameworkMomentaryExposure", am)) {
160             return;
161         }
162         if (hasAudioSilentProperty()) {
163             Log.w(TAG, "Device has ro.audio.silent set, skipping testFrameworkMomentaryExposure");
164             return;
165         }
166 
167         am.forceComputeCsdOnAllDevices(/* computeCsdOnAllDevices= */true);
168         am.forceUseFrameworkMel(/* useFrameworkMel= */true);
169         am.setCsd(-1.f);  // reset csd timeouts
170         am.setRs2Value(MIN_RS2_VALUE);  // lower the RS2 as much as possible
171 
172         IVolumeController sysUiVolumeController = null;
173         int prevVolume = -1;
174         try {
175             sysUiVolumeController = am.getVolumeController();
176             prevVolume = am.getStreamVolume(STREAM_MUSIC);
177             am.setVolumeController(mVolumeController);
178 
179             playLoudSound(am);
180 
181             Thread.sleep(TEST_MAX_TIME_EXPOSURE_WARNING_MS);
182             assertTrue("Exposure warning should have been triggered once!",
183                     mDisplayedCsdWarningTimes.get() > 0);
184         } finally {
185             if (prevVolume != -1) {
186                 // restore the previous volume
187                 am.setStreamVolume(STREAM_MUSIC, prevVolume, /* flags= */0);
188             }
189             if (sysUiVolumeController != null) {
190                 // restore SysUI volume controller
191                 am.setVolumeController(sysUiVolumeController);
192             }
193             am.setRs2Value(DEFAULT_RS2_VALUE);  // restore RS2 to default
194             am.forceComputeCsdOnAllDevices(/* computeCsdOnAllDevices= */false);
195             am.forceUseFrameworkMel(/* useFrameworkMel= */false);
196         }
197     }
198 
playLoudSound(AudioManager am)199     private void playLoudSound(AudioManager am) throws Exception {
200         int maxVolume = am.getStreamMaxVolume(STREAM_MUSIC);
201         am.setStreamVolume(STREAM_MUSIC, maxVolume, /* flags= */0);
202 
203         final Object loadLock = new Object();
204         final SoundPool soundpool = new SoundPool.Builder()
205                 .setAudioAttributes(ATTRIBUTES)
206                 .setMaxStreams(1)
207                 .build();
208         // load a sound and play it once load completion is reported
209         soundpool.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
210             assertEquals("Load completion error", 0 /*success expected*/, status);
211             synchronized (loadLock) {
212                 loadLock.notify();
213             }
214         });
215         final int loadId = soundpool.load(mContext, R.raw.sine1320hz5sec, 1/*priority*/);
216         synchronized (loadLock) {
217             loadLock.wait(TEST_TIMEOUT_SOUNDPOOL_LOAD_MS);
218         }
219 
220         int res = soundpool.play(loadId, 1.0f /*leftVolume*/, 1.0f /*rightVolume*/, 1 /*priority*/,
221                 0 /*loop*/, 1.0f/*rate*/);
222         assertTrue("Error playing sound through SoundPool", res > 0);
223     }
224 
platformSupportsSoundDose(String testName, AudioManager am)225     private boolean platformSupportsSoundDose(String testName, AudioManager am) {
226         if (!mContext.getPackageManager()
227                 .hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) {
228             Log.w(TAG, "AUDIO_OUTPUT feature not found. This system might not have a valid "
229                     + "audio output HAL, skipping test " + testName);
230             return false;
231         }
232 
233         if (!am.isCsdEnabled()) {
234             Log.w(TAG, "Device does not have the sound dose feature enabled, skipping test "
235                     + testName);
236             return false;
237         }
238 
239         return true;
240     }
241 }
242