1// Copyright 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/extensions/api/braille_display_private/braille_controller_brlapi.h"
6
7#include <algorithm>
8#include <cerrno>
9#include <cstring>
10#include <vector>
11
12#include "base/bind.h"
13#include "base/bind_helpers.h"
14#include "base/time/time.h"
15#include "chrome/browser/extensions/api/braille_display_private/brlapi_connection.h"
16#include "chrome/browser/extensions/api/braille_display_private/brlapi_keycode_map.h"
17#include "content/public/browser/browser_thread.h"
18
19namespace extensions {
20using content::BrowserThread;
21using base::Time;
22using base::TimeDelta;
23namespace api {
24namespace braille_display_private {
25
26namespace {
27// Delay between detecting a directory update and trying to connect
28// to the brlapi.
29const int64 kConnectionDelayMs = 500;
30// How long to periodically retry connecting after a brltty restart.
31// Some displays are slow to connect.
32const int64 kConnectRetryTimeout = 20000;
33}  // namespace
34
35BrailleController::BrailleController() {
36}
37
38BrailleController::~BrailleController() {
39}
40
41// static
42BrailleController* BrailleController::GetInstance() {
43  return BrailleControllerImpl::GetInstance();
44}
45
46// static
47BrailleControllerImpl* BrailleControllerImpl::GetInstance() {
48  return Singleton<BrailleControllerImpl,
49                   LeakySingletonTraits<BrailleControllerImpl> >::get();
50}
51
52BrailleControllerImpl::BrailleControllerImpl()
53    : started_connecting_(false),
54      connect_scheduled_(false) {
55  create_brlapi_connection_function_ = base::Bind(
56      &BrailleControllerImpl::CreateBrlapiConnection,
57      base::Unretained(this));
58}
59
60BrailleControllerImpl::~BrailleControllerImpl() {
61}
62
63void BrailleControllerImpl::TryLoadLibBrlApi() {
64  DCHECK_CURRENTLY_ON(BrowserThread::IO);
65  if (libbrlapi_loader_.loaded())
66    return;
67  // These versions of libbrlapi work the same for the functions we
68  // are using.  (0.6.0 adds brlapi_writeWText).
69  static const char* kSupportedVersions[] = {
70    "libbrlapi.so.0.5",
71    "libbrlapi.so.0.6"
72  };
73  for (size_t i = 0; i < arraysize(kSupportedVersions); ++i) {
74    if (libbrlapi_loader_.Load(kSupportedVersions[i]))
75      return;
76  }
77  LOG(WARNING) << "Couldn't load libbrlapi: " << strerror(errno);
78}
79
80scoped_ptr<DisplayState> BrailleControllerImpl::GetDisplayState() {
81  DCHECK_CURRENTLY_ON(BrowserThread::IO);
82  StartConnecting();
83  scoped_ptr<DisplayState> display_state(new DisplayState);
84  if (connection_.get() && connection_->Connected()) {
85    size_t size;
86    if (!connection_->GetDisplaySize(&size)) {
87      Disconnect();
88    } else if (size > 0) {  // size == 0 means no display present.
89      display_state->available = true;
90      display_state->text_cell_count.reset(new int(size));
91    }
92  }
93  return display_state.Pass();
94}
95
96void BrailleControllerImpl::WriteDots(const std::string& cells) {
97  DCHECK_CURRENTLY_ON(BrowserThread::IO);
98  if (connection_ && connection_->Connected()) {
99    size_t size;
100    if (!connection_->GetDisplaySize(&size)) {
101      Disconnect();
102    }
103    std::vector<unsigned char> sizedCells(size);
104    std::memcpy(&sizedCells[0], cells.data(), std::min(cells.size(), size));
105    if (size > cells.size())
106      std::fill(sizedCells.begin() + cells.size(), sizedCells.end(), 0);
107    if (!connection_->WriteDots(&sizedCells[0]))
108      Disconnect();
109  }
110}
111
112void BrailleControllerImpl::AddObserver(BrailleObserver* observer) {
113  DCHECK_CURRENTLY_ON(BrowserThread::UI);
114  if (!BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
115                               base::Bind(
116                                   &BrailleControllerImpl::StartConnecting,
117                                   base::Unretained(this)))) {
118    NOTREACHED();
119  }
120  observers_.AddObserver(observer);
121}
122
123void BrailleControllerImpl::RemoveObserver(BrailleObserver* observer) {
124  DCHECK_CURRENTLY_ON(BrowserThread::UI);
125  observers_.RemoveObserver(observer);
126}
127
128void BrailleControllerImpl::SetCreateBrlapiConnectionForTesting(
129    const CreateBrlapiConnectionFunction& function) {
130  if (function.is_null()) {
131    create_brlapi_connection_function_ = base::Bind(
132        &BrailleControllerImpl::CreateBrlapiConnection,
133        base::Unretained(this));
134  } else {
135    create_brlapi_connection_function_ = function;
136  }
137}
138
139void BrailleControllerImpl::PokeSocketDirForTesting() {
140  OnSocketDirChangedOnIOThread();
141}
142
143void BrailleControllerImpl::StartConnecting() {
144  DCHECK_CURRENTLY_ON(BrowserThread::IO);
145  if (started_connecting_)
146    return;
147  started_connecting_ = true;
148  TryLoadLibBrlApi();
149  if (!libbrlapi_loader_.loaded()) {
150    return;
151  }
152  // Only try to connect after we've started to watch the
153  // socket directory.  This is necessary to avoid a race condition
154  // and because we don't retry to connect after errors that will
155  // persist until there's a change to the socket directory (i.e.
156  // ENOENT).
157  BrowserThread::PostTaskAndReply(
158      BrowserThread::FILE, FROM_HERE,
159      base::Bind(
160          &BrailleControllerImpl::StartWatchingSocketDirOnFileThread,
161          base::Unretained(this)),
162      base::Bind(
163          &BrailleControllerImpl::TryToConnect,
164          base::Unretained(this)));
165  ResetRetryConnectHorizon();
166}
167
168void BrailleControllerImpl::StartWatchingSocketDirOnFileThread() {
169  DCHECK_CURRENTLY_ON(BrowserThread::FILE);
170  base::FilePath brlapi_dir(BRLAPI_SOCKETPATH);
171  if (!file_path_watcher_.Watch(
172          brlapi_dir, false, base::Bind(
173              &BrailleControllerImpl::OnSocketDirChangedOnFileThread,
174              base::Unretained(this)))) {
175    LOG(WARNING) << "Couldn't watch brlapi directory " << BRLAPI_SOCKETPATH;
176  }
177}
178
179void BrailleControllerImpl::OnSocketDirChangedOnFileThread(
180    const base::FilePath& path, bool error) {
181  DCHECK_CURRENTLY_ON(BrowserThread::FILE);
182  if (error) {
183    LOG(ERROR) << "Error watching brlapi directory: " << path.value();
184    return;
185  }
186  BrowserThread::PostTask(
187      BrowserThread::IO, FROM_HERE, base::Bind(
188          &BrailleControllerImpl::OnSocketDirChangedOnIOThread,
189          base::Unretained(this)));
190}
191
192void BrailleControllerImpl::OnSocketDirChangedOnIOThread() {
193  DCHECK_CURRENTLY_ON(BrowserThread::IO);
194  VLOG(1) << "BrlAPI directory changed";
195  // Every directory change resets the max retry time to the appropriate delay
196  // into the future.
197  ResetRetryConnectHorizon();
198  // Try after an initial delay to give the driver a chance to connect.
199  ScheduleTryToConnect();
200}
201
202void BrailleControllerImpl::TryToConnect() {
203  DCHECK_CURRENTLY_ON(BrowserThread::IO);
204  DCHECK(libbrlapi_loader_.loaded());
205  connect_scheduled_ = false;
206  if (!connection_.get())
207    connection_ = create_brlapi_connection_function_.Run();
208  if (connection_.get() && !connection_->Connected()) {
209    VLOG(1) << "Trying to connect to brlapi";
210    BrlapiConnection::ConnectResult result = connection_->Connect(base::Bind(
211        &BrailleControllerImpl::DispatchKeys,
212        base::Unretained(this)));
213    switch (result) {
214      case BrlapiConnection::CONNECT_SUCCESS:
215        DispatchOnDisplayStateChanged(GetDisplayState());
216        break;
217      case BrlapiConnection::CONNECT_ERROR_NO_RETRY:
218        break;
219      case BrlapiConnection::CONNECT_ERROR_RETRY:
220        ScheduleTryToConnect();
221        break;
222      default:
223        NOTREACHED();
224    }
225  }
226}
227
228void BrailleControllerImpl::ResetRetryConnectHorizon() {
229  DCHECK_CURRENTLY_ON(BrowserThread::IO);
230  retry_connect_horizon_ = Time::Now() + TimeDelta::FromMilliseconds(
231      kConnectRetryTimeout);
232}
233
234void BrailleControllerImpl::ScheduleTryToConnect() {
235  DCHECK_CURRENTLY_ON(BrowserThread::IO);
236  TimeDelta delay(TimeDelta::FromMilliseconds(kConnectionDelayMs));
237  // Don't reschedule if there's already a connect scheduled or
238  // the next attempt would fall outside of the retry limit.
239  if (connect_scheduled_)
240    return;
241  if (Time::Now() + delay > retry_connect_horizon_) {
242    VLOG(1) << "Stopping to retry to connect to brlapi";
243    return;
244  }
245  VLOG(1) << "Scheduling connection retry to brlapi";
246  connect_scheduled_ = true;
247  BrowserThread::PostDelayedTask(BrowserThread::IO, FROM_HERE,
248                                 base::Bind(
249                                     &BrailleControllerImpl::TryToConnect,
250                                     base::Unretained(this)),
251                                 delay);
252}
253
254void BrailleControllerImpl::Disconnect() {
255  DCHECK_CURRENTLY_ON(BrowserThread::IO);
256  if (!connection_ || !connection_->Connected())
257    return;
258  connection_->Disconnect();
259  DispatchOnDisplayStateChanged(scoped_ptr<DisplayState>(new DisplayState()));
260}
261
262scoped_ptr<BrlapiConnection> BrailleControllerImpl::CreateBrlapiConnection() {
263  DCHECK(libbrlapi_loader_.loaded());
264  return BrlapiConnection::Create(&libbrlapi_loader_);
265}
266
267void BrailleControllerImpl::DispatchKeys() {
268  DCHECK(connection_.get());
269  brlapi_keyCode_t code;
270  while (true) {
271    int result = connection_->ReadKey(&code);
272    if (result < 0) {  // Error.
273      brlapi_error_t* err = connection_->BrlapiError();
274      if (err->brlerrno == BRLAPI_ERROR_LIBCERR && err->libcerrno == EINTR)
275        continue;
276      // Disconnect on other errors.
277      VLOG(1) << "BrlAPI error: " << connection_->BrlapiStrError();
278      Disconnect();
279      return;
280    } else if (result == 0) { // No more data.
281      return;
282    }
283    scoped_ptr<KeyEvent> event = BrlapiKeyCodeToEvent(code);
284    if (event)
285      DispatchKeyEvent(event.Pass());
286  }
287}
288
289void BrailleControllerImpl::DispatchKeyEvent(scoped_ptr<KeyEvent> event) {
290  if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
291    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
292                            base::Bind(
293                                &BrailleControllerImpl::DispatchKeyEvent,
294                                base::Unretained(this),
295                                base::Passed(&event)));
296    return;
297  }
298  VLOG(1) << "Dispatching key event: " << *event->ToValue();
299  FOR_EACH_OBSERVER(BrailleObserver, observers_, OnBrailleKeyEvent(*event));
300}
301
302void BrailleControllerImpl::DispatchOnDisplayStateChanged(
303    scoped_ptr<DisplayState> new_state) {
304  if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
305    if (!BrowserThread::PostTask(
306            BrowserThread::UI, FROM_HERE,
307            base::Bind(&BrailleControllerImpl::DispatchOnDisplayStateChanged,
308                       base::Unretained(this),
309                       base::Passed(&new_state)))) {
310      NOTREACHED();
311    }
312    return;
313  }
314  FOR_EACH_OBSERVER(BrailleObserver, observers_,
315                    OnBrailleDisplayStateChanged(*new_state));
316}
317
318}  // namespace braille_display_private
319}  // namespace api
320}  // namespace extensions
321