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@end
117
118@implementation RTCAudioSessionTest
119
120- (void)testAddAndRemoveDelegates {
121  RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
122  NSMutableArray *delegates = [NSMutableArray array];
123  const size_t count = 5;
124  for (size_t i = 0; i < count; ++i) {
125    RTCAudioSessionTestDelegate *delegate =
126        [[RTCAudioSessionTestDelegate alloc] init];
127    [session addDelegate:delegate];
128    [delegates addObject:delegate];
129    EXPECT_EQ(i + 1, session.delegates.size());
130  }
131  [delegates enumerateObjectsUsingBlock:^(RTCAudioSessionTestDelegate *obj,
132                                          NSUInteger idx,
133                                          BOOL *stop) {
134    [session removeDelegate:obj];
135  }];
136  EXPECT_EQ(0u, session.delegates.size());
137}
138
139- (void)testPushDelegate {
140  RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
141  NSMutableArray *delegates = [NSMutableArray array];
142  const size_t count = 2;
143  for (size_t i = 0; i < count; ++i) {
144    RTCAudioSessionTestDelegate *delegate =
145        [[RTCAudioSessionTestDelegate alloc] init];
146    [session addDelegate:delegate];
147    [delegates addObject:delegate];
148  }
149  // Test that it gets added to the front of the list.
150  RTCAudioSessionTestDelegate *pushedDelegate =
151      [[RTCAudioSessionTestDelegate alloc] init];
152  [session pushDelegate:pushedDelegate];
153  EXPECT_TRUE(pushedDelegate == session.delegates[0]);
154
155  // Test that it stays at the front of the list.
156  for (size_t i = 0; i < count; ++i) {
157    RTCAudioSessionTestDelegate *delegate =
158        [[RTCAudioSessionTestDelegate alloc] init];
159    [session addDelegate:delegate];
160    [delegates addObject:delegate];
161  }
162  EXPECT_TRUE(pushedDelegate == session.delegates[0]);
163
164  // Test that the next one goes to the front too.
165  pushedDelegate = [[RTCAudioSessionTestDelegate alloc] init];
166  [session pushDelegate:pushedDelegate];
167  EXPECT_TRUE(pushedDelegate == session.delegates[0]);
168}
169
170// Tests that delegates added to the audio session properly zero out. This is
171// checking an implementation detail (that vectors of __weak work as expected).
172- (void)testZeroingWeakDelegate {
173  RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
174  @autoreleasepool {
175    // Add a delegate to the session. There should be one delegate at this
176    // point.
177    RTCAudioSessionTestDelegate *delegate =
178        [[RTCAudioSessionTestDelegate alloc] init];
179    [session addDelegate:delegate];
180    EXPECT_EQ(1u, session.delegates.size());
181    EXPECT_TRUE(session.delegates[0]);
182  }
183  // The previously created delegate should've de-alloced, leaving a nil ptr.
184  EXPECT_FALSE(session.delegates[0]);
185  RTCAudioSessionTestDelegate *delegate =
186      [[RTCAudioSessionTestDelegate alloc] init];
187  [session addDelegate:delegate];
188  // On adding a new delegate, nil ptrs should've been cleared.
189  EXPECT_EQ(1u, session.delegates.size());
190  EXPECT_TRUE(session.delegates[0]);
191}
192
193// Tests that we don't crash when removing delegates in dealloc.
194// Added as a regression test.
195- (void)testRemoveDelegateOnDealloc {
196  @autoreleasepool {
197    RTCTestRemoveOnDeallocDelegate *delegate =
198        [[RTCTestRemoveOnDeallocDelegate alloc] init];
199    EXPECT_TRUE(delegate);
200  }
201  RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
202  EXPECT_EQ(0u, session.delegates.size());
203}
204
205- (void)testAudioSessionActivation {
206  RTC_OBJC_TYPE(RTCAudioSession) *audioSession = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
207  EXPECT_EQ(0, audioSession.activationCount);
208  [audioSession audioSessionDidActivate:[AVAudioSession sharedInstance]];
209  EXPECT_EQ(1, audioSession.activationCount);
210  [audioSession audioSessionDidDeactivate:[AVAudioSession sharedInstance]];
211  EXPECT_EQ(0, audioSession.activationCount);
212}
213
214// Hack - fixes OCMVerify link error
215// Link error is: Undefined symbols for architecture i386:
216// "OCMMakeLocation(objc_object*, char const*, int)", referenced from:
217// -[RTCAudioSessionTest testConfigureWebRTCSession] in RTCAudioSessionTest.o
218// ld: symbol(s) not found for architecture i386
219// REASON: https://github.com/erikdoe/ocmock/issues/238
220OCMLocation *OCMMakeLocation(id testCase, const char *fileCString, int line){
221  return [OCMLocation locationWithTestCase:testCase
222                                      file:[NSString stringWithUTF8String:fileCString]
223                                      line:line];
224}
225
226- (void)testConfigureWebRTCSession {
227  NSError *error = nil;
228
229  void (^setActiveBlock)(NSInvocation *invocation) = ^(NSInvocation *invocation) {
230    __autoreleasing NSError **retError;
231    [invocation getArgument:&retError atIndex:4];
232    *retError = [NSError errorWithDomain:@"AVAudioSession"
233                                    code:AVAudioSessionErrorInsufficientPriority
234                                userInfo:nil];
235    BOOL failure = NO;
236    [invocation setReturnValue:&failure];
237  };
238
239  id mockAVAudioSession = OCMPartialMock([AVAudioSession sharedInstance]);
240  OCMStub([[mockAVAudioSession ignoringNonObjectArgs] setActive:YES
241                                                    withOptions:0
242                                                          error:([OCMArg anyObjectRef])])
243      .andDo(setActiveBlock);
244
245  id mockAudioSession = OCMPartialMock([RTC_OBJC_TYPE(RTCAudioSession) sharedInstance]);
246  OCMStub([mockAudioSession session]).andReturn(mockAVAudioSession);
247
248  RTC_OBJC_TYPE(RTCAudioSession) *audioSession = mockAudioSession;
249  EXPECT_EQ(0, audioSession.activationCount);
250  [audioSession lockForConfiguration];
251  // configureWebRTCSession is forced to fail in the above mock interface,
252  // so activationCount should remain 0
253  OCMExpect([[mockAVAudioSession ignoringNonObjectArgs] setActive:YES
254                                                      withOptions:0
255                                                            error:([OCMArg anyObjectRef])])
256      .andDo(setActiveBlock);
257  OCMExpect([mockAudioSession session]).andReturn(mockAVAudioSession);
258  EXPECT_FALSE([audioSession configureWebRTCSession:&error]);
259  EXPECT_EQ(0, audioSession.activationCount);
260
261  id session = audioSession.session;
262  EXPECT_EQ(session, mockAVAudioSession);
263  EXPECT_EQ(NO, [mockAVAudioSession setActive:YES withOptions:0 error:&error]);
264  [audioSession unlockForConfiguration];
265
266  OCMVerify([mockAudioSession session]);
267  OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:YES withOptions:0 error:&error]);
268  OCMVerify([[mockAVAudioSession ignoringNonObjectArgs] setActive:NO withOptions:0 error:&error]);
269
270  [mockAVAudioSession stopMocking];
271  [mockAudioSession stopMocking];
272}
273
274- (void)testAudioVolumeDidNotify {
275  MockAVAudioSession *mockAVAudioSession = [[MockAVAudioSession alloc] init];
276  RTC_OBJC_TYPE(RTCAudioSession) *session =
277      [[RTC_OBJC_TYPE(RTCAudioSession) alloc] initWithAudioSession:mockAVAudioSession];
278  RTCAudioSessionTestDelegate *delegate =
279      [[RTCAudioSessionTestDelegate alloc] init];
280  [session addDelegate:delegate];
281
282  float expectedVolume = 0.75;
283  mockAVAudioSession.outputVolume = expectedVolume;
284
285  EXPECT_EQ(expectedVolume, delegate.outputVolume);
286}
287
288@end
289
290namespace webrtc {
291
292class AudioSessionTest : public ::testing::Test {
293 protected:
294  void TearDown() override {
295    RTC_OBJC_TYPE(RTCAudioSession) *session = [RTC_OBJC_TYPE(RTCAudioSession) sharedInstance];
296    for (id<RTC_OBJC_TYPE(RTCAudioSessionDelegate)> delegate : session.delegates) {
297      [session removeDelegate:delegate];
298    }
299  }
300};
301
302TEST_F(AudioSessionTest, AddAndRemoveDelegates) {
303  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
304  [test testAddAndRemoveDelegates];
305}
306
307TEST_F(AudioSessionTest, PushDelegate) {
308  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
309  [test testPushDelegate];
310}
311
312TEST_F(AudioSessionTest, ZeroingWeakDelegate) {
313  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
314  [test testZeroingWeakDelegate];
315}
316
317TEST_F(AudioSessionTest, RemoveDelegateOnDealloc) {
318  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
319  [test testRemoveDelegateOnDealloc];
320}
321
322TEST_F(AudioSessionTest, AudioSessionActivation) {
323  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
324  [test testAudioSessionActivation];
325}
326
327TEST_F(AudioSessionTest, ConfigureWebRTCSession) {
328  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
329  [test testConfigureWebRTCSession];
330}
331
332TEST_F(AudioSessionTest, AudioVolumeDidNotify) {
333  RTCAudioSessionTest *test = [[RTCAudioSessionTest alloc] init];
334  [test testAudioVolumeDidNotify];
335}
336
337}  // namespace webrtc
338