1// Copyright (c) 2013 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/accessibility/accessibility_manager.h"
6
7#include "ash/audio/sounds.h"
8#include "ash/autoclick/autoclick_controller.h"
9#include "ash/high_contrast/high_contrast_controller.h"
10#include "ash/metrics/user_metrics_recorder.h"
11#include "ash/session/session_state_delegate.h"
12#include "ash/shell.h"
13#include "ash/sticky_keys/sticky_keys_controller.h"
14#include "ash/system/tray/system_tray_notifier.h"
15#include "base/callback.h"
16#include "base/callback_helpers.h"
17#include "base/memory/scoped_ptr.h"
18#include "base/memory/singleton.h"
19#include "base/metrics/histogram.h"
20#include "base/path_service.h"
21#include "base/prefs/pref_member.h"
22#include "base/prefs/pref_service.h"
23#include "base/strings/string_split.h"
24#include "base/strings/string_util.h"
25#include "base/time/time.h"
26#include "base/values.h"
27#include "chrome/browser/accessibility/accessibility_extension_api.h"
28#include "chrome/browser/browser_process.h"
29#include "chrome/browser/chrome_notification_types.h"
30#include "chrome/browser/chromeos/accessibility/magnification_manager.h"
31#include "chrome/browser/chromeos/login/lock/screen_locker.h"
32#include "chrome/browser/chromeos/login/ui/login_display_host.h"
33#include "chrome/browser/chromeos/login/ui/login_display_host_impl.h"
34#include "chrome/browser/chromeos/login/ui/webui_login_view.h"
35#include "chrome/browser/chromeos/profiles/profile_helper.h"
36#include "chrome/browser/chromeos/ui/accessibility_focus_ring_controller.h"
37#include "chrome/browser/extensions/component_loader.h"
38#include "chrome/browser/extensions/extension_service.h"
39#include "chrome/browser/profiles/profile.h"
40#include "chrome/browser/profiles/profile_manager.h"
41#include "chrome/common/chrome_paths.h"
42#include "chrome/common/extensions/api/accessibility_private.h"
43#include "chrome/common/extensions/extension_constants.h"
44#include "chrome/common/extensions/manifest_handlers/content_scripts_handler.h"
45#include "chrome/common/pref_names.h"
46#include "chrome/grit/browser_resources.h"
47#include "chromeos/audio/chromeos_sounds.h"
48#include "chromeos/ime/input_method_manager.h"
49#include "chromeos/login/login_state.h"
50#include "components/user_manager/user_manager.h"
51#include "content/public/browser/browser_accessibility_state.h"
52#include "content/public/browser/browser_thread.h"
53#include "content/public/browser/notification_details.h"
54#include "content/public/browser/notification_service.h"
55#include "content/public/browser/notification_source.h"
56#include "content/public/browser/render_process_host.h"
57#include "content/public/browser/render_view_host.h"
58#include "content/public/browser/web_contents.h"
59#include "content/public/browser/web_ui.h"
60#include "extensions/browser/extension_system.h"
61#include "extensions/browser/file_reader.h"
62#include "extensions/common/extension.h"
63#include "extensions/common/extension_messages.h"
64#include "extensions/common/extension_resource.h"
65#include "media/audio/sounds/sounds_manager.h"
66#include "ui/base/resource/resource_bundle.h"
67#include "ui/keyboard/keyboard_controller.h"
68#include "ui/keyboard/keyboard_util.h"
69
70using content::BrowserThread;
71using content::RenderViewHost;
72using extensions::api::braille_display_private::BrailleController;
73using extensions::api::braille_display_private::DisplayState;
74using extensions::api::braille_display_private::KeyEvent;
75
76namespace chromeos {
77
78namespace {
79
80static chromeos::AccessibilityManager* g_accessibility_manager = NULL;
81
82static BrailleController* g_braille_controller_for_test = NULL;
83
84BrailleController* GetBrailleController() {
85  return g_braille_controller_for_test
86      ? g_braille_controller_for_test
87      : BrailleController::GetInstance();
88}
89
90base::FilePath GetChromeVoxPath() {
91  base::FilePath path;
92  if (!PathService::Get(chrome::DIR_RESOURCES, &path))
93    NOTREACHED();
94  path = path.Append(extension_misc::kChromeVoxExtensionPath);
95  return path;
96}
97
98// Helper class that directly loads an extension's content scripts into
99// all of the frames corresponding to a given RenderViewHost.
100class ContentScriptLoader {
101 public:
102  // Initialize the ContentScriptLoader with the ID of the extension
103  // and the RenderViewHost where the scripts should be loaded.
104  ContentScriptLoader(const std::string& extension_id,
105                      int render_process_id,
106                      int render_view_id)
107      : extension_id_(extension_id),
108        render_process_id_(render_process_id),
109        render_view_id_(render_view_id) {}
110
111  // Call this once with the ExtensionResource corresponding to each
112  // content script to be loaded.
113  void AppendScript(extensions::ExtensionResource resource) {
114    resources_.push(resource);
115  }
116
117  // Finally, call this method once to fetch all of the resources and
118  // load them. This method will delete this object when done.
119  void Run() {
120    if (resources_.empty()) {
121      delete this;
122      return;
123    }
124
125    extensions::ExtensionResource resource = resources_.front();
126    resources_.pop();
127    scoped_refptr<FileReader> reader(new FileReader(resource, base::Bind(
128        &ContentScriptLoader::OnFileLoaded, base::Unretained(this))));
129    reader->Start();
130  }
131
132 private:
133  void OnFileLoaded(bool success, const std::string& data) {
134    if (success) {
135      ExtensionMsg_ExecuteCode_Params params;
136      params.request_id = 0;
137      params.extension_id = extension_id_;
138      params.is_javascript = true;
139      params.code = data;
140      params.run_at = extensions::UserScript::DOCUMENT_IDLE;
141      params.all_frames = true;
142      params.match_about_blank = false;
143      params.in_main_world = false;
144
145      RenderViewHost* render_view_host =
146          RenderViewHost::FromID(render_process_id_, render_view_id_);
147      if (render_view_host) {
148        render_view_host->Send(new ExtensionMsg_ExecuteCode(
149            render_view_host->GetRoutingID(), params));
150      }
151    }
152    Run();
153  }
154
155  std::string extension_id_;
156  int render_process_id_;
157  int render_view_id_;
158  std::queue<extensions::ExtensionResource> resources_;
159};
160
161void InjectChromeVoxContentScript(
162    ExtensionService* extension_service,
163    int render_process_id,
164    int render_view_id,
165    const base::Closure& done_cb);
166
167void LoadChromeVoxExtension(
168    Profile* profile,
169    RenderViewHost* render_view_host,
170    base::Closure done_cb) {
171  ExtensionService* extension_service =
172      extensions::ExtensionSystem::Get(profile)->extension_service();
173  if (render_view_host) {
174    // Wrap the passed in callback to inject the content script.
175    done_cb = base::Bind(
176        &InjectChromeVoxContentScript,
177        extension_service,
178        render_view_host->GetProcess()->GetID(),
179        render_view_host->GetRoutingID(),
180        done_cb);
181  }
182  extension_service->component_loader()->AddChromeVoxExtension(done_cb);
183}
184
185void InjectChromeVoxContentScript(
186    ExtensionService* extension_service,
187    int render_process_id,
188    int render_view_id,
189    const base::Closure& done_cb) {
190  // Make sure to always run |done_cb|.  ChromeVox was loaded even if we end up
191  // not injecting into this particular render view.
192  base::ScopedClosureRunner done_runner(done_cb);
193  RenderViewHost* render_view_host =
194      RenderViewHost::FromID(render_process_id, render_view_id);
195  if (!render_view_host)
196    return;
197  const extensions::Extension* extension =
198      extension_service->extensions()->GetByID(
199          extension_misc::kChromeVoxExtensionId);
200
201  // Set a flag to tell ChromeVox that it's just been enabled,
202  // so that it won't interrupt our speech feedback enabled message.
203  ExtensionMsg_ExecuteCode_Params params;
204  params.request_id = 0;
205  params.extension_id = extension->id();
206  params.is_javascript = true;
207  params.code = "window.INJECTED_AFTER_LOAD = true;";
208  params.run_at = extensions::UserScript::DOCUMENT_IDLE;
209  params.all_frames = true;
210  params.match_about_blank = false;
211  params.in_main_world = false;
212  render_view_host->Send(new ExtensionMsg_ExecuteCode(
213      render_view_host->GetRoutingID(), params));
214
215  // Inject ChromeVox' content scripts.
216  ContentScriptLoader* loader = new ContentScriptLoader(
217      extension->id(), render_view_host->GetProcess()->GetID(),
218      render_view_host->GetRoutingID());
219
220  const extensions::UserScriptList& content_scripts =
221      extensions::ContentScriptsInfo::GetContentScripts(extension);
222  for (size_t i = 0; i < content_scripts.size(); i++) {
223    const extensions::UserScript& script = content_scripts[i];
224    for (size_t j = 0; j < script.js_scripts().size(); ++j) {
225      const extensions::UserScript::File& file = script.js_scripts()[j];
226      extensions::ExtensionResource resource = extension->GetResource(
227          file.relative_path());
228      loader->AppendScript(resource);
229    }
230  }
231  loader->Run();  // It cleans itself up when done.
232}
233
234void UnloadChromeVoxExtension(Profile* profile) {
235  base::FilePath path = GetChromeVoxPath();
236  ExtensionService* extension_service =
237      extensions::ExtensionSystem::Get(profile)->extension_service();
238  extension_service->component_loader()->Remove(path);
239}
240
241}  // namespace
242
243///////////////////////////////////////////////////////////////////////////////
244// AccessibilityStatusEventDetails
245
246AccessibilityStatusEventDetails::AccessibilityStatusEventDetails(
247    AccessibilityNotificationType notification_type,
248    bool enabled,
249    ash::AccessibilityNotificationVisibility notify)
250  : notification_type(notification_type),
251    enabled(enabled),
252    magnifier_type(ash::kDefaultMagnifierType),
253    notify(notify) {}
254
255AccessibilityStatusEventDetails::AccessibilityStatusEventDetails(
256    AccessibilityNotificationType notification_type,
257    bool enabled,
258    ash::MagnifierType magnifier_type,
259    ash::AccessibilityNotificationVisibility notify)
260  : notification_type(notification_type),
261    enabled(enabled),
262    magnifier_type(magnifier_type),
263    notify(notify) {}
264
265///////////////////////////////////////////////////////////////////////////////
266//
267// AccessibilityManager::PrefHandler
268
269AccessibilityManager::PrefHandler::PrefHandler(const char* pref_path)
270    : pref_path_(pref_path) {}
271
272AccessibilityManager::PrefHandler::~PrefHandler() {}
273
274void AccessibilityManager::PrefHandler::HandleProfileChanged(
275    Profile* previous_profile, Profile* current_profile) {
276  // Returns if the current profile is null.
277  if (!current_profile)
278    return;
279
280  // If the user set a pref value on the login screen and is now starting a
281  // session with a new profile, copy the pref value to the profile.
282  if ((previous_profile &&
283       ProfileHelper::IsSigninProfile(previous_profile) &&
284       current_profile->IsNewProfile() &&
285       !ProfileHelper::IsSigninProfile(current_profile)) ||
286      // Special case for Guest mode:
287      // Guest mode launches a guest-mode browser process before session starts,
288      // so the previous profile is null.
289      (!previous_profile &&
290       current_profile->IsGuestSession())) {
291    // Returns if the pref has not been set by the user.
292    const PrefService::Preference* pref = ProfileHelper::GetSigninProfile()->
293        GetPrefs()->FindPreference(pref_path_);
294    if (!pref || !pref->IsUserControlled())
295      return;
296
297    // Copy the pref value from the signin screen.
298    const base::Value* value_on_login = pref->GetValue();
299    PrefService* user_prefs = current_profile->GetPrefs();
300    user_prefs->Set(pref_path_, *value_on_login);
301  }
302}
303
304///////////////////////////////////////////////////////////////////////////////
305//
306// AccessibilityManager
307
308// static
309void AccessibilityManager::Initialize() {
310  CHECK(g_accessibility_manager == NULL);
311  g_accessibility_manager = new AccessibilityManager();
312}
313
314// static
315void AccessibilityManager::Shutdown() {
316  CHECK(g_accessibility_manager);
317  delete g_accessibility_manager;
318  g_accessibility_manager = NULL;
319}
320
321// static
322AccessibilityManager* AccessibilityManager::Get() {
323  return g_accessibility_manager;
324}
325
326AccessibilityManager::AccessibilityManager()
327    : profile_(NULL),
328      chrome_vox_loaded_on_lock_screen_(false),
329      chrome_vox_loaded_on_user_screen_(false),
330      large_cursor_pref_handler_(prefs::kAccessibilityLargeCursorEnabled),
331      spoken_feedback_pref_handler_(prefs::kAccessibilitySpokenFeedbackEnabled),
332      high_contrast_pref_handler_(prefs::kAccessibilityHighContrastEnabled),
333      autoclick_pref_handler_(prefs::kAccessibilityAutoclickEnabled),
334      autoclick_delay_pref_handler_(prefs::kAccessibilityAutoclickDelayMs),
335      virtual_keyboard_pref_handler_(
336          prefs::kAccessibilityVirtualKeyboardEnabled),
337      large_cursor_enabled_(false),
338      sticky_keys_enabled_(false),
339      spoken_feedback_enabled_(false),
340      high_contrast_enabled_(false),
341      autoclick_enabled_(false),
342      autoclick_delay_ms_(ash::AutoclickController::kDefaultAutoclickDelayMs),
343      virtual_keyboard_enabled_(false),
344      spoken_feedback_notification_(ash::A11Y_NOTIFICATION_NONE),
345      should_speak_chrome_vox_announcements_on_user_screen_(true),
346      system_sounds_enabled_(false),
347      braille_display_connected_(false),
348      scoped_braille_observer_(this),
349      braille_ime_current_(false),
350      weak_ptr_factory_(this) {
351  notification_registrar_.Add(this,
352                              chrome::NOTIFICATION_LOGIN_OR_LOCK_WEBUI_VISIBLE,
353                              content::NotificationService::AllSources());
354  notification_registrar_.Add(this,
355                              chrome::NOTIFICATION_SESSION_STARTED,
356                              content::NotificationService::AllSources());
357  notification_registrar_.Add(this,
358                              chrome::NOTIFICATION_PROFILE_DESTROYED,
359                              content::NotificationService::AllSources());
360  notification_registrar_.Add(this,
361                              chrome::NOTIFICATION_SCREEN_LOCK_STATE_CHANGED,
362                              content::NotificationService::AllSources());
363
364  input_method::InputMethodManager::Get()->AddObserver(this);
365
366  ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
367  media::SoundsManager* manager = media::SoundsManager::Get();
368  manager->Initialize(SOUND_SHUTDOWN,
369                      bundle.GetRawDataResource(IDR_SOUND_SHUTDOWN_WAV));
370  manager->Initialize(
371      SOUND_SPOKEN_FEEDBACK_ENABLED,
372      bundle.GetRawDataResource(IDR_SOUND_SPOKEN_FEEDBACK_ENABLED_WAV));
373  manager->Initialize(
374      SOUND_SPOKEN_FEEDBACK_DISABLED,
375      bundle.GetRawDataResource(IDR_SOUND_SPOKEN_FEEDBACK_DISABLED_WAV));
376  manager->Initialize(SOUND_PASSTHROUGH,
377                      bundle.GetRawDataResource(IDR_SOUND_PASSTHROUGH_WAV));
378  manager->Initialize(SOUND_EXIT_SCREEN,
379                      bundle.GetRawDataResource(IDR_SOUND_EXIT_SCREEN_WAV));
380  manager->Initialize(SOUND_ENTER_SCREEN,
381                      bundle.GetRawDataResource(IDR_SOUND_ENTER_SCREEN_WAV));
382}
383
384AccessibilityManager::~AccessibilityManager() {
385  CHECK(this == g_accessibility_manager);
386  AccessibilityStatusEventDetails details(
387      ACCESSIBILITY_MANAGER_SHUTDOWN,
388      false,
389      ash::A11Y_NOTIFICATION_NONE);
390  NotifyAccessibilityStatusChanged(details);
391  input_method::InputMethodManager::Get()->RemoveObserver(this);
392}
393
394bool AccessibilityManager::ShouldShowAccessibilityMenu() {
395  // If any of the loaded profiles has an accessibility feature turned on - or
396  // enforced to always show the menu - we return true to show the menu.
397  std::vector<Profile*> profiles =
398      g_browser_process->profile_manager()->GetLoadedProfiles();
399  for (std::vector<Profile*>::iterator it = profiles.begin();
400       it != profiles.end();
401       ++it) {
402    PrefService* pref_service = (*it)->GetPrefs();
403    if (pref_service->GetBoolean(prefs::kAccessibilityStickyKeysEnabled) ||
404        pref_service->GetBoolean(prefs::kAccessibilityLargeCursorEnabled) ||
405        pref_service->GetBoolean(prefs::kAccessibilitySpokenFeedbackEnabled) ||
406        pref_service->GetBoolean(prefs::kAccessibilityHighContrastEnabled) ||
407        pref_service->GetBoolean(prefs::kAccessibilityAutoclickEnabled) ||
408        pref_service->GetBoolean(prefs::kShouldAlwaysShowAccessibilityMenu) ||
409        pref_service->GetBoolean(prefs::kAccessibilityScreenMagnifierEnabled) ||
410        pref_service->GetBoolean(prefs::kAccessibilityVirtualKeyboardEnabled))
411      return true;
412  }
413  return false;
414}
415
416bool AccessibilityManager::ShouldEnableCursorCompositing() {
417#if defined(OS_CHROMEOS)
418  if (!profile_)
419    return false;
420  PrefService* pref_service = profile_->GetPrefs();
421  // Enable cursor compositing when one or more of the listed accessibility
422  // features are turned on.
423  if (pref_service->GetBoolean(prefs::kAccessibilityLargeCursorEnabled) ||
424      pref_service->GetBoolean(prefs::kAccessibilityHighContrastEnabled) ||
425      pref_service->GetBoolean(prefs::kAccessibilityScreenMagnifierEnabled))
426    return true;
427#endif
428  return false;
429}
430
431void AccessibilityManager::EnableLargeCursor(bool enabled) {
432  if (!profile_)
433    return;
434
435  PrefService* pref_service = profile_->GetPrefs();
436  pref_service->SetBoolean(prefs::kAccessibilityLargeCursorEnabled, enabled);
437  pref_service->CommitPendingWrite();
438}
439
440void AccessibilityManager::UpdateLargeCursorFromPref() {
441  if (!profile_)
442    return;
443
444  const bool enabled =
445      profile_->GetPrefs()->GetBoolean(prefs::kAccessibilityLargeCursorEnabled);
446
447  if (large_cursor_enabled_ == enabled)
448    return;
449
450  large_cursor_enabled_ = enabled;
451
452  AccessibilityStatusEventDetails details(
453      ACCESSIBILITY_TOGGLE_LARGE_CURSOR,
454      enabled,
455      ash::A11Y_NOTIFICATION_NONE);
456
457  NotifyAccessibilityStatusChanged(details);
458#if !defined(USE_ATHENA)
459  // crbug.com/408733 (and for all USE_ATHENA in this file)
460
461#if defined(USE_ASH)
462  // Large cursor is implemented only in ash.
463  ash::Shell::GetInstance()->cursor_manager()->SetCursorSet(
464      enabled ? ui::CURSOR_SET_LARGE : ui::CURSOR_SET_NORMAL);
465#endif
466
467#if defined(OS_CHROMEOS)
468  ash::Shell::GetInstance()->SetCursorCompositingEnabled(
469      ShouldEnableCursorCompositing());
470#endif
471
472#endif // !USE_ATHENA
473}
474
475bool AccessibilityManager::IsIncognitoAllowed() {
476  // Supervised users can't create incognito-mode windows.
477  return !(user_manager::UserManager::Get()->IsLoggedInAsSupervisedUser());
478}
479
480bool AccessibilityManager::IsLargeCursorEnabled() {
481  return large_cursor_enabled_;
482}
483
484void AccessibilityManager::EnableStickyKeys(bool enabled) {
485  if (!profile_)
486    return;
487  PrefService* pref_service = profile_->GetPrefs();
488  pref_service->SetBoolean(prefs::kAccessibilityStickyKeysEnabled, enabled);
489  pref_service->CommitPendingWrite();
490}
491
492bool AccessibilityManager::IsStickyKeysEnabled() {
493  return sticky_keys_enabled_;
494}
495
496void AccessibilityManager::UpdateStickyKeysFromPref() {
497  if (!profile_)
498    return;
499
500  const bool enabled =
501      profile_->GetPrefs()->GetBoolean(prefs::kAccessibilityStickyKeysEnabled);
502
503  if (sticky_keys_enabled_ == enabled)
504    return;
505
506  sticky_keys_enabled_ = enabled;
507#if defined(USE_ASH) && !defined(USE_ATHENA)
508  ash::Shell::GetInstance()->sticky_keys_controller()->Enable(enabled);
509#endif
510}
511
512void AccessibilityManager::EnableSpokenFeedback(
513    bool enabled,
514    ash::AccessibilityNotificationVisibility notify) {
515  if (!profile_)
516    return;
517#if !defined(USE_ATHENA)
518  ash::Shell::GetInstance()->metrics()->RecordUserMetricsAction(
519      enabled ? ash::UMA_STATUS_AREA_ENABLE_SPOKEN_FEEDBACK
520              : ash::UMA_STATUS_AREA_DISABLE_SPOKEN_FEEDBACK);
521#endif
522
523  spoken_feedback_notification_ = notify;
524
525  PrefService* pref_service = profile_->GetPrefs();
526  pref_service->SetBoolean(prefs::kAccessibilitySpokenFeedbackEnabled, enabled);
527  pref_service->CommitPendingWrite();
528
529  spoken_feedback_notification_ = ash::A11Y_NOTIFICATION_NONE;
530}
531
532void AccessibilityManager::UpdateSpokenFeedbackFromPref() {
533  if (!profile_)
534    return;
535
536  const bool enabled = profile_->GetPrefs()->GetBoolean(
537      prefs::kAccessibilitySpokenFeedbackEnabled);
538
539  if (spoken_feedback_enabled_ == enabled)
540    return;
541
542  spoken_feedback_enabled_ = enabled;
543
544  ExtensionAccessibilityEventRouter::GetInstance()->
545      SetAccessibilityEnabled(enabled);
546
547  AccessibilityStatusEventDetails details(
548      ACCESSIBILITY_TOGGLE_SPOKEN_FEEDBACK,
549      enabled,
550      spoken_feedback_notification_);
551
552  NotifyAccessibilityStatusChanged(details);
553
554  if (enabled) {
555    LoadChromeVox();
556  } else {
557    UnloadChromeVox();
558  }
559  UpdateBrailleImeState();
560}
561
562void AccessibilityManager::LoadChromeVox() {
563  base::Closure done_cb = base::Bind(&AccessibilityManager::PostLoadChromeVox,
564                                     weak_ptr_factory_.GetWeakPtr(),
565                                     profile_);
566  ScreenLocker* screen_locker = ScreenLocker::default_screen_locker();
567  if (screen_locker && screen_locker->locked()) {
568    // If on the lock screen, loads ChromeVox only to the lock screen as for
569    // now. On unlock, it will be loaded to the user screen.
570    // (see. AccessibilityManager::Observe())
571    LoadChromeVoxToLockScreen(done_cb);
572  } else {
573    LoadChromeVoxToUserScreen(done_cb);
574  }
575}
576
577void AccessibilityManager::LoadChromeVoxToUserScreen(
578    const base::Closure& done_cb) {
579  if (chrome_vox_loaded_on_user_screen_)
580    return;
581
582  // Determine whether an OOBE screen is currently being shown. If so,
583  // ChromeVox will be injected directly into that screen.
584  content::WebUI* login_web_ui = NULL;
585
586  if (ProfileHelper::IsSigninProfile(profile_)) {
587    LoginDisplayHost* login_display_host = LoginDisplayHostImpl::default_host();
588    if (login_display_host) {
589      WebUILoginView* web_ui_login_view =
590          login_display_host->GetWebUILoginView();
591      if (web_ui_login_view)
592        login_web_ui = web_ui_login_view->GetWebUI();
593    }
594
595    // Lock screen uses the signin progile.
596    chrome_vox_loaded_on_lock_screen_ = true;
597  }
598
599  chrome_vox_loaded_on_user_screen_ = true;
600  LoadChromeVoxExtension(
601      profile_, login_web_ui ?
602      login_web_ui->GetWebContents()->GetRenderViewHost() : NULL,
603      done_cb);
604}
605
606void AccessibilityManager::LoadChromeVoxToLockScreen(
607    const base::Closure& done_cb) {
608  if (chrome_vox_loaded_on_lock_screen_)
609    return;
610
611  ScreenLocker* screen_locker = ScreenLocker::default_screen_locker();
612  if (screen_locker && screen_locker->locked()) {
613    content::WebUI* lock_web_ui = screen_locker->GetAssociatedWebUI();
614    if (lock_web_ui) {
615      Profile* profile = Profile::FromWebUI(lock_web_ui);
616      chrome_vox_loaded_on_lock_screen_ = true;
617      LoadChromeVoxExtension(
618          profile,
619          lock_web_ui->GetWebContents()->GetRenderViewHost(),
620          done_cb);
621    }
622  }
623}
624
625void AccessibilityManager::UnloadChromeVox() {
626  if (chrome_vox_loaded_on_lock_screen_)
627    UnloadChromeVoxFromLockScreen();
628
629  if (chrome_vox_loaded_on_user_screen_) {
630    UnloadChromeVoxExtension(profile_);
631    chrome_vox_loaded_on_user_screen_ = false;
632  }
633
634  PostUnloadChromeVox(profile_);
635}
636
637void AccessibilityManager::UnloadChromeVoxFromLockScreen() {
638  // Lock screen uses the signin progile.
639  Profile* signin_profile = ProfileHelper::GetSigninProfile();
640  UnloadChromeVoxExtension(signin_profile);
641  chrome_vox_loaded_on_lock_screen_ = false;
642}
643
644bool AccessibilityManager::IsSpokenFeedbackEnabled() {
645  return spoken_feedback_enabled_;
646}
647
648void AccessibilityManager::ToggleSpokenFeedback(
649    ash::AccessibilityNotificationVisibility notify) {
650  EnableSpokenFeedback(!IsSpokenFeedbackEnabled(), notify);
651}
652
653void AccessibilityManager::EnableHighContrast(bool enabled) {
654  if (!profile_)
655    return;
656
657  PrefService* pref_service = profile_->GetPrefs();
658  pref_service->SetBoolean(prefs::kAccessibilityHighContrastEnabled, enabled);
659  pref_service->CommitPendingWrite();
660}
661
662void AccessibilityManager::UpdateHighContrastFromPref() {
663  if (!profile_)
664    return;
665
666  const bool enabled = profile_->GetPrefs()->GetBoolean(
667      prefs::kAccessibilityHighContrastEnabled);
668
669  if (high_contrast_enabled_ == enabled)
670    return;
671
672  high_contrast_enabled_ = enabled;
673
674  AccessibilityStatusEventDetails details(
675      ACCESSIBILITY_TOGGLE_HIGH_CONTRAST_MODE,
676      enabled,
677      ash::A11Y_NOTIFICATION_NONE);
678
679  NotifyAccessibilityStatusChanged(details);
680
681#if !defined(USE_ATHENA)
682
683#if defined(USE_ASH)
684  ash::Shell::GetInstance()->high_contrast_controller()->SetEnabled(enabled);
685#endif
686
687#if defined(OS_CHROMEOS)
688  ash::Shell::GetInstance()->SetCursorCompositingEnabled(
689      ShouldEnableCursorCompositing());
690#endif
691
692#endif
693}
694
695void AccessibilityManager::OnLocaleChanged() {
696  if (!profile_)
697    return;
698
699  if (!IsSpokenFeedbackEnabled())
700    return;
701
702  // If the system locale changes and spoken feedback is enabled,
703  // reload ChromeVox so that it switches its internal translations
704  // to the new language.
705  EnableSpokenFeedback(false, ash::A11Y_NOTIFICATION_NONE);
706  EnableSpokenFeedback(true, ash::A11Y_NOTIFICATION_NONE);
707}
708
709void AccessibilityManager::PlayEarcon(int sound_key) {
710  DCHECK(sound_key < chromeos::SOUND_COUNT);
711  ash::PlaySystemSoundIfSpokenFeedback(sound_key);
712}
713
714bool AccessibilityManager::IsHighContrastEnabled() {
715  return high_contrast_enabled_;
716}
717
718void AccessibilityManager::EnableAutoclick(bool enabled) {
719  if (!profile_)
720    return;
721
722  PrefService* pref_service = profile_->GetPrefs();
723  pref_service->SetBoolean(prefs::kAccessibilityAutoclickEnabled, enabled);
724  pref_service->CommitPendingWrite();
725}
726
727bool AccessibilityManager::IsAutoclickEnabled() {
728  return autoclick_enabled_;
729}
730
731void AccessibilityManager::UpdateAutoclickFromPref() {
732  bool enabled =
733      profile_->GetPrefs()->GetBoolean(prefs::kAccessibilityAutoclickEnabled);
734
735  if (autoclick_enabled_ == enabled)
736    return;
737  autoclick_enabled_ = enabled;
738
739#if defined(USE_ASH) && !defined(USE_ATHENA)
740  ash::Shell::GetInstance()->autoclick_controller()->SetEnabled(enabled);
741#endif
742}
743
744void AccessibilityManager::SetAutoclickDelay(int delay_ms) {
745  if (!profile_)
746    return;
747
748  PrefService* pref_service = profile_->GetPrefs();
749  pref_service->SetInteger(prefs::kAccessibilityAutoclickDelayMs, delay_ms);
750  pref_service->CommitPendingWrite();
751}
752
753int AccessibilityManager::GetAutoclickDelay() const {
754  return autoclick_delay_ms_;
755}
756
757void AccessibilityManager::UpdateAutoclickDelayFromPref() {
758  int autoclick_delay_ms =
759      profile_->GetPrefs()->GetInteger(prefs::kAccessibilityAutoclickDelayMs);
760
761  if (autoclick_delay_ms == autoclick_delay_ms_)
762    return;
763  autoclick_delay_ms_ = autoclick_delay_ms;
764
765#if defined(USE_ASH) && !defined(USE_ATHENA)
766  ash::Shell::GetInstance()->autoclick_controller()->SetAutoclickDelay(
767      autoclick_delay_ms_);
768#endif
769}
770
771void AccessibilityManager::EnableVirtualKeyboard(bool enabled) {
772  if (!profile_)
773    return;
774
775  PrefService* pref_service = profile_->GetPrefs();
776  pref_service->SetBoolean(prefs::kAccessibilityVirtualKeyboardEnabled,
777                           enabled);
778  pref_service->CommitPendingWrite();
779}
780
781bool AccessibilityManager::IsVirtualKeyboardEnabled() {
782  return virtual_keyboard_enabled_;
783}
784
785void AccessibilityManager::UpdateVirtualKeyboardFromPref() {
786  if (!profile_)
787    return;
788
789  const bool enabled = profile_->GetPrefs()->GetBoolean(
790      prefs::kAccessibilityVirtualKeyboardEnabled);
791
792  if (virtual_keyboard_enabled_ == enabled)
793    return;
794  virtual_keyboard_enabled_ = enabled;
795
796  AccessibilityStatusEventDetails details(
797      ACCESSIBILITY_TOGGLE_VIRTUAL_KEYBOARD,
798      enabled,
799      ash::A11Y_NOTIFICATION_NONE);
800
801  NotifyAccessibilityStatusChanged(details);
802
803#if defined(USE_ASH) && !defined(USE_ATHENA)
804  keyboard::SetAccessibilityKeyboardEnabled(enabled);
805  // Note that there are two versions of the on-screen keyboard. A full layout
806  // is provided for accessibility, which includes sticky modifier keys to
807  // enable typing of hotkeys. A compact version is used in touchview mode
808  // to provide a layout with larger keys to facilitate touch typing. In the
809  // event that the a11y keyboard is being disabled, an on-screen keyboard might
810  // still be enabled and a forced reset is required to pick up the layout
811  // change.
812  if (keyboard::IsKeyboardEnabled())
813    ash::Shell::GetInstance()->CreateKeyboard();
814  else
815    ash::Shell::GetInstance()->DeactivateKeyboard();
816#endif
817}
818
819bool AccessibilityManager::IsBrailleDisplayConnected() const {
820  return braille_display_connected_;
821}
822
823void AccessibilityManager::CheckBrailleState() {
824  BrailleController* braille_controller = GetBrailleController();
825  if (!scoped_braille_observer_.IsObserving(braille_controller))
826    scoped_braille_observer_.Add(braille_controller);
827  BrowserThread::PostTaskAndReplyWithResult(
828      BrowserThread::IO,
829      FROM_HERE,
830      base::Bind(&BrailleController::GetDisplayState,
831                 base::Unretained(braille_controller)),
832      base::Bind(&AccessibilityManager::ReceiveBrailleDisplayState,
833                 weak_ptr_factory_.GetWeakPtr()));
834}
835
836void AccessibilityManager::ReceiveBrailleDisplayState(
837    scoped_ptr<extensions::api::braille_display_private::DisplayState> state) {
838  OnBrailleDisplayStateChanged(*state);
839}
840
841void AccessibilityManager::UpdateBrailleImeState() {
842  if (!profile_)
843    return;
844  PrefService* pref_service = profile_->GetPrefs();
845  std::vector<std::string> preload_engines;
846  base::SplitString(pref_service->GetString(prefs::kLanguagePreloadEngines),
847                    ',',
848                    &preload_engines);
849  std::vector<std::string>::iterator it =
850      std::find(preload_engines.begin(),
851                preload_engines.end(),
852                extension_misc::kBrailleImeEngineId);
853  bool is_enabled = (it != preload_engines.end());
854  bool should_be_enabled =
855      (spoken_feedback_enabled_ && braille_display_connected_);
856  if (is_enabled == should_be_enabled)
857    return;
858  if (should_be_enabled)
859    preload_engines.push_back(extension_misc::kBrailleImeEngineId);
860  else
861    preload_engines.erase(it);
862  pref_service->SetString(prefs::kLanguagePreloadEngines,
863                          JoinString(preload_engines, ','));
864  braille_ime_current_ = false;
865}
866
867// Overridden from InputMethodManager::Observer.
868void AccessibilityManager::InputMethodChanged(
869    input_method::InputMethodManager* manager,
870    bool show_message) {
871#if defined(USE_ASH) && !defined(USE_ATHENA)
872  // Sticky keys is implemented only in ash.
873  // TODO(dpolukhin): support Athena, crbug.com/408733.
874  ash::Shell::GetInstance()->sticky_keys_controller()->SetModifiersEnabled(
875      manager->IsISOLevel5ShiftUsedByCurrentInputMethod(),
876      manager->IsAltGrUsedByCurrentInputMethod());
877#endif
878  const chromeos::input_method::InputMethodDescriptor descriptor =
879      manager->GetActiveIMEState()->GetCurrentInputMethod();
880  braille_ime_current_ =
881      (descriptor.id() == extension_misc::kBrailleImeEngineId);
882}
883
884void AccessibilityManager::SetProfile(Profile* profile) {
885  pref_change_registrar_.reset();
886  local_state_pref_change_registrar_.reset();
887
888  if (profile) {
889    // TODO(yoshiki): Move following code to PrefHandler.
890    pref_change_registrar_.reset(new PrefChangeRegistrar);
891    pref_change_registrar_->Init(profile->GetPrefs());
892    pref_change_registrar_->Add(
893        prefs::kAccessibilityLargeCursorEnabled,
894        base::Bind(&AccessibilityManager::UpdateLargeCursorFromPref,
895                   base::Unretained(this)));
896    pref_change_registrar_->Add(
897        prefs::kAccessibilityStickyKeysEnabled,
898        base::Bind(&AccessibilityManager::UpdateStickyKeysFromPref,
899                   base::Unretained(this)));
900    pref_change_registrar_->Add(
901        prefs::kAccessibilitySpokenFeedbackEnabled,
902        base::Bind(&AccessibilityManager::UpdateSpokenFeedbackFromPref,
903                   base::Unretained(this)));
904    pref_change_registrar_->Add(
905        prefs::kAccessibilityHighContrastEnabled,
906        base::Bind(&AccessibilityManager::UpdateHighContrastFromPref,
907                   base::Unretained(this)));
908    pref_change_registrar_->Add(
909        prefs::kAccessibilityAutoclickEnabled,
910        base::Bind(&AccessibilityManager::UpdateAutoclickFromPref,
911                   base::Unretained(this)));
912    pref_change_registrar_->Add(
913        prefs::kAccessibilityAutoclickDelayMs,
914        base::Bind(&AccessibilityManager::UpdateAutoclickDelayFromPref,
915                   base::Unretained(this)));
916    pref_change_registrar_->Add(
917        prefs::kAccessibilityVirtualKeyboardEnabled,
918        base::Bind(&AccessibilityManager::UpdateVirtualKeyboardFromPref,
919                   base::Unretained(this)));
920
921    local_state_pref_change_registrar_.reset(new PrefChangeRegistrar);
922    local_state_pref_change_registrar_->Init(g_browser_process->local_state());
923    local_state_pref_change_registrar_->Add(
924        prefs::kApplicationLocale,
925        base::Bind(&AccessibilityManager::OnLocaleChanged,
926                   base::Unretained(this)));
927
928    content::BrowserAccessibilityState::GetInstance()->AddHistogramCallback(
929        base::Bind(
930            &AccessibilityManager::UpdateChromeOSAccessibilityHistograms,
931            base::Unretained(this)));
932  }
933
934  large_cursor_pref_handler_.HandleProfileChanged(profile_, profile);
935  spoken_feedback_pref_handler_.HandleProfileChanged(profile_, profile);
936  high_contrast_pref_handler_.HandleProfileChanged(profile_, profile);
937  autoclick_pref_handler_.HandleProfileChanged(profile_, profile);
938  autoclick_delay_pref_handler_.HandleProfileChanged(profile_, profile);
939  virtual_keyboard_pref_handler_.HandleProfileChanged(profile_, profile);
940
941  bool had_profile = (profile_ != NULL);
942  profile_ = profile;
943
944  if (!had_profile && profile)
945    CheckBrailleState();
946  else
947    UpdateBrailleImeState();
948  UpdateLargeCursorFromPref();
949  UpdateStickyKeysFromPref();
950  UpdateSpokenFeedbackFromPref();
951  UpdateHighContrastFromPref();
952  UpdateAutoclickFromPref();
953  UpdateAutoclickDelayFromPref();
954  UpdateVirtualKeyboardFromPref();
955}
956
957void AccessibilityManager::ActiveUserChanged(const std::string& user_id) {
958  SetProfile(ProfileManager::GetActiveUserProfile());
959}
960
961void AccessibilityManager::SetProfileForTest(Profile* profile) {
962  SetProfile(profile);
963}
964
965void AccessibilityManager::SetBrailleControllerForTest(
966    BrailleController* controller) {
967  g_braille_controller_for_test = controller;
968}
969
970void AccessibilityManager::EnableSystemSounds(bool system_sounds_enabled) {
971  system_sounds_enabled_ = system_sounds_enabled;
972}
973
974base::TimeDelta AccessibilityManager::PlayShutdownSound() {
975  if (!system_sounds_enabled_)
976    return base::TimeDelta();
977  system_sounds_enabled_ = false;
978#if !defined(USE_ATHENA)
979  if (!ash::PlaySystemSoundIfSpokenFeedback(SOUND_SHUTDOWN))
980    return base::TimeDelta();
981#endif
982  return media::SoundsManager::Get()->GetDuration(SOUND_SHUTDOWN);
983}
984
985void AccessibilityManager::InjectChromeVox(RenderViewHost* render_view_host) {
986  LoadChromeVoxExtension(profile_, render_view_host, base::Closure());
987}
988
989scoped_ptr<AccessibilityStatusSubscription>
990    AccessibilityManager::RegisterCallback(
991        const AccessibilityStatusCallback& cb) {
992  return callback_list_.Add(cb);
993}
994
995void AccessibilityManager::NotifyAccessibilityStatusChanged(
996    AccessibilityStatusEventDetails& details) {
997  callback_list_.Notify(details);
998}
999
1000void AccessibilityManager::UpdateChromeOSAccessibilityHistograms() {
1001  UMA_HISTOGRAM_BOOLEAN("Accessibility.CrosSpokenFeedback",
1002                        IsSpokenFeedbackEnabled());
1003  UMA_HISTOGRAM_BOOLEAN("Accessibility.CrosHighContrast",
1004                        IsHighContrastEnabled());
1005  UMA_HISTOGRAM_BOOLEAN("Accessibility.CrosVirtualKeyboard",
1006                        IsVirtualKeyboardEnabled());
1007  UMA_HISTOGRAM_BOOLEAN("Accessibility.CrosStickyKeys", IsStickyKeysEnabled());
1008  if (MagnificationManager::Get()) {
1009    uint32 type = MagnificationManager::Get()->IsMagnifierEnabled() ?
1010                      MagnificationManager::Get()->GetMagnifierType() : 0;
1011    // '0' means magnifier is disabled.
1012    UMA_HISTOGRAM_ENUMERATION("Accessibility.CrosScreenMagnifier",
1013                              type,
1014                              ash::kMaxMagnifierType + 1);
1015  }
1016  if (profile_) {
1017    const PrefService* const prefs = profile_->GetPrefs();
1018    UMA_HISTOGRAM_BOOLEAN(
1019        "Accessibility.CrosLargeCursor",
1020        prefs->GetBoolean(prefs::kAccessibilityLargeCursorEnabled));
1021    UMA_HISTOGRAM_BOOLEAN(
1022        "Accessibility.CrosAlwaysShowA11yMenu",
1023        prefs->GetBoolean(prefs::kShouldAlwaysShowAccessibilityMenu));
1024
1025    bool autoclick_enabled =
1026        prefs->GetBoolean(prefs::kAccessibilityAutoclickEnabled);
1027    UMA_HISTOGRAM_BOOLEAN("Accessibility.CrosAutoclick", autoclick_enabled);
1028    if (autoclick_enabled) {
1029      // We only want to log the autoclick delay if the user has actually
1030      // enabled autoclick.
1031      UMA_HISTOGRAM_CUSTOM_TIMES(
1032          "Accessibility.CrosAutoclickDelay",
1033          base::TimeDelta::FromMilliseconds(
1034              prefs->GetInteger(prefs::kAccessibilityAutoclickDelayMs)),
1035          base::TimeDelta::FromMilliseconds(1),
1036          base::TimeDelta::FromMilliseconds(3000),
1037          50);
1038    }
1039  }
1040}
1041
1042void AccessibilityManager::Observe(
1043    int type,
1044    const content::NotificationSource& source,
1045    const content::NotificationDetails& details) {
1046  switch (type) {
1047    case chrome::NOTIFICATION_LOGIN_OR_LOCK_WEBUI_VISIBLE: {
1048      // Update |profile_| when entering the login screen.
1049      Profile* profile = ProfileManager::GetActiveUserProfile();
1050      if (ProfileHelper::IsSigninProfile(profile))
1051        SetProfile(profile);
1052      break;
1053    }
1054    case chrome::NOTIFICATION_SESSION_STARTED:
1055      // Update |profile_| when entering a session.
1056      SetProfile(ProfileManager::GetActiveUserProfile());
1057
1058      // Ensure ChromeVox makes announcements at the start of new sessions.
1059      should_speak_chrome_vox_announcements_on_user_screen_ = true;
1060
1061      // Add a session state observer to be able to monitor session changes.
1062      if (!session_state_observer_.get() && ash::Shell::HasInstance())
1063        session_state_observer_.reset(
1064            new ash::ScopedSessionStateObserver(this));
1065      break;
1066    case chrome::NOTIFICATION_PROFILE_DESTROYED: {
1067      // Update |profile_| when exiting a session or shutting down.
1068      Profile* profile = content::Source<Profile>(source).ptr();
1069      if (profile_ == profile)
1070        SetProfile(NULL);
1071      break;
1072    }
1073    case chrome::NOTIFICATION_SCREEN_LOCK_STATE_CHANGED: {
1074      bool is_screen_locked = *content::Details<bool>(details).ptr();
1075      if (spoken_feedback_enabled_) {
1076        if (is_screen_locked)
1077          LoadChromeVoxToLockScreen(base::Closure());
1078        // If spoken feedback was enabled, make sure it is also enabled on
1079        // the user screen.
1080        // The status tray gets verbalized by user screen ChromeVox, so we need
1081        // to load it on the user screen even if the screen is locked.
1082        LoadChromeVoxToUserScreen(base::Closure());
1083      }
1084      break;
1085    }
1086  }
1087}
1088
1089void AccessibilityManager::OnBrailleDisplayStateChanged(
1090    const DisplayState& display_state) {
1091  braille_display_connected_ = display_state.available;
1092  if (braille_display_connected_) {
1093    EnableSpokenFeedback(true, ash::A11Y_NOTIFICATION_SHOW);
1094  }
1095  UpdateBrailleImeState();
1096
1097  AccessibilityStatusEventDetails details(
1098      ACCESSIBILITY_BRAILLE_DISPLAY_CONNECTION_STATE_CHANGED,
1099      braille_display_connected_,
1100      ash::A11Y_NOTIFICATION_SHOW);
1101  NotifyAccessibilityStatusChanged(details);
1102}
1103
1104void AccessibilityManager::OnBrailleKeyEvent(const KeyEvent& event) {
1105  // Ensure the braille IME is active on braille keyboard (dots) input.
1106  if ((event.command ==
1107       extensions::api::braille_display_private::KEY_COMMAND_DOTS) &&
1108      !braille_ime_current_) {
1109    input_method::InputMethodManager::Get()
1110        ->GetActiveIMEState()
1111        ->ChangeInputMethod(extension_misc::kBrailleImeEngineId,
1112                            false /* show_message */);
1113  }
1114}
1115
1116void AccessibilityManager::PostLoadChromeVox(Profile* profile) {
1117  // Do any setup work needed immediately after ChromeVox actually loads.
1118  if (system_sounds_enabled_)
1119    ash::PlaySystemSoundAlways(SOUND_SPOKEN_FEEDBACK_ENABLED);
1120
1121  ExtensionAccessibilityEventRouter::GetInstance()->
1122      OnChromeVoxLoadStateChanged(profile_,
1123          IsSpokenFeedbackEnabled(),
1124          chrome_vox_loaded_on_lock_screen_ ||
1125              should_speak_chrome_vox_announcements_on_user_screen_);
1126
1127  should_speak_chrome_vox_announcements_on_user_screen_ =
1128      chrome_vox_loaded_on_lock_screen_;
1129}
1130
1131void AccessibilityManager::PostUnloadChromeVox(Profile* profile) {
1132  // Do any teardown work needed immediately after ChromeVox actually unloads.
1133  if (system_sounds_enabled_)
1134    ash::PlaySystemSoundAlways(SOUND_SPOKEN_FEEDBACK_DISABLED);
1135  // Clear the accessibility focus ring.
1136  AccessibilityFocusRingController::GetInstance()->SetFocusRing(
1137      std::vector<gfx::Rect>());
1138}
1139
1140}  // namespace chromeos
1141