Skip to content

Commit

Permalink
Accessibility: Add an API to get DLC file contents
Browse files Browse the repository at this point in the history
This change adds an extension API called
chrome.accessibilityPrivate.getDlcContents, which reads a DLC file and
returns the contents to the calling extension.

The motivation behind this API is described in both the TTS Using
Language Packs [1] and Pumpkin for Dictation [2] design documents.
More broadly, there are now multiple Accessibility extensions that
are leveraging DLCs, and as a result, need an easy way to access them.

For an example of how this API can be used, please see this
work-in-progress CL [3], which calls the API from the Google TTS
extension.

[1] https://docs.google.com/document/d/1M_uGwwWfq-g5FI1d0JPT3WXo9M2Z5c0hK-5cPNwEOTY/edit?usp=sharing&resourcekey=0-cGb_-SIBLBQyQpMLVPqktw
[2] https://docs.google.com/document/d/1nZxo9rG0kXKbI4ee_dSudxFWhsciazHIN-CZGgkjBsE/edit?usp=sharing
[3] https://critique.corp.google.com/cl/463904872

AX-Relnotes: N/A
Bug: b:213337877
Change-Id: Iab965f8529380d61f46a64b723fb9011406a9525
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3787009
Reviewed-by: Katie Dektar <katie@chromium.org>
Commit-Queue: Akihiro Ota <akihiroota@chromium.org>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1034549}
  • Loading branch information
akihiroota87 authored and Chromium LUCI CQ committed Aug 12, 2022
1 parent bc050ae commit 046fe5d
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 1 deletion.
Expand Up @@ -882,3 +882,29 @@ void AccessibilityPrivateInstallPumpkinForDictationFunction::
OnPumpkinInstallFinished(bool success) {
Respond(OneArgument(base::Value(success)));
}

ExtensionFunction::ResponseAction
AccessibilityPrivateGetDlcContentsFunction::Run() {
std::unique_ptr<accessibility_private::GetDlcContents::Params> params(
accessibility_private::GetDlcContents::Params::Create(args()));
EXTENSION_FUNCTION_VALIDATE(params);
accessibility_private::DlcType dlc = params->dlc;

AccessibilityManager::Get()->GetDlcContents(
dlc,
base::BindOnce(
&AccessibilityPrivateGetDlcContentsFunction::OnDlcContentsRetrieved,
base::RetainedRef(this)));
return RespondLater();
}

void AccessibilityPrivateGetDlcContentsFunction::OnDlcContentsRetrieved(
const std::vector<uint8_t>& contents,
absl::optional<std::string> error) {
if (error.has_value()) {
Respond(Error(error.value()));
return;
}

Respond(OneArgument(base::Value(contents)));
}
Expand Up @@ -8,6 +8,7 @@
#include "build/chromeos_buildflags.h"
#include "chrome/common/extensions/api/accessibility_private.h"
#include "extensions/browser/extension_function.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

// API function that enables or disables web content accessibility support.
class AccessibilityPrivateSetNativeAccessibilityEnabledFunction
Expand Down Expand Up @@ -278,4 +279,15 @@ class AccessibilityPrivateInstallPumpkinForDictationFunction
void OnPumpkinInstallFinished(bool success);
};

// API function that retrieves DLC file contents.
class AccessibilityPrivateGetDlcContentsFunction : public ExtensionFunction {
~AccessibilityPrivateGetDlcContentsFunction() override = default;
ResponseAction Run() override;
DECLARE_EXTENSION_FUNCTION("accessibilityPrivate.getDlcContents",
ACCESSIBILITY_PRIVATE_GETDLCCONTENTS)
private:
void OnDlcContentsRetrieved(const std::vector<uint8_t>& contents,
absl::optional<std::string> error);
};

#endif // CHROME_BROWSER_ACCESSIBILITY_ACCESSIBILITY_EXTENSION_API_CHROMEOS_H_
Expand Up @@ -5,7 +5,10 @@
#include "ash/accessibility/accessibility_controller_impl.h"
#include "ash/accessibility/ui/accessibility_confirmation_dialog.h"
#include "ash/shell.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/accessibility/dictation_bubble_test_helper.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
Expand Down Expand Up @@ -295,6 +298,26 @@ IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
ASSERT_TRUE(RunSubtest("testInstallPumpkinForDictation")) << message_;
}

IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest,
GetDlcContentsDlcNotOnDevice) {
ASSERT_TRUE(RunSubtest("testGetDlcContentsDlcNotOnDevice")) << message_;
}

IN_PROC_BROWSER_TEST_P(AccessibilityPrivateApiTest, GetDlcContentsSuccess) {
// Create a fake DLC file. We need to put this in a ScopedTempDir because this
// test doesn't have write access to the actual DLC directory
// (/run/imageloader/).
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir dlc_dir;
ASSERT_TRUE(dlc_dir.CreateUniqueTempDir());
AccessibilityManager::Get()->SetDlcPathForTest(dlc_dir.GetPath());
std::string content = "Fake DLC file content";
ASSERT_TRUE(
base::WriteFile(dlc_dir.GetPath().Append("voice.zvoice"), content));

ASSERT_TRUE(RunSubtest("testGetDlcContentsSuccess")) << message_;
}

INSTANTIATE_TEST_SUITE_P(PersistentBackground,
AccessibilityPrivateApiTest,
::testing::Values(ContextType::kPersistentBackground));
Expand Down
87 changes: 86 additions & 1 deletion chrome/browser/ash/accessibility/accessibility_manager.cc
Expand Up @@ -26,6 +26,8 @@
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/singleton.h"
#include "base/metrics/histogram_functions.h"
Expand All @@ -35,6 +37,9 @@
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/values.h"
#include "chrome/browser/accessibility/accessibility_extension_api_chromeos.h"
#include "chrome/browser/ash/accessibility/accessibility_extension_loader.h"
Expand All @@ -57,7 +62,6 @@
#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
#include "chrome/browser/ui/singleton_tabs.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/api/accessibility_private.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
Expand Down Expand Up @@ -100,6 +104,7 @@
namespace ash {
namespace {

using ::extensions::api::accessibility_private::DlcType;
using ::extensions::api::braille_display_private::BrailleController;
using ::extensions::api::braille_display_private::DisplayState;
using ::extensions::api::braille_display_private::KeyEvent;
Expand All @@ -121,6 +126,10 @@ const char kUserBluetoothBrailleDisplayAddress[] =
// The name of the Brltty upstart job.
constexpr char kBrlttyUpstartJobName[] = "brltty";

// The path to the tts-es-us DLC.
constexpr char kTtsEsUsDlcPath[] =
"/run/imageloader/tts-es-us/package/root/voice.zvoice";

static AccessibilityManager* g_accessibility_manager = nullptr;

static BrailleController* g_braille_controller_for_test = nullptr;
Expand Down Expand Up @@ -209,6 +218,58 @@ absl::optional<bool> GetDictationOfflineNudgePrefForLocale(
return offline_nudges.FindBoolByDottedPath(dictation_locale);
}

// Represents response data returned by `ReadDlcFile`.
struct ReadDlcFileResponse {
ReadDlcFileResponse(std::vector<uint8_t> contents,
absl::optional<std::string> error)
: contents(contents), error(error) {}
~ReadDlcFileResponse() = default;
ReadDlcFileResponse(const ReadDlcFileResponse&) = default;
ReadDlcFileResponse& operator=(const ReadDlcFileResponse&) = default;

// The content of the DLC file.
std::vector<uint8_t> contents;
// An error, if any.
absl::optional<std::string> error;
};

// Reads the contents of a DLC file specified by `path`. Must run asynchronously
// on a new ThreadPool.
ReadDlcFileResponse ReadDlcFile(base::FilePath path) {
DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);

std::string error;
if (!base::PathExists(path)) {
error = "Error: DLC file does not exist on-device: " + path.AsUTF8Unsafe();
return ReadDlcFileResponse(std::vector<uint8_t>(), error);
}

int64_t file_size = 0;
if (!base::GetFileSize(path, &file_size) || (file_size <= 0)) {
error = "Error: failed to read size of file: " + path.AsUTF8Unsafe();
return ReadDlcFileResponse(std::vector<uint8_t>(), error);
}

std::vector<uint8_t> contents(file_size);
int bytes_read =
base::ReadFile(path, reinterpret_cast<char*>(contents.data()),
base::checked_cast<int>(file_size));
if (bytes_read != file_size) {
error = "Error: could not read file: " + path.AsUTF8Unsafe();
return ReadDlcFileResponse(std::vector<uint8_t>(), error);
}

return ReadDlcFileResponse(contents, absl::nullopt);
}

// Runs when `ReadDlcFile` returns the contents of a file.
void OnReadDlcFile(GetDlcContentsCallback callback,
ReadDlcFileResponse response) {
std::move(callback).Run(response.contents, response.error);
}

} // namespace

class AccessibilityPanelWidgetObserver : public views::WidgetObserver {
Expand Down Expand Up @@ -2238,4 +2299,28 @@ void AccessibilityManager::OnPumpkinError(const std::string& error) {
// TODO(akihiroota): Consider showing the error message to the user.
}

void AccessibilityManager::GetDlcContents(DlcType dlc,
GetDlcContentsCallback callback) {
base::FilePath path = DlcTypeToPath(dlc);
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()}, base::BindOnce(&ReadDlcFile, path),
base::BindOnce(&OnReadDlcFile, std::move(callback)));
}

base::FilePath AccessibilityManager::DlcTypeToPath(DlcType dlc) {
bool use_test_dlc_path = !dlc_path_for_test_.empty();
switch (dlc) {
case DlcType::DLC_TYPE_TTSESUS:
return use_test_dlc_path ? dlc_path_for_test_.Append("voice.zvoice")
: base::FilePath(kTtsEsUsDlcPath);
case DlcType::DLC_TYPE_NONE:
NOTREACHED();
return base::FilePath();
}
}

void AccessibilityManager::SetDlcPathForTest(base::FilePath path) {
dlc_path_for_test_ = std::move(path);
}

} // namespace ash
15 changes: 15 additions & 0 deletions chrome/browser/ash/accessibility/accessibility_manager.h
Expand Up @@ -22,6 +22,7 @@
#include "chrome/browser/extensions/api/braille_display_private/braille_controller.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_observer.h"
#include "chrome/common/extensions/api/accessibility_private.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/session_manager/core/session_manager.h"
#include "components/session_manager/core/session_manager_observer.h"
Expand Down Expand Up @@ -86,6 +87,9 @@ using AccessibilityStatusCallbackList =
base::RepeatingCallbackList<void(const AccessibilityStatusEventDetails&)>;
using AccessibilityStatusCallback =
AccessibilityStatusCallbackList::CallbackType;
using GetDlcContentsCallback =
base::OnceCallback<void(const std::vector<uint8_t>&,
absl::optional<std::string>)>;

class AccessibilityPanelWidgetObserver;

Expand Down Expand Up @@ -395,6 +399,11 @@ class AccessibilityManager
// value of false.
void InstallPumpkinForDictation(base::OnceCallback<void(bool)> callback);

// Reads the contents of a DLC file and runs `callback` with the results.
void GetDlcContents(::extensions::api::accessibility_private::DlcType dlc,
GetDlcContentsCallback callback);
void SetDlcPathForTest(base::FilePath path);

protected:
AccessibilityManager();
~AccessibilityManager() override;
Expand Down Expand Up @@ -513,6 +522,10 @@ class AccessibilityManager

void OnAppTerminating();

// Returns a full file path given a DLC.
base::FilePath DlcTypeToPath(
::extensions::api::accessibility_private::DlcType dlc);

// Profile which has the current a11y context.
Profile* profile_ = nullptr;
base::ScopedObservation<Profile, ProfileObserver> profile_observation_{this};
Expand Down Expand Up @@ -602,6 +615,8 @@ class AccessibilityManager
base::OnceCallback<void(bool)> install_pumpkin_callback_;
bool is_pumpkin_installed_for_testing_ = false;

base::FilePath dlc_path_for_test_;

base::CallbackListSubscription focus_changed_subscription_;

base::CallbackListSubscription on_app_terminating_subscription_;
Expand Down
29 changes: 29 additions & 0 deletions chrome/common/extensions/api/accessibility_private.json
Expand Up @@ -285,6 +285,12 @@
"optional": true
}
}
},
{
"id": "DlcType",
"type": "string",
"enum": ["ttsEsUs"],
"description": "Types of accessibility-specific DLCs."
}
],
"properties": {
Expand Down Expand Up @@ -733,6 +739,29 @@
"type": "function",
"description": "Cancels the current and queued speech from ChromeVox.",
"parameters": []
},
{
"name": "getDlcContents",
"type": "function",
"description": "Returns the contents of a DLC.",
"parameters": [
{
"name": "dlc",
"$ref": "DlcType",
"description": "The DLC of interest."
}
],
"returns_async": {
"name": "callback",
"description": "A callback that is run when the contents are returned.",
"parameters": [
{
"name": "contents",
"type": "binary",
"description": "The contents of the DLC as a Uint8Array."
}
]
}
}
],
"events": [
Expand Down
Expand Up @@ -159,6 +159,26 @@ var availableTests = [
chrome.test.succeed();
});
},

function testGetDlcContentsDlcNotOnDevice() {
const ttsDlc = chrome.accessibilityPrivate.DlcType.TTS_ES_US;
const error = 'Error: DLC file does not exist on-device: ' +
'/run/imageloader/tts-es-us/package/root/voice.zvoice';
chrome.accessibilityPrivate.getDlcContents(ttsDlc, (contents) => {
chrome.test.assertLastError(error);
chrome.test.succeed();
});
},

function testGetDlcContentsSuccess() {
const ttsDlc = chrome.accessibilityPrivate.DlcType.TTS_ES_US;
chrome.accessibilityPrivate.getDlcContents(ttsDlc, (contents) => {
chrome.test.assertNoLastError();
chrome.test.assertEq(
'Fake DLC file content', new TextDecoder().decode(contents));
chrome.test.succeed();
});
}
];

chrome.test.getConfig(function(config) {
Expand Down
1 change: 1 addition & 0 deletions extensions/browser/extension_function_histogram_value.h
Expand Up @@ -1759,6 +1759,7 @@ enum HistogramValue {
AUTOTESTPRIVATE_SETALLOWEDPREF = 1696,
PASSWORDSPRIVATE_REQUESTCREDENTIALDETAILS = 1697,
DEVELOPERPRIVATE_GETMATCHINGEXTENSIONSFORSITE = 1698,
ACCESSIBILITY_PRIVATE_GETDLCCONTENTS = 1699,
// Last entry: Add new entries above, then run:
// tools/metrics/histograms/update_extension_histograms.py
ENUM_BOUNDARY
Expand Down
15 changes: 15 additions & 0 deletions third_party/closure_compiler/externs/accessibility_private.js
Expand Up @@ -323,6 +323,13 @@ chrome.accessibilityPrivate.DictationBubbleHintType = {
*/
chrome.accessibilityPrivate.DictationBubbleProperties;

/**
* @enum {string}
*/
chrome.accessibilityPrivate.DlcType = {
TTS_ES_US: 'ttsEsUs',
};

/**
* Property to indicate whether event source should default to touch.
* @type {number}
Expand Down Expand Up @@ -561,6 +568,14 @@ chrome.accessibilityPrivate.updateDictationBubble = function(properties) {};
*/
chrome.accessibilityPrivate.silenceSpokenFeedback = function() {};

/**
* Returns the contents of a DLC.
* @param {!chrome.accessibilityPrivate.DlcType} dlc The DLC of interest.
* @param {function(ArrayBuffer): void} callback A callback that is run when the
* contents are returned.
*/
chrome.accessibilityPrivate.getDlcContents = function(dlc, callback) {};

/**
* Fired whenever ChromeVox should output introduction.
* @type {!ChromeEvent}
Expand Down
1 change: 1 addition & 0 deletions tools/metrics/histograms/enums.xml
Expand Up @@ -35337,6 +35337,7 @@ Called by update_extension_histograms.py.-->
<int value="1696" label="AUTOTESTPRIVATE_SETALLOWEDPREF"/>
<int value="1697" label="PASSWORDSPRIVATE_REQUESTCREDENTIALDETAILS"/>
<int value="1698" label="DEVELOPERPRIVATE_GETMATCHINGEXTENSIONSFORSITE"/>
<int value="1699" label="ACCESSIBILITY_PRIVATE_GETDLCCONTENTS"/>
</enum>

<enum name="ExtensionIconState">
Expand Down

0 comments on commit 046fe5d

Please sign in to comment.