1/*
2 *  Copyright 2016 The WebRTC project authors. All Rights Reserved.
3 *
4 *  Use of this source code is governed by a BSD-style license
5 *  that can be found in the LICENSE file in the root of the source
6 *  tree. An additional intellectual property rights grant can be found
7 *  in the file PATENTS.  All contributing project authors may
8 *  be found in the AUTHORS file in the root of the source tree.
9 */
10
11#import <Foundation/Foundation.h>
12#import <OCMock/OCMock.h>
13
14#include <vector>
15
16#include "rtc_base/gunit.h"
17
18#import "components/audio/RTCAudioSession+Private.h"
19
20#import "components/audio/RTCAudioSession.h"
21#import "components/audio/RTCAudioSessionConfiguration.h"
22
23@interface RTCAudioSession (UnitTesting)
24
25@property(nonatomic, readonly) std::vector<__weak id<RTCAudioSessionDelegate> > delegates;
26
27- (instancetype)initWithAudioSession:(id)audioSession;
28
29@end
30
31@interface MockAVAudioSession : NSObject
32
33@property (nonatomic, readwrite, assign) float outputVolume;
34
35@end
36
37@implementation MockAVAudioSession
38@synthesize outputVolume = _outputVolume;
39@end
40
41@interface RTCAudioSessionTestDelegate : NSObject <RTCAudioSessionDelegate>
42
43@property (nonatomic, readonly) float outputVolume;
44
45@end
46
47@implementation RTCAudioSessionTestDelegate
48
49@synthesize outputVolume = _outputVolume;
50
51- (instancetype)init {
52  if (self = [super init]) {
53    _outputVolume = -1;
54  }
55  return self;
56}
57
58- (void)audioSessionDidBeginInterruption:(RTCAudioSession *)session {
59}
60
61- (void)audioSessionDidEndInterruption:(RTCAudioSession *)session
62                   shouldResumeSession:(BOOL)shouldResumeSession {
63}
64
65- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
66           reason:(AVAudioSessionRouteChangeReason)reason
67    previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
68}
69
70- (void)audioSessionMediaServerTerminated:(RTCAudioSession *)session {
71}
72
73- (void)audioSessionMediaServerReset:(RTCAudioSession *)session {
74}
75
76- (void)audioSessionShouldConfigure:(RTCAudioSession *)session {
77}
78
79- (void)audioSessionShouldUnconfigure:(RTCAudioSession *)session {
80}
81
82- (void)audioSession:(RTCAudioSession *)audioSession
83    didChangeOutputVolume:(float)outputVolume {
84  _outputVolume = outputVolume;
85}
86
87@end
88
89// A delegate that adds itself to the audio session on init and removes itself
90// in its dealloc.
91@interface RTCTestRemoveOnDeallocDelegate : RTCAudioSessionTestDelegate
92@end
93
94@implementation RTCTestRemoveOnDeallocDelegate
95
96- (instancetype)init {
97  if (self = [super init]) {
98    RTCAudioSession *session = [RTCAudioSession sharedInstance];
99    [session addDelegate:self];
100  }
101  return self;
102}
103
104- (void)dealloc {
105  RTCAudioSession *session = [RTCAudioSession sharedInstance];
106  [session removeDelegate:self];
107}
108
109@end
110
111
112@interface RTCAudioSessionTest : NSObject
113
114- (void)testLockForConfiguration;
115
116@end
117
118@implementation RTCAudioSessionTest
119
120- (void)testLockForConfiguration {
121  RTCAudioSession *session = [RTCAudioSession sharedInstance];
122
123  for (size_t i = 0; i < 2; i++) {
124    [session lockForConfiguration];
125    EXPECT_TRUE(session.isLocked);
126  }
127  for (size_t i = 0; i < 2; i++) {
128    EXPECT_TRUE(session.isLocked);
129    [session unlockForConfiguration];
130  }
131  EXPECT_FALSE(session.isLocked);
132}
133
134- (void)testAddAndRemoveDelegates {
135  RTCAudioSession *session = [RTCAudioSession sharedInstance];
136  NSMutableArray *delegates = [NSMutableArray array];
137  const size_t count = 5;
138  for (size_t i = 0; i < count; ++i) {
139    RTCAudioSessionTestDelegate *delegate =
140        [[RTCAudioSessionTestDelegate alloc] init];
141    [session addDelegate:delegate];
142    [delegates addObject:delegate];
143    EXPECT_EQ(i + 1, session.delegates.size());
144  }
145  [delegates enumerateObjectsUsingBlock:^(RTCAudioSessionTestDelegate *obj,
146                                          NSUInteger idx,
147                                          BOOL *stop) {
148    [session removeDelegate:obj];
149  }];
150  EXPECT_EQ(0u, session.delegates.size());
151}
152
153- (void)testPushDelegate {
154  RTCAudioSession *session = [RTCAudioSession sharedInstance];
155  NSMutableArray *delegates = [NSMutableArray array];
156  const size_t count = 2;
157  for (size_t i = 0; i < count; ++i) {
158    RTCAudioSessionTestDelegate *delegate =
159        [[RTCAudioSessionTestDelegate alloc] init];
160    [session addDelegate:delegate];
161    [delegates addObject:delegate];
162  }
163  // Test that it gets added to the front of the list.
164  RTCAudioSessionTestDelegate *pushedDelegate =
165      [[RTCAudioSessionTestDelegate alloc] init];
166  [session pushDelegate:pushedDelegate];
167  EXPECT_TRUE(pushedDelegate == session.delegates[0]);
168
169  // Test that it stays at the front of the list.
170  for (size_t i = 0; i < count; ++i) {
171    RTCAudioSessionTestDelegate *delegate =
172        [[RTCAudioSessionTestDelegate alloc] init];
173    [session addDelegate:delegate];
174    [delegates addObject:delegate];
175  }
176  EXPECT_TRUE(pushedDelegate == session.delegates[0]);
177
178  // Test that the next one goes to the front too.
179  pushedDelegate = [[RTCAudioSessionTestDelegate alloc] init];
180  [session pushDelegate:pushedDelegate];
181  EXPECT_TRUE(pushedDelegate == session.delegates[0]);
182}
183
184// Tests that delegates added to the audio session properly zero out. This is
185// checking an implementation detail (that vectors of __weak work as expected).
186- (void)testZeroingWeakDelegate {
187  RTCAudioSession *session = [RTCAudioSession sharedInstance];
188  @autoreleasepool {
189    // Add a delegate to the session. There should be one delegate at this
190    // point.
191    RTCAudioSessionTestDelegate *delegate =
192        [[RTCAudioSessionTestDelegate alloc] init];
193    [session addDelegate:delegate];
194    EXPECT_EQ(1u, session.delegates.size());
195    EXPECT_TRUE(session.delegates[0]);
196  }
197  // The previously created delegate should've de-alloced, leaving a nil ptr.
198  EXPECT_FALSE(session.delegates[0]);
199  RTCAudioSessionTestDelegate *delegate =
200      [[RTCAudioSessionTestDelegate alloc] init];
201  [session addDelegate:delegate];
202  // On adding a new delegate, nil ptrs should've been cleared.
203  EXPECT_EQ(1u, session.delegates.size());
204  EXPECT_TRUE(session.delegates[0]);
205}
206
207// Tests that we don't crash when removing delegates in dealloc.
208// Added as a regression test.
209- (void)testRemoveDelegateOnDealloc {
210  @autoreleasepool {
211    RTCTestRemoveOnDeallocDelegate *delegate =
212        [[RTCTestRemoveOnDeallocDelegate alloc] init];
213    EXPECT_TRUE(delegate);
214  }
215  RTCAudioSession *session = [RTCAudioSession sharedInstance];
216  EXPECT_EQ(0u, session.delegates.size());
217}
218
219- (void)testAudioSessionActivation {
220  RTCAudioSession *audioSession = [RTCAudioSession sharedInstance];
221  EXPECT_EQ(0, audioSession.activationCount);
222  [audioSession audioSessionDidActivate:[AVAudioSession sharedInstance]];
223  EXPECT_EQ(1, audioSession.activationCount);
224  [audioSession audioSessionDidDeactivate:[AVAudioSession sharedInstance]];
225  EXPECT_EQ(0, audioSession.activationCount);
226}
227
228// Hack - fixes OCMVerify link error
229// Link error is: Undefined symbols for architecture i386:
230// "OCMMakeLocation(objc_object*, char const*, int)", referenced from:
231// -[RTCAudioSessionTest testConfigureWebRTCSession] in RTCAudioSessionTest.o
232// ld: symbol(s) not found for architecture i386
233// REASON: https://github.com/erikdoe/ocmock/issues/238
234OCMLocation *OCMMakeLocation(id testCase, const char *fileCString, int line){
235  return [OCMLocation locationWithTestCase:testCase
236                                      file:[NSString stringWithUTF8String:fileCString]
237                                      line:line];
238}
239
240- (void)testConfigureWebRTCSession {
241  NSError *error = nil;
242
243  void (^setActiveBlock)(NSInvocation *invocation) = ^(NSInvocation *invocation) {
244    __autoreleasing NSError **retError;
245    [invocation getArgument:&retError atIndex:4];
246    *retError = [NSError errorWithDomain:@"AVAudioSession"
247                                    code:AVAudioSessionErrorInsufficientPriority
248                                userInfo:nil];
249    BOOL failure = NO;
250    [invocation setReturnValue:&failure];
251  };
252
253  id mockAVAudioSession = OCMPartialMock([AVAudioSession sharedInstance]);
254  OCMStub([[mockAVAudioSession ignoringNonObjectArgs]
255      setActive:YES withOptions:0 error:((NSError __autoreleasing **)[OCMArg anyPointer])]).
256      andDo(setActiveBlock);
257
258  id mockAudioSession = OCMPartialMock([RTCAudioSession sharedInstance]);
259  OCMStub([mockAudioSession session]).andReturn(mockAVAudioSession);
260
261  RTCAudioSession *audioSession = mockAudioSession;
262  EXPECT_EQ(0, audioSession.activationCount);
263  [audioSession lockForConfiguration];
264  EXPECT_TRUE([audioSession checkLock:nil]);
265  // configureWebRTCSession is forced to fail in the above mock interface,
266  // so activationCount should remain 0
267  OCMExpect([[mockAVAudioSession ignoringNonObjectArgs]
268      setActive:YES withOptions:0 error:((NSError __autoreleasing **)[OCMArg anyPointer])]).
269      andDo(setActiveBlock);
270  OCMExpect([mockAudioSession session]).andReturn(mockAVAudioSession);
271  EXPECT_FALSE([audioSession configureWebRTCSession:&error]);
272  EXPECT_EQ(0, audioSession.activationCount);
273
274  id session = audioSession.session;
275  EXPECT_EQ(session, mockAVAudioSession);
276  EXPECT_EQ(NO, [mockAVAudioSession setActive:YES withOptions:0 error:&error]);
277  [audioSession unlockForConfiguration];
278
279  OCMVerify([mockAudioSession session]);
280  OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:YES withOptions:0 error:&error]);
281  OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:NO withOptions:0 error:&error]);
282
283  [mockAVAudioSession stopMocking];
284  [mockAudioSession stopMocking];
285}
286
287- (void)testAudioVolumeDidNotify {
288  MockAVAudioSession *mockAVAudioSession = [[MockAVAudioSession alloc] init];
289  RTCAudioSession *session = [[RTCAudioSession alloc] initWithAudioSession:mockAVAudioSession];
290  RTCAudioSessionTestDelegate *delegate =
291      [[RTCAudioSessionTestDelegate alloc] init];
292  [session addDelegate:delegate];
293
294  float expectedVolume = 0.75;
295  mockAVAudioSession.outputVolume = expectedVolume;
296
297  EXPECT_EQ(expectedVolume, delegate.outputVolume);
298}
299
300@end
301
302namespace webrtc {
303
304class AudioSessionTest : public ::testing::Test {
305 protected:
306  void TearDown() override {
307    RTCAudioSession *session = [RTCAudioSession sharedInstance];
308    for (id<RTCAudioSessionDelegate> delegate : session.delegates) {
309      [session removeDelegate:delegate];
310    }
311  }
312};
313
314TEST_F(AudioSessionTest, LockForConfiguration) {
315  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
316  [test testLockForConfiguration];
317}
318
319TEST_F(AudioSessionTest, AddAndRemoveDelegates) {
320  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
321  [test testAddAndRemoveDelegates];
322}
323
324TEST_F(AudioSessionTest, PushDelegate) {
325  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
326  [test testPushDelegate];
327}
328
329TEST_F(AudioSessionTest, ZeroingWeakDelegate) {
330  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
331  [test testZeroingWeakDelegate];
332}
333
334TEST_F(AudioSessionTest, RemoveDelegateOnDealloc) {
335  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
336  [test testRemoveDelegateOnDealloc];
337}
338
339TEST_F(AudioSessionTest, AudioSessionActivation) {
340  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
341  [test testAudioSessionActivation];
342}
343
344TEST_F(AudioSessionTest, ConfigureWebRTCSession) {
345  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
346  [test testConfigureWebRTCSession];
347}
348
349TEST_F(AudioSessionTest, AudioVolumeDidNotify) {
350  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
351  [test testAudioVolumeDidNotify];
352}
353
354}  // namespace webrtc
355