accessibility_api_handler.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)// Copyright 2014 The Chromium Authors. All rights reserved.
258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)// found in the LICENSE file.
458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)
558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)/**
658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @fileoverview Accesses Chrome's accessibility extension API and gives
758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * spoken feedback for events that happen in the "Chrome of Chrome".
858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) *
958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) */
1058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)
1158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.provide('cvox.AccessibilityApiHandler');
1258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)
1358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.AbstractEarcons');
1458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.AbstractTts');
1558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.BrailleInterface');
1658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.BrailleUtil');
1758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.ChromeVoxEditableTextBase');
1858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)goog.require('cvox.NavBraille');
19f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)
2058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)
2158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)/**
2258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * The chrome.experimental.accessibility API is moving to
2358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * chrome.accessibilityPrivate, so provide an alias during the transition.
2458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) *
2558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * TODO(dmazzoni): Remove after the stable version of Chrome no longer
2658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * has the experimental accessibility API.
2758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) */
2858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)if (!chrome.accessibilityPrivate)
2958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  chrome.accessibilityPrivate = chrome.experimental.accessibility;
3058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)
3158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)
3258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)/**
3358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * Class that adds listeners and handles events from the accessibility API.
3458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @constructor
3558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @implements {cvox.TtsCapturingEventListener}
3658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @param {cvox.TtsInterface} tts The TTS to use for speaking.
3758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @param {cvox.BrailleInterface} braille The braille interface to use for
3858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * brailing.
3958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @param {Object} earcons The earcons object to use for playing
4058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) *        earcons.
4158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) */
4258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)cvox.AccessibilityApiHandler = function(tts, braille, earcons) {
4358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  this.tts = tts;
4458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  this.braille = braille;
4558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  this.earcons = earcons;
4658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  /**
4758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)   * Tracks the previous description received.
4858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)   * @type {Object}
4958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)   * @private
5058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)   */
5158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  this.prevDescription_ = {};
5258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  try {
5358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)    chrome.accessibilityPrivate.setAccessibilityEnabled(true);
5458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)    chrome.accessibilityPrivate.setNativeAccessibilityEnabled(
5558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)        !cvox.ChromeVox.isActive);
5658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)    this.addEventListeners_();
5758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)    if (cvox.ChromeVox.isActive) {
5858537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)      this.queueAlertsForActiveTab();
5958537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)    }
6058537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  } catch (err) {
6158537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)    console.log('Error trying to access accessibility extension api.');
6258537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)  }
6358537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)};
6458537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)
6558537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles)/**
6658537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * The interface used to manage speech.
6758537e28ecd584eab876aee8be7156509866d23aTorne (Richard Coles) * @type {cvox.TtsInterface}
68 */
69cvox.AccessibilityApiHandler.prototype.tts = null;
70
71/**
72 * The interface used to manage braille.
73 * @type {cvox.BrailleInterface}
74 */
75cvox.AccessibilityApiHandler.prototype.braille = null;
76
77/**
78 * The object used to manage arcons.
79 * @type Object
80 */
81cvox.AccessibilityApiHandler.prototype.earcons = null;
82
83/**
84 * The object that can describe changes and cursor movement in a generic
85 *     editable text field.
86 * @type {Object}
87 */
88cvox.AccessibilityApiHandler.prototype.editableTextHandler = null;
89
90/**
91 * The name of the editable text field associated with
92 * |editableTextHandler|, so we can tell when focus moves.
93 * @type {string}
94 */
95cvox.AccessibilityApiHandler.prototype.editableTextName = '';
96
97/**
98 * The queue mode for the next focus event.
99 * @type {number}
100 */
101cvox.AccessibilityApiHandler.prototype.nextQueueMode = 0;
102
103/**
104 * The timeout id for the pending text changed event - the return
105 * value from window.setTimeout. We need to delay text events slightly
106 * and return only the last one because sometimes we get a rapid
107 * succession of related events that should all be considered one
108 * bulk change - in particular, autocomplete in the location bar comes
109 * as multiple events in a row.
110 * @type {?number}
111 */
112cvox.AccessibilityApiHandler.prototype.textChangeTimeout = null;
113
114/**
115 * Most controls have a "context" - the name of the window, dialog, toolbar,
116 * or menu they're contained in. We announce a context once, when you
117 * first enter it - and we don't announce it again when you move to something
118 * else within the same context. This variable keeps track of the most
119 * recent context.
120 * @type {?string}
121 */
122cvox.AccessibilityApiHandler.prototype.lastContext = null;
123
124/**
125 * Delay in ms between when a text event is received and when it's spoken.
126 * @type {number}
127 * @const
128 */
129cvox.AccessibilityApiHandler.prototype.TEXT_CHANGE_DELAY = 10;
130
131/**
132 * ID returned from setTimeout to queue up speech on idle.
133 * @type {?number}
134 * @private
135 */
136cvox.AccessibilityApiHandler.prototype.idleSpeechTimeout_ = null;
137
138/**
139 * Array of strings to speak the next time TTS is idle.
140 * @type {!Array.<string>}
141 * @private
142 */
143cvox.AccessibilityApiHandler.prototype.idleSpeechQueue_ = [];
144
145/**
146 * Milliseconds of silence to wait before considering speech to be idle.
147 * @const
148 */
149cvox.AccessibilityApiHandler.prototype.IDLE_SPEECH_DELAY_MS = 500;
150
151/**
152 * Called to let us know that the last speech came from web, and not from
153 * native UI. Clear the context and any state associated with the last
154 * focused control.
155 */
156cvox.AccessibilityApiHandler.prototype.setWebContext = function(control) {
157  // This will never be spoken - it's just supposed to be a string that
158  // won't match the context of the next control that gets focused.
159  this.lastContext = '--internal-web--';
160  this.editableTextHandler = null;
161  this.editableTextName = '';
162}
163
164/**
165 * Adds event listeners.
166 */
167cvox.AccessibilityApiHandler.prototype.addEventListeners_ = function() {
168  /** Alias getMsg as msg. */
169  var msg = goog.bind(cvox.ChromeVox.msgs.getMsg, cvox.ChromeVox.msgs);
170
171  var accessibility = chrome.accessibilityPrivate;
172
173  chrome.tabs.onCreated.addListener(goog.bind(function(tab) {
174    if (!cvox.ChromeVox.isActive) {
175      return;
176    }
177    this.tts.speak(msg('chrome_tab_created'),
178                   0,
179                   cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
180    this.braille.write(cvox.NavBraille.fromText(msg('chrome_tab_created')));
181    this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_OPEN);
182  }, this));
183
184  chrome.tabs.onRemoved.addListener(goog.bind(function(tab) {
185    if (!cvox.ChromeVox.isActive) {
186      return;
187    }
188    this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_CLOSE);
189  }, this));
190
191  chrome.tabs.onActivated.addListener(goog.bind(function(activeInfo) {
192    if (!cvox.ChromeVox.isActive) {
193      return;
194    }
195    chrome.tabs.get(activeInfo.tabId, goog.bind(function(tab) {
196      if (tab.status == 'loading') {
197        return;
198      }
199      var title = tab.title ? tab.title : tab.url;
200      this.tts.speak(msg('chrome_tab_selected',
201                         [title]),
202                     cvox.AbstractTts.QUEUE_MODE_FLUSH,
203                     cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
204      this.braille.write(
205          cvox.NavBraille.fromText(msg('chrome_tab_selected', [title])));
206      this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_SELECT);
207      this.queueAlertsForActiveTab();
208    }, this));
209  }, this));
210
211  chrome.tabs.onUpdated.addListener(goog.bind(function(tabId, selectInfo) {
212    if (!cvox.ChromeVox.isActive) {
213      return;
214    }
215    chrome.tabs.get(tabId, goog.bind(function(tab) {
216      if (!tab.active) {
217        return;
218      }
219      if (tab.status == 'loading') {
220        this.earcons.playEarcon(cvox.AbstractEarcons.BUSY_PROGRESS_LOOP);
221      } else {
222        this.earcons.playEarcon(cvox.AbstractEarcons.TASK_SUCCESS);
223      }
224    }, this));
225  }, this));
226
227  chrome.windows.onFocusChanged.addListener(goog.bind(function(windowId) {
228    if (!cvox.ChromeVox.isActive) {
229      return;
230    }
231    if (windowId == chrome.windows.WINDOW_ID_NONE) {
232      return;
233    }
234    chrome.windows.get(windowId, goog.bind(function(window) {
235      chrome.tabs.getSelected(windowId, goog.bind(function(tab) {
236        var msg_id = window.incognito ? 'chrome_incognito_window_selected' :
237          'chrome_normal_window_selected';
238        var title = tab.title ? tab.title : tab.url;
239        this.tts.speak(msg(msg_id, [title]),
240                       cvox.AbstractTts.QUEUE_MODE_FLUSH,
241                       cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
242        this.braille.write(cvox.NavBraille.fromText(msg(msg_id, [title])));
243        this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_SELECT);
244      }, this));
245    }, this));
246  }, this));
247
248  chrome.accessibilityPrivate.onWindowOpened.addListener(
249      goog.bind(function(win) {
250    if (!cvox.ChromeVox.isActive) {
251      return;
252    }
253    this.tts.speak(win.name,
254                   cvox.AbstractTts.QUEUE_MODE_FLUSH,
255                   cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
256    this.braille.write(cvox.NavBraille.fromText(win.name));
257    // Queue the next utterance because a window opening is always followed
258    // by a focus event.
259    this.nextQueueMode = 1;
260    this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_OPEN);
261    this.queueAlertsForActiveTab();
262  }, this));
263
264  chrome.accessibilityPrivate.onWindowClosed.addListener(
265      goog.bind(function(win) {
266    if (!cvox.ChromeVox.isActive) {
267      return;
268    }
269    // Don't speak, just play the earcon.
270    this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_CLOSE);
271  }, this));
272
273  chrome.accessibilityPrivate.onMenuOpened.addListener(
274      goog.bind(function(menu) {
275    if (!cvox.ChromeVox.isActive) {
276      return;
277    }
278    this.tts.speak(msg('chrome_menu_opened', [menu.name]),
279                   cvox.AbstractTts.QUEUE_MODE_FLUSH,
280                   cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
281    this.braille.write(
282        cvox.NavBraille.fromText(msg('chrome_menu_opened', [menu.name])));
283    this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_OPEN);
284  }, this));
285
286  chrome.accessibilityPrivate.onMenuClosed.addListener(
287      goog.bind(function(menu) {
288    if (!cvox.ChromeVox.isActive) {
289      return;
290    }
291    // Don't speak, just play the earcon.
292    this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_CLOSE);
293  }, this));
294
295  // systemPrivate API is only available when this extension is loaded as a
296  // component extension embedded in Chrome.
297  chrome.permissions.contains(
298      { permissions: ['systemPrivate'] },
299      goog.bind(function(result) {
300    if (!result) {
301      return;
302    }
303
304    // TODO(plundblad): Remove when the native sound is turned on by default.
305    // See crbug.com:225886.
306    var addOnVolumeChangedListener = goog.bind(function() {
307      chrome.systemPrivate.onVolumeChanged.addListener(goog.bind(
308          function(volume) {
309        if (!cvox.ChromeVox.isActive) {
310          return;
311        }
312        // Don't speak, just play the earcon.
313        this.earcons.playEarcon(cvox.AbstractEarcons.TASK_SUCCESS);
314      }, this));
315    }, this);
316    if (chrome.commandLinePrivate) {
317      chrome.commandLinePrivate.hasSwitch('disable-volume-adjust-sound',
318          goog.bind(function(result) {
319        if (result) {
320          addOnVolumeChangedListener();
321        }
322      }, this));
323    } else {
324      addOnVolumeChangedListener();
325    }
326
327    chrome.systemPrivate.onBrightnessChanged.addListener(
328        goog.bind(
329        /**
330         * @param {{brightness: number, userInitiated: boolean}} brightness
331         */
332        function(brightness) {
333          if (brightness.userInitiated) {
334            this.earcons.playEarcon(cvox.AbstractEarcons.TASK_SUCCESS);
335            this.tts.speak(
336                msg('chrome_brightness_changed', [brightness.brightness]),
337                cvox.AbstractTts.QUEUE_MODE_FLUSH,
338                cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
339            this.braille.write(cvox.NavBraille.fromText(
340                msg('chrome_brightness_changed', [brightness.brightness])));
341          }
342        }, this));
343
344    chrome.systemPrivate.onScreenUnlocked.addListener(goog.bind(function() {
345      chrome.systemPrivate.getUpdateStatus(goog.bind(function(status) {
346        if (!cvox.ChromeVox.isActive) {
347          return;
348        }
349        // Speak about system update when it's ready, otherwise speak nothing.
350        if (status.state == 'NeedRestart') {
351          this.tts.speak(msg('chrome_system_need_restart'),
352                         cvox.AbstractTts.QUEUE_MODE_FLUSH,
353                         cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
354          this.braille.write(
355              cvox.NavBraille.fromText(msg('chrome_system_need_restart')));
356        }
357      }, this));
358    }, this));
359
360    chrome.systemPrivate.onWokeUp.addListener(goog.bind(function() {
361      if (!cvox.ChromeVox.isActive) {
362        return;
363      }
364      // Don't speak, just play the earcon.
365      this.earcons.playEarcon(cvox.AbstractEarcons.OBJECT_OPEN);
366    }, this));
367  }, this));
368
369  chrome.accessibilityPrivate.onControlFocused.addListener(
370      goog.bind(this.onControlFocused, this));
371
372  chrome.accessibilityPrivate.onControlAction.addListener(
373      goog.bind(function(ctl) {
374    if (!cvox.ChromeVox.isActive) {
375      return;
376    }
377
378    var description = this.describe(ctl, true);
379    this.tts.speak(description.utterance,
380                   cvox.AbstractTts.QUEUE_MODE_FLUSH,
381                   description.ttsProps);
382    description.braille.write();
383    if (description.earcon) {
384      this.earcons.playEarcon(description.earcon);
385    }
386  }, this));
387
388  chrome.accessibilityPrivate.onTextChanged.addListener(
389       goog.bind(function(ctl) {
390    if (!cvox.ChromeVox.isActive) {
391      return;
392    }
393
394    if (!this.editableTextHandler ||
395        this.editableTextName != ctl.name ||
396        this.lastContext != ctl.context) {
397      // Chrome won't send a text change event on a control that isn't
398      // focused. If we get a text change event and it doesn't match the
399      // focused control, treat it as a focus event initially.
400      this.onControlFocused(ctl);
401      return;
402    }
403
404    // Only send the most recent text changed event - throw away anything
405    // that was pending.
406    if (this.textChangeTimeout) {
407      window.clearTimeout(this.textChangeTimeout);
408    }
409
410    // Handle the text change event after a small delay, so multiple
411    // events in rapid succession are handled as a single change. This is
412    // specifically for the location bar with autocomplete - typing a
413    // character and getting the autocompleted text and getting that
414    // text selected may be three separate events.
415    this.textChangeTimeout = window.setTimeout(
416        goog.bind(function() {
417          var textChangeEvent = new cvox.TextChangeEvent(
418              ctl.details.value,
419              ctl.details.selectionStart,
420              ctl.details.selectionEnd,
421              true);  // triggered by user
422          this.editableTextHandler.changed(
423              textChangeEvent);
424          this.describe(ctl, false).braille.write();
425        }, this), this.TEXT_CHANGE_DELAY);
426  }, this));
427
428  this.tts.addCapturingEventListener(this);
429};
430
431/**
432 * Handle the feedback when a new control gets focus.
433 * @param {AccessibilityObject} ctl The focused control.
434 */
435cvox.AccessibilityApiHandler.prototype.onControlFocused = function(ctl) {
436  if (!cvox.ChromeVox.isActive) {
437    return;
438  }
439
440  // Call this first because it may clear this.editableTextHandler.
441  var description = this.describe(ctl, false);
442
443  if (ctl.type == 'textbox') {
444    var start = ctl.details.selectionStart;
445    var end = ctl.details.selectionEnd;
446    if (start > end) {
447      start = ctl.details.selectionEnd;
448      end = ctl.details.selectionStart;
449    }
450    this.editableTextName = ctl.name;
451    this.editableTextHandler =
452        new cvox.ChromeVoxEditableTextBase(
453            ctl.details.value,
454            start,
455            end,
456            ctl.details.isPassword,
457            this.tts);
458  } else {
459    this.editableTextHandler = null;
460  }
461
462  this.tts.speak(description.utterance,
463                 this.nextQueueMode,
464                 description.ttsProps);
465  description.braille.write();
466  this.nextQueueMode = 0;
467  if (description.earcon) {
468    this.earcons.playEarcon(description.earcon);
469  }
470};
471
472/**
473 * Called when any speech starts.
474 */
475cvox.AccessibilityApiHandler.prototype.onTtsStart = function() {
476  if (this.idleSpeechTimeout_) {
477    window.clearTimeout(this.idleSpeechTimeout_);
478  }
479};
480
481/**
482 * Called when any speech ends.
483 */
484cvox.AccessibilityApiHandler.prototype.onTtsEnd = function() {
485  if (this.idleSpeechQueue_.length > 0) {
486    this.idleSpeechTimeout_ = window.setTimeout(
487        goog.bind(this.onTtsIdle, this),
488        this.IDLE_SPEECH_DELAY_MS);
489  }
490};
491
492/**
493 * Called when speech has been idle for a certain minimum delay.
494 * Speaks queued messages.
495 */
496cvox.AccessibilityApiHandler.prototype.onTtsIdle = function() {
497  if (this.idleSpeechQueue_.length == 0) {
498    return;
499  }
500  var utterance = this.idleSpeechQueue_.shift();
501  var msg = goog.bind(cvox.ChromeVox.msgs.getMsg, cvox.ChromeVox.msgs);
502  this.tts.speak(utterance,
503                 cvox.AbstractTts.QUEUE_MODE_FLUSH,
504                 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
505};
506
507/**
508 * Given a control received from the accessibility api, determine an
509 * utterance to speak, text to braille, and an earcon to play to describe it.
510 * @param {Object} control The control that had an action performed on it.
511 * @param {boolean} isSelect True if the action is a select action,
512 *     otherwise it's a focus action.
513 * @return {Object} An object containing a string field |utterance|, object
514 *      |ttsProps|, |braille|, and earcon |earcon|.
515 */
516cvox.AccessibilityApiHandler.prototype.describe = function(control, isSelect) {
517  /** Alias getMsg as msg. */
518  var msg = goog.bind(cvox.ChromeVox.msgs.getMsg, cvox.ChromeVox.msgs);
519
520  var s = '';
521  var braille = {};
522  var ttsProps = cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT;
523
524  var context = control.context;
525  if (context && context != this.lastContext) {
526    s += context + ', ';
527    this.lastContext = context;
528    this.editableTextHandler = null;
529  }
530
531  var earcon = undefined;
532  var name = control.name.replace(/[_&]+/g, '').replace('...', '');
533  braille.name = control.name;
534  switch (control.type) {
535    case 'checkbox':
536      braille.roleMsg = 'input_type_checkbox';
537      if (control.details.isChecked) {
538        earcon = cvox.AbstractEarcons.CHECK_ON;
539        s += msg('describe_checkbox_checked', [name]);
540        braille.state = msg('checkbox_checked_state_brl');
541      } else {
542        earcon = cvox.AbstractEarcons.CHECK_OFF;
543        s += msg('describe_checkbox_unchecked', [name]);
544        braille.state = msg('checkbox_unchecked_state_brl');
545      }
546      break;
547    case 'radiobutton':
548      s += name;
549      braille.roleMsg = 'input_type_radio';
550      if (control.details.isChecked) {
551        earcon = cvox.AbstractEarcons.CHECK_ON;
552        s += msg('describe_radio_selected', [name]);
553        braille.state = msg('radio_selected_state_brl');
554      } else {
555        earcon = cvox.AbstractEarcons.CHECK_OFF;
556        s += msg('describe_radio_unselected', [name]);
557        braille.state = msg('radio_unselected_state_brl');
558      }
559      break;
560    case 'menu':
561      s += msg('describe_menu', [name]);
562      braille.roleMsg = 'aria_role_menu';
563      break;
564    case 'menuitem':
565      s += msg(
566          control.details.hasSubmenu ?
567              'describe_menu_item_with_submenu' : 'describe_menu_item', [name]);
568      braille.roleMsg = 'aria_role_menuitem';
569      if (control.details.hasSubmenu) {
570        braille.state = msg('aria_has_submenu_brl');
571      }
572      break;
573    case 'window':
574      s += msg('describe_window', [name]);
575      // No specialization for braille.
576      braille.name = s;
577      break;
578    case 'alert':
579      earcon = cvox.AbstractEarcons.ALERT_NONMODAL;
580      s += msg('aria_role_alert') + ': ' + name;
581      ttsProps = cvox.AbstractTts.PERSONALITY_SYSTEM_ALERT;
582      braille.roleMsg = 'aria_role_alert';
583      isSelect = false;
584      break;
585    case 'textbox':
586      earcon = cvox.AbstractEarcons.EDITABLE_TEXT;
587      var unnamed = name == '' ? 'unnamed_' : '';
588      var type, value;
589      if (control.details.isPassword) {
590        type = 'password';
591        braille.roleMsg = 'input_type_password';
592        value = control.details.value.replace(/./g, '*');
593      } else {
594        type = 'textbox';
595        braille.roleMsg = 'input_type_text';
596        value = control.details.value;
597      }
598      s += msg('describe_' + unnamed + type, [value, name]);
599      braille.value = cvox.BrailleUtil.createValue(
600          value, control.details.selectionStart, control.details.selectionEnd);
601      break;
602    case 'button':
603      earcon = cvox.AbstractEarcons.BUTTON;
604      s += msg('describe_button', [name]);
605      braille.roleMsg = 'tag_button';
606      break;
607    case 'combobox':
608    case 'listbox':
609      earcon = cvox.AbstractEarcons.LISTBOX;
610      var unnamed = name == '' ? 'unnamed_' : '';
611      s += msg('describe_' + unnamed + control.type,
612                            [control.details.value, name]);
613      braille.roleMsg = 'tag_select';
614      break;
615    case 'link':
616      earcon = cvox.AbstractEarcons.LINK;
617      s += msg('describe_link', [name]);
618      braille.roleMsg = 'tag_link';
619      break;
620    case 'tab':
621      s += msg('describe_tab', [name]);
622      braille.roleMsg = 'aria_role_tab';
623      break;
624    case 'slider':
625      s += msg('describe_slider', [control.details.stringValue, name]);
626      braille.value = cvox.BrailleUtil.createValue(control.details.stringValue);
627      braille.roleMsg = 'aria_role_slider';
628      break;
629    case 'treeitem':
630      if (this.prevDescription_ &&
631          this.prevDescription_.details &&
632          goog.isDef(control.details.itemDepth) &&
633          this.prevDescription_.details.itemDepth !=
634              control.details.itemDepth) {
635        s += msg('describe_depth', [control.details.itemDepth]);
636      }
637      s += name + ' ' + msg('aria_role_treeitem');
638      s += control.details.isItemExpanded ?
639          msg('aria_expanded_true') : msg('aria_expanded_false');
640
641      braille.name = Array(control.details.itemDepth).join(' ') + braille.name;
642      braille.roleMsg = 'aria_role_treeitem';
643      braille.state = control.details.isItemExpanded ?
644          msg('aria_expanded_true_brl') : msg('aria_expanded_false_brl');
645      break;
646
647    default:
648      s += name + ', ' + control.type;
649      braille.role = control.type;
650  }
651
652  if (isSelect && control.type != 'slider') {
653    s += msg('describe_selected');
654  }
655  if (control.details && control.details.itemCount >= 0) {
656    s += msg('describe_index',
657        [control.details.itemIndex + 1, control.details.itemCount]);
658    braille.state = braille.state ? braille.state + ' ' : '';
659    braille.state += msg('LIST_POSITION_BRL',
660        [control.details.itemIndex + 1, control.details.itemCount]);
661  }
662
663  var description = {};
664  description.utterance = s;
665  description.ttsProps = ttsProps;
666  var spannable = cvox.BrailleUtil.getTemplated(null, null, braille);
667  var valueSelectionSpan = spannable.getSpanInstanceOf(
668      cvox.BrailleUtil.ValueSelectionSpan);
669  var brailleObj = {text: spannable};
670  if (valueSelectionSpan) {
671    brailleObj.startIndex = spannable.getSpanStart(valueSelectionSpan);
672    brailleObj.endIndex = spannable.getSpanEnd(valueSelectionSpan);
673  }
674  description.braille = new cvox.NavBraille(brailleObj);
675  description.earcon = earcon;
676  this.prevDescription_ = control;
677  return description;
678};
679
680/**
681 * Queues alerts for the active tab, if any, which will be spoken
682 * as soon as speech is idle.
683 */
684cvox.AccessibilityApiHandler.prototype.queueAlertsForActiveTab = function() {
685  this.idleSpeechQueue_.length = 0;
686  var msg = goog.bind(cvox.ChromeVox.msgs.getMsg, cvox.ChromeVox.msgs);
687
688  chrome.tabs.query({'active': true, 'currentWindow': true},
689      goog.bind(function(tabs) {
690    if (tabs.length < 1) {
691      return;
692    }
693    chrome.accessibilityPrivate.getAlertsForTab(
694        tabs[0].id, goog.bind(function(alerts) {
695      if (alerts.length == 0) {
696        return;
697      }
698
699      var utterance = '';
700
701      if (alerts.length == 1) {
702        utterance += msg('page_has_one_alert_singular');
703      } else {
704        utterance += msg('page_has_alerts_plural',
705                         [alerts.length]);
706      }
707
708      for (var i = 0; i < alerts.length; i++) {
709        utterance += ' ' + alerts[i].message;
710      }
711
712      utterance += ' ' + msg('review_alerts');
713
714      if (this.idleSpeechQueue_.indexOf(utterance) == -1) {
715        this.idleSpeechQueue_.push(utterance);
716      }
717    }, this));
718  }, this));
719};
720