123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // Threema iOS Client
- // Copyright (c) 2014-2020 Threema GmbH
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License, version 3,
- // as published by the Free Software Foundation.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- #import "PlayRecordAudioViewController.h"
- #import <AVFoundation/AVFoundation.h>
- #import "AudioRecorder.h"
- #import "BundleUtil.h"
- #import "AppDelegate.h"
- #import "UserSettings.h"
- #import "Threema-Swift.h"
- #import "AudioMessageSender.h"
- #ifdef DEBUG
- static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
- #else
- static const DDLogLevel ddLogLevel = DDLogLevelWarning;
- #endif
- @interface PlayRecordAudioViewController () <AVAudioPlayerDelegate, AudioRecorderDelegate, PlayRecordAudioViewDelegate>
- @property AudioRecorder *recorder;
- @property AVAudioPlayer *player;
- @property UIView *coverView;
- @property UIView *containerView;
- @property NSString *prevAudioCategory;
- @property Conversation *conversation;
- @property UIViewController *parentController;
- @property NSURL *audioFile;
- @property BOOL cancelled;
- @property BOOL hideOnFinishPlayback;
- @property dispatch_semaphore_t sema;
- @end
- @implementation PlayRecordAudioViewController
- + (BOOL)canRecordAudio {
- return [AVAudioSession sharedInstance].inputAvailable;
- }
- + (void)activateProximityMonitoring {
- if (![UserSettings sharedUserSettings].disableProximityMonitoring) {
- [[UIDevice currentDevice] setProximityMonitoringEnabled: YES];
- }
- }
- + (void)deactivateProximityMonitoring {
- [[UIDevice currentDevice] setProximityMonitoringEnabled: NO];
- }
- +(instancetype)playRecordAudioViewControllerIn:(UIViewController *)viewController {
- PlayRecordAudioViewController *instance = [[PlayRecordAudioViewController alloc] init];
- instance.parentController = viewController;
-
- return instance;
- }
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- self.cancelled = NO;
- self.hideOnFinishPlayback = NO;
-
- [[NSBundle mainBundle] loadNibNamed:@"PlayRecordAudioView" owner:self options:nil];
-
- [self storeCurrentAudioSession];
-
- [_audioView setup];
- _audioView.delegate = self;
-
- [_audioView setStopped];
-
- AVAudioSessionRouteDescription *currentRoute = [AVAudioSession sharedInstance].currentRoute;
- if ([currentRoute.outputs[0].portType isEqualToString:@"Speaker"] || [currentRoute.outputs[0].portType isEqualToString:@"Receiver"]) {
- [self registerForNotifications];
- if (![UserSettings sharedUserSettings].disableProximityMonitoring) {
- [[UIDevice currentDevice] setProximityMonitoringEnabled: YES];
- }
- }
- }
-
- return self;
- }
- - (void)dealloc {
- [self stopAll];
- [self unregisterFromNotifications];
- [[UIDevice currentDevice] setProximityMonitoringEnabled: NO];
-
- // make sure delegate methods are not called anymore
- _recorder.delegate = nil;
- _player.delegate = nil;
- }
- - (void)storeCurrentAudioSession {
- _prevAudioCategory = [AVAudioSession sharedInstance].category;
- }
- - (void)setupAudioSessionWithSpeaker:(BOOL)speaker {
- NSInteger state = [[VoIPCallStateManager shared] currentCallState];
- if (state == CallStateIdle) {
- NSError *error = nil;
- AVAudioSession *session = [AVAudioSession sharedInstance];
-
- if (![session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeSpokenAudio options:AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error]) {
- DDLogError(@"Cannot set audio session category: %@", error);
- [UIAlertTemplate showAlertWithOwner:self title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
- return;
- }
-
- if (![session overrideOutputAudioPort:speaker ? AVAudioSessionPortOverrideSpeaker : AVAudioSessionPortOverrideNone error:&error]) {
- DDLogError(@"Cannot set audio session override outputaudio port: %@", error);
- [UIAlertTemplate showAlertWithOwner:self title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
- return;
- }
- [session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
- }
- }
- - (void)setupAudioSessionForRecordWithSpeaker {
- NSInteger state = [[VoIPCallStateManager shared] currentCallState];
- if (state == CallStateIdle) {
- NSError *error = nil;
- AVAudioSession *session = [AVAudioSession sharedInstance];
-
- if (![session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeSpokenAudio options:AVAudioSessionCategoryOptionAllowBluetooth|AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error]) {
- DDLogError(@"Cannot set audio session category: %@", error);
- [UIAlertTemplate showAlertWithOwner:self title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
- return;
- }
-
- [session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
- }
- }
- - (void)resetAudioSession {
- NSInteger state = [[VoIPCallStateManager shared] currentCallState];
- if (state == CallStateIdle) {
- [[AVAudioSession sharedInstance] setCategory:_prevAudioCategory error:nil];
- [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
- _recorder.delegate = nil;
- _player.delegate = nil;
- }
- }
- - (NSURL *)tmpAudioUrlWithFileNamed:(NSString *)filename {
- NSURL *tmpDirUrl = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
- NSURL *url = [[tmpDirUrl URLByAppendingPathComponent:filename] URLByAppendingPathExtension: MEDIA_EXTENSION_AUDIO];
-
- DDLogInfo(@"fileURL: %@", [url path]);
-
- return url;
- }
- + (void)requestMicrophoneAccessOnCompletion:(void(^)(void))onCompletion {
- [self checkPermissionOnCompletion:^{
- if (onCompletion != nil) {
- onCompletion();
- }
- }];
- }
- - (void)blendInView {
- UIViewController *rootViewController = _parentController.view.window.rootViewController;
- CGRect parentRect = rootViewController.view.bounds;
-
- /* create container view that we can make modal for accessibility purposes */
- _containerView = [[UIView alloc] initWithFrame:parentRect];
- _containerView.accessibilityViewIsModal = YES;
- _containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
- [rootViewController.view addSubview:_containerView];
-
- /* add view to cover view controller contents */
- _coverView = [[UIView alloc] initWithFrame:parentRect];
- _coverView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4];
- _coverView.alpha = 0;
- _coverView.isAccessibilityElement = YES;
-
- _coverView.accessibilityLabel = @"";
-
- _coverView.accessibilityIdentifier = @"FinishAudio";
- _coverView.accessibilityActivationPoint = CGPointMake(0.0, 0.0);
-
- _coverView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
- [_containerView addSubview:_coverView];
-
- UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapCover:)];
- [_coverView addGestureRecognizer: tapGesture];
-
- CGRect frame = _audioView.frame;
-
- frame.origin.x = (parentRect.size.width - frame.size.width) / 2;
- frame.origin.y = (parentRect.size.height - frame.size.height) / 2;
-
- _audioView.frame = frame;
- _audioView.alpha = 0;
-
- [_containerView addSubview:_audioView];
-
- /* fade in */
- [UIView animateWithDuration:0.3 animations:^{
- _audioView.alpha = 1.0;
- _coverView.alpha = 1.0;
- }];
-
- [AppDelegate sharedAppDelegate].magicTapHandler = self;
-
- }
- - (void)removeFromView {
- _coverView.userInteractionEnabled = NO;
-
- if ([AppDelegate sharedAppDelegate].magicTapHandler == (id)self)
- [AppDelegate sharedAppDelegate].magicTapHandler = nil;
-
- [self stopAll];
- [self resetAudioSession];
- [UIView animateWithDuration:0.3 animations:^{
- _coverView.alpha = 0;
- _audioView.alpha = 0;
- } completion:^(BOOL finished) {
- [_containerView removeFromSuperview];
- [self stopAll];
- [self unregisterFromNotifications];
- [[UIDevice currentDevice] setProximityMonitoringEnabled: NO];
- [_delegate audioPlayerDidHide];
-
- // make sure delegate methods are not called anymore
- _recorder.delegate = nil;
- _player.delegate = nil;
- }];
- }
- - (void)startRecordingForConversation:(Conversation *)conversation {
- [self blendInView];
-
- _conversation = conversation;
- _hideOnFinishPlayback = NO;
-
- _recorder = [[AudioRecorder alloc] init];
- _recorder.delegate = self;
- [self startRecording];
- }
- - (void)startRecording {
- [_audioView toggleButtonsForRecording: YES];
- [self setupAudioSessionForRecordWithSpeaker];
- [_recorder start];
- [_audioView setupForRecording:_recorder];
- }
- - (void)startPlaying:(NSURL *)audioFile {
- [_audioView toggleButtonsForRecording: NO];
- [self blendInView];
- [self setupAudioPlayer: audioFile];
-
- [self startPlaying];
- }
- - (void)setupAudioPlayer: (NSURL *)audioFile {
- _audioFile = audioFile;
-
- NSError *error;
- _player = [[AVAudioPlayer alloc] initWithContentsOfURL:_audioFile error:&error];
- [_player prepareToPlay];
- if (_player == nil) {
- DDLogError(@"Cannot create audio player: %@", error);
- [UIAlertTemplate showAlertWithOwner:self title:error.localizedDescription message:error.localizedFailureReason actionOk:nil];
- return;
- }
-
- _player.numberOfLoops = 0;
- _player.delegate = self;
-
- [_audioView setupForPlaying: _player];
-
- _player.currentTime = 0;
-
- }
- - (void)startPlaying {
- [self adaptToProximityState];
-
- [_player play];
- [_audioView setPlaying];
- [UIApplication sharedApplication].idleTimerDisabled = YES;
- }
- - (void)stopRecording {
- [_recorder stop];
-
- [self adaptToProximityState];
-
- [self stopRecordingUI];
- }
- - (void)stopRecordingUI {
- [_audioView setStopped];
-
- [self adaptToProximityState];
-
- [UIApplication sharedApplication].idleTimerDisabled = NO;
- }
- - (void)stopAll {
- if (_player) {
- [_player stop];
- }
-
- if (_recorder) {
- [_recorder stop];
- }
- }
- - (void)pause {
- [_player pause];
-
- [_audioView setPaused];
-
- [UIApplication sharedApplication].idleTimerDisabled = NO;
- }
- - (void)cancel {
- if (_recorder != nil) {
- [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) {
- _cancelled = YES;
- [self stopRecording];
- [self removeFromView];
- } titleCancel:[BundleUtil localizedStringForKey:@"cancel"] actionCancel:nil];
- } else {
- [self stopRecording];
- [self removeFromView];
- }
- }
- - (IBAction)playPauseStopButtonPressed:(id)sender {
- _hideOnFinishPlayback = NO;
-
- if (_player.playing) {
- [self pause];
- } else if (_recorder.recorder.recording) {
- [self stopRecording];
- } else {
- [self startPlaying];
- }
- }
- - (IBAction)recordButtonPressed:(id)sender {
- [self startRecording];
- }
- - (IBAction)sendButtonPressed:(id)sender {
- [self stopRecording];
-
- [self sendFile];
- [self removeFromView];
- }
- + (void)checkPermissionOnCompletion:(void(^)(void))onCompletion {
- AVAudioSession *session = [AVAudioSession sharedInstance];
- if ([session respondsToSelector:@selector(requestRecordPermission:)]) {
- [session performSelector:@selector(requestRecordPermission:) withObject:^(BOOL granted) {
- dispatch_async(dispatch_get_main_queue(), ^{
- if (granted) {
- DDLogInfo(@"Microphone access granted.");
- onCompletion();
- } else {
- DDLogInfo(@"Microphone access not granted.");
- [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:NSLocalizedString(@"microphone_disabled_title", nil) message:NSLocalizedString(@"microphone_disabled_message", nil) actionOk:nil];
- }
- });
- }];
- } else {
- onCompletion();
- }
- }
- - (void)sendFile {
- DDLogVerbose(@"Sending file");
-
- AudioMessageSender *sender = [[AudioMessageSender alloc] init];
- NSURL *url = [_recorder audioURL];
- [sender startWithAudioFile: url inConversation: _conversation requestId:nil];
-
- // NSURL *url = [_recorder audioURL];
- // URLSenderItem *item = [URLSenderItem itemWithUrl:url type:(NSString *)kUTTypeAudio renderType:@1 sendAsFile:true];
- // FileMessageSender *sender = [[FileMessageSender alloc] init];
- // [sender sendItem:item inConversation:_conversation requestId:nil];
- }
- #pragma mark - AudioRecorderDelegate
- - (void)recorderDidFinish {
- dispatch_async(dispatch_get_main_queue(), ^{
- NSURL *url = [_recorder audioURL];
- BOOL hasAudioFile = [[NSFileManager defaultManager] fileExistsAtPath:url.path];
- if (hasAudioFile && _cancelled == NO) {
- [self setupAudioPlayer: url];
- [_audioView setFinishedRecording];
- }
- });
- }
- - (void)setAccessibilityLabelForQuit {
- _coverView.accessibilityLabel = [BundleUtil localizedStringForKey:@"quit"];
- }
- - (void)recorderResumedAfterInterrupt {
- [_audioView setupForRecording:_recorder];
- }
- #pragma mark - AVAudioPlayerDelegate
- - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self setupAudioPlayer: _audioFile];
- [self pause];
- AVAudioSessionRouteDescription *currentRoute = [AVAudioSession sharedInstance].currentRoute;
- if ([currentRoute.outputs[0].portType isEqualToString:@"Speaker"]) {
- [self setupAudioSessionWithSpeaker:true];
- }
- else if ([currentRoute.outputs[0].portType isEqualToString:@"Receiver"]) {
- [self setupAudioSessionWithSpeaker:false];
- }
-
- if (_hideOnFinishPlayback) {
- [self cancel];
- }
- });
- }
- #pragma mark - Gestures
- - (void)tapCover:(UIGestureRecognizer*)gestureRecognizer {
- if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
- /* guard against accidental cancellation */
- if (_recorder.recorder.recording && _recorder.recorder.currentTime > 2) {
- return;
- }
-
- [self stopAll];
- [self cancel];
- }
- }
- #pragma mark - Notifications
- - (void)registerForNotifications {
- NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
- [nc addObserver:self selector:@selector(proximityStateChanged:) name:UIDeviceProximityStateDidChangeNotification object:nil];
- }
- - (void)unregisterFromNotifications {
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceProximityStateDidChangeNotification object:nil];
- }
- - (void)proximityStateChanged:(NSNotification *)notification {
- AVAudioSessionRouteDescription *currentRoute = [AVAudioSession sharedInstance].currentRoute;
- if (_player.isPlaying && ([currentRoute.outputs[0].portType isEqualToString:@"Speaker"] || [currentRoute.outputs[0].portType isEqualToString:@"Receiver"])) {
- [self adaptToProximityState];
- }
- }
- - (void)adaptToProximityState {
- AVAudioSessionRouteDescription *currentRoute = [AVAudioSession sharedInstance].currentRoute;
- if ([currentRoute.outputs[0].portType isEqualToString:@"Speaker"] || [currentRoute.outputs[0].portType isEqualToString:@"Receiver"]) {
- if ([UIDevice currentDevice].proximityState) {
- // close to ear
- [self setupAudioSessionWithSpeaker:false];
- } else {
- // speaker
- [self setupAudioSessionWithSpeaker:true];
- }
- } else {
- [self setupAudioSessionWithSpeaker:false];
- }
- }
- #pragma mark - Accessibility
- /* Note: this is intentionally not accessibilityPerformMagicTap, as it doesn't appear to get delivered reliably with our complicated
- view controller hierarchies. Instead, we catch it in the AppDelegate and dispatch it from there. */
- - (BOOL)handleMagicTap {
- _hideOnFinishPlayback = NO;
-
- if (_player.playing) {
- [self pause];
- UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString(@"pause", nil));
- } else if (_recorder.recorder.recording) {
- [self stopRecording];
- UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString(@"stop", nil));
- } else {
- [self startPlaying];
- }
-
- return YES;
- }
- @end
|