/* globals CallsHandler, CallScreen, ConferenceGroupHandler, Contacts, ContactPhotoHelper, FontSizeManager, FontSizeUtils, Utils, Voicemail, TonePlayer */ 'use strict'; function HandledCall(aCall) { this.photo = null; this._leftGroup = false; this.call = aCall; aCall.addEventListener('statechange', this); aCall.ongroupchange = (function onGroupChange() { if (this.call.group) { ConferenceGroupHandler.addToGroupDetails(this.node); this._leftGroup = false; } else if (this._wasUnmerged()) { if (ConferenceGroupHandler.isGroupDetailsShown()) { // Since the call has been unmerged and its node will be moved from the // participant list overlay to the main call screen, the call node is // cloned and added to the call node parent so it is kept in the // participant list overlay until it is hidden. this.node.parentNode.insertBefore(this.node.cloneNode(true), this.node); } // Move the call node from the conference call participant list overlay // to the main call screen page. CallScreen.insertCall(this.node); this._leftGroup = false; } else { this._leftGroup = !this.node.dataset.groupHangup; } }).bind(this); this._initialState = this.call.state; this._cachedInfo = null; this._cachedAdditionalTel = null; this._cachedAdditionalTelType = null; this._removed = false; this._wasConnected = false; this.node = document.getElementById('handled-call-template').cloneNode(true); this.node.id = ''; this.node.classList.add('handled-call'); this.node.hidden = false; // TODO: The structure of the duration related elements will be refactored in // https://bugzilla.mozilla.org/show_bug.cgi?id=1007148 this.durationNode = this.node.querySelector('.duration'); this.durationChildNode = this.node.querySelector('.duration span'); this.totalDurationNode = this.node.querySelector('.total-duration'); this.viaSimNode = this.node.querySelector('.sim .via-sim'); this.simNumberNode = this.node.querySelector('.sim .sim-number'); this.numberNode = this.node.querySelector('.numberWrapper .number bdi'); this.outerNode = this.node.querySelector('.numberWrapper .number'); this.groupCallNumberNode = document.getElementById('group-call-label'); this.additionalTelNode = this.node.querySelector('.additionalContactInfo .tel'); this.additionalTelTypeNode = this.node.querySelector('.additionalContactInfo .tel-type'); this.hangupButton = this.node.querySelector('.hangup-button'); this.hangupButton.onclick = (function() { this.call.hangUp(); }.bind(this)); this.updateCallNumber(); this.createCustomStyles(); /* Observe changes to the node that holds the call duration, this is also * used for displaying the call ended string and needs to be resized * dynamically to accomodate for certain locales. */ this.mutationObserverConfig = { childList: true }; this.mutationObserver = new MutationObserver(this.observeMutation.bind(this)); this.mutationObserver.observe(this.durationChildNode, this.mutationObserverConfig); navigator.mozL10n.setAttributes(this.durationChildNode, (this.call.state === 'incoming') ? 'incoming' : 'connecting'); this.updateDirection(); if (navigator.mozIccManager.iccIds.length > 1) { this.node.classList.add('sim-info'); var n = this.call.serviceId + 1; navigator.mozL10n.setAttributes(this.viaSimNode, 'via-sim', { n: n }); navigator.mozL10n.setAttributes(this.simNumberNode, 'sim-number', { n: n }); } else { this.viaSimNode.hidden = true; this.simNumberNode.hidden = true; } // Some calls might be already connected if (this._initialState === 'connected') { this.connected(); } } /** * Creates the global custom style used for resizing the call ended string. * Re-uses the existing one it if the style is already present. */ HandledCall.prototype.createCustomStyles = function hc_createCustomStyles() { const CUSTOM_STYLE_ID = 'call-ended-custom-style'; var style = document.getElementById(CUSTOM_STYLE_ID); if (!style) { style = document.createElement('style'); style.id = CUSTOM_STYLE_ID; document.head.appendChild(style); } this.callEndedStyleSheet = style.sheet; }; /** * Recomputes the font size rules so that the call ended string fits both in * the callscreen display and in the statusbar. */ HandledCall.prototype.computeCallEndedFontSizeRules = function hc_computeCallEndedFontSizeRules() { // Remove the existing rules while (this.callEndedStyleSheet.cssRules.length > 0) { this.callEndedStyleSheet.deleteRule(0); } var computedStyle = window.getComputedStyle(this.durationChildNode); var fontFamily = computedStyle.fontFamily; var allowedSizes = [ 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27 ]; /* Compute the size of the font for displaying the string in the callscreen, * this rules only apply when there's a single call, i.e. when the * big-duration class is present in the calls node. */ var info = FontSizeUtils.getMaxFontSizeInfo( this.durationChildNode.textContent, allowedSizes, fontFamily, 180); var rule = '@media (min-height: 4.5em) {' + ' #calls.big-duration > section.ended .duration > span,' + ' #calls.big-duration > section.ended .total-duration {' + ' font-size: ' + (info.fontSize / 10.0) + 'rem;' + ' }' + '}'; this.callEndedStyleSheet.insertRule(rule, this.callEndedStyleSheet.cssRules.length); }; /** * Observe DOM mutations. This will automatically resize the call ended string * once it's displayed at the end of a call. */ HandledCall.prototype.observeMutation = function hc_observeMutation(mutations) { if (this.durationChildNode.hasAttribute('data-l10n-id') && this.durationChildNode.getAttribute('data-l10n-id') === 'callEnded') { /* Disable the observer to prevent it from being called recursively * while we modify the DOM tree, re-enable it once we're done . */ this.mutationObserver.disconnect(); this.computeCallEndedFontSizeRules(); this.mutationObserver.observe(this.durationChildNode, this.mutationObserverConfig); } }; HandledCall.prototype._wasUnmerged = function hc_wasUnmerged() { return !this.node.dataset.groupHangup && this.call.state != 'disconnecting' && this.call.state != 'disconnected'; }; HandledCall.prototype.handleEvent = function hc_handle(evt) { CallsHandler.updatePlaceNewCall(); CallsHandler.updateMergeAndOnHoldStatus(); CallsHandler.updateMuteAndSpeakerStatus(); switch (evt.call.state) { case 'connected': CallScreen.render('connected'); this.connected(); break; case 'disconnected': this.disconnected(); break; case 'held': this.node.classList.add('held'); CallScreen.render('connected-hold'); break; } }; HandledCall.prototype.updateCallNumber = function hc_updateCallNumber() { var number = this.call.id.number; var self = this; CallScreen.setCallerContactImage(); /* If we have a second call waiting in CDMA mode then we don't know which * number is currently active */ if (this.call.secondId) { this.replacePhoneNumber('switch-calls'); this.replaceAdditionalContactInfo(null, null); return; } if (!number) { this.replacePhoneNumber('withheld-number'); return; } var isEmergencyNumber = this.call.emergency; if (isEmergencyNumber) { this.node.classList.add('emergency'); this.replacePhoneNumber({ raw: number }, 'end'); this.replaceAdditionalContactInfo('emergencyNumber'); return; } Voicemail.check(number, this.call.serviceId).then( function(isVoicemailNumber) { if (isVoicemailNumber) { self.replacePhoneNumber('voiceMail'); } else { Contacts.findByNumber(number, lookupContact); checkICCMessage(); } }); function checkICCMessage() { var callMessageReq = navigator.mozSettings.createLock(). get('icc.callmessage'); callMessageReq.onsuccess = function onCallMessageSuccess() { self._iccCallMessage = callMessageReq.result['icc.callmessage']; if (self._iccCallMessage) { self.replacePhoneNumber({ raw: self._iccCallMessage }, 'end'); navigator.mozSettings.createLock().set({'icc.callmessage': null}); } }; } function lookupContact(contact, matchingTel, contactsWithSameNumber) { if (self._iccCallMessage) { return; } if (contact) { var primaryInfo = Utils.getPhoneNumberPrimaryInfo(matchingTel, contact); if (primaryInfo) { self.replacePhoneNumber({ raw: primaryInfo }, 'end'); } else { self.replacePhoneNumber('withheld-number'); } self.replaceAdditionalContactInfo({ raw: matchingTel.value }, Utils.getPhoneNumberAdditionalInfo(matchingTel)); var photo = ContactPhotoHelper.getFullResolution(contact); if (photo) { self.photo = photo; CallScreen.setCallerContactImage(); } return; } self.replacePhoneNumber({ raw: number }, 'end'); self.replaceAdditionalContactInfo(self._cachedAdditionalTel, self._cachedAdditionalTelType); } }; HandledCall.prototype.replaceAdditionalContactInfo = function hc_replaceAdditionalContactInfo(additionalTel, additionalTelType) { if (!additionalTel && !additionalTelType) { this.additionalTelNode.removeAttribute('data-l10n-id'); this.additionalTelNode.textContent = ''; this.additionalTelTypeNode.removeAttribute('data-l10n-id'); this.additionalTelTypeNode.textContent = ''; this.node.classList.remove('additionalInfo'); } else { if (typeof(additionalTel) === 'string') { navigator.mozL10n.setAttributes(this.additionalTelNode, additionalTel); } else if (additionalTel.id) { navigator.mozL10n.setAttributes(this.additionalTelNode, additionalTel.id, additionalTel.args); } else if (additionalTel.raw) { this.additionalTelNode.removeAttribute('data-l10n-id'); this.additionalTelNode.textContent = additionalTel.raw; } if (additionalTelType) { navigator.mozL10n.setAttributes(this.additionalTelTypeNode, additionalTelType.id, additionalTelType.args); } this.node.classList.add('additionalInfo'); } this._cachedAdditionalTel = additionalTel; this._cachedAdditionalTelType = additionalTelType || null; }; HandledCall.prototype.restoreAdditionalContactInfo = function hc_restoreAdditionalContactInfo() { this.replaceAdditionalContactInfo(this._cachedAdditionalTel, this._cachedAdditionalTelType); }; HandledCall.prototype.formatPhoneNumber = function hc_formatPhoneNumber(ellipsisSide) { if (this._removed) { return; } // Don't format if the call is in a conference. if (this.call.group) { this.numberNode.style = ''; return; } var scenario = CallScreen.getScenario(); // To cover the second incoming call sub-scenario of the call waiting one, // we have to check if the current call is in incoming state and if the // incoming lower pane is being shown. if (scenario === FontSizeManager.CALL_WAITING && this.call.state === 'incoming' && CallScreen.incomingContainer.classList.contains('displayed')) { scenario = FontSizeManager.SECOND_INCOMING_CALL; } FontSizeManager.adaptToSpace( scenario, this.outerNode, false, ellipsisSide); if (this.node.classList.contains('additionalInfo')) { FontSizeManager.ensureFixedBaseline(scenario, this.numberNode); } else { FontSizeManager.resetFixedBaseline(this.numberNode); } }; /** * This method renders a phone number. It can be used to display numbers typed * during a call without altering the original number stored in the object. */ HandledCall.prototype.renderPhoneNumber = function hc_renderPhoneNumber(number, ellipsisSide) { if (typeof(number) === 'string') { navigator.mozL10n.setAttributes(this.numberNode, number); } else if (number.id) { navigator.mozL10n.setAttributes(this.numberNode, number.id, number.args); this.numberNode.style.fontSize = ''; } else if (number.raw) { this.numberNode.removeAttribute('data-l10n-id'); this.numberNode.textContent = number.raw; this.formatPhoneNumber(ellipsisSide); } }; /** * This method replaces the phone number in the call with a new number and * renders it. */ HandledCall.prototype.replacePhoneNumber = function hc_replacePhoneNumber(number, ellipsisSide) { this.renderPhoneNumber(number, ellipsisSide); this._cachedInfo = number; }; /** * This method renders the number that was originally associated with this * object in case it was altered (e.g. by typing numbers during a call). */ HandledCall.prototype.restorePhoneNumber = function hc_restorePhoneNumber() { this._cachedInfo && this.replacePhoneNumber(this._cachedInfo, 'end'); }; HandledCall.prototype.updateDirection = function hc_updateDirection() { var classList = this.node.classList; if (this._initialState === 'incoming') { classList.add('incoming'); this.node.setAttribute('data-l10n-id', 'incoming_screen_reader'); } else { classList.add('outgoing'); this.node.setAttribute('data-l10n-id', 'outgoing_screen_reader'); } if (this.call.state === 'connected') { classList.add('ongoing'); } }; HandledCall.prototype.remove = function hc_remove() { this._removed = true; this.call.removeEventListener('statechange', this); this.photo = null; var self = this; CallScreen.stopTicker(this.durationNode); var currentDuration = ConferenceGroupHandler.isGroupDetailsShown() ? ConferenceGroupHandler.currentDuration : this.durationChildNode.textContent; this.node.classList.add('ended'); this.durationNode.classList.remove('isTimer'); /* This string will be resized automatically to fit its container, see * hc_observeMutations() & hc_computeCallEndedFontSizeRules. */ navigator.mozL10n.setAttributes(this.durationChildNode, 'callEnded'); // FIXME/bug 1007148: Refactor duration element structure. No number or ':' // existence checking will be necessary. var totalDuration = !!currentDuration.match(/\d+/g) ? currentDuration : ''; this.totalDurationNode.textContent = totalDuration; setTimeout(function(evt) { CallScreen.removeCall(self.node); self.node = null; }, CallScreen.callEndPromptTime); }; HandledCall.prototype.connected = function hc_connected() { this.show(); this.node.classList.remove('held'); this.updateDirection(); CallScreen.createTicker(this.durationNode); CallScreen.syncSpeakerEnabled(); this.updateCallNumber(); this._wasConnected = true; }; HandledCall.prototype.disconnected = function hc_disconnected() { if (this._leftGroup) { if (typeof(this._cachedInfo) === 'string') { CallScreen.showStatusMessage(this._cachedInfo); } else if (this._cachedInfo.id) { const disconnectedMessageL10n = { 'withheld-number': 'withheld-number-left-call', 'voiceMail': 'voicemail-left-call' }; var id = disconnectedMessageL10n[this._cachedInfo.id]; CallScreen.showStatusMessage(id); } else if (this._cachedInfo.raw) { CallScreen.showStatusMessage({ id: 'caller-left-call', args: { caller: this._cachedInfo.raw } }); } this._leftGroup = false; } // Play End call tone only if the call was connected. if (this._wasConnected) { TonePlayer.playSequence([[480, 620, 250]]); } this._wasConnected = false; this.remove(); }; HandledCall.prototype.show = function hc_show() { if (this.node) { this.node.hidden = false; } CallScreen.updateCallsDisplay(); }; HandledCall.prototype.hide = function hc_hide() { if (this.node) { this.node.hidden = true; } CallScreen.updateCallsDisplay(); };