• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2011 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 #include "chrome/browser/chromeos/login/wizard_accessibility_handler.h"
6 
7 #include <algorithm>
8 
9 #include "base/i18n/char_iterator.h"
10 #include "base/logging.h"
11 #include "base/memory/scoped_ptr.h"
12 #include "base/string_number_conversions.h"
13 #include "chrome/browser/accessibility_events.h"
14 #include "chrome/browser/chromeos/cros/cros_library.h"
15 #include "chrome/browser/chromeos/cros/speech_synthesis_library.h"
16 #include "chrome/browser/extensions/extension_accessibility_api.h"
17 #include "chrome/browser/extensions/extension_accessibility_api_constants.h"
18 #include "chrome/browser/profiles/profile_manager.h"
19 #include "content/common/notification_details.h"
20 #include "content/common/notification_source.h"
21 #include "grit/generated_resources.h"
22 #include "ui/base/l10n/l10n_util.h"
23 
24 namespace keys = extension_accessibility_api_constants;
25 
26 namespace {
27 
SubstringUTF8(std::string str,int start,int len)28 static std::string SubstringUTF8(std::string str, int start, int len) {
29   base::i18n::UTF8CharIterator iter(&str);
30   for (int i = 0; i < start; i++) {
31     if (!iter.Advance())
32       return std::string();
33   }
34 
35   int byte_start = iter.array_pos();
36   for (int i = 0; i < len; i++) {
37     if (!iter.Advance())
38       break;
39   }
40   int byte_len = iter.array_pos() - byte_start;
41 
42   return str.substr(byte_start, byte_len);
43 }
44 
45 // If the string consists of a single character and that character is
46 // punctuation that is not normally spoken by TTS, replace the string
47 // with a description of that character (like "period" for ".").
DescribePunctuation(const std::string & str)48 std::string DescribePunctuation(const std::string& str) {
49   if (str == "!") {
50     return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_EXCLAMATION_POINT);
51   } else if (str == "(") {
52     return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_LEFT_PAREN);
53   } else if (str == ")") {
54     return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_RIGHT_PAREN);
55   } else if (str == ";") {
56     return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_SEMICOLON);
57   } else if (str == ":") {
58     return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_COLON);
59   } else if (str == "\"") {
60     return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_QUOTE);
61   } else if (str == ",") {
62     return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_COMMA);
63   } else if (str == ".") {
64     return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_PERIOD);
65   } else if (str == " ") {
66     return l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_SPACE);
67   } else {
68     return str;
69   }
70 }
71 
72 // Append words and separate adding a space if needed.  Call
73 // DescribePunctuation on to_append so that single punctuation
74 // characters are expanded ('.' -> 'period') but punctuation
75 // in the middle of a larger phrase are handled by the speech
76 // engine.
AppendUtterance(std::string to_append,std::string * str)77 void AppendUtterance(std::string to_append, std::string* str) {
78   if ((*str).size())
79     *str += " ";
80 
81   *str += DescribePunctuation(to_append);
82 }
83 
84 // Append a localized string from its message ID, adding a space if needed.
AppendUtterance(int message_id,std::string * str)85 void AppendUtterance(int message_id, std::string* str) {
86   AppendUtterance(l10n_util::GetStringUTF8(message_id), str);
87 }
88 
89 // Append a phrase of the form "3 of 5", adding a space if needed.
AppendIndexOfCount(int index,int count,std::string * str)90 void AppendIndexOfCount(int index, int count, std::string* str) {
91   string16 index_str = base::IntToString16(index);
92   string16 count_str = base::IntToString16(count);
93   AppendUtterance(l10n_util::GetStringFUTF8(IDS_CHROMEOS_ACC_INDEX_OF_COUNT,
94                                             index_str,
95                                             count_str), str);
96 }
97 
98 }  // anonymous namespace
99 
100 namespace chromeos {
101 
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)102 void WizardAccessibilityHandler::Observe(
103     NotificationType type,
104     const NotificationSource& source,
105     const NotificationDetails& details) {
106   const AccessibilityControlInfo *control_info =
107       Details<const AccessibilityControlInfo>(details).ptr();
108   std::string description;
109   EarconType earcon = NO_EARCON;
110   DescribeAccessibilityEvent(type, control_info, &description, &earcon);
111   Speak(description.c_str(), false, true);
112 }
113 
Speak(const char * speak_str,bool queue,bool interruptible)114 void WizardAccessibilityHandler::Speak(const char* speak_str,
115                                        bool queue,
116                                        bool interruptible) {
117   if (chromeos::CrosLibrary::Get()->EnsureLoaded()) {
118     if (queue || !interruptible) {
119       std::string props = "";
120       props.append("enqueue=");
121       props.append(queue ? "1;" : "0;");
122       props.append("interruptible=");
123       props.append(interruptible ? "1;" : "0;");
124       chromeos::CrosLibrary::Get()->GetSpeechSynthesisLibrary()->
125           SetSpeakProperties(props.c_str());
126     }
127     chromeos::CrosLibrary::Get()->GetSpeechSynthesisLibrary()->
128         Speak(speak_str);
129   }
130 }
131 
DescribeAccessibilityEvent(NotificationType event_type,const AccessibilityControlInfo * control_info,std::string * out_spoken_description,EarconType * out_earcon)132 void WizardAccessibilityHandler::DescribeAccessibilityEvent(
133     NotificationType event_type,
134     const AccessibilityControlInfo* control_info,
135     std::string* out_spoken_description,
136     EarconType* out_earcon) {
137   *out_spoken_description = std::string();
138   *out_earcon = NO_EARCON;
139 
140   switch (event_type.value) {
141     case NotificationType::ACCESSIBILITY_CONTROL_FOCUSED:
142       DescribeControl(control_info, false, out_spoken_description, out_earcon);
143       break;
144     case NotificationType::ACCESSIBILITY_CONTROL_ACTION:
145       DescribeControl(control_info, true, out_spoken_description, out_earcon);
146       break;
147     case NotificationType::ACCESSIBILITY_TEXT_CHANGED:
148       DescribeTextChanged(control_info, out_spoken_description, out_earcon);
149       break;
150     case NotificationType::ACCESSIBILITY_MENU_OPENED:
151       *out_earcon = EARCON_OBJECT_OPENED;
152       break;
153     case NotificationType::ACCESSIBILITY_MENU_CLOSED:
154       *out_earcon = EARCON_OBJECT_CLOSED;
155       break;
156     default:
157       NOTREACHED();
158       return;
159   }
160 
161   if (control_info->type() == keys::kTypeTextBox) {
162     const AccessibilityTextBoxInfo* text_box =
163         static_cast<const AccessibilityTextBoxInfo*>(control_info);
164     previous_text_value_ = GetTextBoxValue(text_box);
165     previous_text_selection_start_ = text_box->selection_start();
166     previous_text_selection_end_ = text_box->selection_end();
167   }
168 }
169 
DescribeControl(const AccessibilityControlInfo * control_info,bool is_action,std::string * out_spoken_description,EarconType * out_earcon)170 void WizardAccessibilityHandler::DescribeControl(
171     const AccessibilityControlInfo* control_info,
172     bool is_action,
173     std::string* out_spoken_description,
174     EarconType* out_earcon) {
175   if (control_info->type() == keys::kTypeButton) {
176     *out_earcon = EARCON_BUTTON;
177     AppendUtterance(control_info->name(), out_spoken_description);
178     AppendUtterance(IDS_CHROMEOS_ACC_BUTTON, out_spoken_description);
179   } else if (control_info->type() == keys::kTypeCheckbox) {
180     AppendUtterance(control_info->name(), out_spoken_description);
181     const AccessibilityCheckboxInfo* checkbox_info =
182         static_cast<const AccessibilityCheckboxInfo*>(control_info);
183     if (checkbox_info->checked()) {
184       *out_earcon = EARCON_CHECK_ON;
185       AppendUtterance(IDS_CHROMEOS_ACC_CHECKBOX_CHECKED,
186                       out_spoken_description);
187     } else {
188       *out_earcon = EARCON_CHECK_OFF;
189       AppendUtterance(IDS_CHROMEOS_ACC_CHECKBOX_UNCHECKED,
190                       out_spoken_description);
191     }
192   } else if (control_info->type() == keys::kTypeComboBox) {
193     *out_earcon = EARCON_LISTBOX;
194     const AccessibilityComboBoxInfo* combobox_info =
195         static_cast<const AccessibilityComboBoxInfo*>(control_info);
196     AppendUtterance(combobox_info->value(), out_spoken_description);
197     AppendUtterance(combobox_info->name(), out_spoken_description);
198     AppendUtterance(IDS_CHROMEOS_ACC_COMBOBOX, out_spoken_description);
199     AppendIndexOfCount(combobox_info->item_index() + 1,
200                        combobox_info->item_count(),
201                        out_spoken_description);
202   } else if (control_info->type() == keys::kTypeLink) {
203     *out_earcon = EARCON_LINK;
204     AppendUtterance(control_info->name(), out_spoken_description);
205     AppendUtterance(IDS_CHROMEOS_ACC_LINK, out_spoken_description);
206   } else if (control_info->type() == keys::kTypeListBox) {
207     *out_earcon = EARCON_LISTBOX;
208     const AccessibilityListBoxInfo* listbox_info =
209         static_cast<const AccessibilityListBoxInfo*>(control_info);
210     AppendUtterance(listbox_info->value(), out_spoken_description);
211     AppendUtterance(listbox_info->name(), out_spoken_description);
212     AppendUtterance(IDS_CHROMEOS_ACC_LISTBOX, out_spoken_description);
213     AppendIndexOfCount(listbox_info->item_index() + 1,
214                        listbox_info->item_count(),
215                        out_spoken_description);
216   } else if (control_info->type() == keys::kTypeMenu) {
217     *out_earcon = EARCON_MENU;
218     AppendUtterance(control_info->name(), out_spoken_description);
219     AppendUtterance(IDS_CHROMEOS_ACC_MENU, out_spoken_description);
220   } else if (control_info->type() == keys::kTypeMenuItem) {
221     const AccessibilityMenuItemInfo* menu_item_info =
222         static_cast<const AccessibilityMenuItemInfo*>(control_info);
223     AppendUtterance(menu_item_info->name(), out_spoken_description);
224     if (menu_item_info->has_submenu())
225       AppendUtterance(IDS_CHROMEOS_ACC_HAS_SUBMENU, out_spoken_description);
226     AppendIndexOfCount(menu_item_info->item_index() + 1,
227                        menu_item_info->item_count(),
228                        out_spoken_description);
229   } else if (control_info->type() == keys::kTypeRadioButton) {
230     AppendUtterance(control_info->name(), out_spoken_description);
231     const AccessibilityRadioButtonInfo* radio_info =
232         static_cast<const AccessibilityRadioButtonInfo*>(control_info);
233     if (radio_info->checked()) {
234       *out_earcon = EARCON_CHECK_ON;
235       AppendUtterance(IDS_CHROMEOS_ACC_RADIO_SELECTED, out_spoken_description);
236     } else {
237       *out_earcon = EARCON_CHECK_OFF;
238       AppendUtterance(IDS_CHROMEOS_ACC_RADIO_UNSELECTED,
239                       out_spoken_description);
240     }
241     AppendIndexOfCount(radio_info->item_index() + 1,
242                        radio_info->item_count(),
243                        out_spoken_description);
244   } else if (control_info->type() == keys::kTypeTab) {
245     *out_earcon = EARCON_TAB;
246     AppendUtterance(control_info->name(), out_spoken_description);
247     const AccessibilityTabInfo* tab_info =
248         static_cast<const AccessibilityTabInfo*>(control_info);
249     AppendUtterance(IDS_CHROMEOS_ACC_TAB, out_spoken_description);
250     AppendIndexOfCount(tab_info->tab_index() + 1,
251                        tab_info->tab_count(),
252                        out_spoken_description);
253   } else if (control_info->type() == keys::kTypeTextBox) {
254     *out_earcon = EARCON_TEXTBOX;
255     const AccessibilityTextBoxInfo* textbox_info =
256         static_cast<const AccessibilityTextBoxInfo*>(control_info);
257     AppendUtterance(GetTextBoxValue(textbox_info), out_spoken_description);
258     AppendUtterance(textbox_info->name(), out_spoken_description);
259     if (textbox_info->password()) {
260       AppendUtterance(IDS_CHROMEOS_ACC_PASSWORDBOX, out_spoken_description);
261     } else {
262       AppendUtterance(IDS_CHROMEOS_ACC_TEXTBOX, out_spoken_description);
263     }
264   } else if (control_info->type() == keys::kTypeWindow) {
265     // No feedback when a window gets focus
266   }
267 
268   if (is_action)
269     AppendUtterance(IDS_CHROMEOS_ACC_SELECTED, out_spoken_description);
270 }
271 
DescribeTextChanged(const AccessibilityControlInfo * control_info,std::string * out_spoken_description,EarconType * out_earcon)272 void WizardAccessibilityHandler::DescribeTextChanged(
273     const AccessibilityControlInfo* control_info,
274     std::string* out_spoken_description,
275     EarconType* out_earcon) {
276   DCHECK_EQ(control_info->type(), keys::kTypeTextBox);
277   const AccessibilityTextBoxInfo* text_box =
278       static_cast<const AccessibilityTextBoxInfo*>(control_info);
279 
280   std::string old_value = previous_text_value_;
281   int old_start = previous_text_selection_start_;
282   int old_end = previous_text_selection_end_;
283   std::string new_value = GetTextBoxValue(text_box);
284   int new_start = text_box->selection_start();
285   int new_end = text_box->selection_end();
286 
287   if (new_value == old_value) {
288     DescribeTextSelectionChanged(new_value,
289                                  old_start, old_end,
290                                  new_start, new_end,
291                                  out_spoken_description);
292   } else {
293     DescribeTextContentsChanged(old_value, new_value,
294                                 out_spoken_description);
295   }
296 }
297 
GetTextBoxValue(const AccessibilityTextBoxInfo * textbox_info)298 std::string WizardAccessibilityHandler::GetTextBoxValue(
299     const AccessibilityTextBoxInfo* textbox_info) {
300   std::string value = textbox_info->value();
301   if (textbox_info->password()) {
302     base::i18n::UTF8CharIterator iter(&value);
303     std::string obscured;
304     while (!iter.end()) {
305       obscured += "*";
306       iter.Advance();
307     }
308     return obscured;
309   } else {
310     return value;
311   }
312 }
313 
DescribeTextSelectionChanged(const std::string & value,int old_start,int old_end,int new_start,int new_end,std::string * out_spoken_description)314 void WizardAccessibilityHandler::DescribeTextSelectionChanged(
315     const std::string& value,
316     int old_start,
317     int old_end,
318     int new_start,
319     int new_end,
320     std::string* out_spoken_description) {
321   if (new_start == new_end) {
322     // It's currently a cursor.
323     if (old_start != old_end) {
324       // It was previously a selection, so just announce 'unselected'.
325       AppendUtterance(IDS_CHROMEOS_ACC_TEXT_UNSELECTED, out_spoken_description);
326     } else if (old_start == new_start + 1 || old_start == new_start - 1) {
327       // Moved by one character; read it.
328       AppendUtterance(SubstringUTF8(value, std::min(old_start, new_start), 1),
329                       out_spoken_description);
330     } else {
331       // Moved by more than one character. Read all characters crossed.
332       AppendUtterance(SubstringUTF8(value,
333                                     std::min(old_start, new_start),
334                                     abs(old_start - new_start)),
335                       out_spoken_description);
336     }
337   } else {
338     // It's currently a selection.
339     if (old_start == old_end) {
340       // It was previously a cursor.
341       AppendUtterance(SubstringUTF8(value, new_start, new_end - new_start),
342                       out_spoken_description);
343     } else if (old_start == new_start && old_end < new_end) {
344       // Added to end of selection.
345       AppendUtterance(SubstringUTF8(value, old_end, new_end - old_end),
346                       out_spoken_description);
347     } else if (old_start == new_start && old_end > new_end) {
348       // Removed from end of selection.
349       AppendUtterance(SubstringUTF8(value, new_end, old_end - new_end),
350                       out_spoken_description);
351     } else if (old_end == new_end && old_start > new_start) {
352       // Added to beginning of selection.
353       AppendUtterance(SubstringUTF8(value, new_start, old_start - new_start),
354                       out_spoken_description);
355     } else if (old_end == new_end && old_start < new_start) {
356       // Removed from beginning of selection.
357       AppendUtterance(SubstringUTF8(value, old_start, new_start - old_start),
358                       out_spoken_description);
359     } else {
360       // The selection changed but it wasn't an obvious extension of
361       // a previous selection. Just read the new selection.
362       AppendUtterance(SubstringUTF8(value, new_start, new_end - new_start),
363                       out_spoken_description);
364     }
365   }
366 }
367 
DescribeTextContentsChanged(const std::string & old_value,const std::string & new_value,std::string * out_spoken_description)368 void WizardAccessibilityHandler::DescribeTextContentsChanged(
369     const std::string& old_value,
370     const std::string& new_value,
371     std::string* out_spoken_description) {
372   int old_array_len = old_value.size();
373   int new_array_len = new_value.size();
374 
375   // Get the unicode characters and indices of the start of each
376   // character's UTF8-encoded representation.
377   scoped_array<int32> old_chars(new int32[old_array_len]);
378   scoped_array<int> old_indices(new int[old_array_len + 1]);
379   base::i18n::UTF8CharIterator old_iter(&old_value);
380   while (!old_iter.end()) {
381     old_chars[old_iter.char_pos()] = old_iter.get();
382     old_indices[old_iter.char_pos()] = old_iter.array_pos();
383     old_iter.Advance();
384   }
385   int old_char_len = old_iter.char_pos();
386   old_indices[old_char_len] = old_iter.array_pos();
387 
388   scoped_array<int32> new_chars(new int32[new_array_len]);
389   scoped_array<int> new_indices(new int[new_array_len + 1]);
390   base::i18n::UTF8CharIterator new_iter(&new_value);
391   while (!new_iter.end()) {
392     new_chars[new_iter.char_pos()] = new_iter.get();
393     new_indices[new_iter.char_pos()] = new_iter.array_pos();
394     new_iter.Advance();
395   }
396   int new_char_len = new_iter.char_pos();
397   new_indices[new_char_len] = new_iter.array_pos();
398 
399   // Find the common prefix of the two strings.
400   int prefix_char_len = 0;
401   while (prefix_char_len < old_char_len &&
402          prefix_char_len < new_char_len &&
403          old_chars[prefix_char_len] == new_chars[prefix_char_len]) {
404     prefix_char_len++;
405   }
406 
407   // Find the common suffix of the two stirngs.
408   int suffix_char_len = 0;
409   while (suffix_char_len < old_char_len - prefix_char_len &&
410          suffix_char_len < new_char_len - prefix_char_len &&
411          (old_chars[old_char_len - suffix_char_len - 1] ==
412           new_chars[new_char_len - suffix_char_len - 1])) {
413     suffix_char_len++;
414   }
415 
416   int old_suffix_char_start = old_char_len - suffix_char_len;
417   int new_suffix_char_start = new_char_len - suffix_char_len;
418 
419   // Find the substring that was deleted (if any) to get the new string
420   // from the old - it's the part in the middle of the old string if you
421   // remove the common prefix and suffix.
422   std::string deleted = old_value.substr(
423       old_indices[prefix_char_len],
424       old_indices[old_suffix_char_start] - old_indices[prefix_char_len]);
425 
426   // Find the substring that was inserted (if any) to get the new string
427   // from the old - it's the part in the middle of the new string if you
428   // remove the common prefix and suffix.
429   std::string inserted = new_value.substr(
430       new_indices[prefix_char_len],
431       new_indices[new_suffix_char_start] - new_indices[prefix_char_len]);
432 
433   if (!inserted.empty() && !deleted.empty()) {
434     // Replace one substring with another, speak inserted text.
435     AppendUtterance(inserted, out_spoken_description);
436   } else if (!inserted.empty()) {
437     // Speak inserted text.
438     AppendUtterance(inserted, out_spoken_description);
439   } else if (!deleted.empty()) {
440     // Speak deleted text.
441     AppendUtterance(deleted, out_spoken_description);
442   }
443 }
444 
445 }  // namespace chromeos
446