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