Merge branch 'mkirk/bt-audio-2'
This commit is contained in:
commit
77fb1dbf45
21
Signal/Images.xcassets/ic_speaker_bluetooth_inactive_audio_mode.imageset/Contents.json
vendored
Normal file
21
Signal/Images.xcassets/ic_speaker_bluetooth_inactive_audio_mode.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_speaker_bluetooth_inactive_audio_mode.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
21
Signal/Images.xcassets/ic_speaker_bluetooth_inactive_video_mode.imageset/Contents.json
vendored
Normal file
21
Signal/Images.xcassets/ic_speaker_bluetooth_inactive_video_mode.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_speaker_bluetooth_inactive_video_mode.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
@ -7,6 +7,11 @@ import Foundation
|
||||
/**
|
||||
* Strings re-used in multiple places should be added here.
|
||||
*/
|
||||
|
||||
@objc class CommonStrings: NSObject {
|
||||
static let dismissButton = NSLocalizedString("DISMISS_BUTTON_TEXT", comment: "Short text to dismiss current modal / actionsheet / screen")
|
||||
}
|
||||
|
||||
@objc class CallStrings: NSObject {
|
||||
|
||||
static let callStatusFormat = NSLocalizedString("CALL_STATUS_FORMAT", comment: "embeds {{Call Status}} in call screen label. For ongoing calls, {{Call Status}} is a seconds timer like 01:23, otherwise {{Call Status}} is a short text like 'Ringing', 'Busy', or 'Failed Call'")
|
||||
|
||||
@ -41,7 +41,7 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
var ongoingCallView: UIView!
|
||||
|
||||
var hangUpButton: UIButton!
|
||||
var speakerPhoneButton: UIButton!
|
||||
var audioSourceButton: UIButton!
|
||||
var audioModeMuteButton: UIButton!
|
||||
var audioModeVideoButton: UIButton!
|
||||
var videoModeMuteButton: UIButton!
|
||||
@ -86,11 +86,45 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
var settingsNagView: UIView!
|
||||
var settingsNagDescriptionLabel: UILabel!
|
||||
|
||||
// MARK: Audio Source
|
||||
|
||||
var hasAlternateAudioSources: Bool {
|
||||
Logger.info("\(TAG) available audio routes count: \(allAudioSources.count)")
|
||||
// internal mic and speakerphone will be the first two, any more than one indicates e.g. an attached bluetooth device.
|
||||
|
||||
// TODO is this sufficient? Are their devices w/ bluetooth but no external speaker? e.g. ipod?
|
||||
return allAudioSources.count > 2
|
||||
}
|
||||
|
||||
var allAudioSources: Set<AudioSource>
|
||||
|
||||
var appropriateAudioSources: Set<AudioSource> {
|
||||
if call.hasLocalVideo {
|
||||
let appropriateForVideo = allAudioSources.filter { audioSource in
|
||||
if audioSource.isBuiltInSpeaker {
|
||||
return true
|
||||
} else {
|
||||
guard let portDescription = audioSource.portDescription else {
|
||||
owsFail("Only built in speaker should be lacking a port description.")
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't use receiver when video is enabled. Only bluetooth or speaker
|
||||
return portDescription.portType != AVAudioSessionPortBuiltInMic
|
||||
}
|
||||
}
|
||||
return Set(appropriateForVideo)
|
||||
} else {
|
||||
return allAudioSources
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
contactsManager = Environment.getCurrent().contactsManager
|
||||
callUIAdapter = Environment.getCurrent().callUIAdapter
|
||||
allAudioSources = Set(callUIAdapter.audioService.availableInputs)
|
||||
super.init(coder: aDecoder)
|
||||
observeNotifications()
|
||||
}
|
||||
@ -98,6 +132,7 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
required init() {
|
||||
contactsManager = Environment.getCurrent().contactsManager
|
||||
callUIAdapter = Environment.getCurrent().callUIAdapter
|
||||
allAudioSources = Set(callUIAdapter.audioService.availableInputs)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
observeNotifications()
|
||||
}
|
||||
@ -107,6 +142,11 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
selector:#selector(didBecomeActive),
|
||||
name:NSNotification.Name.UIApplicationDidBecomeActive,
|
||||
object:nil)
|
||||
|
||||
NotificationCenter.default.addObserver(forName: CallAudioServiceSessionChanged, object: nil, queue: nil) { _ in
|
||||
self.didChangeAudioSession()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -157,7 +197,7 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
// Subscribe for future call updates
|
||||
call.addObserverAndSyncState(observer: self)
|
||||
|
||||
Environment.getCurrent().callService.addObserverAndSyncState(observer:self)
|
||||
Environment.getCurrent().callService.addObserverAndSyncState(observer: self)
|
||||
}
|
||||
|
||||
// MARK: - Create Views
|
||||
@ -288,8 +328,8 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
|
||||
// textMessageButton = createButton(imageName:"message-active-wide",
|
||||
// action:#selector(didPressTextMessage))
|
||||
speakerPhoneButton = createButton(imageName:"audio-call-speaker-inactive",
|
||||
action:#selector(didPressSpeakerphone))
|
||||
audioSourceButton = createButton(imageName:"audio-call-speaker-inactive",
|
||||
action:#selector(didPressAudioSource))
|
||||
hangUpButton = createButton(imageName:"hangup-active-wide",
|
||||
action:#selector(didPressHangup))
|
||||
audioModeMuteButton = createButton(imageName:"audio-call-mute-inactive",
|
||||
@ -305,12 +345,53 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
setButtonSelectedImage(button: videoModeMuteButton, imageName: "video-mute-selected")
|
||||
setButtonSelectedImage(button: audioModeVideoButton, imageName: "audio-call-video-active")
|
||||
setButtonSelectedImage(button: videoModeVideoButton, imageName: "video-video-selected")
|
||||
setButtonSelectedImage(button: speakerPhoneButton, imageName: "audio-call-speaker-active")
|
||||
|
||||
ongoingCallView = createContainerForCallControls(controlGroups : [
|
||||
[audioModeMuteButton, speakerPhoneButton, audioModeVideoButton ],
|
||||
[audioModeMuteButton, audioSourceButton, audioModeVideoButton ],
|
||||
[videoModeMuteButton, hangUpButton, videoModeVideoButton ]
|
||||
])
|
||||
])
|
||||
}
|
||||
|
||||
func didChangeAudioSession() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// Which sources are available depends on the state of your Session.
|
||||
// When the audio session is not yet in PlayAndRecord none are available
|
||||
// Then if we're in speakerphone, bluetooth isn't available.
|
||||
// So we acrew all possible audio sources in a set, and that list lives as longs as the CallViewController
|
||||
// The downside of this is that if you e.g. unpair your bluetooth mid call, it will still appear as an option
|
||||
// until your next call.
|
||||
// FIXME: There's got to be a better way, but this is where I landed after a bit of work, and seems to work
|
||||
// pretty well in practrice.
|
||||
let availableInputs = callUIAdapter.audioService.availableInputs
|
||||
self.allAudioSources.formUnion(availableInputs)
|
||||
}
|
||||
|
||||
func presentAudioSourcePicker() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
let dismissAction = UIAlertAction(title: CommonStrings.dismissButton, style: .cancel, handler: nil)
|
||||
actionSheetController.addAction(dismissAction)
|
||||
|
||||
let currentAudioSource = callUIAdapter.audioService.currentAudioSource(call: self.call)
|
||||
for audioSource in self.appropriateAudioSources {
|
||||
let routeAudioAction = UIAlertAction(title: audioSource.localizedName, style: .default) { _ in
|
||||
self.callUIAdapter.setAudioSource(call: self.call, audioSource: audioSource)
|
||||
}
|
||||
|
||||
// HACK: private API to create checkmark for active audio source.
|
||||
routeAudioAction.setValue(currentAudioSource == audioSource, forKey: "checked")
|
||||
|
||||
// TODO: pick some icons. Leaving out for MVP
|
||||
// HACK: private API to add image to actionsheet
|
||||
// routeAudioAction.setValue(audioSource.image, forKey: "image")
|
||||
|
||||
actionSheetController.addAction(routeAudioAction)
|
||||
}
|
||||
|
||||
self.present(actionSheetController, animated: true)
|
||||
}
|
||||
|
||||
func setButtonSelectedImage(button: UIButton, imageName: String) {
|
||||
@ -653,7 +734,6 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
videoModeMuteButton.isSelected = call.isMuted
|
||||
audioModeVideoButton.isSelected = call.hasLocalVideo
|
||||
videoModeVideoButton.isSelected = call.hasLocalVideo
|
||||
speakerPhoneButton.isSelected = call.isSpeakerphoneEnabled
|
||||
|
||||
// Show Incoming vs. Ongoing call controls
|
||||
let isRinging = callState == .localRinging
|
||||
@ -668,7 +748,8 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
|
||||
// Rework control state if local video is available.
|
||||
let hasLocalVideo = !localVideoView.isHidden
|
||||
for subview in [speakerPhoneButton, audioModeMuteButton, audioModeVideoButton] {
|
||||
|
||||
for subview in [audioModeMuteButton, audioModeVideoButton] {
|
||||
subview?.isHidden = hasLocalVideo
|
||||
}
|
||||
for subview in [videoModeMuteButton, videoModeVideoButton] {
|
||||
@ -685,6 +766,32 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
callStatusLabel.isHidden = false
|
||||
}
|
||||
|
||||
// Audio Source Handling (bluetooth)
|
||||
if self.hasAlternateAudioSources {
|
||||
// With bluetooth, button does not stay selected. Pressing it pops an actionsheet
|
||||
// and the button should immediately "unselect".
|
||||
audioSourceButton.isSelected = false
|
||||
|
||||
if hasLocalVideo {
|
||||
audioSourceButton.setImage(#imageLiteral(resourceName: "ic_speaker_bluetooth_inactive_video_mode"), for: .normal)
|
||||
audioSourceButton.setImage(#imageLiteral(resourceName: "ic_speaker_bluetooth_inactive_video_mode"), for: .selected)
|
||||
} else {
|
||||
audioSourceButton.setImage(#imageLiteral(resourceName: "ic_speaker_bluetooth_inactive_audio_mode"), for: .normal)
|
||||
audioSourceButton.setImage(#imageLiteral(resourceName: "ic_speaker_bluetooth_inactive_audio_mode"), for: .selected)
|
||||
}
|
||||
audioSourceButton.isHidden = false
|
||||
} else {
|
||||
// No bluetooth audio detected
|
||||
|
||||
audioSourceButton.isSelected = call.isSpeakerphoneEnabled
|
||||
audioSourceButton.setImage(#imageLiteral(resourceName: "audio-call-speaker-inactive"), for: .normal)
|
||||
audioSourceButton.setImage(#imageLiteral(resourceName: "audio-call-speaker-active"), for: .selected)
|
||||
|
||||
// If there's no bluetooth, we always use speakerphone, so no need for
|
||||
// a button, giving more screen back for the video.
|
||||
audioSourceButton.isHidden = hasLocalVideo
|
||||
}
|
||||
|
||||
// Dismiss Handling
|
||||
switch callState {
|
||||
case .remoteHangup, .remoteBusy, .localFailure:
|
||||
@ -742,17 +849,32 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
}
|
||||
}
|
||||
|
||||
func didPressSpeakerphone(sender speakerphoneButton: UIButton) {
|
||||
func didPressAudioSource(sender button: UIButton) {
|
||||
Logger.info("\(TAG) called \(#function)")
|
||||
speakerphoneButton.isSelected = !speakerphoneButton.isSelected
|
||||
|
||||
if self.hasAlternateAudioSources {
|
||||
presentAudioSourcePicker()
|
||||
} else {
|
||||
didPressSpeakerphone(sender: button)
|
||||
}
|
||||
}
|
||||
|
||||
func didPressSpeakerphone(sender button: UIButton) {
|
||||
Logger.info("\(TAG) called \(#function)")
|
||||
button.isSelected = !button.isSelected
|
||||
if let call = self.call {
|
||||
callUIAdapter.setIsSpeakerphoneEnabled(call: call, isEnabled: speakerphoneButton.isSelected)
|
||||
if button.isSelected {
|
||||
callUIAdapter.setAudioSource(call: call, audioSource: AudioSource.builtInSpeaker)
|
||||
} else {
|
||||
// use default audio source
|
||||
callUIAdapter.setAudioSource(call: call, audioSource: nil)
|
||||
}
|
||||
} else {
|
||||
Logger.warn("\(TAG) pressed mute, but call was unexpectedly nil")
|
||||
}
|
||||
}
|
||||
|
||||
func didPressTextMessage(sender speakerphoneButton: UIButton) {
|
||||
func didPressTextMessage(sender button: UIButton) {
|
||||
Logger.info("\(TAG) called \(#function)")
|
||||
|
||||
dismissIfPossible(shouldDelay:false)
|
||||
@ -860,7 +982,7 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R
|
||||
self.updateCallUI(callState: call.state)
|
||||
}
|
||||
|
||||
internal func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) {
|
||||
internal func audioSourceDidChange(call: SignalCall, audioSource: AudioSource?) {
|
||||
AssertIsOnMainThread()
|
||||
self.updateCallUI(callState: call.state)
|
||||
}
|
||||
|
||||
@ -301,7 +301,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
message:error.localizedDescription
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
}
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"DISMISS_BUTTON_TEXT", nil)
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.dismissButton
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[_challengeTextField becomeFirstResponder];
|
||||
|
||||
@ -872,13 +872,11 @@ typedef enum : NSUInteger {
|
||||
}];
|
||||
[actionSheetController addAction:verifyAction];
|
||||
|
||||
UIAlertAction *dismissAction =
|
||||
[UIAlertAction actionWithTitle:NSLocalizedString(@"DISMISS_BUTTON_TEXT",
|
||||
@"Generic short text for button to dismiss a dialog")
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[weakSelf resetVerificationStateToDefault];
|
||||
}];
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.dismissButton
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[weakSelf resetVerificationStateToDefault];
|
||||
}];
|
||||
[actionSheetController addAction:dismissAction];
|
||||
|
||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||
@ -2950,9 +2948,8 @@ typedef enum : NSUInteger {
|
||||
@"Alert body when picking a document fails because user picked a directory/bundle")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"DISMISS_BUTTON_TEXT", nil)
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil];
|
||||
UIAlertAction *dismissAction =
|
||||
[UIAlertAction actionWithTitle:CommonStrings.dismissButton style:UIAlertActionStyleCancel handler:nil];
|
||||
[alertController addAction:dismissAction];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@ -2978,9 +2975,8 @@ typedef enum : NSUInteger {
|
||||
message:nil
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"DISMISS_BUTTON_TEXT", nil)
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil];
|
||||
UIAlertAction *dismissAction =
|
||||
[UIAlertAction actionWithTitle:CommonStrings.dismissButton style:UIAlertActionStyleCancel handler:nil];
|
||||
[alertController addAction:dismissAction];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
@ -194,7 +194,7 @@ class ExperienceUpgradesPageViewController: UIViewController, UIPageViewControll
|
||||
// Dismiss button
|
||||
let dismissButton = UIButton()
|
||||
view.addSubview(dismissButton)
|
||||
dismissButton.setTitle(NSLocalizedString("DISMISS_BUTTON_TEXT", comment: ""), for: .normal)
|
||||
dismissButton.setTitle(CommonStrings.dismissButton, for: .normal)
|
||||
dismissButton.setTitleColor(UIColor.white, for: .normal)
|
||||
dismissButton.isUserInteractionEnabled = true
|
||||
dismissButton.addTarget(self, action:#selector(didTapDismissButton), for: .touchUpInside)
|
||||
|
||||
@ -189,7 +189,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
DDLogInfo(@"%@ Successfully verified safety numbers.", tag);
|
||||
|
||||
NSString *successTitle = NSLocalizedString(@"SUCCESSFUL_VERIFICATION_TITLE", nil);
|
||||
NSString *dismissText = NSLocalizedString(@"DISMISS_BUTTON_TEXT", nil);
|
||||
NSString *descriptionFormat = NSLocalizedString(
|
||||
@"SUCCESSFUL_VERIFICATION_DESCRIPTION", @"Alert body after verifying privacy with {{other user's name}}");
|
||||
NSString *successDescription = [NSString stringWithFormat:descriptionFormat, contactName];
|
||||
@ -209,7 +208,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
[viewController dismissViewControllerAnimated:true completion:nil];
|
||||
}]];
|
||||
UIAlertAction *dismissAction =
|
||||
[UIAlertAction actionWithTitle:dismissText
|
||||
[UIAlertAction actionWithTitle:CommonStrings.dismissButton
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[viewController dismissViewControllerAnimated:true completion:nil];
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
// Created by Michael Kirk on 11/18/16.
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Social
|
||||
@ -31,7 +32,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||
super.init()
|
||||
|
||||
actionSheetController.addAction(dismissAction())
|
||||
|
||||
|
||||
if #available(iOS 9.0, *) {
|
||||
if let messageAction = messageAction() {
|
||||
actionSheetController.addAction(messageAction)
|
||||
@ -72,7 +73,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||
twitterViewController.add(#imageLiteral(resourceName: "twitter_sharing_image"))
|
||||
|
||||
let tweetTitle = NSLocalizedString("SHARE_ACTION_TWEET", comment:"action sheet item")
|
||||
return UIAlertAction(title: tweetTitle, style: .default) { action in
|
||||
return UIAlertAction(title: tweetTitle, style: .default) { _ in
|
||||
Logger.debug("\(self.TAG) Chose tweet")
|
||||
|
||||
self.presentingViewController.present(twitterViewController, animated: true, completion: nil)
|
||||
@ -80,7 +81,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||
}
|
||||
|
||||
func dismissAction() -> UIAlertAction {
|
||||
return UIAlertAction(title: NSLocalizedString("DISMISS_BUTTON_TEXT", comment:""), style: .cancel)
|
||||
return UIAlertAction(title: CommonStrings.dismissButton, style: .cancel)
|
||||
}
|
||||
|
||||
// MARK: ContactsPickerDelegate
|
||||
@ -134,10 +135,10 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||
}
|
||||
|
||||
let messageTitle = NSLocalizedString("SHARE_ACTION_MESSAGE", comment: "action sheet item to open native messages app")
|
||||
return UIAlertAction(title: messageTitle, style: .default) { action in
|
||||
return UIAlertAction(title: messageTitle, style: .default) { _ in
|
||||
Logger.debug("\(self.TAG) Chose message.")
|
||||
self.channel = .message
|
||||
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .phoneNumber)
|
||||
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .phoneNumber)
|
||||
let navigationController = UINavigationController(rootViewController: picker)
|
||||
self.presentingViewController.present(navigationController, animated: true)
|
||||
}
|
||||
@ -173,7 +174,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment:"Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: NSLocalizedString("DISMISS_BUTTON_TEXT", comment:""), style: .default, handler: nil))
|
||||
warning.addAction(UIAlertAction(title: CommonStrings.dismissButton, style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("\(self.TAG) user successfully invited their friends via SMS.")
|
||||
@ -192,7 +193,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||
}
|
||||
|
||||
let mailActionTitle = NSLocalizedString("SHARE_ACTION_MAIL", comment: "action sheet item to open native mail app")
|
||||
return UIAlertAction(title: mailActionTitle, style: .default) { action in
|
||||
return UIAlertAction(title: mailActionTitle, style: .default) { _ in
|
||||
Logger.debug("\(self.TAG) Chose mail.")
|
||||
self.channel = .mail
|
||||
|
||||
@ -216,8 +217,8 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||
mailComposeViewController.setMessageBody(body, isHTML: false)
|
||||
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
self.presentingViewController.navigationController?.present(mailComposeViewController, animated:true) {
|
||||
UIUtil.applySignalAppearence();
|
||||
self.presentingViewController.navigationController?.present(mailComposeViewController, animated:true) {
|
||||
UIUtil.applySignalAppearence()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -230,7 +231,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment:"Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: NSLocalizedString("DISMISS_BUTTON_TEXT", comment:""), style: .default, handler: nil))
|
||||
warning.addAction(UIAlertAction(title: CommonStrings.dismissButton, style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("\(self.TAG) user successfully invited their friends via mail.")
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#import "OWSLinkedDevicesTableViewController.h"
|
||||
#import "OWSDeviceTableViewCell.h"
|
||||
#import "OWSLinkDeviceViewController.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "UIViewController+CameraPermissions.h"
|
||||
#import <SignalServiceKit/NSTimer+OWS.h>
|
||||
#import <SignalServiceKit/OWSDevice.h>
|
||||
@ -166,10 +167,9 @@ int const OWSLinkedDevicesTableViewControllerSectionAddDevice = 1;
|
||||
}];
|
||||
[alertController addAction:retryAction];
|
||||
|
||||
NSString *dismissTitle
|
||||
= NSLocalizedString(@"DISMISS_BUTTON_TEXT", @"Generic short text for button to dismiss a dialog");
|
||||
UIAlertAction *dismissAction =
|
||||
[UIAlertAction actionWithTitle:dismissTitle style:UIAlertActionStyleCancel handler:nil];
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.dismissButton
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil];
|
||||
[alertController addAction:dismissAction];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
@ -379,8 +379,7 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi
|
||||
- (void)presentInvalidCountryCodeError {
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"REGISTER_CC_ERR_ALERT_VIEW_TITLE", @"")
|
||||
message:NSLocalizedString(@"REGISTER_CC_ERR_ALERT_VIEW_MESSAGE", @"")
|
||||
buttonTitle:NSLocalizedString(
|
||||
@"DISMISS_BUTTON_TEXT", @"Generic short text for button to dismiss a dialog")];
|
||||
buttonTitle:CommonStrings.dismissButton];
|
||||
}
|
||||
|
||||
#pragma mark - CountryCodeViewControllerDelegate
|
||||
|
||||
@ -5,6 +5,71 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
public let CallAudioServiceSessionChanged = Notification.Name("CallAudioServiceSessionChanged")
|
||||
|
||||
struct AudioSource: Hashable {
|
||||
|
||||
let image: UIImage
|
||||
let localizedName: String
|
||||
let portDescription: AVAudioSessionPortDescription?
|
||||
let isBuiltInSpeaker: Bool
|
||||
|
||||
init(localizedName: String, image: UIImage, isBuiltInSpeaker: Bool, portDescription: AVAudioSessionPortDescription? = nil) {
|
||||
self.localizedName = localizedName
|
||||
self.image = image
|
||||
self.isBuiltInSpeaker = isBuiltInSpeaker
|
||||
self.portDescription = portDescription
|
||||
}
|
||||
|
||||
init(portDescription: AVAudioSessionPortDescription) {
|
||||
self.init(localizedName: portDescription.portName,
|
||||
image:#imageLiteral(resourceName: "button_phone_white"), // TODO
|
||||
isBuiltInSpeaker: false,
|
||||
portDescription: portDescription)
|
||||
}
|
||||
|
||||
// Speakerphone is handled separately from the other audio routes as it doesn't appear as an "input"
|
||||
static var builtInSpeaker: AudioSource {
|
||||
return self.init(localizedName: NSLocalizedString("AUDIO_ROUTE_BUILT_IN_SPEAKER", comment: "action sheet button title to enable built in speaker during a call"),
|
||||
image: #imageLiteral(resourceName: "button_phone_white"), //TODO
|
||||
isBuiltInSpeaker: true)
|
||||
}
|
||||
|
||||
// MARK: Hashable
|
||||
|
||||
static func ==(lhs: AudioSource, rhs: AudioSource) -> Bool {
|
||||
// Simply comparing the `portDescription` vs the `portDescription.uid`
|
||||
// caused multiple instances of the built in mic to turn up in a set.
|
||||
if lhs.isBuiltInSpeaker && rhs.isBuiltInSpeaker {
|
||||
return true
|
||||
}
|
||||
|
||||
if lhs.isBuiltInSpeaker || rhs.isBuiltInSpeaker {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let lhsPortDescription = lhs.portDescription else {
|
||||
owsFail("only the built in speaker should lack a port description")
|
||||
return false
|
||||
}
|
||||
|
||||
guard let rhsPortDescription = rhs.portDescription else {
|
||||
owsFail("only the built in speaker should lack a port description")
|
||||
return false
|
||||
}
|
||||
|
||||
return lhsPortDescription.uid == rhsPortDescription.uid
|
||||
}
|
||||
|
||||
var hashValue: Int {
|
||||
guard let portDescription = self.portDescription else {
|
||||
assert(self.isBuiltInSpeaker)
|
||||
return "Built In Speaker".hashValue
|
||||
}
|
||||
return portDescription.uid.hash
|
||||
}
|
||||
}
|
||||
|
||||
@objc class CallAudioService: NSObject, CallObserver {
|
||||
|
||||
private let TAG = "[CallAudioService]"
|
||||
@ -74,10 +139,19 @@ import AVFoundation
|
||||
Logger.verbose("\(TAG) in \(#function) is no-op")
|
||||
}
|
||||
|
||||
internal func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) {
|
||||
internal func audioSourceDidChange(call: SignalCall, audioSource: AudioSource?) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
ensureProperAudioSession(call: call)
|
||||
|
||||
// It's importent to set preferred input *after* ensuring properAudioSession
|
||||
// because some sources are only valid for certain categories.
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try session.setPreferredInput(audioSource?.portDescription)
|
||||
} catch {
|
||||
owsFail("\(TAG) setPreferredInput in \(#function) failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
internal func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool) {
|
||||
@ -98,14 +172,17 @@ import AVFoundation
|
||||
setAudioSession(category: AVAudioSessionCategorySoloAmbient,
|
||||
mode: AVAudioSessionModeDefault)
|
||||
} else if call.hasLocalVideo {
|
||||
// Auto-enable speakerphone when local video is enabled.
|
||||
// Don't allow bluetooth for local video if speakerphone has been explicitly chosen by the user.
|
||||
let options: AVAudioSessionCategoryOptions = call.isSpeakerphoneEnabled ? [.defaultToSpeaker] : [.defaultToSpeaker, .allowBluetooth]
|
||||
|
||||
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord,
|
||||
mode: AVAudioSessionModeVideoChat,
|
||||
options: [.defaultToSpeaker, .allowBluetooth])
|
||||
options: options)
|
||||
} else if call.isSpeakerphoneEnabled {
|
||||
// Ensure no bluetooth if user has specified speakerphone
|
||||
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord,
|
||||
mode: AVAudioSessionModeVoiceChat,
|
||||
options: [.defaultToSpeaker, .allowBluetooth])
|
||||
options: [.defaultToSpeaker])
|
||||
} else {
|
||||
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord,
|
||||
mode: AVAudioSessionModeVoiceChat,
|
||||
@ -308,11 +385,61 @@ import AVFoundation
|
||||
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
|
||||
}
|
||||
|
||||
// MARK - AudioSession MGMT
|
||||
// TODO move this to CallAudioSession?
|
||||
|
||||
// Note this method is sensitive to the current audio session configuration.
|
||||
// Specifically if you call it while speakerphone is enabled you won't see
|
||||
// any connected bluetooth routes.
|
||||
var availableInputs: [AudioSource] {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
|
||||
guard let availableInputs = session.availableInputs else {
|
||||
// I'm not sure when this would happen.
|
||||
owsFail("No available inputs or inputs not ready")
|
||||
return [AudioSource.builtInSpeaker]
|
||||
}
|
||||
|
||||
Logger.info("\(TAG) in \(#function) availableInputs: \(availableInputs)")
|
||||
return [AudioSource.builtInSpeaker] + availableInputs.map { portDescription in
|
||||
return AudioSource(portDescription: portDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func currentAudioSource(call: SignalCall) -> AudioSource? {
|
||||
if let audioSource = call.audioSource {
|
||||
return audioSource
|
||||
}
|
||||
|
||||
// Before the user has specified an audio source on the call, we rely on the existing
|
||||
// system state to determine the current audio source.
|
||||
// If a bluetooth is connected, this will be bluetooth, otherwise
|
||||
// this will be the receiver.
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
guard let portDescription = session.currentRoute.inputs.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return AudioSource(portDescription: portDescription)
|
||||
}
|
||||
|
||||
public func setPreferredInput(call: SignalCall, audioSource: AudioSource?) {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
Logger.debug("\(TAG) in \(#function) audioSource: \(String(describing: audioSource))")
|
||||
try session.setPreferredInput(audioSource?.portDescription)
|
||||
} catch {
|
||||
owsFail("\(TAG) failed with error: \(error)")
|
||||
}
|
||||
self.ensureProperAudioSession(call: call)
|
||||
}
|
||||
|
||||
private func setAudioSession(category: String,
|
||||
mode: String? = nil,
|
||||
options: AVAudioSessionCategoryOptions = AVAudioSessionCategoryOptions(rawValue: 0)) {
|
||||
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
var audioSessionChanged = false
|
||||
do {
|
||||
if #available(iOS 10.0, *), let mode = mode {
|
||||
let oldCategory = session.category
|
||||
@ -323,6 +450,8 @@ import AVFoundation
|
||||
return
|
||||
}
|
||||
|
||||
audioSessionChanged = true
|
||||
|
||||
if oldCategory != category {
|
||||
Logger.debug("\(self.TAG) audio session changed category: \(oldCategory) -> \(category) ")
|
||||
}
|
||||
@ -342,6 +471,8 @@ import AVFoundation
|
||||
return
|
||||
}
|
||||
|
||||
audioSessionChanged = true
|
||||
|
||||
if oldCategory != category {
|
||||
Logger.debug("\(self.TAG) audio session changed category: \(oldCategory) -> \(category) ")
|
||||
}
|
||||
@ -355,5 +486,10 @@ import AVFoundation
|
||||
let message = "\(self.TAG) in \(#function) failed to set category: \(category) mode: \(String(describing: mode)), options: \(options) with error: \(error)"
|
||||
owsFail(message)
|
||||
}
|
||||
|
||||
if audioSessionChanged {
|
||||
Logger.info("\(TAG) in \(#function)")
|
||||
NotificationCenter.default.post(name: CallAudioServiceSessionChanged, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1442,7 +1442,7 @@ protocol CallServiceObserver: class {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
internal func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) {
|
||||
internal func audioSourceDidChange(call: SignalCall, audioSource: AudioSource?) {
|
||||
AssertIsOnMainThread()
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ protocol CallObserver: class {
|
||||
func stateDidChange(call: SignalCall, state: CallState)
|
||||
func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool)
|
||||
func muteDidChange(call: SignalCall, isMuted: Bool)
|
||||
func speakerphoneDidChange(call: SignalCall, isEnabled: Bool)
|
||||
func audioSourceDidChange(call: SignalCall, audioSource: AudioSource?)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,18 +104,25 @@ protocol CallObserver: class {
|
||||
}
|
||||
}
|
||||
|
||||
var isSpeakerphoneEnabled = false {
|
||||
var audioSource: AudioSource? = nil {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.debug("\(TAG) isSpeakerphoneEnabled changed: \(oldValue) -> \(self.isSpeakerphoneEnabled)")
|
||||
Logger.debug("\(TAG) audioSource changed: \(String(describing:oldValue)) -> \(String(describing: audioSource))")
|
||||
|
||||
for observer in observers {
|
||||
observer.value?.speakerphoneDidChange(call: self, isEnabled: isSpeakerphoneEnabled)
|
||||
observer.value?.audioSourceDidChange(call: self, audioSource: audioSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isSpeakerphoneEnabled: Bool {
|
||||
guard let audioSource = self.audioSource else {
|
||||
return false
|
||||
}
|
||||
|
||||
return audioSource.isBuiltInSpeaker
|
||||
}
|
||||
|
||||
var isOnHold = false
|
||||
|
||||
var connectedDate: NSDate?
|
||||
|
||||
@ -80,7 +80,7 @@ extension CallUIAdaptee {
|
||||
let TAG = "[CallUIAdapter]"
|
||||
private let adaptee: CallUIAdaptee
|
||||
private let contactsManager: OWSContactsManager
|
||||
private let audioService: CallAudioService
|
||||
internal let audioService: CallAudioService
|
||||
|
||||
required init(callService: CallService, contactsManager: OWSContactsManager, notificationsAdapter: CallNotificationsAdapter) {
|
||||
AssertIsOnMainThread()
|
||||
@ -207,13 +207,13 @@ extension CallUIAdaptee {
|
||||
adaptee.setHasLocalVideo(call: call, hasLocalVideo: hasLocalVideo)
|
||||
}
|
||||
|
||||
internal func setIsSpeakerphoneEnabled(call: SignalCall, isEnabled: Bool) {
|
||||
internal func setAudioSource(call: SignalCall, audioSource: AudioSource?) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// Speakerphone is not handled by CallKit (e.g. there is no CXAction), so we handle it w/o going through the
|
||||
// adaptee, relying on the AudioService CallObserver to put the system in a state consistent with the call's
|
||||
// AudioSource is not handled by CallKit (e.g. there is no CXAction), so we handle it w/o going through the
|
||||
// adaptee, relying on the AudioService CallObserver to put the system in a state consistent with the call's
|
||||
// assigned property.
|
||||
call.isSpeakerphoneEnabled = isEnabled
|
||||
call.audioSource = audioSource
|
||||
}
|
||||
|
||||
// CallKit handles ringing state on it's own. But for non-call kit we trigger ringing start/stop manually.
|
||||
|
||||
@ -44,7 +44,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
}];
|
||||
[alert addAction:openSettingsAction];
|
||||
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"DISMISS_BUTTON_TEXT", nil)
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.dismissButton
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *action) {
|
||||
failureCallback();
|
||||
|
||||
@ -12,8 +12,7 @@ import Foundation
|
||||
let alertTitle = NSLocalizedString("CALL_AUDIO_PERMISSION_TITLE", comment:"Alert title when calling and permissions for microphone are missing")
|
||||
let alertMessage = NSLocalizedString("CALL_AUDIO_PERMISSION_MESSAGE", comment:"Alert message when calling and permissions for microphone are missing")
|
||||
let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
|
||||
let dismiss = NSLocalizedString("DISMISS_BUTTON_TEXT", comment: "Generic short text for button to dismiss a dialog")
|
||||
let dismissAction = UIAlertAction(title: dismiss, style: .cancel)
|
||||
let dismissAction = UIAlertAction(title: CommonStrings.dismissButton, style: .cancel)
|
||||
let settingsString = NSLocalizedString("OPEN_SETTINGS_BUTTON", comment: "Button text which opens the settings app")
|
||||
let settingsAction = UIAlertAction(title: settingsString, style: .default) { _ in
|
||||
UIApplication.shared.openSystemSettings()
|
||||
|
||||
@ -139,6 +139,9 @@
|
||||
/* Short text label for a voice message attachment, used for thread preview and on lockscreen */
|
||||
"ATTACHMENT_TYPE_VOICE_MESSAGE" = "Voice Message";
|
||||
|
||||
/* action sheet button title to enable built in speaker during a call */
|
||||
"AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Built in Speaker";
|
||||
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
|
||||
|
||||
@ -376,7 +379,7 @@
|
||||
/* Accessibility label for disappearing messages */
|
||||
"DISAPPEARING_MESSAGES_LABEL" = "Disappearing messages settings";
|
||||
|
||||
/* Generic short text for button to dismiss a dialog */
|
||||
/* Short text to dismiss current modal / actionsheet / screen */
|
||||
"DISMISS_BUTTON_TEXT" = "Dismiss";
|
||||
|
||||
/* Section title for the 'domain fronting country' view. */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user