diff --git a/app/app.js b/app/app.js index 23041dc..10db121 100644 --- a/app/app.js +++ b/app/app.js @@ -1,3009 +1,3009 @@ import React, { Component, Fragment } from 'react'; import { Alert, View, SafeAreaView, ImageBackground, AppState, Linking, Platform, StyleSheet, Vibration, PermissionsAndroid} from 'react-native'; import { DeviceEventEmitter, BackHandler } from 'react-native'; import { Provider as PaperProvider, DefaultTheme } from 'react-native-paper'; import { registerGlobals } from 'react-native-webrtc'; import { Router, Route, Link, Switch } from 'react-router-native'; import history from './history'; import Logger from "../Logger"; import autoBind from 'auto-bind'; import { firebase } from '@react-native-firebase/messaging'; import VoipPushNotification from 'react-native-voip-push-notification'; import uuid from 'react-native-uuid'; import { getUniqueId, getBundleId, isTablet, getPhoneNumber} from 'react-native-device-info'; import RNDrawOverlay from 'react-native-draw-overlay'; import PushNotificationIOS from "@react-native-community/push-notification-ios"; import Contacts from 'react-native-contacts'; import BackgroundTimer from 'react-native-background-timer'; import DeepLinking from 'react-native-deep-linking'; registerGlobals(); import * as sylkrtc from 'react-native-sylkrtc'; import InCallManager from 'react-native-incall-manager'; import RNCallKeep, { CONSTANTS as CK_CONSTANTS } from 'react-native-callkeep'; import RegisterBox from './components/RegisterBox'; import ReadyBox from './components/ReadyBox'; import Call from './components/Call'; import Conference from './components/Conference'; import FooterBox from './components/FooterBox'; import StatusBox from './components/StatusBox'; import IncomingCallModal from './components/IncomingCallModal'; import LogsModal from './components/LogsModal'; import NotificationCenter from './components/NotificationCenter'; import LoadingScreen from './components/LoadingScreen'; import NavigationBar from './components/NavigationBar'; import Preview from './components/Preview'; -import CallManager from "./CallManager"; +import CallManager from './CallManager'; import utils from './utils'; import config from './config'; import storage from './storage'; const RNFS = require('react-native-fs'); const logfile = RNFS.DocumentDirectoryPath + '/logs.txt'; import styles from './assets/styles/blink/root.scss'; const backgroundImage = require('./assets/images/dark_linen.png'); const logger = new Logger("App"); function checkIosPermissions() { return new Promise(resolve => PushNotificationIOS.checkPermissions(resolve)); } const theme = { ...DefaultTheme, dark: true, roundness: 2, colors: { ...DefaultTheme.colors, primary: '#337ab7', // accent: '#f1c40f', }, }; const URL_SCHEMES = [ 'sylk://', ]; const ONE_SECOND_IN_MS = 1000; const VIBRATION_PATTERN = [ 1 * ONE_SECOND_IN_MS, 1 * ONE_SECOND_IN_MS, 4 * ONE_SECOND_IN_MS ]; let bundleId = `${getBundleId()}`; const deviceId = getUniqueId(); const version = '1.0.0'; const MAX_LOG_LINES = 300; if (Platform.OS == 'ios') { bundleId = `${bundleId}.${__DEV__ ? 'dev' : 'prod'}`; //bundleId = 'com.agprojects.sylk-ios.dev'; } const mainStyle = StyleSheet.create({ MainContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', margin: 0 } }); (function() { if ( typeof Object.id == "undefined" ) { var id = 0; Object.id = function(o) { if ( o && typeof o.__uniqueid == "undefined" ) { Object.defineProperty(o, "__uniqueid", { value: ++id, enumerable: false, // This could go either way, depending on your // interpretation of what an "id" is writable: false }); } return o ? o.__uniqueid : null; }; } })(); const requestCameraPermission = async () => { if (Platform.OS !== 'android') { return; } try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.CAMERA, { title: "Sylk camera permission", message: "Sylk needs access to your camera " + "so you can have video chat.", buttonNeutral: "Ask Me Later", buttonNegative: "Cancel", buttonPositive: "OK" } ); if (granted === PermissionsAndroid.RESULTS.GRANTED) { //console.log("You can use the camera"); } else { console.log("Camera permission denied"); } } catch (err) { console.warn(err); } try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { title: "Sylk microphone permission", message: "Sylk needs access to your microphone " + "so you can have audio calls.", buttonNeutral: "Ask Me Later", buttonNegative: "Cancel", buttonPositive: "OK" } ); if (granted === PermissionsAndroid.RESULTS.GRANTED) { //console.log("You can use the microphone"); } else { console.log("Microphone permission denied"); } } catch (err) { console.warn(err); } }; class Sylk extends Component { constructor() { super(); autoBind(this) this._loaded = false; this._initialState = { appState: null, autoLogin: true, inFocus: false, accountId: '', password: '', displayName: '', account: null, registrationState: null, registrationKeepalive: false, incomingCall: null, currentCall: null, connection: null, showIncomingModal: false, showScreenSharingModal: false, status: null, targetUri: '', missedTargetUri: '', loading: null, localMedia: null, generatedVideoTrack: false, contacts: [], devices: {}, speakerPhoneEnabled: null, orientation : 'portrait', Height_Layout : '', Width_Layout : '', outgoingCallUUID: null, incomingCallUUID: null, hardware: '', phoneNumber: '', isTablet: isTablet(), refreshHistory: false, refreshFavorites: false, myPhoneNumber: null, localHistory: [], favoriteUris: [], blockedUris: [], initialUrl: null, reconnectingCall: false, muted: false, participantsToInvite: [], myInvitedParties: {}, myDisplayNames: {}, defaultDomain: config.defaultDomain, declineReason: null, showLogsModal: false, logs: '', proximityEnabled: true }; utils.timestampedLog('Init app'); this.outgoingMedia = null; this.participantsToInvite = []; this.tokenSent = false; this.mustLogout = false; this.currentRoute = null; this.pushtoken = null; this.pushkittoken = null; this.intercomDtmfTone = null; this.registrationFailureTimer = null; this.contacts = []; this.startedByPush = false; this.heartbeats = 0; this.cachedHistory = []; // used for caching server history this.state = Object.assign({}, this._initialState); this.myParticipants = {}; this._historyConferenceParticipants = new Map(); // for saving to local history this._terminatedCalls = new Map(); this.__notificationCenter = null; this.redirectTo = null; this.prevPath = null; this.shouldUseHashRouting = false; this.goToReadyTimer = null; storage.initialize(); this.callKeeper = new CallManager(RNCallKeep, this.acceptCall, this.rejectCall, this.hangupCall, this.timeoutCall, this.callKeepStartConference, this.startCallFromCallKeeper, this.toggleMute, this.getConnection, this.addConferenceHistoryEntry, this.changeRoute, this.respawnConnection, this.isUnmounted ); if (InCallManager.recordPermission !== 'granted') { InCallManager.requestRecordPermission() .then((requestedRecordPermissionResult) => { //console.log("InCallManager.requestRecordPermission() requestedRecordPermissionResult: ", requestedRecordPermissionResult); }) .catch((err) => { //console.log("InCallManager.requestRecordPermission() catch: ", err); }); } requestCameraPermission(); // Load camera/mic preferences storage.get('devices').then((devices) => { if (devices) { this.setState({devices: devices}); } }); storage.get('history').then((history) => { if (history) { //console.log('Loaded', history.length, 'local history entries'); this.setState({localHistory: history}); } else { //console.log('Loaded 0 local history entries'); } }); storage.get('cachedHistory').then((history) => { if (history) { //console.log('Loaded', history.length, 'cached history entries'); this.cachedHistory = history; } }); storage.get('myParticipants').then((myParticipants) => { if (myParticipants) { this.myParticipants = myParticipants; //console.log('My participants', this.myParticipants); } }); storage.get('myInvitedParties').then((myInvitedParties) => { if (myInvitedParties) { if (Array.isArray(myInvitedParties)) { myInvitedParties = {}; } this.myInvitedParties = myInvitedParties; //console.log('My invited parties', this.myInvitedParties); this.setState({myInvitedParties: this.myInvitedParties}); } }); storage.get('displayName').then((displayName) => { //console.log('My display name is', displayName); this.setState({displayName: displayName}); }); storage.get('proximityEnabled').then((proximityEnabled) => { this.setState({proximityEnabled: proximityEnabled}); }); if (this.state.proximityEnabled) { utils.timestampedLog('Proximity sensor enabled'); } else { utils.timestampedLog('Proximity sensor disabled'); } storage.get('myDisplayNames').then((myDisplayNames) => { this.setState({myDisplayNames: myDisplayNames}); }); storage.get('favoriteUris').then((favoriteUris) => { if (favoriteUris) { this.setState({favoriteUris: favoriteUris}); } }); storage.get('blockedUris').then((blockedUris) => { if (blockedUris) { this.setState({blockedUris: blockedUris}); //console.log('My blocked Uris', blockedUris); } }); for (let scheme of URL_SCHEMES) { DeepLinking.addScheme(scheme); } } async loadContacts() { Contacts.checkPermission((err, permission) => { if (permission === Contacts.PERMISSION_UNDEFINED) { Contacts.requestPermission((err, requestedContactsPermissionResult) => { if (err) { console.log("Contacts.requestPermission()catch: ", err); } console.log("Contacts.requestPermission() requestPermission: ", requestedContactsPermissionResult); }) } }) Contacts.getAll((err, contacts) => { if (err === 'denied'){ console.log('Access to contacts denied') } else { // contacts returned in Array let contact_cards = []; let name; let photo; let seen_uris = new Map(); var arrayLength = contacts.length; for (var i = 0; i < arrayLength; i++) { photo = null; contact = contacts[i]; if (contact['givenName'] && contact['familyName']) { name = contact['givenName'] + ' ' + contact['familyName']; } else if (contact['givenName']) { name = contact['givenName']; } else if (contact['familyName']) { name = contact['familyName']; } else if (contact['company']) { name = contact['company']; } else { continue; } if (contact.hasThumbnail) { photo = contact.thumbnailPath; } //console.log(name); contact['phoneNumbers'].forEach(function (number, index) { let number_stripped = number['number'].replace(/\s|\-|\(|\)/g, ''); if (number_stripped) { if (!seen_uris.has(number_stripped)) { //console.log(' ----> ', number['label'], number_stripped); var contact_card = {id: uuid.v4(), displayName: name, remoteParty: number_stripped, type: 'contact', photo: photo, label: number['label'], tags: ['contact']}; contact_cards.push(contact_card); seen_uris.set(number_stripped, true); var contact_card = {id: uuid.v4(), displayName: name, remoteParty: number_stripped, type: 'contact', photo: photo, label: number['label'], tags: ['contact'] }; } } }); contact['emailAddresses'].forEach(function (email, index) { let email_stripped = email['email'].replace(/\s|\(|\)/g, ''); if (!seen_uris.has(email_stripped)) { //console.log(name, email['label'], email_stripped); //console.log(' ----> ', email['label'], email_stripped); var contact_card = {id: uuid.v4(), displayName: name, remoteParty: email_stripped, type: 'contact', photo: photo, label: email['label'], tags: ['contact'] }; contact_cards.push(contact_card); seen_uris.set(email_stripped, true); } }); } this.contacts = contact_cards; } }) } get _notificationCenter() { // getter to lazy-load the NotificationCenter ref if (!this.__notificationCenter) { this.__notificationCenter = this.refs.notificationCenter; } return this.__notificationCenter; } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } _detectOrientation() { if(this.state.Width_Layout > this.state.Height_Layout && this.state.orientation !== 'landscape') { this.setState({orientation: 'landscape'}); } else { this.setState({orientation: 'portrait'}); } } changeRoute(route, reason) { if (this.currentRoute === route) { return; } if (this.currentRoute !== route) { utils.timestampedLog('Change route:', this.currentRoute, '->', route, reason); } if (route === '/ready') { Vibration.cancel(); if (reason === 'conference_really_ended' && this.callKeeper.countCalls) { utils.timestampedLog('Change route cancelled because we still have calls'); return; } this.startedByPush = false; this.setState({ outgoingCallUUID: null, currentCall: null, incomingCall: (reason === 'accept_new_call' || reason === 'user_hangup_call') ? this.state.incomingCall: null, targetUri: '', reconnectingCall: false, muted: false }); if (this.currentRoute === '/call' || this.currentRoute === '/conference') { if (reason !== 'user_hangup_call') { this.stopRingback(); InCallManager.stop(); } this.closeLocalMedia(); if (reason === 'accept_new_call') { if (this.state.incomingCall) { // then answer the new call if any let hasVideo = (this.state.incomingCall && this.state.incomingCall.mediaTypes && this.state.incomingCall.mediaTypes.video) ? true : false; this.getLocalMedia(Object.assign({audio: true, video: hasVideo}), '/call'); } } else if (reason === 'escalate_to_conference') { const uri = `${utils.generateSillyName()}@${config.defaultConferenceDomain}`; const options = {audio: this.outgoingMedia ? this.outgoingMedia.audio: true, video: this.outgoingMedia ? this.outgoingMedia.video: true, participants: this.participantsToInvite} this.callKeepStartConference(uri.toLowerCase(), options); } else { if (this.state.account && this._loaded) { setTimeout(() => { this.updateServerHistory() }, 1500); } } } if (reason === 'registered') { setTimeout(() => { this.updateServerHistory() }, 1500); } if (reason === 'no_more_calls') { this.updateServerHistory() } } this.currentRoute = route; history.push(route); } componentWillUnmount() { utils.timestampedLog('App will unmount'); AppState.removeEventListener('change', this._handleAppStateChange); this.callKeeper.destroy(); this.closeConnection(); this._loaded = false; } get unmounted() { return !this._loaded; } isUnmounted() { return this.unmounted; } backPressed() { console.log('Back button pressed'); if (this.currentRoute === '/call' || this.currentRoute === '/conference') { let call = this.state.currentCall || this.state.incomingCall; if (call && call.id) { this.hangupCall(call.id, 'user_hangup_call'); } } return true; } async componentDidMount() { utils.timestampedLog('App did mount'); this._loaded = true; BackHandler.addEventListener('hardwareBackPress', this.backPressed); // Start a timer that runs once after X milliseconds BackgroundTimer.runBackgroundTimer(() => { // this will be executed once after 10 seconds // even when app is the the background this.heartbeat(); }, 5000); try { await RNCallKeep.supportConnectionService (); //utils.timestampedLog('Connection service is enabled'); } catch(err) { utils.timestampedLog(err); } try { await RNCallKeep.hasPhoneAccount(); //utils.timestampedLog('Phone account is enabled'); } catch(err) { utils.timestampedLog(err); } if (Platform.OS === 'android') { RNDrawOverlay.askForDispalayOverOtherAppsPermission() .then(res => { //utils.timestampedLog("Display over other apps was granted"); // res will be true if permission was granted }) .catch(e => { utils.timestampedLog("Display over other apps was declined"); // permission was declined }) } // prime the ref //logger.debug('NotificationCenter ref: %o', this._notificationCenter); this._boundOnPushkitRegistered = this._onPushkitRegistered.bind(this); this._boundOnPushRegistered = this._onPushRegistered.bind(this); this._detectOrientation(); getPhoneNumber().then(phoneNumber => { this.setState({myPhoneNumber: phoneNumber}); this.loadContacts(); }); this.listenforPushNotifications(); } listenforPushNotifications() { if (this.state.appState === null) { this.setState({appState: 'active'}); } else { return; } if (Platform.OS === 'android') { Linking.getInitialURL().then((url) => { if (url) { utils.timestampedLog('Initial external URL: ' + url); this.eventFromUrl(url); this.changeRoute('/login', 'start up'); } else { this.changeRoute('/login', 'start up'); } }).catch(err => { logger.error({ err }, 'Error getting external URL'); }); firebase.messaging().getToken() .then(fcmToken => { if (fcmToken) { this._onPushRegistered(fcmToken); } }); Linking.addEventListener('url', this.updateLinkingURL); } else if (Platform.OS === 'ios') { this.changeRoute('/login', 'start up'); VoipPushNotification.addEventListener('register', this._boundOnPushkitRegistered); VoipPushNotification.registerVoipToken(); PushNotificationIOS.addEventListener('register', this._boundOnPushRegistered); //let permissions = await checkIosPermissions(); //if (!permissions.alert) { PushNotificationIOS.requestPermissions(); //} } this.boundProximityDetect = this._proximityDetect.bind(this); DeviceEventEmitter.addListener('Proximity', this.boundProximityDetect); AppState.addEventListener('change', this._handleAppStateChange); if (Platform.OS === 'ios') { this._boundOnNotificationReceivedBackground = this._onNotificationReceivedBackground.bind(this); this._boundOnLocalNotificationReceivedBackground = this._onLocalNotificationReceivedBackground.bind(this); VoipPushNotification.addEventListener('notification', this._boundOnNotificationReceivedBackground); VoipPushNotification.addEventListener('localNotification', this._boundOnLocalNotificationReceivedBackground); } else if (Platform.OS === 'android') { AppState.addEventListener('focus', this._handleAndroidFocus); AppState.addEventListener('blur', this._handleAndroidBlur); firebase .messaging() .requestPermission() .then(() => { // User has authorised }) .catch(error => { // User has rejected permissions }); this.messageListener = firebase .messaging() .onMessage((message: RemoteMessage) => { // this will just wake up the app to receive // the web-socket invite handled by this.incomingCall() let event = message.data.event; const callUUID = message.data['session-id']; const from = message.data['from_uri']; const to = message.data['to_uri']; const displayName = message.data['from_display_name']; const outgoingMedia = {audio: true, video: message.data['media-type'] === 'video'}; const mediaType = message.data['media-type'] || 'audio'; if (this.unmounted) { return; } if (event === 'incoming_conference_request') { utils.timestampedLog('Push notification: incoming conference', callUUID); this.incomingConference(callUUID, to, from, displayName, outgoingMedia); } else if (event === 'incoming_session') { utils.timestampedLog('Push notification: incoming call', callUUID); this.incomingCallFromPush(callUUID, from, displayName, mediaType); } else if (event === 'cancel') { this.cancelIncomingCall(callUUID); } }); } } cancelIncomingCall(callUUID) { if (this.unmounted) { return; } if (this.callKeeper._acceptedCalls.has(callUUID)) { return; } utils.timestampedLog('Push notification: cancel call', callUUID); let call = this.callKeeper._calls.get(callUUID); if (!call) { if (!this.callKeeper._cancelledCalls.has(callUUID)) { utils.timestampedLog('Cancel incoming call that did not arrive on web socket', callUUID); this.callKeeper.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED); this.startedByPush = false; if (this.startedByPush) { this.changeRoute('/ready', 'incoming_call_cancelled'); } } return; } if (call.state === 'incoming') { utils.timestampedLog('Cancel incoming call that was not yet accepted', callUUID); this.callKeeper.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED); if (this.startedByPush) { this.changeRoute('/ready', 'incoming_call_cancelled'); } } } _proximityDetect(data) { //utils.timestampedLog('Proximity changed, isNear is', data.isNear); if (!this.state.proximityEnabled) { return; } if (data.isNear) { this.speakerphoneOff(); } else { this.speakerphoneOn(); } } startCallWhenReady(targetUri, options) { this.resetGoToReadyTimer(); if (options.conference) { this.startConference(targetUri, options); } else { this.startCall(targetUri, options); } } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } _onPushkitRegistered(token) { this.pushkittoken = token; } _onPushRegistered(token) { this.pushtoken = token; } _sendPushToken() { if ((this.state.account && this.pushtoken && !this.tokenSent)) { let token = null; if (Platform.OS === 'ios') { token = `${this.pushkittoken}#${this.pushtoken}`; } else if (Platform.OS === 'android') { token = this.pushtoken; } utils.timestampedLog('Push token sent to server'); this.state.account.setDeviceToken(token, Platform.OS, deviceId, true, bundleId); this.tokenSent = true; } } _handleAndroidFocus = nextFocus => { //utils.timestampedLog('----- APP in focus'); this.setState({inFocus: true}); this.respawnConnection(); } _handleAndroidBlur = nextBlur => { //utils.timestampedLog('----- APP out of focus'); this.setState({inFocus: false}); } _handleAppStateChange = nextAppState => { //utils.timestampedLog('----- APP state changed', this.state.appState, '->', nextAppState); if (nextAppState === this.state.appState) { return; } if (this.callKeeper.countCalls === 0 && !this.state.outgoingCallUUID) { /* utils.timestampedLog('----- APP state changed', this.state.appState, '->', nextAppState); if (this.callKeeper.countCalls) { utils.timestampedLog('- APP state changed, we have', this.callKeeper.countCalls, 'calls'); } if (this.callKeeper.countPushCalls) { utils.timestampedLog('- APP state changed, we have', this.callKeeper.countPushCalls, 'push calls'); } if (this.startedByPush) { utils.timestampedLog('- APP state changed, started by push in', nextAppState, 'state'); } if (this.state.connection) { utils.timestampedLog('- APP state changed from', this.state.appState, 'to', nextAppState, 'with connection', Object.id(this.state.connection)); } else { utils.timestampedLog('- APP state changed from', this.state.appState, 'to', nextAppState); } */ } if (this.state.appState === 'background' && nextAppState === 'active') { this.respawnConnection(nextAppState); } this.setState({appState: nextAppState}); } respawnConnection(state) { if (!this.state.connection) { utils.timestampedLog('Web socket does not exist'); } else if (!this.state.connection.state) { utils.timestampedLog('Web socket is waiting for connection...'); } else { /* if (this.state.connection.state !== 'ready' && this.state.connection.state !== 'connecting') { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'reconnecting because', this.state.connection.state); this.state.connection.reconnect(); utils.timestampedLog('Web socket', Object.id(this.state.connection), 'new state is', this.state.connection.state); } */ } if (this.state.account) { if (!this.state.connection) { utils.timestampedLog('Active account without connection removed'); this.setState({account: null}); } } else { utils.timestampedLog('No active account'); } if (this.state.accountId && (!this.state.connection || !this.state.account)) { this.handleRegistration(this.state.accountId, this.state.password); } } closeConnection(reason='unmount') { if (!this.state.connection) { return; } if (!this.state.account && this.state.connection) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.state.connection.close(); utils.timestampedLog('Web socket', Object.id(this.state.connection), 'will close'); this.setState({connection: null, account: null}); } else if (this.state.connection && this.state.account) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this.state.account.removeListener('outgoingCall', this.outgoingCall); this.state.account.removeListener('conferenceCall', this.outgoingConference); this.state.account.removeListener('incomingCall', this.incomingCallFromWebSocket); this.state.account.removeListener('missedCall', this.missedCall); this.state.account.removeListener('conferenceInvite', this.conferenceInviteFromWebSocket); this.state.connection.removeAccount(this.state.account, (error) => { if (error) { utils.timestampedLog('Failed to remove account:', error); } else { //utils.timestampedLog('Account removed'); } if (this.state.connection) { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'will close'); this.state.connection.close(); } this.setState({connection: null, account: null}); } ); } else { this.setState({connection: null, account: null}); } } startCallFromCallKeeper(data) { utils.timestampedLog('Starting call from OS...'); let callUUID = data.callUUID || uuid.v4(); let is_conf = data.handle.search('videoconference.') === -1 ? false: true; this.backToForeground(); if (is_conf) { this.callKeepStartConference(data.handle, {audio: true, video: data.video || true, callUUID: callUUID}); } else { this.callKeepStartCall(data.handle, {audio: true, video: data.video, callUUID: callUUID}); } this._notificationCenter.removeNotification(); } connectionStateChanged(oldState, newState) { if (this.unmounted) { return; } const connection = this.getConnection(); if (oldState) { utils.timestampedLog('Web socket', connection, 'state changed:', oldState, '->' , newState); } switch (newState) { case 'closed': if (this.state.connection) { utils.timestampedLog('Web socket was terminated'); this.state.connection.removeListener('stateChanged', this.connectionStateChanged); this._notificationCenter.postSystemNotification('Connection lost'); } //this.setState({connection: null, account: null}); this.setState({account: null}); break; case 'ready': this._notificationCenter.removeNotification(); this.processRegistration(this.state.accountId, this.state.password); this.callKeeper.setAvailable(true); break; case 'disconnected': if (this.registrationFailureTimer) { clearTimeout(this.registrationFailureTimer); this.registrationFailureTimer = null; } if (this.state.currentCall && this.state.currentCall.direction === 'outgoing') { this.hangupCall(this.state.currentCall.id, 'outgoing_connection_failed'); } if (this.state.incomingCall) { this.hangupCall(this.state.incomingCall.id, 'connection_failed'); } this.setState({ registrationState: 'failed', generatedVideoTrack: false, }); break; default: if (this.state.registrationKeepalive !== true) { this.setState({loading: 'Connecting...'}); } break; } } notificationCenter() { return this._notificationCenter; } showRegisterFailure(reason) { const connection = this.getConnection(); utils.timestampedLog('Registration error: ' + reason, 'on web socket', connection); this.setState({ loading : null, registrationState: 'failed', status : { msg : 'Sign In failed: ' + reason, level : 'danger' } }); if (this.startedByPush) { // TODO: hangup incoming call } } registrationStateChanged(oldState, newState, data) { if (this.unmounted) { return; } const connection = this.getConnection(); if (oldState) { utils.timestampedLog('Registration state changed:', oldState, '->', newState, 'on web socket', connection); } if (!this.state.account) { utils.timestampedLog('Account disabled'); return; } if (newState === 'failed') { let reason = data.reason; if (reason === 904) { // Sofia SIP: WAT reason = 'Wrong account or password'; } else if (reason === 408) { reason = 'Timeout'; } this.showRegisterFailure(reason); if (this.state.registrationKeepalive === true) { if (this.state.connection !== null && this.state.connection.state === 'ready') { utils.timestampedLog('Retry to register...'); this.state.account.register(); } } else { // add a timer to retry register after awhile if (reason >= 500 || reason === 408) { utils.timestampedLog('Retry to register after 5 seconds delay...'); setTimeout(this.state.account.register(), 5000); } else { if (this.registrationFailureTimer) { utils.timestampedLog('Cancel registration timer'); clearTimeout(this.registrationFailureTimer); this.registrationFailureTimer = null; } } } } else if (newState === 'registered') { if (this.registrationFailureTimer) { clearTimeout(this.registrationFailureTimer); this.registrationFailureTimer = null; } this.setState({loading: null, registrationKeepalive: true, registrationState: 'registered', defaultDomain: this.state.account ? this.state.account.id.split('@')[1]: null }); //if (this.currentRoute === '/login' && (!this.startedByPush || Platform.OS === 'ios')) { // TODO if the call does not arrive, we never get back to ready if (this.currentRoute === '/login') { this.changeRoute('/ready', 'registered'); } return; } else { this.setState({status: null, registrationState: newState }); } if (this.mustLogout) { this.logout(); } } showInternalAlertPanel() { this.setState({showIncomingModal: true}); //Vibration.vibrate(VIBRATION_PATTERN, true); setTimeout(() => { Vibration.cancel(); }, 30000); } hideInternalAlertPanel() { Vibration.cancel(); this.setState({showIncomingModal: false}); } heartbeat() { if (this.unmounted) { return; } this.heartbeats = this.heartbeats + 1; if (this.heartbeats % 40 == 0) { this.trimLogs(); } if (this.state.connection) { //console.log('Check calls in', this.state.appState, 'with connection', Object.id(this.state.connection), this.state.connection.state); } else { //console.log('Check calls in', this.state.appState, 'with no connection'); } let callState; if (this.state.currentCall && this.state.incomingCall && this.state.incomingCall === this.state.currentCall) { //utils.timestampedLog('We have an incoming call:', this.state.currentCall ? (this.state.currentCall.id + ' ' + this.state.currentCall.state): 'None'); callState = this.state.currentCall.state; } else if (this.state.incomingCall) { //utils.timestampedLog('We have an incoming call:', this.state.incomingCall ? (this.state.incomingCall.id + ' ' + this.state.incomingCall.state): 'None'); callState = this.state.incomingCall.state; } else if (this.state.currentCall) { //utils.timestampedLog('We have an outgoing call:', this.state.currentCall ? (this.state.currentCall.id + ' ' + this.state.currentCall.state): 'None'); callState = this.state.currentCall.state; } else if (this.state.outgoingCallUUID) { //utils.timestampedLog('We have a pending outgoing call:', this.state.outgoingCallUUID); } else { //utils.timestampedLog('We have no calls'); if (this.state.appState === 'background' && this.state.connection && this.state.connection.state === 'ready') { //this.closeConnection('background with no calls'); } } this.callKeeper.heartbeat(); } stopRingback() { //utils.timestampedLog('Stop ringback'); InCallManager.stopRingback(); } resetGoToReadyTimer() { if (this.goToReadyTimer !== null) { clearTimeout(this.goToReadyTimer); this.goToReadyTimer = null; } } goToReadyNowAndCancelTimer() { if (this.goToReadyTimer !== null) { clearTimeout(this.goToReadyTimer); this.goToReadyTimer = null; this.changeRoute('/ready', 'cancel_timer_incoming_call'); } } isConference(call) { const _call = call || this.state.currentCall; if (_call && _call.hasOwnProperty('_participants')) { return true; } return false; } callStateChanged(oldState, newState, data) { if (this.unmounted) { return; } // outgoing accepted: null -> progress -> accepted -> established -> terminated // outgoing accepted: null -> progress -> established -> accepted -> terminated (with early media) // incoming accepted: null -> incoming -> accepted -> established -> terminated // 2nd incoming call is automatically rejected by sylkrtc library /* utils.timestampedLog('---currentCall start:', this.state.currentCall); utils.timestampedLog('---incomingCall start:', this.state.incomingCall); */ let call = this.callKeeper._calls.get(data.id); if (!call) { utils.timestampedLog("callStateChanged error: call", data.id, 'not found in callkeep manager'); return; } let callUUID = call.id; const connection = this.getConnection(); utils.timestampedLog('Sylkrtc call', callUUID, 'state change:', oldState, '->', newState, 'on web socket', connection); /* if (newState === 'established' || newState === 'accepted') { // restore the correct UI state if it has transitioned illegally to /ready state if (call.hasOwnProperty('_participants')) { this.changeRoute('/conference', 'correct call state'); } else { this.changeRoute('/call', 'correct call state'); } } */ let newCurrentCall; let newincomingCall; let direction = call.direction; let hasVideo = false; let mediaType = 'audio'; let tracks; let readyDelay = 5000; if (this.state.incomingCall && this.state.currentCall) { if (newState === 'terminated') { if (this.state.incomingCall == this.state.currentCall) { newCurrentCall = null; newincomingCall = null; } if (this.state.incomingCall.id === call.id) { if (oldState === 'incoming') { //utils.timestampedLog('Call state changed:', 'incoming call must be cancelled'); this.hideInternalAlertPanel(); } if (oldState === 'established' || oldState === 'accepted') { //utils.timestampedLog('Call state changed:', 'incoming call ended'); this.hideInternalAlertPanel(); } // new call must be cancelled newincomingCall = null; newCurrentCall = this.state.currentCall; } if (this.state.currentCall != this.state.incomingCall && this.state.currentCall.id === call.id) { if (oldState === 'established' || newState === 'accepted') { //utils.timestampedLog('Call state changed:', 'outgoing call must be hangup'); // old call must be closed } newCurrentCall = null; newincomingCall = this.state.incomingCall; } } else if (newState === 'accepted') { if (this.state.incomingCall === this.state.currentCall) { newCurrentCall = this.state.incomingCall; newincomingCall = this.state.incomingCall; } else { newCurrentCall = this.state.currentCall; } this.backToForeground(); } else if (newState === 'established') { if (this.state.incomingCall === this.state.currentCall) { //utils.timestampedLog("Incoming call media started"); newCurrentCall = this.state.incomingCall; newincomingCall = this.state.incomingCall; } else { //utils.timestampedLog("Outgoing call media started"); newCurrentCall = this.state.currentCall; } } else { //utils.timestampedLog('Call state changed:', 'We have two calls in unclear state'); } } else if (this.state.incomingCall) { //this.backToForeground(); //utils.timestampedLog('Call state changed: We have one incoming call'); newincomingCall = this.state.incomingCall; newCurrentCall = this.state.incomingCall; if (this.state.incomingCall.id === call.id) { if (newState === 'terminated') { this.startedByPush = false; //utils.timestampedLog("Incoming call was cancelled"); this.setState({showIncomingModal: false}); this.hideInternalAlertPanel(); newincomingCall = null; newCurrentCall = null; readyDelay = 10; } else if (newState === 'accepted') { //utils.timestampedLog("Incoming call was accepted"); this.hideInternalAlertPanel(); this.backToForeground(); } else if (newState === 'established') { //utils.timestampedLog("Incoming call media started"); this.hideInternalAlertPanel(); } } } else if (this.state.currentCall) { //utils.timestampedLog('Call state changed: We have one current call'); newCurrentCall = newState === 'terminated' ? null : call; newincomingCall = null; if (newState !== 'terminated') { this.setState({reconnectingCall: false}); } } else { newincomingCall = null; newCurrentCall = null; } /* utils.timestampedLog('---currentCall:', newCurrentCall); utils.timestampedLog('---incomingCall:', newincomingCall); */ switch (newState) { case 'progress': this.callKeeper.setCurrentCallActive(callUUID); this.backToForeground(); this.resetGoToReadyTimer(); tracks = call.getLocalStreams()[0].getVideoTracks(); mediaType = (tracks && tracks.length > 0) ? 'video' : 'audio'; if (mediaType === 'video') { this.speakerphoneOn(); } else { this.speakerphoneOff(); } if (!this.isConference(call)){ InCallManager.startRingback('_BUNDLE_'); } break; case 'established': this.callKeeper.setCurrentCallActive(callUUID); this.backToForeground(); this.resetGoToReadyTimer(); tracks = call.getLocalStreams()[0].getVideoTracks(); mediaType = (tracks && tracks.length > 0) ? 'video' : 'audio'; InCallManager.start({media: mediaType}); if (direction === 'outgoing') { this.stopRingback(); if (this.state.speakerPhoneEnabled) { this.speakerphoneOn(); } else { this.speakerphoneOff(); } } else { if (mediaType === 'video') { this.speakerphoneOn(); } else { this.speakerphoneOff(); } } break; case 'accepted': this.callKeeper.setCurrentCallActive(callUUID); this.backToForeground(); this.resetGoToReadyTimer(); if (direction === 'outgoing') { this.stopRingback(); } break; case 'terminated': this._terminatedCalls.set(callUUID, true); utils.timestampedLog(callUUID, direction, 'terminated with reason', data.reason); if (this.state.incomingCall && this.state.incomingCall.id === call.id) { newincomingCall = null; } if (this.state.currentCall && this.state.currentCall.id === call.id) { newCurrentCall = null; } let callSuccesfull = false; let reason = data.reason; let play_busy_tone = !this.isConference(call); let CALLKEEP_REASON; if (!reason || reason.match(/200/)) { if (oldState === 'progress' && direction === 'outgoing') { reason = 'Cancelled'; play_busy_tone = false; } else if (oldState === 'incoming') { reason = 'Cancelled'; play_busy_tone = false; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.UNANSWERED; } else { reason = 'Hangup'; callSuccesfull = true; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; } } else if (reason.match(/402/)) { reason = 'Payment required'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/403/)) { //reason = 'Forbidden'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/404/)) { reason = 'User not found'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/408/)) { reason = 'Timeout'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/480/)) { reason = 'Is not online'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.UNANSWERED; } else if (reason.match(/486/)) { reason = 'Is busy'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; if (direction === 'outgoing') { play_busy_tone = false; } } else if (reason.match(/603/)) { reason = 'Cannot answer now'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; if (direction === 'outgoing') { play_busy_tone = false; } } else if (reason.match(/487/)) { reason = 'Cancelled'; play_busy_tone = false; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED; } else if (reason.match(/488/)) { reason = 'Unacceptable media'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/5\d\d/)) { reason = 'Server failure'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (reason.match(/904/)) { // Sofia SIP: WAT reason = 'Wrong account or password'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else { reason = 'Connection failed'; CALLKEEP_REASON = CK_CONSTANTS.END_CALL_REASONS.FAILED; } if (play_busy_tone) { this.playBusyTone(); } if (direction === 'outgoing') { this.setState({declineReason: reason}); } this.stopRingback(); this.callKeeper.endCall(callUUID, CALLKEEP_REASON); if (play_busy_tone && oldState !== 'established' && direction === 'outgoing') { this._notificationCenter.postSystemNotification('Call ended:', {body: reason}); } this.updateHistoryEntry(callUUID); break; default: break; } /* utils.timestampedLog('---currentCall end:', newCurrentCall); utils.timestampedLog('---incomingCall end:', newincomingCall); */ this.setState({ currentCall: newCurrentCall, incomingCall: newincomingCall }); if (!this.state.currentCall && !this.state.incomingCall) { this.speakerphoneOn(); if (!this.state.reconnectingCall) { if (this.state.inFocus) { if (this.currentRoute !== '/ready') { utils.timestampedLog('Will go to ready in', readyDelay/1000, 'seconds (terminated)', callUUID); this.goToReadyTimer = setTimeout(() => { this.changeRoute('/ready', 'no_more_calls'); }, readyDelay); } } else { if (this.currentRoute !== '/conference') { this.changeRoute('/ready', 'no_more_calls'); } } } } if (this.state.currentCall) { //console.log('Current:', this.state.currentCall.id); } if (this.state.incomingCall) { //console.log('Incoming:', this.state.incomingCall.id); } } handleRegistration(accountId, password, remember=true) { if (this.state.account !== null && this.state.registrationState === 'registered' ) { return; } this.setState({ accountId : accountId, password : password, loading : 'Connecting...' }); if (this.state.connection === null) { utils.timestampedLog('Web socket handle registration for', accountId); const userAgent = 'Sylk Mobile'; if (this.state.phoneNumber) { console.log('Phone number:', this.state.phoneNumber); } let connection = sylkrtc.createConnection({server: config.wsServer}); utils.timestampedLog('Web socket', Object.id(connection), 'was opened'); connection.on('stateChanged', this.connectionStateChanged); this.setState({connection: connection}); } else { if (this.state.connection.state === 'ready' && this.state.registrationState !== 'registered') { utils.timestampedLog('Web socket', Object.id(this.state.connection), 'handle registration for', accountId); this.processRegistration(accountId, password); } } } processRegistration(accountId, password, displayName) { if (!displayName) { displayName = this.state.displayName; } if (!this.state.connection) { return; } this.updateServerHistory(); utils.timestampedLog('Process registration for', accountId, '(', displayName, ')'); if (this.state.account && this.state.connection) { this.state.connection.removeAccount(this.state.account, (error) => { this.setState({registrationState: null, registrationKeepalive: false}); } ); } const options = { account: accountId, password: password, displayName: displayName || '' }; if (this.state.connection._accounts.has(options.account)) { return; } //utils.timestampedLog('Add register 10 sec timer'); this.registrationFailureTimer = setTimeout(() => { this.showRegisterFailure('Register timeout'); this.processRegistration(accountId, password); }, 10000); const account = this.state.connection.addAccount(options, (error, account) => { if (!error) { account.on('outgoingCall', this.outgoingCall); account.on('conferenceCall', this.outgoingConference); account.on('registrationStateChanged', this.registrationStateChanged); account.on('incomingCall', this.incomingCallFromWebSocket); account.on('missedCall', this.missedCall); account.on('conferenceInvite', this.conferenceInviteFromWebSocket); //utils.timestampedLog('Web socket account', account.id, 'is ready, registering...'); this.setState({account: account}); this._sendPushToken(); account.register(); storage.set('account', { accountId: this.state.accountId, password: this.state.password }); } else { this.showRegisterFailure(408); } }); } setDevice(device) { const oldDevices = Object.assign({}, this.state.devices); if (device.kind === 'videoinput') { oldDevices['camera'] = device; } else if (device.kind === 'audioinput') { oldDevices['mic'] = device; } this.setState({devices: oldDevices}); storage.set('devices', oldDevices); sylkrtc.utils.closeMediaStream(this.state.localMedia); this.getLocalMedia(); } getLocalMedia(mediaConstraints={audio: true, video: true}, nextRoute=null) { // eslint-disable-line space-infix-ops let callType = mediaConstraints.video ? 'video': 'audio'; utils.timestampedLog('Get local media for', callType, 'call'); const constraints = Object.assign({}, mediaConstraints); if (constraints.video === true) { if ((nextRoute === '/conference')) { constraints.video = { 'width': { 'ideal': 640 }, 'height': { 'ideal': 480 } }; // TODO: remove this, workaround so at least safari works when joining a video conference } else if (nextRoute === '/conference' && isSafari) { constraints.video = false; } else { // ask for 720p video constraints.video = { 'width': { 'ideal': 640 }, 'height': { 'ideal': 480 } }; } } logger.debug('getLocalMedia(), (modified) mediaConstraints=%o', constraints); navigator.mediaDevices.enumerateDevices() .then((devices) => { devices.forEach((device) => { //console.log(device); if ('video' in constraints && 'camera' in this.state.devices) { if (constraints.video && constraints.video !== false && (device.deviceId === this.state.devices.camera.deviceId || device.label === this.state.devices.camera.label)) { constraints.video.deviceId = { exact: device.deviceId }; } } if ('mic' in this.state.devices) { if (device.deviceId === this.state.devices.mic.deviceId || device.label === this.state.devices.mic.Label) { // constraints.audio = { // deviceId: { // exact: device.deviceId // } // }; } } }); }) .catch((error) => { utils.timestampedLog('Error: device enumeration failed:', error); }) .then(() => { return navigator.mediaDevices.getUserMedia(constraints) }) .then((localStream) => { clearTimeout(this.loadScreenTimer); //utils.timestampedLog('Local media acquired'); this.setState({localMedia: localStream}); if (nextRoute !== null) { this.changeRoute(nextRoute); } }) .catch((error) => { utils.timestampedLog('Access to local media failed, trying audio only', error); navigator.mediaDevices.getUserMedia({ audio: true, video: false }) .then((localStream) => { clearTimeout(this.loadScreenTimer); if (nextRoute !== null) { this.changeRoute(nextRoute, 'local media aquired'); } }) .catch((error) => { utils.timestampedLog('Access to local media failed:', error); clearTimeout(this.loadScreenTimer); this._notificationCenter.postSystemNotification("Can't access camera or microphone"); this.setState({ loading: null }); this.changeRoute('/ready', 'local media failure'); }); }); } getConnection() { return this.state.connection ? Object.id(this.state.connection): null; } callKeepStartConference(targetUri, options={audio: true, video: true, participants: []}) { if (!targetUri) { return; } this.resetGoToReadyTimer(); let callUUID = options.callUUID || uuid.v4(); let participants = options.participants || null; this.addConferenceHistoryEntry(targetUri, callUUID); let participantsToInvite = []; if (participants) { participants.forEach((participant_uri) => { if (participant_uri === this.state.accountId) { return; } participantsToInvite.push(participant_uri); }); } this.outgoingMedia = options; this.setState({outgoingCallUUID: callUUID, reconnectingCall: false, participantsToInvite: participantsToInvite }); const media = options.video ? 'video' : 'audio'; if (participantsToInvite) { utils.timestampedLog('Will start', media, 'conference', callUUID, 'to', targetUri, 'with', participantsToInvite); } else { utils.timestampedLog('Will start', media, 'conference', callUUID, 'to', targetUri); } this.respawnConnection(); this.startCallWhenReady(targetUri, {audio: options.audio, video: options.video, conference: true, callUUID: callUUID}); } callKeepStartCall(targetUri, options) { this.resetGoToReadyTimer(); let callUUID = options.callUUID || uuid.v4(); this.setState({outgoingCallUUID: callUUID, reconnectingCall: false}); utils.timestampedLog('User will start call', callUUID, 'to', targetUri); this.respawnConnection(); this.startCallWhenReady(targetUri, {audio: options.audio, video: options.video, callUUID: callUUID}); } startCall(targetUri, options) { this.setState({targetUri: targetUri}); this.getLocalMedia(Object.assign({audio: true, video: options.video}, options), '/call'); } timeoutCall(callUUID, uri) { utils.timestampedLog('Timeout answering call', callUUID); this.addConferenceHistoryEntry(uri, callUUID, direction='received'); this.forceUpdate(); } closeLocalMedia() { if (this.state.localMedia != null) { utils.timestampedLog('Close local media'); sylkrtc.utils.closeMediaStream(this.state.localMedia); this.setState({localMedia: null}); } } callKeepAcceptCall(callUUID) { // called from user interaction with Old alert panel // options used to be media to accept audio only but native panels do not have this feature utils.timestampedLog('CallKeep will answer call', callUUID); this.callKeeper.acceptCall(callUUID); this.hideInternalAlertPanel(); } callKeepRejectCall(callUUID) { // called from user interaction with Old alert panel utils.timestampedLog('CallKeep will reject call', callUUID); this.callKeeper.rejectCall(callUUID); this.hideInternalAlertPanel(); } acceptCall(callUUID) { utils.timestampedLog('User accepted call', callUUID); this.hideInternalAlertPanel(); this.resetGoToReadyTimer(); if (this.state.currentCall) { utils.timestampedLog('Will hangup current call first'); this.hangupCall(this.state.currentCall.id, 'accept_new_call'); // call will continue after transition to /ready } else { utils.timestampedLog('Will get local media now'); let hasVideo = (this.state.incomingCall && this.state.incomingCall.mediaTypes && this.state.incomingCall.mediaTypes.video) ? true : false; this.getLocalMedia(Object.assign({audio: true, video: hasVideo}), '/call'); } } rejectCall(callUUID) { // called by Call Keep when user rejects call utils.timestampedLog('User rejected call', callUUID); this.hideInternalAlertPanel(); if (!this.state.currentCall) { this.changeRoute('/ready', 'rejected'); } if (this.state.incomingCall && this.state.incomingCall.id === callUUID) { utils.timestampedLog('Sylkrtc terminate call', callUUID, 'in', this.state.incomingCall.state, 'state'); this.state.incomingCall.terminate(); } } hangupCall(callUUID, reason) { utils.timestampedLog('Call', callUUID, 'hangup with reason:', reason); let call = this.callKeeper._calls.get(callUUID); let direction = null; let targetUri = null; if (call) { let direction = call.direction; utils.timestampedLog('Sylkrtc terminate call', callUUID, 'in', call.state, 'state'); call.terminate(); } if (this.busyToneInterval) { clearInterval(this.busyToneInterval); this.busyToneInterval = null; } if (reason === 'outgoing_connection_failed') { this.setState({reconnectingCall: true, outgoingCallUUID: uuid.v4()}); return; } if (reason === 'user_cancel_call' || reason === 'user_hangup_call' || reason === 'answer_failed' || reason === 'callkeep_hangup_call' || reason === 'accept_new_call' || reason === 'stop_preview' || reason === 'escalate_to_conference' || reason === 'user_hangup_conference_confirmed' || reason === 'timeout' ) { this.changeRoute('/ready', reason); } else if (reason === 'user_hangup_conference') { utils.timestampedLog('Save conference maybe?'); setTimeout(() => { this.changeRoute('/ready', 'conference_really_ended'); }, 15000); } else if (reason === 'user_cancelled_conference') { utils.timestampedLog('Save conference maybe?'); setTimeout(() => { this.changeRoute('/ready', 'conference_really_ended'); }, 15000); } else { utils.timestampedLog('Will go to ready in 4 seconds (hangup)'); setTimeout(() => { this.changeRoute('/ready', reason); }, 4000); } } playBusyTone() { //utils.timestampedLog('Play busy tone'); InCallManager.stop({busytone: '_BUNDLE_'}); } callKeepSendDtmf(digits) { utils.timestampedLog('Send DTMF', digits); if (this.state.currentCall) { this.callKeeper.sendDTMF(this.state.currentCall.id, digits); } } toggleProximity() { storage.set('proximityEnabled', !this.state.proximityEnabled); if (!this.state.proximityEnabled) { utils.timestampedLog('Proximity sensor enabled'); } else { utils.timestampedLog('Proximity sensor disabled'); } this.setState({proximityEnabled: !this.state.proximityEnabled}); } toggleMute(callUUID, mute) { utils.timestampedLog('Toggle mute for call', callUUID, ':', mute); this.callKeeper.setMutedCall(callUUID, mute); this.setState({muted: mute}); } toggleSpeakerPhone() { if (this.state.speakerPhoneEnabled === true) { this.speakerphoneOff(); } else { this.speakerphoneOn(); } } speakerphoneOn() { utils.timestampedLog('Speakerphone On'); this.setState({speakerPhoneEnabled: true}); InCallManager.setForceSpeakerphoneOn(true); } speakerphoneOff() { utils.timestampedLog('Speakerphone Off'); this.setState({speakerPhoneEnabled: false}); InCallManager.setForceSpeakerphoneOn(false); } startGuestConference(targetUri) { this.setState({targetUri: targetUri}); this.getLocalMedia({audio: true, video: true}); } outgoingCall(call) { // called by sylkrtc.js when an outgoing call starts const localStreams = call.getLocalStreams(); let mediaType = 'audio'; let hasVideo = false; if (localStreams.length > 0) { const localStream = call.getLocalStreams()[0]; mediaType = localStream.getVideoTracks().length > 0 ? 'video' : 'audio'; hasVideo = localStream.getVideoTracks().length > 0 ? true : false; } this.callKeeper.startOutgoingCall(call.id, call.remoteIdentity.uri, hasVideo); utils.timestampedLog('Outgoing', mediaType, 'call', call.id, 'started to', call.remoteIdentity.uri); this.callKeeper.addWebsocketCall(call); call.on('stateChanged', this.callStateChanged); this.setState({currentCall: call}); } outgoingConference(call) { // called by sylrtc.js when an outgoing conference starts const localStreams = call.getLocalStreams(); let mediaType = 'audio'; let hasVideo = false; if (localStreams.length > 0) { const localStream = call.getLocalStreams()[0]; mediaType = localStream.getVideoTracks().length > 0 ? 'video' : 'audio'; hasVideo = localStream.getVideoTracks().length > 0 ? true : false; } utils.timestampedLog('Outgoing', mediaType, 'conference', call.id, 'started to', call.remoteIdentity.uri); this.callKeeper.addWebsocketCall(call); call.on('stateChanged', this.callStateChanged); this.setState({currentCall: call}); } _onLocalNotificationReceivedBackground(notification) { let notificationContent = notification.getData(); utils.timestampedLog('Handle local iOS PUSH notification: ', notificationContent); } _onNotificationReceivedBackground(notification) { let notificationContent = notification.getData(); const event = notificationContent['event']; const callUUID = notificationContent['session-id']; const to = notificationContent['to_uri']; const from = notificationContent['from_uri']; const displayName = notificationContent['from_display_name']; const outgoingMedia = {audio: true, video: notificationContent['media-type'] === 'video'}; const mediaType = notificationContent['media-type'] || 'audio'; /* * Local Notification Payload * * - `alertBody` : The message displayed in the notification alert. * - `alertAction` : The "action" displayed beneath an actionable notification. Defaults to "view"; * - `soundName` : The sound played when the notification is fired (optional). * - `category` : The category of this notification, required for actionable notifications (optional). * - `userInfo` : An optional object containing additional notification data. */ if (event === 'incoming_session') { utils.timestampedLog('Push notification: incoming call', callUUID); this.startedByPush = true; this.incomingCallFromPush(callUUID, from, displayName, mediaType); } else if (event === 'incoming_conference_request') { utils.timestampedLog('Push notification: incoming conference', callUUID); this.startedByPush = true; this.incomingConference(callUUID, to, from, displayName, outgoingMedia); } else if (event === 'cancel') { utils.timestampedLog('Push notification: cancel call', callUUID); VoipPushNotification.presentLocalNotification({alertBody:'Call cancelled'}); this.callKeeper.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.REMOTE_ENDED); } /* if (notificationContent['event'] === 'incoming_session') { VoipPushNotification.presentLocalNotification({ alertBody:'Incoming ' + notificationContent['media-type'] + ' call from ' + notificationContent['from_display_name'] }); } */ if (VoipPushNotification.wakeupByPush) { utils.timestampedLog('We wake up by push notification'); VoipPushNotification.wakeupByPush = false; VoipPushNotification.onVoipNotificationCompleted(callUUID); } } backToForeground() { if (this.state.appState !== 'active') { this.callKeeper.backToForeground(); } if (this.state.accountId) { this.handleRegistration(this.state.accountId, this.state.password); } } incomingConference(callUUID, to, from, displayName, outgoingMedia={audio: true, video: true}) { if (this.unmounted) { return; } const mediaType = outgoingMedia.video ? 'video' : 'audio'; utils.timestampedLog('Incoming', mediaType, 'conference invite from', from, displayName, 'to room', to); if (this.state.account && from === this.state.account.id) { utils.timestampedLog('Reject conference call from myself', callUUID); this.callKeeper.rejectCall(callUUID); return; } if (this.autoRejectIncomingCall(callUUID, from, to)) { return; } this.setState({incomingCallUUID: callUUID}); this.callKeeper.handleConference(callUUID, to, from, displayName, mediaType, outgoingMedia); } startConference(targetUri, options={audio: true, video: true, participants: []}) { utils.timestampedLog('New outgoing conference to room', targetUri); this.setState({targetUri: targetUri}); this.getLocalMedia({audio: options.audio, video: options.video}, '/conference'); } escalateToConference(participants) { let outgoingMedia = {audio: true, video: true}; let mediaType = 'video'; let call; if (this.state.currentCall) { call = this.state.currentCall; } else if (this.state.incomingCall) { call = this.state.currentCall; } else { console.log('No call to escalate'); return } const localStreams = call.getLocalStreams(); if (localStreams.length > 0) { const localStream = call.getLocalStreams()[0]; if (localStream.getVideoTracks().length == 0) { outgoingMedia.video = false; mediaType = 'audio'; } } this.outgoingMedia = outgoingMedia; this.participantsToInvite = participants; console.log('Escalate', mediaType, 'call', call.id, 'to conference with', participants.toString()); this.hangupCall(call.id, 'escalate_to_conference'); } conferenceInviteFromWebSocket(data) { // comes from web socket utils.timestampedLog('Conference invite from websocket', data.id, 'from', data.originator, 'for room', data.room); if (this.isConference()) { return; } //this._notificationCenter.postSystemNotification('Expecting conference invite', {body: `from ${data.originator.displayName || data.originator.uri}`}); } updateLinkingURL = (event) => { // this handles the use case where the app is running in the background and is activated by the listener... //console.log('Updated Linking url', event.url); this.eventFromUrl(event.url); DeepLinking.evaluateUrl(event.url); } eventFromUrl(url) { url = decodeURI(url); try { let direction; let event; let callUUID; let from; let to; let displayName; var url_parts = url.split("/"); let scheme = url_parts[0]; //console.log(url_parts); if (scheme === 'sylk:') { //sylk://conference/incoming/callUUID/from/to/media - when Android is asleep //sylk://call/outgoing/callUUID/to/displayName - from system dialer/history //sylk://call/incoming/callUUID/from/to/displayName - when Android is asleep //sylk://call/cancel//callUUID - when Android is asleep event = url_parts[2]; direction = url_parts[3]; callUUID = url_parts[4]; from = url_parts[5]; to = url_parts[6]; displayName = url_parts[7]; mediaType = url_parts[8] || 'audio'; if (event !== 'cancel' && from && from.search('@videoconference.') > -1) { event = 'conference'; to = from; } this.setState({targetUri: from}); } else if (scheme === 'https:') { // https://webrtc.sipthor.net/conference/DaffodilFlyChill0 from external web link // https://webrtc.sipthor.net/call/alice@example.com from external web link direction = 'outgoing'; event = url_parts[3]; to = url_parts[4]; callUUID = uuid.v4(); if (to.indexOf('@') === -1 && event === 'conference') { to = url_parts[4] + '@' + config.defaultConferenceDomain; } else if (to.indexOf('@') === -1 && event === 'call') { to = url_parts[4] + '@' + this.state.defaultDomain; } this.setState({targetUri: to}); } if (event === 'conference') { utils.timestampedLog('Conference from external URL:', url); this.startedByPush = true; if (direction === 'outgoing' && to) { utils.timestampedLog('Outgoing conference to', to); this.backToForeground(); this.callKeepStartConference(to, {audio: true, video: true, callUUID: callUUID}); } else if (direction === 'incoming' && from) { utils.timestampedLog('Incoming conference from', from); // allow app to wake up this.backToForeground(); const media = {audio: true, video: mediaType === 'video'} this.incomingConference(callUUID, to, from, displayName, media); } } else if (event === 'call') { this.startedByPush = true; if (direction === 'outgoing') { utils.timestampedLog('Call from external URL:', url); utils.timestampedLog('Outgoing call to', from); this.backToForeground(); this.callKeepStartCall(from, {audio: true, video: false, callUUID: callUUID}); } else if (direction === 'incoming') { utils.timestampedLog('Call from external URL:', url); utils.timestampedLog('Incoming call from', from); this.backToForeground(); this.incomingCallFromPush(callUUID, from, displayName, mediaType, true); } else if (direction === 'cancel') { this.cancelIncomingCall(callUUID); } } else { utils.timestampedLog('Error: Invalid external URL event', event); } } catch (err) { utils.timestampedLog('Error parsing URL', url, ":", err); } } autoRejectIncomingCall(callUUID, from, to) { //utils.timestampedLog('Check auto reject call from', from); if (this.state.blockedUris && this.state.blockedUris.indexOf(from) > -1) { utils.timestampedLog('Reject call', callUUID, 'from blocked URI', from); this.callKeeper.rejectCall(callUUID); this._notificationCenter.postSystemNotification('Call rejected', {body: `from ${from}`}); return true; } const fromDomain = '@' + from.split('@')[1] if (this.state.blockedUris && this.state.blockedUris.indexOf(fromDomain) > -1) { utils.timestampedLog('Reject call', callUUID, 'from blocked domain', fromDomain); this.callKeeper.rejectCall(callUUID); this._notificationCenter.postSystemNotification('Call rejected', {body: `from domain ${fromDomain}`}); return true; } if (this.state.currentCall && this.state.incomingCall && this.state.currentCall === this.state.incomingCall && this.state.incomingCall.id !== callUUID) { utils.timestampedLog('Reject second incoming call'); this.callKeeper.rejectCall(callUUID); } if (this.state.account && from === this.state.account.id && this.state.currentCall && this.state.currentCall.remoteIdentity.uri === from) { utils.timestampedLog('Reject call to myself', callUUID); this.callKeeper.rejectCall(callUUID); return true; } if (this._terminatedCalls.has(callUUID)) { utils.timestampedLog('Reject call already terminated', callUUID); this.cancelIncomingCall(callUUID); return true; } if (this.isConference()) { utils.timestampedLog('Reject call while in a conference', callUUID); if (to !== this.state.targetUri) { this._notificationCenter.postSystemNotification('Missed call from', {body: from}); } this.callKeeper.rejectCall(callUUID); return true; } if (this.state.currentCall && this.state.currentCall.state === 'progress' && this.state.currentCall.remoteIdentity.uri !== from) { utils.timestampedLog('Reject call while outgoing in progress', callUUID); this.callKeeper.rejectCall(callUUID); this._notificationCenter.postSystemNotification('Missed call from', {body: from}); return true; } return false; } autoAcceptIncomingCall(callUUID, from) { // TODO: handle ping pong where we call each other back if (this.state.currentCall && this.state.currentCall.direction === 'outgoing' && this.state.currentCall.state === 'progress' && this.state.currentCall.remoteIdentity.uri === from) { this.hangupCall(this.state.currentCall.id, 'accept_new_call'); this.setState({currentCall: null}); utils.timestampedLog('Auto accept incoming call from same address I am calling', callUUID); return true; } return false; } incomingCallFromPush(callUUID, from, displayName, mediaType, force) { //utils.timestampedLog('Handle incoming PUSH call', callUUID, 'from', from, '(', displayName, ')'); if (this.unmounted) { return; } if (this.autoRejectIncomingCall(callUUID, from)) { return; } //this.showInternalAlertPanel(); if (this.autoAcceptIncomingCall(callUUID, from)) { return; } this.goToReadyNowAndCancelTimer(); this.setState({targetUri: from}); let skipNativePanel = false; if (!this.callKeeper._calls.get(callUUID) || (this.state.currentCall && this.state.currentCall.direction === 'outgoing')) { //this._notificationCenter.postSystemNotification('Incoming call', {body: `from ${from}`}); if (Platform.OS === 'android' && this.state.appState === 'foreground') { skipNativePanel = true; } } this.callKeeper.incomingCallFromPush(callUUID, from, displayName, mediaType, force, skipNativePanel); } incomingCallFromWebSocket(call, mediaTypes) { if (this.unmounted) { return; } this.callKeeper.addWebsocketCall(call); const callUUID = call.id; const from = call.remoteIdentity.uri; //utils.timestampedLog('Handle incoming web socket call', callUUID, 'from', from, 'on connection', Object.id(this.state.connection)); // because of limitation in Sofia stack, we cannot have more then two calls at a time // we can have one outgoing call and one incoming call but not two incoming calls // we cannot have two incoming calls, second one is automatically rejected by sylkrtc.js if (this.autoRejectIncomingCall(callUUID, from)) { return; } const autoAccept = this.autoAcceptIncomingCall(callUUID, from); this.goToReadyNowAndCancelTimer(); call.mediaTypes = mediaTypes; call.on('stateChanged', this.callStateChanged); this.setState({incomingCall: call}); let skipNativePanel = false; if (this.state.currentCall && this.state.currentCall.direction === 'outgoing') { if (Platform.OS === 'android') { this.showInternalAlertPanel(); skipNativePanel = true; } } this.callKeeper.incomingCallFromWebSocket(call, autoAccept, skipNativePanel); } missedCall(data) { utils.timestampedLog('Missed call from ' + data.originator.uri, '(', data.originator.displayName, ')'); if (!this.state.currentCall) { let from = data.originator.displayName || data.originator.uri; this._notificationCenter.postSystemNotification('Missed call', {body: `from ${from}`}); if (Platform.OS === 'ios') { VoipPushNotification.presentLocalNotification({alertBody:'Missed call from ' + from}); } } this.updateServerHistory() } updateServerHistory() { if (this.currentRoute === '/ready') { this.setState({refreshHistory: !this.state.refreshHistory}); } } startPreview() { this.getLocalMedia({audio: true, video: true}, '/preview'); } updateHistoryEntry(callUUID) { let newHistory = this.state.localHistory; var historyItem = this.findObjectByKey(newHistory, 'sessionId', callUUID); if (historyItem) { let current_datetime = new Date(); let stopTime = current_datetime.getFullYear() + "-" + utils.appendLeadingZeroes(current_datetime.getMonth() + 1) + "-" + utils.appendLeadingZeroes(current_datetime.getDate()) + " " + utils.appendLeadingZeroes(current_datetime.getHours()) + ":" + utils.appendLeadingZeroes(current_datetime.getMinutes()) + ":" + utils.appendLeadingZeroes(current_datetime.getSeconds()); historyItem.stopTime = stopTime; var diff = current_datetime.getTime() - historyItem.startTimeObject.getTime(); historyItem.duration = parseInt(diff/1000); delete historyItem['startTimeObject']; if (this._historyConferenceParticipants.has(callUUID)) { historyItem.participants = this._historyConferenceParticipants.get(callUUID); } else { historyItem.participants = []; } this.setState({localHistory: newHistory}); storage.set('history', newHistory); } } setFavoriteUri(uri) { let favoriteUris = this.state.favoriteUris; let idx = favoriteUris.indexOf(uri); let ret; if (idx === -1) { favoriteUris.push(uri); ret = true; } else { let removed = favoriteUris.splice(idx, 1); ret = false; } storage.set('favoriteUris', favoriteUris); this.setState({favoriteUris: favoriteUris, refreshFavorites: !this.state.refreshFavorites}); return ret; } setBlockedUri(uri) { let blockedUris = this.state.blockedUris; console.log('Old blocked Uris:', blockedUris); let ret; let idx = blockedUris.indexOf(uri); if (idx === -1) { blockedUris.push(uri); ret = true; } else { let removed = blockedUris.splice(idx, 1); console.log('Removed', removed); ret = false; } console.log('New blocked Uris:', blockedUris); storage.set('blockedUris', blockedUris); this.setState({blockedUris: blockedUris}); return ret; } saveParticipant(callUUID, room, uri) { if (this._historyConferenceParticipants.has(callUUID)) { let old_participants = this._historyConferenceParticipants.get(callUUID); if (old_participants.indexOf(uri) === -1) { old_participants.push(uri); } } else { let new_participants = [uri]; this._historyConferenceParticipants.set(callUUID, new_participants); } if (!this.myParticipants) { this.myParticipants = new Object(); } if (this.myParticipants.hasOwnProperty(room)) { let old_uris = this.myParticipants[room]; if (old_uris.indexOf(uri) === -1 && uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { this.myParticipants[room].push(uri); } } else { let new_uris = []; if (uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { new_uris.push(uri); } if (new_uris) { this.myParticipants[room] = new_uris; } } storage.set('myParticipants', this.myParticipants); } saveDisplayName(uri, displayName) { displayName = displayName.trim(); let myDisplayNames; if (!this.state.myDisplayNames) { myDisplayNames = new Object(); } else { myDisplayNames = this.state.myDisplayNames; } myDisplayNames[uri] = displayName; storage.set('myDisplayNames', myDisplayNames); this.setState({myDisplayNames: myDisplayNames}); if (displayName && uri === this.state.accountId) { storage.set('displayName', displayName); this.setState({displayName: displayName}); if (this.state.account && displayName !== this.state.account.displayName) { this.processRegistration(this.state.accountId, this.state.password, displayName); } } } saveInvitedParties(room, uris) { room = room.split('@')[0]; //console.log('Save invited parties', uris, 'for room', room); if (!this.myInvitedParties) { this.myInvitedParties = new Object(); } if (this.myInvitedParties.hasOwnProperty(room)) { let old_uris = this.myInvitedParties[room]; uris.forEach((uri) => { if (old_uris.indexOf(uri) === -1 && uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { this.myInvitedParties[room].push(uri); } }); } else { let new_uris = []; uris.forEach((uri) => { if (uri !== this.state.account.id && (uri + '@' + this.state.defaultDomain) !== this.state.account.id) { new_uris.push(uri); } }); if (new_uris) { this.myInvitedParties[room] = new_uris; } } storage.set('myInvitedParties', this.myInvitedParties); this.setState({myInvitedParties: this.myInvitedParties}); } deleteHistoryEntry(uri) { let history = this.state.localHistory; for (var i = history.length - 1; i >= 0; --i) { if (history[i].remoteParty === uri) { history.splice(i,1); } } storage.set('history', history); this.setState({localHistory: history}); } addConferenceHistoryEntry(uri, callUUID, direction='placed', participants=[]) { let current_datetime = new Date(); let startTime = current_datetime.getFullYear() + "-" + utils.appendLeadingZeroes(current_datetime.getMonth() + 1) + "-" + utils.appendLeadingZeroes(current_datetime.getDate()) + " " + utils.appendLeadingZeroes(current_datetime.getHours()) + ":" + utils.appendLeadingZeroes(current_datetime.getMinutes()) + ":" + utils.appendLeadingZeroes(current_datetime.getSeconds()); let item = { remoteParty: uri, direction: direction, type: 'history', conference: true, participants: participants, media: ['audio', 'video'], displayName: uri.split('@')[0], sessionId: callUUID, startTime: startTime, stopTime: startTime, startTimeObject: current_datetime, duration: 0, tags: ['history', 'local'] }; const historyItem = Object.assign({}, item); let newHistory = this.state.localHistory; newHistory.push(historyItem); this.setState({localHistory: newHistory}); storage.set('history', newHistory); } render() { let footerBox = ; let extraStyles = {}; if (this.state.localMedia || this.state.registrationState === 'registered') { footerBox = null; } return ( this.setState({ Width_Layout : event.nativeEvent.layout.width, Height_Layout : event.nativeEvent.layout.height }, ()=> this._detectOrientation())}> ); } notFound(match) { const status = { title : '404', message : 'Oops, the page your looking for can\'t found', level : 'danger', width : 'large' } return ( ); } saveHistoryForLater(history) { //console.log('Cache history for later', history.length) this.cachedHistory = history; storage.set('cachedHistory', history); } hideLogsModal() { this.setState({showLogsModal: false}); } purgeLogs() { RNFS.unlink(logfile) .then(() => { utils.timestampedLog('Log file initialized'); this.showLogs(); }) // `unlink` will throw an error, if the item to unlink does not exist .catch((err) => { console.log(err.message); }); } showLogs() { this.setState({showLogsModal: true}); RNFS.readFile(logfile, 'utf8').then((content) => { console.log('Read', content.length, 'bytes from', logfile); const lastlines = content.split('\n').slice(-MAX_LOG_LINES).join('\n'); this.setState({logs: lastlines}); }); } trimLogs() { RNFS.readFile(logfile, 'utf8').then((content) => { const lines = content.split('\n'); //console.log('Read', lines.length, 'lines and', content.length, 'bytes from', logfile); if (lines.length > (MAX_LOG_LINES + 50) || content.length > 100000) { const text = lines.slice(-MAX_LOG_LINES).join('\n'); RNFS.writeFile(logfile, text + '\r\n', 'utf8') .then((success) => { //console.log('Trimmed logs to', MAX_LOG_LINES, 'lines and', text.length, 'bytes'); }) .catch((err) => { console.log(err.message); }); } }); } ready() { return ( ); } preview() { return ( ); } call() { let call = this.state.currentCall || this.state.incomingCall; return ( ) } conference() { let _previousParticipants = new Set(); /* if (this.myParticipants) { let room = this.state.targetUri.split('@')[0]; if (this.myParticipants.hasOwnProperty(room)) { let uris = this.myParticipants[room]; if (uris) { uris.forEach((uri) => { if (uri.search(this.state.defaultDomain) > -1) { let user = uri.split('@')[0]; _previousParticipants.add(user); } else { _previousParticipants.add(uri); } }); } } } */ if (this.myInvitedParties) { let room = this.state.targetUri.split('@')[0]; if (this.myInvitedParties.hasOwnProperty(room)) { let uris = this.myInvitedParties[room]; if (uris) { uris.forEach((uri) => { _previousParticipants.add(uri); }); } } } let previousParticipants = Array.from(_previousParticipants); return ( ) } conferenceByUri(urlParameters) { const targetUri = utils.normalizeUri(urlParameters.targetUri, config.defaultConferenceDomain); const idx = targetUri.indexOf('@'); const uri = {}; const pattern = /^[A-Za-z0-9\-\_]+$/g; uri.user = targetUri.substring(0, idx); // check if the uri.user is valid if (!pattern.test(uri.user)) { const status = { title : 'Invalid conference', message : `Oops, the conference ID is invalid: ${targetUri}`, level : 'danger', width : 'large' } return ( ); } return ( ); } login() { let registerBox; let statusBox; this.mustLogout = false; if (this.state.status !== null) { statusBox = ( ); } if (this.state.registrationState !== 'registered') { registerBox = ( ); } return ( {registerBox} {statusBox} ); } logout() { this.callKeeper.setAvailable(false); if (!this.mustLogout && this.state.registrationState !== null && this.state.connection && this.state.connection.state === 'ready') { // remove token from server this.mustLogout = true; this.state.account.setDeviceToken('None', Platform.OS, deviceId, true, bundleId); this.state.account.register(); return; } else if (this.mustLogout && this.state.connection && this.state.account) { this.state.account.unregister(); } this.tokenSent = false; if (this.state.connection && this.state.account) { this.state.connection.removeAccount(this.state.account, (error) => { if (error) { logger.debug(error); } }); } storage.set('account', {accountId: this.state.accountId, password: this.state.password}); this.serverHistory = []; this.setState({account: null, registrationState: null, registrationKeepalive: false, status: null, autoLogin: false, history: [], localHistory: [], cachedHistory: [], defaultDomain: config.defaultDomain }); this.changeRoute('/login'); return null; } main() { return null; } } export default Sylk; diff --git a/ios/sylk/AppDelegate.m b/ios/sylk/AppDelegate.m index c89821c..cc2a6c9 100644 --- a/ios/sylk/AppDelegate.m +++ b/ios/sylk/AppDelegate.m @@ -1,195 +1,207 @@ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "AppDelegate.h" #import #import #import #import #import #import #import @import Firebase; #import "RNCallKeep.h" #import "RNVoipPushNotificationManager.h" #import #import @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // RCTSetLogThreshold(RCTLogLevelInfo - 1); // RTCSetMinDebugLogLevel(RTCLoggingSeverityInfo); if ([FIRApp defaultApp] == nil) { [FIRApp configure]; } // Define UNUserNotificationCenter UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Sylk" initialProperties:nil]; rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; return YES; } //Called when a notification is delivered to a foreground app. -(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { completionHandler(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge); } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation ]; } - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler { BOOL handled = [RNCallKeep application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; if (!handled) { handled = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; } return handled; } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } // Required to register for notifications - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { [RNCPushNotificationIOS didRegisterUserNotificationSettings:notificationSettings]; } // Required for the register event. - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } // Required for the notification event. You must call the completion handler after handling the remote notification. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { NSLog(@"Got a PUSH NOTIFICATION"); NSString *eventType = userInfo[@"event"]; NSLog(@"Value of eventType = %@", eventType); if ([eventType isEqualToString:@"cancel"]) { NSString *calluuid = userInfo[@"session-id"]; BOOL active = [RNCallKeep isCallActive:calluuid]; if (active) { [RNCallKeep endCallWithUUID:calluuid reason:2]; } return completionHandler(UIBackgroundFetchResultNoData); } [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; } // Required for the registrationError event. - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error]; } // IOS 10+ Required for localNotification event - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { [RNCPushNotificationIOS didReceiveNotificationResponse:response]; completionHandler(); } // IOS 4-10 Required for the localNotification event. - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { [RNCPushNotificationIOS didReceiveLocalNotification:notification]; } // --- Handle updated push credentials - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type { // Register VoIP push token (a property of PKPushCredentials) with server [RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type]; } - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type { // --- The system calls this method when a previously provided push token is no longer valid for use. No action is necessary on your part to reregister the push type. Instead, use this method to notify your server not to send push notifications using the matching push token. } // --- Handle incoming pushes (for ios <= 10) - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type { [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type]; } // Handle incoming pushes - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type withCompletionHandler:(void (^)(void))completion{ NSLog(@"Got a PUSHKIT NOTIFICATION"); [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type]; // Retrieve information like handle and callerName here NSString *eventType = [payload.dictionaryPayload valueForKey:@"event"]; NSLog(@"Value of eventType = %@", eventType); // if ([eventType isEqualToString:@"incoming_session"]) // { NSString *calluuid = [payload.dictionaryPayload valueForKey:@"session-id"]; NSString *mediaType = [payload.dictionaryPayload valueForKey:@"media-type"]; NSString *callerName = [payload.dictionaryPayload valueForKey:@"from_display_name"]; NSString *handle = [payload.dictionaryPayload valueForKey:@"from_uri"]; [RNVoipPushNotificationManager addCompletionHandler:calluuid completionHandler:completion]; //you can't do this check - you HAVE to call reportNewIncomingCall otherwise apple will start killing your app if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { - [RNCallKeep reportNewIncomingCall:calluuid handle:handle handleType:@"generic" hasVideo:[mediaType isEqualToString:@"video"] localizedCallerName:callerName fromPushKit: YES payload:payload.dictionaryPayload withCompletionHandler:nil]; + + [RNCallKeep reportNewIncomingCall: calluuid + handle: handle + handleType: @"generic" + hasVideo: [mediaType isEqualToString:@"video"] + localizedCallerName: callerName + supportsHolding: NO + supportsDTMF: YES + supportsGrouping: YES + supportsUngrouping: YES + fromPushKit: YES + payload: payload.dictionaryPayload + withCompletionHandler: completion]; } // } // else { //completion(); // } } @end diff --git a/patches/react-native-callkeep+4.0.1.patch b/patches/react-native-callkeep+4.0.1.patch index 83cac6c..37cd8e3 100644 --- a/patches/react-native-callkeep+4.0.1.patch +++ b/patches/react-native-callkeep+4.0.1.patch @@ -1,662 +1,328 @@ diff --git a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/Constants.java b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/Constants.java index e0b8bc5..b48417a 100644 --- a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/Constants.java +++ b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/Constants.java @@ -13,7 +13,7 @@ public class Constants { public static final String ACTION_UNMUTE_CALL = "ACTION_UNMUTE_CALL"; public static final String ACTION_WAKE_APP = "ACTION_WAKE_APP"; - public static final String EXTRA_CALL_NUMBER = "EXTRA_CALL_NUMBER"; + public static final String EXTRA_CALL_IDENTIFIER = "EXTRA_CALL_NUMBER"; public static final String EXTRA_CALL_UUID = "EXTRA_CALL_UUID"; public static final String EXTRA_CALLER_NAME = "EXTRA_CALLER_NAME"; // Can't use telecom.EXTRA_DISABLE_ADD_CALL ... diff --git a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepBackgroundMessagingService.java b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepBackgroundMessagingService.java index c069072..a1e49ce 100644 --- a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepBackgroundMessagingService.java +++ b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepBackgroundMessagingService.java @@ -27,7 +27,7 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.jstasks.HeadlessJsTaskConfig; import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME; -import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER; +import static io.wazo.callkeep.Constants.EXTRA_CALL_IDENTIFIER; import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID; import javax.annotation.Nullable; diff --git a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java index 327d49b..7372350 100644 --- a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java +++ b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java @@ -44,6 +44,7 @@ import android.telecom.DisconnectCause; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; +import android.telecom.VideoProfile; import android.telephony.TelephonyManager; import android.util.Log; @@ -73,7 +74,7 @@ import static androidx.core.app.ActivityCompat.requestPermissions; import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME; import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID; -import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER; +import static io.wazo.callkeep.Constants.EXTRA_CALL_IDENTIFIER; import static io.wazo.callkeep.Constants.ACTION_END_CALL; import static io.wazo.callkeep.Constants.ACTION_ANSWER_CALL; import static io.wazo.callkeep.Constants.ACTION_MUTE_CALL; @@ -155,20 +156,24 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { } @ReactMethod - public void displayIncomingCall(String uuid, String number, String callerName) { + public void displayIncomingCall(String uuid, String identifier, String callerType, boolean callHasVideo, String callerName) { if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { return; } - Log.d(TAG, "displayIncomingCall number: " + number + ", callerName: " + callerName); + Log.d(TAG, "displayIncomingCall identifier: " + identifier + ", callerName: " + callerName + ", callerType: " + callerType + ", callHasVideo: " + callHasVideo); Bundle extras = new Bundle(); - Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null); + Uri uri = Uri.fromParts((callerType.equals("sip") ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL), identifier, null); extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri); extras.putString(EXTRA_CALLER_NAME, callerName); extras.putString(EXTRA_CALL_UUID, uuid); + if (callHasVideo) { + extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_BIDIRECTIONAL); + } + telecomManager.addNewIncomingCall(handle, extras); } @@ -187,24 +192,28 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { } @ReactMethod - public void startCall(String uuid, String number, String callerName) { - if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || number == null) { + public void startCall(String uuid, String identifer, String callerName, String callerType, boolean callHasVideo) { + if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || identifer == null) { return; } - Log.d(TAG, "startCall number: " + number + ", callerName: " + callerName); + Log.d(TAG, "startCall identifer: " + identifer + ", callerName: " + callerName + ", callerType: " + callerType + ", callHasVideo: " + callHasVideo); Bundle extras = new Bundle(); - Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null); + Uri uri = Uri.fromParts((callerType.equals("sip") ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL), identifer, null); Bundle callExtras = new Bundle(); callExtras.putString(EXTRA_CALLER_NAME, callerName); callExtras.putString(EXTRA_CALL_UUID, uuid); - callExtras.putString(EXTRA_CALL_NUMBER, number); + callExtras.putString(EXTRA_CALL_IDENTIFIER, identifer); extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle); extras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras); + if (callHasVideo) { + extras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, VideoProfile.STATE_BIDIRECTIONAL); + } + telecomManager.placeCall(uri, extras); } @@ -485,7 +494,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { return; } - conn.setConnectionCapabilities(conn.getConnectionCapabilities() | Connection.CAPABILITY_HOLD); + conn.setConnectionCapabilities(conn.getConnectionCapabilities()); conn.setActive(); } @@ -575,7 +584,8 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { String appName = this.getApplicationName(this.getAppContext()); PhoneAccount.Builder builder = new PhoneAccount.Builder(handle, appName) - .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER); + .addSupportedUriScheme(PhoneAccount.SCHEME_SIP) + .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER | PhoneAccount.CAPABILITY_VIDEO_CALLING); if (_settings != null && _settings.hasKey("imageName")) { int identifier = appContext.getResources().getIdentifier(_settings.getString("imageName"), "drawable", appContext.getPackageName()); @@ -700,7 +710,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { sendEventToJS("RNCallKeepDidPerformDTMFAction", args); break; case ACTION_ONGOING_CALL: - args.putString("handle", attributeMap.get(EXTRA_CALL_NUMBER)); + args.putString("handle", attributeMap.get(EXTRA_CALL_IDENTIFIER)); args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); args.putString("name", attributeMap.get(EXTRA_CALLER_NAME)); sendEventToJS("RNCallKeepDidReceiveStartCallAction", args); @@ -715,8 +725,8 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { Intent headlessIntent = new Intent(reactContext, RNCallKeepBackgroundMessagingService.class); headlessIntent.putExtra("callUUID", attributeMap.get(EXTRA_CALL_UUID)); headlessIntent.putExtra("name", attributeMap.get(EXTRA_CALLER_NAME)); - headlessIntent.putExtra("handle", attributeMap.get(EXTRA_CALL_NUMBER)); - Log.d(TAG, "wakeUpApplication: " + attributeMap.get(EXTRA_CALL_UUID) + ", number : " + attributeMap.get(EXTRA_CALL_NUMBER) + ", displayName:" + attributeMap.get(EXTRA_CALLER_NAME)); + headlessIntent.putExtra("handle", attributeMap.get(EXTRA_CALL_IDENTIFIER)); + Log.d(TAG, "wakeUpApplication: " + attributeMap.get(EXTRA_CALL_UUID) + ", number : " + attributeMap.get(EXTRA_CALL_IDENTIFIER) + ", displayName:" + attributeMap.get(EXTRA_CALLER_NAME)); ComponentName name = reactContext.startService(headlessIntent); if (name != null) { diff --git a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnection.java b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnection.java index 3deced5..3e4d44b 100644 --- a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnection.java +++ b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnection.java @@ -46,7 +46,7 @@ import static io.wazo.callkeep.Constants.ACTION_MUTE_CALL; import static io.wazo.callkeep.Constants.ACTION_UNHOLD_CALL; import static io.wazo.callkeep.Constants.ACTION_UNMUTE_CALL; import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME; -import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER; +import static io.wazo.callkeep.Constants.EXTRA_CALL_IDENTIFIER; import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID; @TargetApi(Build.VERSION_CODES.M) @@ -61,11 +61,11 @@ public class VoiceConnection extends Connection { this.handle = handle; this.context = context; - String number = handle.get(EXTRA_CALL_NUMBER); + String identifier = handle.get(EXTRA_CALL_IDENTIFIER); String name = handle.get(EXTRA_CALLER_NAME); - if (number != null) { - setAddress(Uri.parse(number), TelecomManager.PRESENTATION_ALLOWED); + if (identifier != null) { + setAddress(Uri.parse(identifier), TelecomManager.PRESENTATION_ALLOWED); } if (name != null && !name.equals("")) { setCallerDisplayName(name, TelecomManager.PRESENTATION_ALLOWED); @@ -96,7 +96,7 @@ public class VoiceConnection extends Connection { super.onAnswer(); Log.d(TAG, "onAnswer called"); - setConnectionCapabilities(getConnectionCapabilities() | Connection.CAPABILITY_HOLD); + setConnectionCapabilities(getConnectionCapabilities()); setAudioModeIsVoip(true); sendCallRequestToActivity(ACTION_ANSWER_CALL, handle); diff --git a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java index aa83b53..91cbbe9 100644 --- a/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java +++ b/node_modules/react-native-callkeep/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java @@ -61,7 +61,7 @@ import static io.wazo.callkeep.Constants.ACTION_ONGOING_CALL; import static io.wazo.callkeep.Constants.ACTION_CHECK_REACHABILITY; import static io.wazo.callkeep.Constants.ACTION_WAKE_APP; import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME; -import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER; +import static io.wazo.callkeep.Constants.EXTRA_CALL_IDENTIFIER; import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID; import static io.wazo.callkeep.Constants.EXTRA_DISABLE_ADD_CALL; import static io.wazo.callkeep.Constants.FOREGROUND_SERVICE_TYPE_MICROPHONE; @@ -138,7 +138,7 @@ public class VoiceConnectionService extends ConnectionService { @Override public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { Bundle extra = request.getExtras(); - Uri number = request.getAddress(); + Uri identifier = request.getAddress(); String name = extra.getString(EXTRA_CALLER_NAME); Connection incomingCallConnection = createConnection(request); incomingCallConnection.setRinging(); @@ -166,27 +166,27 @@ public class VoiceConnectionService extends ConnectionService { private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Boolean forceWakeUp) { Bundle extras = request.getExtras(); Connection outgoingCallConnection = null; - String number = request.getAddress().getSchemeSpecificPart(); - String extrasNumber = extras.getString(EXTRA_CALL_NUMBER); + String identifier = request.getAddress().getSchemeSpecificPart(); + String extrasIdentifier = extras.getString(EXTRA_CALL_IDENTIFIER); String displayName = extras.getString(EXTRA_CALLER_NAME); Boolean isForeground = VoiceConnectionService.isRunning(this.getApplicationContext()); - Log.d(TAG, "makeOutgoingCall:" + uuid + ", number: " + number + ", displayName:" + displayName); + Log.d(TAG, "makeOutgoingCall:" + uuid + ", identifier: " + identifier + ", displayName:" + displayName); // Wakeup application if needed if (!isForeground || forceWakeUp) { Log.d(TAG, "onCreateOutgoingConnection: Waking up application"); - this.wakeUpApplication(uuid, number, displayName); + this.wakeUpApplication(uuid, identifier, displayName); } else if (!this.canMakeOutgoingCall() && isReachable) { Log.d(TAG, "onCreateOutgoingConnection: not available"); return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.LOCAL)); } // TODO: Hold all other calls - if (extrasNumber == null || !extrasNumber.equals(number)) { + if (extrasIdentifier == null || !extrasIdentifier.equals(identifier)) { extras.putString(EXTRA_CALL_UUID, uuid); extras.putString(EXTRA_CALLER_NAME, displayName); - extras.putString(EXTRA_CALL_NUMBER, number); + extras.putString(EXTRA_CALL_IDENTIFIER, identifier); } if (!canMakeMultipleCalls) { @@ -251,15 +251,15 @@ public class VoiceConnectionService extends ConnectionService { startForeground(FOREGROUND_SERVICE_TYPE_MICROPHONE, notification); } - private void wakeUpApplication(String uuid, String number, String displayName) { + private void wakeUpApplication(String uuid, String identifier, String displayName) { Intent headlessIntent = new Intent( this.getApplicationContext(), RNCallKeepBackgroundMessagingService.class ); headlessIntent.putExtra("callUUID", uuid); headlessIntent.putExtra("name", displayName); - headlessIntent.putExtra("handle", number); - Log.d(TAG, "wakeUpApplication: " + uuid + ", number : " + number + ", displayName:" + displayName); + headlessIntent.putExtra("handle", identifier); + Log.d(TAG, "wakeUpApplication: " + uuid + ", identifier : " + identifier + ", displayName:" + displayName); ComponentName name = this.getApplicationContext().startService(headlessIntent); if (name != null) { @@ -273,9 +273,9 @@ public class VoiceConnectionService extends ConnectionService { } Log.d(TAG, "checkReachability timeout, force wakeup"); Bundle extras = request.getExtras(); - String number = request.getAddress().getSchemeSpecificPart(); + String identifier = request.getAddress().getSchemeSpecificPart(); String displayName = extras.getString(EXTRA_CALLER_NAME); - wakeUpApplication(this.notReachableCallUuid, number, displayName); + wakeUpApplication(this.notReachableCallUuid, identifier, displayName); VoiceConnectionService.currentConnectionRequest = null; } @@ -301,9 +301,9 @@ public class VoiceConnectionService extends ConnectionService { private Connection createConnection(ConnectionRequest request) { Bundle extras = request.getExtras(); HashMap extrasMap = this.bundleToMap(extras); - extrasMap.put(EXTRA_CALL_NUMBER, request.getAddress().toString()); + extrasMap.put(EXTRA_CALL_IDENTIFIER, request.getAddress().toString()); VoiceConnection connection = new VoiceConnection(this, extrasMap); - connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD); + connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE); connection.setInitializing(); connection.setExtras(extras); currentConnections.put(extras.getString(EXTRA_CALL_UUID), connection); diff --git a/node_modules/react-native-callkeep/index.js b/node_modules/react-native-callkeep/index.js index 6ec6c20..27a6788 100644 --- a/node_modules/react-native-callkeep/index.js +++ b/node_modules/react-native-callkeep/index.js @@ -73,7 +73,7 @@ class RNCallKeep { displayIncomingCall = (uuid, handle, localizedCallerName = '', handleType = 'number', hasVideo = false, options = null) => { if (!isIOS) { - RNCallKeepModule.displayIncomingCall(uuid, handle, localizedCallerName); + RNCallKeepModule.displayIncomingCall(uuid, handle, handleType, hasVideo, localizedCallerName); return; } @@ -94,7 +94,7 @@ class RNCallKeep { startCall = (uuid, handle, contactIdentifier, handleType = 'number', hasVideo = false ) => { if (!isIOS) { - RNCallKeepModule.startCall(uuid, handle, contactIdentifier); + RNCallKeepModule.startCall(uuid, handle, contactIdentifier, handleType, hasVideo); return; } diff --git a/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m b/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m index 04af403..f6caf24 100644 --- a/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m +++ b/node_modules/react-native-callkeep/ios/RNCallKeep/RNCallKeep.m @@ -285,7 +285,7 @@ + (void)initCallKitProvider { NSLog(@"[RNCallKeep][updateDisplay] uuidString = %@ displayName = %@ uri = %@", uuidString, displayName, uri); #endif NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; - CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:uri]; + CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:uri]; CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; callUpdate.localizedCallerName = displayName; callUpdate.remoteHandle = callHandle; @@ -511,7 +511,7 @@ + (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary*)settings int _handleType = [RNCallKeep getHandleType:settings[@"handleType"]]; providerConfiguration.supportedHandleTypes = [NSSet setWithObjects:[NSNumber numberWithInteger:_handleType], nil]; }else{ - providerConfiguration.supportedHandleTypes = [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypePhoneNumber], nil]; + providerConfiguration.supportedHandleTypes = [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil]; } if (settings[@"supportsVideo"]) { - providerConfiguration.supportsVideo = [settings[@"supportsVideo"] boolValue]; -diff --git a/node_modules/react-native-callkeep/react-native-callkeep.patch b/node_modules/react-native-callkeep/react-native-callkeep.patch -new file mode 100644 -index 0000000..f441e61 ---- /dev/null -+++ b/node_modules/react-native-callkeep/react-native-callkeep.patch -@@ -0,0 +1,328 @@ -+diff --git a/android/src/main/java/io/wazo/callkeep/Constants.java b/android/src/main/java/io/wazo/callkeep/Constants.java -+index e0b8bc5..b48417a 100644 -+--- a/android/src/main/java/io/wazo/callkeep/Constants.java -++++ b/android/src/main/java/io/wazo/callkeep/Constants.java -+@@ -13,7 +13,7 @@ public class Constants { -+ public static final String ACTION_UNMUTE_CALL = "ACTION_UNMUTE_CALL"; -+ public static final String ACTION_WAKE_APP = "ACTION_WAKE_APP"; -+ -+- public static final String EXTRA_CALL_NUMBER = "EXTRA_CALL_NUMBER"; -++ public static final String EXTRA_CALL_IDENTIFIER = "EXTRA_CALL_NUMBER"; -+ public static final String EXTRA_CALL_UUID = "EXTRA_CALL_UUID"; -+ public static final String EXTRA_CALLER_NAME = "EXTRA_CALLER_NAME"; -+ // Can't use telecom.EXTRA_DISABLE_ADD_CALL ... -+diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepBackgroundMessagingService.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepBackgroundMessagingService.java -+index c069072..a1e49ce 100644 -+--- a/android/src/main/java/io/wazo/callkeep/RNCallKeepBackgroundMessagingService.java -++++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepBackgroundMessagingService.java -+@@ -27,7 +27,7 @@ import com.facebook.react.bridge.Arguments; -+ import com.facebook.react.jstasks.HeadlessJsTaskConfig; -+ -+ import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME; -+-import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER; -++import static io.wazo.callkeep.Constants.EXTRA_CALL_IDENTIFIER; -+ import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID; -+ -+ import javax.annotation.Nullable; -+diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java -+index 327d49b..7372350 100644 -+--- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java -++++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java -+@@ -44,6 +44,7 @@ import android.telecom.DisconnectCause; -+ import android.telecom.PhoneAccount; -+ import android.telecom.PhoneAccountHandle; -+ import android.telecom.TelecomManager; -++import android.telecom.VideoProfile; -+ import android.telephony.TelephonyManager; -+ import android.util.Log; -+ -+@@ -73,7 +74,7 @@ import static androidx.core.app.ActivityCompat.requestPermissions; -+ -+ import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME; -+ import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID; -+-import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER; -++import static io.wazo.callkeep.Constants.EXTRA_CALL_IDENTIFIER; -+ import static io.wazo.callkeep.Constants.ACTION_END_CALL; -+ import static io.wazo.callkeep.Constants.ACTION_ANSWER_CALL; -+ import static io.wazo.callkeep.Constants.ACTION_MUTE_CALL; -+@@ -155,20 +156,24 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { -+ } -+ -+ @ReactMethod -+- public void displayIncomingCall(String uuid, String number, String callerName) { -++ public void displayIncomingCall(String uuid, String identifier, String callerType, boolean callHasVideo, String callerName) { -+ if (!isConnectionServiceAvailable() || !hasPhoneAccount()) { -+ return; -+ } -+ -+- Log.d(TAG, "displayIncomingCall number: " + number + ", callerName: " + callerName); -++ Log.d(TAG, "displayIncomingCall identifier: " + identifier + ", callerName: " + callerName + ", callerType: " + callerType + ", callHasVideo: " + callHasVideo); -+ -+ Bundle extras = new Bundle(); -+- Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null); -++ Uri uri = Uri.fromParts((callerType.equals("sip") ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL), identifier, null); -+ -+ extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri); -+ extras.putString(EXTRA_CALLER_NAME, callerName); -+ extras.putString(EXTRA_CALL_UUID, uuid); -+ -++ if (callHasVideo) { -++ extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_BIDIRECTIONAL); -++ } -++ -+ telecomManager.addNewIncomingCall(handle, extras); -+ } -+ -+@@ -187,24 +192,28 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { -+ } -+ -+ @ReactMethod -+- public void startCall(String uuid, String number, String callerName) { -+- if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || number == null) { -++ public void startCall(String uuid, String identifer, String callerName, String callerType, boolean callHasVideo) { -++ if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || identifer == null) { -+ return; -+ } -+ -+- Log.d(TAG, "startCall number: " + number + ", callerName: " + callerName); -++ Log.d(TAG, "startCall identifer: " + identifer + ", callerName: " + callerName + ", callerType: " + callerType + ", callHasVideo: " + callHasVideo); -+ -+ Bundle extras = new Bundle(); -+- Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null); -++ Uri uri = Uri.fromParts((callerType.equals("sip") ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL), identifer, null); -+ -+ Bundle callExtras = new Bundle(); -+ callExtras.putString(EXTRA_CALLER_NAME, callerName); -+ callExtras.putString(EXTRA_CALL_UUID, uuid); -+- callExtras.putString(EXTRA_CALL_NUMBER, number); -++ callExtras.putString(EXTRA_CALL_IDENTIFIER, identifer); -+ -+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle); -+ extras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras); -+ -++ if (callHasVideo) { -++ extras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, VideoProfile.STATE_BIDIRECTIONAL); -++ } -++ -+ telecomManager.placeCall(uri, extras); -+ } -+ -+@@ -485,7 +494,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { -+ return; -+ } -+ -+- conn.setConnectionCapabilities(conn.getConnectionCapabilities() | Connection.CAPABILITY_HOLD); -++ conn.setConnectionCapabilities(conn.getConnectionCapabilities()); -+ conn.setActive(); -+ } -+ -+@@ -575,7 +584,8 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { -+ String appName = this.getApplicationName(this.getAppContext()); -+ -+ PhoneAccount.Builder builder = new PhoneAccount.Builder(handle, appName) -+- .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER); -++ .addSupportedUriScheme(PhoneAccount.SCHEME_SIP) -++ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER | PhoneAccount.CAPABILITY_VIDEO_CALLING); -+ -+ if (_settings != null && _settings.hasKey("imageName")) { -+ int identifier = appContext.getResources().getIdentifier(_settings.getString("imageName"), "drawable", appContext.getPackageName()); -+@@ -700,7 +710,7 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { -+ sendEventToJS("RNCallKeepDidPerformDTMFAction", args); -+ break; -+ case ACTION_ONGOING_CALL: -+- args.putString("handle", attributeMap.get(EXTRA_CALL_NUMBER)); -++ args.putString("handle", attributeMap.get(EXTRA_CALL_IDENTIFIER)); -+ args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID)); -+ args.putString("name", attributeMap.get(EXTRA_CALLER_NAME)); -+ sendEventToJS("RNCallKeepDidReceiveStartCallAction", args); -+@@ -715,8 +725,8 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule { -+ Intent headlessIntent = new Intent(reactContext, RNCallKeepBackgroundMessagingService.class); -+ headlessIntent.putExtra("callUUID", attributeMap.get(EXTRA_CALL_UUID)); -+ headlessIntent.putExtra("name", attributeMap.get(EXTRA_CALLER_NAME)); -+- headlessIntent.putExtra("handle", attributeMap.get(EXTRA_CALL_NUMBER)); -+- Log.d(TAG, "wakeUpApplication: " + attributeMap.get(EXTRA_CALL_UUID) + ", number : " + attributeMap.get(EXTRA_CALL_NUMBER) + ", displayName:" + attributeMap.get(EXTRA_CALLER_NAME)); -++ headlessIntent.putExtra("handle", attributeMap.get(EXTRA_CALL_IDENTIFIER)); -++ Log.d(TAG, "wakeUpApplication: " + attributeMap.get(EXTRA_CALL_UUID) + ", number : " + attributeMap.get(EXTRA_CALL_IDENTIFIER) + ", displayName:" + attributeMap.get(EXTRA_CALLER_NAME)); -+ -+ ComponentName name = reactContext.startService(headlessIntent); -+ if (name != null) { -+diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnection.java b/android/src/main/java/io/wazo/callkeep/VoiceConnection.java -+index 3deced5..3e4d44b 100644 -+--- a/android/src/main/java/io/wazo/callkeep/VoiceConnection.java -++++ b/android/src/main/java/io/wazo/callkeep/VoiceConnection.java -+@@ -46,7 +46,7 @@ import static io.wazo.callkeep.Constants.ACTION_MUTE_CALL; -+ import static io.wazo.callkeep.Constants.ACTION_UNHOLD_CALL; -+ import static io.wazo.callkeep.Constants.ACTION_UNMUTE_CALL; -+ import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME; -+-import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER; -++import static io.wazo.callkeep.Constants.EXTRA_CALL_IDENTIFIER; -+ import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID; -+ -+ @TargetApi(Build.VERSION_CODES.M) -+@@ -61,11 +61,11 @@ public class VoiceConnection extends Connection { -+ this.handle = handle; -+ this.context = context; -+ -+- String number = handle.get(EXTRA_CALL_NUMBER); -++ String identifier = handle.get(EXTRA_CALL_IDENTIFIER); -+ String name = handle.get(EXTRA_CALLER_NAME); -+ -+- if (number != null) { -+- setAddress(Uri.parse(number), TelecomManager.PRESENTATION_ALLOWED); -++ if (identifier != null) { -++ setAddress(Uri.parse(identifier), TelecomManager.PRESENTATION_ALLOWED); -+ } -+ if (name != null && !name.equals("")) { -+ setCallerDisplayName(name, TelecomManager.PRESENTATION_ALLOWED); -+@@ -96,7 +96,7 @@ public class VoiceConnection extends Connection { -+ super.onAnswer(); -+ Log.d(TAG, "onAnswer called"); -+ -+- setConnectionCapabilities(getConnectionCapabilities() | Connection.CAPABILITY_HOLD); -++ setConnectionCapabilities(getConnectionCapabilities()); -+ setAudioModeIsVoip(true); -+ -+ sendCallRequestToActivity(ACTION_ANSWER_CALL, handle); -+diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java -+index aa83b53..91cbbe9 100644 -+--- a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java -++++ b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java -+@@ -61,7 +61,7 @@ import static io.wazo.callkeep.Constants.ACTION_ONGOING_CALL; -+ import static io.wazo.callkeep.Constants.ACTION_CHECK_REACHABILITY; -+ import static io.wazo.callkeep.Constants.ACTION_WAKE_APP; -+ import static io.wazo.callkeep.Constants.EXTRA_CALLER_NAME; -+-import static io.wazo.callkeep.Constants.EXTRA_CALL_NUMBER; -++import static io.wazo.callkeep.Constants.EXTRA_CALL_IDENTIFIER; -+ import static io.wazo.callkeep.Constants.EXTRA_CALL_UUID; -+ import static io.wazo.callkeep.Constants.EXTRA_DISABLE_ADD_CALL; -+ import static io.wazo.callkeep.Constants.FOREGROUND_SERVICE_TYPE_MICROPHONE; -+@@ -138,7 +138,7 @@ public class VoiceConnectionService extends ConnectionService { -+ @Override -+ public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { -+ Bundle extra = request.getExtras(); -+- Uri number = request.getAddress(); -++ Uri identifier = request.getAddress(); -+ String name = extra.getString(EXTRA_CALLER_NAME); -+ Connection incomingCallConnection = createConnection(request); -+ incomingCallConnection.setRinging(); -+@@ -166,27 +166,27 @@ public class VoiceConnectionService extends ConnectionService { -+ private Connection makeOutgoingCall(ConnectionRequest request, String uuid, Boolean forceWakeUp) { -+ Bundle extras = request.getExtras(); -+ Connection outgoingCallConnection = null; -+- String number = request.getAddress().getSchemeSpecificPart(); -+- String extrasNumber = extras.getString(EXTRA_CALL_NUMBER); -++ String identifier = request.getAddress().getSchemeSpecificPart(); -++ String extrasIdentifier = extras.getString(EXTRA_CALL_IDENTIFIER); -+ String displayName = extras.getString(EXTRA_CALLER_NAME); -+ Boolean isForeground = VoiceConnectionService.isRunning(this.getApplicationContext()); -+ -+- Log.d(TAG, "makeOutgoingCall:" + uuid + ", number: " + number + ", displayName:" + displayName); -++ Log.d(TAG, "makeOutgoingCall:" + uuid + ", identifier: " + identifier + ", displayName:" + displayName); -+ -+ // Wakeup application if needed -+ if (!isForeground || forceWakeUp) { -+ Log.d(TAG, "onCreateOutgoingConnection: Waking up application"); -+- this.wakeUpApplication(uuid, number, displayName); -++ this.wakeUpApplication(uuid, identifier, displayName); -+ } else if (!this.canMakeOutgoingCall() && isReachable) { -+ Log.d(TAG, "onCreateOutgoingConnection: not available"); -+ return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.LOCAL)); -+ } -+ -+ // TODO: Hold all other calls -+- if (extrasNumber == null || !extrasNumber.equals(number)) { -++ if (extrasIdentifier == null || !extrasIdentifier.equals(identifier)) { -+ extras.putString(EXTRA_CALL_UUID, uuid); -+ extras.putString(EXTRA_CALLER_NAME, displayName); -+- extras.putString(EXTRA_CALL_NUMBER, number); -++ extras.putString(EXTRA_CALL_IDENTIFIER, identifier); -+ } -+ -+ if (!canMakeMultipleCalls) { -+@@ -251,15 +251,15 @@ public class VoiceConnectionService extends ConnectionService { -+ startForeground(FOREGROUND_SERVICE_TYPE_MICROPHONE, notification); -+ } -+ -+- private void wakeUpApplication(String uuid, String number, String displayName) { -++ private void wakeUpApplication(String uuid, String identifier, String displayName) { -+ Intent headlessIntent = new Intent( -+ this.getApplicationContext(), -+ RNCallKeepBackgroundMessagingService.class -+ ); -+ headlessIntent.putExtra("callUUID", uuid); -+ headlessIntent.putExtra("name", displayName); -+- headlessIntent.putExtra("handle", number); -+- Log.d(TAG, "wakeUpApplication: " + uuid + ", number : " + number + ", displayName:" + displayName); -++ headlessIntent.putExtra("handle", identifier); -++ Log.d(TAG, "wakeUpApplication: " + uuid + ", identifier : " + identifier + ", displayName:" + displayName); -+ -+ ComponentName name = this.getApplicationContext().startService(headlessIntent); -+ if (name != null) { -+@@ -273,9 +273,9 @@ public class VoiceConnectionService extends ConnectionService { -+ } -+ Log.d(TAG, "checkReachability timeout, force wakeup"); -+ Bundle extras = request.getExtras(); -+- String number = request.getAddress().getSchemeSpecificPart(); -++ String identifier = request.getAddress().getSchemeSpecificPart(); -+ String displayName = extras.getString(EXTRA_CALLER_NAME); -+- wakeUpApplication(this.notReachableCallUuid, number, displayName); -++ wakeUpApplication(this.notReachableCallUuid, identifier, displayName); -+ -+ VoiceConnectionService.currentConnectionRequest = null; -+ } -+@@ -301,9 +301,9 @@ public class VoiceConnectionService extends ConnectionService { -+ private Connection createConnection(ConnectionRequest request) { -+ Bundle extras = request.getExtras(); -+ HashMap extrasMap = this.bundleToMap(extras); -+- extrasMap.put(EXTRA_CALL_NUMBER, request.getAddress().toString()); -++ extrasMap.put(EXTRA_CALL_IDENTIFIER, request.getAddress().toString()); -+ VoiceConnection connection = new VoiceConnection(this, extrasMap); -+- connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD); -++ connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE); -+ connection.setInitializing(); -+ connection.setExtras(extras); -+ currentConnections.put(extras.getString(EXTRA_CALL_UUID), connection); -+diff --git a/index.js b/index.js -+index 6ec6c20..27a6788 100644 -+--- a/index.js -++++ b/index.js -+@@ -73,7 +73,7 @@ class RNCallKeep { -+ -+ displayIncomingCall = (uuid, handle, localizedCallerName = '', handleType = 'number', hasVideo = false, options = null) => { -+ if (!isIOS) { -+- RNCallKeepModule.displayIncomingCall(uuid, handle, localizedCallerName); -++ RNCallKeepModule.displayIncomingCall(uuid, handle, handleType, hasVideo, localizedCallerName); -+ return; -+ } -+ -+@@ -94,7 +94,7 @@ class RNCallKeep { -+ -+ startCall = (uuid, handle, contactIdentifier, handleType = 'number', hasVideo = false ) => { -+ if (!isIOS) { -+- RNCallKeepModule.startCall(uuid, handle, contactIdentifier); -++ RNCallKeepModule.startCall(uuid, handle, contactIdentifier, handleType, hasVideo); -+ return; -+ } -+ -+diff --git a/ios/RNCallKeep/RNCallKeep.m b/ios/RNCallKeep/RNCallKeep.m -+index 04af403..f6caf24 100644 -+--- a/ios/RNCallKeep/RNCallKeep.m -++++ b/ios/RNCallKeep/RNCallKeep.m -+@@ -285,7 +285,7 @@ RCT_EXPORT_METHOD(updateDisplay:(NSString *)uuidString :(NSString *)displayName -+ NSLog(@"[RNCallKeep][updateDisplay] uuidString = %@ displayName = %@ uri = %@", uuidString, displayName, uri); -+ #endif -+ NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; -+- CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:uri]; -++ CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:uri]; -+ CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; -+ callUpdate.localizedCallerName = displayName; -+ callUpdate.remoteHandle = callHandle; -+@@ -511,7 +511,7 @@ RCT_EXPORT_METHOD(isCallActive:(NSString *)uuidString) -+ int _handleType = [RNCallKeep getHandleType:settings[@"handleType"]]; -+ providerConfiguration.supportedHandleTypes = [NSSet setWithObjects:[NSNumber numberWithInteger:_handleType], nil]; -+ }else{ -+- providerConfiguration.supportedHandleTypes = [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypePhoneNumber], nil]; -++ providerConfiguration.supportedHandleTypes = [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil]; -+ } -+ if (settings[@"supportsVideo"]) { -+ providerConfiguration.supportsVideo = [settings[@"supportsVideo"] boolValue]; + providerConfiguration.supportsVideo = [settings[@"supportsVideo"] boolValue]; \ No newline at end of file