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.audiopolicytest;
18 
19 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assume.assumeNoException;
24 import static org.junit.Assume.assumeTrue;
25 
26 import android.content.BroadcastReceiver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.pm.PackageManager;
31 import android.media.AudioAttributes;
32 import android.media.AudioFormat;
33 import android.media.AudioManager;
34 import android.media.AudioTrack;
35 import android.os.Bundle;
36 import android.os.RemoteCallback;
37 import android.platform.test.annotations.Presubmit;
38 import android.util.Log;
39 
40 import androidx.test.ext.junit.runners.AndroidJUnit4;
41 
42 import org.junit.Before;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 
46 import java.util.concurrent.CompletableFuture;
47 import java.util.concurrent.CountDownLatch;
48 import java.util.concurrent.ExecutionException;
49 import java.util.concurrent.TimeUnit;
50 import java.util.concurrent.TimeoutException;
51 
52 @Presubmit
53 @RunWith(AndroidJUnit4.class)
54 public class AudioPolicyDeathTest {
55     private static final String TAG = "AudioPolicyDeathTest";
56 
57     private static final int SAMPLE_RATE = 48000;
58     private static final int PLAYBACK_TIME_MS = 4000;
59     private static final int RECORD_TIME_MS = 1000;
60     private static final int ACTIVITY_TIMEOUT_SEC = 5;
61     private static final int BROADCAST_TIMEOUT_SEC = 10;
62     private static final int MAX_ATTEMPTS = 5;
63     private static final int DELAY_BETWEEN_ATTEMPTS_MS = 2000;
64 
65     private static final IntentFilter AUDIO_NOISY_INTENT_FILTER =
66             new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
67 
68     private class MyBroadcastReceiver extends BroadcastReceiver {
69         private CountDownLatch mLatch = new CountDownLatch(1);
70 
71         @Override
onReceive(Context context, Intent intent)72         public void onReceive(Context context, Intent intent) {
73             if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
74                 mLatch.countDown();
75             }
76         }
77 
reset()78         public void reset() {
79             mLatch = new CountDownLatch(1);
80         }
81 
waitForBroadcast()82         public boolean waitForBroadcast() {
83             boolean received = false;
84             long startTimeMs = System.currentTimeMillis();
85             long elapsedTimeMs = 0;
86 
87             Log.i(TAG, "waiting for broadcast");
88 
89             while (elapsedTimeMs < BROADCAST_TIMEOUT_SEC && !received) {
90                 try {
91                     received = mLatch.await(BROADCAST_TIMEOUT_SEC, TimeUnit.SECONDS);
92                 } catch (InterruptedException e) {
93                     Log.w(TAG, "wait interrupted");
94                 }
95                 elapsedTimeMs = System.currentTimeMillis() - startTimeMs;
96             }
97             Log.i(TAG, "broadcast " + (received ? "" : "NOT ") + "received");
98             return received;
99         }
100     }
101 
102     private final MyBroadcastReceiver mReceiver = new MyBroadcastReceiver();
103 
104     private Context mContext;
105 
106     @Before
setUp()107     public void setUp() {
108         mContext = getApplicationContext();
109         assertEquals(PackageManager.PERMISSION_GRANTED,
110                 mContext.checkSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING));
111     }
112 
113     //-----------------------------------------------------------------
114     // Tests that an AUDIO_BECOMING_NOISY intent is broadcast when an app having registered
115     // a dynamic audio policy that intercepts an active media playback dies
116     //-----------------------------------------------------------------
117     @Test
testPolicyClientDeathSendBecomingNoisyIntent()118     public void testPolicyClientDeathSendBecomingNoisyIntent() {
119         mContext.registerReceiver(mReceiver, AUDIO_NOISY_INTENT_FILTER);
120 
121         boolean result = false;
122         for (int numAttempts = 1; numAttempts <= MAX_ATTEMPTS && !result; numAttempts++) {
123             mReceiver.reset();
124 
125             CompletableFuture<Integer> callbackReturn = new CompletableFuture<>();
126             RemoteCallback cb = new RemoteCallback((Bundle res) -> {
127                 callbackReturn.complete(
128                         res.getInt(mContext.getResources().getString(R.string.status_key)));
129             });
130 
131             // Launch process registering a dynamic auido policy and dying after RECORD_TIME_MS ms
132             // RECORD_TIME_MS must be shorter than PLAYBACK_TIME_MS
133             Intent intent = new Intent(mContext, AudioPolicyDeathTestActivity.class);
134             intent.putExtra(mContext.getResources().getString(R.string.capture_duration_key),
135                     RECORD_TIME_MS);
136             intent.putExtra(mContext.getResources().getString(R.string.callback_key), cb);
137             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
138 
139             mContext.startActivity(intent);
140 
141             Integer status = AudioManager.ERROR;
142             try {
143                 status = callbackReturn.get(ACTIVITY_TIMEOUT_SEC, TimeUnit.SECONDS);
144             } catch (InterruptedException | ExecutionException | TimeoutException e) {
145                 assumeNoException(e);
146             }
147             assumeTrue(status != null && status == AudioManager.SUCCESS);
148 
149             Log.i(TAG, "Activity started");
150             AudioTrack track = null;
151             try {
152                 track = createAudioTrack();
153                 track.play();
154                 result = mReceiver.waitForBroadcast();
155             } finally {
156                 if (track != null) {
157                     track.stop();
158                     track.release();
159                 }
160             }
161             if (!result) {
162                 try {
163                     Log.i(TAG, "Retrying after attempt: " + numAttempts);
164                     Thread.sleep(DELAY_BETWEEN_ATTEMPTS_MS);
165                 } catch (InterruptedException e) {
166                 }
167             }
168         }
169         assertTrue(result);
170     }
171 
createAudioTrack()172     private AudioTrack createAudioTrack() {
173         AudioFormat format = new AudioFormat.Builder()
174                 .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
175                 .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
176                 .setSampleRate(SAMPLE_RATE)
177                 .build();
178 
179         short[] data = new short[PLAYBACK_TIME_MS * SAMPLE_RATE * format.getChannelCount() / 1000];
180         AudioAttributes attributes =
181                 new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
182 
183         AudioTrack track = new AudioTrack(attributes, format, data.length,
184                 AudioTrack.MODE_STATIC, AudioManager.AUDIO_SESSION_ID_GENERATE);
185         track.write(data, 0, data.length);
186 
187         return track;
188     }
189 }
190