1 /**
  2  * @fileOverview cwic.js
  3  * CWIC is a jQuery plugin to access the Cisco Web Communicator<br>
  4  * Audio and Video media require the CWC browser plugin to be installed <br>
  5  */
  6 
  7 /*
  8     CWIC is a jQuery plugin to access the Cisco Web Communicator
  9 
 10     CWIC uses jQuery features such as:<ul>
 11     <li>'cwic' namespacing: jQuery.cwic();</li>
 12     <li>custom events (.cwic namespace): conversationStart.cwic</li>
 13     <li>attach data with the 'cwic' key: ui.data('cwic', conversation)</li>
 14     </ul>
 15 
 16     Audio and Video media require the CWC browser plugin to be installed
 17 
 18 */
 19 /**
 20  * Global window namespace
 21  * @name window
 22  * @namespace
 23  */
 24 /*global jQuery window ActiveXObject _triggerError _reset _registerSystemCallbacks _registerCallChangeCallbacks _triggerConversationEvent unregisterPhone shutdown _log*/
 25 (function( $ )
 26 /** @scope $.fn.cwic */
 27 {
 28 
 29     // a global reference to the CWC native plugin API
 30     var _plugin = null;
 31     /** cwic global settings, they can be overridden by passing options to init
 32      * @namespace
 33      */
 34     var settings = {
 35         /** A function to implement base64 encoding required for HTTP Basic authentication against the {@link $.fn.cwic-settings.node} service.<br>
 36          * This function is required if attempting authentication against the node service using some versions of Internet Explorer.<br>
 37          * If not provided cwic will attempt to use window.btoa which is not available on all browsers.
 38          * @type Function=null
 39          * @function
 40          * @param {String} Input buffer to encode.
 41          * @return {String} Base64 encoded string.
 42          */
 43         encodeBase64: null,
 44         /** The handler to be called when the API is ready.<br>
 45          * The values in the defaults parameter can be used when invoking registerPhone
 46          * The API is ready when:<ul>
 47          *      <li>The document (DOM) is ready.</li>
 48          *      <li>The CWC plugin was found and could be loaded.</li></ul>
 49          * @type Function=null
 50          * @param {Object} defaults An object containing default values retrieved from URL query parameters user and/or cucm <i>e.g: http://myserver/phone?user=foo&cucm=1.2.3.4 </i><br>
 51          * @param {Boolean} registered Phone registration status - true if the phone is already registered (can be used when using SDK in multiple browser tabs), false otherwise
 52          * @param {String} mode The phone's current call control mode - "SoftPhone" or "DeskPhone"
 53          */
 54         ready: null,
 55         /** use ccmcip when authenticating
 56          * @type Boolean=true
 57          */
 58         useCcmcip: true,
 59         /** device prefix to use for default device prediction algorithm
 60          * @type String='ecp'
 61          */
 62         devicePrefix: 'ecp',
 63         /** callback function to predict softphone device name<br>
 64          * default implementation is to concatenate settings.devicePrefix + options.username
 65          * @name $.fn.cwic-settings.predictDevice
 66          * @type Function=null
 67          * @function
 68          * @param {Object} options
 69          * @param {String} options.username
 70          */
 71         /** A flag to indicate to cwic that it should log more messages.
 72          * @type Boolean=false
 73          */
 74         verbose: false,
 75         /** Handler to be called when cwic needs to log information.<br>
 76          * Default is to use console.log if available, otherwise do nothing.
 77          * @function
 78          * @param {String} msg the message
 79          * @param {Object} [context] the context of the message
 80          * @type Function
 81          */
 82         log: function(/** String */msg, /** Object */context) {
 83             if (typeof console !== "undefined" && console.log) {
 84                 console.log(msg);
 85                 if (context) {
 86                     console.log(context);
 87                 }
 88             }
 89         },
 90         /** The handler to be called if the API could not be initialized.<br>
 91          * The basic properties of the error object are listed, however more may be added based on the context of the error.<br>
 92          * If the triggered error originated from a caught exception, the original error properties are included in the error parameter.<br>
 93          * An error with code 1 (PluginNotAvailable) can have an extra 'pluginDisabled' property set to true.<br>
 94          * @type Function
 95          * @param {Object} error see {@link $.fn.cwic-errorMap}
 96          * @param {String} [error.message] message associated with the error.
 97          * @param {Number} error.code code associated with the error
 98          */
 99         error: function(/** Object */error) {
100             _log("Error: ",error);
101         },
102         /** The node address: host name or IP address.<br>
103          * cwic can use some network services hosted on a web server (the node). By default, this server is the host that serves the web application based on cwic.<br>
104          * The node deployment is optional.  It provides advanced features such as directory integration. See additional documentation in the 'node' folder. <br>
105          * @type String=''
106          */
107         node: '', // to access node.cwc services, can be cross-domain (JSONP)
108         /**
109          * Allows the application to extend the default error map.<br>
110          * This parameter is a map of error id to {@link $.fn.cwic-errorMapEntry}
111          * It may also be a map of error id to String
112          * By default error messages (String) are associated to error codes (map keys, Numbers).<br>
113          * The application can define new error codes, or associate a different message/object to a pre-defined error code. <br>
114          *   Default error map: {@link $.fn.cwic-errorMap}<br>
115          * @name $.fn.cwic-settings.errorMap
116          * @type $.fn.cwic-errorMapEntry{}
117          */
118         /**
119          * The default "move audio port on video call" policy
120          * @type Boolean=false
121          */
122         mediaPortSplitting: false
123     };
124 
125     // jsdoc does not seem to like enumerating properties (fields) of objects it already considers as properties (fields).
126     /** cwic error object
127      * @name $.fn.cwic-errorMapEntry
128      * @namespace
129      * @property {Number} code a unique error code
130      * @property {String} message the message associated with the error
131      * @property {Any} [propertyName] Additional properties that will be passed back when an error is raised.
132      */
133     /**
134      * The error map used to build errors triggered by cwic. <br>
135      * Keys are error codes (numbers), values objects associated to codes. <br>
136      * By default the error object contains a single 'message' property. <br>
137      * The error map can be customized via the init function. <br>
138      * @namespace
139      */
140     var errorMap = {
141         /** 0: unknown error or exception
142          * @type $.fn.cwic-errorMapEntry
143          */
144         Unknown  :                { code:   0, message: "Unknown error" },
145         /** 1: plugin not available (not installed, not enabled or unable to load)
146          * @type $.fn.cwic-errorMapEntry
147          */
148         PluginNotAvailable :      { code:   1, message: "Plugin not available" },
149         /** 2: browser not supported
150          * @type $.fn.cwic-errorMapEntry
151          */
152         BrowserNotSupported :     { code:   2, message: "Browser not supported" },
153         /** 3: invalid arguments
154          * @type $.fn.cwic-errorMapEntry
155          */
156         InvalidArguments :        { code:   3, message: "Invalid arguments" },
157         /** 4: invalid state for operation (e.g. startconversation when phone is not registered)
158          * @type $.fn.cwic-errorMapEntry
159          */
160         InvalidState :            { code:   4, message: "Invalid State" },
161         /** 5: plugin returned an error
162          * @type $.fn.cwic-errorMapEntry
163          */
164         NativePluginError:        { code:   5, message: "Native plugin error" },
165         /** 6: operation not supported
166          * @type $.fn.cwic-errorMapEntry
167          */
168 		OperationNotSupported:	  { code:   6, message: "Operation not supported" },
169         /** 7: no call manager specified
170          * @type $.fn.cwic-errorMapEntry
171          */
172         ReleaseMismatch :         { code:   7, message: "Release mismatch" },
173         /** 10: release mismatch
174          * @type $.fn.cwic-errorMapEntry
175          */
176         NoCallManagerConfigured : { code:  10, message: "No CUCM found" },
177         /** 11: no devices found for supplied credentials
178          * @type $.fn.cwic-errorMapEntry
179          */
180         NoDevicesFound :          { code:  11, message: "No devices found" },
181         /** 12: no softphone (CSF) devices found for supplied credentials
182          * @type $.fn.cwic-errorMapEntry
183          */
184         NoCsfDevicesFound :       { code:  12, message: "No CSF device found" },
185         /** 13: other phone configuration error
186          * @type $.fn.cwic-errorMapEntry
187          */
188         PhoneConfigGenError :     { code:  13, message: "Phone configuration error" },
189         /** 14: SIP profile error
190          * @type $.fn.cwic-errorMapEntry
191          */
192         SipProfileGenError :      { code:  14, message: "Sip profile error" },
193         /** 15: configuration not set e.g. missing TFTP/CTI/CCMCIP addresses
194          * @type $.fn.cwic-errorMapEntry
195          */
196         ConfigNotSet :            { code:  15, message: "Configuration not set" },
197         /** 16: could not fetch phone configuration
198          * @type $.fn.cwic-errorMapEntry
199          */
200         TftpFetchError :          { code:  16, message: "TFTP fetch error" },
201         /** 18: already logged in in another process (browser or window in internet explorer)
202          * @type $.fn.cwic-errorMapEntry
203          */
204         LoggedInElseWhere :       { code:  18, message: "Already logged in" },
205         /** 19: authentication failed - invalid username or password for configured server
206          * @type $.fn.cwic-errorMapEntry
207          */
208         AuthenticationFailure :   { code:  19, message: "Authentication failed" },
209         /** 20: other login error
210          * @type $.fn.cwic-errorMapEntry
211          */
212         LoginError:               { code:  20, message: "Login Error"},
213         /** 21: no username and/or password supplied
214          * @type $.fn.cwic-errorMapEntry
215          */
216         NoCredentialsConfigured:  { code:  21, message: "No credentials configured"},
217         /** 30: error performing call control operation
218          * @type $.fn.cwic-errorMapEntry
219          */
220         CallControlError:         { code:  30, message: "Call control error"},
221         /** 40: error modifying video association (e.g. removing non-attached window or adding non-existing window)
222          * @type $.fn.cwic-errorMapEntry
223          */
224         VideoWindowError:         { code:  40, message: "Video window error"},
225         /** 999: no error (success)
226          * @type $.fn.cwic-errorMapEntry
227          */
228         NoError:                  { code: 999, message: "No Error"}
229     };
230 
231     var errorMapAlias = {
232         // ApiReturnCodeEnum
233             Ok:                     'NoError',
234             eNoError:               'NoError',
235             eInvalidCallId:         'InvalidArguments',
236             eCreateCallFailed:      'CallControlError',
237             eNoActiveDevice:        'PhoneConfigGenError',
238             eTftpNotConfigured:     'ConfigNotSet',
239             eCallOperationFailed:   'CallControlError',
240             eLoggedInLock:          'LoggedInElseWhere',
241             eLogoutFailed:          'LoginError',
242             eCcmcipNotConfigured:   'ConfigNotSet',
243             eWindowAlreadyExists:   'VideoWindowError',
244             eInvalidState:          'InvalidState',
245             eCtiNotConfigured:      'ConfigNotSet',
246             eNoPhoneMode:           'InvalidArgument',
247             eNoWindowExists:        'VideoWindowError',
248             eInvalidArgument:       'InvalidArguments',
249         // ConnectionFailureCodeEnum
250             //eNoError,
251             eUnknownFailure:                    'LoginError',
252             //eInvalidState,
253             eUnsupportedPhoneMode:              'LoginError',
254             eNoAuthServersConfigured:           'NoCallManagerConfigured',
255             eNoCredentialsConfigured:           'NoCredentialsConfigured',
256             eAuthCouldNotConnect:               'LoginError',
257             eAuthServerCertificateRejected:     'LoginError',
258             eCredentialsRejected:               'LoginError',
259             eAuthResponseEmpty:                 'LoginError',
260             eAuthResponseInvalid:               'LoginError',
261             eNoTftpServersConfigured:           'NoCallManagerConfigured',
262             eNoDeviceNameConfigured:            'LoginError',
263             eLineMustNotBeConfigured:           'LoginError',
264             eNoLocalIpConfigured:               'LoginError',
265             eTftpCouldNotConnect:               'TftpFetchError',
266             eTftpFileNotFound:                  'TftpFetchError',
267             eTftpFileEmpty:                     'TftpFetchError',
268             eTftpFileInvalid:                   'TftpFetchError',
269             eRequireAuthenticationString:       'LoginError',
270             eCapfEnrolmentFailed:               'LoginError',
271             eRequireSecureCachePath:            'LoginError',
272             eRequireSecurityLibrary:            'LoginError',
273             eStorageError:                      'LoginError',
274             eSecurityLibraryError:              'LoginError',
275             eCapfEnrolmentRequired:             'LoginError',
276             eNoCtiServersConfigured:            'LoginError',
277             eDeviceNotInService:                'LoginError',
278         // AuthenticationFailureCode
279             //eNoError:                           '',
280             eNoServersConfigured:               'NoCallManagerConfigured',
281             //eNoCredentialsConfigured:           'NoCredentialsConfigured',
282             eCouldNotConnect:                   'LoginError',
283             eServerCertificateRejected:         'LoginError',
284             //eCredentialsRejected:               'LoginError',
285             eResponseEmpty:                     'LoginError',
286             eResponseInvalid:                   'LoginError',
287 			eOperationNotSupported:				'OperationNotSupported'
288 
289     };
290     var getError = function(key,backupkey) {
291         var errorMapKey = 'Unknown';
292         if(errorMapAlias[key]) {
293             errorMapKey = errorMapAlias[key];
294         } else if(errorMap[key]) {
295             errorMapKey = key;
296         } else if(backupkey && errorMapAlias[backupkey]) {
297             errorMapKey = errorMapAlias[backupkey];
298         } else if(backupkey && errorMap[backupkey]) {
299             errorMapKey = backupkey;
300         }
301         return errorMap[errorMapKey];
302     };
303     /**
304      * Registration object with properties of the currently logged in session <br>
305      * expanded below in _getRegistrationObject() and authenticateCcmcip() <br>
306      * @type Object
307      * @private
308      */
309     var registration = {
310             devices: {} // map of available devices (key is device name)
311     };
312 
313     var registering = {
314         registeringPhone : false,
315         switchingMode: false,
316         successCb : null,
317         errorCb : null,
318         CUCM : [],
319         password : '',
320         unregisterCb : null
321     };
322 
323     var videowindows = {};
324     /**
325      * an internal function to log messages
326      * @param (Boolean) [isVerbose] indicates if msg should be logged in verbose mode only (configurable by the application)  <br>
327      * @param (String) msg the message to be logged (to console.log by default, configurable by the application)  <br>
328      * @param (Object) [context] a context to be logged <br>
329     */
330     function _log() {
331         var isVerbose = typeof arguments[0] === "boolean" ? arguments[0] : false;
332         var msg = typeof arguments[0] === "string" ? arguments[0] : arguments[1];
333         var context = typeof arguments[1] === "object" ? arguments[1] : arguments[2];
334 
335         if ((!isVerbose || (isVerbose && settings.verbose)) && $.isFunction(settings.log)) {
336             try {
337                 settings.log('[cwic] ' + msg, context);
338             } catch(e) {
339                 // Exceptions in application-define log functions can't really be logged
340             }
341         }
342     }
343 
344     // Helper function to check if plugin is still available.
345     // Related to DE3975. The CK advanced editor causes the overflow CSS attribute to change, which in turn
346     // removes and replaces the plugin during the reflow losing all state.
347     var _doesPluginExist = function() {
348         var ret = true;
349         try {
350             var fnref = _plugin.api.getCall;
351         } catch(e) {
352             ret = false;
353         }
354         return ret;
355     };
356 
357     // support unit tests for IE ($.browser.jasmine means we're running unit tests).
358     // should be more transparent than this, but tell that to internet explorer - you can't override attachEvent...
359     var _addListener = function(obj,type,handler) {
360         try {
361             if($.browser.jasmine) {
362                 obj._addListener(type,handler,false);
363             } else if(document.attachEvent) {
364                 obj.attachEvent("on" + type, handler);
365             } else {
366                 obj.addEventListener(type, handler, false);
367             }
368         }
369         catch(e) {
370         }
371     };
372     var _removeListener = function(obj,type,handler) {
373         try {
374             if($.browser.jasmine) {
375                 return;
376                 //obj._removeListener(type,handler,false);
377             } else if(document.attachEvent) {
378                 obj.detachEvent("on" + type, handler);
379             } else {
380                 obj.removeEventListener(type, handler, false);
381             }
382         } catch(e) {
383         }
384     };
385 
386     var rebootIfBroken = function(callback) {
387         var pluginExists = _doesPluginExist();
388         if(!pluginExists) {
389             _log("Plugin has been unloaded. Restarting....");
390             _plugin = null;
391             callback();
392         }
393         return pluginExists;
394     };
395 
396     /**
397      * predict a device based on username
398      * @param {Object} options
399      * @type {String}
400      */
401     function _predictDevice(options) {
402         if($.isFunction(settings.predictDevice)) {
403             try {
404                 return settings.predictDevice(options);
405             } catch(predictDeviceException) {
406                 _log('Exception occurred in application predictDevice callback',predictDeviceException);
407                 if(typeof console !== "undefined" && console.trace) {
408                     console.trace();
409                 }
410             }
411             
412         } else {
413 			return (options.username)? settings.devicePrefix+options.username : '';
414         }
415     }
416     /**
417      * encode a string into base64
418      * @param {String} str
419      * @type {String}
420      */
421     function _encodeBase64(str) {
422         // application encoding
423         if ($.isFunction(settings.encodeBase64)) {
424             try {
425                 return settings.encodeBase64(str);
426             } catch(encodeBase64Exception) {
427                 _log('Exception occurred in application encodeBase64 callback',encodeBase64Exception);
428                 if(typeof console !== "undefined" && console.trace) {
429                     console.trace();
430                 }
431             }
432             
433         }
434 
435         // btoa exists on Mozilla
436         if ($.isFunction(window.btoa)) {
437             return window.btoa(str);
438         }
439 
440         // IE ?
441         _log('error: cannot encode base64');
442 
443         return '';
444     }
445 
446     /**
447      * manages asynchronous loading/resetting of windowhandles for local preview windows
448      * @private
449      */
450     var previewwindows = {
451         // windows: map keyed by document->pluginId, value is:
452         //      plugin: the plugin object (JSAPI/DOM)
453         //      windowhandle: last known windowhandle
454         windows: [],
455         windowobjects: [],
456         getWindowId: function(args) {
457             var win = args.window;
458             var windowid = this.windowobjects.indexOf(win);
459             if(windowid === -1 && !args.readOnly) {
460                 this.windowobjects.push(win);
461                 windowid = this.windowobjects.indexOf(win);
462             }
463             return windowid;
464         },
465         /**
466          * adds a video plugin as a preview window
467          * @param {Object} args
468          * @param {String|Object} args.plugin id of the video plugin or the plugin object itself
469          * @param {Object} args.window the parent window for this plugin (for popup support)
470          * @private
471          */
472         add: function(args) {
473             if(!args) {
474                 return;
475             }
476             if(!args.window) {
477                 args.window = window;
478             }
479             var windowid = this.getWindowId({window:args.window});
480             if(!this.windows[windowid]) {
481                 this.windows[windowid]= {};
482             }
483             if(typeof args.plugin === "string") {
484                 this.windows[windowid][args.plugin] = {
485                     windowhandle: null,
486                     plugin: null,
487                     used: false
488                 };
489                 var elem = args.window.document.getElementById(args.plugin);
490                 if(elem && elem.windowhandle) {
491                     this.update({window: args.window, plugin: elem});
492                 }
493             } else if(args.plugin && args.plugin.id) {
494                 this.windows[windowid][args.plugin.id] = {
495                    windowhandle: args.plugin.windowhandle,
496                    plugin: args.plugin,
497                    used:false
498                 };
499                 if(args.plugin.windowhandle && args.plugin.windowhandle !== "00000000") {
500                     _plugin.api.addPreviewWindow({windowhandle: args.plugin.windowhandle});
501                     this.windows[windowid][args.plugin.id].used = true;
502                 }
503             }
504         },
505         /**
506          * updates all preview Windows or the specific one passed in
507          * @param {Object} [args]
508          * @param {Object} args.window the parent window for this plugin (for popup support)
509          * @param {Object} args.plugin
510          * @private
511          */
512         update: function(args) {
513             var win;
514             // Add any queued preview windows if they've been loaded (i.e. have a windowhandle now) since being added
515             if(!args || !args.plugin || !args.plugin.id) {
516                 for(win in this.windows) {
517                     if(this.windows.hasOwnProperty(win)) {
518                         for(var pluginId in this.windows[win]) {
519                             if(this.windows[win].hasOwnProperty(pluginId)) {
520                                 if(!this.windows[win][pluginId].plugin) {
521                                     this.windows[win][pluginId].plugin = this.windowobjects[win].document.getElementById(pluginId);
522                                 }
523                                 if(this.windows[win][pluginId].plugin && this.windows[win][pluginId].plugin.windowhandle) {
524                                     if(this.windows[win][pluginId].used) {
525                                         if(this.windows[win][pluginId].windowhandle !== this.windows[win][pluginId].plugin.windowhandle) {
526                                             _plugin.api.removePreviewWindow({windowhandle: this.windows[win][pluginId].windowhandle});
527                                             this.windows[win][pluginId].windowhandle = this.windows[win][pluginId].plugin.windowhandle;
528                                             _plugin.api.addPreviewWindow({windowhandle: this.windows[win][pluginId].windowhandle});
529                                         }
530                                     } else {
531                                         this.windows[win][pluginId].windowhandle = this.windows[win][pluginId].plugin.windowhandle;
532                                         _plugin.api.addPreviewWindow({windowhandle: this.windows[win][pluginId].windowhandle});
533                                         this.windows[win][pluginId].used = true;
534                                     }
535                                 }
536                             }
537                         }
538                     }
539                 }
540                 return;
541             }
542             // Check if the window handle has changed since last update
543             // If so, remove the old one, add the new one.
544             if(!args.window) {
545                 args.window = window;
546             }
547             var windowid = this.getWindowId({window: args.window, readonly: true});
548             if(windowid === -1 || !this.windows[windowid] || !this.windows[windowid][args.plugin.id]) {
549                 return;
550             }
551             if(this.windows[windowid][args.plugin.id].windowhandle !== args.plugin.windowhandle) {
552                 if(this.windows[windowid][args.plugin.id].windowhandle) {
553                     _plugin.api.removePreviewWindow({windowhandle: this.windows[windowid][args.plugin.id].windowhandle});
554                     this.windows[windowid][args.plugin.id].used = false;
555                 }
556                 this.windows[windowid][args.plugin.id].windowhandle = args.plugin.windowhandle;
557                 if(args.plugin.windowhandle) {
558                     _plugin.api.addPreviewWindow({windowhandle: this.windows[windowid][args.plugin.id].windowhandle});
559                     this.windows[windowid][args.plugin.id].used = true;
560                 }
561             }
562         },
563         /**
564          * removes a plugin object from the list of preview windows
565          * @param {Object} args
566          * @param {String|Object} args.plugin
567          * @param {Object} args.window the parent window for this plugin (for popup support)
568          * @private
569          */
570         remove: function(args) {
571             if(!args) {
572                 return;
573             }
574             if(!args.window) {
575                 args.window = window;
576             }
577             var windowid = this.getWindowId({window:args.window, readOnly: true});
578             if(windowid === -1 || !this.windows[windowid]) {
579                 return;
580             }
581             if(typeof args.plugin === "string" && windowid >=0 && this.windows[windowid] && this.windows[windowid][args.plugin]) {
582                 if(this.windows[windowid][args.plugin].windowhandle) {
583                     _plugin.api.removePreviewWindow({windowhandle: this.windows[windowid][args.plugin].windowhandle});
584                 }
585                 delete this.windows[windowid][args.plugin];
586             }
587             if(args.plugin && args.plugin.id) {
588                 _plugin.api.removePreviewWindow({windowhandle: args.plugin.windowhandle});
589                 delete this.windows[windowid][args.plugin.id];
590             }
591         },
592         dump: function() {
593             _log("Dumping Preview Windows...");
594             for(var win in this.windows) {
595                 if(this.windows.hasOwnProperty(win)) {
596                     _log("  window: "+this.windowobjects[win].document.location.pathname);
597                     for(var pluginId in this.windows[win]) {
598                         if(this.windows[win].hasOwnProperty(pluginId)) {
599                             _log("    ID: " + pluginId + ", windowhandle: " + this.windows[win][pluginId].windowhandle + ", used: "+this.windows[win][pluginId].used);
600                         }
601                     }
602                 }
603             }
604         }
605     };
606     /**
607      * manages asynchronous loading/resetting of windowhandles for remote video objects related to calls
608      * and adds the window to the call if it's async loaded after the id has been added to the call
609      *
610      * @private
611      */
612     var videowindowsbycall = {
613         // calls: map keyed by callId
614         //        each element is a map keyed by window and pluginId, containing:
615         //        plugin: the plugin object (JSAPI/DOM)
616         //        windowhandle: the last known windowhandle added for this plugin id
617         calls: {},
618         windowobjects: [],
619         getWindowId: function(args) {
620             var win = args.window;
621             var windowid = this.windowobjects.indexOf(win);
622             if(windowid === -1 && !args.readOnly) {
623                 this.windowobjects.push(win);
624                 windowid = this.windowobjects.indexOf(win);
625             }
626             return windowid;
627         },
628         /**
629          * adds a video plugin to a call, adding the call to the map if it doesn't exist
630          * @param {Object} args
631          * @param {String|Number} args.callId
632          * @param {String|Object} args.plugin id of the video plugin or the plugin object itself
633          * @param {Object} args.window the parent window for this plugin (for popup support)
634          * @private
635          */
636         add: function(args) {
637             var windowid;
638             if(!args) {
639                 return;
640             }
641             if(!args.callId) {
642                 return;
643             }
644             if(!args.window) {
645                 args.window = window;
646             }
647             if(!this.calls[args.callId]) {
648                 this.calls[args.callId] = [];
649             }
650             windowid = this.getWindowId({window:args.window});
651             if(!this.calls[args.callId][windowid]) {
652                 this.calls[args.callId][windowid] = {};
653             }
654             if(typeof args.plugin === "string") {
655                 this.calls[args.callId][windowid][args.plugin] = {
656                     windowhandle: null,
657                     plugin: null,
658                     used: false
659                 };
660                 var elem = args.window.document.getElementById(args.plugin);
661                 if(elem && elem.windowhandle) {
662                     this.update({window: args.window, callId: args.callId, plugin: elem});
663                 }
664             } else if(args.plugin && args.plugin.id) {
665                 this.calls[args.callId][windowid][args.plugin.id] = {
666                    windowhandle: args.plugin.windowhandle,
667                    plugin: args.plugin,
668                    used:false
669                 };
670                 if(args.plugin.windowhandle && args.plugin.windowhandle !== "00000000") {
671                     _plugin.api.addWindowToCall({callId: args.callId, windowhandle: args.plugin.windowhandle});
672                     this.calls[args.callId][windowid][args.plugin.id].used = true;
673                 }
674             }
675         },
676         /**
677          * removes a plugin object from a call
678          * @param {Object} args
679          * @param {String|Number} args.callId
680          * @param {String|Object} args.plugin
681          * @param {Object} args.window the parent window for this plugin (for popup support)
682          * @private
683          */
684         remove: function(args) {
685             var windowid;
686             if(!args) {
687                 return;
688             }
689             if(!args.callId) {
690                 return;
691             }
692             if(!this.calls[args.callId]) {
693                 return;
694             }
695             if(!args.window) {
696                 args.window = window;
697             }
698             windowid = this.getWindowId({window:args.window, readOnly: true});
699             if(windowid === -1 || !this.calls[args.callId][windowid]) {
700                 return;
701             }
702             if(typeof args.plugin === "string" && this.calls[args.callId][windowid][args.plugin]) {
703                 if(this.calls[args.callId][windowid][args.plugin].windowhandle) {
704                     _plugin.api.removeWindowFromCall({callId: args.callId, windowhandle: this.calls[args.callId][windowid][args.plugin].windowhandle});
705                 }
706                 delete this.calls[args.callId][windowid][args.plugin];
707             }
708             if(args.plugin && args.plugin.id) {
709                 _plugin.api.removeWindowFromCall({callId: args.callId, windowhandle: args.plugin.windowhandle});
710                 delete this.calls[args.callId][windowid][args.plugin.id];
711             }
712         },
713         /**
714          * updates all remote video plugins associated with a callid or the specific one passed in
715          * @param {Object} args
716          * @param {String|Number} args.callId
717          * @param {Object} [args.plugin]
718          * @param {Object} args.window the parent window for this plugin (for popup support)
719          * @private
720          */
721         update: function(args) {
722             if(!args) {
723                 return;
724             }
725             var win;
726             // Add any queued windows to the call if they've been loaded (i.e. have a windowhandle now) since being added
727             if(!args.plugin || !args.plugin.id) {
728                 if(args.callId) {
729                     if(this.calls[args.callId]) {
730                         for(win in this.calls[args.callId]) {
731                             if(this.calls[args.callId].hasOwnProperty(win)) {
732                                 for(var pluginId in this.calls[args.callId][win]) {
733                                     if(this.calls[args.callId][win].hasOwnProperty(pluginId)) {
734                                         if(!this.calls[args.callId][win][pluginId].plugin) {
735                                             this.calls[args.callId][win][pluginId].plugin = this.windowobjects[win].document.getElementById(pluginId);
736                                         }
737                                         if(this.calls[args.callId][win][pluginId].plugin && this.calls[args.callId][win][pluginId].plugin.windowhandle) {
738                                             if(this.calls[args.callId][win][pluginId].used) {
739                                                 if(this.calls[args.callId][win][pluginId].windowhandle !== this.calls[args.callId][win][pluginId].plugin.windowhandle) {
740                                                 _plugin.api.removeWindowFromCall({callId: args.callId, windowhandle: this.calls[args.callId][win][pluginId].windowhandle});
741                                                 this.calls[args.callId][win][pluginId].windowhandle = this.calls[args.callId][win][pluginId].plugin.windowhandle;
742                                                 _plugin.api.addWindowToCall({callId: args.callId, windowhandle: this.calls[args.callId][win][pluginId].windowhandle});
743                                                 }
744                                             } else {
745                                                 this.calls[args.callId][win][pluginId].windowhandle = this.calls[args.callId][win][pluginId].plugin.windowhandle;
746                                                 _plugin.api.addWindowToCall({callId: args.callId, windowhandle: this.calls[args.callId][win][pluginId].windowhandle});
747                                                 this.calls[args.callId][win][pluginId].used = true;
748                                             }
749                                         }
750                                     }
751                                 }
752                             }
753                         }
754                     }
755                 }
756                 return;
757             }
758             // Check if the window handle has changed since last update
759             // If so, remove the old one, add the new one.
760             if(!args.window) {
761                 args.window = window;
762             }
763             win = this.getWindowId({window:args.window, readOnly:true});
764             if(win === -1) {
765                 return;
766             }
767             for(var call in this.calls) {
768                 if(this.calls.hasOwnProperty(call)) {
769                     if(this.calls[call][win] && this.calls[call][win][args.plugin.id]) {
770                         if(this.calls[call][win][args.plugin.id].windowhandle !== args.plugin.windowhandle) {
771                             if(this.calls[call][win][args.plugin.id].windowhandle) {
772                                 _plugin.api.removeWindowFromCall({callId: call, windowhandle: this.calls[call][win][args.plugin.id].windowhandle});
773                             }
774                             this.calls[call][win][args.plugin.id].windowhandle = args.plugin.windowhandle;
775                             if(args.plugin.windowhandle) {
776                                 _plugin.api.addWindowToCall({callId: call, windowhandle: this.calls[call][win][args.plugin.id].windowhandle});
777                             }
778                         }
779                     }
780                 }
781             }
782         },
783         /**
784          * currently unused
785          * @param {Object} args
786          * @param {String|Number} args.callId
787          * @private
788          */
789         addCall: function(args) {
790             if(args.callId) {
791                 if(this.calls[args.callId]) {
792                     return;
793                 } else {
794                     this.calls[args.callId] = [];
795                 }
796             }
797         },
798         /**
799          * delete a callId from the map, removing all window handles and clearing up
800          * @param {Object} args
801          * @param {String|Number} args.callId
802          * @private
803          */
804         deleteCall: function(args) {
805             if(args.callId) {
806                 if(this.calls[args.callId]) {
807                     for(var win in this.calls[args.callId]) {
808                         if(this.calls[args.callId].hasOwnProperty(win)) {
809                             for(var plugin in this.calls[args.callId][win]) {
810                                 if(this.calls[args.callId][win].hasOwnProperty(plugin)) {
811                                     if(this.calls[args.callId][win][plugin].windowhandle) {
812                                         _plugin.api.removeWindowFromCall({callId: args.callId, windowhandle: this.calls[args.callId][win][plugin].windowhandle});
813                                     }
814                                 }
815                             }
816                         }
817                     }
818                     delete this.calls[args.callId];
819                 }
820             }
821         },
822         dump: function() {
823             _log("Dumping Video Windows...");
824             for(var call in this.calls) {
825                 if(this.calls.hasOwnProperty(call)) {
826                     _log("  callId: "+call);
827                     for(var win in this.calls[call]) {
828                         if(this.calls[call].hasOwnProperty(win)) {
829                             _log("    window: "+this.windowobjects[win].document.location.pathname);
830                             for(var pluginId in this.calls[call][win]) {
831                                 if(this.calls[call][win].hasOwnProperty(pluginId)) {
832                                     _log("      ID: " + pluginId + ", windowhandle: " + this.calls[call][win][pluginId].windowhandle + ", used: "+this.calls[call][win][pluginId].used);
833                                 }
834                             }
835                         }
836                     }
837                 }
838             }
839         }
840 
841     };
842 
843     var videowindowloadedcallbacks = {
844         windowobjects: [],
845         getWindowId: function(args) {
846             var win = args.window;
847             var windowid = this.windowobjects.indexOf(win);
848             if(windowid === -1 && !args.readOnly) {
849                 this.windowobjects.push(win);
850                 windowid = this.windowobjects.indexOf(win);
851                 this.callbacks[windowid] = {};
852             }
853             return windowid;
854         },
855         callbacks: []
856     };
857 
858     /**
859      * called when a video plugin object is loaded or the window handle for a video plugin object changes
860      * @param pluginobject the DOM element (sort-of - not so much on IE) of the plugin object
861      * @param windowhandle the window handle of the plugin object - passed here because dereferencing it from the plugin object on IE sometimes causes a reference error (even checking typeof on it)
862      * @private
863      */
864     var windowhandleupdated = function(pluginobject,windowhandle, win) {
865         if(!win) {
866             win = window;
867         }
868         _log("Updating plugin " + " on " + win.document.location.pathname + " windowhandle: " + windowhandle, pluginobject);
869         var element;
870         // updated window handle may be either a preview or a remote window - update both, updates will ignore if not already stored...
871         var windowid = videowindowloadedcallbacks.getWindowId({window:win});
872         var updated = false;
873         for(var vidpluginid in videowindowloadedcallbacks.callbacks[windowid]) {
874             if(videowindowloadedcallbacks.callbacks[windowid].hasOwnProperty(vidpluginid)) {
875                 try {
876                     updated = true;
877                     element = win.document.getElementById(vidpluginid);
878                     previewwindows.update({"window": win, plugin: element, windowhandle: windowhandle});
879                     videowindowsbycall.update({"window":win, plugin: element, windowhandle: windowhandle});
880                     if(element && element.windowhandle && pluginobject.windowhandle && element.windowhandle === pluginobject.windowhandle) {
881                         var onloaded = videowindowloadedcallbacks.callbacks[windowid][vidpluginid];
882                         if(!videowindowloadedcallbacks.callbacks[windowid][vidpluginid].wascalled) {
883                             if(onloaded && $.isFunction(onloaded.callback)) {
884                                 try {
885                                     onloaded.callback(vidpluginid);
886                                 } catch(videoLoadedException) {
887                                     _log('Exception occurred in application videoLoaded callback',videoLoadedException);
888                                     if(typeof console !== "undefined" && console.trace) {
889                                         console.trace();
890                                     }
891                                 }
892                             }
893                             videowindowloadedcallbacks.callbacks[windowid][vidpluginid].wascalled = true;
894                         }
895                     }
896                 }
897                 catch(e) {
898                     _log("Warning: internal window handle update error", e);
899                     if(typeof console !== "undefined" && console.trace) {
900                         console.trace();
901                     }
902                     // IE throws an exception, ff/chrome leaves windowhandle undefined on not fully instantiated plugins
903                 }
904             }
905         }
906         //previewwindows.dump();
907         //videowindowsbycall.dump();
908     };
909     var windowHandleUpdaters = {
910         windowobjects: [],
911         windowHandleUpdaters: [],
912         _getWindowId: function(args) {
913             var win = args.window;
914             var windowid = this.windowobjects.indexOf(win);
915             if(windowid === -1 && !args.readOnly) {
916                 this.windowobjects.push(win);
917                 windowid = this.windowobjects.indexOf(win);
918             }
919             return windowid;
920         },
921         get: function(win) {
922             var windowId = this._getWindowId({window:win});
923             if(!this.windowHandleUpdaters[windowId]) {
924                 this.windowHandleUpdaters[windowId] = function(pluginobject,windowhandle) {
925                     return(windowhandleupdated(pluginobject,windowhandle,win));
926                 };
927             }
928             return this.windowHandleUpdaters[windowId];
929         }
930     };
931     /**
932      * global(window) level function object to handle video plugin object onLoad
933      * @type function
934      * @param  {JSAPI} videopluginobject video plugin object (DOM Element)
935      * @returns undefined
936      */
937     window._cwic_onVideoPluginLoaded = function(videopluginobject) {
938         try {
939             windowhandleupdated(videopluginobject, videopluginobject.windowhandle, window);
940             _log("_window updated");
941         }
942         catch(e) {
943             // IE throws an exception when dereferencing a property on an invisible (display: none) object
944         }
945         _removeListener(videopluginobject,'windowhandleupdated',windowHandleUpdaters.get(window));
946         _addListener(videopluginobject,'windowhandleupdated',windowHandleUpdaters.get(window));
947     };
948 
949 
950     /**
951      * global(window) level function to handle video plugins loaded in iframe/popup window
952      * Should be called from video object onLoad handler {@link $.fn.cwic-createVideoWindow}<br>
953      * Example onLoad function in iframe.html
954      * @example
955      * function onvideoPluginLoaded(obj) {
956      *     window.parent._cwic_onPopupVideoPluginLoaded(obj, window);
957      * }
958      * @returns undefined
959      * @param  {JSAPI} videopluginobject video plugin object (DOM Element)
960      * @param  {DOMWindow} win iframe or popup window
961      * @public
962      */
963     window._cwic_onPopupVideoPluginLoaded = function(videopluginobject, win) {
964         try {
965             windowhandleupdated(videopluginobject, videopluginobject.windowhandle, win);
966             _log("popup _window updated: " + win.document.location.pathname);
967         }
968         catch(e) {
969             // IE throws an exception when dereferencing a property on an invisible (display: none) object
970         }
971         _removeListener(videopluginobject,'windowhandleupdated',windowHandleUpdaters.get(win));
972         _addListener(videopluginobject,'windowhandleupdated',windowHandleUpdaters.get(win));
973     };
974 
975 
976     /**
977      * Wait for the document to be ready, and try to load the plugin object.<br>
978      * If cwic was successfully initialized, call the options.ready handler, <br>
979      * passing some stored properties (possibly empty), <br>
980      * otherwise call the options.error handler<br>
981      * @param {Object} options Is a set of key/value pairs to configure the phone registration.  See {@link $.fn.cwic-settings} for options.
982      * @example
983      * jQuery('#phone').cwic('init', {
984      *   ready: function(defaults) {
985      *     console.log('phone is ready');
986      *   },
987      *   error: function(status) {
988      *     console.log('phone cannot be initialized : ' + status);
989      *   },
990      *   log: function(msg, exception) {
991      *     console.log(msg); if (exception) { console.log(exception); }
992      *   },
993      *   errorMap: {
994      *     // localized message for error code #17
995      *     17 : { message: 'Nom d'utilisateur ou mot de passe incorrect' }
996      *   },
997      *   node: 'http://mynode.com:8080',
998      *   predictDevice: function(args) {
999      *       return settings.devicePrefix+args.username;
1000      *   }
1001      *   encodeBase64: function(str) {
1002      *     encoded = myBase64Implementation(str);
1003      *     return encoded;
1004      *   }
1005      *});
1006      */
1007     function init(options) {
1008         _log(true, 'init', arguments);
1009 
1010         var $this = this;
1011 
1012         // the application can replace/extend the default error map
1013         if (typeof options.errorMap !== "undefined") {
1014             // extend the default errorMap
1015             $.each(options.errorMap, function(key, info) {
1016 
1017                 if (typeof info === "string") {
1018                     errorMap[key] = $.extend({}, errorMap[key], { message: info });
1019                 }
1020                 else if (typeof info === "object") {
1021                     errorMap[key] = $.extend({}, errorMap[key], info);
1022                 }
1023                 else {
1024                     _log('ignoring invalid custom error [key=' + key +']', info);
1025                 }
1026             });
1027         }
1028 
1029         // extend the default settings with options
1030         $.extend(settings, options);
1031 
1032         // this function is called by the browser after the cwic plugin object is loaded
1033         // see the <param> of the inserted plugin <object>
1034         /**
1035          * Phone Object onLoad handler
1036          * @returns undefined
1037          */
1038         window._cwic_onPluginLoaded = function() {
1039             var error;
1040             try {
1041                 var cwcObject = document.getElementById('cwc-plugin'); // re-select object
1042 
1043                 if (cwcObject) {
1044                     // plugin is available, update global reference
1045                     _plugin = {};
1046 
1047                     _plugin.api = cwcObject;
1048                     _plugin.version = _plugin.api.version;
1049                  }
1050 
1051                  if (_plugin !== null) {
1052                     _log("initialized plugin version " + _plugin.version.plugin + " (ecc " + _plugin.version.ecc + ")");
1053 
1054                     var defaults = {},s,m,phoneRegistered=false;
1055 
1056                     s = $(location).get(0).search;
1057                     if ((m = (/user=(.*?)($|&)/i).exec(s))) { defaults.user = m[1]; }
1058                     if ((m = (/cucm=(.*?)($|&)/i).exec(s))) { defaults.cucm = m[1]; }
1059 
1060                     // automatically shutdown when the window unloads
1061                     $(window).unload(function() { shutdown(); });
1062 
1063                     var currState = _plugin.api.connectionStatus;
1064                     if(currState === 'eReady') {
1065                         phoneRegistered=true;
1066                     }
1067                     _registerSystemCallbacks($this);
1068                     _registerCallChangeCallbacks($this);
1069 					
1070 					_log('setting media port splitting to ' + settings.mediaPortSplitting);
1071 					_plugin.api.MediaPortSplitting = settings.mediaPortSplitting;
1072 
1073 					var phoneMode = _plugin.api.mode;
1074                     if ($.isFunction(settings.ready)) {
1075                         try {
1076                             settings.ready(defaults,phoneRegistered,phoneMode);
1077                         } catch(readyException) {
1078                             _log('Exception occurred in application ready callback',readyException);
1079                             if(typeof console !== "undefined" && console.trace) {
1080                                 console.trace();
1081                             }
1082                         }
1083                         
1084                     }
1085                     if(phoneRegistered) {
1086                         var calls = _plugin.api.getCalls();
1087                         for(var i=0;i<calls.calls.length;i++) {
1088                             _triggerConversationEvent($this, calls.calls[i], 'state');
1089                         }
1090                     }
1091 
1092                     return;
1093                 }
1094                 else {
1095                     error = errorMap.PluginNotAvailable;
1096                 }
1097             }
1098             catch (e) {
1099                 if(typeof console !== "undefined" && console.trace) {
1100                     console.trace();
1101                 }
1102                 _plugin = null;
1103                 error = $.extend({}, errorMap.PluginNotAvailable, e);
1104             }
1105 
1106             // TODO: Remove hardcoded string
1107             _triggerError($this, settings.error, 'cannot initialize Cisco Web Communicator', error);
1108 
1109         }; // _cwic_onPluginLoaded
1110 
1111         $(document.body).ready(function() {
1112 
1113             if (_plugin === null) {
1114                 // create plugin object and attach to DOM body
1115 
1116                 try {
1117                 	// try to load the ActiveX/NPAPI plugin, throw an error if it fails
1118                 	
1119                     if (!$.browser.jasmine) {
1120                         if ($.browser.msie && parseInt($.browser.version, 10) >= 7) {
1121                             // Internet Explorer 7+
1122                             try {
1123     	                        var pluginExists = new ActiveXObject("CiscoSystems.CWCVideoCall");
1124         	                    // no exception, plugin is available
1125         	                    // how to check plugin is enabled in IE ?
1126             	                pluginExists = null;
1127                 			}
1128                 			catch(dummy_e) {
1129                 				// exception, plugin is not available
1130                 				try {
1131                 					// check if previous release is installed
1132 	                				var previousPluginExists = new ActiveXObject("ActivexPlugin.WebPhonePlugin.1");
1133                 					// no exception. previous plugin is available
1134                 					previousPluginExists = null;
1135                 					throw getError('ReleaseMismatch');
1136                 				}
1137                 				catch(ppe) {
1138                 					// no previous plugin, ignore silently
1139                 				}
1140                 			
1141                 				throw getError('PluginNotAvailable');
1142                 			}
1143                         }
1144 
1145                         else if ($.browser.mozilla || $.browser.webkit) {
1146                             // Mozilla/Webkit
1147                             // check plugin availability through MIME types
1148                             var pluginMimeType = navigator.mimeTypes["application/x-ciscowebcommunicator"];
1149                             if (typeof pluginMimeType === "undefined") {
1150                             	// plugin not available, check if any previous release is installed
1151                             	pluginMimeType = navigator.mimeTypes["application/x-ciscowebphone"];
1152                             	if (typeof pluginMimeType !== "undefined") {
1153                             		// previous plugin is available
1154                             		throw getError('ReleaseMismatch');
1155                             	}
1156                             	
1157                                 throw getError('PluginNotAvailable');
1158                             }
1159                             
1160                             if (pluginMimeType.enabledPlugin === null) {
1161 	                            // additional flag indicating the plugin is installed but disabled
1162                             	var err = getError('PluginNotAvailable');
1163                             	err.pluginDisabled = true;
1164                             	throw err;
1165                             }
1166                         }
1167 
1168                         else {
1169                             throw getError('BrowserNotSupported');
1170                         }
1171 
1172                         $(document.body).append('<object id="cwc-plugin" width="1" height="1" type="application/x-ciscowebcommunicator"><param name="onload" value="_cwic_onPluginLoaded"></param></object>');
1173                     }
1174                 }
1175                 catch (e) {
1176                     _plugin = null;
1177 	                _triggerError($this, settings.error, e);
1178                 }
1179             }
1180 
1181         }); // document ready
1182 
1183         return $this;
1184     } // end of init
1185 
1186     var videopluginid = 1;
1187     /**
1188      * Update the global registration object with information from the native plugin
1189      */
1190     function _updateRegistration(state) {
1191         //registration.mode = _plugin.api.mode;
1192 
1193         // get the available devices returned by the plugin
1194         var ret = _plugin.api.getAvailableDevices();
1195         var devices = ret.devices;
1196 
1197         // merge device information returned by the plugin
1198         $.each($.makeArray(devices), function(i, device) {
1199             if (device.name){
1200                 var deviceName = $.trim(device.name);
1201                 registration.devices[deviceName] = $.extend({}, registration.devices[deviceName], device);
1202                 // associate an array of lines to each device
1203                 //registration.devices[deviceName].lines = _plugin.api.getAvailableLines(deviceName);
1204             }
1205         });
1206 
1207         var pluginDevice = '';//_plugin.api.PreferredDevice;
1208         if (pluginDevice !== '') {
1209             $.extend(registration.device, { name: pluginDevice });
1210         }
1211 
1212 		// add device and line info except during logout
1213 		if (state != 'eIdle') {
1214 			registration.device = $.extend({}, _plugin.api.device);
1215 
1216 			registration.line = $.extend(registration.line, _plugin.api.line);
1217 		}
1218     }
1219 
1220     /**
1221      * Creates an object that can be passed to startConversation, addPreviewWindow or updateConversation('addRemoteVideoWindow').
1222      * The object is inserted into the element defined by the jQuery context - e.g. jQuery('#placeholder').cwic('createVideoWindow') inserts the videowindow under jQuery('#placeholder')
1223      * @example $('#videocontainer').cwic('createVideoWindow',{id: 'videoplugin',success: function(pluginid) {$('#conversation').cwic('updateConversation',{'addRemoteVideoWindow': pluginid});}});
1224      * @param {Object} [settings] Settings to use when creating the video render object
1225      * @param {String} [settings.id = generated] The DOM id of the element to be created
1226      * @param {Function} [settings.success] Called when the object is loaded and ready for use plugin id is passed as a parameter
1227      * @param {Function} [onload] Optional, not recommended for video windows created in the same window as the main phone plugin.
1228      * <br>Mandatory in popup windows or iframes, must call parent or opener {@link window._cwic_onPopupVideoPluginLoaded} in the onLoad handler.
1229      * <br>Single parameter is the videoplugin object that must be passed to the parent handler.
1230      * <br>
1231      * <br>NOTE: If video isn't supported on the platform, this function will just 'do nothing' and the success() callback will never be called.
1232      */
1233     function createVideoWindow(settings) {
1234         var $this = this;
1235         var videoSupportedByPlugin = ((!_plugin || !_plugin.api) ? undefined : _plugin.api.supportsVideo);
1236         if(videoSupportedByPlugin) {
1237             settings = settings || {};
1238             settings.window = settings.window || window;
1239             /* TODO: Remotely inject function into other window if possible
1240             if(settings.window !== window && !settings.window._cwic_onVideoPluginLoaded) {
1241                 settings.window._cwic_onVideoPluginLoaded = _cwic_onVideoPluginLoaded;
1242             } */
1243             var mimetype = "application/x-cisco-cwc-videocall";
1244             var onload = settings.onload || "_cwic_onVideoPluginLoaded";
1245             var callback = settings.success;// || videopluginloaded;
1246             var id = settings.id || '_cwic_vw'+videopluginid;
1247             videopluginid++;
1248             var windowid = videowindowloadedcallbacks.getWindowId({window:settings.window});
1249             videowindowloadedcallbacks.callbacks[windowid][id] = {callback: callback, wascalled: false};
1250             var elemtext='<object type="'+mimetype+'" id="'+id+'"><param name="onload" value="'+onload+'"></param></object>';
1251             //jQuery(settings.window.document.getElementById($this.selector)).append(elemtext);
1252             jQuery($this).append(elemtext);
1253         }
1254         return $this;
1255     }
1256     /**
1257      * Assign a video window object to preview (self view)
1258      * @param {Object} args arguments object
1259      * @param {DOMWindow} [args.window] DOM Window that contains the plugin Object defaults to current window
1260      * @param {String|PluginObject} args.previewWindow id or DOM element of preview window
1261      */
1262     function addPreviewWindow(args) {
1263         var $this = this;
1264         args.window = args.window || window;
1265         if(args.previewWindow) {
1266             previewwindows.add({plugin: args.previewWindow, "window": args.window});
1267             //previewwindows.dump();
1268         }
1269         return $this;
1270     }
1271     /**
1272      * Remove a video window object from preview (self view)
1273      * @example
1274      * $('#phone').cwic('removePreviewWindow',{previewWindow: 'previewVideoObjectID'});
1275      * $('#phone').cwic('removePreviewWindow',{previewWindow: previewVideoObject, window: iFramePinPWindow});
1276      * @param {Object} args arguments object
1277      * @param {DOMWindow} [args.window] DOM Window that contains the plugin Object defaults to current window
1278      * @param {String|Object} args.previewWindow id or DOM element of preview window
1279      */
1280     function removePreviewWindow(args) {
1281         var $this = this;
1282         args.window = args.window || window;
1283         if(args.previewWindow) {
1284             previewwindows.remove({plugin: args.previewWindow, "window": args.window});
1285             //previewwindows.dump();
1286         }
1287         return $this;
1288     }
1289     /**
1290      * Shuts down the API<br>
1291      * <ul><li>Unregisters the phone</li>
1292      * <li>Unbinds all cwic events handlers</li>
1293      * <li>Clears all cwic data</li>
1294      * <li>Releases the CWC browser plugin instance</li></ul>
1295      * @example
1296      *  jQuery(window).unload(function() { <br>
1297      *      jQuery('#phone').cwic('shutdown'); <br>
1298      *  }); <br>
1299      */
1300     function shutdown() {
1301         _log(true, 'shutdown', arguments);
1302 
1303         unregisterPhone();
1304 
1305         // unbind all cwic events handlers
1306         $(document).unbind('.cwic');
1307 
1308         // clear callbacks and logout if needed
1309         _reset();
1310 
1311         try {
1312             // release the plugin instance
1313             if (_plugin && _plugin.api && typeof _plugin.api.releaseInstance !== "undefined") {
1314                 _plugin.api.releaseInstance();
1315             }
1316         }
1317         catch(e) {
1318         }
1319 
1320         //$('#cwc-plugin').remove();
1321         _plugin = null;
1322 
1323         }
1324 
1325     /**
1326      * @private
1327      */
1328     function _triggerAuthenticationResult($this, result) {
1329         _log(true, 'authenticationResult '+result);
1330         if(result === "eNoError") {
1331             if(registering.authenticatedCallback) {
1332                 registering.authenticatedCallback();
1333             }
1334         } else {
1335             if(registering.authenticatedCallback) {
1336                 registering.authenticatedCallback = null;
1337                 delete registering.authenticatedCallback;
1338             }
1339             _triggerError($this,registering.errorCb,getError(result,'LoginError'),result, {registration: registration});
1340         }
1341     }
1342     /**
1343      * @private
1344      */
1345     function _triggerProviderEvent($this,state) {
1346         _log(true, 'providerState ' + state);
1347 
1348         var event = $.Event('system.cwic');
1349         event.phone = { status: state };
1350 
1351         // update global registration and add it to the system event
1352         _updateRegistration(state);
1353         event.phone.registration = registration;
1354 
1355         //providerStates.push(state);
1356 
1357         if (state === 'eReady') {
1358             // check providerState contains 'AwaitingIpAddress' while RecoveryPending,
1359             // otherwise not really ready
1360             /*
1361             if ($.inArray('RecoveryPending', providerStates) != -1 &&
1362                 $.inArray('AwaitingIpAddress', providerStates) == -1) {
1363                 return;
1364             }
1365             */
1366 
1367             // clear provider state history
1368             //providerStates = [];
1369 
1370             // call success callback only if registering phone
1371             if (registering.registeringPhone || registering.switchingMode) {
1372                 registering.registeringPhone = false;
1373                 registering.switchingMode = false;
1374 
1375                 // finish registering
1376 
1377                 if (registering.successCb) {
1378                     try {
1379                         registering.successCb($.extend({}, registration, {
1380                             cucm: $.makeArray(registering.CUCM),
1381                             password: registering.password,
1382                             mode: _plugin.api.mode,
1383                             successfulCucm: {
1384                                 tftp: _plugin.api.successfulTftpAddress,
1385                                 cti: _plugin.api.successfulCtiAddress
1386                             }
1387                         }));
1388                     } catch(successException) {
1389                         _log('Exception occurred in application success callback',successException);
1390                         if(typeof console !== "undefined" && console.trace) {
1391                             console.trace();
1392                         }
1393                     }
1394                 }
1395                 else {
1396                     _log('warning: no registerPhone success callback');
1397                 }
1398             }
1399 
1400             event.phone.ready = true;
1401             $this.trigger(event);
1402             var calls = _plugin.api.getCalls();
1403             for(var i=0;i<calls.calls.length;i++) {
1404                 _triggerConversationEvent($this, calls.calls[i], 'state');
1405             }
1406         }
1407         else if (state === 'eIdle') {
1408             if(registering.unregisterCb) {
1409                 registering.unregisterCb();
1410             }
1411             $this.trigger(event);
1412         }
1413         else {
1414             $this.trigger(event);
1415         }
1416     } // end of _triggerProviderEvent
1417 
1418     function _registerSystemCallbacks($this) {
1419         _addListener(_plugin.api,"connectionstatuschange",function(_state) {
1420             _triggerProviderEvent($this,_state);
1421         });
1422         _addListener(_plugin.api,"connectionfailure",function(_status) {
1423             var errorKey = getError(_status, 'LoginError');
1424             _log(true, _status,errorKey);
1425             _triggerError($this, registering.errorCb, errorKey, _status, { registration: registration });
1426         });
1427         _addListener(_plugin.api,"authenticationresult",function(result) {
1428             _triggerAuthenticationResult($this,result);
1429         });
1430          _addListener(_plugin.api,"authenticationstatuschange",function(result) {
1431             _log(true, 'authenticationStatus '+result);
1432             //_triggerAuthenticationResult($this,result);
1433         });
1434          _addListener(_plugin.api,"callcontrolmodechange",function(result) {
1435 			if(result.phoneMode !== 'NoMode') {
1436 				registration.mode = result.phoneMode;
1437 				_triggerProviderEvent($this, 'ePhoneModeChanged');
1438 			}
1439             _log(true, 'callcontrolmodechange. Mode: ' + result.phoneMode + ', deviceName: ' + result.deviceName + ', DN: ' + result.lineDN );
1440         });
1441 
1442 
1443     }
1444     function _registerCallChangeCallbacks($this) {
1445         // subscribe to call updates
1446 
1447         _addListener(_plugin.api,"callstatechange",function(call) {
1448             _triggerConversationEvent($this,call,'state');
1449         });
1450 
1451         _addListener(_plugin.api, "videoresolutionchange", function(params) {
1452             _log(true, 'video resolution change detected for call ' + params.callId +
1453                  '. Height: ' + params.height +
1454                  ', width: ' + params.width, params);
1455 
1456             // trigger a conversation event with a 'videoResolution' property
1457             _triggerConversationEvent($this, {
1458                 callId: params.callId,
1459                 videoResolution: {
1460                     width: params.width,
1461                     height: params.height
1462                 }
1463             },
1464             'render');
1465         });
1466     }
1467 
1468     /**
1469      * Switch mode on a session that's already authorised <br>
1470      * @example
1471      * $('#phone').cwic('switchMode',{
1472      *     success: function(registration) { console.log("Phone is in "+registration.mode+" mode"); },
1473      *     error: function(err) { console.log("Error: "+error.message+" while switching mode"); },
1474      *     mode: "DeskPhone",
1475      *     device: "SEP01234567"
1476      *     });
1477      * @param options
1478      * @param {Function} [options.progress] A handler called when the mode switch has passed pre-conditions.<br>If specified, the handler is called when the switchMode operation starts.
1479      * @param {Function} [options.success] A handler called when mode switch complete with registration as a parameter
1480      * @param {Function} [options.error] A handler called when the mode switch fails on pre-conditions.
1481      * @param {Function} [options.mode] The new mode 'SoftPhone'/'DeskPhone'. defaults to SoftPhone
1482      * @param {Function} [options.device] Name of the device (e.g. SEP012345678, CSFUSER) to control. defaults to picking first available
1483      * @param {Function} [options.line] Phone number of a line valid for the specified device (e.g. '0000'). defaults to picking first available
1484      * @param {Boolean} [options.forceRegistration] Specifies whether to forcibly unregister other softphone instances with CUCM. Default is false. If you use false and your CUCM device is already in use by another client, the error callback will be invoked with an eDeviceAlreadyRegistered error. Note that this feature requires CUCM 9.0 and currently is only supported on Windows. See GracefulRegistration doc for more info.
1485      */
1486     function switchPhoneMode(options) {
1487         var ret;
1488         var $this = this;
1489         registering.successCb = $.isFunction(options.success) ? options.success : null;
1490         registering.errorCb = $.isFunction(options.error) ? options.error : null;
1491         registering.switchingMode = true;
1492 
1493         //TODO: {phoneMode: registration.mode, deviceName: _predictDevice({username: registration.user}),lineDN: ""}
1494         var mode = options.mode || 'SoftPhone';
1495         var device = options.device || (options.mode === 'SoftPhone'? _predictDevice({username: registration.user}) : '');
1496         var linedn = options.line || '';
1497         var forceRegistration =  options.forceRegistration || false;
1498         _log('About to call switchMode with mode: ' + mode + ', deviceName: ' + device + ', lineDn: ' + linedn);
1499         ret = _plugin.api.switchMode({phoneMode: mode, deviceName: device, lineDN: linedn, forceRegistration: forceRegistration});
1500 
1501         if (ret.error) {
1502             if(options.error && $.isFunction(options.error)) {
1503                 _triggerError($this, options.error, getError(ret), {message: ret});
1504             }
1505         } else {
1506             if (options.progress && $.isFunction(options.progress)) {
1507                 try {
1508                     options.progress({message: ret});
1509                 } catch(progressException) {
1510                     _log('Exception occurred in application switchPhoneMode progress callback',progressException);
1511                     if(typeof console !== "undefined" && console.trace) {
1512                         console.trace();
1513                     }
1514                 }
1515                 
1516             }
1517         }
1518 
1519         return this;
1520     }
1521 
1522     /**
1523      * Register phone to CUCM (SIP register)
1524      * @param args A map with:
1525      * @param {String} args.user The CUCM end user name (required)
1526      * @param {String|Object} args.password String - clear password. Object - {encrypted: encoded password, cipher:"cucm"}
1527      * @param {boolean} [args.authenticate] A flag to specify if user should be authenticated against CUCM (optional).
1528      * If the user is already authenticated then the application has the option to bypass this additional authentication against Cisco Unified Communications Manager. Authentication can be made against the CCMCIP interface of CUCM (HTTP Basic).
1529      * This additional authentication requires a server-side component to be deployed (see the node parameters of the init function).
1530      * @param {String|Object|Array} args.cucm The list of CUCM(s) to attempt to register with (required).<br>
1531      * If String, it will be used as both the CCMCIP and TFTP address.<br>
1532      * If Array, a list of String or Object as described above.
1533      * @param {String[]} args.cucm.tftp TFTP addresses
1534      * @param {String[]} [args.cucm.ccmcip] CCMCIP address (will use tftp values if not present).
1535      * @param {String} [args.mode]  Register the phone in this mode.  Available modes are "SoftPhone" or "DeskPhone".Default of intelligent guess is applied after a device is selected.<br>
1536      * @param {Function} [args.devicesAvailable(devices)] Callback called after successful authentication.
1537      * If this callback is not specified, cwic applies the default device selection algorithm.  An array of {@link device} objects is passed so the application can select the device.<br>
1538      * Return one of:<ul><li>device object to register with that device</li><li>null to fall back to default selection algorithm</li><li>false to stop registration (raises an error).</li></ul>
1539      * @param {Function} [args.error(err)] Callback called if the registration fails.
1540      * @param {Boolean} args.useCcmcip Authenticate using ccmcip (overrides settings if present).
1541      * @param {Boolean} args.forceRegistration Specifies whether to forcibly unregister other softphone instances with CUCM. Default is false. If you use false and your CUCM device is already in use by another client, the error callback will be invoked with an eDeviceAlreadyRegistered error. Please note that this feature requires CUCM 9.0 and currently is only supported on Windows. See GracefulRegistration doc for more info.
1542      * @param {Function} [args.success(registration)] Callback called when registration succeeds. A {@link registration} object is passed to the callback:
1543      * registerPhone examples <br>
1544      * @example
1545      * // *************************************
1546      * // register with lab CUCM in default mode (SoftPhone)
1547      * jQuery('#phone').cwic('registerPhone', {
1548      *     user: 'fbar',
1549      *     password: 'secret', // clear password
1550      *     cucm: '1.2.3.4',
1551      *     success: function(registration) {
1552      *         console.log('registered in mode ' + registration.mode);
1553      *         console.log('registered with device ' + registration.device.name);
1554      *     }
1555      * });
1556      * @example
1557      * // *************************************
1558      * // register with Alpha CUCM in DeskPhone mode
1559      * jQuery('#phone').cwic('registerPhone', {
1560      *     user: 'fbar',
1561      *     password: {
1562      *         encoded: 'GJH$&*"@$%$^BLKJ==',
1563      *         cipher: 'cucm'
1564      *     },
1565      *     mode: 'DeskPhone',
1566      *     cucm: '1.2.3.4',
1567      *     success: function(registration) {
1568      *         console.log('registered in mode ' + registration.mode);
1569      *         console.log('registered with device ' + registration.device.name);
1570      *     }
1571      * );
1572      * @example
1573      * // *************************************
1574      * // register with Alpha CUCM in SoftPhone mode, select ECP{user} device
1575      * jQuery('#phone').cwic('registerPhone', {
1576      *     user: 'tvanier',
1577      *     password: {
1578      *         encoded: 'GJH$&*"@$%$^BLKJ==',
1579      *         cipher: 'cucm'
1580      *     },
1581      *     mode: 'SoftPhone',
1582      *     cucm: {
1583      *         ccmcip: ['1.2.3.4'],
1584      *         tftp: ['1.2.3.5']
1585      *     },
1586      *     devicesAvailable: function(devices) {
1587      *         for (var i = 0; i < devices.length; i++) {
1588      *             var device = devices[i];
1589      *             if (device.name.match(/^ECP/i)) {
1590      *                 return device;
1591      *             } // starts with 'ECP'
1592      *         }
1593      *         return false; // stop registration if no ECP{user} device found
1594      *     },
1595      *     success: function(registration) {
1596      *         console.log('registered in mode ' + registration.mode);
1597      *         console.log('registered with device ' + registration.device.name);
1598      *     },
1599      *     error: function(status) {
1600      *         console.log('cannot register phone: ' + status);
1601      *     }
1602      * );
1603      */
1604     function registerPhone(args) {
1605         _log(true, 'registerPhone', arguments);
1606 
1607         var $this = this;
1608 		
1609 		if(!about().capabilities['multireg'] && typeof args.forceRegistration !== "undefined") {
1610 			_log('Warning: Attempting to use forceRegistration parameter on unsupported platform. Parameter will be ignored.');
1611 		}
1612 
1613         // flag to indicate cwic is in the process of registering a phone
1614         registering.registeringPhone = true;
1615 
1616         // reset global registration object
1617         registration = {
1618             user: args.user,
1619             mode: args.mode || "SoftPhone",
1620             devices: {},
1621             forceRegistration: args.forceRegistration || false,
1622             authenticate: typeof args.authenticate === 'boolean' ? args.authenticate : false
1623         };
1624 
1625         // validate password and make it an object
1626         var password = args.password;
1627         var clearPassword = '';
1628 
1629         var devicesAvailableCb = $.isFunction(args.devicesAvailable) ? args.devicesAvailable : null;
1630 
1631         registering.successCb = $.isFunction(args.success) ? args.success : null;
1632         registering.errorCb = $.isFunction(args.error) ? args.error : null;
1633         registering.CUCM = args.cucm;
1634         registration.useCcmcip = false;
1635         if(typeof args.useCcmcip !== "undefined") {
1636             registration.useCcmcip = args.useCcmcip;
1637         } else {
1638             registration.useCcmcip = args.useCcmcip;
1639         }
1640 
1641         if (!_plugin) {
1642             //TODO: remove hardcoded string
1643             return _triggerError($this, registering.errorCb, errorMap.PluginNotAvailable, 'Plugin is not available or has not been initialized', { registration: registration });
1644         }
1645 
1646         if (typeof password === "string") {
1647             // clear password, encrypt it
1648             password = { cipher: 'cucm', encrypted: _plugin.api.encryptCucmPassword(args.password) };
1649             if (registration.authenticate) { clearPassword = args.password; }
1650         } else if (typeof password !== "object" || (password.cipher !== "cucm" && password.cipher !== "base64"))  {
1651             return _triggerError($this, registering.errorCb, errorMap.InvalidArguments, 'invalid password (type ' + typeof password + ')', { registration: registration });
1652         }
1653 
1654         // make preferredDevice a string (possibly empty)
1655         var preferredDevice = args.device || _plugin.api.PreferredDevice;
1656         if (typeof preferredDevice === "object") {
1657             preferredDevice = (preferredDevice.name ? preferredDevice.name : '');
1658         }
1659 
1660         // make preferredLine a string (possibly empty)
1661         var preferredLine = args.line || _plugin.api.PreferredLine;
1662         if (typeof preferredLine === "object") {
1663             preferredLine = (preferredLine.directoryNumber ? preferredLine.directoryNumber : '');
1664         }
1665 
1666         // build an array of strings (CUCM addresses)
1667         var cucm = [];
1668         var ccmcip = [];
1669 
1670         $.each($.makeArray(args.cucm), function(i, elem) {
1671             if (typeof elem === "string") {
1672                 // cucm string can be 'lab call manager 1.2.3.4'
1673                 var a = elem.split(' ');
1674                 cucm.push(a[a.length-1]);
1675             }
1676             else if (typeof elem === "object") {
1677                 if ($.isArray(elem.ccmcip)) { ccmcip = elem.ccmcip; }
1678                 if ($.isArray(elem.tftp)) { cucm = elem.tftp; }
1679 
1680                 if (!$.isArray(elem.ccmcip) && !$.isArray(elem.tftp)) {
1681                     _log('registerPhone: no ccmcip and tftp properties for cucm');
1682                 }
1683             }
1684             else {
1685                 _log('registerPhone: ignoring cucm argument of type ' + typeof elem);
1686             }
1687         });
1688 
1689         _log('registerPhone: ' + cucm.length + ' cucm addresses');
1690         _log(true, cucm);
1691 
1692         _log("registerPhone of user=" + registration.user +
1693              ' (authenticate=' + registration.authenticate + ') in mode="' + registration.mode + '"');
1694 
1695         // reset first
1696         //_reset();
1697 
1698         if (!registration.user || registration.user === '') {
1699             return _triggerError($this, registering.errorCb, errorMap.InvalidArguments, 'Missing user name', { registration: registration });
1700         }
1701 
1702         if (!$.isArray(cucm) || cucm.length < 1) {
1703             return _triggerError($this, registering.errorCb, errorMap.NoCallManagerConfigured, 'Missing CUCM address', { registration: registration });
1704         }
1705 
1706         if (!registration.mode.match(/^(SoftPhone|DeskPhone)$/)) {
1707             return _triggerError($this, registering.errorCb, errorMap.InvalidArguments, 'Invalid phone mode "' + registration.mode + '"', { registration: registration });
1708         }
1709 
1710         //_plugin.api.PreferredPhoneMode = registration.mode;
1711 
1712 
1713         _plugin.api.TftpAddressList = cucm;
1714         _plugin.api.CtiAddressList = cucm;
1715         _plugin.api.CcmcipAddressList = (ccmcip.length > 0)? ccmcip : cucm;
1716 
1717         // keep track of previous states
1718         //var providerStates = [];
1719 
1720 
1721         // is the plugin already ready ?
1722         var currState = _plugin.api.connectionStatus;
1723         if (currState === 'eReady') {
1724             _triggerProviderEvent($this,currState);
1725         }
1726         registering.password = password;
1727         if (!registration.authenticate || registration.mode === "DeskPhone") {
1728 
1729             if (registration.mode === "DeskPhone" && (!preferredDevice || !preferredDevice.match(/^\s*SEP/i))) {
1730                 preferredDevice = '';
1731                 preferredLine = '';
1732             }
1733 
1734             //_plugin.api.PreferredDevice = preferredDevice;
1735             //_plugin.api.PreferredLine = preferredLine;
1736 
1737             if(currState !== 'eReady') {
1738                 // CUCM user password is encrypted
1739                 var ret = "Did nothing";
1740                 if(password.encrypted) {
1741                     registering.authenticatedCallback = function() {
1742                         /*
1743                          * name
1744                          * description
1745                          * model
1746                          * modelDescription
1747                          * isSoftPhone
1748                          * isDeskPhone
1749                          * lineDNs[]
1750                          * serviceState
1751                          */
1752                         var ret = _plugin.api.getAvailableDevices();
1753                         var _devices = ret.devices;
1754                         _updateRegistration(currState);
1755                         if(devicesAvailableCb) {
1756                             try {
1757                                 devicesAvailableCb(_devices,registration.mode,function(phoneMode,deviceName,lineDN) {
1758                                     var res = _plugin.api.connect({phoneMode: phoneMode, deviceName: deviceName, lineDN: lineDN, forceRegistration: registration.forceRegistration});
1759                                     if(res.error) {
1760                                         var error = getError(res.error);//errorMap[res.error] ? errorMap[res.error] : errorMap.LoginError;
1761                                         return _triggerError($this, registering.errorCb, error, res.error, { registration: registration });
1762                                     }
1763                                 });
1764                             } catch(devicesAvailableException) {
1765                                 _log('Exception occurred in application devicesAvailable callback',devicesAvailableException);
1766                                 if(typeof console !== "undefined" && console.trace) {
1767                                     console.trace();
1768                                 }
1769                             }
1770                         } else {
1771                             var deviceName = "";
1772                             // If user has specified a device, use it to connect....
1773                             if(preferredDevice) {
1774                                 deviceName = preferredDevice;
1775                             } else { //....otherwise, use the first available one
1776                                 for(var i=0;i<_devices.length;i++) {
1777                                     if(registration.mode==="SoftPhone" && _devices[i].isSoftPhone) {
1778                                         deviceName = _devices[i].name;
1779                                         break;
1780                                     }
1781                                     if(registration.mode==="DeskPhone" && _devices[i].isDeskPhone) {
1782                                         deviceName = _devices[i].name;
1783                                         break;
1784                                     }
1785                                 }
1786                             }
1787                             var res = _plugin.api.connect({phoneMode: registration.mode, deviceName: deviceName, lineDN: '', forceRegistration: registration.forceRegistration});
1788                             if(res.error) {
1789                                 var error = getError(res.error);//errorMap[res.error] ? errorMap[res.error] : errorMap.LoginError;
1790                                 return _triggerError($this, registering.errorCb, error, res.error, { registration: registration });
1791                             }
1792                         }
1793 
1794                         registering.authenticatedCallback = null;
1795                         delete registering.authenticatedCallback;
1796                     };
1797                     ret = _plugin.api.authenticate({username:registration.user, password:password.encrypted, useCcmcip: settings.useCcmcip});
1798                 } else {
1799                     ret = _plugin.api.connect({phoneMode: registration.mode, deviceName: _predictDevice({username: registration.user}),lineDN: "", forceRegistration: registration.forceRegistration});
1800                 }
1801                 if(ret.error) {
1802                     var error = getError(ret.error);//errorMap[ret['Error']] ? errorMap[ret['Error']] : errorMap.LoginError;
1803                     return _triggerError($this, registering.errorCb, error, ret.error, { registration: registration });
1804                 }
1805             }
1806         }
1807         else {
1808             // need to authenticate against CUCM here until the plugin supports it
1809 
1810             // to be called for each CUCM address until success
1811             var cucmAddresses = [].concat(cucm);
1812 
1813             var authenticateCcmcip = function() {
1814 
1815                 var cucmAddress = cucmAddresses.shift();
1816 
1817                 if (typeof cucmAddress === "undefined") {
1818                     _triggerError($this, registering.errorCb, errorMap.NoCucmFound, 'cannot register phone', { registration: registration });
1819                     return;
1820                 }
1821 
1822                 _log(true, 'authenticate against CUCM "' + cucmAddress + '"');
1823 
1824                 $.ajax({
1825                     url: settings.node + '/phoneconfig/devices' + '?ccmcip=' + cucmAddress,
1826                     beforeSend : function(req) {
1827                         var auth = 'Basic ' + _encodeBase64(registration.user + ':' + clearPassword);
1828                         req.setRequestHeader('Authorization', auth);
1829                     },
1830                     error: function(jqXHR, textStatus, errorThrown) {
1831                         _log(true, 'CCMCIP failure with CUCM "' + cucmAddress + '" : ' + textStatus, errorThrown);
1832                         authenticateCcmcip(); // try next CUCM address
1833                     },
1834                     success: function(devices) {
1835                         registration.cucm = {
1836                             ccmcip: [cucmAddress],
1837                             tftp: [cucmAddress]
1838                         };
1839 
1840                         var selectedDevice = null;
1841                         var defaultDevice = null;
1842 
1843                         // default device selection algorithm
1844                         $.each(devices, function(i, device) {
1845 
1846                             _log(true, 'device=' + device.name + ' model="' + device.model + '"');
1847 
1848                             if (typeof device.name === "undefined" || typeof device.model === "undefined") {
1849                                 // ignore this device and continue
1850                                 return;
1851                             }
1852 
1853                             if (device.model.match(/^\s*Cisco\s+Unified\s+Client\s+Services\s+Framework\s*$/i)) {
1854                                 device.csf = true;
1855 
1856                                 // in SoftPhone mode, select a CSF device only
1857                                 if (registration.mode === "SoftPhone") {
1858                                     if (device.name === preferredDevice) {
1859                                         selectedDevice = device;
1860                                     }
1861 
1862                                     // select a device whose name starts with ECP in priority, first CSF device otherwise
1863                                     if (device.name.match(/^ECP/i) || !defaultDevice) {
1864                                         defaultDevice = device;
1865                                     }
1866                                 }
1867                             }
1868 
1869                             if (device.name.match(/^\s*SEP/i)) {
1870                                 device.deskphone = true;
1871 
1872                                 if (registration.mode === "DeskPhone") {
1873                                     if (device.name === preferredDevice) {
1874                                         selectedDevice = device;
1875                                     }
1876 
1877                                     // in DeskPhone mode, select first device whose name starts with SEP
1878                                     if (!defaultDevice) {
1879                                         defaultDevice = device;
1880                                     }
1881                                 }
1882                             }
1883 
1884                             registration.devices[$.trim(device.name)] =
1885                                 $.extend({}, registration.devices[$.trim(device.name)], device);
1886                         });
1887 
1888                         _log(true, 'default device is ' + (defaultDevice === null ? defaultDevice : defaultDevice.name));
1889 
1890                         if (devicesAvailableCb) {
1891                             // application wants to select device by itself
1892                             try {
1893                                 selectedDevice = devicesAvailableCb(devices, registration.mode);
1894                             } catch(devicesAvailableException) {
1895                                 _log('Exception occurred in application devicesAvailable callback',devicesAvailableException);
1896                                 if(typeof console !== "undefined" && console.trace) {
1897                                     console.trace();
1898                                 }
1899                             }
1900                                 
1901 
1902                             // false means stop registering (null means use default selection)
1903                             if (selectedDevice === false) {
1904                                 _log('registration interrupted');
1905                                 return;
1906                             }
1907                         }
1908 
1909                         if (selectedDevice === null) {
1910                             selectedDevice = defaultDevice;
1911                         }
1912 
1913                         if (!selectedDevice) {
1914                             // TODO: remove hardcoded string
1915                             return _triggerError($this, registering.errorCb, errorMap.NoDevicesFound, 'no device found', { registration: registration });
1916                         }
1917 
1918                         if ((registration.mode !== "SoftPhone" && selectedDevice.csf) ||
1919                             (registration.mode !== "DeskPhone" && selectedDevice.deskphone)) {
1920                             // TODO: remove hardcoded string, convert error string to map of mode, deviceName
1921                             return _triggerError($this, registering.errorCb, errorMap.NoDevicesFound,
1922                                 'cannot register in ' + registration.mode + ' mode with device "' + selectedDevice.name + '"', { registration: registration });
1923                          }
1924 
1925                         _log('selected device is "' + selectedDevice.name +'"');
1926 
1927                         _plugin.api.PreferredDevice = selectedDevice.name;
1928 
1929                         registration.device = selectedDevice;
1930 
1931                         _plugin.api.connect({phoneMode: registration.mode, deviceName: _plugin.api.PreferredDevice, lineDN: '', forceRegistration: registration.forceRegistration});
1932                     } // success
1933                 }); // $.ajax
1934 
1935             }; // function authenticate
1936 
1937             authenticateCcmcip();
1938         }
1939 
1940         return $this;
1941     } // end of registerPhone
1942 
1943     /** <br>
1944      * Unregisters a phone from CUCM:<ul>
1945      * <li>Ends any active call if this is the last instance or forceLogout is set to true.</li>
1946      * <li>Unbinds all cwic event handlers</li>
1947      * <li>In softphone mode, SIP unregisters, in deskphone mode, closes the CTI connection.</li>
1948      * <li>Calls the optional complete handler (always called)</li></ul>
1949      * @param args Is a set of key/value pairs to configure the phone unregistration.
1950      * @param {function} [args.complete] Callback called when unregistration is complete.<br>
1951      * If specified, the handler is always called, even if the phone was not registered first, or if the unregistrations caused errors.
1952      * @param {boolean} args.forceLogout: If true, end the phone session even if registered in other instances.
1953      * unregisterPhone examples
1954      * @example
1955      * // *************************************
1956      * // unregister phone
1957      * jQuery('#phone')
1958      *     .unbind('.cwic')             // optional, done by unregisterPhone
1959      *     .cwic('unregisterPhone', {
1960      *         complete: function() {
1961      *             console.log('phone is unregistered');
1962      *         }
1963      * });
1964      */
1965     function unregisterPhone() {
1966         _log(true, 'unregisterPhone', arguments);
1967 
1968         var $this = this;
1969 
1970 
1971         if(typeof arguments[0] === "object" && typeof arguments[0].forceLogout !== 'undefined' && arguments[0].forceLogout) {
1972             _plugin.api.logout();
1973 
1974             // reset global registration object
1975             registration = {devices:{}};
1976         }
1977 
1978         _reset();
1979 
1980         if (typeof arguments[0] === "object" && typeof arguments[0].complete !== "undefined") {
1981             // call complete callback
1982             var complete = arguments[0].complete;
1983             registering.unregisterCb = function() {
1984                 try {
1985                     complete();
1986                 } catch(completeException) {
1987                     _log('Exception occurred in application unregister complete callback',completeException);
1988                     if(typeof console !== "undefined" && console.trace) {
1989                         console.trace();
1990                     }
1991                 }
1992                 
1993                 registering.unregisterCb = null;
1994             };
1995         }
1996 
1997         return $this;
1998     }
1999 
2000     function _reset() {
2001         // clear all cwic data
2002         $('.cwic-data').removeData('cwic');
2003 
2004         // unbind/unregister event callbacks ?
2005     }
2006 
2007     function _triggerConversationEvent($this, conversation, topic) {
2008         var conversationId = conversation.callId;
2009         var conversationState = conversation.callState;
2010 
2011 
2012         // determine participant name and number
2013         var participant = {};
2014         // TODO: handle "Forwarded" and possibly "None"
2015         if (conversation.callType === "Outgoing") {
2016             participant.name = conversation.calledPartyName;
2017             participant.recipient =
2018                 conversation.calledPartyDirectoryNumber !== "" ? conversation.calledPartyDirectoryNumber : conversation.calledPartyNumber;
2019         }
2020 
2021         if (conversation.callType === "Incoming") {
2022             participant.name = conversation.callingPartyName;
2023             participant.recipient =
2024                 conversation.callingPartyDirectoryNumber !== "" ? conversation.callingPartyDirectoryNumber : conversation.callingPartyNumber;
2025         }
2026 
2027         // select the conversation container with class cwic-conversation-{conversationId}
2028         var container = $('.cwic-conversation-' + conversationId);
2029 
2030         // if no container, select the outgoing conversation (see startConversation)
2031         if (container.length === 0) {
2032             container = $('.cwic-conversation-outgoing');
2033 
2034             // in deskphone mode, container may not exist yet if conversation was initiated from deskphone
2035             //if (container.length == 0 && conversation.callType == "Outgoing") {
2036                 //container = $('<div>').addClass('cwic-conversation-outgoing');
2037             //}
2038         }
2039 
2040         // at this point container may be empty, which means the conversation is incoming
2041 
2042         var data = container.data('cwic') || {};
2043 
2044         _log(true, 'conversation id=' + conversationId + ' state=' + conversation.callState || data.state,conversation);
2045 
2046         // extend conversation
2047         conversation = $.extend({}, data, conversation, {
2048             id: conversationId,
2049             state: conversationState || data.state,
2050             media: 'audio',
2051             participant: $.extend(data.participant, participant)
2052         });
2053 
2054         /* ECC call states and old skittles/webphone states
2055             OnHook : Disconnected
2056             OffHook : Created
2057             Ringout : RemotePartyAlerting
2058             Ringin : Alerting
2059             Proceed : Ringin on Deskphone while on a call amongst others
2060             Connected : Connected
2061             Hold : Held
2062             RemHold : "Passive Held"
2063             Resume : ?
2064             Busy : n/a (connected)
2065             Reorder : Failed
2066             Conference : n/a
2067             Dialing : Dialing
2068             RemInUse : "Passive not held"
2069             HoldRevert : n/a
2070             Whisper : n/a
2071             Parked : n/a
2072             ParkRevert : n/a
2073             ParkRetrieved : n/a
2074             Preservation : n/a
2075             WaitingForDigits : na/ ? Overlapdial capability ?
2076             Spoof_Ringout : n/a
2077         */
2078         // check for an incoming call - based on the following arcane conditions:
2079         // Empty container and one of the following:
2080         // *  Incoming/Created = ringing
2081         // *  Incoming/Proceed = ringing (cti mode, already on a call)
2082         // *  Ringin = ringing
2083         if ((((conversation.state === "Proceed" || conversation.state === "Created") && conversation.callType==="Incoming")|| conversation.state === "Ringin") && container.length === 0) {
2084             // new container for incoming call, application is supposed to attach it to the DOM
2085             container = $('<div>').addClass('cwic-data cwic-conversation cwic-conversation-' + conversationId).data('cwic', conversation);
2086             $this.trigger('conversationIncoming.cwic', [conversation, container]);
2087             return;
2088         }
2089 
2090         // If we can originate a call, onHook does not mean the call has ended - it means it's just about to start
2091         else if ((conversation.state === "OnHook" && !conversation.capabilities.canOriginateCall) || !conversation.exists) {
2092             videowindowsbycall.deleteCall({callId: conversationId});
2093             if (container.length === 0) {
2094                 _log('warning: no container for ended conversation ' + conversationId);
2095                 $this.trigger('conversationEnd.cwic', [conversation]);
2096                 return;
2097             }
2098 
2099             container
2100                 .removeData('cwic')
2101                 .removeClass('cwic-data cwic-conversation cwic-conversation-' + conversation.id)
2102                 .trigger('conversationEnd.cwic', [conversation]);
2103             return;
2104         }
2105 
2106         else {
2107             if (conversation.state === "OffHook" || conversation.state === "Connected") {
2108 
2109                 // store media connection time
2110                 if (typeof conversation.connect === "undefined" && conversation.state === "Connected") {
2111                     if (container.length === 0) {
2112                         container = $('<div>').addClass('cwic-conversation cwic-conversation-' + conversationId);
2113                     }
2114                     $.extend(conversation, { connect: new Date() });
2115                     container.data('cwic', conversation);
2116                 }
2117 
2118                 // store start time and trigger start event only once
2119                 if (typeof conversation.start === "undefined") {
2120                     if (container.length === 0) {
2121                         container = $('<div>');
2122                     }
2123                     $.extend(conversation, { start: new Date() });
2124                     container.data('cwic', conversation);
2125 
2126                     container
2127                         .removeClass('cwic-conversation-outgoing')
2128                         .addClass('cwic-conversation cwic-conversation-' + conversationId)
2129                         .data('cwic', conversation);
2130 
2131                     $this.trigger('conversationStart.cwic', [conversation, container]);
2132                     return;
2133                 }
2134             }
2135 
2136             if (container.length === 0) {
2137                 // if we've just switched to deskphone mode and there's already a call, create a container div
2138                 // or if we've just opened a new tab, we also need to trigger a conversation start for an ongoing call
2139                 container = $('<div>').data('cwic',conversation).addClass('cwic-conversation cwic-conversation-' + conversationId);
2140                 if(conversation.exists) {
2141                     $this.trigger('conversationStart.cwic', [conversation, container]);
2142                     return;
2143                 } else {
2144                     $this.trigger('conversationUpdate.cwic', [conversation, container]); // trigger update event
2145                     return;
2146                 }
2147                 _log('warning: no container for updated conversation ' + conversationId);
2148             } else {
2149                 container.data('cwic',conversation);
2150             }
2151 
2152             videowindowsbycall.update({callId: conversationId});
2153             container.trigger('conversationUpdate.cwic', [conversation, container]); // trigger update event
2154         }
2155 
2156     } // function _triggerConversationEvent
2157 
2158     /**
2159     * _triggerError(target, [callback], [code], [data]) <br>
2160     * <br>
2161     * - target (Object): a jQuery selection where to trigger the event error from <br>
2162     * - callback (Function): an optional callback to be called call with the error. if specifed, prevents the generic error event to be triggered <br>
2163     * - code (Number): an optional cwic error code (defaults to 0 – Unknown) <br>
2164     * - data (String, Object): some optional error data, if String, used as error message. if Object, used to extend the error. <br>
2165     * <br>
2166     * cwic builds an error object with the following properties: <br>
2167     *  code: a pre-defined error code <br>
2168     *  message: the error message (optional) <br>
2169     *  any other data passed to _triggerError or set to errorMap (see the init function) <br>
2170     *  <br>
2171     * When an error event is triggered, the event object is extended with the error properties. <br>
2172     * <br>
2173     */
2174     function _triggerError() {
2175         var $this = arguments[0]; // target (first mandatory argument)
2176         var errorCb = null;
2177 
2178         // the default error
2179         var error = $.extend({ details: [] }, errorMap.Unknown);
2180 
2181         // extend error from arguments
2182         for (var i=1; i<arguments.length; i++) {
2183             var arg = arguments[i];
2184 
2185             // is the argument a specific error callback ?
2186             if ($.isFunction(arg)) { errorCb = arg; }
2187 
2188             else if (typeof arg === "string") { error.details.push(arg); }
2189 
2190             else if (typeof arg === "object") { $.extend(error, arg); }
2191 
2192         } // for
2193 
2194         _log(error.message, error);
2195 
2196         // if specific error callback, call it
2197         if (errorCb) {
2198             try {
2199                 errorCb(error);
2200             } catch(errorException) {
2201                 _log('Exception occurred in application error callback',errorException);
2202                 if(typeof console !== "undefined" && console.trace) {
2203                     console.trace();
2204                 }
2205             }
2206             
2207         }
2208         else {
2209             // if no specific error callback, raise generic error event
2210             var event = $.Event('error.cwic');
2211             $.extend(event, error);
2212             $this.trigger(event);
2213         }
2214 
2215         return $this;
2216     }
2217 
2218     /**
2219      * @param {Object} [conversation] Can be a new object to start a new conversation or an existing conversation which you wish to answer
2220      * @param {String} [conversation.id] Unique identifier of the conversation.
2221      * @param {Object|Array} conversation.participant Conversation participants, does not include the local user.  May be asingle participant Object or an array of participant Objects.
2222      * @param {String} conversation.participant.recipient The phone number or jabber id of the participant.
2223      * @param {String} [conversation.participant.name] The participant name.
2224      * @param {String} [conversation.participant.photosrc] A suitable value for the src attribute of an <img> element.
2225      * @param {String} [conversation.media] Media used to start the conversation.Contains the media used to join participants: audio, video or chat.<br>
2226      * If not specified, depends on all participant's recipients: if phone number, default media is "audio", if jabber id, default media is "chat".
2227      * @param {String} [conversation.state] Current state of the conversation. Can be OffHook, Ringing, Connected, OnHook, Reorder.
2228      * @param {Date} [conversation.start] Start time. Defined on resolution update only.
2229      * @param {Date} [conversation.connect] Media connection time. Defined on resolution update only.
2230      * @param {Object} [conversation.videoResolution] Resolution of the video conversation, contains width and height properties. Defined on resolution update only.
2231      * @param {String|Object} [conversation.container] The HTML element which contains the conversation (optional). Conversation events are triggered on this element.
2232      * If String, specifies a jQuery selector If Object, specifies a jQuery wrapper of matched elements(s).
2233      * By default container is $(this), that is the first element of the matched set startConversation is called on.
2234      * @param {String} [conversation.subject] The subject of the conversation to start.
2235      * @param {Function} [conversation.error(status)] A function to be called if the conversation cannot be started (optional). An error status (String) is passed.
2236      * @param {String} [conversation.videoDirection] The video media direction: 'Inactive' or undefined (audio only by default), 'SendOnly', 'RecvOnly' or 'SendRecv'.
2237      * @param {Object} [conversation.remoteVideoWindow] The video object (must be of mime type application/x-cisco-cwc-videocall).
2238      * @param {DOMWindow} [conversation.window] DOM window that contains the remoteVideoWindow (default to this DOM window) required if specifying a video object on another window (popup/iframe).
2239      * @description Start a conversation with a participant.
2240      * <br>If conversation contain both an id and a state property, cwic assumes you want to answer that incoming conversation, in this case starting the passed conversation means accepting(answering) it.
2241      * @example
2242      * // start an audio conversation with element #foo as container
2243      * jQuery('#phone').cwic('startConversation', {
2244      *   participant: {
2245      *     recipient: '1234'
2246      *   },
2247      *   container: '#foo'
2248      * });
2249      * // start an audio conversation with a contact (call work phone number)
2250      * jQuery('#conversation').cwic('startConversation', {
2251      *   participant: {
2252      *     recipient: '1234',
2253      *     displayName: 'Foo Bar',
2254      *     screenName: ' fbar',
2255      *     phoneNumbers: {
2256      *       work: '1234',
2257      *       mobile: '5678'
2258      *     }
2259      *   }
2260      * });
2261      * // start a room chat (ad-hoc)
2262      * jQuery('#conversation').cwic('startConversation', {
2263      *   participant: [
2264      *     'thomas@domain.com', 'niall@domain.com', 'david@domain.com',
2265      *     'martin@domain.com', 'jing@domain.com'
2266      *   ],
2267      *   media: 'chat',
2268      *   subject: 'The Cisco Jabber SDK looks great !',
2269      *   error: function(status) {
2270      *     console.log('chat cannot be started: ' + status);
2271      *   }
2272      * });
2273      * // answer an incoming conversation (input has an id property)
2274      * // see another example about the conversationIncoming event
2275      * jQuery('#conversation').cwic('startConversation', {
2276      *   participant: {
2277      *     recipient: '1234'
2278      *   },
2279      *   id: '612',
2280      *   state: 'Ringin'
2281      * });
2282      * // answer an incoming conversation with video
2283      * jQuery('#conversation').cwic('startConversation',
2284      *   jQuery.extend(conversation,{
2285      *   videoDirection: (sendingVideo ? 'SendRecv':''),
2286      *   remoteVideoWindow: 'remoteVideoWindow',  // pass id
2287      *   id: callId
2288      * }));
2289      * // answer an incoming conversation with video object hosted in popoutwindow
2290      * jQuery('#conversation').cwic('startConversation',
2291      *   jQuery.extend(conversation,{
2292      *   videoDirection: (sendingVideo ? 'SendRecv':''),
2293      *   remoteVideoWindow: $('#remoteVideoWindow', popoutwindow.document)[0] // pass object setting jQuery context to popoutwindow's document
2294      *   window: popoutwindow,
2295      *   id: callId
2296      * });
2297      * // answer an incoming conversation without video
2298      * jQuery('#callcontainer').cwic('startConversation', conversation);
2299     */
2300     function startConversation() {
2301         _log(true, 'startConversation', arguments);
2302 
2303         var $this = this;
2304 
2305         var callsettings = arguments[0] || $this.data('cwic') || {};
2306         var windowhandle,videoDirection;
2307 
2308         if ($this.length === 0) {
2309             // TODO: remove hardcoded string
2310             return _triggerError($this, callsettings.error, errorMap.InvalidArguments, 'cannot start conversation with empty selection');
2311         }
2312 
2313         // container is the jQuery wrapper of the video container
2314         var container = $this;
2315         if      (typeof callsettings.container === "string") { container = $(callsettings.container); }
2316         else if (typeof callsettings.container === "object") { container = callsettings.container; }
2317         container = container.first();
2318 
2319         if (typeof callsettings.id !== "undefined") {
2320             // start an incoming conversation
2321             container.addClass('cwic-data cwic-conversation cwic-conversation-' + callsettings.id).data('cwic', callsettings);
2322 
2323             if (arguments.length >= 1 ) {
2324                 videoDirection = callsettings.videoDirection;
2325                 if(callsettings.remoteVideoWindow) {
2326                     videowindowsbycall.add({callId: callsettings.id, plugin: callsettings.remoteVideoWindow});
2327                     if(callsettings.remoteVideoWindow.windowhandle) {
2328                         windowhandle = callsettings.remoteVideoWindow.windowhandle;
2329                     }
2330                 }
2331             } else {
2332                 videoDirection = "";
2333             }
2334 
2335             var answerObject = {
2336                 callId: callsettings.id,
2337                 videoDirection: videoDirection
2338             };
2339             if(windowhandle) {
2340                 answerObject.windowhandle = windowhandle;
2341             }
2342             _plugin.api.answer(answerObject);
2343         }
2344         else {
2345             // start an outgoing conversation
2346             var participant = callsettings.participant || {};
2347 
2348             if (typeof participant === "string") {
2349                 participant = { recipient: participant };
2350             }
2351 
2352             if (typeof participant.recipient === "undefined") {
2353                 // TODO: remove hardcoded string
2354                 return _triggerError($this, callsettings.error, errorMap.InvalidArguments, 'cannot start conversation: undefined or empty recipient');
2355             }
2356 
2357             container.addClass('cwic-data cwic-conversation cwic-conversation-outgoing').data('cwic', { participant: participant });
2358             //var call = _plugin.api.getCall({ callId: -1 });
2359 
2360             //if(_hasCapability(call,'OverlapDial')) {
2361                 //_plugin.api.overlapDial(participant.recipient);
2362             //} else {
2363                 if (container.is(':hidden')) {
2364                     _log(true, 'startConversation - warning: container is hidden');
2365                 }
2366 
2367                 // for now use jQuery dimensions and offset utilities
2368                 // TO DO: offset() does not work with hidden elements, and does not support margins/borders, see http://api.jquery.com/offset/
2369                 //var containerOffset = container.offset();
2370                 //var top = settings.top || containerOffset.top;
2371                 //var left = settings.left || containerOffset.left;
2372                 //var width = settings.width || container.width();
2373                 //var height = settings.height || container.height();
2374 
2375                 //_log(true, 'startConversation container top=' + top + ' left=' + left + ' width=' + width + ' height=' + height);
2376 
2377                 var originateObject = {
2378                     recipient: participant.recipient,
2379                     videoDirection: callsettings.videoDirection
2380                 };
2381                 if(callsettings.remoteVideoWindow && callsettings.remoteVideoWindow.windowhandle) {
2382                     originateObject.windowhandle = callsettings.remoteVideoWindow.windowhandle;
2383                 }
2384 
2385                 var res = _plugin.api.originate(originateObject);
2386 
2387                 if (res.error) {
2388                     _log(true, 'originate result', res);
2389                     // TODO: remove hardcoded string
2390                     _triggerError($this, callsettings.error, 'cannot start conversation', { nativeError: res.error });
2391                 }
2392                 if(res.callId && res.callId >=1) {
2393                     if(callsettings.remoteVideoWindow) {
2394                         callsettings.window = callsettings.window || window;
2395                         videowindowsbycall.add({callId: res.callId, plugin: callsettings.remoteVideoWindow, window: callsettings.window});
2396                     }
2397                 }
2398             //}
2399         }
2400 
2401         return $this;
2402     }
2403 
2404     /**
2405     * @description Ends a conversation. <br>
2406     * @param{boolean} iDivert: If it is a true, the conversation is diverted to the default recipient *(voicemail for example, sometimes referred as "iDivert", configured by the admin).
2407     * Triggers a conversationEnd event.
2408     * @param{String|Object} id A conversation identifier (String) or an Object containing an id property.<br>
2409     *  endConversation examples <br>
2410     *  @example
2411     *  // typeof input is string
2412     * jQuery('#phone').cwic('endConversation', '1234');
2413     *  // or
2414     * jQuery('#phone').cwic('updateConversation', conversation.id);
2415     *  // typeof input is object
2416     * jQuery('#phone').cwic('endConversation', conversation);
2417     *  // let cwic find the conversation data attached to #conversation
2418     * jQuery('#conversation').cwic('endConversation');
2419     *  // DIVERT
2420     * jQuery('#phone').cwic('endConversation', true, '1234');
2421     * jQuery('#myconversation').cwic('endConversation', true);
2422     *
2423     */
2424     function endConversation() {
2425         _log(true, 'endConversation', arguments);
2426 
2427         var $this = this;
2428 
2429         if ($this.length === 0) { return $this; }
2430 
2431         var iDivert = null;
2432         var conversation = null;
2433         var conversationId = null;
2434 
2435         if (arguments.length === 0) {
2436             conversation = $this.data('cwic');
2437             if(!conversation) {
2438                 // TODO: remove hardcoded string
2439                 return _triggerError($this, 'cannot end conversation: no conversation exists for this element');
2440             }
2441             conversationId = conversation.id;
2442         }
2443         else if (arguments.length === 1) {
2444             iDivert = typeof arguments[0] === "boolean" ? arguments[0] : null;
2445             conversation = typeof arguments[0] === "object" ? arguments[0] : $this.data('cwic');
2446             conversationId = typeof arguments[0] === "string" ? arguments[0] : conversation.id;
2447         }
2448         else if (arguments.length === 2) {
2449             iDivert = typeof arguments[0] === "boolean" ? arguments[0] : null;
2450             conversation = typeof arguments[1] === "object" ? arguments[1] : $this.data('cwic');
2451             conversationId = typeof arguments[1] === "string" ? arguments[1] : conversation.id;
2452         }
2453 
2454         if (!conversationId) {
2455             // TODO: remove hardcoded string
2456             return _triggerError($this, errorMap.InvalidArguments, 'cannot end conversation: undefined or empty conversation id');
2457         }
2458 
2459         if (iDivert) {
2460             // need to check capabilities first
2461             conversation = conversation || $('.cwic-conversation-' + conversationId).data('cwic');
2462 
2463             if (!conversation) {
2464                 // TODO: remove hardcoded string
2465                 return _triggerError($this, 'cannot iDivert - undefined conversation');
2466             }
2467 
2468             if(!conversation.capabilities || !conversation.capabilities.canImmediateDivert) {
2469                 // TODO: remove hardcoded string
2470                 return _triggerError($this, errorMap.MissingCapability, 'cannot iDivert - missing capability', { conversation: conversation });
2471             }
2472 
2473             _log(true, 'iDivert conversation', conversation);
2474 
2475             _plugin.api.iDivert({ callId: conversationId });
2476         }
2477         else {
2478             _log(true, 'end conversation', conversation);
2479             _plugin.api.endCall({ callId: conversationId });
2480         }
2481 
2482         return $this;
2483     }
2484     /**
2485     * @description Updates an existing conversation.<br>
2486     * This function controls the call allowing the following operations<ul>
2487     * <li>hold call</li>
2488     * <li>resume call</li>
2489     * <li>mute call</li>
2490     * <li>unmute call</li>
2491     * <li>mute audio only</li>
2492     * <li>mute video only</li>
2493     * <li>unmute audio only</li>
2494     * <li>unmute video only</li>
2495     * <li>add video window for remote sender</li>
2496     * <li>remove video window for remote sender</li>
2497     * <li>update video preference on a call video escalate/de-escalate</li>
2498     * <li>conference two calls together</li>
2499     * <li>transfer a call</li>
2500     * </ul>
2501     * @param {String|Object} update Update a started conversation. update can be: <br>
2502     * A String: hold, resume, mute, unmute, muteAudio, muteVideo, unmuteAudio, unmuteVideo.<br>
2503     * An Object: contains one or more writable conversation properties to update e.g. videoDirection.<br>
2504     * Triggers a conversationUpdate event.
2505     * @param {String|Object} id A conversation identifier (String) or Object containing an id property <br>
2506     * @example
2507     * // typeof input is string HOLD/RESUME
2508     * jQuery('#phone').cwic('updateConversation', 'hold', '1234')
2509     * jQuery('body').cwic('updateConversation', 'hold', conversation.id);
2510     * jQuery('#myid').cwic('updateConversation', 'hold', conversation);
2511     *   // typeof input is object
2512     * jQuery('#conversation').cwic('updateConversation', 'hold');
2513     *   // resume the same conversation,
2514     *   // let cwic find the conversation data attached to #conversation
2515     * jQuery('#conversation').cwic('updateConversation', 'resume');
2516     *   // MUTE/UNMUTE
2517     *   // typeof input is string
2518     * jQuery('#phone').cwic('updateConversation', 'mute', '1234');
2519     * jQuery('body').cwic('updateConversation', 'mute', conversation.id);
2520     * jQuery('#myid').cwic('updateConversation', 'mute', conversation);
2521     *   // typeof input is object <br>
2522     * jQuery('#conversation').cwic('updateConversation', 'mute');
2523     *   // unmute the same conversation,
2524     *   // let cwic find the conversation data attached to #conversation
2525     * jQuery('#conversation').cwic('updateConversation', 'unmute');
2526     *
2527     *  // add/remove video object in this (default) DOMWindow
2528     * jQuery('#conversation').cwic('updateConversation',
2529     *               { 'addRemoteVideoWindow':videoObject });
2530     * jQuery('#conversation').cwic('updateConversation',
2531     *               { 'removeRemoteVideoWindow':videoObject });
2532     * // add/remove video object from another DOMWindow
2533     * jQuery('#conversation').cwic('updateConversation',
2534     *               { 'addRemoteVideoWindow':videoObject, window:popupWindow });
2535     * jQuery('#conversation').cwic('updateConversation',
2536     *               { 'removeRemoteVideoWindow':videoObject, window:popupWindow });
2537 	*
2538 	* // Escalate to video
2539     * jQuery('#conversation').cwic('updateConversation', {'videoDirection': 'SendRecv'}); // implied source call is call associated with conversation div
2540     * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'SendRecv'}, conversation.id}); // source call id passed
2541     * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'SendRecv'}, conversation}); // source call passed
2542 	* // De-escalate from video
2543     * jQuery('#conversation').cwic('updateConversation', {'videoDirection': 'Inactive'}); // implied source call is call associated with conversation div
2544     * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'Inactive'}, conversation.id}); // source call id passed
2545     * jQuery('#phone').cwic('updateConversation', {'videoDirection': 'Inactive'}, conversation}); // source call passed
2546 	*
2547     * // Transfer call to target callid
2548     * jQuery('#conversation').cwic('updateConversation', {'transferCall':callId}); // implied source call is call associated with conversation div
2549     * jQuery('#phone').cwic('updateConversation', {'transferCall':callId}, conversation.id}); // source call id passed
2550     * jQuery('#phone').cwic('updateConversation', {'transferCall':callId}, conversation}); // source call passed
2551 	*
2552     * // Join target callId to source call
2553     * jQuery('#conversation').cwic('updateConversation', {'joinCall':callId}); // implied source call is call associated with conversation div
2554     * jQuery('#phone').cwic('updateConversation', {'joinCall':callId}, conversation.id}); // source call id passed
2555     * jQuery('#phone').cwic('updateConversation', {'joinCall':callId}, conversation}); // source call passed
2556     */
2557     function updateConversation() {
2558         _log(true, 'updateConversation', arguments);
2559 
2560         var $this = this;
2561         if ($this.length === 0) { return $this; }
2562 
2563         // mandatory first argument
2564         var update = arguments[0];
2565 
2566         // find conversation information
2567         var conversation = null;
2568         var conversationId = null;
2569         if (typeof arguments[1] === "object") {
2570             conversation = arguments[1];
2571             conversationId = conversation.id;
2572         }
2573         else if (typeof arguments[1] === "undefined") {
2574             conversation = $this.data('cwic'); // attached conversation object
2575             if (typeof conversation === "object") { conversationId = conversation.id; }
2576         }
2577         else {
2578             conversationId = arguments[1];
2579             conversation = $('.cwic-conversation-' + conversationId).data('cwic') || $this.data('cwic');
2580         }
2581 
2582         if (!conversationId || !conversation) {
2583             // TODO: remove hardcoded string
2584             return _triggerError($this, errorMap.InvalidArguments, 'cannot update conversation: undefined or empty conversation id');
2585         }
2586 
2587         var nativeResult = {};
2588         if (typeof update === "string") {
2589             if (update.match(/^hold$/i)) {
2590                 nativeResult = _plugin.api.hold({ callId: conversationId });
2591             }
2592             else if (update.match(/^resume$/i)) {
2593                 nativeResult = _plugin.api.resume({ callId: conversationId });
2594             }
2595             else if (update.match(/^mute$/i)) {
2596                 nativeResult = _plugin.api.mute({ callId: conversationId });
2597             }
2598             else if (update.match(/^unmute$/i)) {
2599                 nativeResult = _plugin.api.unmute({ callId: conversationId });
2600             }
2601             else if (update.match(/^muteAudio$/i)) {
2602                 nativeResult = _plugin.api.mute({ callId: conversationId, muteAudio: true });
2603             }
2604             else if (update.match(/^muteVideo$/i)) {
2605                 nativeResult = _plugin.api.mute({ callId: conversationId, muteVideo: true });
2606             }
2607             else if (update.match(/^unmuteAudio$/i)) {
2608                 nativeResult = _plugin.api.unmute({ callId: conversationId, unmuteAudio: true });
2609             }
2610             else if (update.match(/^unmuteVideo$/i)) {
2611                 nativeResult = _plugin.api.unmute({ callId: conversationId, unmuteVideo: true });
2612             }
2613             else {
2614                 // TODO: remove hardcoded string
2615                 return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (update conversation) - '+update, arguments);
2616             }
2617 
2618             if (nativeResult.error) {
2619                 return _triggerError($this, getError(nativeResult.error), nativeResult.error);
2620             }
2621         }
2622         else if (typeof update === "object") {
2623             var foundWritable = false;
2624 
2625             if (update.transferCall) {
2626                 foundWritable = true;
2627                 nativeResult = _plugin.api.transferCall({ callId: conversationId, transferCallId: update.transferCall});
2628                 if (nativeResult.error) {
2629                     return _triggerError($this, getError(nativeResult.error,'NativePluginError'),'transferCall',nativeResult);
2630                 }
2631             }
2632             if (update.joinCall) {
2633                 foundWritable = true;
2634                 nativeResult = _plugin.api.joinCalls({ joinCallId: conversationId, callId: update.joinCall});
2635                 if (nativeResult.error) {
2636                     return _triggerError($this, getError(nativeResult.error,'NativePluginError'),'joinCall',nativeResult);
2637                 }
2638             }
2639             if (update.videoDirection) {
2640                 foundWritable = true;
2641                 nativeResult = _plugin.api.setVideoDirection({ callId: conversationId, videoDirection: update.videoDirection });
2642                 if (nativeResult.error) {
2643                     return _triggerError($this, errorMap.NativePluginError, 'videoDirection', nativeResult.error);
2644                 }
2645             }
2646             if(update.addRemoteVideoWindow) {
2647                 foundWritable = true;
2648                 videowindowsbycall.add({callId: conversationId, plugin: update.addRemoteVideoWindow, "window": update.window});
2649                 //videowindowsbycall.dump();
2650             }
2651             if(update.removeRemoteVideoWindow) {
2652                 foundWritable = true;
2653                 videowindowsbycall.remove({callId: conversationId, plugin: update.addRemoteVideoWindow, "window": update.window});
2654                 //videowindowsbycall.dump();
2655             }
2656             if (!foundWritable) {
2657                 return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (update conversation)', arguments);
2658             }
2659         }
2660         else {
2661             return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (update conversation)', arguments);
2662         }
2663 
2664         return $this;
2665     }
2666     /**
2667     * Sends digit (String) as Dual-Tone Multi-Frequency (DTMF)
2668     * @example
2669     *  // SEND DTMF EXAMPLE
2670     * jQuery('#phone').cwic('sendDTMF', '5', '1234');
2671     * jQuery('#mydiv').cwic('sendDTMF', '3', conversation.id);
2672     * jQuery('body').cwic('sendDTMF', '7', conversation);
2673     * jQuery('#conversation').cwic('sendDTMF', '1');
2674     * @param {String} digit Dual-Tone Multi-Frequency (DTMF) digit to send.  Does not trigger any event.
2675     * @param {String|Object} [id] a {String} conversation identifier or an {Object} containing an id property
2676     */
2677     function sendDTMF() {
2678         _log(true, 'sendDTMF'); // don't send dtmf digits to logger
2679 
2680         var $this = this;
2681         var digit = null;
2682         var conversation = $this.data('cwic');
2683         var conversationId = conversation ? conversation.id : null;
2684 
2685         // inspect arguments
2686         if (arguments.length > 0) {
2687             digit = typeof arguments[0] === "string" ? arguments[0] : null;
2688 
2689             if (arguments.length > 1) {
2690                 if (typeof arguments[1] === "object") {
2691                     conversation = arguments[1];
2692                     conversationId = conversation.id;
2693                 }
2694                 else if (typeof arguments[1] === "string") {
2695                     conversationId = arguments[1];
2696                 }
2697             }
2698         }
2699 
2700         if (typeof digit !== "string" || !conversationId) {
2701             return _triggerError($this, errorMap.InvalidArguments, 'wrong arguments (sendDTMF)', arguments);
2702         }
2703 
2704         var nativeResult = _plugin.api.sendDTMF({
2705             callId: conversationId,
2706             digit: digit
2707         });
2708 
2709         if (nativeResult.error) {
2710                 return _triggerError($this, errorMap.NativePluginError, nativeResult.error);
2711         }
2712 
2713         return $this;
2714     }
2715 
2716 /**
2717  * Versions, states and capabilities
2718  * @returns {aboutObject}
2719  */
2720     function about() {
2721         _log(true, 'about', arguments);
2722 
2723         /*
2724         Versioning scheme: Release.Major.Minor.Revision
2725 
2726 		Release should be for major feature releases (e.g. video)
2727 		Major for an API-breaking ship within a release (or additional APIs that won't work without error checking on previous plugins).
2728 		Minor for non API-breaking builds - e.g. bug fix releases that strongly recommend updating the plugin
2729 		Revision for unique build tracking.
2730         */
2731 
2732         var ab = {
2733             javascript: {
2734                 version: '2.1.0.60779'
2735             },
2736             jquery: {
2737                 version: $.fn.jquery
2738             },
2739             plugin: (_plugin === null) ? null : { version: _plugin.version },
2740             states: {
2741                 system: (!_plugin || !_plugin.api) ? 'unknown' : _plugin.api.connectionStatus
2742             },
2743             device: (!_plugin || !_plugin.api) ? {exists: false, inService: false, lines: {}, model: -1, modelDescription: '', name: ''} : _plugin.api.device,
2744             capabilities: {
2745                 video: ((!_plugin || !_plugin.api) ? undefined : _plugin.api.supportsVideo),
2746 				// Undocumented capability - used to differentiate between MR versions on Mac and Win that contain
2747 				// differing abilities when it comes to multireg support. Because (for this release) Mac doesn't support video,
2748 				// multireg support == video support
2749 				multireg: ((!_plugin || !_plugin.api) ? undefined : _plugin.api.supportsVideo)
2750             },
2751             upgrade: {}
2752          };
2753 		
2754 		var m = ab.javascript.version.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
2755         if (m) {
2756         	ab.javascript.release = m[1];
2757         	ab.javascript.major = m[2];
2758         	ab.javascript.minor = m[3];
2759         	ab.javascript.revision = m[4];
2760         }
2761         
2762         if (ab.plugin) {
2763         	m = ab.plugin.version.plugin.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
2764         	if (m) {
2765         		ab.plugin.release = m[1];
2766         		ab.plugin.major = m[2];
2767         		ab.plugin.minor = m[3];
2768         		ab.plugin.revision = m[4];
2769         	}
2770         	
2771         	// compare javascript and plugin versions to advise about upgrade
2772         	if (ab.javascript.release > ab.plugin.release) {
2773         		// release mismatch, upgrade plugin
2774         		ab.upgrade.plugin = 'mandatory';
2775         	}
2776         	else if (ab.javascript.release < ab.plugin.release) {
2777         		// release mismatch, upgrade javascript
2778         		ab.upgrade.javascript = 'mandatory';
2779         	}
2780         	else if (ab.javascript.release == ab.plugin.release) {
2781         		// same release, compare major
2782         		if      (ab.javascript.major >  ab.plugin.major) { ab.upgrade.plugin = 'mandatory'; }
2783         		else if (ab.javascript.major <  ab.plugin.major) { ab.upgrade.javascript = 'mandatory'; }
2784         		else if (ab.javascript.major == ab.plugin.major) {
2785         			// same release.major, compare minor
2786         			if      (ab.javascript.minor >  ab.plugin.minor) { ab.upgrade.plugin ='recommended'; }
2787         			else if (ab.javascript.minor <  ab.plugin.minor) { ab.upgrade.javascript = 'recommended'; }
2788         		}
2789         	}
2790         }
2791         else {
2792         	// no plugin available
2793         	ab.upgrade.plugin = 'mandatory';
2794         }
2795         
2796         return ab;
2797     }
2798 
2799     function getInstanceId() {
2800         _log(true, 'getInstanceId');
2801         return _plugin.api.instanceId;
2802     }
2803 
2804     // a map with all exposed methods
2805     var methods = {
2806         about: about,
2807         init : init,
2808         shutdown: shutdown,
2809         rebootIfBroken: rebootIfBroken,
2810         registerPhone: registerPhone,
2811         switchPhoneMode: switchPhoneMode,
2812         unregisterPhone: unregisterPhone,
2813         startConversation: startConversation,
2814         updateConversation: updateConversation,
2815         endConversation: endConversation,
2816         createVideoWindow: createVideoWindow,
2817         addPreviewWindow: addPreviewWindow,
2818         removePreviewWindow: removePreviewWindow,
2819         sendDTMF: sendDTMF,
2820         getInstanceId: getInstanceId
2821     };
2822 
2823   // the jQuery plugin
2824   /**
2825    * @description
2826    * CWIC is a jQuery plugin to access the Cisco Web Communicator<br>
2827    * Audio and Video media require the CWC browser plugin to be installed <br>
2828    * <h3>Fields overview</h3>
2829    * <h3>Methods overview</h3>
2830    * All cwic methods are called in the following manner<br>
2831    * <pre class="code">$('#selector').cwic('method',parameters)</pre><br>
2832    * <h3>Events overview</h3>
2833    * All events are part of the cwic namespace.  For example:
2834    * <ul>
2835    * <li>conversationStart.cwic</li>
2836    * <li>system.cwic</li>
2837    * <li>error.cwic</li>
2838    * </ul>
2839    * <h4>Example conversation events:</h4>
2840    * These are conversation-related events that can be triggered by the SDK.<br>
2841    * The event handlers are passed the conversation properties as a single object. For example:<br>
2842    * @example
2843    * // start an audio conversation with phone a number and bind to conversation events
2844    * jQuery('#conversation')
2845    *   .cwic('startConversation', '+1 234 567')  // container defaults to $(this)
2846    *   .bind('conversationStart.cwic', function(event, conversation, container) {
2847    *      console.log('conversation has just started');
2848    *      // container is jQuery('#conversation')
2849    *    })
2850    *    .bind('conversationUpdate.cwic', function(event, conversation) {
2851    *      console.log('conversation has just been updated');
2852    *    })
2853    *    .bind('conversationEnd.cwic', function(event, conversation) {
2854    *      console.log('conversation has just ended');
2855    *    });
2856    * @example
2857    * // listen for incoming conversation
2858    * jQuery('#phone')
2859    *   .bind('conversationIncoming.cwic', function(event, conversation, container) {
2860    *     console.log('incoming conversation with id ' + conversation.id);
2861    *     // attach the "toast" container to the DOM and bind to events
2862    *     container
2863    *       .appendTo('#phone')
2864    *       .bind('conversationUpdate.cwic', function(event, conversation) {
2865    *         // update on incoming conversation
2866    *       })
2867    *       .bind('conversationEnd.cwic', function(event, conversation) {
2868    *         // incoming conversation has ended
2869    *         container.remove();
2870    *       });
2871    *     // suppose UI has a button with id 'answer'
2872    *     jQuery('#answer').click(function() {
2873    *       // answer the incoming conversation
2874    *       // conversation has an id property, so startConversation accepts it
2875    *       // use element #conversation as container
2876    *       jQuery('#conversation').cwic('startConversation', conversation);
2877    *       // remove incoming container
2878    *       container.remove();
2879    *     });
2880    *   });
2881    * @class
2882    * @static
2883    * @param {String} method The name of the method to call
2884    * @param {Variable} arguments trailing arguments are passed to the specific call see methods below
2885    */
2886   $.fn.cwic = function( method ) {
2887 
2888     try {
2889         // Method calling logic
2890         if ( methods[method] ) {
2891             return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
2892         }
2893         else if ( typeof method === 'object' || ! method ) {
2894             return methods.init.apply( this, arguments );
2895         }
2896         else {
2897             throw method + ': no such method on jQuery.cwic';
2898         }
2899     }
2900     catch(e) {
2901         if(typeof console !== "undefined" && console.trace) {
2902             console.trace();
2903         }
2904         _triggerError(this, e);
2905     }
2906   };
2907 }(jQuery));
2908