Home | History | Annotate | Download | only in login
      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