1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * @fileoverview 7 * A module that contains basic utility components and methods for the 8 * chromoting project 9 * 10 */ 11 12'use strict'; 13 14var base = {}; 15base.debug = function() {}; 16 17/** 18 * Whether to break in debugger and alert when an assertion fails. 19 * Set it to true for debugging. 20 * @type {boolean} 21 */ 22base.debug.breakOnAssert = false; 23 24/** 25 * Assert that |expr| is true else print the |opt_msg|. 26 * @param {boolean} expr 27 * @param {string=} opt_msg 28 */ 29base.debug.assert = function(expr, opt_msg) { 30 if (!expr) { 31 var msg = 'Assertion Failed.'; 32 if (opt_msg) { 33 msg += ' ' + opt_msg; 34 } 35 console.error(msg); 36 if (base.debug.breakOnAssert) { 37 alert(msg); 38 debugger; 39 } 40 } 41}; 42 43/** 44 * @return {string} The callstack of the current method. 45 */ 46base.debug.callstack = function() { 47 try { 48 throw new Error(); 49 } catch (e) { 50 var error = /** @type {Error} */ e; 51 var callstack = error.stack 52 .replace(/^\s+(at eval )?at\s+/gm, '') // Remove 'at' and indentation. 53 .split('\n'); 54 callstack.splice(0,2); // Remove the stack of the current function. 55 } 56 return callstack.join('\n'); 57}; 58 59/** 60 * @interface 61 */ 62base.Disposable = function() {}; 63base.Disposable.prototype.dispose = function() {}; 64 65/** 66 * A utility function to invoke |obj|.dispose without a null check on |obj|. 67 * @param {base.Disposable} obj 68 */ 69base.dispose = function(obj) { 70 if (obj) { 71 base.debug.assert(typeof obj.dispose == 'function'); 72 obj.dispose(); 73 } 74}; 75 76/** 77 * Copy all properties from src to dest. 78 * @param {Object} dest 79 * @param {Object} src 80 */ 81base.mix = function(dest, src) { 82 for (var prop in src) { 83 if (src.hasOwnProperty(prop)) { 84 base.debug.assert(!dest.hasOwnProperty(prop),"Don't override properties"); 85 dest[prop] = src[prop]; 86 } 87 } 88}; 89 90/** 91 * Adds a mixin to a class. 92 * @param {Object} dest 93 * @param {Object} src 94 * @suppress {checkTypes} 95 */ 96base.extend = function(dest, src) { 97 base.mix(dest.prototype, src.prototype || src); 98}; 99 100base.doNothing = function() {}; 101 102/** 103 * Returns an array containing the values of |dict|. 104 * @param {!Object} dict 105 * @return {Array} 106 */ 107base.values = function(dict) { 108 return Object.keys(dict).map( 109 /** @param {string} key */ 110 function(key) { 111 return dict[key]; 112 }); 113}; 114 115/** 116 * @type {boolean|undefined} 117 * @private 118 */ 119base.isAppsV2_ = undefined; 120 121/** 122 * @return {boolean} True if this is a v2 app; false if it is a legacy app. 123 */ 124base.isAppsV2 = function() { 125 if (base.isAppsV2_ === undefined) { 126 var manifest = chrome.runtime.getManifest(); 127 base.isAppsV2_ = 128 Boolean(manifest && manifest.app && manifest.app.background); 129 } 130 return base.isAppsV2_; 131}; 132 133/** 134 * Joins the |url| with optional query parameters defined in |opt_params| 135 * See unit test for usage. 136 * @param {string} url 137 * @param {Object.<string>=} opt_params 138 * @return {string} 139 */ 140base.urlJoin = function(url, opt_params) { 141 if (!opt_params) { 142 return url; 143 } 144 var queryParameters = []; 145 for (var key in opt_params) { 146 queryParameters.push(encodeURIComponent(key) + "=" + 147 encodeURIComponent(opt_params[key])); 148 } 149 return url + '?' + queryParameters.join('&'); 150}; 151 152/** 153 * Convert special characters (e.g. &, < and >) to HTML entities. 154 * 155 * @param {string} str 156 * @return {string} 157 */ 158base.escapeHTML = function(str) { 159 var div = document.createElement('div'); 160 div.appendChild(document.createTextNode(str)); 161 return div.innerHTML; 162}; 163 164/** 165 * Promise is a great tool for writing asynchronous code. However, the construct 166 * var p = new promise(function init(resolve, reject) { 167 * ... // code that fulfills the Promise. 168 * }); 169 * forces the Promise-resolving logic to reside in the |init| function 170 * of the constructor. This is problematic when you need to resolve the 171 * Promise in a member function(which is quite common for event callbacks). 172 * 173 * base.Deferred comes to the rescue. It encapsulates a Promise 174 * object and exposes member methods (resolve/reject) to fulfill it. 175 * 176 * Here are the recommended steps to follow when implementing an asynchronous 177 * function that returns a Promise: 178 * 1. Create a deferred object by calling 179 * var deferred = new base.Deferred(); 180 * 2. Call deferred.resolve() when the asynchronous operation finishes. 181 * 3. Call deferred.reject() when the asynchronous operation fails. 182 * 4. Return deferred.promise() to the caller so that it can subscribe 183 * to status changes using the |then| handler. 184 * 185 * Sample Usage: 186 * function myAsyncAPI() { 187 * var deferred = new base.Deferred(); 188 * window.setTimeout(function() { 189 * deferred.resolve(); 190 * }, 100); 191 * return deferred.promise(); 192 * }; 193 * 194 * @constructor 195 */ 196base.Deferred = function() { 197 /** 198 * @type {?function(?=)} 199 * @private 200 */ 201 this.resolve_ = null; 202 203 /** 204 * @type {?function(?)} 205 * @private 206 */ 207 this.reject_ = null; 208 209 /** 210 * @type {Promise} 211 * @private 212 */ 213 this.promise_ = new Promise( 214 /** 215 * @param {function(?=):void} resolve 216 * @param {function(?):void} reject 217 * @this {base.Deferred} 218 */ 219 function(resolve, reject) { 220 this.resolve_ = resolve; 221 this.reject_ = reject; 222 }.bind(this) 223 ); 224}; 225 226/** @param {*} reason */ 227base.Deferred.prototype.reject = function(reason) { 228 this.reject_(reason); 229}; 230 231/** @param {*=} opt_value */ 232base.Deferred.prototype.resolve = function(opt_value) { 233 this.resolve_(opt_value); 234}; 235 236/** @return {Promise} */ 237base.Deferred.prototype.promise = function() { 238 return this.promise_; 239}; 240 241base.Promise = function() {}; 242 243/** 244 * @param {number} delay 245 * @return {Promise} a Promise that will be fulfilled after |delay| ms. 246 */ 247base.Promise.sleep = function(delay) { 248 return new Promise( 249 /** @param {function():void} fulfill */ 250 function(fulfill) { 251 window.setTimeout(fulfill, delay); 252 }); 253}; 254 255 256/** 257 * @param {Promise} promise 258 * @return {Promise} a Promise that will be fulfilled iff the specified Promise 259 * is rejected. 260 */ 261base.Promise.negate = function(promise) { 262 return promise.then( 263 /** @return {Promise} */ 264 function() { 265 return Promise.reject(); 266 }, 267 /** @return {Promise} */ 268 function() { 269 return Promise.resolve(); 270 }); 271}; 272 273/** 274 * A mixin for classes with events. 275 * 276 * For example, to create an alarm event for SmokeDetector: 277 * functionSmokeDetector() { 278 * this.defineEvents(['alarm']); 279 * }; 280 * base.extend(SmokeDetector, base.EventSource); 281 * 282 * To fire an event: 283 * SmokeDetector.prototype.onCarbonMonoxideDetected = function() { 284 * var param = {} // optional parameters 285 * this.raiseEvent('alarm', param); 286 * } 287 * 288 * To listen to an event: 289 * var smokeDetector = new SmokeDetector(); 290 * smokeDetector.addEventListener('alarm', listenerObj.someCallback) 291 * 292 */ 293 294/** 295 * Helper interface for the EventSource. 296 * @constructor 297 */ 298base.EventEntry = function() { 299 /** @type {Array.<function():void>} */ 300 this.listeners = []; 301}; 302 303/** 304 * @constructor 305 * Since this class is implemented as a mixin, the constructor may not be 306 * called. All initializations should be done in defineEvents. 307 */ 308base.EventSource = function() { 309 /** @type {Object.<string, base.EventEntry>} */ 310 this.eventMap_; 311}; 312 313/** 314 * @param {base.EventSource} obj 315 * @param {string} type 316 */ 317base.EventSource.isDefined = function(obj, type) { 318 base.debug.assert(Boolean(obj.eventMap_), 319 "The object doesn't support events"); 320 base.debug.assert(Boolean(obj.eventMap_[type]), 'Event <' + type + 321 '> is undefined for the current object'); 322}; 323 324base.EventSource.prototype = { 325 /** 326 * Define |events| for this event source. 327 * @param {Array.<string>} events 328 */ 329 defineEvents: function(events) { 330 base.debug.assert(!Boolean(this.eventMap_), 331 'defineEvents can only be called once.'); 332 this.eventMap_ = {}; 333 events.forEach( 334 /** 335 * @this {base.EventSource} 336 * @param {string} type 337 */ 338 function(type) { 339 base.debug.assert(typeof type == 'string'); 340 this.eventMap_[type] = new base.EventEntry(); 341 }, this); 342 }, 343 344 /** 345 * Add a listener |fn| to listen to |type| event. 346 * @param {string} type 347 * @param {function(?=):void} fn 348 */ 349 addEventListener: function(type, fn) { 350 base.debug.assert(typeof fn == 'function'); 351 base.EventSource.isDefined(this, type); 352 353 var listeners = this.eventMap_[type].listeners; 354 listeners.push(fn); 355 }, 356 357 /** 358 * Remove the listener |fn| from the event source. 359 * @param {string} type 360 * @param {function(?=):void} fn 361 */ 362 removeEventListener: function(type, fn) { 363 base.debug.assert(typeof fn == 'function'); 364 base.EventSource.isDefined(this, type); 365 366 var listeners = this.eventMap_[type].listeners; 367 // find the listener to remove. 368 for (var i = 0; i < listeners.length; i++) { 369 var listener = listeners[i]; 370 if (listener == fn) { 371 listeners.splice(i, 1); 372 break; 373 } 374 } 375 }, 376 377 /** 378 * Fire an event of a particular type on this object. 379 * @param {string} type 380 * @param {*=} opt_details The type of |opt_details| should be ?= to 381 * match what is defined in add(remove)EventListener. However, JSCompile 382 * cannot handle invoking an unknown type as an argument to |listener| 383 * As a hack, we set the type to *=. 384 */ 385 raiseEvent: function(type, opt_details) { 386 base.EventSource.isDefined(this, type); 387 388 var entry = this.eventMap_[type]; 389 var listeners = entry.listeners.slice(0); // Make a copy of the listeners. 390 391 listeners.forEach( 392 /** @param {function(*=):void} listener */ 393 function(listener){ 394 if (listener) { 395 listener(opt_details); 396 } 397 }); 398 } 399}; 400 401/** 402 * Converts UTF-8 string to ArrayBuffer. 403 * 404 * @param {string} string 405 * @return {ArrayBuffer} 406 */ 407base.encodeUtf8 = function(string) { 408 var utf8String = unescape(encodeURIComponent(string)); 409 var result = new Uint8Array(utf8String.length); 410 for (var i = 0; i < utf8String.length; i++) 411 result[i] = utf8String.charCodeAt(i); 412 return result.buffer; 413} 414 415/** 416 * Decodes UTF-8 string from ArrayBuffer. 417 * 418 * @param {ArrayBuffer} buffer 419 * @return {string} 420 */ 421base.decodeUtf8 = function(buffer) { 422 return decodeURIComponent( 423 escape(String.fromCharCode.apply(null, new Uint8Array(buffer)))); 424} 425