Home | History | Annotate | Download | only in ime
      1 // Copyright 2014 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 "chromeos/ime/ime_keyboard.h"
      6 
      7 #include <cstdlib>
      8 #include <cstring>
      9 #include <queue>
     10 #include <set>
     11 #include <utility>
     12 
     13 #include "base/bind.h"
     14 #include "base/logging.h"
     15 #include "base/memory/scoped_ptr.h"
     16 #include "base/message_loop/message_loop.h"
     17 #include "base/process/kill.h"
     18 #include "base/process/launch.h"
     19 #include "base/process/process_handle.h"
     20 #include "base/strings/string_util.h"
     21 #include "base/strings/stringprintf.h"
     22 #include "base/sys_info.h"
     23 #include "base/threading/thread_checker.h"
     24 #include "ui/gfx/x/x11_types.h"
     25 
     26 // These includes conflict with base/tracked_objects.h so must come last.
     27 #include <X11/XKBlib.h>
     28 #include <X11/Xlib.h>
     29 
     30 namespace chromeos {
     31 namespace input_method {
     32 namespace {
     33 
     34 // The delay in milliseconds that we'll wait between checking if
     35 // setxkbmap command finished.
     36 const int kSetLayoutCommandCheckDelayMs = 100;
     37 
     38 // The command we use to set the current XKB layout and modifier key mapping.
     39 // TODO(yusukes): Use libxkbfile.so instead of the command (crosbug.com/13105)
     40 const char kSetxkbmapCommand[] = "/usr/bin/setxkbmap";
     41 
     42 // A string for obtaining a mask value for Num Lock.
     43 const char kNumLockVirtualModifierString[] = "NumLock";
     44 
     45 const char *kISOLevel5ShiftLayoutIds[] = {
     46   "ca(multix)",
     47   "de(neo)",
     48 };
     49 
     50 const char *kAltGrLayoutIds[] = {
     51   "be",
     52   "be",
     53   "be",
     54   "bg",
     55   "bg(phonetic)",
     56   "br",
     57   "ca",
     58   "ca(eng)",
     59   "ca(multix)",
     60   "ch",
     61   "ch(fr)",
     62   "cz",
     63   "de",
     64   "de(neo)",
     65   "dk",
     66   "ee",
     67   "es",
     68   "es(cat)",
     69   "fi",
     70   "fr",
     71   "gb(dvorak)",
     72   "gb(extd)",
     73   "gr",
     74   "hr",
     75   "il",
     76   "it",
     77   "latam",
     78   "lt",
     79   "no",
     80   "pl",
     81   "pt",
     82   "ro",
     83   "se",
     84   "si",
     85   "sk",
     86   "tr",
     87   "ua",
     88   "us(altgr-intl)",
     89   "us(intl)",
     90 };
     91 
     92 
     93 // Returns false if |layout_name| contains a bad character.
     94 bool CheckLayoutName(const std::string& layout_name) {
     95   static const char kValidLayoutNameCharacters[] =
     96       "abcdefghijklmnopqrstuvwxyz0123456789()-_";
     97 
     98   if (layout_name.empty()) {
     99     DVLOG(1) << "Invalid layout_name: " << layout_name;
    100     return false;
    101   }
    102 
    103   if (layout_name.find_first_not_of(kValidLayoutNameCharacters) !=
    104       std::string::npos) {
    105     DVLOG(1) << "Invalid layout_name: " << layout_name;
    106     return false;
    107   }
    108 
    109   return true;
    110 }
    111 
    112 class ImeKeyboardX11 : public ImeKeyboard {
    113  public:
    114   ImeKeyboardX11();
    115   virtual ~ImeKeyboardX11() {}
    116 
    117   // Adds/removes observer.
    118   virtual void AddObserver(Observer* observer) OVERRIDE;
    119   virtual void RemoveObserver(Observer* observer) OVERRIDE;
    120 
    121   // ImeKeyboard:
    122   virtual bool SetCurrentKeyboardLayoutByName(
    123       const std::string& layout_name) OVERRIDE;
    124   virtual bool ReapplyCurrentKeyboardLayout() OVERRIDE;
    125   virtual void ReapplyCurrentModifierLockStatus() OVERRIDE;
    126   virtual void DisableNumLock() OVERRIDE;
    127   virtual void SetCapsLockEnabled(bool enable_caps_lock) OVERRIDE;
    128   virtual bool CapsLockIsEnabled() OVERRIDE;
    129   virtual bool IsISOLevel5ShiftAvailable() const OVERRIDE;
    130   virtual bool IsAltGrAvailable() const OVERRIDE;
    131   virtual bool SetAutoRepeatEnabled(bool enabled) OVERRIDE;
    132   virtual bool SetAutoRepeatRate(const AutoRepeatRate& rate) OVERRIDE;
    133 
    134  private:
    135   // Returns a mask for Num Lock (e.g. 1U << 4). Returns 0 on error.
    136   unsigned int GetNumLockMask();
    137 
    138   // Sets the caps-lock status. Note that calling this function always disables
    139   // the num-lock.
    140   void SetLockedModifiers(bool caps_lock_enabled);
    141 
    142   // This function is used by SetLayout() and RemapModifierKeys(). Calls
    143   // setxkbmap command if needed, and updates the last_full_layout_name_ cache.
    144   bool SetLayoutInternal(const std::string& layout_name, bool force);
    145 
    146   // Executes 'setxkbmap -layout ...' command asynchronously using a layout name
    147   // in the |execute_queue_|. Do nothing if the queue is empty.
    148   // TODO(yusukes): Use libxkbfile.so instead of the command (crosbug.com/13105)
    149   void MaybeExecuteSetLayoutCommand();
    150 
    151   // Polls to see setxkbmap process exits.
    152   void PollUntilChildFinish(const base::ProcessHandle handle);
    153 
    154   // Called when execve'd setxkbmap process exits.
    155   void OnSetLayoutFinish();
    156 
    157   const bool is_running_on_chrome_os_;
    158   unsigned int num_lock_mask_;
    159 
    160   // The current Caps Lock status. If true, enabled.
    161   bool current_caps_lock_status_;
    162 
    163   // The XKB layout name which we set last time like "us" and "us(dvorak)".
    164   std::string current_layout_name_;
    165 
    166   // A queue for executing setxkbmap one by one.
    167   std::queue<std::string> execute_queue_;
    168 
    169   base::ThreadChecker thread_checker_;
    170 
    171   base::WeakPtrFactory<ImeKeyboardX11> weak_factory_;
    172 
    173   ObserverList<Observer> observers_;
    174 
    175   DISALLOW_COPY_AND_ASSIGN(ImeKeyboardX11);
    176 };
    177 
    178 ImeKeyboardX11::ImeKeyboardX11()
    179     : is_running_on_chrome_os_(base::SysInfo::IsRunningOnChromeOS()),
    180       weak_factory_(this) {
    181   // X must be already initialized.
    182   CHECK(gfx::GetXDisplay());
    183 
    184   num_lock_mask_ = GetNumLockMask();
    185 
    186   if (is_running_on_chrome_os_) {
    187     // Some code seems to assume that Mod2Mask is always assigned to
    188     // Num Lock.
    189     //
    190     // TODO(yusukes): Check the assumption is really okay. If not,
    191     // modify the Aura code, and then remove the CHECK below.
    192     LOG_IF(ERROR, num_lock_mask_ != Mod2Mask)
    193         << "NumLock is not assigned to Mod2Mask.  : " << num_lock_mask_;
    194   }
    195 
    196   current_caps_lock_status_ = CapsLockIsEnabled();
    197 }
    198 
    199 void ImeKeyboardX11::AddObserver(Observer* observer) {
    200   observers_.AddObserver(observer);
    201 }
    202 
    203 void ImeKeyboardX11::RemoveObserver(Observer* observer) {
    204   observers_.RemoveObserver(observer);
    205 }
    206 
    207 unsigned int ImeKeyboardX11::GetNumLockMask() {
    208   DCHECK(thread_checker_.CalledOnValidThread());
    209   static const unsigned int kBadMask = 0;
    210 
    211   unsigned int real_mask = kBadMask;
    212   XkbDescPtr xkb_desc =
    213       XkbGetKeyboard(gfx::GetXDisplay(), XkbAllComponentsMask, XkbUseCoreKbd);
    214   if (!xkb_desc)
    215     return kBadMask;
    216 
    217   if (xkb_desc->dpy && xkb_desc->names) {
    218     const std::string string_to_find(kNumLockVirtualModifierString);
    219     for (size_t i = 0; i < XkbNumVirtualMods; ++i) {
    220       const unsigned int virtual_mod_mask = 1U << i;
    221       char* virtual_mod_str_raw_ptr =
    222           XGetAtomName(xkb_desc->dpy, xkb_desc->names->vmods[i]);
    223       if (!virtual_mod_str_raw_ptr)
    224         continue;
    225       const std::string virtual_mod_str = virtual_mod_str_raw_ptr;
    226       XFree(virtual_mod_str_raw_ptr);
    227 
    228       if (string_to_find == virtual_mod_str) {
    229         if (!XkbVirtualModsToReal(xkb_desc, virtual_mod_mask, &real_mask)) {
    230           DVLOG(1) << "XkbVirtualModsToReal failed";
    231           real_mask = kBadMask;  // reset the return value, just in case.
    232         }
    233         break;
    234       }
    235     }
    236   }
    237   XkbFreeKeyboard(xkb_desc, 0, True /* free all components */);
    238   return real_mask;
    239 }
    240 
    241 void ImeKeyboardX11::SetLockedModifiers(bool caps_lock_enabled) {
    242   DCHECK(thread_checker_.CalledOnValidThread());
    243 
    244   // Always turn off num lock.
    245   unsigned int affect_mask = num_lock_mask_;
    246   unsigned int value_mask = 0;
    247 
    248   affect_mask |= LockMask;
    249   value_mask |= (caps_lock_enabled ? LockMask : 0);
    250   current_caps_lock_status_ = caps_lock_enabled;
    251 
    252   XkbLockModifiers(gfx::GetXDisplay(), XkbUseCoreKbd, affect_mask, value_mask);
    253 }
    254 
    255 bool ImeKeyboardX11::SetLayoutInternal(const std::string& layout_name,
    256                                        bool force) {
    257   if (!is_running_on_chrome_os_) {
    258     // We should not try to change a layout on Linux or inside ui_tests. Just
    259     // return true.
    260     return true;
    261   }
    262 
    263   if (!CheckLayoutName(layout_name))
    264     return false;
    265 
    266   if (!force && (current_layout_name_ == layout_name)) {
    267     DVLOG(1) << "The requested layout is already set: " << layout_name;
    268     return true;
    269   }
    270 
    271   DVLOG(1) << (force ? "Reapply" : "Set") << " layout: " << layout_name;
    272 
    273   const bool start_execution = execute_queue_.empty();
    274   // If no setxkbmap command is in flight (i.e. start_execution is true),
    275   // start the first one by explicitly calling MaybeExecuteSetLayoutCommand().
    276   // If one or more setxkbmap commands are already in flight, just push the
    277   // layout name to the queue. setxkbmap command for the layout will be called
    278   // via OnSetLayoutFinish() callback later.
    279   execute_queue_.push(layout_name);
    280   if (start_execution)
    281     MaybeExecuteSetLayoutCommand();
    282 
    283   return true;
    284 }
    285 
    286 // Executes 'setxkbmap -layout ...' command asynchronously using a layout name
    287 // in the |execute_queue_|. Do nothing if the queue is empty.
    288 // TODO(yusukes): Use libxkbfile.so instead of the command (crosbug.com/13105)
    289 void ImeKeyboardX11::MaybeExecuteSetLayoutCommand() {
    290   if (execute_queue_.empty())
    291     return;
    292   const std::string layout_to_set = execute_queue_.front();
    293 
    294   std::vector<std::string> argv;
    295   base::ProcessHandle handle = base::kNullProcessHandle;
    296 
    297   argv.push_back(kSetxkbmapCommand);
    298   argv.push_back("-layout");
    299   argv.push_back(layout_to_set);
    300   argv.push_back("-synch");
    301 
    302   if (!base::LaunchProcess(argv, base::LaunchOptions(), &handle)) {
    303     DVLOG(1) << "Failed to execute setxkbmap: " << layout_to_set;
    304     execute_queue_ = std::queue<std::string>();  // clear the queue.
    305     return;
    306   }
    307 
    308   PollUntilChildFinish(handle);
    309 
    310   DVLOG(1) << "ExecuteSetLayoutCommand: " << layout_to_set
    311            << ": pid=" << base::GetProcId(handle);
    312 }
    313 
    314 // Delay and loop until child process finishes and call the callback.
    315 void ImeKeyboardX11::PollUntilChildFinish(const base::ProcessHandle handle) {
    316   int exit_code;
    317   DVLOG(1) << "PollUntilChildFinish: poll for pid=" << base::GetProcId(handle);
    318   switch (base::GetTerminationStatus(handle, &exit_code)) {
    319     case base::TERMINATION_STATUS_STILL_RUNNING:
    320       DVLOG(1) << "PollUntilChildFinish: Try waiting again";
    321       base::MessageLoop::current()->PostDelayedTask(
    322           FROM_HERE,
    323           base::Bind(&ImeKeyboardX11::PollUntilChildFinish,
    324                      weak_factory_.GetWeakPtr(),
    325                      handle),
    326           base::TimeDelta::FromMilliseconds(kSetLayoutCommandCheckDelayMs));
    327       return;
    328 
    329     case base::TERMINATION_STATUS_NORMAL_TERMINATION:
    330       DVLOG(1) << "PollUntilChildFinish: Child process finished";
    331       OnSetLayoutFinish();
    332       return;
    333 
    334     case base::TERMINATION_STATUS_ABNORMAL_TERMINATION:
    335       DVLOG(1) << "PollUntilChildFinish: Abnormal exit code: " << exit_code;
    336       OnSetLayoutFinish();
    337       return;
    338 
    339     default:
    340       NOTIMPLEMENTED();
    341       OnSetLayoutFinish();
    342       return;
    343   }
    344 }
    345 
    346 bool ImeKeyboardX11::CapsLockIsEnabled() {
    347   DCHECK(thread_checker_.CalledOnValidThread());
    348   XkbStateRec status;
    349   XkbGetState(gfx::GetXDisplay(), XkbUseCoreKbd, &status);
    350   return (status.locked_mods & LockMask);
    351 }
    352 
    353 bool ImeKeyboardX11::IsISOLevel5ShiftAvailable() const {
    354   for (size_t i = 0; i < arraysize(kISOLevel5ShiftLayoutIds); ++i) {
    355     if (current_layout_name_ == kISOLevel5ShiftLayoutIds[i])
    356       return true;
    357   }
    358   return false;
    359 }
    360 
    361 bool ImeKeyboardX11::IsAltGrAvailable() const {
    362   for (size_t i = 0; i < arraysize(kAltGrLayoutIds); ++i) {
    363     if (current_layout_name_ == kAltGrLayoutIds[i])
    364       return true;
    365   }
    366   return false;
    367 }
    368 
    369 bool ImeKeyboardX11::SetAutoRepeatEnabled(bool enabled) {
    370   if (enabled)
    371     XAutoRepeatOn(gfx::GetXDisplay());
    372   else
    373     XAutoRepeatOff(gfx::GetXDisplay());
    374   DVLOG(1) << "Set auto-repeat mode to: " << (enabled ? "on" : "off");
    375   return true;
    376 }
    377 
    378 bool ImeKeyboardX11::SetAutoRepeatRate(const AutoRepeatRate& rate) {
    379   DVLOG(1) << "Set auto-repeat rate to: "
    380            << rate.initial_delay_in_ms << " ms delay, "
    381            << rate.repeat_interval_in_ms << " ms interval";
    382   if (XkbSetAutoRepeatRate(gfx::GetXDisplay(), XkbUseCoreKbd,
    383                            rate.initial_delay_in_ms,
    384                            rate.repeat_interval_in_ms) != True) {
    385     DVLOG(1) << "Failed to set auto-repeat rate";
    386     return false;
    387   }
    388   return true;
    389 }
    390 
    391 void ImeKeyboardX11::SetCapsLockEnabled(bool enable_caps_lock) {
    392   bool old_state = current_caps_lock_status_;
    393   SetLockedModifiers(enable_caps_lock);
    394   if (old_state != enable_caps_lock) {
    395     FOR_EACH_OBSERVER(ImeKeyboard::Observer, observers_,
    396                       OnCapsLockChanged(enable_caps_lock));
    397   }
    398 }
    399 
    400 bool ImeKeyboardX11::SetCurrentKeyboardLayoutByName(
    401     const std::string& layout_name) {
    402   if (SetLayoutInternal(layout_name, false)) {
    403     current_layout_name_ = layout_name;
    404     return true;
    405   }
    406   return false;
    407 }
    408 
    409 bool ImeKeyboardX11::ReapplyCurrentKeyboardLayout() {
    410   if (current_layout_name_.empty()) {
    411     DVLOG(1) << "Can't reapply XKB layout: layout unknown";
    412     return false;
    413   }
    414   return SetLayoutInternal(current_layout_name_, true /* force */);
    415 }
    416 
    417 void ImeKeyboardX11::ReapplyCurrentModifierLockStatus() {
    418   SetLockedModifiers(current_caps_lock_status_);
    419 }
    420 
    421 void ImeKeyboardX11::DisableNumLock() {
    422   SetCapsLockEnabled(current_caps_lock_status_);
    423 }
    424 
    425 void ImeKeyboardX11::OnSetLayoutFinish() {
    426   if (execute_queue_.empty()) {
    427     DVLOG(1) << "OnSetLayoutFinish: execute_queue_ is empty. "
    428              << "base::LaunchProcess failed?";
    429     return;
    430   }
    431   execute_queue_.pop();
    432   MaybeExecuteSetLayoutCommand();
    433 }
    434 
    435 }  // namespace
    436 
    437 // static
    438 bool ImeKeyboard::GetAutoRepeatEnabledForTesting() {
    439   XKeyboardState state = {};
    440   XGetKeyboardControl(gfx::GetXDisplay(), &state);
    441   return state.global_auto_repeat != AutoRepeatModeOff;
    442 }
    443 
    444 // static
    445 bool ImeKeyboard::GetAutoRepeatRateForTesting(AutoRepeatRate* out_rate) {
    446   return XkbGetAutoRepeatRate(gfx::GetXDisplay(),
    447                               XkbUseCoreKbd,
    448                               &(out_rate->initial_delay_in_ms),
    449                               &(out_rate->repeat_interval_in_ms)) == True;
    450 }
    451 
    452 // static
    453 bool ImeKeyboard::CheckLayoutNameForTesting(const std::string& layout_name) {
    454   return CheckLayoutName(layout_name);
    455 }
    456 
    457 // static
    458 ImeKeyboard* ImeKeyboard::Create() { return new ImeKeyboardX11(); }
    459 
    460 }  // namespace input_method
    461 }  // namespace chromeos
    462