aboutsummaryrefslogtreecommitdiffstats
path: root/tests/src/org/lineageos/tests/media/unit/LineageAudioManagerTest.java
blob: e35d3c883dfaa77f2345ad6d1a05389a00b0807c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/*
 * Copyright (C) 2016 The CyanogenMod 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 org.lineageos.tests.media.unit;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;

import org.junit.Assume;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import lineageos.app.LineageContextConstants;
import lineageos.media.AudioSessionInfo;
import lineageos.media.LineageAudioManager;
import lineageos.media.ILineageAudioService;

public class LineageAudioManagerTest extends AndroidTestCase {

    private static final String TAG = "LineageAudioManagerTest";

    private LineageAudioManager mLineageAudioManager;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        Assume.assumeTrue(mContext.getPackageManager().hasSystemFeature(
                LineageContextConstants.Features.AUDIO));

        mLineageAudioManager = LineageAudioManager.getInstance(mContext);
    }

    /**
     * EXPECT: The platform should return a valid manager instance.
     */
    @SmallTest
    public void testManagerExists() {
        assertNotNull(mLineageAudioManager);
    }

    /**
     * EXPECT: The service in the manager should also be valid.
     */
    @SmallTest
    public void testManagerServiceIsAvailable() {
        ILineageAudioService service = LineageAudioManager.getService();
        assertNotNull(service);
    }

    /**
     * EXPECT: listAudioSessions should be populated when a new stream is
     * created, and it's session ID should match what AudioTrack says it is.
     *
     * We simply create an audio track, and query the policy for sessions.
     */
    @SmallTest
    public void testSessionList() {

        AudioTrack track = createTestTrack();
        int session = track.getAudioSessionId();

        AudioSessionInfo info = findAudioSessionInfo(session);
        assertNotNull(info);
        assertEquals(session, info.getSessionId());
        assertEquals(3, info.getChannelMask());

        track.release();

        info = findAudioSessionInfo(session);
        assertNull(info);
    }

    /**
     * EXPECT: LineageAudioManager.ACTION_AUDIO_SESSIONS_CHANGED should be broadcast when
     * audio sessions are opened and closed.
     *
     * We register a receiver for the broadcast, create an AudioTrack, and wait to
     * observe both the open and close session events. The info in the returned
     * AudioSessionInfo should match our expectations.
     */
    @SmallTest
    public void testSessionInfoBroadcast() throws Exception {

        IntentFilter filter = new IntentFilter(LineageAudioManager.ACTION_AUDIO_SESSIONS_CHANGED);
        AudioSessionReceiver receiver = new AudioSessionReceiver(2);
        mContext.registerReceiver(receiver, filter);

        AudioTrack track = createTestTrack();
        track.play();
        track.release();

        receiver.waitForSessions();

        mContext.unregisterReceiver(receiver);

        assertEquals(1, receiver.getNumAdded());
        assertEquals(1, receiver.getNumRemoved());

        assertEquals(1, receiver.getSessions().size());

        AudioSessionInfo info = receiver.getSessions().get(0);
        assertNotNull(info);
        assertNotNull(info.toString());
        assertEquals(track.getAudioSessionId(), info.getSessionId());
        assertEquals(3, info.getChannelMask());
        assertEquals(AudioManager.STREAM_MUSIC, info.getStream());

    }

    private static final int SESSIONS = 50;

    /**
     * EXPECT: The ACTION_AUDIO_SESSIONS_CHANGED broadcast and associated backend should
     * be resilent to multithreaded and/or aggressive/destructive usage. A single add
     * and a single remove event should be broadcast for the lifecycle of a stream.
     *
     * We register a receiver for session events, spawn a small thread pool, and create
     * up to SESSIONS AudioTracks and play + release them on the thread. A small delay
     * is inserted to prevent AudioFlinger from running out of tracks. Once the expected
     * number of sessions arrives, we verify our expectation regarding event counts,
     * and additionally verify that all the session ids we saw when creating our
     * AudioTracks were returned in the AudioSessionInfo broadcasts.
     */
    @SmallTest
    public void testSymphonyOfDestruction() throws Exception {
        IntentFilter filter = new IntentFilter(LineageAudioManager.ACTION_AUDIO_SESSIONS_CHANGED);
        AudioSessionReceiver receiver = new AudioSessionReceiver(SESSIONS * 2);
        mContext.registerReceiver(receiver, filter);

        final List<Integer> sessions = new ArrayList<Integer>();

        ExecutorService sexecutioner = Executors.newFixedThreadPool(5);
        for (int i = 0; i < SESSIONS; i++) {
            sexecutioner.submit(new Runnable() {
                @Override
                public void run() {
                    AudioTrack track = createTestTrack();
                    synchronized (sessions) {
                        sessions.add(track.getAudioSessionId());
                    }
                    track.play();
                    track.release();
                }
            });
            if ((i % 2) == 0) {
                Thread.sleep(100);
            }
        }

        receiver.waitForSessions();
        sexecutioner.shutdown();

        assertEquals(SESSIONS, sessions.size());
        assertEquals(SESSIONS, receiver.getNumAdded());
        assertEquals(SESSIONS, receiver.getNumRemoved());

        for (AudioSessionInfo info : receiver.getSessions()) {
            assertTrue(sessions.contains(info.getSessionId()));
        }
    }

    private static class AudioSessionReceiver extends BroadcastReceiver {

        private int mAdded = 0;
        private int mRemoved = 0;

        private final CountDownLatch mLatch;

        private List<AudioSessionInfo> mSessions = new ArrayList<AudioSessionInfo>();

        public AudioSessionReceiver(int count) {
            mLatch = new CountDownLatch(count);
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            assertNotNull(intent);

            boolean added = intent.getBooleanExtra(LineageAudioManager.EXTRA_SESSION_ADDED, false);

            AudioSessionInfo info = intent.getParcelableExtra(LineageAudioManager.EXTRA_SESSION_INFO);
            Log.d(TAG, "onReceive: " + info);

            assertNotNull(info);

            synchronized (mSessions) {
                if (added) {
                    mAdded++;
                    mSessions.add(info);
                } else {
                    mRemoved++;
                }
            }

            mLatch.countDown();
        }

        public int getNumAdded() {
            return mAdded;
        }

        public int getNumRemoved() {
            return mRemoved;
        }

        public List<AudioSessionInfo> getSessions() {
            return mSessions;
        }

        public void waitForSessions() throws InterruptedException {
            mLatch.await(60, TimeUnit.SECONDS);
        }
    };

    private AudioSessionInfo findAudioSessionInfo(int sessionId) {
        List<AudioSessionInfo> infos = mLineageAudioManager.listAudioSessions(AudioManager.STREAM_MUSIC);
        for (AudioSessionInfo info : infos) {
            if (info.getSessionId() == sessionId) {
                return info;
            }
        }
        return null;
    }

    private AudioTrack createTestTrack() {
        int bytes = 2 * 44100 / 1000;
        AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC, 44100,
                AudioFormat.CHANNEL_OUT_STEREO,
                AudioFormat.ENCODING_PCM_16BIT,
                bytes,
                AudioTrack.STATE_INITIALIZED);
        return track;
    }
}