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