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 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 "."). 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. 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. 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. 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 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 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 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 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 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 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 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 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