├── LICENSE ├── README.md ├── package.json ├── plugin.xml ├── src ├── android │ ├── CordovaCall.java │ └── MyConnectionService.java └── ios │ ├── AppDelegateCordovaCall.m │ ├── CordovaCall.h │ └── CordovaCall.m └── www ├── CordovaCall.js └── VoIPPushNotification.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matthew Khaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cordova-plugin-callkit 2 | Cordova plugin that enables CallKit + PushKit (iOS) & ConnectionService (Android) functionality to display native UI. 3 | 4 | This plugin is basically a merged of 2 plugins, which are, [WebsiteBeaver/CordovaCall](https://github.com/WebsiteBeaver/CordovaCall) and [Hitman666/cordova-ios-voip-push](https://github.com/Hitman666/cordova-ios-voip-push), to basically fulfill iOS 13's requirement for VOIP Push Notification. All credits goes to both of them. 5 | 6 | For those who are unaware of iOS 13's requirement for VOIP Push Notification, the change being made here is once your app receives a VOIP Push Notification, it is mandatory to inform the OS that there's a new incoming call, otherwise your app will be forced terminated and banned from getting further VOIP Push Notifications if there's too many failed attempts until the app is reinstalled. 7 | 8 | Why the merge is essential? This is essential because in iOS 13, Cordova WebView won't initialize if the app is not started, upon receiving the VOIP Push Notification. It is started only when the incoming call is reported hence, everything must be handled natively. 9 | 10 | # Install 11 | 12 | Add the plugin to your Cordova project: 13 | 14 | `cordova plugin add cordova-plugin-callkit` 15 | 16 | # API Guide 17 | 18 | For this, just refer to [WebsiteBeaver/CordovaCall](https://github.com/WebsiteBeaver/CordovaCall) and [Hitman666/cordova-ios-voip-push](https://github.com/Hitman666/cordova-ios-voip-push). I'm not gonna be bothered to merge the documentations at all since both of them already provide excellent guides on how to use them. The namespaces in this plugin are identical to both of the repos since this plugin combines both of them into one, like I mentioned above. 19 | 20 | # Usage 21 | 22 | Once the plugin is installed, the only thing that you need to do is to push a VOIP notification with the following data payload structure: 23 | 24 | ```javascript 25 | { 26 | Caller: { 27 | Username: 'Display Name', 28 | ConnectionId: 'Unique Call ID' 29 | } 30 | } 31 | ``` 32 | 33 | If you need more parameters, just add them into the structure to your liking. Basically, the `Username` property is mapped to the name displayed on the call screen and the `ConnectionId` property is mapped to the unique ID for the incoming call (value is optional but property must be provided in the object). 34 | 35 | You guys might be wondering, so far it is all about iOS, how about Android? As for Android, no modifications are required for the original plugin since background notifications can handle everything. 36 | 37 | # Conclusion 38 | 39 | I hope that this plugin will help other people out there who is struggling to figure this portion out. Again, I wanted to thank the original creators of these plugins. Without them, I couldn't figure out on how to do all this. If there's any questions, feel free to contact me. I'll try my best to help. 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-callkit", 3 | "version": "1.0.0", 4 | "description": "Cordova plugin that lets you use iOS CallKit UI (with PushKit) and Android ConnectionService UI", 5 | "cordova": { 6 | "id": "cordova-plugin-callkit", 7 | "platforms": [ 8 | "ios", 9 | "android" 10 | ] 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/mattkhaw/cordova-plugin-callkit" 15 | }, 16 | "keywords": [ 17 | "voip", 18 | "cordova", 19 | "push", 20 | "callkit", 21 | "pushkit", 22 | "ecosystem:cordova", 23 | "cordova-android", 24 | "cordova-ios" 25 | ], 26 | "homepage": "https://github.com/mattkhaw/cordova-plugin-callkit", 27 | "author": "Matthew Khaw", 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cordova CallKit 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | fetch 51 | remote-notification 52 | voip 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/android/CordovaCall.java: -------------------------------------------------------------------------------- 1 | package com.dmarc.cordovacall; 2 | 3 | import org.apache.cordova.CordovaPlugin; 4 | import org.apache.cordova.CallbackContext; 5 | import org.apache.cordova.CordovaInterface; 6 | import org.apache.cordova.CordovaWebView; 7 | import org.apache.cordova.PluginResult; 8 | 9 | import android.os.Bundle; 10 | import android.telecom.DisconnectCause; 11 | import android.telecom.PhoneAccount; 12 | import android.telecom.PhoneAccountHandle; 13 | import android.telecom.TelecomManager; 14 | import android.content.ComponentName; 15 | import android.content.Intent; 16 | import android.content.Context; 17 | import android.content.pm.ApplicationInfo; 18 | import android.content.pm.PackageManager; 19 | import android.net.Uri; 20 | import android.Manifest; 21 | import android.telecom.Connection; 22 | import org.json.JSONArray; 23 | import org.json.JSONException; 24 | import java.util.ArrayList; 25 | import java.util.HashMap; 26 | import android.graphics.drawable.Icon; 27 | import android.media.AudioManager; 28 | 29 | public class CordovaCall extends CordovaPlugin { 30 | 31 | private static String TAG = "CordovaCall"; 32 | public static final int CALL_PHONE_REQ_CODE = 0; 33 | public static final int REAL_PHONE_CALL = 1; 34 | private int permissionCounter = 0; 35 | private String pendingAction; 36 | private TelecomManager tm; 37 | private PhoneAccountHandle handle; 38 | private PhoneAccount phoneAccount; 39 | private CallbackContext callbackContext; 40 | private String appName; 41 | private String from; 42 | private String to; 43 | private String realCallTo; 44 | private static HashMap> callbackContextMap = new HashMap>(); 45 | private static CordovaInterface cordovaInterface; 46 | private static CordovaWebView cordovaWebView; 47 | private static Icon icon; 48 | private static CordovaCall instance; 49 | 50 | public static HashMap> getCallbackContexts() { 51 | return callbackContextMap; 52 | } 53 | 54 | public static CordovaInterface getCordova() { 55 | return cordovaInterface; 56 | } 57 | 58 | public static CordovaWebView getWebView() { 59 | return cordovaWebView; 60 | } 61 | 62 | public static Icon getIcon() { 63 | return icon; 64 | } 65 | 66 | public static CordovaCall getInstance() { 67 | return instance; 68 | } 69 | 70 | @Override 71 | public void initialize(CordovaInterface cordova, CordovaWebView webView) { 72 | cordovaInterface = cordova; 73 | cordovaWebView = webView; 74 | super.initialize(cordova, webView); 75 | appName = getApplicationName(this.cordova.getActivity().getApplicationContext()); 76 | handle = new PhoneAccountHandle(new ComponentName(this.cordova.getActivity().getApplicationContext(),MyConnectionService.class),appName); 77 | tm = (TelecomManager)this.cordova.getActivity().getApplicationContext().getSystemService(this.cordova.getActivity().getApplicationContext().TELECOM_SERVICE); 78 | if(android.os.Build.VERSION.SDK_INT >= 26) { 79 | phoneAccount = new PhoneAccount.Builder(handle, appName) 80 | .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED) 81 | .build(); 82 | tm.registerPhoneAccount(phoneAccount); 83 | } 84 | if(android.os.Build.VERSION.SDK_INT >= 23) { 85 | phoneAccount = new PhoneAccount.Builder(handle, appName) 86 | .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) 87 | .build(); 88 | tm.registerPhoneAccount(phoneAccount); 89 | } 90 | callbackContextMap.put("answer",new ArrayList()); 91 | callbackContextMap.put("reject",new ArrayList()); 92 | callbackContextMap.put("hangup",new ArrayList()); 93 | callbackContextMap.put("sendCall",new ArrayList()); 94 | callbackContextMap.put("receiveCall",new ArrayList()); 95 | 96 | instance = this; 97 | } 98 | 99 | @Override 100 | public void onResume(boolean multitasking) { 101 | super.onResume(multitasking); 102 | this.checkCallPermission(); 103 | } 104 | 105 | @Override 106 | public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { 107 | this.callbackContext = callbackContext; 108 | if (action.equals("receiveCall")) { 109 | Connection conn = MyConnectionService.getConnection(); 110 | if(conn != null) { 111 | if(conn.getState() == Connection.STATE_ACTIVE) { 112 | this.callbackContext.error("You can't receive a call right now because you're already in a call"); 113 | } else { 114 | this.callbackContext.error("You can't receive a call right now"); 115 | } 116 | } else { 117 | from = args.getString(0); 118 | permissionCounter = 2; 119 | pendingAction = "receiveCall"; 120 | this.checkCallPermission(); 121 | } 122 | return true; 123 | } else if (action.equals("sendCall")) { 124 | Connection conn = MyConnectionService.getConnection(); 125 | if(conn != null) { 126 | if(conn.getState() == Connection.STATE_ACTIVE) { 127 | this.callbackContext.error("You can't make a call right now because you're already in a call"); 128 | } else if(conn.getState() == Connection.STATE_DIALING) { 129 | this.callbackContext.error("You can't make a call right now because you're already trying to make a call"); 130 | } else { 131 | this.callbackContext.error("You can't make a call right now"); 132 | } 133 | } else { 134 | to = args.getString(0); 135 | permissionCounter = 2; 136 | pendingAction = "sendCall"; 137 | this.checkCallPermission(); 138 | /*cordova.getThreadPool().execute(new Runnable() { 139 | public void run() { 140 | getCallPhonePermission(); 141 | } 142 | });*/ 143 | } 144 | return true; 145 | } else if (action.equals("connectCall")) { 146 | Connection conn = MyConnectionService.getConnection(); 147 | if(conn == null) { 148 | this.callbackContext.error("No call exists for you to connect"); 149 | } else if(conn.getState() == Connection.STATE_ACTIVE) { 150 | this.callbackContext.error("Your call is already connected"); 151 | } else { 152 | conn.setActive(); 153 | Intent intent = new Intent(this.cordova.getActivity().getApplicationContext(), this.cordova.getActivity().getClass()); 154 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_SINGLE_TOP); 155 | this.cordova.getActivity().getApplicationContext().startActivity(intent); 156 | this.callbackContext.success("Call connected successfully"); 157 | } 158 | return true; 159 | } else if (action.equals("endCall")) { 160 | Connection conn = MyConnectionService.getConnection(); 161 | if(conn == null) { 162 | this.callbackContext.error("No call exists for you to end"); 163 | } else { 164 | DisconnectCause cause = new DisconnectCause(DisconnectCause.LOCAL); 165 | conn.setDisconnected(cause); 166 | conn.destroy(); 167 | MyConnectionService.deinitConnection(); 168 | ArrayList callbackContexts = CordovaCall.getCallbackContexts().get("hangup"); 169 | for (final CallbackContext cbContext : callbackContexts) { 170 | cordova.getThreadPool().execute(new Runnable() { 171 | public void run() { 172 | PluginResult result = new PluginResult(PluginResult.Status.OK, "hangup event called successfully"); 173 | result.setKeepCallback(true); 174 | cbContext.sendPluginResult(result); 175 | } 176 | }); 177 | } 178 | this.callbackContext.success("Call ended successfully"); 179 | } 180 | return true; 181 | } else if (action.equals("registerEvent")) { 182 | String eventType = args.getString(0); 183 | ArrayList callbackContextList = callbackContextMap.get(eventType); 184 | callbackContextList.add(this.callbackContext); 185 | return true; 186 | } else if (action.equals("setAppName")) { 187 | String appName = args.getString(0); 188 | handle = new PhoneAccountHandle(new ComponentName(this.cordova.getActivity().getApplicationContext(),MyConnectionService.class),appName); 189 | if(android.os.Build.VERSION.SDK_INT >= 26) { 190 | phoneAccount = new PhoneAccount.Builder(handle, appName) 191 | .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED) 192 | .build(); 193 | tm.registerPhoneAccount(phoneAccount); 194 | } 195 | if(android.os.Build.VERSION.SDK_INT >= 23) { 196 | phoneAccount = new PhoneAccount.Builder(handle, appName) 197 | .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) 198 | .build(); 199 | tm.registerPhoneAccount(phoneAccount); 200 | } 201 | this.callbackContext.success("App Name Changed Successfully"); 202 | return true; 203 | } else if (action.equals("setIcon")) { 204 | String iconName = args.getString(0); 205 | int iconId = this.cordova.getActivity().getApplicationContext().getResources().getIdentifier(iconName, "drawable", this.cordova.getActivity().getPackageName()); 206 | if(iconId != 0) { 207 | icon = Icon.createWithResource(this.cordova.getActivity(), iconId); 208 | this.callbackContext.success("Icon Changed Successfully"); 209 | } else { 210 | this.callbackContext.error("This icon does not exist. Make sure to add it to the res/drawable folder the right way."); 211 | } 212 | return true; 213 | } else if (action.equals("mute")) { 214 | this.mute(); 215 | this.callbackContext.success("Muted Successfully"); 216 | return true; 217 | } else if (action.equals("unmute")) { 218 | this.unmute(); 219 | this.callbackContext.success("Unmuted Successfully"); 220 | return true; 221 | } else if (action.equals("speakerOn")) { 222 | this.speakerOn(); 223 | this.callbackContext.success("Speakerphone is on"); 224 | return true; 225 | } else if (action.equals("speakerOff")) { 226 | this.speakerOff(); 227 | this.callbackContext.success("Speakerphone is off"); 228 | return true; 229 | } else if (action.equals("callNumber")) { 230 | realCallTo = args.getString(0); 231 | if(realCallTo != null) { 232 | cordova.getThreadPool().execute(new Runnable() { 233 | public void run() { 234 | callNumberPhonePermission(); 235 | } 236 | }); 237 | this.callbackContext.success("Call Successful"); 238 | } else { 239 | this.callbackContext.error("Call Failed. You need to enter a phone number."); 240 | } 241 | return true; 242 | } 243 | return false; 244 | } 245 | 246 | private void checkCallPermission() { 247 | if(permissionCounter >= 1) { 248 | PhoneAccount currentPhoneAccount = tm.getPhoneAccount(handle); 249 | if(currentPhoneAccount.isEnabled()) { 250 | if(pendingAction == "receiveCall") { 251 | this.receiveCall(); 252 | } else if(pendingAction == "sendCall") { 253 | this.sendCall(); 254 | } 255 | } else { 256 | if(permissionCounter == 2) { 257 | Intent phoneIntent = new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS); 258 | phoneIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); 259 | this.cordova.getActivity().getApplicationContext().startActivity(phoneIntent); 260 | } else { 261 | this.callbackContext.error("You need to accept phone account permissions in order to send and receive calls"); 262 | } 263 | } 264 | } 265 | permissionCounter--; 266 | } 267 | 268 | private void receiveCall() { 269 | Bundle callInfo = new Bundle(); 270 | callInfo.putString("from",from); 271 | tm.addNewIncomingCall(handle, callInfo); 272 | permissionCounter = 0; 273 | this.callbackContext.success("Incoming call successful"); 274 | } 275 | 276 | private void sendCall() { 277 | Uri uri = Uri.fromParts("tel", to, null); 278 | Bundle callInfoBundle = new Bundle(); 279 | callInfoBundle.putString("to",to); 280 | Bundle callInfo = new Bundle(); 281 | callInfo.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS,callInfoBundle); 282 | callInfo.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle); 283 | callInfo.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, true); 284 | tm.placeCall(uri, callInfo); 285 | permissionCounter = 0; 286 | this.callbackContext.success("Outgoing call successful"); 287 | } 288 | 289 | private void mute() { 290 | AudioManager audioManager = (AudioManager) this.cordova.getActivity().getApplicationContext().getSystemService(Context.AUDIO_SERVICE); 291 | audioManager.setMicrophoneMute(true); 292 | } 293 | 294 | private void unmute() { 295 | AudioManager audioManager = (AudioManager) this.cordova.getActivity().getApplicationContext().getSystemService(Context.AUDIO_SERVICE); 296 | audioManager.setMicrophoneMute(false); 297 | } 298 | 299 | private void speakerOn() { 300 | AudioManager audioManager = (AudioManager) this.cordova.getActivity().getApplicationContext().getSystemService(Context.AUDIO_SERVICE); 301 | audioManager.setSpeakerphoneOn(true); 302 | } 303 | 304 | private void speakerOff() { 305 | AudioManager audioManager = (AudioManager) this.cordova.getActivity().getApplicationContext().getSystemService(Context.AUDIO_SERVICE); 306 | audioManager.setSpeakerphoneOn(false); 307 | } 308 | 309 | public static String getApplicationName(Context context) { 310 | ApplicationInfo applicationInfo = context.getApplicationInfo(); 311 | int stringId = applicationInfo.labelRes; 312 | return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : context.getString(stringId); 313 | } 314 | 315 | protected void getCallPhonePermission() { 316 | cordova.requestPermission(this, CALL_PHONE_REQ_CODE, Manifest.permission.CALL_PHONE); 317 | } 318 | 319 | protected void callNumberPhonePermission() { 320 | cordova.requestPermission(this, REAL_PHONE_CALL, Manifest.permission.CALL_PHONE); 321 | } 322 | 323 | private void callNumber() { 324 | try { 325 | Intent intent = new Intent(Intent.ACTION_CALL, Uri.fromParts("tel", realCallTo, null)); 326 | this.cordova.getActivity().getApplicationContext().startActivity(intent); 327 | } catch(Exception e) { 328 | this.callbackContext.error("Call Failed"); 329 | } 330 | this.callbackContext.success("Call Successful"); 331 | } 332 | 333 | @Override 334 | public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException 335 | { 336 | for(int r:grantResults) 337 | { 338 | if(r == PackageManager.PERMISSION_DENIED) 339 | { 340 | this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, "CALL_PHONE Permission Denied")); 341 | return; 342 | } 343 | } 344 | switch(requestCode) 345 | { 346 | case CALL_PHONE_REQ_CODE: 347 | this.sendCall(); 348 | break; 349 | case REAL_PHONE_CALL: 350 | this.callNumber(); 351 | break; 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/android/MyConnectionService.java: -------------------------------------------------------------------------------- 1 | package com.dmarc.cordovacall; 2 | 3 | import org.apache.cordova.CallbackContext; 4 | import org.apache.cordova.PluginResult; 5 | import android.content.Intent; 6 | import android.graphics.drawable.Icon; 7 | import android.os.Bundle; 8 | import android.telecom.Connection; 9 | import android.telecom.ConnectionRequest; 10 | import android.telecom.ConnectionService; 11 | import android.telecom.DisconnectCause; 12 | import android.telecom.PhoneAccountHandle; 13 | import android.telecom.StatusHints; 14 | import android.telecom.TelecomManager; 15 | import android.os.Handler; 16 | import android.net.Uri; 17 | import java.util.ArrayList; 18 | import android.util.Log; 19 | 20 | public class MyConnectionService extends ConnectionService { 21 | 22 | private static String TAG = "MyConnectionService"; 23 | private static Connection conn; 24 | 25 | public static Connection getConnection() { 26 | return conn; 27 | } 28 | 29 | public static void deinitConnection() { 30 | conn = null; 31 | } 32 | 33 | @Override 34 | public Connection onCreateIncomingConnection(final PhoneAccountHandle connectionManagerPhoneAccount, final ConnectionRequest request) { 35 | final Connection connection = new Connection() { 36 | @Override 37 | public void onAnswer() { 38 | this.setActive(); 39 | Intent intent = new Intent(CordovaCall.getCordova().getActivity().getApplicationContext(), CordovaCall.getCordova().getActivity().getClass()); 40 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_SINGLE_TOP); 41 | CordovaCall.getCordova().getActivity().getApplicationContext().startActivity(intent); 42 | ArrayList callbackContexts = CordovaCall.getCallbackContexts().get("answer"); 43 | for (final CallbackContext callbackContext : callbackContexts) { 44 | CordovaCall.getCordova().getThreadPool().execute(new Runnable() { 45 | public void run() { 46 | PluginResult result = new PluginResult(PluginResult.Status.OK, "answer event called successfully"); 47 | result.setKeepCallback(true); 48 | callbackContext.sendPluginResult(result); 49 | } 50 | }); 51 | } 52 | } 53 | 54 | @Override 55 | public void onReject() { 56 | DisconnectCause cause = new DisconnectCause(DisconnectCause.REJECTED); 57 | this.setDisconnected(cause); 58 | this.destroy(); 59 | conn = null; 60 | ArrayList callbackContexts = CordovaCall.getCallbackContexts().get("reject"); 61 | for (final CallbackContext callbackContext : callbackContexts) { 62 | CordovaCall.getCordova().getThreadPool().execute(new Runnable() { 63 | public void run() { 64 | PluginResult result = new PluginResult(PluginResult.Status.OK, "reject event called successfully"); 65 | result.setKeepCallback(true); 66 | callbackContext.sendPluginResult(result); 67 | } 68 | }); 69 | } 70 | } 71 | 72 | @Override 73 | public void onAbort() { 74 | super.onAbort(); 75 | } 76 | 77 | @Override 78 | public void onDisconnect() { 79 | DisconnectCause cause = new DisconnectCause(DisconnectCause.LOCAL); 80 | this.setDisconnected(cause); 81 | this.destroy(); 82 | conn = null; 83 | ArrayList callbackContexts = CordovaCall.getCallbackContexts().get("hangup"); 84 | for (final CallbackContext callbackContext : callbackContexts) { 85 | CordovaCall.getCordova().getThreadPool().execute(new Runnable() { 86 | public void run() { 87 | PluginResult result = new PluginResult(PluginResult.Status.OK, "hangup event called successfully"); 88 | result.setKeepCallback(true); 89 | callbackContext.sendPluginResult(result); 90 | } 91 | }); 92 | } 93 | } 94 | }; 95 | connection.setAddress(Uri.parse(request.getExtras().getString("from")), TelecomManager.PRESENTATION_ALLOWED); 96 | Icon icon = CordovaCall.getIcon(); 97 | if(icon != null) { 98 | StatusHints statusHints = new StatusHints((CharSequence)"", icon, new Bundle()); 99 | connection.setStatusHints(statusHints); 100 | } 101 | conn = connection; 102 | ArrayList callbackContexts = CordovaCall.getCallbackContexts().get("receiveCall"); 103 | for (final CallbackContext callbackContext : callbackContexts) { 104 | CordovaCall.getCordova().getThreadPool().execute(new Runnable() { 105 | public void run() { 106 | PluginResult result = new PluginResult(PluginResult.Status.OK, "receiveCall event called successfully"); 107 | result.setKeepCallback(true); 108 | callbackContext.sendPluginResult(result); 109 | } 110 | }); 111 | } 112 | return connection; 113 | } 114 | 115 | @Override 116 | public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { 117 | final Connection connection = new Connection() { 118 | @Override 119 | public void onAnswer() { 120 | super.onAnswer(); 121 | } 122 | 123 | @Override 124 | public void onReject() { 125 | super.onReject(); 126 | } 127 | 128 | @Override 129 | public void onAbort() { 130 | super.onAbort(); 131 | } 132 | 133 | @Override 134 | public void onDisconnect() { 135 | DisconnectCause cause = new DisconnectCause(DisconnectCause.LOCAL); 136 | this.setDisconnected(cause); 137 | this.destroy(); 138 | conn = null; 139 | ArrayList callbackContexts = CordovaCall.getCallbackContexts().get("hangup"); 140 | for (final CallbackContext callbackContext : callbackContexts) { 141 | CordovaCall.getCordova().getThreadPool().execute(new Runnable() { 142 | public void run() { 143 | PluginResult result = new PluginResult(PluginResult.Status.OK, "hangup event called successfully"); 144 | result.setKeepCallback(true); 145 | callbackContext.sendPluginResult(result); 146 | } 147 | }); 148 | } 149 | } 150 | 151 | @Override 152 | public void onStateChanged(int state) { 153 | if(state == Connection.STATE_DIALING) { 154 | final Handler handler = new Handler(); 155 | handler.postDelayed(new Runnable() { 156 | @Override 157 | public void run() { 158 | Intent intent = new Intent(CordovaCall.getCordova().getActivity().getApplicationContext(), CordovaCall.getCordova().getActivity().getClass()); 159 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_SINGLE_TOP); 160 | CordovaCall.getCordova().getActivity().getApplicationContext().startActivity(intent); 161 | } 162 | }, 500); 163 | } 164 | } 165 | }; 166 | connection.setAddress(Uri.parse(request.getExtras().getString("to")), TelecomManager.PRESENTATION_ALLOWED); 167 | Icon icon = CordovaCall.getIcon(); 168 | if(icon != null) { 169 | StatusHints statusHints = new StatusHints((CharSequence)"", icon, new Bundle()); 170 | connection.setStatusHints(statusHints); 171 | } 172 | connection.setDialing(); 173 | conn = connection; 174 | ArrayList callbackContexts = CordovaCall.getCallbackContexts().get("sendCall"); 175 | if(callbackContexts != null) { 176 | for (final CallbackContext callbackContext : callbackContexts) { 177 | CordovaCall.getCordova().getThreadPool().execute(new Runnable() { 178 | public void run() { 179 | PluginResult result = new PluginResult(PluginResult.Status.OK, "sendCall event called successfully"); 180 | result.setKeepCallback(true); 181 | callbackContext.sendPluginResult(result); 182 | } 183 | }); 184 | } 185 | } 186 | return connection; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/ios/AppDelegateCordovaCall.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import "Intents/Intents.h" 3 | #import 4 | #import 5 | 6 | @implementation AppDelegate (CordovaCall) 7 | 8 | - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler 9 | { 10 | INInteraction *interaction = userActivity.interaction; 11 | INIntent *intent = interaction.intent; 12 | BOOL isVideo = [intent isKindOfClass:[INStartVideoCallIntent class]]; 13 | INPerson *contact; 14 | if(isVideo) { 15 | INStartVideoCallIntent *startCallIntent = (INStartVideoCallIntent *)intent; 16 | contact = startCallIntent.contacts.firstObject; 17 | } else { 18 | INStartAudioCallIntent *startCallIntent = (INStartAudioCallIntent *)intent; 19 | contact = startCallIntent.contacts.firstObject; 20 | } 21 | INPersonHandle *personHandle = contact.personHandle; 22 | NSString *callId = personHandle.value; 23 | NSString *callName = [[NSUserDefaults standardUserDefaults] stringForKey:callId]; 24 | if(!callName) { 25 | callName = callId; 26 | } 27 | NSDictionary *intentInfo = @{ @"callName" : callName, @"callId" : callId, @"isVideo" : isVideo?@YES:@NO}; 28 | [[NSNotificationCenter defaultCenter] postNotificationName:@"RecentsCallNotification" object:intentInfo]; 29 | return YES; 30 | } 31 | @end 32 | -------------------------------------------------------------------------------- /src/ios/CordovaCall.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface CordovaCall : CDVPlugin 6 | 7 | // PushKit 8 | @property (nonatomic, copy) NSString *VoIPPushCallbackId; 9 | @property (nonatomic, copy) NSString *VoIPPushClassName; 10 | @property (nonatomic, copy) NSString *VoIPPushMethodName; 11 | 12 | - (void)init:(CDVInvokedUrlCommand*)command; 13 | 14 | // CallKit 15 | @property (nonatomic, strong) CXProvider *provider; 16 | @property (nonatomic, strong) CXCallController *callController; 17 | 18 | - (void)updateProviderConfig; 19 | - (void)setupAudioSession; 20 | 21 | - (void)setAppName:(CDVInvokedUrlCommand*)command; 22 | - (void)setIcon:(CDVInvokedUrlCommand*)command; 23 | - (void)setRingtone:(CDVInvokedUrlCommand*)command; 24 | - (void)setIncludeInRecents:(CDVInvokedUrlCommand*)command; 25 | - (void)setDTMFState:(CDVInvokedUrlCommand*)command; 26 | - (void)setVideo:(CDVInvokedUrlCommand*)command; 27 | 28 | - (void)receiveCall:(CDVInvokedUrlCommand*)command; 29 | - (void)sendCall:(CDVInvokedUrlCommand*)command; 30 | - (void)connectCall:(CDVInvokedUrlCommand*)command; 31 | - (void)endCall:(CDVInvokedUrlCommand*)command; 32 | - (void)registerEvent:(CDVInvokedUrlCommand*)command; 33 | - (void)mute:(CDVInvokedUrlCommand*)command; 34 | - (void)unmute:(CDVInvokedUrlCommand*)command; 35 | - (void)speakerOn:(CDVInvokedUrlCommand*)command; 36 | - (void)speakerOff:(CDVInvokedUrlCommand*)command; 37 | - (void)callNumber:(CDVInvokedUrlCommand*)command; 38 | 39 | - (void)receiveCallFromRecents:(NSNotification *) notification; 40 | - (void)handleAudioRouteChange:(NSNotification *) notification; 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /src/ios/CordovaCall.m: -------------------------------------------------------------------------------- 1 | #import "CordovaCall.h" 2 | #import 3 | #import 4 | 5 | @implementation CordovaCall 6 | 7 | @synthesize VoIPPushCallbackId, VoIPPushClassName, VoIPPushMethodName; 8 | 9 | BOOL hasVideo = NO; 10 | NSString* appName; 11 | NSString* ringtone; 12 | NSString* icon; 13 | BOOL includeInRecents = NO; 14 | NSMutableDictionary *callbackIds; 15 | NSDictionary* pendingCallFromRecents; 16 | BOOL monitorAudioRouteChange = NO; 17 | BOOL enableDTMF = NO; 18 | 19 | - (void)pluginInitialize 20 | { 21 | CXProviderConfiguration *providerConfiguration; 22 | appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; 23 | providerConfiguration = [[CXProviderConfiguration alloc] initWithLocalizedName:appName]; 24 | providerConfiguration.maximumCallGroups = 1; 25 | providerConfiguration.maximumCallsPerCallGroup = 1; 26 | NSMutableSet *handleTypes = [[NSMutableSet alloc] init]; 27 | [handleTypes addObject:@(CXHandleTypePhoneNumber)]; 28 | providerConfiguration.supportedHandleTypes = handleTypes; 29 | providerConfiguration.supportsVideo = YES; 30 | if (@available(iOS 11.0, *)) { 31 | providerConfiguration.includesCallsInRecents = NO; 32 | } 33 | self.provider = [[CXProvider alloc] initWithConfiguration:providerConfiguration]; 34 | [self.provider setDelegate:self queue:nil]; 35 | self.callController = [[CXCallController alloc] init]; 36 | //initialize callback dictionary 37 | callbackIds = [[NSMutableDictionary alloc]initWithCapacity:5]; 38 | [callbackIds setObject:[NSMutableArray array] forKey:@"answer"]; 39 | [callbackIds setObject:[NSMutableArray array] forKey:@"reject"]; 40 | [callbackIds setObject:[NSMutableArray array] forKey:@"hangup"]; 41 | [callbackIds setObject:[NSMutableArray array] forKey:@"sendCall"]; 42 | [callbackIds setObject:[NSMutableArray array] forKey:@"receiveCall"]; 43 | [callbackIds setObject:[NSMutableArray array] forKey:@"mute"]; 44 | [callbackIds setObject:[NSMutableArray array] forKey:@"unmute"]; 45 | [callbackIds setObject:[NSMutableArray array] forKey:@"speakerOn"]; 46 | [callbackIds setObject:[NSMutableArray array] forKey:@"speakerOff"]; 47 | [callbackIds setObject:[NSMutableArray array] forKey:@"DTMF"]; 48 | //allows user to make call from recents 49 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveCallFromRecents:) name:@"RecentsCallNotification" object:nil]; 50 | //detect Audio Route Changes to make speakerOn and speakerOff event handlers 51 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioRouteChange:) name:AVAudioSessionRouteChangeNotification object:nil]; 52 | } 53 | 54 | // CallKit - Interface 55 | - (void)updateProviderConfig 56 | { 57 | CXProviderConfiguration *providerConfiguration; 58 | providerConfiguration = [[CXProviderConfiguration alloc] initWithLocalizedName:appName]; 59 | providerConfiguration.maximumCallGroups = 1; 60 | providerConfiguration.maximumCallsPerCallGroup = 1; 61 | if(ringtone != nil) { 62 | providerConfiguration.ringtoneSound = ringtone; 63 | } 64 | if(icon != nil) { 65 | UIImage *iconImage = [UIImage imageNamed:icon]; 66 | NSData *iconData = UIImagePNGRepresentation(iconImage); 67 | providerConfiguration.iconTemplateImageData = iconData; 68 | } 69 | NSMutableSet *handleTypes = [[NSMutableSet alloc] init]; 70 | [handleTypes addObject:@(CXHandleTypePhoneNumber)]; 71 | providerConfiguration.supportedHandleTypes = handleTypes; 72 | providerConfiguration.supportsVideo = hasVideo; 73 | if (@available(iOS 11.0, *)) { 74 | providerConfiguration.includesCallsInRecents = includeInRecents; 75 | } 76 | 77 | self.provider.configuration = providerConfiguration; 78 | } 79 | 80 | - (void)setupAudioSession 81 | { 82 | @try { 83 | AVAudioSession *sessionInstance = [AVAudioSession sharedInstance]; 84 | [sessionInstance setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; 85 | [sessionInstance setMode:AVAudioSessionModeVoiceChat error:nil]; 86 | NSTimeInterval bufferDuration = .005; 87 | [sessionInstance setPreferredIOBufferDuration:bufferDuration error:nil]; 88 | [sessionInstance setPreferredSampleRate:44100 error:nil]; 89 | NSLog(@"Configuring Audio"); 90 | } 91 | @catch (NSException *exception) { 92 | NSLog(@"Unknown error returned from setupAudioSession"); 93 | } 94 | return; 95 | } 96 | 97 | - (void)setAppName:(CDVInvokedUrlCommand*)command 98 | { 99 | CDVPluginResult* pluginResult = nil; 100 | NSString* proposedAppName = [command.arguments objectAtIndex:0]; 101 | 102 | if (proposedAppName != nil && [proposedAppName length] > 0) { 103 | appName = proposedAppName; 104 | [self updateProviderConfig]; 105 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"App Name Changed Successfully"]; 106 | } else { 107 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"App Name Can't Be Empty"]; 108 | } 109 | 110 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 111 | } 112 | 113 | - (void)setIcon:(CDVInvokedUrlCommand*)command 114 | { 115 | CDVPluginResult* pluginResult = nil; 116 | NSString* proposedIconName = [command.arguments objectAtIndex:0]; 117 | 118 | if (proposedIconName == nil || [proposedIconName length] == 0) { 119 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Icon Name Can't Be Empty"]; 120 | } else if([UIImage imageNamed:proposedIconName] == nil) { 121 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"This icon does not exist. Make sure to add it to your project the right way."]; 122 | } else { 123 | icon = proposedIconName; 124 | [self updateProviderConfig]; 125 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Icon Changed Successfully"]; 126 | } 127 | 128 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 129 | } 130 | 131 | - (void)setRingtone:(CDVInvokedUrlCommand*)command 132 | { 133 | CDVPluginResult* pluginResult = nil; 134 | NSString* proposedRingtoneName = [command.arguments objectAtIndex:0]; 135 | 136 | if (proposedRingtoneName == nil || [proposedRingtoneName length] == 0) { 137 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Ringtone Name Can't Be Empty"]; 138 | } else { 139 | ringtone = [NSString stringWithFormat: @"%@.caf", proposedRingtoneName]; 140 | [self updateProviderConfig]; 141 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Ringtone Changed Successfully"]; 142 | } 143 | 144 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 145 | } 146 | 147 | - (void)setIncludeInRecents:(CDVInvokedUrlCommand*)command 148 | { 149 | CDVPluginResult* pluginResult = nil; 150 | includeInRecents = [[command.arguments objectAtIndex:0] boolValue]; 151 | [self updateProviderConfig]; 152 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"includeInRecents Changed Successfully"]; 153 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 154 | } 155 | 156 | - (void)setDTMFState:(CDVInvokedUrlCommand*)command 157 | { 158 | CDVPluginResult* pluginResult = nil; 159 | enableDTMF = [[command.arguments objectAtIndex:0] boolValue]; 160 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"enableDTMF Changed Successfully"]; 161 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 162 | } 163 | 164 | - (void)setVideo:(CDVInvokedUrlCommand*)command 165 | { 166 | CDVPluginResult* pluginResult = nil; 167 | hasVideo = [[command.arguments objectAtIndex:0] boolValue]; 168 | [self updateProviderConfig]; 169 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"hasVideo Changed Successfully"]; 170 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 171 | } 172 | 173 | - (void)receiveCall:(CDVInvokedUrlCommand*)command 174 | { 175 | BOOL hasId = ![[command.arguments objectAtIndex:1] isEqual:[NSNull null]]; 176 | CDVPluginResult* pluginResult = nil; 177 | NSString* callName = [command.arguments objectAtIndex:0]; 178 | NSString* callId = hasId?[command.arguments objectAtIndex:1]:callName; 179 | NSUUID *callUUID = [[NSUUID alloc] init]; 180 | 181 | if (hasId) { 182 | [[NSUserDefaults standardUserDefaults] setObject:callName forKey:[command.arguments objectAtIndex:1]]; 183 | [[NSUserDefaults standardUserDefaults] synchronize]; 184 | } 185 | 186 | if (callName != nil && [callName length] > 0) { 187 | CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:callId]; 188 | CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; 189 | callUpdate.remoteHandle = handle; 190 | callUpdate.hasVideo = hasVideo; 191 | callUpdate.localizedCallerName = callName; 192 | callUpdate.supportsGrouping = NO; 193 | callUpdate.supportsUngrouping = NO; 194 | callUpdate.supportsHolding = NO; 195 | callUpdate.supportsDTMF = enableDTMF; 196 | 197 | [self.provider reportNewIncomingCallWithUUID:callUUID update:callUpdate completion:^(NSError * _Nullable error) { 198 | if(error == nil) { 199 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Incoming call successful"] callbackId:command.callbackId]; 200 | } else { 201 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error localizedDescription]] callbackId:command.callbackId]; 202 | } 203 | }]; 204 | for (id callbackId in callbackIds[@"receiveCall"]) { 205 | CDVPluginResult* pluginResult = nil; 206 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"receiveCall event called successfully"]; 207 | [pluginResult setKeepCallbackAsBool:YES]; 208 | [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; 209 | } 210 | } else { 211 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Caller id can't be empty"] callbackId:command.callbackId]; 212 | } 213 | } 214 | 215 | - (void)sendCall:(CDVInvokedUrlCommand*)command 216 | { 217 | BOOL hasId = ![[command.arguments objectAtIndex:1] isEqual:[NSNull null]]; 218 | NSString* callName = [command.arguments objectAtIndex:0]; 219 | NSString* callId = hasId?[command.arguments objectAtIndex:1]:callName; 220 | NSUUID *callUUID = [[NSUUID alloc] init]; 221 | 222 | if (hasId) { 223 | [[NSUserDefaults standardUserDefaults] setObject:callName forKey:[command.arguments objectAtIndex:1]]; 224 | [[NSUserDefaults standardUserDefaults] synchronize]; 225 | } 226 | 227 | if (callName != nil && [callName length] > 0) { 228 | CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:callId]; 229 | CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:callUUID handle:handle]; 230 | startCallAction.contactIdentifier = callName; 231 | startCallAction.video = hasVideo; 232 | CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction]; 233 | [self.callController requestTransaction:transaction completion:^(NSError * _Nullable error) { 234 | if (error == nil) { 235 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Outgoing call successful"] callbackId:command.callbackId]; 236 | } else { 237 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error localizedDescription]] callbackId:command.callbackId]; 238 | } 239 | }]; 240 | } else { 241 | [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"The caller id can't be empty"] callbackId:command.callbackId]; 242 | } 243 | } 244 | 245 | - (void)connectCall:(CDVInvokedUrlCommand*)command 246 | { 247 | CDVPluginResult* pluginResult = nil; 248 | NSArray *calls = self.callController.callObserver.calls; 249 | 250 | if([calls count] == 1) { 251 | [self.provider reportOutgoingCallWithUUID:calls[0].UUID connectedAtDate:nil]; 252 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Call connected successfully"]; 253 | } else { 254 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No call exists for you to connect"]; 255 | } 256 | 257 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 258 | } 259 | 260 | - (void)endCall:(CDVInvokedUrlCommand*)command 261 | { 262 | CDVPluginResult* pluginResult = nil; 263 | NSArray *calls = self.callController.callObserver.calls; 264 | 265 | if([calls count] == 1) { 266 | //[self.provider reportCallWithUUID:calls[0].UUID endedAtDate:nil reason:CXCallEndedReasonRemoteEnded]; 267 | CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:calls[0].UUID]; 268 | CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; 269 | [self.callController requestTransaction:transaction completion:^(NSError * _Nullable error) { 270 | if (error == nil) { 271 | } else { 272 | NSLog(@"%@",[error localizedDescription]); 273 | } 274 | }]; 275 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Call ended successfully"]; 276 | } else { 277 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No call exists for you to connect"]; 278 | } 279 | 280 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 281 | } 282 | 283 | - (void)registerEvent:(CDVInvokedUrlCommand*)command 284 | { 285 | NSString* eventName = [command.arguments objectAtIndex:0]; 286 | if(callbackIds[eventName] != nil) { 287 | [callbackIds[eventName] addObject:command.callbackId]; 288 | } 289 | if(pendingCallFromRecents && [eventName isEqual:@"sendCall"]) { 290 | NSDictionary *callData = pendingCallFromRecents; 291 | CDVPluginResult* pluginResult = nil; 292 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:callData]; 293 | [pluginResult setKeepCallbackAsBool:YES]; 294 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 295 | } 296 | } 297 | 298 | - (void)mute:(CDVInvokedUrlCommand*)command 299 | { 300 | CDVPluginResult* pluginResult = nil; 301 | AVAudioSession *sessionInstance = [AVAudioSession sharedInstance]; 302 | if(sessionInstance.isInputGainSettable) { 303 | BOOL success = [sessionInstance setInputGain:0.0 error:nil]; 304 | if(success) { 305 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Muted Successfully"]; 306 | } else { 307 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"An error occurred"]; 308 | } 309 | } else { 310 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Not muted because this device does not allow changing inputGain"]; 311 | } 312 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 313 | } 314 | 315 | - (void)unmute:(CDVInvokedUrlCommand*)command 316 | { 317 | CDVPluginResult* pluginResult = nil; 318 | AVAudioSession *sessionInstance = [AVAudioSession sharedInstance]; 319 | if(sessionInstance.isInputGainSettable) { 320 | BOOL success = [sessionInstance setInputGain:1.0 error:nil]; 321 | if(success) { 322 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Muted Successfully"]; 323 | } else { 324 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"An error occurred"]; 325 | } 326 | } else { 327 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Not unmuted because this device does not allow changing inputGain"]; 328 | } 329 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 330 | } 331 | 332 | - (void)speakerOn:(CDVInvokedUrlCommand*)command 333 | { 334 | CDVPluginResult* pluginResult = nil; 335 | AVAudioSession *sessionInstance = [AVAudioSession sharedInstance]; 336 | BOOL success = [sessionInstance overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil]; 337 | if(success) { 338 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Speakerphone is on"]; 339 | } else { 340 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"An error occurred"]; 341 | } 342 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 343 | } 344 | 345 | - (void)speakerOff:(CDVInvokedUrlCommand*)command 346 | { 347 | CDVPluginResult* pluginResult = nil; 348 | AVAudioSession *sessionInstance = [AVAudioSession sharedInstance]; 349 | BOOL success = [sessionInstance overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil]; 350 | if(success) { 351 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Speakerphone is off"]; 352 | } else { 353 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"An error occurred"]; 354 | } 355 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 356 | } 357 | 358 | - (void)callNumber:(CDVInvokedUrlCommand*)command 359 | { 360 | CDVPluginResult* pluginResult = nil; 361 | NSString* phoneNumber = [command.arguments objectAtIndex:0]; 362 | NSString* telNumber = [@"tel://" stringByAppendingString:phoneNumber]; 363 | if (@available(iOS 10.0, *)) { 364 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:telNumber] 365 | options:nil 366 | completionHandler:^(BOOL success) { 367 | if(success) { 368 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Call Successful"]; 369 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 370 | } else { 371 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Call Failed"]; 372 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 373 | } 374 | }]; 375 | } else { 376 | BOOL success = [[UIApplication sharedApplication] openURL:[NSURL URLWithString:telNumber]]; 377 | if(success) { 378 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Call Successful"]; 379 | } else { 380 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Call Failed"]; 381 | } 382 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 383 | } 384 | 385 | } 386 | 387 | - (void)receiveCallFromRecents:(NSNotification *) notification 388 | { 389 | NSString* callID = notification.object[@"callId"]; 390 | NSString* callName = notification.object[@"callName"]; 391 | NSUUID *callUUID = [[NSUUID alloc] init]; 392 | CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:callID]; 393 | CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:callUUID handle:handle]; 394 | startCallAction.video = [notification.object[@"isVideo"] boolValue]?YES:NO; 395 | startCallAction.contactIdentifier = callName; 396 | CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction]; 397 | [self.callController requestTransaction:transaction completion:^(NSError * _Nullable error) { 398 | if (error == nil) { 399 | } else { 400 | NSLog(@"%@",[error localizedDescription]); 401 | } 402 | }]; 403 | } 404 | 405 | - (void)handleAudioRouteChange:(NSNotification *) notification 406 | { 407 | if(monitorAudioRouteChange) { 408 | NSNumber* reasonValue = notification.userInfo[@"AVAudioSessionRouteChangeReasonKey"]; 409 | AVAudioSessionRouteDescription* previousRouteKey = notification.userInfo[@"AVAudioSessionRouteChangePreviousRouteKey"]; 410 | NSArray* outputs = [previousRouteKey outputs]; 411 | if([outputs count] > 0) { 412 | AVAudioSessionPortDescription *output = outputs[0]; 413 | if(![output.portType isEqual: @"Speaker"] && [reasonValue isEqual:@4]) { 414 | for (id callbackId in callbackIds[@"speakerOn"]) { 415 | CDVPluginResult* pluginResult = nil; 416 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"speakerOn event called successfully"]; 417 | [pluginResult setKeepCallbackAsBool:YES]; 418 | [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; 419 | } 420 | } else if([output.portType isEqual: @"Speaker"] && [reasonValue isEqual:@3]) { 421 | for (id callbackId in callbackIds[@"speakerOff"]) { 422 | CDVPluginResult* pluginResult = nil; 423 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"speakerOff event called successfully"]; 424 | [pluginResult setKeepCallbackAsBool:YES]; 425 | [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; 426 | } 427 | } 428 | } 429 | } 430 | } 431 | 432 | // CallKit - Provider 433 | - (void)providerDidReset:(CXProvider *)provider 434 | { 435 | NSLog(@"%s","providerdidreset"); 436 | } 437 | 438 | - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action 439 | { 440 | [self setupAudioSession]; 441 | CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; 442 | callUpdate.remoteHandle = action.handle; 443 | callUpdate.hasVideo = action.video; 444 | callUpdate.localizedCallerName = action.contactIdentifier; 445 | callUpdate.supportsGrouping = NO; 446 | callUpdate.supportsUngrouping = NO; 447 | callUpdate.supportsHolding = NO; 448 | callUpdate.supportsDTMF = enableDTMF; 449 | 450 | [self.provider reportCallWithUUID:action.callUUID updated:callUpdate]; 451 | [action fulfill]; 452 | NSDictionary *callData = @{@"callName":action.contactIdentifier, @"callId": action.handle.value, @"isVideo": action.video?@YES:@NO, @"message": @"sendCall event called successfully"}; 453 | for (id callbackId in callbackIds[@"sendCall"]) { 454 | CDVPluginResult* pluginResult = nil; 455 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:callData]; 456 | [pluginResult setKeepCallbackAsBool:YES]; 457 | [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; 458 | } 459 | if([callbackIds[@"sendCall"] count] == 0) { 460 | pendingCallFromRecents = callData; 461 | } 462 | //[action fail]; 463 | } 464 | 465 | - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession 466 | { 467 | NSLog(@"activated audio"); 468 | monitorAudioRouteChange = YES; 469 | } 470 | 471 | - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession 472 | { 473 | NSLog(@"deactivated audio"); 474 | } 475 | 476 | - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action 477 | { 478 | [self setupAudioSession]; 479 | [action fulfill]; 480 | for (id callbackId in callbackIds[@"answer"]) { 481 | CDVPluginResult* pluginResult = nil; 482 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"answer event called successfully"]; 483 | [pluginResult setKeepCallbackAsBool:YES]; 484 | [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; 485 | } 486 | //[action fail]; 487 | } 488 | 489 | - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action 490 | { 491 | NSArray *calls = self.callController.callObserver.calls; 492 | if([calls count] == 1) { 493 | if(calls[0].hasConnected) { 494 | for (id callbackId in callbackIds[@"hangup"]) { 495 | CDVPluginResult* pluginResult = nil; 496 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"hangup event called successfully"]; 497 | [pluginResult setKeepCallbackAsBool:YES]; 498 | [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; 499 | } 500 | } else { 501 | for (id callbackId in callbackIds[@"reject"]) { 502 | CDVPluginResult* pluginResult = nil; 503 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"reject event called successfully"]; 504 | [pluginResult setKeepCallbackAsBool:YES]; 505 | [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; 506 | } 507 | } 508 | } 509 | monitorAudioRouteChange = NO; 510 | [action fulfill]; 511 | //[action fail]; 512 | } 513 | 514 | - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action 515 | { 516 | [action fulfill]; 517 | BOOL isMuted = action.muted; 518 | for (id callbackId in callbackIds[isMuted?@"mute":@"unmute"]) { 519 | CDVPluginResult* pluginResult = nil; 520 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:isMuted?@"mute event called successfully":@"unmute event called successfully"]; 521 | [pluginResult setKeepCallbackAsBool:YES]; 522 | [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; 523 | } 524 | //[action fail]; 525 | } 526 | 527 | - (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action 528 | { 529 | NSLog(@"DTMF Event"); 530 | NSString *digits = action.digits; 531 | [action fulfill]; 532 | for (id callbackId in callbackIds[@"DTMF"]) { 533 | CDVPluginResult* pluginResult = nil; 534 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:digits]; 535 | [pluginResult setKeepCallbackAsBool:YES]; 536 | [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; 537 | } 538 | } 539 | 540 | // PushKit 541 | - (void)init:(CDVInvokedUrlCommand*)command 542 | { 543 | self.VoIPPushCallbackId = command.callbackId; 544 | NSLog(@"[objC] callbackId: %@", self.VoIPPushCallbackId); 545 | 546 | //http://stackoverflow.com/questions/27245808/implement-pushkit-and-test-in-development-behavior/28562124#28562124 547 | PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; 548 | pushRegistry.delegate = self; 549 | pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; 550 | } 551 | 552 | - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type{ 553 | if([credentials.token length] == 0) { 554 | NSLog(@"[objC] No device token!"); 555 | return; 556 | } 557 | 558 | //http://stackoverflow.com/a/9372848/534755 559 | NSLog(@"[objC] Device token: %@", credentials.token); 560 | const unsigned *tokenBytes = [credentials.token bytes]; 561 | NSString *sToken = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x", 562 | ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]), 563 | ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]), 564 | ntohl(tokenBytes[6]), ntohl(tokenBytes[7])]; 565 | 566 | NSMutableDictionary* results = [NSMutableDictionary dictionaryWithCapacity:2]; 567 | [results setObject:sToken forKey:@"deviceToken"]; 568 | [results setObject:@"true" forKey:@"registration"]; 569 | 570 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:results]; 571 | [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; //[pluginResult setKeepCallbackAsBool:YES]; 572 | [self.commandDelegate sendPluginResult:pluginResult callbackId:self.VoIPPushCallbackId]; 573 | } 574 | 575 | - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type 576 | { 577 | NSDictionary *payloadDict = payload.dictionaryPayload[@"aps"]; 578 | NSLog(@"[objC] didReceiveIncomingPushWithPayload: %@", payloadDict); 579 | 580 | NSString *message = payloadDict[@"alert"]; 581 | NSLog(@"[objC] received VoIP message: %@", message); 582 | 583 | NSString *data = payload.dictionaryPayload[@"data"]; 584 | NSLog(@"[objC] received data: %@", data); 585 | 586 | NSMutableDictionary* results = [NSMutableDictionary dictionaryWithCapacity:2]; 587 | [results setObject:message forKey:@"function"]; 588 | [results setObject:data forKey:@"extra"]; 589 | 590 | @try { 591 | NSError* error; 592 | NSDictionary* json = [NSJSONSerialization JSONObjectWithData:[data dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; 593 | 594 | NSObject* caller = [json objectForKey:@"Caller"]; 595 | NSArray* args = [NSArray arrayWithObjects:[caller valueForKey:@"Username"], [caller valueForKey:@"ConnectionId"], nil]; 596 | 597 | CDVInvokedUrlCommand* newCommand = [[CDVInvokedUrlCommand alloc] initWithArguments:args callbackId:@"" className:self.VoIPPushClassName methodName:self.VoIPPushMethodName]; 598 | 599 | [self receiveCall:newCommand]; 600 | } 601 | @catch (NSException *exception) { 602 | NSLog(@"[objC] error: %@", exception.reason); 603 | } 604 | @finally { 605 | CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:results]; 606 | [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; 607 | [self.commandDelegate sendPluginResult:pluginResult callbackId:self.VoIPPushCallbackId]; 608 | } 609 | } 610 | 611 | @end 612 | -------------------------------------------------------------------------------- /www/CordovaCall.js: -------------------------------------------------------------------------------- 1 | var exec = require('cordova/exec'); 2 | 3 | exports.setAppName = function(appName, success, error) { 4 | exec(success, error, "CordovaCall", "setAppName", [appName]); 5 | }; 6 | 7 | exports.setIcon = function(iconName, success, error) { 8 | exec(success, error, "CordovaCall", "setIcon", [iconName]); 9 | }; 10 | 11 | exports.setRingtone = function(ringtoneName, success, error) { 12 | exec(success, error, "CordovaCall", "setRingtone", [ringtoneName]); 13 | }; 14 | 15 | exports.setIncludeInRecents = function(value, success, error) { 16 | if(typeof value == "boolean") { 17 | exec(success, error, "CordovaCall", "setIncludeInRecents", [value]); 18 | } else { 19 | error("Value Must Be True Or False"); 20 | } 21 | }; 22 | 23 | exports.setDTMFState = function(value, success, error) { 24 | if(typeof value == "boolean") { 25 | exec(success, error, "CordovaCall", "setDTMFState", [value]); 26 | } else { 27 | error("Value Must Be True Or False"); 28 | } 29 | }; 30 | 31 | exports.setVideo = function(value, success, error) { 32 | if(typeof value == "boolean") { 33 | exec(success, error, "CordovaCall", "setVideo", [value]); 34 | } else { 35 | error("Value Must Be True Or False"); 36 | } 37 | }; 38 | 39 | exports.receiveCall = function(from, id, success, error) { 40 | if(typeof id == "function") { 41 | error = success; 42 | success = id; 43 | id = undefined; 44 | } else if(id) { 45 | id = id.toString(); 46 | } 47 | exec(success, error, "CordovaCall", "receiveCall", [from, id]); 48 | }; 49 | 50 | exports.sendCall = function(to, id, success, error) { 51 | if(typeof id == "function") { 52 | error = success; 53 | success = id; 54 | id = undefined; 55 | } else if(id) { 56 | id = id.toString(); 57 | } 58 | exec(success, error, "CordovaCall", "sendCall", [to, id]); 59 | }; 60 | 61 | exports.connectCall = function(success, error) { 62 | exec(success, error, "CordovaCall", "connectCall", []); 63 | }; 64 | 65 | exports.endCall = function(success, error) { 66 | exec(success, error, "CordovaCall", "endCall", []); 67 | }; 68 | 69 | exports.mute = function(success, error) { 70 | exec(success, error, "CordovaCall", "mute", []); 71 | }; 72 | 73 | exports.unmute = function(success, error) { 74 | exec(success, error, "CordovaCall", "unmute", []); 75 | }; 76 | 77 | exports.speakerOn = function(success, error) { 78 | exec(success, error, "CordovaCall", "speakerOn", []); 79 | }; 80 | 81 | exports.speakerOff = function(success, error) { 82 | exec(success, error, "CordovaCall", "speakerOff", []); 83 | }; 84 | 85 | exports.callNumber = function(to, success, error) { 86 | exec(success, error, "CordovaCall", "callNumber", [to]); 87 | }; 88 | 89 | exports.on = function(e, f) { 90 | var success = function(message) { 91 | f(message); 92 | }; 93 | var error = function() { 94 | }; 95 | exec(success, error, "CordovaCall", "registerEvent", [e]); 96 | }; 97 | -------------------------------------------------------------------------------- /www/VoIPPushNotification.js: -------------------------------------------------------------------------------- 1 | /* Forked from: https://github.com/phonegap/phonegap-plugin-push/blob/master/www/push.js*/ 2 | /* global cordova:false */ 3 | /* globals window */ 4 | 5 | /*! 6 | * Module dependencies. 7 | */ 8 | 9 | var exec = cordova.require('cordova/exec'); 10 | 11 | /** 12 | * VoIPPushNotification constructor. 13 | * 14 | * @return {VoIPPushNotification} instance that can be monitored and cancelled. 15 | */ 16 | 17 | var VoIPPushNotification = function() { 18 | this._handlers = { 19 | 'registration': [], 20 | 'notification': [], 21 | 'error': [] 22 | }; 23 | 24 | // triggered on registration and notification 25 | var that = this; 26 | var success = function(result) { 27 | if (result && result.registration === 'true') { 28 | that.emit('registration', result); 29 | } 30 | else if (result) { 31 | that.emit('notification', result); 32 | } 33 | }; 34 | 35 | // triggered on error 36 | var fail = function(msg) { 37 | var e = (typeof msg === 'string') ? new Error(msg) : msg; 38 | that.emit('error', e); 39 | }; 40 | 41 | // wait at least one process tick to allow event subscriptions 42 | setTimeout(function() { 43 | exec(success, fail, 'CordovaCall', 'init'); 44 | }, 10); 45 | }; 46 | 47 | /** 48 | * Listen for an event. 49 | * 50 | * The following events are supported: 51 | * 52 | * - registration 53 | * - notification 54 | * - error 55 | * 56 | * @param {String} eventName to subscribe to. 57 | * @param {Function} callback triggered on the event. 58 | */ 59 | 60 | VoIPPushNotification.prototype.on = function(eventName, callback) { 61 | if (this._handlers.hasOwnProperty(eventName)) { 62 | this._handlers[eventName].push(callback); 63 | } 64 | }; 65 | 66 | /** 67 | * Remove event listener. 68 | * 69 | * @param {String} eventName to match subscription. 70 | * @param {Function} handle function associated with event. 71 | */ 72 | 73 | VoIPPushNotification.prototype.off = function (eventName, handle) { 74 | if (this._handlers.hasOwnProperty(eventName)) { 75 | var handleIndex = this._handlers[eventName].indexOf(handle); 76 | if (handleIndex >= 0) { 77 | this._handlers[eventName].splice(handleIndex, 1); 78 | } 79 | } 80 | }; 81 | 82 | /** 83 | * Emit an event. 84 | * 85 | * This is intended for internal use only. 86 | * 87 | * @param {String} eventName is the event to trigger. 88 | * @param {*} all arguments are passed to the event listeners. 89 | * 90 | * @return {Boolean} is true when the event is triggered otherwise false. 91 | */ 92 | 93 | VoIPPushNotification.prototype.emit = function() { 94 | var args = Array.prototype.slice.call(arguments); 95 | var eventName = args.shift(); 96 | 97 | if (!this._handlers.hasOwnProperty(eventName)) { 98 | return false; 99 | } 100 | 101 | for (var i = 0, length = this._handlers[eventName].length; i < length; i++) { 102 | var callback = this._handlers[eventName][i]; 103 | if (typeof callback === 'function') { 104 | callback.apply(undefined,args); 105 | } else { 106 | console.log('event handler: ' + eventName + ' must be a function'); 107 | } 108 | } 109 | 110 | return true; 111 | }; 112 | 113 | /*! 114 | * VoIP Push Notification Plugin. 115 | */ 116 | module.exports = { 117 | /** 118 | * Register for VoIPPush Notifications. 119 | * 120 | * This method will instantiate a new copy of the VoIPPushNotification object 121 | * and start the registration process. 122 | * 123 | * @param {Object} options 124 | * @return {VoIPPushNotification} instance 125 | */ 126 | 127 | init: function(options) { 128 | return new VoIPPushNotification(options); 129 | }, 130 | 131 | /** 132 | * VoIPPushNotification Object. 133 | * 134 | * Expose the VoIPPushNotification object for direct use 135 | * and testing. Typically, you should use the 136 | * .init helper method. 137 | */ 138 | 139 | VoIPPushNotification: VoIPPushNotification 140 | }; --------------------------------------------------------------------------------