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
24namespace keys = extension_accessibility_api_constants;
25
26namespace {
27
28static 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 ".").
48std::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.
77void 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.
85void 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.
90void 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
100namespace chromeos {
101
102void 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
114void 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
132void 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
170void 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
272void 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
298std::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
314void 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
368void 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