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