Commit 0f23553f authored by Aaron Leventhal's avatar Aaron Leventhal Committed by Commit Bot

Friendly screen reader download status updates

When a download starts or finishes, notify screen reader users
with a friendly message that contains useful information such
as the size and name of the file to be downloaded.

In addition, for long downloads, provide screen reader update
notifications every 30 seconds, and include the time or % remaining.

Bug: 787139
Change-Id: I903dd626fddad16504079c69d4e1f3edd88d8052
Reviewed-on: https://chromium-review.googlesource.com/947769
Commit-Queue: Aaron Leventhal <aleventhal@chromium.org>
Reviewed-by: default avatarPeter Kasting <pkasting@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#541674}
parent 20f515cc
...@@ -1646,6 +1646,32 @@ are declared in build/common.gypi. ...@@ -1646,6 +1646,32 @@ are declared in build/common.gypi.
Downloaded in Incognito Downloaded in Incognito
</message> </message>
<!-- Download Alerts for Accessibility -->
<message name="IDS_DOWNLOAD_STATUS_IN_PROGRESS_ACCESSIBLE_ALERT"
desc="The title of a download notification: the current download status is in progress. This message is for screen reader users.">
Downloading <ph name="SIZE">$1<ex>52.7 MB</ex></ph>, <ph name="FILE_NAME">$2<ex>somedocument.pdf</ex></ph>
</message>
<message name="IDS_DOWNLOAD_STATUS_PERCENT_COMPLETE_ACCESSIBLE_ALERT"
desc="The title of a download notification: the current download status is in progress and time remaining is known. This message is for screen reader users.">
Downloading, <ph name="PERCENT_REMAINING">$1<ex>33</ex>% remaining</ph>
</message>
<message name="IDS_DOWNLOAD_STATUS_TIME_REMAINING_ACCESSIBLE_ALERT"
desc="The title of a download notification: the current download status is in progress and time remaining is known. This message is for screen reader users.">
Downloading, <ph name="STATUS">$1<ex>42 mins left</ex></ph>
</message>
<message name="IDS_DOWNLOAD_FAILED_ACCESSIBLE_ALERT"
desc="The title of a download notification: the current download status is failed. This message is for screen reader users.">
Download unsuccessful: <ph name="FILE_NAME">$1<ex>somedocument.pdf</ex></ph>
</message>
<message name="IDS_DOWNLOAD_CANCELLED_ACCESSIBLE_ALERT"
desc="The title of a download notification: the current download status is cancelled. This message is for screen reader users.">
Download cancelled: <ph name="FILE_NAME">$1<ex>somedocument.pdf</ex>.</ph>
</message>
<message name="IDS_DOWNLOAD_COMPLETE_ACCESSIBLE_ALERT"
desc="The title of a download notification: the current download status is finished successufully. This message is for screen reader users.">
Download complete: <ph name="FILE_NAME">$1<ex>somedocument.pdf</ex>. Press Shift+F6 to cycle to the downloads bar area.</ph>
</message>
<!-- Download Notification Labels --> <!-- Download Notification Labels -->
<message name="IDS_DOWNLOAD_NOTIFICATION_COPY_TO_CLIPBOARD" <message name="IDS_DOWNLOAD_NOTIFICATION_COPY_TO_CLIPBOARD"
desc="In the download notification, the text of 'Copy to clipboard' button, which is to copy the file content (eg. an image for image files) to the clipboard"> desc="In the download notification, the text of 'Copy to clipboard' button, which is to copy the file content (eg. an image for image files) to the clipboard">
......
...@@ -46,7 +46,9 @@ ...@@ -46,7 +46,9 @@
#include "third_party/icu/source/common/unicode/uchar.h" #include "third_party/icu/source/common/unicode/uchar.h"
#include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#include "ui/base/resource/resource_bundle.h" #include "ui/base/resource/resource_bundle.h"
#include "ui/base/text/bytes_formatting.h"
#include "ui/base/theme_provider.h" #include "ui/base/theme_provider.h"
#include "ui/events/event.h" #include "ui/events/event.h"
#include "ui/gfx/animation/slide_animation.h" #include "ui/gfx/animation/slide_animation.h"
...@@ -57,6 +59,7 @@ ...@@ -57,6 +59,7 @@
#include "ui/gfx/paint_vector_icon.h" #include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_elider.h" #include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h" #include "ui/gfx/text_utils.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/flood_fill_ink_drop_ripple.h" #include "ui/views/animation/flood_fill_ink_drop_ripple.h"
#include "ui/views/animation/ink_drop_highlight.h" #include "ui/views/animation/ink_drop_highlight.h"
#include "ui/views/animation/ink_drop_impl.h" #include "ui/views/animation/ink_drop_impl.h"
...@@ -123,6 +126,10 @@ const int kInterruptedAnimationDurationMs = 2500; ...@@ -123,6 +126,10 @@ const int kInterruptedAnimationDurationMs = 2500;
// downloaded item. // downloaded item.
const int kDisabledOnOpenDuration = 3000; const int kDisabledOnOpenDuration = 3000;
// Amount of time between accessible alert events.
constexpr base::TimeDelta kAccessibleAlertInterval =
base::TimeDelta::FromSeconds(30);
// The separator is drawn as a border. It's one dp wide. // The separator is drawn as a border. It's one dp wide.
class SeparatorBorder : public views::FocusableBorder { class SeparatorBorder : public views::FocusableBorder {
public: public:
...@@ -159,7 +166,8 @@ class SeparatorBorder : public views::FocusableBorder { ...@@ -159,7 +166,8 @@ class SeparatorBorder : public views::FocusableBorder {
} // namespace } // namespace
DownloadItemView::DownloadItemView(DownloadItem* download_item, DownloadItemView::DownloadItemView(DownloadItem* download_item,
DownloadShelfView* parent) DownloadShelfView* parent,
views::View* accessible_alert)
: shelf_(parent), : shelf_(parent),
status_text_(l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_STARTING)), status_text_(l10n_util::GetStringUTF16(IDS_DOWNLOAD_STATUS_STARTING)),
dropdown_state_(NORMAL), dropdown_state_(NORMAL),
...@@ -175,6 +183,8 @@ DownloadItemView::DownloadItemView(DownloadItem* download_item, ...@@ -175,6 +183,8 @@ DownloadItemView::DownloadItemView(DownloadItem* download_item,
disabled_while_opening_(false), disabled_while_opening_(false),
creation_time_(base::Time::Now()), creation_time_(base::Time::Now()),
time_download_warning_shown_(base::Time()), time_download_warning_shown_(base::Time()),
accessible_alert_(accessible_alert),
announce_accessible_alert_soon_(false),
weak_ptr_factory_(this) { weak_ptr_factory_(this) {
SetInkDropMode(InkDropMode::ON_NO_GESTURE_HANDLER); SetInkDropMode(InkDropMode::ON_NO_GESTURE_HANDLER);
DCHECK(download()); DCHECK(download());
...@@ -223,6 +233,7 @@ void DownloadItemView::StartDownloadProgress() { ...@@ -223,6 +233,7 @@ void DownloadItemView::StartDownloadProgress() {
} }
void DownloadItemView::StopDownloadProgress() { void DownloadItemView::StopDownloadProgress() {
accessible_alert_timer_.AbandonAndStop();
if (!progress_timer_.IsRunning()) if (!progress_timer_.IsRunning())
return; return;
previous_progress_elapsed_ += base::TimeTicks::Now() - progress_start_time_; previous_progress_elapsed_ += base::TimeTicks::Now() - progress_start_time_;
...@@ -268,6 +279,7 @@ void DownloadItemView::MaybeSubmitDownloadToFeedbackService( ...@@ -268,6 +279,7 @@ void DownloadItemView::MaybeSubmitDownloadToFeedbackService(
// Update the progress graphic on the icon and our text status label // Update the progress graphic on the icon and our text status label
// to reflect our current bytes downloaded, time remaining. // to reflect our current bytes downloaded, time remaining.
// Also updates the accessible status view for screen reader users.
void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) { void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) {
DCHECK_EQ(download(), download_item); DCHECK_EQ(download(), download_item);
...@@ -279,13 +291,27 @@ void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) { ...@@ -279,13 +291,27 @@ void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) {
if (IsShowingWarningDialog() != model_.IsDangerous()) { if (IsShowingWarningDialog() != model_.IsDangerous()) {
ToggleWarningDialog(); ToggleWarningDialog();
} else { } else {
status_text_ = model_.GetStatusText();
switch (download()->GetState()) { switch (download()->GetState()) {
case DownloadItem::IN_PROGRESS: case DownloadItem::IN_PROGRESS:
// No need to send accessible alert for "paused", as the button ends
// up being refocused in the actual use case, and the name of the
// button reports that the download has been paused.
// Reset the status counter so that user receives immediate feedback
// once the download is resumed.
if (!download()->IsPaused())
UpdateAccessibleAlert(GetInProgressAccessibleAlertText(), false);
download()->IsPaused() ? StopDownloadProgress() download()->IsPaused() ? StopDownloadProgress()
: StartDownloadProgress(); : StartDownloadProgress();
LoadIconIfItemPathChanged(); LoadIconIfItemPathChanged();
break; break;
case DownloadItem::INTERRUPTED: case DownloadItem::INTERRUPTED:
download()->GetFileNameToReportUser().LossyDisplayName();
UpdateAccessibleAlert(
l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_FAILED_ACCESSIBLE_ALERT,
download()->GetFileNameToReportUser().LossyDisplayName()),
true);
StopDownloadProgress(); StopDownloadProgress();
complete_animation_.reset(new gfx::SlideAnimation(this)); complete_animation_.reset(new gfx::SlideAnimation(this));
complete_animation_->SetSlideDuration(kInterruptedAnimationDurationMs); complete_animation_->SetSlideDuration(kInterruptedAnimationDurationMs);
...@@ -294,6 +320,11 @@ void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) { ...@@ -294,6 +320,11 @@ void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) {
LoadIcon(); LoadIcon();
break; break;
case DownloadItem::COMPLETE: case DownloadItem::COMPLETE:
UpdateAccessibleAlert(
l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_COMPLETE_ACCESSIBLE_ALERT,
download()->GetFileNameToReportUser().LossyDisplayName()),
true);
if (model_.ShouldRemoveFromShelfWhenComplete()) { if (model_.ShouldRemoveFromShelfWhenComplete()) {
shelf_->RemoveDownloadView(this); // This will delete us! shelf_->RemoveDownloadView(this); // This will delete us!
return; return;
...@@ -306,6 +337,11 @@ void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) { ...@@ -306,6 +337,11 @@ void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) {
LoadIcon(); LoadIcon();
break; break;
case DownloadItem::CANCELLED: case DownloadItem::CANCELLED:
UpdateAccessibleAlert(
l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_CANCELLED_ACCESSIBLE_ALERT,
download()->GetFileNameToReportUser().LossyDisplayName()),
true);
StopDownloadProgress(); StopDownloadProgress();
if (complete_animation_) if (complete_animation_)
complete_animation_->Stop(); complete_animation_->Stop();
...@@ -314,7 +350,6 @@ void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) { ...@@ -314,7 +350,6 @@ void DownloadItemView::OnDownloadUpdated(DownloadItem* download_item) {
default: default:
NOTREACHED(); NOTREACHED();
} }
status_text_ = model_.GetStatusText();
SchedulePaint(); SchedulePaint();
} }
...@@ -489,11 +524,9 @@ bool DownloadItemView::GetTooltipText(const gfx::Point& p, ...@@ -489,11 +524,9 @@ bool DownloadItemView::GetTooltipText(const gfx::Point& p,
void DownloadItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) { void DownloadItemView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->SetName(accessible_name_); node_data->SetName(accessible_name_);
node_data->role = ax::mojom::Role::kButton; node_data->role = ax::mojom::Role::kButton;
if (model_.IsDangerous()) { if (model_.IsDangerous())
node_data->SetRestriction(ax::mojom::Restriction::kDisabled); node_data->SetRestriction(ax::mojom::Restriction::kDisabled);
} else {
node_data->AddState(ax::mojom::State::kHaspopup);
}
// Set the description to the empty string, otherwise the tooltip will be // Set the description to the empty string, otherwise the tooltip will be
// used, which is redundant with the accessible name. // used, which is redundant with the accessible name.
node_data->SetDescription(base::string16()); node_data->SetDescription(base::string16());
...@@ -756,7 +789,8 @@ void DownloadItemView::OpenDownload() { ...@@ -756,7 +789,8 @@ void DownloadItemView::OpenDownload() {
UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download", UMA_HISTOGRAM_LONG_TIMES("clickjacking.open_download",
base::Time::Now() - creation_time_); base::Time::Now() - creation_time_);
UpdateAccessibleName(); // If this is still around for the next status update, it will be read.
announce_accessible_alert_soon_ = true;
// Calling download()->OpenDownload may delete this, so this must be // Calling download()->OpenDownload may delete this, so this must be
// the last thing we do. // the last thing we do.
...@@ -1083,6 +1117,8 @@ void DownloadItemView::Reenable() { ...@@ -1083,6 +1117,8 @@ void DownloadItemView::Reenable() {
void DownloadItemView::ReleaseDropdown() { void DownloadItemView::ReleaseDropdown() {
SetDropdownState(NORMAL); SetDropdownState(NORMAL);
// Make sure any new status from activating a context menu option is read.
announce_accessible_alert_soon_ = true;
} }
void DownloadItemView::UpdateAccessibleName() { void DownloadItemView::UpdateAccessibleName() {
...@@ -1094,12 +1130,74 @@ void DownloadItemView::UpdateAccessibleName() { ...@@ -1094,12 +1130,74 @@ void DownloadItemView::UpdateAccessibleName() {
download()->GetFileNameToReportUser().LossyDisplayName(); download()->GetFileNameToReportUser().LossyDisplayName();
} }
// If the name has changed, notify assistive technology that the name // Do not fire text changed notifications. Screen readers are notified of
// has changed so they can announce it immediately. // status changes via the accessible alert notifications, and text change
if (new_name != accessible_name_) { // notifications would be redundant.
accessible_name_ = new_name; accessible_name_ = new_name;
NotifyAccessibilityEvent(ax::mojom::Event::kTextChanged, true); }
base::string16 DownloadItemView::GetInProgressAccessibleAlertText() {
// If opening when complete or there is a warning, use the full status text.
if (download()->GetOpenWhenComplete() || IsShowingWarningDialog()) {
UpdateAccessibleName();
return accessible_name_;
}
// Prefer to announce the time remaining, if known.
base::TimeDelta remaining;
if (download()->TimeRemaining(&remaining)) {
// If complete, skip this round: a completion status update is coming soon.
if (remaining.is_zero())
return base::string16();
base::string16 remaining_string =
ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_REMAINING,
ui::TimeFormat::LENGTH_SHORT, remaining);
return l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_STATUS_TIME_REMAINING_ACCESSIBLE_ALERT, remaining_string);
}
// Time remaining is unknown, try to announce percent remaining.
if (model_.PercentComplete() > 0) {
DCHECK_LE(model_.PercentComplete(), 100);
return l10n_util::GetStringFUTF16Int(
IDS_DOWNLOAD_STATUS_PERCENT_COMPLETE_ACCESSIBLE_ALERT,
100 - model_.PercentComplete());
} }
// Percent remaining is also unknown, announce bytes to download.
base::string16 file_name =
download()->GetFileNameToReportUser().LossyDisplayName();
return l10n_util::GetStringFUTF16(
IDS_DOWNLOAD_STATUS_IN_PROGRESS_ACCESSIBLE_ALERT,
ui::FormatBytes(model_.GetTotalBytes()), file_name);
}
void DownloadItemView::UpdateAccessibleAlert(
const base::string16& accessible_alert_text,
bool is_last_update) {
views::ViewAccessibility& ax = accessible_alert_->GetViewAccessibility();
ax.OverrideRole(ax::mojom::Role::kAlert);
ax.OverrideName(accessible_alert_text);
if (is_last_update) {
// Last update: stop the announcement interval timer and make the last
// announcement immediately.
accessible_alert_timer_.AbandonAndStop();
AnnounceAccessibleAlert();
} else if (!accessible_alert_timer_.IsRunning()) {
// First update: start the announcement interval timer and make the first
// announcement immediately.
accessible_alert_timer_.Start(FROM_HERE, kAccessibleAlertInterval, this,
&DownloadItemView::AnnounceAccessibleAlert);
AnnounceAccessibleAlert();
} else if (announce_accessible_alert_soon_) {
accessible_alert_timer_.Reset();
AnnounceAccessibleAlert();
}
}
void DownloadItemView::AnnounceAccessibleAlert() {
accessible_alert_->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
announce_accessible_alert_soon_ = false;
} }
void DownloadItemView::AnimateStateTransition(State from, void DownloadItemView::AnimateStateTransition(State from,
......
...@@ -64,7 +64,9 @@ class DownloadItemView : public views::InkDropHostView, ...@@ -64,7 +64,9 @@ class DownloadItemView : public views::InkDropHostView,
public download::DownloadItem::Observer, public download::DownloadItem::Observer,
public gfx::AnimationDelegate { public gfx::AnimationDelegate {
public: public:
DownloadItemView(download::DownloadItem* download, DownloadShelfView* parent); DownloadItemView(download::DownloadItem* download,
DownloadShelfView* parent,
views::View* accessible_alert);
~DownloadItemView() override; ~DownloadItemView() override;
// Timer callback for handling animations // Timer callback for handling animations
...@@ -237,9 +239,24 @@ class DownloadItemView : public views::InkDropHostView, ...@@ -237,9 +239,24 @@ class DownloadItemView : public views::InkDropHostView,
// Update the accessible name to reflect the current state of the control, // Update the accessible name to reflect the current state of the control,
// so that screenreaders can access the filename, status text, and // so that screenreaders can access the filename, status text, and
// dangerous download warning message (if any). // dangerous download warning message (if any). The name will be presented
// when the download item receives focus.
void UpdateAccessibleName(); void UpdateAccessibleName();
// Update accessible status text.
// If |is_last_update| is false, then a timer is used to notify screen readers
// to speak the alert text on a regular interval. If |is_last_update| is true,
// then the screen reader is notified of the request to speak the alert
// immediately, and any running timer is ended.
void UpdateAccessibleAlert(const base::string16& alert, bool is_last_update);
// Get the accessible alert text for a download that is currently in progress.
base::string16 GetInProgressAccessibleAlertText();
// Callback for |accessible_update_timer_|, or can be used to ask a screen
// reader to speak the current alert immediately.
void AnnounceAccessibleAlert();
// Show/Hide/Reset |animation| based on the state transition specified by // Show/Hide/Reset |animation| based on the state transition specified by
// |from| and |to|. // |from| and |to|.
void AnimateStateTransition(State from, void AnimateStateTransition(State from,
...@@ -333,6 +350,17 @@ class DownloadItemView : public views::InkDropHostView, ...@@ -333,6 +350,17 @@ class DownloadItemView : public views::InkDropHostView,
// The name of this view as reported to assistive technology. // The name of this view as reported to assistive technology.
base::string16 accessible_name_; base::string16 accessible_name_;
// A hidden view for accessible status alerts, that are spoken by screen
// readers when a download changes state.
views::View* accessible_alert_;
// A timer for accessible alerts that helps reduce the number of similar
// messages spoken in a short period of time.
base::RepeatingTimer accessible_alert_timer_;
// Force the reading of the current alert text the next time it updates.
bool announce_accessible_alert_soon_;
// The icon loaded in the download shelf is based on the file path of the // The icon loaded in the download shelf is based on the file path of the
// item. Store the path used, so that we can detect a change in the path // item. Store the path used, so that we can detect a change in the path
// and reload the icon. // and reload the icon.
......
...@@ -117,6 +117,9 @@ DownloadShelfView::DownloadShelfView(Browser* browser, BrowserView* parent) ...@@ -117,6 +117,9 @@ DownloadShelfView::DownloadShelfView(Browser* browser, BrowserView* parent)
l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE)); l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE));
AddChildView(close_button_); AddChildView(close_button_);
accessible_alert_ = new views::View();
AddChildView(accessible_alert_);
new_item_animation_.SetSlideDuration(kNewItemAnimationDurationMs); new_item_animation_.SetSlideDuration(kNewItemAnimationDurationMs);
shelf_animation_.SetSlideDuration(kShelfAnimationDurationMs); shelf_animation_.SetSlideDuration(kShelfAnimationDurationMs);
...@@ -147,7 +150,7 @@ void DownloadShelfView::AddDownloadView(DownloadItemView* view) { ...@@ -147,7 +150,7 @@ void DownloadShelfView::AddDownloadView(DownloadItemView* view) {
} }
void DownloadShelfView::DoAddDownload(DownloadItem* download) { void DownloadShelfView::DoAddDownload(DownloadItem* download) {
AddDownloadView(new DownloadItemView(download, this)); AddDownloadView(new DownloadItemView(download, this, accessible_alert_));
} }
void DownloadShelfView::MouseMovedOutOfHost() { void DownloadShelfView::MouseMovedOutOfHost() {
......
...@@ -154,6 +154,10 @@ class DownloadShelfView : public views::AccessiblePaneView, ...@@ -154,6 +154,10 @@ class DownloadShelfView : public views::AccessiblePaneView,
// deleted by View. // deleted by View.
views::ImageButton* close_button_; views::ImageButton* close_button_;
// Hidden view that will contain status text for immediate output by
// screen readers.
views::View* accessible_alert_;
// The window this shelf belongs to. // The window this shelf belongs to.
BrowserView* parent_; BrowserView* parent_;
......
...@@ -97,7 +97,7 @@ void ViewAccessibility::GetAccessibleNodeData(ui::AXNodeData* data) const { ...@@ -97,7 +97,7 @@ void ViewAccessibility::GetAccessibleNodeData(ui::AXNodeData* data) const {
if (!owner_view_->enabled()) if (!owner_view_->enabled())
data->SetRestriction(ax::mojom::Restriction::kDisabled); data->SetRestriction(ax::mojom::Restriction::kDisabled);
if (!owner_view_->visible()) if (!owner_view_->visible() && data->role != ax::mojom::Role::kAlert)
data->AddState(ax::mojom::State::kInvisible); data->AddState(ax::mojom::State::kInvisible);
if (owner_view_->context_menu_controller()) if (owner_view_->context_menu_controller())
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment