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
30namespace chromeos {
31namespace input_method {
32namespace {
33
34// The delay in milliseconds that we'll wait between checking if
35// setxkbmap command finished.
36const 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)
40const char kSetxkbmapCommand[] = "/usr/bin/setxkbmap";
41
42// A string for obtaining a mask value for Num Lock.
43const char kNumLockVirtualModifierString[] = "NumLock";
44
45const char *kISOLevel5ShiftLayoutIds[] = {
46  "ca(multix)",
47  "de(neo)",
48};
49
50const 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.
94bool 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
112class 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
178ImeKeyboardX11::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
199void ImeKeyboardX11::AddObserver(Observer* observer) {
200  observers_.AddObserver(observer);
201}
202
203void ImeKeyboardX11::RemoveObserver(Observer* observer) {
204  observers_.RemoveObserver(observer);
205}
206
207unsigned 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
241void 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
255bool 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)
289void 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.
315void 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
346bool ImeKeyboardX11::CapsLockIsEnabled() {
347  DCHECK(thread_checker_.CalledOnValidThread());
348  XkbStateRec status;
349  XkbGetState(gfx::GetXDisplay(), XkbUseCoreKbd, &status);
350  return (status.locked_mods & LockMask);
351}
352
353bool 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
361bool 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
369bool 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
378bool 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
391void 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
400bool 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
409bool 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
417void ImeKeyboardX11::ReapplyCurrentModifierLockStatus() {
418  SetLockedModifiers(current_caps_lock_status_);
419}
420
421void ImeKeyboardX11::DisableNumLock() {
422  SetCapsLockEnabled(current_caps_lock_status_);
423}
424
425void 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
438bool ImeKeyboard::GetAutoRepeatEnabledForTesting() {
439  XKeyboardState state = {};
440  XGetKeyboardControl(gfx::GetXDisplay(), &state);
441  return state.global_auto_repeat != AutoRepeatModeOff;
442}
443
444// static
445bool 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
453bool ImeKeyboard::CheckLayoutNameForTesting(const std::string& layout_name) {
454  return CheckLayoutName(layout_name);
455}
456
457// static
458ImeKeyboard* ImeKeyboard::Create() { return new ImeKeyboardX11(); }
459
460}  // namespace input_method
461}  // namespace chromeos
462