| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/extensions/api/braille_display_private/braille_controller_brlapi.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <algorithm> |
| #include <cerrno> |
| #include <cstring> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/macros.h" |
| #include "base/task_scheduler/post_task.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/extensions/api/braille_display_private/brlapi_connection.h" |
| #include "chrome/browser/extensions/api/braille_display_private/brlapi_keycode_map.h" |
| #include "content/public/browser/browser_thread.h" |
| |
| namespace extensions { |
| using content::BrowserThread; |
| using base::Time; |
| using base::TimeDelta; |
| namespace api { |
| namespace braille_display_private { |
| |
| namespace { |
| // Delay between detecting a directory update and trying to connect |
| // to the brlapi. |
| const int64_t kConnectionDelayMs = 500; |
| // How long to periodically retry connecting after a brltty restart. |
| // Some displays are slow to connect. |
| const int64_t kConnectRetryTimeout = 20000; |
| } // namespace |
| |
| BrailleController::BrailleController() { |
| } |
| |
| BrailleController::~BrailleController() { |
| } |
| |
| // static |
| BrailleController* BrailleController::GetInstance() { |
| return BrailleControllerImpl::GetInstance(); |
| } |
| |
| // static |
| BrailleControllerImpl* BrailleControllerImpl::GetInstance() { |
| return base::Singleton< |
| BrailleControllerImpl, |
| base::LeakySingletonTraits<BrailleControllerImpl>>::get(); |
| } |
| |
| BrailleControllerImpl::BrailleControllerImpl() |
| : started_connecting_(false), |
| connect_scheduled_(false) { |
| create_brlapi_connection_function_ = base::Bind( |
| &BrailleControllerImpl::CreateBrlapiConnection, |
| base::Unretained(this)); |
| } |
| |
| BrailleControllerImpl::~BrailleControllerImpl() { |
| } |
| |
| void BrailleControllerImpl::TryLoadLibBrlApi() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| if (libbrlapi_loader_.loaded()) |
| return; |
| // These versions of libbrlapi work the same for the functions we |
| // are using. (0.6.0 adds brlapi_writeWText). |
| static const char* const kSupportedVersions[] = { |
| "libbrlapi.so.0.5", |
| "libbrlapi.so.0.6" |
| }; |
| for (size_t i = 0; i < arraysize(kSupportedVersions); ++i) { |
| if (libbrlapi_loader_.Load(kSupportedVersions[i])) |
| return; |
| } |
| LOG(WARNING) << "Couldn't load libbrlapi: " << strerror(errno); |
| } |
| |
| std::unique_ptr<DisplayState> BrailleControllerImpl::GetDisplayState() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| StartConnecting(); |
| std::unique_ptr<DisplayState> display_state(new DisplayState); |
| if (connection_.get() && connection_->Connected()) { |
| unsigned int columns = 0; |
| unsigned int rows = 0; |
| if (!connection_->GetDisplaySize(&columns, &rows)) { |
| Disconnect(); |
| } else if (rows * columns > 0) { |
| // rows * columns == 0 means no display present. |
| display_state->available = true; |
| display_state->text_column_count.reset(new int(columns)); |
| display_state->text_row_count.reset(new int(rows)); |
| } |
| } |
| return display_state; |
| } |
| |
| void BrailleControllerImpl::WriteDots(const std::vector<char>& cells, |
| unsigned int cells_cols, |
| unsigned int cells_rows) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| if (connection_ && connection_->Connected()) { |
| // Row count and column count of current display. |
| unsigned int columns = 0; |
| unsigned int rows = 0; |
| if (!connection_->GetDisplaySize(&columns, &rows)) { |
| Disconnect(); |
| } |
| std::vector<unsigned char> sized_cells(rows * columns, 0); |
| unsigned int row_limit = std::min(rows, cells_rows); |
| unsigned int col_limit = std::min(columns, cells_cols); |
| for (unsigned int row = 0; row < row_limit; row++) { |
| for (unsigned int col = 0; col < col_limit; col++) { |
| sized_cells[row * columns + col] = cells[row * cells_cols + col]; |
| } |
| } |
| |
| if (!connection_->WriteDots(sized_cells)) |
| Disconnect(); |
| } |
| } |
| |
| void BrailleControllerImpl::AddObserver(BrailleObserver* observer) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!BrowserThread::PostTask( |
| BrowserThread::IO, FROM_HERE, |
| base::BindOnce(&BrailleControllerImpl::StartConnecting, |
| base::Unretained(this)))) { |
| NOTREACHED(); |
| } |
| observers_.AddObserver(observer); |
| } |
| |
| void BrailleControllerImpl::RemoveObserver(BrailleObserver* observer) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| observers_.RemoveObserver(observer); |
| } |
| |
| void BrailleControllerImpl::SetCreateBrlapiConnectionForTesting( |
| const CreateBrlapiConnectionFunction& function) { |
| if (function.is_null()) { |
| create_brlapi_connection_function_ = base::Bind( |
| &BrailleControllerImpl::CreateBrlapiConnection, |
| base::Unretained(this)); |
| } else { |
| create_brlapi_connection_function_ = function; |
| } |
| } |
| |
| void BrailleControllerImpl::PokeSocketDirForTesting() { |
| OnSocketDirChangedOnIOThread(); |
| } |
| |
| void BrailleControllerImpl::StartConnecting() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| if (started_connecting_) |
| return; |
| started_connecting_ = true; |
| TryLoadLibBrlApi(); |
| if (!libbrlapi_loader_.loaded()) { |
| return; |
| } |
| |
| if (!sequenced_task_runner_) { |
| sequenced_task_runner_ = base::CreateSequencedTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::USER_VISIBLE}); |
| } |
| |
| // Only try to connect after we've started to watch the |
| // socket directory. This is necessary to avoid a race condition |
| // and because we don't retry to connect after errors that will |
| // persist until there's a change to the socket directory (i.e. |
| // ENOENT). |
| sequenced_task_runner_->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce(&BrailleControllerImpl::StartWatchingSocketDirOnTaskThread, |
| base::Unretained(this)), |
| base::BindOnce(&BrailleControllerImpl::TryToConnect, |
| base::Unretained(this))); |
| ResetRetryConnectHorizon(); |
| } |
| |
| void BrailleControllerImpl::StartWatchingSocketDirOnTaskThread() { |
| base::ThreadRestrictions::AssertIOAllowed(); |
| base::FilePath brlapi_dir(BRLAPI_SOCKETPATH); |
| if (!file_path_watcher_.Watch( |
| brlapi_dir, false, |
| base::Bind(&BrailleControllerImpl::OnSocketDirChangedOnTaskThread, |
| base::Unretained(this)))) { |
| LOG(WARNING) << "Couldn't watch brlapi directory " << BRLAPI_SOCKETPATH; |
| } |
| } |
| |
| void BrailleControllerImpl::OnSocketDirChangedOnTaskThread( |
| const base::FilePath& path, |
| bool error) { |
| base::ThreadRestrictions::AssertIOAllowed(); |
| if (error) { |
| LOG(ERROR) << "Error watching brlapi directory: " << path.value(); |
| return; |
| } |
| BrowserThread::PostTask( |
| BrowserThread::IO, FROM_HERE, |
| base::BindOnce(&BrailleControllerImpl::OnSocketDirChangedOnIOThread, |
| base::Unretained(this))); |
| } |
| |
| void BrailleControllerImpl::OnSocketDirChangedOnIOThread() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| VLOG(1) << "BrlAPI directory changed"; |
| // Every directory change resets the max retry time to the appropriate delay |
| // into the future. |
| ResetRetryConnectHorizon(); |
| // Try after an initial delay to give the driver a chance to connect. |
| ScheduleTryToConnect(); |
| } |
| |
| void BrailleControllerImpl::TryToConnect() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(libbrlapi_loader_.loaded()); |
| connect_scheduled_ = false; |
| if (!connection_.get()) |
| connection_ = create_brlapi_connection_function_.Run(); |
| if (connection_.get() && !connection_->Connected()) { |
| VLOG(1) << "Trying to connect to brlapi"; |
| BrlapiConnection::ConnectResult result = connection_->Connect(base::Bind( |
| &BrailleControllerImpl::DispatchKeys, |
| base::Unretained(this))); |
| switch (result) { |
| case BrlapiConnection::CONNECT_SUCCESS: |
| DispatchOnDisplayStateChanged(GetDisplayState()); |
| break; |
| case BrlapiConnection::CONNECT_ERROR_NO_RETRY: |
| break; |
| case BrlapiConnection::CONNECT_ERROR_RETRY: |
| ScheduleTryToConnect(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| void BrailleControllerImpl::ResetRetryConnectHorizon() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| retry_connect_horizon_ = Time::Now() + TimeDelta::FromMilliseconds( |
| kConnectRetryTimeout); |
| } |
| |
| void BrailleControllerImpl::ScheduleTryToConnect() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| TimeDelta delay(TimeDelta::FromMilliseconds(kConnectionDelayMs)); |
| // Don't reschedule if there's already a connect scheduled or |
| // the next attempt would fall outside of the retry limit. |
| if (connect_scheduled_) |
| return; |
| if (Time::Now() + delay > retry_connect_horizon_) { |
| VLOG(1) << "Stopping to retry to connect to brlapi"; |
| return; |
| } |
| VLOG(1) << "Scheduling connection retry to brlapi"; |
| connect_scheduled_ = true; |
| BrowserThread::PostDelayedTask( |
| BrowserThread::IO, FROM_HERE, |
| base::BindOnce(&BrailleControllerImpl::TryToConnect, |
| base::Unretained(this)), |
| delay); |
| } |
| |
| void BrailleControllerImpl::Disconnect() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| if (!connection_ || !connection_->Connected()) |
| return; |
| connection_->Disconnect(); |
| DispatchOnDisplayStateChanged( |
| std::unique_ptr<DisplayState>(new DisplayState())); |
| } |
| |
| std::unique_ptr<BrlapiConnection> |
| BrailleControllerImpl::CreateBrlapiConnection() { |
| DCHECK(libbrlapi_loader_.loaded()); |
| return BrlapiConnection::Create(&libbrlapi_loader_); |
| } |
| |
| void BrailleControllerImpl::DispatchKeys() { |
| DCHECK(connection_.get()); |
| brlapi_keyCode_t code; |
| while (true) { |
| int result = connection_->ReadKey(&code); |
| if (result < 0) { // Error. |
| brlapi_error_t* err = connection_->BrlapiError(); |
| if (err->brlerrno == BRLAPI_ERROR_LIBCERR && err->libcerrno == EINTR) |
| continue; |
| // Disconnect on other errors. |
| VLOG(1) << "BrlAPI error: " << connection_->BrlapiStrError(); |
| Disconnect(); |
| return; |
| } else if (result == 0) { // No more data. |
| return; |
| } |
| std::unique_ptr<KeyEvent> event = BrlapiKeyCodeToEvent(code); |
| if (event) |
| DispatchKeyEvent(std::move(event)); |
| } |
| } |
| |
| void BrailleControllerImpl::DispatchKeyEvent(std::unique_ptr<KeyEvent> event) { |
| if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) { |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&BrailleControllerImpl::DispatchKeyEvent, |
| base::Unretained(this), base::Passed(&event))); |
| return; |
| } |
| VLOG(1) << "Dispatching key event: " << *event->ToValue(); |
| for (auto& observer : observers_) |
| observer.OnBrailleKeyEvent(*event); |
| } |
| |
| void BrailleControllerImpl::DispatchOnDisplayStateChanged( |
| std::unique_ptr<DisplayState> new_state) { |
| if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) { |
| if (!BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce( |
| &BrailleControllerImpl::DispatchOnDisplayStateChanged, |
| base::Unretained(this), base::Passed(&new_state)))) { |
| NOTREACHED(); |
| } |
| return; |
| } |
| for (auto& observer : observers_) |
| observer.OnBrailleDisplayStateChanged(*new_state); |
| } |
| |
| } // namespace braille_display_private |
| } // namespace api |
| } // namespace extensions |