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