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 com.android.loudnesscodecapitest;
18 
19 import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertThrows;
24 import static org.junit.Assert.assertTrue;
25 import static org.mockito.ArgumentMatchers.any;
26 import static org.mockito.ArgumentMatchers.anyInt;
27 import static org.mockito.ArgumentMatchers.eq;
28 import static org.mockito.Mockito.verify;
29 import static org.mockito.Mockito.when;
30 
31 import android.content.Context;
32 import android.content.res.AssetFileDescriptor;
33 import android.media.AudioManager;
34 import android.media.IAudioService;
35 import android.media.LoudnessCodecController;
36 import android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener;
37 import android.media.MediaCodec;
38 import android.media.MediaExtractor;
39 import android.media.MediaFormat;
40 import android.os.PersistableBundle;
41 import android.platform.test.annotations.Presubmit;
42 import android.platform.test.annotations.RequiresFlagsEnabled;
43 import android.platform.test.flag.junit.CheckFlagsRule;
44 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
45 import android.util.Log;
46 
47 import androidx.test.ext.junit.runners.AndroidJUnit4;
48 import androidx.test.platform.app.InstrumentationRegistry;
49 
50 import org.junit.Before;
51 import org.junit.Rule;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 import org.mockito.Mock;
55 import org.mockito.junit.MockitoJUnit;
56 import org.mockito.junit.MockitoRule;
57 
58 import java.util.concurrent.Executors;
59 
60 /**
61  * Unit tests for {@link LoudnessCodecController} checking the internal interactions with a mocked
62  * {@link IAudioService} without any real IPC interactions.
63  */
64 @Presubmit
65 @RunWith(AndroidJUnit4.class)
66 public class LoudnessCodecControllerTest {
67     private static final String TAG = "LoudnessCodecConfiguratorTest";
68 
69     private static final String TEST_MEDIA_AUDIO_CODEC_PREFIX = "audio/";
70     private static final int TEST_AUDIO_TRACK_BUFFER_SIZE = 2048;
71     private static final int TEST_AUDIO_TRACK_SAMPLERATE = 48000;
72     private static final int TEST_AUDIO_TRACK_CHANNELS = 2;
73 
74     @Rule
75     public final MockitoRule mockito = MockitoJUnit.rule();
76     @Rule
77     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
78 
79     @Mock
80     private IAudioService mAudioService;
81 
82     private LoudnessCodecController mLcc;
83 
84     private int mSessionId;
85 
86     @Before
setUp()87     public void setUp() {
88         final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
89         final AudioManager audioManager = (AudioManager) context.getSystemService(
90                 AudioManager.class);
91         mSessionId = 0;
92         if (audioManager != null) {
93             mSessionId = audioManager.generateAudioSessionId();
94         }
95         mLcc = LoudnessCodecController.createForTesting(mSessionId,
96                 Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {
97                 }, mAudioService);
98     }
99 
100     @Test
101     @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
createLcc_callsAudioServiceStart()102     public void createLcc_callsAudioServiceStart() throws Exception {
103         verify(mAudioService).startLoudnessCodecUpdates(eq(mSessionId));
104     }
105 
106     @Test
107     @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
getLoudnessCodecParams_callsAudioServiceGetLoudness()108     public void getLoudnessCodecParams_callsAudioServiceGetLoudness() throws Exception {
109         when(mAudioService.getLoudnessParams(any())).thenReturn(new PersistableBundle());
110         final MediaCodec mediaCodec = createAndConfigureMediaCodec();
111 
112         try {
113             mLcc.addMediaCodec(mediaCodec);
114             mLcc.getLoudnessCodecParams(mediaCodec);
115 
116             verify(mAudioService).getLoudnessParams(any());
117         } finally {
118             mediaCodec.release();
119         }
120     }
121 
122     @Test
123     @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
release_stopCodecUpdates()124     public void release_stopCodecUpdates() throws Exception {
125         final MediaCodec mediaCodec = createAndConfigureMediaCodec();
126 
127         try {
128             mLcc.addMediaCodec(mediaCodec);
129             mLcc.close();  // stops updates
130 
131             verify(mAudioService).stopLoudnessCodecUpdates(eq(mSessionId));
132         } finally {
133             mediaCodec.release();
134         }
135     }
136 
137     @Test
138     @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
addMediaCodecTwice_triggersIAE()139     public void addMediaCodecTwice_triggersIAE() throws Exception {
140         final MediaCodec mediaCodec = createAndConfigureMediaCodec();
141 
142         try {
143             mLcc.addMediaCodec(mediaCodec);
144 
145             assertThrows(IllegalArgumentException.class, () -> mLcc.addMediaCodec(mediaCodec));
146         } finally {
147             mediaCodec.release();
148         }
149     }
150 
151     @Test
152     @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
addUnconfiguredMediaCodec_returnsFalse()153     public void addUnconfiguredMediaCodec_returnsFalse() throws Exception {
154         final MediaCodec mediaCodec = MediaCodec.createDecoderByType("audio/mpeg");
155 
156         try {
157             assertFalse(mLcc.addMediaCodec(mediaCodec));
158         } finally {
159             mediaCodec.release();
160         }
161     }
162 
163     @Test
164     @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
removeAddedMediaCodecAfterSetTrack_callsAudioServiceRemoveCodec()165     public void removeAddedMediaCodecAfterSetTrack_callsAudioServiceRemoveCodec() throws Exception {
166         final MediaCodec mediaCodec = createAndConfigureMediaCodec();
167 
168         try {
169             mLcc.addMediaCodec(mediaCodec);
170             mLcc.removeMediaCodec(mediaCodec);
171 
172             verify(mAudioService).removeLoudnessCodecInfo(eq(mSessionId), any());
173         } finally {
174             mediaCodec.release();
175         }
176     }
177 
178     @Test
179     @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
addMediaCodec_callsAudioServiceAdd()180     public void addMediaCodec_callsAudioServiceAdd() throws Exception {
181         final MediaCodec mediaCodec = createAndConfigureMediaCodec();
182 
183         try {
184             mLcc.addMediaCodec(mediaCodec);
185             verify(mAudioService).addLoudnessCodecInfo(eq(mSessionId), anyInt(), any());
186         } finally {
187             mediaCodec.release();
188         }
189     }
190 
191     @Test
192     @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
removeMediaCodec_callsAudioServiceRemove()193     public void removeMediaCodec_callsAudioServiceRemove() throws Exception {
194         final MediaCodec mediaCodec = createAndConfigureMediaCodec();
195 
196         try {
197             mLcc.addMediaCodec(mediaCodec);
198             verify(mAudioService).addLoudnessCodecInfo(eq(mSessionId), anyInt(), any());
199 
200             mLcc.removeMediaCodec(mediaCodec);
201             verify(mAudioService).removeLoudnessCodecInfo(eq(mSessionId), any());
202         } finally {
203             mediaCodec.release();
204         }
205     }
206 
207     @Test
208     @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
removeWrongMediaCodec_triggersIAE()209     public void removeWrongMediaCodec_triggersIAE() throws Exception {
210         final MediaCodec mediaCodec1 = createAndConfigureMediaCodec();
211         final MediaCodec mediaCodec2 = createAndConfigureMediaCodec();
212 
213         try {
214             mLcc.addMediaCodec(mediaCodec1);
215             verify(mAudioService).addLoudnessCodecInfo(eq(mSessionId), anyInt(), any());
216 
217             assertThrows(IllegalArgumentException.class,
218                     () -> mLcc.removeMediaCodec(mediaCodec2));
219         } finally {
220             mediaCodec1.release();
221             mediaCodec2.release();
222         }
223     }
224 
createAndConfigureMediaCodec()225     private MediaCodec createAndConfigureMediaCodec() throws Exception {
226         AssetFileDescriptor testFd = InstrumentationRegistry.getInstrumentation().getContext()
227                 .getResources()
228                 .openRawResourceFd(R.raw.noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4);
229 
230         MediaExtractor extractor;
231         extractor = new MediaExtractor();
232         try {
233             extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
234                     testFd.getLength());
235             assertEquals("wrong number of tracks", 1, extractor.getTrackCount());
236             MediaFormat format = extractor.getTrackFormat(0);
237             String mime = format.getString(MediaFormat.KEY_MIME);
238             assertTrue("not an audio file", mime.startsWith(TEST_MEDIA_AUDIO_CODEC_PREFIX));
239             final MediaCodec mediaCodec = MediaCodec.createDecoderByType(mime);
240 
241             Log.v(TAG, "configuring with " + format);
242             mediaCodec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
243             return mediaCodec;
244         } finally {
245             testFd.close();
246             extractor.release();
247         }
248     }
249 }
250