PlayRecordAudioViewController.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2014-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "PlayRecordAudioViewController.h"
  21. #import <AVFoundation/AVFoundation.h>
  22. #import "AudioRecorder.h"
  23. #import "BundleUtil.h"
  24. #import "AppDelegate.h"
  25. #import "UserSettings.h"
  26. #import "Threema-Swift.h"
  27. #import "AudioMessageSender.h"
  28. #ifdef DEBUG
  29. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  30. #else
  31. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  32. #endif
  33. @interface PlayRecordAudioViewController () <AVAudioPlayerDelegate, AudioRecorderDelegate, PlayRecordAudioViewDelegate>
  34. @property AudioRecorder *recorder;
  35. @property AVAudioPlayer *player;
  36. @property UIView *coverView;
  37. @property UIView *containerView;
  38. @property NSString *prevAudioCategory;
  39. @property Conversation *conversation;
  40. @property UIViewController *parentController;
  41. @property NSURL *audioFile;
  42. @property BOOL cancelled;
  43. @property BOOL hideOnFinishPlayback;
  44. @property dispatch_semaphore_t sema;
  45. @end
  46. @implementation PlayRecordAudioViewController
  47. + (BOOL)canRecordAudio {
  48. return [AVAudioSession sharedInstance].inputAvailable;
  49. }
  50. + (void)activateProximityMonitoring {
  51. if (![UserSettings sharedUserSettings].disableProximityMonitoring) {
  52. [[UIDevice currentDevice] setProximityMonitoringEnabled: YES];
  53. }
  54. }
  55. + (void)deactivateProximityMonitoring {
  56. [[UIDevice currentDevice] setProximityMonitoringEnabled: NO];
  57. }
  58. +(instancetype)playRecordAudioViewControllerIn:(UIViewController *)viewController {
  59. PlayRecordAudioViewController *instance = [[PlayRecordAudioViewController alloc] init];
  60. instance.parentController = viewController;
  61. return instance;
  62. }
  63. - (instancetype)init
  64. {
  65. self = [super init];
  66. if (self) {
  67. self.cancelled = NO;
  68. self.hideOnFinishPlayback = NO;
  69. [[NSBundle mainBundle] loadNibNamed:@"PlayRecordAudioView" owner:self options:nil];
  70. [self storeCurrentAudioSession];
  71. [_audioView setup];
  72. _audioView.delegate = self;
  73. [_audioView setStopped];
  74. AVAudioSessionRouteDescription *currentRoute = [AVAudioSession sharedInstance].currentRoute;
  75. if ([currentRoute.outputs[0].portType isEqualToString:@"Speaker"] || [currentRoute.outputs[0].portType isEqualToString:@"Receiver"]) {
  76. [self registerForNotifications];
  77. if (![UserSettings sharedUserSettings].disableProximityMonitoring) {
  78. [[UIDevice currentDevice] setProximityMonitoringEnabled: YES];
  79. }
  80. }
  81. }
  82. return self;
  83. }
  84. - (void)dealloc {
  85. [self stopAll];
  86. [self unregisterFromNotifications];
  87. [[UIDevice currentDevice] setProximityMonitoringEnabled: NO];
  88. // make sure delegate methods are not called anymore
  89. _recorder.delegate = nil;
  90. _player.delegate = nil;
  91. }
  92. - (void)storeCurrentAudioSession {
  93. _prevAudioCategory = [AVAudioSession sharedInstance].category;
  94. }
  95. - (void)setupAudioSessionWithSpeaker:(BOOL)speaker {
  96. NSInteger state = [[VoIPCallStateManager shared] currentCallState];
  97. if (state == CallStateIdle) {
  98. NSError *error = nil;
  99. AVAudioSession *session = [AVAudioSession sharedInstance];
  100. if (![session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeSpokenAudio options:AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error]) {
  101. DDLogError(@"Cannot set audio session category: %@", error);
  102. [UIAlertTemplate showAlertWithOwner:self title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  103. return;
  104. }
  105. if (![session overrideOutputAudioPort:speaker ? AVAudioSessionPortOverrideSpeaker : AVAudioSessionPortOverrideNone error:&error]) {
  106. DDLogError(@"Cannot set audio session override outputaudio port: %@", error);
  107. [UIAlertTemplate showAlertWithOwner:self title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  108. return;
  109. }
  110. [session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
  111. }
  112. }
  113. - (void)setupAudioSessionForRecordWithSpeaker {
  114. NSInteger state = [[VoIPCallStateManager shared] currentCallState];
  115. if (state == CallStateIdle) {
  116. NSError *error = nil;
  117. AVAudioSession *session = [AVAudioSession sharedInstance];
  118. if (![session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeSpokenAudio options:AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error]) {
  119. DDLogError(@"Cannot set audio session category: %@", error);
  120. [UIAlertTemplate showAlertWithOwner:self title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  121. return;
  122. }
  123. [session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
  124. }
  125. }
  126. - (void)resetAudioSession {
  127. NSInteger state = [[VoIPCallStateManager shared] currentCallState];
  128. if (state == CallStateIdle) {
  129. [[AVAudioSession sharedInstance] setCategory:_prevAudioCategory error:nil];
  130. [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
  131. _recorder.delegate = nil;
  132. _player.delegate = nil;
  133. }
  134. }
  135. - (NSURL *)tmpAudioUrlWithFileNamed:(NSString *)filename {
  136. NSURL *tmpDirUrl = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
  137. NSURL *url = [[tmpDirUrl URLByAppendingPathComponent:filename] URLByAppendingPathExtension: MEDIA_EXTENSION_AUDIO];
  138. DDLogInfo(@"fileURL: %@", [url path]);
  139. return url;
  140. }
  141. + (void)requestMicrophoneAccessOnCompletion:(void(^)(void))onCompletion {
  142. [self checkPermissionOnCompletion:^{
  143. if (onCompletion != nil) {
  144. onCompletion();
  145. }
  146. }];
  147. }
  148. - (void)blendInView {
  149. UIViewController *rootViewController = _parentController.view.window.rootViewController;
  150. CGRect parentRect = rootViewController.view.bounds;
  151. /* create container view that we can make modal for accessibility purposes */
  152. _containerView = [[UIView alloc] initWithFrame:parentRect];
  153. _containerView.accessibilityViewIsModal = YES;
  154. _containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  155. [rootViewController.view addSubview:_containerView];
  156. /* add view to cover view controller contents */
  157. _coverView = [[UIView alloc] initWithFrame:parentRect];
  158. _coverView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4];
  159. _coverView.alpha = 0;
  160. _coverView.isAccessibilityElement = YES;
  161. _coverView.accessibilityLabel = @"";
  162. _coverView.accessibilityIdentifier = @"FinishAudio";
  163. _coverView.accessibilityActivationPoint = CGPointMake(0.0, 0.0);
  164. _coverView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  165. [_containerView addSubview:_coverView];
  166. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapCover:)];
  167. [_coverView addGestureRecognizer: tapGesture];
  168. CGRect frame = _audioView.frame;
  169. frame.origin.x = (parentRect.size.width - frame.size.width) / 2;
  170. frame.origin.y = (parentRect.size.height - frame.size.height) / 2;
  171. _audioView.frame = frame;
  172. _audioView.alpha = 0;
  173. [_containerView addSubview:_audioView];
  174. /* fade in */
  175. [UIView animateWithDuration:0.3 animations:^{
  176. _audioView.alpha = 1.0;
  177. _coverView.alpha = 1.0;
  178. }];
  179. [AppDelegate sharedAppDelegate].magicTapHandler = self;
  180. }
  181. - (void)removeFromView {
  182. _coverView.userInteractionEnabled = NO;
  183. if ([AppDelegate sharedAppDelegate].magicTapHandler == (id)self)
  184. [AppDelegate sharedAppDelegate].magicTapHandler = nil;
  185. [self stopAll];
  186. [self resetAudioSession];
  187. [UIView animateWithDuration:0.3 animations:^{
  188. _coverView.alpha = 0;
  189. _audioView.alpha = 0;
  190. } completion:^(BOOL finished) {
  191. [_containerView removeFromSuperview];
  192. [self stopAll];
  193. [self unregisterFromNotifications];
  194. [[UIDevice currentDevice] setProximityMonitoringEnabled: NO];
  195. [_delegate audioPlayerDidHide];
  196. // make sure delegate methods are not called anymore
  197. _recorder.delegate = nil;
  198. _player.delegate = nil;
  199. }];
  200. }
  201. - (void)startRecordingForConversation:(Conversation *)conversation {
  202. [self blendInView];
  203. _conversation = conversation;
  204. _hideOnFinishPlayback = NO;
  205. _recorder = [[AudioRecorder alloc] init];
  206. _recorder.delegate = self;
  207. [self startRecording];
  208. }
  209. - (void)startRecording {
  210. [_audioView toggleButtonsForRecording: YES];
  211. [self setupAudioSessionForRecordWithSpeaker];
  212. [_recorder start];
  213. [_audioView setupForRecording:_recorder];
  214. }
  215. - (void)startPlaying:(NSURL *)audioFile {
  216. [_audioView toggleButtonsForRecording: NO];
  217. [self blendInView];
  218. [self setupAudioPlayer: audioFile];
  219. [self startPlaying];
  220. }
  221. - (void)setupAudioPlayer: (NSURL *)audioFile {
  222. _audioFile = audioFile;
  223. NSError *error;
  224. _player = [[AVAudioPlayer alloc] initWithContentsOfURL:_audioFile error:&error];
  225. [_player prepareToPlay];
  226. if (_player == nil) {
  227. DDLogError(@"Cannot create audio player: %@", error);
  228. [UIAlertTemplate showAlertWithOwner:self title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
  229. return;
  230. }
  231. _player.numberOfLoops = 0;
  232. _player.delegate = self;
  233. [_audioView setupForPlaying: _player];
  234. _player.currentTime = 0;
  235. }
  236. - (void)startPlaying {
  237. [self adaptToProximityState];
  238. [_player play];
  239. [_audioView setPlaying];
  240. [UIApplication sharedApplication].idleTimerDisabled = YES;
  241. }
  242. - (void)stopRecording {
  243. [_recorder stop];
  244. [self adaptToProximityState];
  245. [self stopRecordingUI];
  246. }
  247. - (void)stopRecordingUI {
  248. [_audioView setStopped];
  249. [self adaptToProximityState];
  250. [UIApplication sharedApplication].idleTimerDisabled = NO;
  251. }
  252. - (void)stopAll {
  253. if (_player) {
  254. [_player stop];
  255. }
  256. if (_recorder) {
  257. [_recorder stop];
  258. }
  259. }
  260. - (void)pause {
  261. [_player pause];
  262. [_audioView setPaused];
  263. [UIApplication sharedApplication].idleTimerDisabled = NO;
  264. }
  265. - (void)cancel {
  266. if (_recorder != nil) {
  267. [UIAlertTemplate showDestructiveAlertWithOwner:self title:[BundleUtil localizedStringForKey:@"record_cancel_title"] message:[BundleUtil localizedStringForKey:@"record_cancel_message"] titleDestructive:[BundleUtil localizedStringForKey:@"record_cancel_button_discard"] actionDestructive:^(UIAlertAction * action) {
  268. _cancelled = YES;
  269. [self stopRecording];
  270. [self removeFromView];
  271. } titleCancel:[BundleUtil localizedStringForKey:@"cancel"] actionCancel:nil];
  272. } else {
  273. [self stopRecording];
  274. [self removeFromView];
  275. }
  276. }
  277. - (IBAction)playPauseStopButtonPressed:(id)sender {
  278. _hideOnFinishPlayback = NO;
  279. if (_player.playing) {
  280. [self pause];
  281. } else if (_recorder.recorder.recording) {
  282. [self stopRecording];
  283. } else {
  284. [self startPlaying];
  285. }
  286. }
  287. - (IBAction)recordButtonPressed:(id)sender {
  288. [self startRecording];
  289. }
  290. - (IBAction)sendButtonPressed:(id)sender {
  291. [self stopRecording];
  292. [self sendFile];
  293. [self removeFromView];
  294. }
  295. + (void)checkPermissionOnCompletion:(void(^)(void))onCompletion {
  296. AVAudioSession *session = [AVAudioSession sharedInstance];
  297. if ([session respondsToSelector:@selector(requestRecordPermission:)]) {
  298. [session performSelector:@selector(requestRecordPermission:) withObject:^(BOOL granted) {
  299. dispatch_async(dispatch_get_main_queue(), ^{
  300. if (granted) {
  301. DDLogInfo(@"Microphone access granted.");
  302. onCompletion();
  303. } else {
  304. DDLogInfo(@"Microphone access not granted.");
  305. [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"microphone_disabled_title", nil) message:NSLocalizedString(@"microphone_disabled_message", nil) actionOk:nil];
  306. }
  307. });
  308. }];
  309. } else {
  310. onCompletion();
  311. }
  312. }
  313. - (void)sendFile {
  314. DDLogVerbose(@"Sending file");
  315. AudioMessageSender *sender = [[AudioMessageSender alloc] init];
  316. NSURL *url = [_recorder audioURL];
  317. [sender startWithAudioFile: url inConversation: _conversation requestId:nil];
  318. // NSURL *url = [_recorder audioURL];
  319. // URLSenderItem *item = [URLSenderItem itemWithUrl:url type:(NSString *)kUTTypeAudio renderType:@1 sendAsFile:true];
  320. // FileMessageSender *sender = [[FileMessageSender alloc] init];
  321. // [sender sendItem:item inConversation:_conversation requestId:nil];
  322. }
  323. #pragma mark - AudioRecorderDelegate
  324. - (void)recorderDidFinish {
  325. dispatch_async(dispatch_get_main_queue(), ^{
  326. NSURL *url = [_recorder audioURL];
  327. BOOL hasAudioFile = [[NSFileManager defaultManager] fileExistsAtPath:url.path];
  328. if (hasAudioFile && _cancelled == NO) {
  329. [self setupAudioPlayer: url];
  330. [_audioView setFinishedRecording];
  331. }
  332. });
  333. }
  334. - (void)setAccessibilityLabelForQuit {
  335. _coverView.accessibilityLabel = [BundleUtil localizedStringForKey:@"quit"];
  336. }
  337. - (void)recorderResumedAfterInterrupt {
  338. [_audioView setupForRecording:_recorder];
  339. }
  340. #pragma mark - AVAudioPlayerDelegate
  341. - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
  342. dispatch_async(dispatch_get_main_queue(), ^{
  343. [self setupAudioPlayer: _audioFile];
  344. [self pause];
  345. AVAudioSessionRouteDescription *currentRoute = [AVAudioSession sharedInstance].currentRoute;
  346. if ([currentRoute.outputs[0].portType isEqualToString:@"Speaker"]) {
  347. [self setupAudioSessionWithSpeaker:true];
  348. }
  349. else if ([currentRoute.outputs[0].portType isEqualToString:@"Receiver"]) {
  350. [self setupAudioSessionWithSpeaker:false];
  351. }
  352. if (_hideOnFinishPlayback) {
  353. [self cancel];
  354. }
  355. });
  356. }
  357. #pragma mark - Gestures
  358. - (void)tapCover:(UIGestureRecognizer*)gestureRecognizer {
  359. if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
  360. /* guard against accidental cancellation */
  361. if (_recorder.recorder.recording && _recorder.recorder.currentTime > 2) {
  362. return;
  363. }
  364. [self stopAll];
  365. [self cancel];
  366. }
  367. }
  368. #pragma mark - Notifications
  369. - (void)registerForNotifications {
  370. NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  371. [nc addObserver:self selector:@selector(proximityStateChanged:) name:UIDeviceProximityStateDidChangeNotification object:nil];
  372. }
  373. - (void)unregisterFromNotifications {
  374. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceProximityStateDidChangeNotification object:nil];
  375. }
  376. - (void)proximityStateChanged:(NSNotification *)notification {
  377. AVAudioSessionRouteDescription *currentRoute = [AVAudioSession sharedInstance].currentRoute;
  378. if (_player.isPlaying && ([currentRoute.outputs[0].portType isEqualToString:@"Speaker"] || [currentRoute.outputs[0].portType isEqualToString:@"Receiver"])) {
  379. [self adaptToProximityState];
  380. }
  381. }
  382. - (void)adaptToProximityState {
  383. AVAudioSessionRouteDescription *currentRoute = [AVAudioSession sharedInstance].currentRoute;
  384. if ([currentRoute.outputs[0].portType isEqualToString:@"Speaker"] || [currentRoute.outputs[0].portType isEqualToString:@"Receiver"]) {
  385. if ([UIDevice currentDevice].proximityState) {
  386. // close to ear
  387. [self setupAudioSessionWithSpeaker:false];
  388. } else {
  389. // speaker
  390. [self setupAudioSessionWithSpeaker:true];
  391. }
  392. } else {
  393. [self setupAudioSessionWithSpeaker:false];
  394. }
  395. }
  396. #pragma mark - Accessibility
  397. /* Note: this is intentionally not accessibilityPerformMagicTap, as it doesn't appear to get delivered reliably with our complicated
  398. view controller hierarchies. Instead, we catch it in the AppDelegate and dispatch it from there. */
  399. - (BOOL)handleMagicTap {
  400. _hideOnFinishPlayback = NO;
  401. if (_player.playing) {
  402. [self pause];
  403. UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString(@"pause", nil));
  404. } else if (_recorder.recorder.recording) {
  405. [self stopRecording];
  406. UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString(@"stop", nil));
  407. } else {
  408. [self startPlaying];
  409. }
  410. return YES;
  411. }
  412. @end