Remove the old signature of NotificationManager::closePersistent().
[chromium-blink-merge.git] / chrome / browser / resources / hotword / page_audio_manager.js
blob8e9891fdddce360d3e3ffce77542a331b04f0caa
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.
5 cr.define('hotword', function() {
6   'use strict';
8   /**
9    * Class used to manage the interaction between hotwording and the
10    * NTP/google.com. Injects a content script to interact with NTP/google.com
11    * and updates the global hotwording state based on interaction with those
12    * pages.
13    * @param {!hotword.StateManager} stateManager
14    * @constructor
15    */
16   function PageAudioManager(stateManager) {
17     /**
18      * Manager of global hotwording state.
19      * @private {!hotword.StateManager}
20      */
21     this.stateManager_ = stateManager;
23     /**
24      * Mapping between tab ID and port that is connected from the injected
25      * content script.
26      * @private {!Object<number, Port>}
27      */
28     this.portMap_ = {};
30     /**
31      * Chrome event listeners. Saved so that they can be de-registered when
32      * hotwording is disabled.
33      */
34     this.connectListener_ = this.handleConnect_.bind(this);
35     this.tabCreatedListener_ = this.handleCreatedTab_.bind(this);
36     this.tabUpdatedListener_ = this.handleUpdatedTab_.bind(this);
37     this.tabActivatedListener_ = this.handleActivatedTab_.bind(this);
38     this.windowFocusChangedListener_ = this.handleChangedWindow_.bind(this);
39     this.messageListener_ = this.handleMessageFromPage_.bind(this);
41     // Need to setup listeners on startup, otherwise events that caused the
42     // event page to start up, will be lost.
43     this.setupListeners_();
45     this.stateManager_.onStatusChanged.addListener(function() {
46       this.updateListeners_();
47       this.updateTabState_();
48     }.bind(this));
49   };
51   var CommandToPage = hotword.constants.CommandToPage;
52   var CommandFromPage = hotword.constants.CommandFromPage;
54   PageAudioManager.prototype = {
55     /**
56      * Helper function to test if a URL path is eligible for hotwording.
57      * @param {!string} url URL to check.
58      * @param {!string} base Base URL to compare against..
59      * @return {boolean} True if url is an eligible hotword URL.
60      * @private
61      */
62     checkUrlPathIsEligible_: function(url, base) {
63       if (url == base ||
64           url == base + '/' ||
65           url.indexOf(base + '/_/chrome/newtab?') == 0 ||  // Appcache NTP.
66           url.indexOf(base + '/?') == 0 ||
67           url.indexOf(base + '/#') == 0 ||
68           url.indexOf(base + '/webhp') == 0 ||
69           url.indexOf(base + '/search') == 0) {
70         return true;
71       }
72       return false;
73     },
75     /**
76      * Determines if a URL is eligible for hotwording. For now, the valid pages
77      * are the Google HP and SERP (this will include the NTP).
78      * @param {!string} url URL to check.
79      * @return {boolean} True if url is an eligible hotword URL.
80      * @private
81      */
82     isEligibleUrl_: function(url) {
83       if (!url)
84         return false;
86       var baseGoogleUrls = [
87         'https://www.google.',
88         'https://encrypted.google.'
89       ];
90       // TODO(amistry): Get this list from a file in the shared module instead.
91       var tlds = [
92         'at',
93         'ca',
94         'com',
95         'com.au',
96         'com.mx',
97         'com.br',
98         'co.jp',
99         'co.kr',
100         'co.nz',
101         'co.uk',
102         'co.za',
103         'de',
104         'es',
105         'fr',
106         'it',
107         'ru'
108       ];
110       // Check for the new tab page first.
111       if (this.checkUrlPathIsEligible_(url, 'chrome://newtab'))
112         return true;
114       // Check URLs with each type of local-based TLD.
115       for (var i = 0; i < baseGoogleUrls.length; i++) {
116         for (var j = 0; j < tlds.length; j++) {
117           var base = baseGoogleUrls[i] + tlds[j];
118           if (this.checkUrlPathIsEligible_(url, base))
119             return true;
120         }
121       }
122       return false;
123     },
125     /**
126      * Locates the current active tab in the current focused window and
127      * performs a callback with the tab as the parameter.
128      * @param {function(?Tab)} callback Function to call with the
129      *     active tab or null if not found. The function's |this| will be set to
130      *     this object.
131      * @private
132      */
133     findCurrentTab_: function(callback) {
134       chrome.windows.getAll(
135           {'populate': true},
136           function(windows) {
137             for (var i = 0; i < windows.length; ++i) {
138               if (!windows[i].focused)
139                 continue;
141               for (var j = 0; j < windows[i].tabs.length; ++j) {
142                 var tab = windows[i].tabs[j];
143                 if (tab.active) {
144                   callback.call(this, tab);
145                   return;
146                 }
147               }
148             }
149             callback.call(this, null);
150           }.bind(this));
151     },
153     /**
154      * This function is called when a tab is activated (comes into focus).
155      * @param {Tab} tab Current active tab.
156      * @private
157      */
158     activateTab_: function(tab) {
159       if (!tab) {
160         this.stopHotwording_();
161         return;
162       }
163       if (tab.id in this.portMap_) {
164         this.startHotwordingIfEligible_();
165         return;
166       }
167       this.stopHotwording_();
168       this.prepareTab_(tab);
169     },
171     /**
172      * Prepare a new or updated tab by injecting the content script.
173      * @param {!Tab} tab Newly updated or created tab.
174      * @private
175      */
176     prepareTab_: function(tab) {
177       if (!this.isEligibleUrl_(tab.url))
178         return;
180       chrome.tabs.executeScript(
181           tab.id,
182           {'file': 'audio_client.js'},
183           function(results) {
184             if (chrome.runtime.lastError) {
185               // Ignore this error. For new tab pages, even though the URL is
186               // reported to be chrome://newtab/, the actual URL is a
187               // country-specific google domain. Since we don't have permission
188               // to inject on every page, an error will happen when the user is
189               // in an unsupported country.
190               //
191               // The property still needs to be accessed so that the error
192               // condition is cleared. If it isn't, exectureScript will log an
193               // error the next time it is called.
194             }
195           });
196     },
198     /**
199      * Updates hotwording state based on the state of current tabs/windows.
200      * @private
201      */
202     updateTabState_: function() {
203       this.findCurrentTab_(this.activateTab_);
204     },
206     /**
207      * Handles a newly created tab.
208      * @param {!Tab} tab Newly created tab.
209      * @private
210      */
211     handleCreatedTab_: function(tab) {
212       this.prepareTab_(tab);
213     },
215     /**
216      * Handles an updated tab.
217      * @param {number} tabId Id of the updated tab.
218      * @param {{status: string}} info Change info of the tab.
219      * @param {!Tab} tab Updated tab.
220      * @private
221      */
222     handleUpdatedTab_: function(tabId, info, tab) {
223       // Chrome fires multiple update events: undefined, loading and completed.
224       // We perform content injection on loading state.
225       if (info['status'] != 'loading')
226         return;
228       this.prepareTab_(tab);
229     },
231     /**
232      * Handles a tab that has just become active.
233      * @param {{tabId: number}} info Information about the activated tab.
234      * @private
235      */
236     handleActivatedTab_: function(info) {
237       this.updateTabState_();
238     },
241     /**
242      * Handles a change in Chrome windows.
243      * Note: this does not always trigger in Linux.
244      * @param {number} windowId Id of newly focused window.
245      * @private
246      */
247     handleChangedWindow_: function(windowId) {
248       this.updateTabState_();
249     },
251     /**
252      * Handles a content script attempting to connect.
253      * @param {!Port} port Communications port from the client.
254      * @private
255      */
256     handleConnect_: function(port) {
257       if (port.name != hotword.constants.CLIENT_PORT_NAME)
258         return;
260       var tab = /** @type {!Tab} */(port.sender.tab);
261       // An existing port from the same tab might already exist. But that port
262       // may be from the previous page, so just overwrite the port.
263       this.portMap_[tab.id] = port;
264       port.onDisconnect.addListener(function() {
265         this.handleClientDisconnect_(port);
266       }.bind(this));
267       port.onMessage.addListener(function(msg) {
268         this.handleMessage_(msg, port.sender, port.postMessage);
269       }.bind(this));
270     },
272     /**
273      * Handles a client content script disconnect.
274      * @param {Port} port Disconnected port.
275      * @private
276      */
277     handleClientDisconnect_: function(port) {
278       var tabId = port.sender.tab.id;
279       if (tabId in this.portMap_ && this.portMap_[tabId] == port) {
280         // Due to a race between port disconnection and tabs.onUpdated messages,
281         // the port could have changed.
282         delete this.portMap_[port.sender.tab.id];
283       }
284       this.stopHotwordingIfIneligibleTab_();
285     },
287     /**
288      * Disconnect all connected clients.
289      * @private
290      */
291     disconnectAllClients_: function() {
292       for (var id in this.portMap_) {
293         var port = this.portMap_[id];
294         port.disconnect();
295         delete this.portMap_[id];
296       }
297     },
299     /**
300      * Sends a command to the client content script on an eligible tab.
301      * @param {hotword.constants.CommandToPage} command Command to send.
302      * @param {number} tabId Id of the target tab.
303      * @private
304      */
305     sendClient_: function(command, tabId) {
306       if (tabId in this.portMap_) {
307         var message = {};
308         message[hotword.constants.COMMAND_FIELD_NAME] = command;
309         this.portMap_[tabId].postMessage(message);
310       }
311     },
313     /**
314      * Sends a command to all connected clients.
315      * @param {hotword.constants.CommandToPage} command Command to send.
316      * @private
317      */
318     sendAllClients_: function(command) {
319       for (var idStr in this.portMap_) {
320         var id = parseInt(idStr, 10);
321         assert(!isNaN(id), 'Tab ID is not a number: ' + idStr);
322         this.sendClient_(command, id);
323       }
324     },
326     /**
327      * Handles a hotword trigger. Sends a trigger message to the currently
328      * active tab.
329      * @private
330      */
331     hotwordTriggered_: function() {
332       this.findCurrentTab_(function(tab) {
333         if (tab)
334           this.sendClient_(CommandToPage.HOTWORD_VOICE_TRIGGER, tab.id);
335       });
336     },
338     /**
339      * Starts hotwording.
340      * @private
341      */
342     startHotwording_: function() {
343       this.stateManager_.startSession(
344           hotword.constants.SessionSource.NTP,
345           function() {
346             this.sendAllClients_(CommandToPage.HOTWORD_STARTED);
347           }.bind(this),
348           this.hotwordTriggered_.bind(this));
349     },
351     /**
352      * Starts hotwording if the currently active tab is eligible for hotwording
353      * (e.g. google.com).
354      * @private
355      */
356     startHotwordingIfEligible_: function() {
357       this.findCurrentTab_(function(tab) {
358         if (!tab) {
359           this.stopHotwording_();
360           return;
361         }
362         if (this.isEligibleUrl_(tab.url))
363           this.startHotwording_();
364       });
365     },
367     /**
368      * Stops hotwording.
369      * @private
370      */
371     stopHotwording_: function() {
372       this.stateManager_.stopSession(hotword.constants.SessionSource.NTP);
373       this.sendAllClients_(CommandToPage.HOTWORD_ENDED);
374     },
376     /**
377      * Stops hotwording if the currently active tab is not eligible for
378      * hotwording (i.e. google.com).
379      * @private
380      */
381     stopHotwordingIfIneligibleTab_: function() {
382       this.findCurrentTab_(function(tab) {
383         if (!tab) {
384           this.stopHotwording_();
385           return;
386         }
387         if (!this.isEligibleUrl_(tab.url))
388           this.stopHotwording_();
389       });
390     },
392     /**
393      * Handles a message from the content script injected into the page.
394      * @param {!Object} request Request from the content script.
395      * @param {!MessageSender} sender Message sender.
396      * @param {!function(Object)} sendResponse Function for sending a response.
397      * @private
398      */
399     handleMessage_: function(request, sender, sendResponse) {
400       switch (request[hotword.constants.COMMAND_FIELD_NAME]) {
401         // TODO(amistry): Handle other messages such as CLICKED_RESUME and
402         // CLICKED_RESTART, if necessary.
403         case CommandFromPage.SPEECH_START:
404           this.stopHotwording_();
405           break;
406         case CommandFromPage.SPEECH_END:
407         case CommandFromPage.SPEECH_RESET:
408           this.startHotwording_();
409           break;
410       }
411     },
414     /**
415      * Handles a message directly from the NTP/HP/SERP.
416      * @param {!Object} request Message from the sender.
417      * @param {!MessageSender} sender Information about the sender.
418      * @param {!function(HotwordStatus)} sendResponse Callback to respond
419      *     to sender.
420      * @return {boolean} Whether to maintain the port open to call sendResponse.
421      * @private
422      */
423     handleMessageFromPage_: function(request, sender, sendResponse) {
424       switch (request.type) {
425         case CommandFromPage.PAGE_WAKEUP:
426           if (sender.tab && this.isEligibleUrl_(sender.tab.url)) {
427             chrome.hotwordPrivate.getStatus(
428                 this.statusDone_.bind(
429                     this,
430                     request.tab || sender.tab || {incognito: true},
431                     sendResponse));
432             return true;
433           }
434           break;
435         case CommandFromPage.CLICKED_OPTIN:
436           chrome.hotwordPrivate.setEnabled(true);
437           break;
438         // User has explicitly clicked 'no thanks'.
439         case CommandFromPage.CLICKED_NO_OPTIN:
440           chrome.hotwordPrivate.setEnabled(false);
441           break;
442       }
443       return false;
444     },
446     /**
447      * Sends the response to the tab.
448      * @param {Tab} tab The tab that the request was sent from.
449      * @param {function(HotwordStatus)} sendResponse Callback function to
450      *     respond to sender.
451      * @param {HotwordStatus} hotwordStatus Status of the hotword extension.
452      * @private
453      */
454     statusDone_: function(tab, sendResponse, hotwordStatus) {
455       var response = {'doNotShowOptinMessage': true};
457       // If always-on is available, then we do not show the promo, as the promo
458       // only works with the sometimes-on pref.
459       if (!tab.incognito && hotwordStatus.available &&
460           !hotwordStatus.enabledSet && !hotwordStatus.alwaysOnAvailable) {
461         response = hotwordStatus;
462       }
464       try {
465         sendResponse(response);
466       } catch (err) {
467         // Suppress the exception thrown by sendResponse() when the page doesn't
468         // specify a response callback in the call to
469         // chrome.runtime.sendMessage().
470         // Unfortunately, there doesn't appear to be a way to detect one-way
471         // messages without explicitly saying in the message itself. This
472         // message is defined as a constant in
473         // extensions/renderer/messaging_bindings.cc
474         if (err.message == 'Attempting to use a disconnected port object')
475           return;
476         throw err;
477       }
478     },
480     /**
481      * Set up event listeners.
482      * @private
483      */
484     setupListeners_: function() {
485       if (chrome.runtime.onConnect.hasListener(this.connectListener_))
486         return;
488       chrome.runtime.onConnect.addListener(this.connectListener_);
489       chrome.tabs.onCreated.addListener(this.tabCreatedListener_);
490       chrome.tabs.onUpdated.addListener(this.tabUpdatedListener_);
491       chrome.tabs.onActivated.addListener(this.tabActivatedListener_);
492       chrome.windows.onFocusChanged.addListener(
493           this.windowFocusChangedListener_);
494       if (chrome.runtime.onMessage.hasListener(this.messageListener_))
495         return;
496       chrome.runtime.onMessageExternal.addListener(
497           this.messageListener_);
498     },
500     /**
501      * Remove event listeners.
502      * @private
503      */
504     removeListeners_: function() {
505       chrome.runtime.onConnect.removeListener(this.connectListener_);
506       chrome.tabs.onCreated.removeListener(this.tabCreatedListener_);
507       chrome.tabs.onUpdated.removeListener(this.tabUpdatedListener_);
508       chrome.tabs.onActivated.removeListener(this.tabActivatedListener_);
509       chrome.windows.onFocusChanged.removeListener(
510           this.windowFocusChangedListener_);
511       // Don't remove the Message listener, as we want them listening all
512       // the time,
513     },
515     /**
516      * Update event listeners based on the current hotwording state.
517      * @private
518      */
519     updateListeners_: function() {
520       if (this.stateManager_.isSometimesOnEnabled()) {
521         this.setupListeners_();
522       } else {
523         this.removeListeners_();
524         this.stopHotwording_();
525         this.disconnectAllClients_();
526       }
527     }
528   };
530   return {
531     PageAudioManager: PageAudioManager
532   };