/* Copyright 2009 Palm, Inc.  All rights reserved. */

var ActivecallAssistant = Class.create({
	audioRouteImages: {
		phone_bluetooth_sco: "url(images/menu-icon-speaker-bluetooth.png)",
		phone_front_speaker: "url(images/menu-icon-speaker-internal.png)",
		phone_back_speaker: "url(images/menu-icon-speaker-external.png)",
		phone_headset_mic: "url(images/menu-icon-headset.png)",
		phone_headset: "url(images/menu-icon-headset.png)",
		phone_tty_full:"url(images/menu-icon-headset-tty.png)",
		phone_tty_hco:"url(images/menu-icon-headset-tty.png)",
		phone_tty_vco:"url(images/menu-icon-headset-tty.png)"
	},
	
	audioRouteImagesPopup: {
		phone_bluetooth_sco: "url(images/menu-popup-bluetooth.png)",
		phone_back_speaker: "url(images/menu-popup-bluetooth-speaker.png)",
		phone_front_speaker: "url(images/menu-popup-bluetooth-internal.png)",
		phone_headset_mic: "url(images/menu-popup-bluetooth-headset.png)",
		phone_headset: "url(images/menu-popup-bluetooth-headset.png)",
		phone_tty_full:"url(images/menu-popup-bluetooth-headset-tty.png)",
		phone_tty_hco:"url(images/menu-popup-bluetooth-headset-tty.png)",
		phone_tty_vco:"url(images/menu-popup-bluetooth-headset-tty.png)"
	},
	
	initialize: function(params){
	    this.appAssistant = Mojo.Controller.getAppController().assistant;
		this.eventListener = this.appAssistant.telephonyEventListener;
		this.announcer = this.appAssistant.announcer;
		this.contacts = this.appAssistant.contacts; 
        QDLogger.log( "activecall initialize");
       	
		this.lastLines = [];
		this.pauseWaitDigits = [];
		this.showWaitButtonOnActive = [];
		this.waitMenuItems = [];
		
		// select multicall rules
		if (PlatformType.isGSM() == true) {
			this.callSetupButtonsMulticall = this.callSetupButtonsGsm;
			this.conferenceTemplate = "activecall/conference-gsm";
			this.conferenceCallSetup = this.conferenceCallSetupGsm;
		} else {
			this.callSetupButtonsMulticall = this.callSetupButtonsCdma;
			this.conferenceTemplate = "activecall/conference-cdma";
			this.conferenceCallSetup = this.conferenceCallSetupCdma;
		}
		
		this.muted = false;
		this.showConferenceDetails = false;
		this.ignoreFirstDtmf = true;
		
		this.puckConnected = this.eventListener.puckConnected;
		this.puckSubscription = TelephonyCommands.puckStatusSubscribe(this.onPuckEvent.bind(this));
		
		this.onBlur = this.onBlur.bind(this);
		this.onFocus = this.onFocus.bind(this);
		this.onUnload = this.onUnload.bindAsEventListener(this);
		this.onResize = this.onResize.bind(this);
		
		this.ignoreFirstDtmfTimeout = this.ignoreFirstDtmfTimeout.bind(this);
		
		// capture parameters
		this.lockScreenMode = (params && params.locked ? true : false);
		this.emergencyMode =  (params && params.emergency ? true : false);
		this.abridged = this.lockScreenMode || this.emergencyMode || this.appAssistant.firstuse;
		
    },
    
    setup: function(){
		this.stageController = this.controller.stageController;
		
		if (this.appAssistant.firstuse === true) {
			this.controller.document.body.className = "firstuse";
		}
		
    	// bind buttons
		this.controller.get('audioroute_button').observe(Mojo.Event.tap, this.onAudioRouteClick.bindAsEventListener(this));
        this.controller.get('mute_button').observe(Mojo.Event.tap, this.toggleMute.bindAsEventListener(this));
        this.controller.get('keypad_button').observe(Mojo.Event.tap, this.toggleDTMF.bindAsEventListener(this));
		
		if (this.lockScreenMode) {
			this.controller.get('addcall_button').observe(Mojo.Event.tap, this.switchToLockScreen.bindAsEventListener(this));
			this.controller.get('addcall_button_icon').removeClassName("addcall");
			this.controller.get('addcall_button_icon').addClassName("unlock");
			this.controller.get("addcall_button_label").textContent = Messages.unlock;
		} else if (this.appAssistant.otasp) {
			this.controller.get('addcall_button').addClassName("disabled");
		} else {
			this.controller.get('addcall_button').observe(Mojo.Event.tap, this.switchToDialpad.bindAsEventListener(this));
		}
		
		// Show where audio is routed to
		this.updateActiveAudioRouteButtonImage(Mojo.Controller.getAppController().assistant.audioActiveProfile);
		
		this.boundEndContinuousDTMF = this.endContinuousDTMF.bind(this);
		this.boundStartDTMFHardKey = this.startDTMFHardKey.bindAsEventListener(this);
		this.boundEndDTMFHardKey = this.endDTMFHardKey.bindAsEventListener(this);
		
		// skip the first, likely inadvertently pressed, dtmf key events
		this.ignoreFirstDtmfTimeout.delay(2);
		this.controller.document.addEventListener("mouseup", this.boundEndContinuousDTMF);
        this.controller.document.addEventListener('keydown', this.boundStartDTMFHardKey);
        this.controller.document.addEventListener('keyup', this.boundEndDTMFHardKey);
		
		this.controller.listen(this.controller.document, Mojo.Event.deactivate, this.onBlur);
		this.controller.listen(this.controller.document, Mojo.Event.activate, this.onFocus);
		
		Mojo.Event.listen(this.controller.window, 'resize', this.onResize, false);
		
		this.controller.window.addEventListener('unload', this.onUnload, false);
		
        this.conferenceListModel = {};
		this.controller.setupWidget('conference_member_list', {
            itemTemplate: 'activecall/conferencelistitem',
            listTemplate: 'activecall/conferencelistcontainer',
        }, this.conferenceListModel);
		
		this.setupDtmfPad();
					
        // only proceed if the initial call state isn't already up to disconnectclear
		var initialCallState = this.appAssistant.telephonyEventListener.getCallState()				
		if (initialCallState 
			&& initialCallState.lines
			&& initialCallState.lines.length == 0
			&& initialCallState.action == TelephonyCallState.DISCONNECTCLEAR) {
			this.crossfadeCancel();	
			this.onExit();
			return;
		}
		this.updateCalls(initialCallState);
		
		//start the timer
		this.timerInterval = this.controller.window.setInterval(this.updateDisplayTimer.bind(this), 1000);
    	
		this.appAssistant.telephonyEventListener.addCallStateListener(this);
		this.appAssistant.telephonyEventListener.addAudioStateListener(this);
		
		var appMenuModel = {
			visible: true,
            items: [{
                label: $L('Sounds & Ringtones'),
                command: 'ringtones'
            }]
		}; 
		this.controller.setupWidget(Mojo.Menu.appMenu, {omitDefaultItems: false}, appMenuModel);
		if (!this.abridged && this.appAssistant.focusOnActiveCallSceneCreation === true) {
			this.appAssistant.focusStage();
		}
		
    },

	handleCommand: function(event) {
		if (event.type == Mojo.Event.command) {
			switch (event.command) {
				case 'ringtones':
					MenuController.showRingtones(this.controller.stageController);
					break;
				case Mojo.Menu.prefsCmd:
					MenuController.showPrefs(this.controller.stageController);
					break;
					
				case Mojo.Menu.helpCmd:
					MenuController.showHelp();
					break;
			}
		}

		else if(event.type == Mojo.Event.back) {
			//Prevent Back Gesture if we are in emergency mode. 
			//Hopefully this works when we make emergency call before getting emergency event.
			if(this.emergencyMode) {
				Mojo.Log.error("Emergency Mode -- Active call Scene --- Prevent back gesture");
				event.preventDefault();
				event.stopPropagation();
			}
		}
		// Enable prefs & help menu items
		else 
			if (event.type == Mojo.Event.commandEnable &&
			(event.command == Mojo.Menu.prefsCmd || event.command == Mojo.Menu.helpCmd)) {
				event.stopPropagation();
			}
	},
		
	setupDtmfPad: function(index) {
		this.showDTMF = false;
		
     	this.controller.get('dtmf').hide();

     	var dtmfContent = Mojo.View.render({
     		template: 'activecall/dtmf'
     	});
     	this.controller.get('dtmf').update(dtmfContent);
     
     	for (var i = 0; i <= 9; i++) {
			var tone = String(i);
     		this.controller.get("dtmf_" + i).observe(Mojo.Event.tap, this.sendShortDTMF.bindAsEventListener(this, tone));
     		this.controller.get("dtmf_" + i).observe(Mojo.Event.hold, this.sendContinuousDTMF.bindAsEventListener(this, tone));
     	}
     
     	this.controller.get("dtmf_star").observe(Mojo.Event.tap, this.sendShortDTMF.bindAsEventListener(this, '*'));
     	this.controller.get("dtmf_star").observe(Mojo.Event.hold, this.sendContinuousDTMF.bindAsEventListener(this, '*'));
     
     	this.controller.get("dtmf_pound").observe(Mojo.Event.tap, this.sendShortDTMF.bindAsEventListener(this, '#'));
     	this.controller.get("dtmf_pound").observe(Mojo.Event.hold, this.sendContinuousDTMF.bindAsEventListener(this, '#'));
		
		this.controller.get("dtmf_disconnect").observe(Mojo.Event.tap, this.disconnectDtmfCallback.bindAsEventListener(this));
     },
    
    processPauseWaitDigits: function(evt, linenumber){
        QDLogger.log( "processpauseWaitDigits", this.pauseWaitDigits[linenumber])
        if ( !(this.pauseWaitDigits[linenumber]) || this.pauseWaitDigits[linenumber].length < 1)
            return;
			
		if (linenumber !== undefined) {
			QDLogger.log( "processpauseWaitDigits", "hiding ", linenumber);
			this.waitDigitsButtonHide(linenumber);
		}
		
		var inMenu = false;
		if (this.lastLines && this.lastLines.length > 1) {
			inMenu = true;		
		}
		
        var len = this.pauseWaitDigits[linenumber].length;
  	//check platform, CDMA has different char for pause and wait from GSM,
		if (PlatformType.isGSM() == true) {
			for (var i = 0; i < len; i++) {
				switch (this.pauseWaitDigits[linenumber].charAt(i)) {
					case 'p':{
						QDLogger.log( "processpauseWaitDigits", "pause ", this.pauseWaitDigits[linenumber].charAt(i));
						this.pauseWaitDigits[linenumber] = this.pauseWaitDigits[linenumber].substring(i + 1);
						this.pauseTimeout = setTimeout(this.processPauseWaitDigits.bind(this, evt, linenumber), 2500);
						return;
					}
break;
					
					case 'w':{
						if(i == len-1)
							return;
						QDLogger.log( "processpauseWaitDigits", "wait ", this.pauseWaitDigits[linenumber].charAt(i))
						this.pauseWaitDigits[linenumber] = this.pauseWaitDigits[linenumber].substring(i + 1);
						this.showWaitButtonOnActive[linenumber] = true;
						// show button again if it was hidden
						if (linenumber !== undefined) {
							
							QDLogger.log( "processpauseWaitDigits", "reshowing " , linenumber);
							this.waitDigitsButtonShow(linenumber, this.pauseWaitDigits[linenumber], inMenu);
						}
						return;
					}
break;
					
					default:
						{
							//DTMF tone to be played					
							QDLogger.log( "processpauseWaitDigits", "dtmf " , this.pauseWaitDigits[linenumber].charAt(i));
							TelephonyCommands.sendDTMF(this.pauseWaitDigits[linenumber].charAt(i), false);
							
							this.showWaitButtonOnActive[linenumber] = false;
							this.pauseWaitDigits[linenumber] = this.pauseWaitDigits[linenumber].substring(i + 1);
							this.pauseTimeout = setTimeout(this.processPauseWaitDigits.bind(this, evt, linenumber), 600);
							return;
						}
				}
			}
		}
		else {//CDMA
			for (var i = 0; i < len; i++) {
				switch (this.pauseWaitDigits[linenumber].charAt(i)) {
					case 't':{
						QDLogger.log( "processpauseWaitDigits", "pause " , this.pauseWaitDigits[linenumber].charAt(i));
						this.pauseWaitDigits[linenumber] = this.pauseWaitDigits[linenumber].substring(i + 1);
						this.pauseTimeout = setTimeout(this.processPauseWaitDigits.bind(this, evt, linenumber), 2500);
						return;
					}
break;
					
					case 'p':{
						if(i == len-1)
							return;
						QDLogger.log( "processpauseWaitDigits", "wait " , this.pauseWaitDigits[linenumber].charAt(i))
						this.pauseWaitDigits[linenumber] = this.pauseWaitDigits[linenumber].substring(i + 1);
						this.showWaitButtonOnActive[linenumber] = true;
						// show button again if it was hidden
						if (linenumber !== undefined) {
							QDLogger.log( "processpauseWaitDigits", "reshowing " , linenumber);
							this.waitDigitsButtonShow(linenumber, this.pauseWaitDigits[linenumber], inMenu);
						}
						return;
					}
break;
					
					default:
						{
							//DTMF tone to be played					
							QDLogger.log( "processpauseWaitDigits", "dtmf " , this.pauseWaitDigits[linenumber].charAt(i));
							TelephonyCommands.sendDTMF(this.pauseWaitDigits[linenumber].charAt(i), false);
							
							this.showWaitButtonOnActive[linenumber] = false;
							this.pauseWaitDigits[linenumber] = this.pauseWaitDigits[linenumber].substring(i + 1);
							this.pauseTimeout = setTimeout(this.processPauseWaitDigits.bind(this, evt, linenumber), 600);
							return;
						}
				}
			}
		
		}
        this.pauseWaitDigits[linenumber] = undefined;
    },
	
	waitDigitsButtonShow: function(lineNumber, digits, useMenu){
    
        var message = Messages.waitDigitsLabel;
        message = new Template(message).evaluate({
            "digits": digits
        });
        
        if (useMenu) {
			this.controller.get('wait_digits_button_' + lineNumber).hide();
            this.controller.get("add_contact_" + lineNumber).show();
            //this.controller.get("input-name_" + lineNumber).removeClassName("no-add-contact-icon");
            this.controller.get("add_contact_" + lineNumber).addClassName("drop-down");
            
            var call;
            
            if (this.lastLines && this.lastLines[lineNumber]) {
                call = this.lastLines[lineNumber];
            }
            
            var items = [{
                "command": "wait",
                "label": message
            }];
            
            // determine if hide or show contact should be used
            if (call && call.contact && call.contact.id) {
                items.push({
                    "command": "view",
                    "label": Messages.viewContact
                });
            } else 
                if (call && this.contacts.isNumberValid(call.number) && !(call.contact && call.contact.name)) {
                    items.push({
                        "command": "add",
                        "label": Messages.addContact
                    });
                }
            
            this.waitMenuItems[lineNumber] = items;
            
            
            
        } else {
            var waitDigitsButton = this.controller.get('wait_digits_button_' + lineNumber)
            waitDigitsButton.innerHTML = message;
            waitDigitsButton.show();
            waitDigitsButton.observe(Mojo.Event.tap, this.processPauseWaitDigits.bindAsEventListener(this, lineNumber));
        }
        
    },
    
    waitDigitsButtonHide: function(lineNumber){
        this.controller.get('wait_digits_button_' + lineNumber).hide();
        if (this.lastLines && this.lastLines.length > 1) {
        
            this.controller.get("add_contact_" + lineNumber).removeClassName("drop-down");
            
            this.waitMenuItems[lineNumber] = undefined;
            var call = this.lastLines[lineNumber];
            
            if (call && call.contact && call.contact.id || !(this.contacts.isNumberValid(call.number))) {
                this.controller.get("add_contact_" + lineNumber).hide();
                this.controller.get("input-name_" + lineNumber).addClassName("no-add-contact-icon");
            }
            
        }
    },
	
	onAddOrWaitButtonTap: function(lineNumber) {
		if (!(this.waitMenuItems[lineNumber])) {
			this.addToContact(this.lastLines[lineNumber].number);
		} else {
			this.controller.popupSubmenu({
				placeNear: this.controller.get("add_contact_" + lineNumber),
				onChoose: this.onWaitMenuTap.bind(this, lineNumber),
				items: this.waitMenuItems[lineNumber]
			});	
		}
	},
	
	onWaitMenuTap: function(lineNumber, value) {
		Mojo.Log.info("WAIT TAP! %o", value)
		switch(value) {
			case "view":
				this.openContact(undefined, lineNumber);
				break;
			case "add":
				if (this.lastLines && this.lastLines[lineNumber]) {
					this.addToContact(this.lastLines[lineNumber].number);
				}
				break;
			case "wait":
				this.processPauseWaitDigits(null, lineNumber);
				break;
		}
	},
    
	// receives TelephonyCallStates and acts on them, possibly redrawing the scene
	// this function gets registered with TelephonyEventListener
    updateCalls: function(callStateMessage){
		if (!callStateMessage) {
			QDLogger.error( "activeCallAssistant::updateCalls", "empty message");
			return;
		}
	
		var lineState = callStateMessage.lines;
		
		// if we didn't end up with a call array, make one to prevent problems later
		if (!lineState) {
			QDLogger.log( "activeCallAssistant#updateCalls", "message received without lines");
			lineState = [];
		}
		
		QDLogger.log( "activeCallAssistant::updateCalls", "call count: " , lineState.length);
        if (callStateMessage.action)
			QDLogger.log( "activeCallAssistant::updateCalls", "action: " , callStateMessage.action);
		
		try {
			// if there are no calls, or we received a crash message, exit
			if (lineState.length == 0 || callStateMessage.action === TelephonyCallState.CRASH) {
				this.onExit();
				return;
			}
			
			// special case ending the last call
			if (lineState.length == 1 
				&& callStateMessage.action === TelephonyCallState.DISCONNECTED 
				&& !(lineState[0].confId)){
				this.controller.get("state_name_0").innerHTML = Messages.callStateEnded;
				this.bannerClose();
				this.showMenuOrRedialOnUpdate(callStateMessage);
				return;
			}
			
			
			// loads pause wait digits on dialing; acts on them when a call goes active
			this.handlePauseWaitDigits(callStateMessage);
			
			// if necessary, freeze screen and get ready for transition
			this.crossfadeStart(lineState);
			
			// determines if call should be automatically unmuted
			this.unMuteOnUpdate(callStateMessage);
			
			// refresh mute state if the first call went active
			this.refreshMuteState(callStateMessage);
			
			// unblock input if the first call is an incoming call
			this.unblockDTMFKeysOnIncoming(callStateMessage);
			
			// update scene with new call information
			this.drawCalls(lineState);					
			
			// shows done and redial buttons if only 1 disconnected call is left; otherwise
			// shows menu
			this.showMenuOrRedialOnUpdate(callStateMessage);
			
			// determine if we should auto-speakerphone when phone is on the puck
			// at the start of a call
			this.enableSpeakerphoneOnConnectOnPuck(callStateMessage);
			
			// hide dtmfpad unless there's 1 call transitioning dialpending -> dialing or
			// dialing -> active
			this.changeDtmfVisibilityOnUpdate(callStateMessage)
			
			// save line state 
			this.lastLines = callStateMessage.lines;
			
			// force timer to refresh -- MUST follow setting this.lastLines, 
			// since it assumes that this.lastLines reflects the current state  
			this.updateDisplayTimer();
			
			// redraw active call banner, if it's open
			this.bannerUpdate(callStateMessage);
			
			// if we have a transition, run it
			this.crossfadeEnd();
			
			
			
		} catch (e) {
			Mojo.Log.error( "PhoneApp: activeCallAssistant#updateCalls CAUGHT EXCEPTION: %j" , e);
		}

    },
	
	// sets up pause/wait digit handling 
	handlePauseWaitDigits: function(callStateMessage) {
		var lineState = callStateMessage.lines;
		
		if (!lineState || lineState.length < 1)
			return;

		// clears out digits on a disconnecting call
		// on GSM, just clear out the disconnected call
		if (PlatformType.isGSM() == true) {
			for (var i = 0; i < lineState.length; i++) {
				if (lineState[i].state === TelephonyCallState.DISCONNECTPENDING 
					|| lineState[i].state === TelephonyCallState.DISCONNECTED) {
				
					this.pauseWaitDigits[i] = '';
					this.showWaitButtonOnActive[i] = false;
				}
			}
		} else {
			// on CDMA, clear all out	
			if (lineState[lineState.length-1].state === TelephonyCallState.DISCONNECTPENDING 
					|| lineState[lineState.length-1].state === TelephonyCallState.DISCONNECTED) {
				for (var i = 0; i < this.pauseWaitDigits.length; i++) {
					this.pauseWaitDigits[i] = ''
					this.showWaitButtonOnActive[i] = false;
				}
			}
		}	
		
		if (callStateMessage.action === TelephonyCallState.DIALING) {
			// each line has its own pausewait digits
			this.pauseWaitDigits[lineState.length-1] = this.eventListener.getPauseWaitDigits();
		} else if (callStateMessage.action === TelephonyCallState.ACTIVE) {
			// process pauseWait digits, if any exist
			this.processPauseWaitDigits(null, lineState.length-1);
		} 
	},
	
	// shows redial and done button at end of last call; otherwise reshows menu
	showMenuOrRedialOnUpdate: function(callStateMessage) {
		var lineState = callStateMessage.lines;
		if (!lineState)
			return;
			
		// if there's only a disconnected call left, show done button	
		if (!(this.abridged)
			&& lineState.length == 1 
			&& lineState[0].state === TelephonyCallState.DISCONNECTED
			&& callStateMessage.action === TelephonyCallState.DISCONNECTED)  {
			
			//  if it's a valid contact id or a blocked number, skip delay
			if (this.callCanAutoDismiss(this.lastLines[0])) {
				this.disconnectClear();
			// otherwise if number is valid, show redial button
			} /*else if (this.contacts.isNumberValid(this.lastLines[0].number)) {
				this.controller.get('menu').hide();
				this.controller.get('redial').observe(Mojo.Event.tap, this.redial.bindAsEventListener(this));
				this.controller.get('redial').show();
			}
			*/
		// otherwise show menu
		} else {
			this.controller.get('menu').show();
			this.controller.get('redial').hide();
		}
	},
	
	// hide dtmfpad on state change, except when dialing -> active
	// autoshow dtmf for voicemail or otasp
	changeDtmfVisibilityOnUpdate: function(callStateMessage) {
		var lineState = callStateMessage.lines
		if (!lineState) {
			lineState = [];
			return;
		} 
		// if voicemail number, and call went active, and we haven't already done this, show dtmf pad
		if(lineState.length == 1
			&& (this.appAssistant.otasp === true 
				|| lineState[0].number === this.eventListener.voicemailNumber 
				|| PlatformType.isOtaspNumber(lineState[0].number))
			&& callStateMessage.action === TelephonyCallState.ACTIVE
			&& !(this.showedDTMFOnVoicemail)) {
				this.showedDTMFOnVoicemail = true;
				if (this.showDTMF) {
					this.setDTMFPadVisibility(false);
				}
				this.setDTMFPadVisibility(true);
		// hide dtmfpad unless there's 1 call transitioning dialpending -> dialing or
		// dialing -> active
		} else if (lineState.length > 0 && !(lineState.length == 1 && this.lastLines.length == 1 
			&& (lineState[0].state == TelephonyCallState.ACTIVE || lineState[0].state == TelephonyCallState.DIALPENDING)
			&& (this.lastLines[0].state == TelephonyCallState.DIALING || this.lastLines[0].state == TelephonyCallState.DIALPENDING))) {
				this.setDTMFPadVisibility(false);
		} else if (this.showDTMF){
			// TODO: remove workaround
			// not clearing & reshowing DTMFpad makes the menubar look like caca
			this.setDTMFPadVisibility(false);
			this.setDTMFPadVisibility(true);
		}
	},
	
	// if the first call is an incoming call, don't block dtmf keys
	unblockDTMFKeysOnIncoming: function(callStateMessage) {
		var lineState = callStateMessage.lines
		if (!lineState) {
			return;
		}
		
		if (lineState.length === 1 
			&& lineState[0].origin !== TelephonyCallState.DIALING) {
				this.ignoreFirstDtmf = false;
			}
	},
	
	// unmute on active or held, if active or held and there's more than one call
	// now or was more han one call before (this keeps mute on from dialing -> active)
	unMuteOnUpdate: function(callStateMessage) {	
		
		if ( (callStateMessage.action === TelephonyCallState.ACTIVE 
				|| callStateMessage.action === TelephonyCallState.HOLD)
			&& this.muted == true 
			&& !((this.lastLines.length == 1) 
					&& (callStateMessage.lines 
						&& callStateMessage.lines.length == 1))) {
				this.toggleMute();
		}
	},
	
	// if the first call just went from dialing->active, resend the mute notification
	refreshMuteState: function(callStateMessage) {
		if ( this.muted == true 
				&& callStateMessage.lines.length == 1 
				&& callStateMessage.action === TelephonyCallState.ACTIVE
				&& this.lastLines
				&& this.lastLines.length == 1
				&& (this.lastLines[0].state == TelephonyCallState.DIALPENDING 
					|| this.lastLines[0].state == TelephonyCallState.DIALING)) {
				TelephonyCommands.mute(true);
		} else if (this.muted === false
				&& callStateMessage.lines.length == 1 
				&& callStateMessage.action === TelephonyCallState.ACTIVE) {
			TelephonyCommands.mute(false);
		}
	},
	
	// returns the line number of the call that's active
	// if there's only one call, assumes it's active
	// if there are more than one, and both are on hold,
	// returns the last line
	getActiveCallLineNumber: function(lines) {
		if (lines.length < 2) {
			return 0;
		} else {
			if (lines[0].state == TelephonyCallState.ACTIVE)
				return 0;
			else
				return 1;
		}
	},
	
	// returns true if call has a valid contact, or comes from an invalid number
	callCanAutoDismiss: function(call) {
		if (!call)
			return false;
			
		return (call.contact && call.contact.id && call.contact.id != -1)
				|| !(this.contacts.isNumberValid(call.number))
				|| call.number === this.eventListener.voicemailNumber
	},
	
	// if we have more than one call, or call count changes, freeze screen
	// and fade after drawing completes  
	crossfadeStart: function(lineState) {
		if (!this.abridged
			&& !this.transition
			&& this.appAssistant.topScene() == "activecall" 
			&& lineState.length > 0
			&& this.lastLines.length > 0) {
			if (!this.skippedFirstTransition) {
				this.skippedFirstTransition = true;
			} else if (this.activated && this.stageController.topScene().sceneName == "activecall"){
				this.transition = this.controller.prepareTransition(Mojo.Transition.crossFade, false);
			}
		}
	},
	
	crossfadeEnd: function() {
		if (this.transition) {
			this.transition.run(function() {
				this.transition = undefined;	
			}.bind(this));
		}
	},

	crossfadeCancel: function() {
		if (this.transition) {
			this.transition.cleanup();
			this.transition = undefined;
		}
	},
	
	// update scene with info from calls
	drawCalls: function(lineState) {
		// TODO used in mock
		if (MockTelephony.useMock && lineState.length == 1) {
			var contact = lineState[0].contact; 
			if (this.appAssistant.mockPicture) {
				contact.id = 424242;
				contact.name = "Mocking Silently";
				contact.pictureLoc = "images/sample-portrait.jpg";
				contact.pictureFormat = "portrait";
				contact.pictureObj = new Image();
				contact.pictureObj.src = contact.pictureLoc;
			} else {
				delete contact.pictureLoc;
				delete contact.pictureFormat;
				delete contact.pictureObj;
			}
		}
		
		var formattedCall = [];
		var callDiv = [this.controller.get('call_top'), this.controller.get('call_btm')];
		
	    // build display info for each call
		for (var i = 0; i < lineState.length; i++) {
			formattedCall[i] = new FormattedCall(i, lineState);
			if(!lineState[i].confId) {
				// set up contact info if not a conference
				formattedCall[i].contactText = this.formatContactText(formattedCall[i]);
				// but only setup contactPicture if we have one call
				if (lineState.length === 1) {
					formattedCall[i].contactPicture = this.formatContactPicture(formattedCall[i]);
				} else {
					formattedCall[i].contactPicture = ""					
				}
			}
		}
        
       	for (var i = 0; i < formattedCall.length; i++) {
			// if it has a conference id, use the conference template
			var callTemplate = (lineState[i].confId !== undefined ? 
					this.conferenceTemplate : "activecall/call") 

			QDLogger.log( "updateCalls", "drawing " , i , " with template:" , callTemplate);
			var callHtml = Mojo.View.render({
				object: formattedCall[i],
				template: callTemplate 
			});
			callDiv[i].innerHTML = callHtml;
			if (lineState[i].confId !== undefined) {
				this.conferenceCallSetup(i, lineState, formattedCall[i]);
			} else {
				// setup parts of display common to both networks: 
				// disconnect and add to contacts button, contact picture, state, 
				this.callSetVisibilityAndBindCommon(i, lineState);
				
				// show/hide and bind buttons appropriately for the network we're on
				this.callSetupButtonsMulticall(i, lineState);
			}
		}
		
		// move divs around with vigor.
		this.doTransitions(this.lastLines, lineState);
		
		// save formatting
		this.lastFormattedCalls = formattedCall;

	},
    
	// configures class names and styles for a non-conference call. 
	// handles buttons that aren't network specific.
	// sets up contact info, and pause/wait if available
	callSetVisibilityAndBindCommon: function(lineNumber, callMsg) {
		var call = callMsg[lineNumber];
		var callState = call.state;
		 
		var discButton = this.controller.get("disc_button_" + lineNumber); 
		// bind disconnect only if the call isn't already disconnected; otherwise, draw disabled disconnect button
		if (callState === TelephonyCallState.DISCONNECTPENDING 
			|| callState === TelephonyCallState.DISCONNECTED) {
					
			if (callMsg.length === 1 && callState === TelephonyCallState.DISCONNECTED
				&& !(this.callCanAutoDismiss(this.lastLines[0]))){
				discButton.className = "empty-button";
				discButton.textContent = Messages.returnToDialpadButton;
				discButton.observe(Mojo.Event.tap, this.disconnectClear.bindAsEventListener(this));
			} else {
				discButton.addClassName("disabled");
			}
		}  else {
			discButton.removeClassName("disabled");
			discButton.observe(Mojo.Event.tap, this.disconnectCallback.bindAsEventListener(this, lineNumber,call.id));
		}
		
		// bind contact tap areas
		this.controller.get("contactText_" + lineNumber).observe(Mojo.Event.tap, this.openContact.bindAsEventListener(this,lineNumber));
		this.controller.get("add_contact_" + lineNumber).observe(Mojo.Event.tap, this.onAddOrWaitButtonTap.bind(this,lineNumber));
		
		this.contactVisibilityUpdate(callMsg, lineNumber);
		
		// handle wait digits
		if (callState === TelephonyCallState.ACTIVE
			&& this.showWaitButtonOnActive[lineNumber]) {
			this.waitDigitsButtonShow(lineNumber, this.pauseWaitDigits[lineNumber], callMsg.length > 1);
		} else {
			this.waitDigitsButtonHide(lineNumber, callMsg.length > 1);
		}
		
		// hide the state description and show the timer if the call is active
		// if muted and active, show mute
		var stateDisplay = this.controller.get("state_name_" + lineNumber);
		var muteDisplay = this.controller.get("mute_state_" + lineNumber);
		var callTimerDisplay = this.controller.get('call_timer_' + lineNumber) 
		if (callState == TelephonyCallState.ACTIVE) {
			stateDisplay.hide();
			if (this.muted) {
				muteDisplay.show();
			} else {
				muteDisplay.hide();
			}
			this.updateDisplayTimer(callMsg);
			callTimerDisplay.show();
		} else {
			muteDisplay.hide();
			stateDisplay.show();
			callTimerDisplay.hide();
		}
		
		var callPanel = this.controller.get('call-panel_' + lineNumber); 
		// if call is held, use held glass. otherwise remove held glass.
		if (callMsg.length === 1) {
			if (callState === TelephonyCallState.HOLD) {
				callPanel.addClassName('hold');
			}
		} else {
			if (callState === TelephonyCallState.HOLD
				|| (lineNumber === 0 && callMsg[1].state !== TelephonyCallState.HOLD)) { // draw top call as held if the other call isn't
				callPanel.addClassName('hold');
			} else {
				callPanel.removeClassName('hold')
			} 
		}
		
	},
	
	// returns HTML fragment containing formatted contact details for injecting into #{contactText}/div id contactText 
	formatContactText: function(formattedCall) {
		return Mojo.View.render({
			object: formattedCall,
			template: "activecall/call-contact-text" 
		});
	},

	// returns HTML fragment containing formatted contact picture for injecting into #{contactPicture}/div id contactPicture 
	formatContactPicture: function(formattedCall) {
		return Mojo.View.render({
			object: formattedCall,
			template: "activecall/call-contact-picture" 
		});
	},
	
	// updates the contact info if we get it late
	lateContactInfoUpdate: function() {
		for (i = 0; i < this.lastLines.length; i++) {
			// only do lookup when it's not a conference and the number is valid
			var call = this.lastLines[i];
			if (!(call.confId !== undefined) 
			 	&& this.contacts.isNumberValid(call.number)
				&& call.contact.id) {
				this.contactInfoDraw(i, call.contact)
			}
		}
	},
	
	// updates displayed contact data by performing reverse lookup on all calls
	contactInfoUpdate: function(force) {
		if (!force && !(this.skippedFirstContactRefresh)) {
			this.skippedFirstContactRefresh = true;
			return;
		}
			
		if (!this.lastLines)
			return;
			
		for (i = 0; i < this.lastLines.length; i++) {
			// only do lookup when it's not a conference and the number is valid
			var call = this.lastLines[i];
			if (!(call.confId !== undefined) 
				&& this.contacts.isNumberValid(call.number)) {
				
				this.contacts.refreshReverseLookup(call.number, 
					call.contact, this.contactInfoDraw.bind(this, i));
			}
		}
	},
	
	// after contact lookup is performed, redraws all contact information on screen for a call
	contactInfoDraw: function(i, contact) {
		// if this call has gone away, bail
		if (!(this.lastFormattedCalls) || i > (this.lastFormattedCalls.length-1) 
			|| !(this.lastLines) || this.lastLines.length == 0)
			return;
		
		// get new set of formatted call data
		var formattedCall = new FormattedCall(i, this.lastLines);
		
		// update contactText and contactPicture with HTML containing new contact info
		if (this.controller) {
			this.controller.get("contactText_" + i).innerHTML = this.formatContactText(formattedCall);
			if (this.lastLines.length === 1 && !(this.lastLines[0].confId)) {
				this.controller.get("contactPicture_" + i).innerHTML = this.formatContactPicture(formattedCall);
				this.contactVisibilityUpdate(this.lastLines, i);
			}
		}
		
		// save new formatted contact info
		this.lastFormattedCalls[i] = formattedCall;
	},
	
	// shows/hides contact info and add button according to contents of contact 
	contactVisibilityUpdate: function(callMsg, lineNumber) {
		var callstate = callMsg[lineNumber];
		var contact = callstate.contact;
		
		// only show and bind the add to contacts button if we have no contact entry
		// and not in first use
		if (this.abridged === true
			|| (contact && contact.name) 
			|| !(this.contacts.isNumberValid(callstate.number))) {
			this.controller.get("add_contact_" + lineNumber).hide();
			this.controller.get("input-name_" + lineNumber).addClassName("no-add-contact-icon");			
		} else {
			this.controller.get("add_contact_" + lineNumber).show();
			this.controller.get("input-name_" + lineNumber).removeClassName("no-add-contact-icon");
		}
		
		var pictureContainer = this.controller.get("contactPicture_" + lineNumber);
		var bottomContainer = this.controller.get("pictureGradient_" + lineNumber);
		var callPanel = this.controller.get("call-panel_" + lineNumber);
		
		// only continue if we have one call; otherwise hide picture
		if (callMsg.length !== 1) {
			if (pictureContainer)
				pictureContainer.hide();
			return;
		}
		
		var pictureElement = this.controller.get("pictureElement_" + lineNumber);
		if (!pictureElement) {
			return;
		}
		
		// show contact picture if there's a valid contact name and only one call
		if (contact.id && contact.name) {
			// crop if necessary
			if (contact.pictureLoc) {
				var classname =""
				switch (contact.pictureFormat) {
					case "portrait":
						classname = "active-picture-portrait";
						break;
          default:
						classname = "active-picture-landscape";
						break;
				}
        
				pictureElement.className = classname;
				
        if (contact.pictureFormat == "landscape" && contact.pictureObj.height < 140 
					|| contact.pictureFormat !== "landscape" && contact.pictureObj.width < 170) {
					pictureContainer.addClassName("stamp-size");
					if (contact.pictureFormat == "square") {
            classname = "active-picture-square";
				    pictureElement.className = classname;            
					}
					callPanel.addClassName("stampsize-pic"); 
				} else {
          bottomContainer.addClassName("gradient");
          callPanel.addClassName("fullscreen-pic");
					var offset = 0;
					
          if (contact.pictureElement == "portrait") {
							offset = contact.pictureObj.height * .04;            
          } else {
              offset = (pictureElement.width - 310)/2;             
          }
				  
  				var bottomClip = AppAssistant.properties.FULLSCREEN_BOTTOM_CLIP;
  				if (offset > 0) {
  					offset = Math.round(offset);
            if (contact.pictureFormat == "portrait") {
    					pictureElement.style.marginTop = -offset + "px";
    					pictureElement.style.clip = "rect("+offset+"px 310px "+(bottomClip+offset)+"px 0px)";            
            } else {
              pictureElement.style.marginLeft = -offset + "px";
    					pictureElement.style.clip = "rect(0px " + (310+offset) + "px " + bottomClip + "px " + offset + "px)";

            }
  				}
  				
				}

			} else {
				pictureElement.className = "active-picture-square"
				pictureContainer.addClassName("stamp-size");
        callPanel.addClassName("stampsize-pic");				
			}
			pictureContainer.show();
		} else {
			pictureContainer.hide();
		}
		
	},
	
	// configures GSM-specific behavior and styles for buttons for a non-conference call
	// (i.e. the merge and swap buttons)
	callSetupButtonsGsm: function(lineNumber, callMsg) {		
    	this.controller.get("call-panel_" + lineNumber).addClassName("gsm");
		// hide the swap/merge button if there's only one call
		if (callMsg.length > 1) {
			var hideConference = false;
			this.controller.get("disc_button_" + lineNumber).removeClassName("disconnect-button");
			this.controller.get("disc_button_" + lineNumber).addClassName("multicall-disconnect-button");
			
			if ( callMsg[lineNumber].state !== TelephonyCallState.HOLD) {
				// this call allows merge
				this.controller.get("swapmerge_button_" + lineNumber).addClassName("multicall-merge-button");	
				// hide the merge button if there are already 6 calls connected
				// assumes conference call is line 0
				if ( (callMsg[lineNumber].state !== TelephonyCallState.ACTIVE) || (callMsg[0].confId !== undefined && callMsg[0].calls.length >= 5)) {
					hideConference = true;
				} else {
					this.controller.get("swapmerge_button_" + lineNumber).observe(Mojo.Event.tap, this.mergeCallback.bindAsEventListener(this));	
				}	
			} else {
				// this call can be resumed
				// entire call area is a big swap button
				this.controller.get("swap_call_" + lineNumber).observe(Mojo.Event.tap, this.swapCallback.bindAsEventListener(this));
				this.controller.get("swapmerge_button_" + lineNumber).observe(Mojo.Event.tap, this.swapCallback.bindAsEventListener(this));
				this.controller.get("swapmerge_button_" + lineNumber).addClassName("multicall-switch-button");
			}
			// hide the button if either of the calls are dialing or ending
			if (!(this.allCallsAreConnected(callMsg)) || hideConference == true) {
				this.controller.get("swapmerge_button_" + lineNumber).addClassName("disabled");
			} else {
				this.controller.get("swapmerge_button_" + lineNumber).removeClassName("disabled");
			}
		} else {
			// if call is on hold, allow user to resume
			if (callMsg[lineNumber].state == TelephonyCallState.HOLD) {
				this.controller.get("disc_button_" + lineNumber).removeClassName("disconnect-button");
				this.controller.get("disc_button_" + lineNumber).addClassName("multicall-disconnect-button");
				this.controller.get("swapmerge_button_" + lineNumber).observe(Mojo.Event.tap, this.swapCallback.bindAsEventListener(this));
				this.controller.get("swapmerge_button_" + lineNumber).addClassName("multicall-resume-button");
				this.controller.get("swapmerge_button_" + lineNumber).show();
			} else {
				this.controller.get("swapmerge_button_" + lineNumber).hide();
			}
		}
	},
	
	// checks if both calls are connected (active or on hold)
	allCallsAreConnected: function(callMsg) {
		return  ( (callMsg[0].state == TelephonyCallState.ACTIVE || callMsg[0].state == TelephonyCallState.HOLD)
					&& (callMsg[1].state == TelephonyCallState.ACTIVE || callMsg[1].state == TelephonyCallState.HOLD)
				);
	},
	
	// configures CDMA-specific behavior and styles for buttons for a non-conference call
	// (i.e. the merge and swap buttons)
	callSetupButtonsCdma: function(lineNumber, callMsg) {
		//QDLogger.log( "callSetupButtonsCdma", "members: " , formattedCall.members.length);		
		this.controller.get("call-panel_" + lineNumber).addClassName("cdma");
		
		// hide the swap/merge button if there's only one call
		if (callMsg[0].confId) {
			this.controller.get("swapmerge_button_" + lineNumber).hide();
		} else if (callMsg.length < 2) {
			if (callMsg[lineNumber].state == TelephonyCallState.HOLD) {
				this.controller.get("disc_button_" + lineNumber).removeClassName("disconnect-button");
				this.controller.get("disc_button_" + lineNumber).addClassName("multicall-disconnect-button");
				this.controller.get("swapmerge_button_" + lineNumber).observe(Mojo.Event.tap, this.swapCallback.bindAsEventListener(this));
				this.controller.get("swapmerge_button_" + lineNumber).addClassName("multicall-resume-button");
				this.controller.get("swapmerge_button_" + lineNumber).show();
			} else {
				this.controller.get("swapmerge_button_" + lineNumber).hide();
			} 
		} else {
			// CDMA only allows 2 calls and no MMI, but don't disable dialpad button
			// because we don't get disconnects and the user might want to dial again
			this.controller.get("disc_button_" + lineNumber).removeClassName("disconnect-button");
			this.controller.get("disc_button_" + lineNumber).addClassName("multicall-disconnect-button");
			this.controller.get("disc_button_" + lineNumber).addClassName("full-width");
			
			// CDMA allows merge but not swap if the second call was outgoing...
			if (callMsg[1].origin == TelephonyCallState.DIALING) {
				// merge button shows up only on the top
				if (lineNumber == 1) {
					this.controller.get("swapmerge_button_" + lineNumber).hide();
				} else {
					this.controller.get("disc_button_" + lineNumber).hide();
					this.controller.get("swapmerge_button_" + lineNumber).addClassName("multicall-merge-button");
					if (this.allCallsAreConnected(callMsg)) {
						this.controller.get("swapmerge_button_" + lineNumber).removeClassName("disabled");
						this.controller.get("swapmerge_button_" + lineNumber).observe(Mojo.Event.tap, this.mergeCallback.bindAsEventListener(this));
					} else {
						this.controller.get("swapmerge_button_" + lineNumber).addClassName("disabled");
					}			
					
				}
			} else {
				// ...and swap but not merge if the second call was incoming
				// show disabled swap button with first call if calls are in process of disconnecting
				if (callMsg[lineNumber].state === TelephonyCallState.HOLD 
					|| (lineNumber == 0
						&& !(callMsg[1].state === TelephonyCallState.HOLD)
						&& (callMsg[0].state === TelephonyCallState.DISCONNECTPENDING 
											|| callMsg[0].state === TelephonyCallState.DISCONNECTED 
											|| callMsg[1].state === TelephonyCallState.DISCONNECTPENDING))) {
					this.controller.get("swapmerge_button_" + lineNumber).show();
					this.controller.get("swapmerge_button_" + lineNumber).addClassName("multicall-switch-button");
					if (this.allCallsAreConnected(callMsg)) {
						this.controller.get("swap_call_" + lineNumber).observe(Mojo.Event.tap, this.swapCallback.bindAsEventListener(this));
						this.controller.get("swapmerge_button_" + lineNumber).observe(Mojo.Event.tap, this.swapCallback.bindAsEventListener(this));
					} else {
						this.controller.get("swapmerge_button_" + lineNumber).addClassName("disabled");
					}
					
				} else {
					this.controller.get("swapmerge_button_" + lineNumber).hide();
					
					this.controller.get("disc_button_" + lineNumber).removeClassName("disconnect-button");
					this.controller.get("disc_button_" + lineNumber).addClassName("multicall-disconnect-button");
					this.controller.get("disc_button_" + lineNumber).addClassName("full-width");
					
					//this.controller.get("disc_button_" + lineNumber).hide();
				}
			}
		}
	},
	
	// configures buttons and styles for a GSM conference call.
	// if only the conference call is connected, sets up the member list
	conferenceCallSetupGsm: function(lineNumber, callMsg, formattedCall) {
		this.callSetupButtonsGsm(lineNumber, callMsg);
		this.controller.get("disc_button_" + lineNumber).observe(Mojo.Event.tap, this.disconnectConferenceCallback.bindAsEventListener(this,callMsg[lineNumber].confId));
		
		this.controller.get("mute_state_" + lineNumber).hide();
		
        // Set up the attributes & model for this widget:
        this.conferenceListModel.items = formattedCall.members;
		//conference is always in the top div
		this.controller.instantiateChildWidgets(this.controller.get('call_top'));
		this.controller.modelChanged(this.conferenceListModel);
		
		QDLogger.log( "conferenceCallSetupGsm", "members: " , formattedCall.members.length);
		// we hide the member list if there's a conference call and another call
		if (callMsg.length == 2) {
			this.controller.get('conference_member_list').hide();
		} else {
			var disableExtract = callMsg[0].state !== TelephonyCallState.ACTIVE;
			for (var i = 0; i < formattedCall.members.length; i++) {
				this.controller.get("conf-hangup_" + i).observe(Mojo.Event.tap, this.memberHangupCallback.bindAsEventListener(this, i));
				if (disableExtract) {
					this.controller.get("conf-extract_" + i).addClassName("disabled");
				} else {
					this.controller.get("conf-extract_" + i).observe(Mojo.Event.tap, this.memberExtractCallback.bindAsEventListener(this, i));
				}
				this.controller.get("conf-buttons-toggle_" + i).observe(Mojo.Event.tap, this.memberButtonsVisibleToggleCallback.bindAsEventListener(this, i));
				this.controller.get("conf-buttons_" + i).hide();
			}
			var discButton = this.controller.get("disc_button_" + lineNumber); 
			if (callMsg[lineNumber].state === TelephonyCallState.DISCONNECTED || callMsg[lineNumber].state === TelephonyCallState.DISCONNECTPENDING) {
				discButton.addClassName("disabled");
			}
		}
	},
	
	// configures buttons and styles for a CDMA conference call.
    conferenceCallSetupCdma: function(lineNumber, callMsg) {
		QDLogger.log( "conferenceCallSetupCdma");
		this.controller.get("disc_button_" + lineNumber).observe(Mojo.Event.tap, this.disconnectConferenceCallback.bindAsEventListener(this));
		
		if (this.muted) {
			this.controller.get("mute_state_" + lineNumber).show();
		} else {
			this.controller.get("mute_state_" + lineNumber).hide();
		}
		var discButton = this.controller.get("disc_button_" + lineNumber); 
		if (callMsg[lineNumber].state === TelephonyCallState.DISCONNECTED || callMsg[lineNumber].state === TelephonyCallState.DISCONNECTPENDING) {
			discButton.addClassName("disabled");
		}
	},
	
	// originally intended to animate top/bottom call divs as the calls
	// connect and disconnect.  not implemented yet.  instead, just
	// handles hiding and showing divs
	doTransitions: function(oldLineState, newLineState) {
		// TODO: this needs to be redone to actually animate...
		// TODO: FIXME: make this deal with figuring out which call closed
		// this is TEMPORARY because oldLineState ends up being newLineState
		// because TelephonyCallState just reuses the object instead of making a new one
		// when dealing with purging a disconnected call
		// var oldCallCount = oldLineState.length;
		var oldCallCount = 0;
		if (oldLineState.length != 0) {
			if (this.controller.get('call_top').visible()) {
				oldCallCount++;
			}
			if (this.controller.get('call_btm').visible()) {
				oldCallCount++;
			}
		}
		
		var newCallCount = newLineState.length;
		QDLogger.log( "doTransitions", "old: " , oldCallCount , " new:" , newCallCount);
		
		// if this is the first time we're called then just display/hide the right divs:
		if (oldCallCount == 0) {
			this.showInitialCallDivs(newCallCount);
		} else if (oldCallCount == 1 && newCallCount == 0) {
			// NOOP
		} else if (oldCallCount == 1 && newCallCount == 2) {
			this.transition1To2();
		} else if (oldCallCount == 2 && newCallCount == 1) {
			this.transition2To1();	
			// this preserves any styles applied to the second call. 
			// hopefully this doesn't live long, because it's certainly not prospering
		} else if (oldCallCount == 2 && newCallCount == 2) {
			this.fixSecondCallDiv();
		} 
		
	},
	
	showInitialCallDivs: function(count) {
		QDLogger.log( "showInitialCallDivs", count);
		// size correctly
		if (count > 1) {
			this.controller.get('call-panel_0').addClassName("half-panel_0"); 
			this.controller.get('call-panel_1').addClassName("half-panel_1"); // puts the second call div in the middle of the screen
		} 
		
		var callDiv = [this.controller.get('call_top'), this.controller.get('call_btm')];
	
		// show the ones we want
		for (var i = 0; i < count; i++) {
			callDiv[i].show();
		}
		
		// hide the others
		for (var i = count; i < 2; i++) {
			callDiv[i].hide();
		}
	},
	
	transition1To2: function() {
		QDLogger.log( "transition1To2")
		this.controller.get('call-panel_0').addClassName("half-panel_0"); 
		this.controller.get('call-panel_1').addClassName("half-panel_1");
		this.controller.get('call_top').show();
		this.controller.get('call_btm').show();
	},
	
	transition2To1: function() {
		QDLogger.log( "transition2To1");
		this.controller.get('call-panel_0').removeClassName("half-panel_0"); 
		this.controller.get('call-panel_1').removeClassName("half-panel_1");
		this.controller.get('call_top').show();
		this.controller.get('call_btm').hide();
	},
	
	fixSecondCallDiv: function() {
		QDLogger.log( "fixSecondCallDiv");
		this.controller.get('call-panel_0').addClassName("half-panel_0");
		this.controller.get('call-panel_1').addClassName("half-panel_1");
		this.controller.get('call_top').show();
		this.controller.get('call_btm').show();
	},
	
	bannerOpen: function(lineState) {
		if (this.bannerId !== undefined) {
			return;
		}
		
		var bannerParams = this.bannerGenerate(lineState)
		if (bannerParams) {
			this.bannerId = PalmSystem.addPhoneActiveCallBanner("images/notification-small-active.png", bannerParams.text, bannerParams.startTime);
		}
	},
	
	bannerClose: function() {
		if (this.bannerId !== undefined) {
			PalmSystem.removePhoneActiveCallBanner(this.bannerId);
			this.bannerId = undefined;
		}
	},
	
	bannerUpdate: function(callStateMessage) {
	  	QDLogger.log( "ActivecallAssistant::bannerUpdate");
		if (this.bannerId === undefined || callStateMessage === undefined) {
			return;
		}
		
		var bannerParams = this.bannerGenerate(callStateMessage.lines);
		if (bannerParams !== undefined) {
			PalmSystem.updatePhoneActiveCallBanner(this.bannerId, "images/notification-small-active.png", bannerParams.text, bannerParams.startTime);
		}
		
	},
	
	bannerGenerate: function(lineState) {
		if (!lineState || PalmSystem.isMinimal || this.appAssistant.activeCallFocused === true) {
			return;
		}
		
		//save this for updating the call timer
		var displayString = "";
		// check for all calls disconnected.  kill this window if they've all gone down the hole
		if (lineState.length == 0 || 
			(lineState.length == 1 && lineState[0].state == TelephonyCallState.DISCONNECTED)) {
			return;
		} else if (lineState.length == 2) {
			displayString = Messages.callBannerTwoCalls;
		} else if (lineState.length == 1) {
			displayString = this.lastFormattedCalls[0].contact;
		}
		
		return {
			text: displayString,
			startTime: lineState[0].startTime
		}
	},
	
	deactivate: function(event) {
		QDLogger.log( "ActiveCallAssistant#deactivate");
	 	
		this.onBlur();
		this.activated = false;	
    },
    
    onBlur: function(event){
		QDLogger.log( "ActiveCallAssistant#onBlur");
        this.appAssistant.activeCallFocused = false;
		
		// don't raise the call banner when we've dropped all calls, or about to
        if (!this.abridged && this.lastLines && this.lastLines.length > 0  
			&& !(this.lastLines.length == 1 
					&& (this.lastLines[0].state == TelephonyCallState.DISCONNECTPENDING 
						|| this.lastLines[0].state == TelephonyCallState.DISCONNECTED))) {
			this.bannerOpen(this.lastLines);
		} else {
			this.bannerClose();
		}
		
    },
   
    aboutToActivate: function(callback) {
		this.onFocus();
		
		callback();
    },
    
	activate: function() {
		QDLogger.log( "ActiveCallAssistant#activate");
		// clear dial debounce
		DialStringParser.clearDebounceTimeout();
		this.activated = true;
	},
    
	incomingDialogLaunch: function() {
		this.announcer.remoteDialogScene = this.controller;
		this.announcer.dupeIncomingPopupToDialog();
	},
	
    onFocus: function(event){
        QDLogger.log( "ActiveCallAssistant#onFocus");
		var topScene = this.appAssistant.topScene();
        // might not be this scene that is focused
		if (topScene !== "activecall")
            return;
		this.contactInfoUpdate();
        this.bannerClose();
    	this.appAssistant.activeCallFocused = true;
    },
	
	// scrolls to bottom when window size is decreased due to a popup 
	onResize: function() {
		var devInfo = Mojo.Environment.DeviceInfo;
		if (devInfo && devInfo.screenHeight) {
			var windowHeight = this.controller.window.innerHeight;
			if (devInfo.screenHeight - windowHeight >= 90) {
				this.controller.revealBottom(); // try the lobster.  tip your server.
				this.scrolledAfterResize = true;
			} else if (this.scrolledAfterResize) {
				this.scrolledAfterResize = false;
				this.controller.revealBottom();
			}
		}
	},
	
	onUnload: function(event) {
		QDLogger.log( "ActiveCallAssistant::onUnload");
		this.eventListener.disconnectAllCalls();
		// TODO FIXME: this.controller.window.removeEventListener fails onUnload when cleanup runs
		this.unloaded = true;
	},
	
	// called when calls disconnected. close window if last call was incoming 
	// or we were launched by firstuse.
	// return to dialpad otherwise
	onExit: function() {
		QDLogger.log( "ActiveCallAssistant#onExit");
		
		// clear out a transition if we had it
		this.crossfadeCancel();
		
		// close banner if open
		this.bannerClose();
		
		var params;
		
		if (this.lockScreenMode) {
			params = { lock: true };
		} else if (this.emergencyMode) {
			params = { emergency: true };	
		}
		
		if (params) {
			this.appAssistant.popActiveCall(params);
			return;
		}
		
		// always pop active call
		// FIXME: actually don't if dialpad is on top
		var closeStage = false;
		if ( this.lastLines && 
			this.lastLines.length > 0 &&
			this.lastLines[0] &&
			(this.lastLines[0].origin === TelephonyCallState.DIALING) &&
			this.appAssistant.firstuse !== true) {
			QDLogger.log( "activeCallAssistant::updateCalls", "last call outgoing, so back to the pad: ");
		} else {
			QDLogger.log( "activeCallAssistant::updateCalls", "last call incoming, so hiding stage");
			closeStage = true;
		}
		
		this.lastLines = [];
		if (closeStage) {
			this.appAssistant.closePhoneStage();
		}
		
		this.appAssistant.popActiveCall();
		// in case scene is reused, clear out contact refresh  
		this.skippedFirstContactRefresh = false;
		return;
	}, 
	
	cleanup: function(event) {
		QDLogger.log( "ActiveCallAssistant::cleanup");
		if (this.controller.window) {
			this.controller.window.clearInterval(this.timerInterval);
			this.controller.window.clearInterval(this.handleOTASPNotificationTimer);
		}
		
		this.appAssistant.telephonyEventListener.removeCallStateListener(this);
		this.appAssistant.telephonyEventListener.removeAudioStateListener(this);
		
		this.appAssistant.activeCallFocused = false;

		if (!this.unloaded) {
			this.controller.stopListening(this.controller.document, Mojo.Event.deactivate, this.onBlur);
			this.controller.stopListening(this.controller.document, Mojo.Event.activate, this.onFocus);
			this.controller.window.removeEventListener('unload', this.onUnload, false);
			Mojo.Event.stopListening(this.controller.window, 'resize', this.onResize);
		} else {
			this.unloaded = false;
		}

		this.controller.document.removeEventListener("mouseup", this.boundEndContinuousDTMF);
        this.controller.document.removeEventListener('keydown', this.boundStartDTMFHardKey);
        this.controller.document.removeEventListener('keyup', this.boundEndDTMFHardKey);
		
		// drop puck subscription
		this.puckSubscription.cancel();
		delete this.puckSubscription;
		
		Mojo.View.removeDOMReferences(this);
		
		this.bannerClose();
	},
    
    /*** BUTTON CALLBACKS ***/
    
	switchToLockScreen: function(){
		this.appAssistant.showLockScreen();
    },
    
	
    switchToDialpad: function(){
		this.appAssistant.showDialpad();
    },
    
	memberExtractCallback: function(evt, linenumber) {
		QDLogger.log( "memberExtractCallback", linenumber);
		TelephonyCommands.extract(this.lastLines[0].calls[linenumber].id);
		this.visibleMember = undefined;
	},
	
	memberHangupCallback: function(evt, linenumber) {
		QDLogger.log( "memberHangupCallback", linenumber);
		TelephonyCommands.disconnect(this.lastLines[0].calls[linenumber].id);
		this.visibleMember = undefined;
	},
	
	scrollToMemberButtons: function(conf_button) {
		var row = conf_button.parentNode;
		var scroller = Mojo.View.getScrollerForElement(row);
		
		var rowBottomOffset = row.offsetTop + row.getHeight();
		var scrollerBottomOffset = scroller.scrollTop + scroller.clientHeight;
		
		// If part of the row is not visible
		if(rowBottomOffset > scrollerBottomOffset) {
			// then smooth scroll to the full height of the
			// row plus 20 pixels for the scroll fade
			scroller.mojo.scrollTo(
				0,
				-(row.offsetTop + row.getHeight() + 20 - scroller.clientHeight),
				true);
		}
	},
	
	// handles showing and hiding conference buttons when a conference member is touched
	memberButtonsVisibleToggleCallback: function(evt, linenumber) {
		QDLogger.log( "memberButtonsVisibleToggleCallback", linenumber);
		
		var conf_button = this.controller.get("conf-buttons_" + linenumber);
		
		// if none are visible, show this one
		if (this.visibleMember === undefined) {
			conf_button.show();
			this.visibleMember = linenumber;
			this.scrollToMemberButtons(conf_button);
		// if this one is already visible, hide it
		} else if (this.visibleMember === linenumber) {
			conf_button.hide();
			this.visibleMember = undefined;
			
			var row = conf_button.parentNode;
			// If the last row is being closed
			if(row.hasClassName("last")) {
				// scroll to the bottom of the scroller to hide the
				// big hunk of green background
				var scroller = Mojo.View.getScrollerForElement(row);
				scroller.mojo.scrollTo(
					0,
					-(row.offsetTop + row.getHeight() - scroller.clientHeight),
					true)
			}
		// another one is visible, so hide it and show this one
		} else {
			this.controller.get("conf-buttons_" + this.visibleMember).hide()
			conf_button.show();
			this.visibleMember = linenumber;
			this.scrollToMemberButtons(conf_button);
		}
	},
	
    disconnectCallback: function(evt, lineNumber, id){
		QDLogger.log( "disconnectCallback", lineNumber  , id );
		
		if (evt)
			Event.stop(evt);
		if (PlatformType.isGSM() == true)
			this.pauseWaitDigits[lineNumber]='';
		else{
			for (var i = 0; i<this.pauseWaitDigits.length;i++){
				this.pauseWaitDigits[i]=''
			}
		}
		
		this.eventListener.disconnectCall(lineNumber);
    },
	
	disconnectConferenceCallback: function(evt){
        TelephonyCommands.disconnectConference();
    },
	
	disconnectClear: function(event) {
		if (this.lastLines
			&& this.lastLines.length == 1
			&& this.lastLines[0].state == TelephonyCallState.DISCONNECTED) {
			this.eventListener.clearDisconnectedCall();
		}
	},
	
	// called by disconnect button in dtmf pad
	disconnectDtmfCallback: function() {
		this.toggleDTMF();
		var activeCall = this.getActiveCallLineNumber(this.lastLines);
		
		this.eventListener.disconnectCall(activeCall);
		
	},
    
	mergeCallback: function(evt){
        TelephonyCommands.merge();
    },
	
	swapCallback: function(evt){
        if (evt)
			Event.stop(evt);

		TelephonyCommands.swap();
    },
	
	redial: function(event) {
		QDLogger.log( "redial");
		if (!this.lastLines || this.lastLines.length !== 1) 
			return;
		
		if (this.contacts.isNumberValid(this.lastLines[0].number)) {
			this.appAssistant.launchDial(this.lastLines[0].number);
		}
	},
	
	// opens dialog box to prompt to add contact
    addToContact: function(number){
		QDLogger.log( "addToContact", number);
        if (this.abridged)
			return;

        TelephonyCommands.contactPromptToAdd(this.controller,  number);
    },
    
	// launches the contact app with contact card or pseudocard depending
	// on if the passed line has a contact book entry or not
	openContact: function(event, lineNumber){
		if (event) event.stop();
		if (this.abridged || !this.lastLines || lineNumber > this.lastLines.length - 1)
			return;
			
		var lineState = this.lastLines[lineNumber];
		
		if (lineState.contact.id) {
			TelephonyCommands.contactsLaunchWithId(lineState.contact.id);
		} else if (this.contacts.isNumberValid(lineState.number)) {
			TelephonyCommands.contactsLaunchWithPseudocard(lineState.number);
		}
	},
	
	// if we were answered or dialed while on the puck, and this is the first call,
	// and we only have two audio routes, auto-enable speakerphone
	enableSpeakerphoneOnConnectOnPuck: function(callStateMessage) {
		QDLogger.log( "ActiveCallAssistant#enableSpeakerphoneOnConnectOnPuck");
		
		if (callStateMessage) {
			var action = callStateMessage.action
			if (action === TelephonyCallState.DIALING
				|| action === TelephonyCallState.DIALPENDING 
				|| action === TelephonyCallState.ACTIVE) {
			
				var scenarios = this.appAssistant.telephonyEventListener.getAvailableAudioScenarios();
				
                if (this.appAssistant.puckMode === true
                	&& this.lastLines.length === 0 // if we haven't saved any line state, this is the first call
	 				//	|| (this.lastLines.length === 1 && this.lastLines[0].state === TelephonyCallState.DIALING // or if we have one line, and 
					&& (this.appAssistant.otasp || this.eventListener.puckConnected === true) 
					&& Object.keys(scenarios).length == 2) {
					this.onAudioRouteChangeClick("phone_back_speaker");
				}
			}
		}
	},
	
	// if puck status has changed, and we only have 2 audio routes,
	// enable speakerphone on puck and disable it off puck
	onPuckEvent: function(response) {
		if (response) {
            QDLogger.log( "ActiveCallAssistant#onPuckEvent", response.type  , response.connected);
		}
		
		var scenarios = this.appAssistant.telephonyEventListener.getAvailableAudioScenarios();
		
        if (this.appAssistant.puckMode === true &&
        response &&
        response.type == "inductive") {
        
            if (Object.keys(scenarios).length == 2) {
			
                if (response.connected === false &&
                this.puckConnected === true) {
				this.onAudioRouteChangeClick("phone_front_speaker");
                } else if (response.connected === true &&
                this.puckConnected == false) {
				this.onAudioRouteChangeClick("phone_back_speaker");
			}		
		}
            this.puckConnected = response.connected;
        }
    },
    
    enableSpeakerphoneOnPuck: function(){
        if (this.puckConnected === true && this.audioActiveProfile !== "phone_back_speaker") {
            this.onAudioRouteChangeClick("phone_back_speaker");
        }
	},
	
	
    toggleMute: function(){
		// if we're on an emergency call, don't allow mute
        if (this.appAssistant.emergencyMode == true 
			|| (this.lastLines && this.lastLines.length > 0 && PlatformType.isEmergencyNumber(this.lastLines[0].number)))
			return;
			
		QDLogger.log( "toggleMute");
        var muteStatus = !(this.muted);
      	TelephonyCommands.mute(muteStatus);
		this.updateMuteDisplay(muteStatus);
		
		// if we're locked, make sure the other window's mute status is updated
		if (this.lockScreenMode) {
			var stageController = Mojo.Controller.getAppController().getStageController("PhoneApp");
			if (stageController) {
				stageController.delegateToSceneAssistant("updateMuteDisplay", muteStatus);
			}
		}
		
    },
	
	updateMuteDisplay: function(muteStatus) {
		this.muted = muteStatus;
		
		var activeLine = this.getActiveCallLineNumber(this.lastLines);
		var muteElement = this.controller.get("mute_state_" + activeLine); 
		if (muteElement !== undefined && this.muted) {
			muteElement.show();
			this.controller.get('mute_button').toggleClassName("active").addClassName("palm-depressed");
		} else {
			muteElement.hide();
			this.controller.get('mute_button').toggleClassName("active").removeClassName("palm-depressed");
		}		
	},
    
	toggleDTMF: function(){
      this.setDTMFPadVisibility(!(this.showDTMF));
    },
    
	setDTMFPadVisibility: function(state) {
		if (state == this.showDTMF)
			return;
			
		 if (state == true) {
            QDLogger.log( "toggleDTMF", "showing pad");
    					
			this.controller.get('call_top').hide();
			if (this.lastLines.length == 2) {
				this.controller.get('call_btm').hide();
			}
            if (this.controller.get('add_contact') != undefined) {
                this.controller.get('add_contact').hide();
            }
            
			this.controller.get('dtmf').show();
			
			var line = this.lastFormattedCalls[this.getActiveCallLineNumber(this.lastLines)];
			if (line) {
				this.controller.get('dtmf_name').innerHTML = line.contact;
			}
			this.showDTMF = true;
            this.showConferenceDetails = false;
			this.controller.get('keypad_button').addClassName("active").addClassName("palm-depressed");
			
        } else {
            QDLogger.log( "toggleDTMF", "hiding pad");
            this.controller.get('dtmf').hide();
			this.controller.get('call_top').show();
			if (this.lastLines.length == 2) {
				this.controller.get('call_btm').show();
			}
			
            if (this.controller.get('add_contact') != undefined) {
                this.controller.get('add_contact').show();
            }
			
			
			this.userInput = "";
			this.controller.get('dtmf_numbers').innerHTML= ""
			
            this.showDTMF = false;
            this.showConferenceDetails = false;
			this.controller.get('keypad_button').removeClassName("active").removeClassName("palm-depressed");
			
        }
	},
	
    /*** AUDIO ***/
    onAudioRouteClick: function(evt){
        var availableAudioRoutes = new Array();
        // Get the list of available audio scenarios
        var scenarios = this.appAssistant.telephonyEventListener.getAvailableAudioScenarios();
        if (!scenarios) 
            return;
       
	    // Create the list of available routes
        var that = this;
		Object.keys(scenarios).each(function(route){
			
            var routeItem = new Object();
            switch (route) {
                case "phone_front_speaker":
                    routeItem.label = Messages.audioRouteNormal;
                    break;
                case "phone_back_speaker":
                    routeItem.label = Messages.audioRouteSpeaker;
                    break;
                case "phone_bluetooth_sco":
                    routeItem.label = Messages.audioRouteBluetooth;
                    //routeItem.label = scenarios[route];
                    break;
				case "phone_headset":
				case "phone_headset_mic":
					routeItem.label = Messages.audioRouteWiredHeadset;
					break;
				case "phone_tty_full":
				case "phone_tty_hco":
				case "phone_tty_vco":
					routeItem.label = Messages.audioRouteTTY;
					break;
            }
            routeItem.command = route;
            QDLogger.log( "onAudioRouteClick", "route: " , route);
			routeItem.secondaryIcon = "list-selector-icon list-"+route;
			
			if (route == Mojo.Controller.getAppController().assistant.audioActiveProfile)
				routeItem.chosen = true;
				
			availableAudioRoutes.push(routeItem);
        });
        Mojo.Log.info( "PhoneApp: onAudioRouteClick availableAudioRoutes = %j" , availableAudioRoutes);
        var hasHeadset = false;
		var frontSpeakerIndex = -1;
		if (availableAudioRoutes.length > 2) {
			for (var i = 0; i < availableAudioRoutes.length; i++) {
				if (availableAudioRoutes[i].command == 'phone_headset_mic' || availableAudioRoutes[i].command == 'phone_headset')
					hasHeadset = true;
				else if (availableAudioRoutes[i].command == 'phone_front_speaker')
					frontSpeakerIndex = i;
			}
			if (hasHeadset == true)
				availableAudioRoutes.splice(frontSpeakerIndex,1);				
		}
		
        // If the list is only 2 items, then toggle between them
        if (availableAudioRoutes.length <= 2) {
            for (var i = 0; i < availableAudioRoutes.length; i++) {
                if (scenarios[availableAudioRoutes[i].command] != "active") {
                    this.onAudioRouteChangeClick(availableAudioRoutes[i].command);
                    break;
                }
            }
            return;
        }
        
        // otherwise popup the list of available audio routes
        var near = evt.originalEvent && evt.originalEvent.target;
        
        this.controller.popupSubmenu({
            placeNear: near,
            onChoose: this.onAudioRouteChangeClick.bind(this),
            items: availableAudioRoutes,
        });
        
    },
    
    onAudioRouteChangeClick: function(value){
		if (value !== undefined) {
			QDLogger.log( "onAudioRouteChangeClick", "New audio route is " , value);
			TelephonyCommands.setAudioScenario(value);
		}
    },
    
	updateActiveAudioRouteButtonImage: function(profile){
        QDLogger.log( "ActiveCallAssistant#updateActiveAudioRouteButtonImage", "audioActiveProfile=" , profile);
        if (!profile) {
	        var scenarios = this.appAssistant.telephonyEventListener.getAvailableAudioScenarios();
	
			if (Object.keys(scenarios).length == 2) {//default should be front speaker
				this.controller.get("audioroute_label").textContent = Messages.audioRouteSpeaker;
				var img = this.audioRouteImages["phone_front_speaker"]
				this.controller.get("audioroute-image").setStyle({
        		   backgroundImage: img
        		});
			}
			else if (Object.keys(scenarios).length == 3 && Object.keys(scenarios).indexOf("phone_headset_mic")>=0 ) {
				this.controller.get("audioroute_label").textContent = Messages.audioRouteSpeaker;
				var img = this.audioRouteImages["phone_headset_mic"]
				this.controller.get("audioroute-image").setStyle({
        		   backgroundImage: img
        		});
			}
			return;
		}
        this.audioActiveProfile = profile;
        
		var img = '';
		var depressed = false; // spot the pun!
		var label = Messages.audioRouteMenu;
		var scenarios = this.appAssistant.telephonyEventListener.getAvailableAudioScenarios();
		
		var scenariosLength = Object.keys(scenarios).length;
        
		// if we've dropped to two scenarios, turn speakerphone on
        if (scenariosLength === 2 && this.lastScenariosLength > 2) {
            this.enableSpeakerphoneOnPuck();
        }
        
        this.lastScenariosLength = scenariosLength;
        
		Object.keys(scenarios).each(function(route){
            if(route == "phone_headset_mic" || route == "phone_headset")
					scenariosLength-=1;
        });

        if ( scenariosLength> 2) {
			img = this.audioRouteImagesPopup[profile];
			if (!img) {
				QDLogger.error( "ActiveCallAssistant#updateActiveAudioRouteButtonImage", "unknown profile");
				img = this.audioRouteImagesPopup[profile];
			}
			depressed = false;
			
		} else {
			label = Messages.audioRouteSpeaker;
			
			img = this.audioRouteImages[profile];
			if (!img) {
				QDLogger.error( "ActiveCallAssistant#updateActiveAudioRouteButtonImage", "unknown profile");
				img = this.audioRouteImages["phone_front_speaker"];
			}
			if (profile == "phone_back_speaker") {
				depressed = true;
			} 
		}
		
		
		this.controller.get("audioroute-image").setStyle({
           backgroundImage: img
        });
		
		
		if (depressed) {
			this.controller.get("audioroute_button").addClassName("palm-depressed");
		} else {
			this.controller.get("audioroute_button").removeClassName("palm-depressed");
		}
		
		this.controller.get("audioroute_label").textContent = label;
    },
    
    /*** TIMER ****/
    formatTimer: function(timer){
        var seconds = timer.getUTCSeconds();
        var minutes = timer.getUTCMinutes();
        var hours = timer.getUTCHours();
        if (seconds < 10) 
            seconds = "0" + seconds;
        if (hours > 0) {
            if (minutes < 10) 
                minutes = "0" + minutes;
            return hours + ":" + minutes + ":" + seconds;
        }
        return minutes + ":" + seconds;
    },
    
    updateDisplayTimer: function(lineState){
		try {
			var lines;
					
			if (lineState !== undefined) {
				lines = lineState;
			} else if (this.lastLines !== undefined && this.lastLines.length > 0) {
				lines = this.lastLines;
			} else {
				return;
			}
			
			var callTimer = "";
	        var currentTime = new Date().getTime();

			for (var i = 0; i < lines.length; i++) {
				if (lines[i].state == TelephonyCallState.ACTIVE) {
					var startTime = lines[i].startTime;
					var elapsed = currentTime - startTime
					// NITZ updates at call start can cause the clock to go backwards
					// this prevents displaying 23:59:59 as the time
					if (elapsed < 0) {
						elapsed = 0;
					}
					
					var timer = new Date(elapsed);
					callTimer = this.formatTimer(timer);
				}
				
				if (this.controller.get('call_timer_' + i))
					this.controller.get('call_timer_' + i).update(callTimer);
			}
		} catch (e) {
			Mojo.Log.error( "PhoneApp: activeCallAssistant#updateDisplayTimer CAUGHT EXCEPTION: %j" , e);
		}        
    },
    
    /*** DTMF ***/
	translateKey:function(c){
		switch (c){
			case 'e':
			case 'E':
				return '1';
			case 'r':
			case 'R':
				return '2';
			case 't':
			case 'T':
				return '3';
			case 'd':
			case 'D':
				return '4';
			case 'f':
			case 'F':
				return '5';
			case 'g':
			case 'G':
				return '6'
			case 'x':
			case 'X':
				return '7';
			case 'c':
			case 'C':
				return '8';
			case 'v':
			case 'V':
				return '9';
			case '@':
				return '0';
			case 'z':
			case 'Z':
				return '*';
			case 'b':
			case 'B':
				return '#';
			default:
				return c;		
		}	
	},
	
    startDTMFHardKey: function(evt){
        // block superfluous initial keypresses
		
		if (this.ignoreFirstDtmf) {
			return;
		}
		
		var c = String.fromCharCode(evt.keyCode);
		if (evt.keyCode && evt.keyCode !== 0 && evt.metaKey == false)
        	QDLogger.log( "staaaartDTMFHardKey", "key: " , c);
		else
			return;
			
		// just for mocking
		if (MockTelephony.useMock == 1 && (c == 'I' || c == 'i') ) {
			this.appAssistant.mockIncomingCall();
		} 
		
		// spacebar kills active call if there's only 1 disconnected call left
		if (c == ' ') {
			this.disconnectClear();
			return;
		}
		
		c = this.translateKey(c);
		
		var stageController = this.stageController;
        var topScene = stageController.topScene();
		// bring dtmf pad up if active call is on top
        if (topScene && topScene.sceneName === "activecall" && ((c >= '0' && c <= '9') || c == '*' || c == '#') ) {
			this.setDTMFPadVisibility(true)
        } else 
			return;					
        if(this.pressedKey == undefined && ((c >= '0' && c <= '9') || c == '*' || c == '#'))
			this.pressedKey = c;
 
        //make sure that we end a current continuous dtmf in progress on a new key press
		if( this.pressedKey != c)
        	this.endContinuousDTMF();
        
        if (this.dtmfHardKey == undefined) {
            if ((c >= '0' && c <= '9') || c == '*' || c == '#') {
                this.dtmfHardKeyTimeout = setTimeout(this.dtmfHardKeyCallback.bind(this), 500);
                this.dtmfHardKey = c;
            }
        }
    },
    
    dtmfHardKeyCallback: function(){
        this.continuousDTMFInProgress = true;
        QDLogger.log( "dtmfHardKeyCallback", "Start continuous DTMF " , this.dtmfHardKey);
		if (this.userInput == undefined)
			this.userInput = this.dtmfHardKey;
		else
			this.userInput = this.userInput + this.dtmfHardKey;
		if(this.userInput.length>18){
			this.userFeedback = "..."+this.userInput.slice(this.userInput.length-16, this.userInput.length)	
			this.controller.get('dtmf_numbers').innerHTML= this.userFeedback;
        } else 
			this.controller.get('dtmf_numbers').innerHTML= this.userInput;
        TelephonyCommands.startDTMF(this.dtmfHardKey, false);
    },
    
    endDTMFHardKey: function(evt){
        
		clearTimeout(this.dtmfHardKeyTimeout);
        
        if (this.dtmfHardKey == undefined) {
            return;
        }
        
		QDLogger.log( "endDTMFHardKey", "key up evt: " , evt.keyCode);
        
		
        if (this.continuousDTMFInProgress == true) {
            QDLogger.log( "endDTMFHardKey", "End hard key continuous DTMF " , this.dtmfHardKey);
            this.continuousDTMFInProgress = false;
            TelephonyCommands.endDTMF();
        } else {
            QDLogger.log( "endDTMFHardKey", "Sending Hard Key short DTMF " , this.dtmfHardKey);
            TelephonyCommands.sendDTMF(this.dtmfHardKey, false);
			if (this.userInput == undefined)
				this.userInput = this.dtmfHardKey;
			else
				this.userInput = this.userInput + this.dtmfHardKey;
			if(this.userInput.length>18){
				this.userFeedback = "..."+this.userInput.slice(this.userInput.length-16, this.userInput.length)	
				this.controller.get('dtmf_numbers').innerHTML= this.userFeedback;
            } else 
				this.controller.get('dtmf_numbers').innerHTML= this.userInput;
        }
        this.continuousDTMFInProgress = false;
        this.dtmfHardKey = undefined;
		this.pressedKey = undefined;
    },
	
	// clear timeout used skip the first, likely inadvertently pressed, dtmf
	ignoreFirstDtmfTimeout: function() {
		this.ignoreFirstDtmf = false;
	},
    
	// TODO: 'assumes the id has the key at position 5' might be a bit workaroundish.
    sendShortDTMF: function(evt, tone){
		if (this.continuousDTMFInProgress == undefined || this.continuousDTMFInProgress == false) {
			// assumes the id has the key at position 5
			QDLogger.log( "sendShortDTMF", "short dtmf event: " , tone);
						
			if (this.userInput == undefined) 
				this.userInput = tone;
			else 
				this.userInput = this.userInput + tone;
			
			if(this.userInput.length>18){
				this.userFeedback = "..."+this.userInput.slice(this.userInput.length-16, this.userInput.length)	
				this.controller.get('dtmf_numbers').innerHTML= this.userFeedback;
            } else 
				this.controller.get('dtmf_numbers').innerHTML= this.userInput;
			
			TelephonyCommands.sendDTMF(tone, false);
		}
		
    },
    
    sendContinuousDTMF: function(evt, tone){
        QDLogger.log( "sendContinuousDTMF", "press and hold event");
		
		if (this.continuousDTMFInProgress == undefined || this.continuousDTMFInProgress == false) {
			// assumes the id has the key at position 5
			this.continuousDTMFInProgress = true;
			QDLogger.log( "sendContinuousDTMF", "continuous dtmf start: " , evt.target.inspect());
			
			if (this.userInput == undefined) 
				this.userInput = tone;
			else 
				this.userInput = this.userInput + tone;
			
			if(this.userInput.length>18){
				this.userFeedback = "..."+this.userInput.slice(this.userInput.length-16, this.userInput.length)	
				this.controller.get('dtmf_numbers').innerHTML= this.userFeedback;
            } else 
				this.controller.get('dtmf_numbers').innerHTML= this.userInput;
			
			TelephonyCommands.startDTMF(tone, false);
		}
		
    },
    
    endContinuousDTMF: function(evt){
        if (this.continuousDTMFInProgress == true) {
            this.continuousDTMFInProgress = false;
            QDLogger.log( "endContinuousDTMF");
            
            TelephonyCommands.endDTMF();
        }
    },
	
	handleOTASPNotification: function(msg) {
		QDLogger.log( "Phone App - ActiveCall", "handleOTASPNotification" , msg);
		this.controller.window.clearInterval(this.handleOTASPNotificationTimer);
		
		var dispMsg = Messages.otaspNotifications[msg] || msg.unescapeHTML();
		
		var info = Mojo.View.render({object: {otaspUpdate:dispMsg},template: 'activecall/firstusebanner'});
        this.controller.get('otasptarget').update(info);
		
		var element = this.controller.get('otasptarget').select('.phone-banner-text').first();
		var offset = element.scrollWidth - element.getWidth();
		
		// scroll if necessary
		if ( offset > 0 ) {
			this.handleOTASPNotificationTimer = this.controller.window.setInterval(function() {
				const RIGHT_BUFFER = 30; // pixel boundary bordering right side of text
				var offsetinterval = 0;
				
				return function() {
					offsetinterval += 10;
					element.style.marginLeft = (-(offsetinterval % (offset + RIGHT_BUFFER))) + 'px';	
				}.bind(this);
			}(), 500);
		}
	},
    
});
