1/* 2 * Background page for Chrome Sounds extension. 3 * This tracks various events from Chrome and plays sounds. 4 */ 5 6// Map of hostname suffixes or URLs without query params to sounds. 7// Yeah OK, some of these are a little cliche... 8var urlSounds = { 9 "http://www.google.ca/": "canadian-hello.mp3", 10 "about:histograms": "time-passing.mp3", 11 "about:memory": "transform!.mp3", 12 "about:crash": "sadtrombone.mp3", 13 "chrome://extensions/": "beepboop.mp3", 14 "http://www.google.com.au/": "didgeridoo.mp3", 15 "http://www.google.com.my/": "my_subway.mp3", 16 "http://www.google.com/appserve/fiberrfi/": "dialup.mp3", 17 "lively.com": "cricket.mp3", 18 "http://www.google.co.uk/": "mind_the_gap.mp3", 19 "http://news.google.com/": "news.mp3", 20 "http://www.bing.com/": "sonar.mp3", 21}; 22 23// Map of query parameter words to sounds. 24// More easy cliches... 25var searchSounds = { 26 "scotland": "bagpipe.mp3", 27 "seattle": "rain.mp3", 28}; 29 30// Map of tab numbers to notes on a scale. 31var tabNoteSounds = { 32 "tab0": "mando-1.mp3", 33 "tab1": "mando-2.mp3", 34 "tab2": "mando-3.mp3", 35 "tab3": "mando-4.mp3", 36 "tab4": "mando-5.mp3", 37 "tab5": "mando-6.mp3", 38 "tab6": "mando-7.mp3", 39}; 40 41// Map of sounds that play in a continuous loop while an event is happening 42// in the content area (e.g. "keypress" while start and keep looping while 43// the user keeps typing). 44var contentSounds = { 45 "keypress": "typewriter-1.mp3", 46 "resize": "harp-transition-2.mp3", 47 "scroll": "shepard.mp3" 48}; 49 50// Map of events to their default sounds 51var eventSounds = { 52 "tabCreated": "conga1.mp3", 53 "tabMoved": "bell-transition.mp3", 54 "tabRemoved": "smash-glass-1.mp3", 55 "tabSelectionChanged": "click.mp3", 56 "tabAttached": "whoosh-15.mp3", 57 "tabDetached": "sword-shrill.mp3", 58 "tabNavigated": "click.mp3", 59 "windowCreated": "bell-small.mp3", 60 "windowFocusChanged": "click.mp3", 61 "bookmarkCreated": "bubble-drop.mp3", 62 "bookmarkMoved": "thud.mp3", 63 "bookmarkRemoved": "explosion-6.mp3", 64 "windowCreatedIncognito": "weird-wind1.mp3", 65 "startup": "whoosh-19.mp3" 66}; 67 68var soundLists = [urlSounds, searchSounds, eventSounds, tabNoteSounds, 69 contentSounds]; 70 71var sounds = {}; 72 73// Map of event names to extension events. 74// Events intentionally skipped: 75// chrome.windows.onRemoved - can't suppress the tab removed that comes first 76var events = { 77 "tabCreated": chrome.tabs.onCreated, 78 "tabMoved": chrome.tabs.onMoved, 79 "tabRemoved": chrome.tabs.onRemoved, 80 "tabSelectionChanged": chrome.tabs.onSelectionChanged, 81 "tabAttached": chrome.tabs.onAttached, 82 "tabDetached": chrome.tabs.onDetached, 83 "tabNavigated": chrome.tabs.onUpdated, 84 "windowCreated": chrome.windows.onCreated, 85 "windowFocusChanged": chrome.windows.onFocusChanged, 86 "bookmarkCreated": chrome.bookmarks.onCreated, 87 "bookmarkMoved": chrome.bookmarks.onMoved, 88 "bookmarkRemoved": chrome.bookmarks.onRemoved 89}; 90 91// Map of event name to a validation function that is should return true if 92// the default sound should be played for this event. 93var eventValidator = { 94 "tabCreated": tabCreated, 95 "tabNavigated": tabNavigated, 96 "tabRemoved": tabRemoved, 97 "tabSelectionChanged": tabSelectionChanged, 98 "windowCreated": windowCreated, 99 "windowFocusChanged": windowFocusChanged, 100}; 101 102var started = false; 103 104function shouldPlay(id) { 105 // Ignore all events until the startup sound has finished. 106 if (id != "startup" && !started) 107 return false; 108 var val = localStorage.getItem(id); 109 if (val && val != "enabled") { 110 console.log(id + " disabled"); 111 return false; 112 } 113 return true; 114} 115 116function didPlay(id) { 117 if (!localStorage.getItem(id)) 118 localStorage.setItem(id, "enabled"); 119} 120 121function playSound(id, loop) { 122 if (!shouldPlay(id)) 123 return; 124 125 var sound = sounds[id]; 126 console.log("playsound: " + id); 127 if (sound && sound.src) { 128 if (!sound.paused) { 129 if (sound.currentTime < 0.2) { 130 console.log("ignoring fast replay: " + id + "/" + sound.currentTime); 131 return; 132 } 133 sound.pause(); 134 sound.currentTime = 0; 135 } 136 if (loop) 137 sound.loop = loop; 138 139 // Sometimes, when playing multiple times, readyState is HAVE_METADATA. 140 if (sound.readyState == 0) { // HAVE_NOTHING 141 console.log("bad ready state: " + sound.readyState); 142 } else if (sound.error) { 143 console.log("media error: " + sound.error); 144 } else { 145 didPlay(id); 146 sound.play(); 147 } 148 } else { 149 console.log("bad playSound: " + id); 150 } 151} 152 153function stopSound(id) { 154 console.log("stopSound: " + id); 155 var sound = sounds[id]; 156 if (sound && sound.src && !sound.paused) { 157 sound.pause(); 158 sound.currentTime = 0; 159 } 160} 161 162var base_url = "http://dl.google.com/dl/chrome/extensions/audio/"; 163 164function soundLoadError(audio, id) { 165 console.log("failed to load sound: " + id + "-" + audio.src); 166 audio.src = ""; 167 if (id == "startup") 168 started = true; 169} 170 171function soundLoaded(audio, id) { 172 console.log("loaded sound: " + id); 173 sounds[id] = audio; 174 if (id == "startup") 175 playSound(id); 176} 177 178// Hack to keep a reference to the objects while we're waiting for them to load. 179var notYetLoaded = {}; 180 181function loadSound(file, id) { 182 if (!file.length) { 183 console.log("no sound for " + id); 184 return; 185 } 186 var audio = new Audio(); 187 audio.id = id; 188 audio.onerror = function() { soundLoadError(audio, id); }; 189 audio.addEventListener("canplaythrough", 190 function() { soundLoaded(audio, id); }, false); 191 if (id == "startup") { 192 audio.addEventListener("ended", function() { started = true; }); 193 } 194 audio.src = base_url + file; 195 audio.load(); 196 notYetLoaded[id] = audio; 197} 198 199// Remember the last event so that we can avoid multiple events firing 200// unnecessarily (e.g. selection changed due to close). 201var eventsToEat = 0; 202 203function eatEvent(name) { 204 if (eventsToEat > 0) { 205 console.log("ate event: " + name); 206 eventsToEat--; 207 return true; 208 } 209 return false; 210} 211 212function soundEvent(event, name) { 213 if (event) { 214 var validator = eventValidator[name]; 215 if (validator) { 216 event.addListener(function() { 217 console.log("handling custom event: " + name); 218 219 // Check this first since the validator may bump the count for future 220 // events. 221 var canPlay = (eventsToEat == 0); 222 if (validator.apply(this, arguments)) { 223 if (!canPlay) { 224 console.log("ate event: " + name); 225 eventsToEat--; 226 return; 227 } 228 playSound(name); 229 } 230 }); 231 } else { 232 event.addListener(function() { 233 console.log("handling event: " + name); 234 if (eatEvent(name)) { 235 return; 236 } 237 playSound(name); 238 }); 239 } 240 } else { 241 console.log("no event for " + name); 242 } 243} 244 245var navSound; 246 247function stopNavSound() { 248 if (navSound) { 249 stopSound(navSound); 250 navSound = null; 251 } 252} 253 254function playNavSound(id) { 255 stopNavSound(); 256 navSound = id; 257 playSound(id); 258} 259 260function tabNavigated(tabId, changeInfo, tab) { 261 // Quick fix to catch the case where the content script doesn't have a chance 262 // to stop itself. 263 stopSound("keypress"); 264 265 //console.log(JSON.stringify(changeInfo) + JSON.stringify(tab)); 266 if (changeInfo.status != "complete") { 267 return false; 268 } 269 if (eatEvent("tabNavigated")) { 270 return false; 271 } 272 273 console.log(JSON.stringify(tab)); 274 275 if (navSound) 276 stopSound(navSound); 277 278 var re = /https?:\/\/([^\/:]*)[^\?]*\??(.*)/i; 279 match = re.exec(tab.url); 280 if (match) { 281 if (match.length == 3) { 282 var query = match[2]; 283 var parts = query.split("&"); 284 for (var i in parts) { 285 if (parts[i].indexOf("q=") == 0) { 286 var q = decodeURIComponent(parts[i].substring(2)); 287 q = q.replace("+", " "); 288 console.log("query == " + q); 289 var words = q.split(" "); 290 for (j in words) { 291 if (searchSounds[words[j]]) { 292 console.log("searchSound: " + words[j]); 293 playNavSound(words[j]); 294 return false; 295 } 296 } 297 break; 298 } 299 } 300 } 301 if (match.length >= 2) { 302 var hostname = match[1]; 303 if (hostname) { 304 var parts = hostname.split("."); 305 if (parts.length > 1) { 306 var tld2 = parts.slice(-2).join("."); 307 var tld3 = parts.slice(-3).join("."); 308 var sound = urlSounds[tld2]; 309 if (sound) { 310 playNavSound(tld2); 311 return false; 312 } 313 sound = urlSounds[tld3]; 314 if (sound) { 315 playNavSound(tld3); 316 return false; 317 } 318 } 319 } 320 } 321 } 322 323 // Now try a direct URL match (without query string). 324 var url = tab.url; 325 var query = url.indexOf("?"); 326 if (query > 0) { 327 url = tab.url.substring(0, query); 328 } 329 console.log(tab.url); 330 var sound = urlSounds[url]; 331 if (sound) { 332 playNavSound(url); 333 return false; 334 } 335 336 return true; 337} 338 339var selectedTabId = -1; 340 341function tabSelectionChanged(tabId) { 342 selectedTabId = tabId; 343 if (eatEvent("tabSelectionChanged")) 344 return false; 345 346 var count = 7; 347 chrome.tabs.get(tabId, function(tab) { 348 var index = tab.index % count; 349 playSound("tab" + index); 350 }); 351 return false; 352} 353 354function tabCreated(tab) { 355 if (eatEvent("tabCreated")) { 356 return false; 357 } 358 eventsToEat++; // tabNavigated or tabSelectionChanged 359 // TODO - unfortunately, we can't detect whether this tab will get focus, so 360 // we can't decide whether or not to eat a second event. 361 return true; 362} 363 364function tabRemoved(tabId) { 365 if (eatEvent("tabRemoved")) { 366 return false; 367 } 368 if (tabId == selectedTabId) { 369 eventsToEat++; // tabSelectionChanged 370 stopNavSound(); 371 } 372 return true; 373} 374 375function windowCreated(window) { 376 if (eatEvent("windowCreated")) { 377 return false; 378 } 379 eventsToEat += 3; // tabNavigated, tabSelectionChanged, windowFocusChanged 380 if (window.incognito) { 381 playSound("windowCreatedIncognito"); 382 return false; 383 } 384 return true; 385} 386 387var selectedWindowId = -1; 388 389function windowFocusChanged(windowId) { 390 if (windowId == selectedWindowId) { 391 return false; 392 } 393 selectedWindowId = windowId; 394 if (eatEvent("windowFocusChanged")) { 395 return false; 396 } 397 return true; 398} 399 400function contentScriptHandler(request) { 401 if (contentSounds[request.eventName]) { 402 if (request.eventValue == "started") { 403 playSound(request.eventName, true); 404 } else if (request.eventValue == "stopped") { 405 stopSound(request.eventName); 406 } else { 407 playSound(request.eventName); 408 } 409 } 410 console.log("got message: " + JSON.stringify(request)); 411} 412 413 414////////////////////////////////////////////////////// 415 416// Listen for messages from content scripts. 417chrome.extension.onRequest.addListener(contentScriptHandler); 418 419// Load the sounds and register event listeners. 420for (var list in soundLists) { 421 for (var id in soundLists[list]) { 422 loadSound(soundLists[list][id], id); 423 } 424} 425for (var name in events) { 426 soundEvent(events[name], name); 427} 428