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