Is there a way to answer a phone call with CallKit? - ios

I'm working on an app for a hands free BLE device for changing music and answering phone calls. Everything I'm reading says this can't be done, but figured I'd ask and see if maybe there was a change now with CallKit.
I'm able to see call events using CXCallObserverDelegate, but am unable to answer phone calls with the CXAnswerCallAction.
Here is my current code, I'm using a delay for testing purposes. Eventually the action would be triggered by an action from the BLE device:
self.currentCallUUID = call.uuid
if let callUUID = self.currentCallUUID
{
let answerAction: CXAnswerCallAction = CXAnswerCallAction(call: callUUID)
let transaction: CXTransaction = CXTransaction(action: answerAction)
let when = DispatchTime.now() + 2
DispatchQueue.main.asyncAfter(deadline: when) {
print("answering call")
self.callController.request(transaction, completion: { error in
if (error != nil)
{
print("did answer")
answerAction.fulfill()
}
else
{
print("answer failed: \(String(describing: error?.localizedDescription))")
}
})
}
}

Related

CallKit Audio session starts when navigating to the application only

I'm working on a voip app now and want to support holding.
But when a second call comes and I hold my current call. Switching to my first call I hear no sound at all.
The way to hear it is to navigate from callKit native screen to my app and hence I can hear the voice.
func configureAudioSession() {
_ = try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord, mode: .videoChat, options: AVAudioSession.CategoryOptions.mixWithOthers)
_ = try? AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.none)
_ = try? AVAudioSession.sharedInstance().setMode(AVAudioSession.Mode.voiceChat)
}
func startAudio() {
print("Starting audio")
do {
_ = try AVAudioSession.sharedInstance().setActive(true)
} catch {
}
}
func stopAudio() {
print("Stopping audio")
do {
_ = try AVAudioSession.sharedInstance().setActive(false)
} catch {
}
}
For supporting holding, you dont have to start/stop audio session, instead you can use CXSetHeldCallAction provided by Callkit itself. Here, is the code for hold that i use.
let callKitCallController = CXCallController()
func performHoldAction(isOnHold:Bool, uuid:UUID) {
let holdCallAction = CXSetHeldCallAction(call: uuid, onHold: isOnHold)
let transaction = CXTransaction(action: holdCallAction)
callKitCallController.request(transaction) { error in
if let error = error {
CPrint("holdCallAction transaction request failed: \(error.localizedDescription).")
return
}
CPrint("holdCallAction transaction request successful")
}
}
Once system puts the call on hold(by above method OR due to other incoming call accepting or any other reason), then in CXProviderDelegate, the method func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) gives you the callback for the detail.
Here, system/callkit itself interacts with audio, you dont have to explicitly start or stop audio for holding.
Note: Make sure that you given supportsHolding to true for the CXCallUpdate that you gave for new call.

UI won't display when entering my app from a VOIP call received in the background?

I'm having a little confusion in regards to showing my UI when opening the app from an incoming background video call. I am successfully causing iOS to summon the default "incoming video call" interface when the app is in the background, but after the call is answered, my app isn't being woken up properly?
When I receive the call push, I setup a CXProvider and notify CallKit of an incoming call:
func handleIncomingCallFromBackground() {
//These properties are parsed from the push payload before this method is triggered
guard let callingUser = callingUser, roomName != nil else {
print("Unexpected nil caller and roomname (background)")
return
}
let callHandleTitle = "\(callingUser.first_name) \(callingUser.surname)"
let configuration = CXProviderConfiguration.default
let callKitProvider = CXProvider(configuration: configuration)
callKitProvider.setDelegate(self, queue: nil)
let callHandle = CXHandle(type: .generic, value: callHandleTitle)
self.callHandle = callHandle
let callUpdate = CXCallUpdate.default
callUpdate.remoteHandle = callHandle
let callUUID = UUID()
self.callUUID = callUUID
callKitProvider.reportNewIncomingCall(with: callUUID, update: callUpdate) { error in
if error != nil {
self.resetTwilioObjects()
}
}
}
I respond to the answer call delegate method for the CXProvider, in which I get an access token for the video call from the server, send a response to the server to alert the caller that we've accepted the call, and perform a segue to our own video call controller (that's all jobsVC.showVideoCallVC() does) which handles connecting the call through a Twilio room etc. Code below.
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
guard let customer = callingUser, let callHandle = self.callHandle, let uuid = callUUID, let roomName = roomName else {
resetTwilioObjects()
return
}
self.twilioAPI = TwilioAPI()
self.twilioAPI!.accessToken(room: roomName) { (success, accessToken) in
switch success{
case true:
guard let token = accessToken else {
return
}
let customerID = customer.userID
DispatchQueue.global().asyncAfter(deadline: .now() , execute: {
self.twilioAPI!.postResponse(customerID: customerID, status: 1) { (success) in
if let success = success, !success {
self.resetTwilioObjects()
}
}
})
guard let jobsVC = P_ChildHomeJobsVC.instance else {
print("Jobs VC unexpectedly nil")
return
}
//All this method does is perform a segue, the parameters are stored for later.
jobsVC.showVideoCallVC(callingUser: customer, callHandle: callHandle, callKitProvider: provider, callUUID: uuid, twilioAccessToken: token, roomName: roomName)
action.fulfill()
default:
action.fail()
self.resetTwilioObjects()
}
}
}
Depending on whether or not the device is locked, I get differing behaviour:
If the device is locked, upon hitting my app icon to open the app, I get the latest app screenshot show up with a green bar at the top instead of the UI.
If the device is unlocked, upon hitting my app icon to open the app nothing happens at all - it stays on the iOS interface.
According to my logs, the segue is actually being performed correctly and the inner workings of the video call controller are even being fired, but shortly after I get the applicationWillResignActive: delegate call and everything stops.
What's odd (or maybe not) is that if the device is locked whilst the app is still in the foreground, everything works as expected: the app is correctly woken up and the updated UI is shown. I noticed that I still get the applicationWillResignActive: call, but immediately get applicationDidBecomeActive: after that.
Does anyone have any suggestions or hints as to what I might be doing wrong?

How to remove callback option UI of CallKit after ending the call

In my app i'm using CallKit for incoming call. There is no outgoing call feature in the app. Everything is fine but when the receiver or dailer ends the call it shows the CallKit UI with call back option. I don't want to show callback option, how can I do it?
My code for ending the call
func end(call: SpeakerboxCall) {
let endCallAction = CXEndCallAction(call: call.uuid)
let transaction = CXTransaction()
transaction.addAction(endCallAction)
requestTransaction(transaction, action: "endCall")
}
private func requestTransaction(_ transaction: CXTransaction, action:
String = "") {
callController.request(transaction) { error in
if let error = error {
print("Error requesting transaction: \(error)")
} else {
print("Requested transaction \(action) successfully")
}
}
}
I have solved it. I was force quitting the CallKit where the transaction is not correctly completing.
AppDelegate.shared.providerDelegate?.provider.reportCall(with: call.uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded)
We need to set endedAt to nil and reason to remoteEnded
Close All Call (Swift4)
func performEndCallAction() {
for call in self.cxCallController.callObserver.calls {
let endCallAction = CXEndCallAction(call: call.uuid)
let transaction = CXTransaction(action: endCallAction)
cxCallController.request(transaction) { error in
if let error = error {
NSLog("EndCallAction transaction request failed: \(error.localizedDescription).")
return
}
NSLog("EndCallAction transaction request successful")
}
}
}

Unit testing BLE application in IOS (Swift)

We've developed an app that connects to a BLE dongle and everything is working thru the BLE connection.
Now we decided to add Unit testing ( And Yes, i know it is much better to do TDD and not this way but this is the situation)
In the app everything is working, but when i'm trying to develop the unit tests i can't go pass the connection phase (GAT) i'm not getting the connection working an in any case the tests go thru one after the other and don't stop to wait for the connection to happen and authentication and nothing)
func testConnect() {
if viewController == nil {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
viewController = storyboard.instantiateViewController(withIdentifier: "ViewController") as? ViewController
if let vc = viewController {
_ = vc.view
}
}
viewController?.connectBluetooth();
}
func testAuthenticateByPin() {
delay(5) {
var error: NSError? = nil
self.datConnection?.connect("ABCDEFG", withError: &error)
XCTAssertNotNil(error, "Connect Error: \(String(describing: error))")
print("Connection: \(String(describing: error))")
self.datConnection?.authenticate(byPIN: "AD$FGR#", withError: &error)
XCTAssertNotNil(error, "Error: \(String(describing: error))")
print("Auth: \(String(describing: error))")
}
}
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
Any one knows how to create a BLE unit test and how to create a Delay between unit tests?
I use expectations for my network operation tests in Objective-C.
You create an expectation and at the end of the test case, wait for it to become fulfilled. When you get the connection notification or whatever you have to wait for, call fulfill(). The wait uses a timeout and if the notification never comes (connection never takes place), the test will fail with the non-fulfilled expectation.
From a sample from Apple's website (here) which is already in Swift:
func testDownloadWebData() {
// Create an expectation for a background download task.
let expectation = XCTestExpectation(description: "Download apple.com home page")
// Create a URL for a web page to be downloaded.
let url = URL(string: "https://apple.com")!
// Create a background task to download the web page.
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, _) in
// Make sure we downloaded some data.
XCTAssertNotNil(data, "No data was downloaded.")
// Fulfill the expectation to indicate that the background task has finished successfully.
expectation.fulfill()
}
// Start the download task.
dataTask.resume()
// Wait until the expectation is fulfilled, with a timeout of 10 seconds.
wait(for: [expectation], timeout: 10.0)
}

How can I simulate an Outgoing call using iOS Callkit

I have gone through the iOS CallKit documentation, the below is the code provided by Apple under the section 'Making Outgoing Calls'. When I try and call the function startOutGoingCall() nothing happens i.e. I don't see any outgoing call UI.
Could someone please suggest me how I could trigger a native outgoing call UI.
func startOutGoingCall(){
let uuid = UUID()
let handle = CXHandle(type: .emailAddress, value: "jappleseed#apple.com")
let startCallAction = CXStartCallAction(call: uuid)
startCallAction.destination = handle
let transaction = CXTransaction(action: startCallAction)
callController.request(transaction) { error in
if let error = error {
print("Error requesting transaction: \(error)")
} else {
print("Requested transaction successfully")
}
}
}
EDIT:
Added the delegate method from my code
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
logMessage(messageText: "provider:performStartCallAction:")
/*
* Configure the audio session, but do not start call audio here, since it must be done once
* the audio session has been activated by the system after having its priority elevated.
*/
localMedia?.audioController.configureAudioSession(.videoChatSpeaker)
callKitProvider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
performRoomConnect(uuid: action.callUUID, roomName: action.handle.value) { (success) in
if (success) {
provider.reportOutgoingCall(with: action.callUUID, connectedAt: Date())
action.fulfill()
} else {
action.fail()
}
}
}
There is no outgoing UI you get from CallKit. When you make an outgoing call, your app is open, and therefore, it is your app that should show the UI.

Resources