// Copyright (c) 2012 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 "ash/system/chromeos/power/tray_power.h"
#include "ash/accessibility_delegate.h"
#include "ash/ash_switches.h"
#include "ash/shell.h"
#include "ash/system/chromeos/power/power_status_view.h"
#include "ash/system/date/date_view.h"
#include "ash/system/system_notifier.h"
#include "ash/system/tray/system_tray_delegate.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_notification_view.h"
#include "ash/system/tray/tray_utils.h"
#include "base/command_line.h"
#include "base/metrics/histogram.h"
#include "base/time/time.h"
#include "grit/ash_resources.h"
#include "grit/ash_strings.h"
#include "third_party/icu/source/i18n/unicode/fieldpos.h"
#include "third_party/icu/source/i18n/unicode/fmtable.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/notification.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
using message_center::MessageCenter;
using message_center::Notification;
namespace ash {
namespace tray {
namespace {
const int kMaxSpringChargerAccessibilityNotifyCount = 3;
const int kSpringChargerAccessibilityTimerFirstTimeNotifyInSeconds = 30;
const int kSpringChargerAccessibilityTimerRepeatInMinutes = 5;
}
// This view is used only for the tray.
class PowerTrayView : public views::ImageView {
public:
PowerTrayView()
: spring_charger_spoken_notification_count_(0) {
UpdateImage();
}
virtual ~PowerTrayView() {
}
// Overriden from views::View.
virtual void GetAccessibleState(ui::AXViewState* state) OVERRIDE {
state->name = accessible_name_;
state->role = ui::AX_ROLE_BUTTON;
}
void UpdateStatus(bool battery_alert) {
UpdateImage();
SetVisible(PowerStatus::Get()->IsBatteryPresent());
if (battery_alert) {
accessible_name_ = PowerStatus::Get()->GetAccessibleNameString(true);
NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);
}
}
void SetupNotifyBadCharger() {
// Poll with a shorter duration timer to notify the charger issue
// for the first time after the charger dialog is displayed.
spring_charger_accessility_timer_.Start(
FROM_HERE, base::TimeDelta::FromSeconds(
kSpringChargerAccessibilityTimerFirstTimeNotifyInSeconds),
this, &PowerTrayView::NotifyChargerIssue);
}
private:
void UpdateImage() {
SetImage(PowerStatus::Get()->GetBatteryImage(PowerStatus::ICON_LIGHT));
}
void NotifyChargerIssue() {
if (!Shell::GetInstance()->accessibility_delegate()->
IsSpokenFeedbackEnabled())
return;
if (!Shell::GetInstance()->system_tray_delegate()->
IsSpringChargerReplacementDialogVisible()) {
spring_charger_accessility_timer_.Stop();
return;
}
accessible_name_ = ui::ResourceBundle::GetSharedInstance().
GetLocalizedString(IDS_CHARGER_REPLACEMENT_ACCESSIBILTY_NOTIFICATION);
NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);
++spring_charger_spoken_notification_count_;
if (spring_charger_spoken_notification_count_ == 1) {
// After notify the charger issue for the first time, repeat the
// notification with a longer duration timer.
spring_charger_accessility_timer_.Stop();
spring_charger_accessility_timer_.Start(
FROM_HERE, base::TimeDelta::FromMinutes(
kSpringChargerAccessibilityTimerRepeatInMinutes),
this, &PowerTrayView::NotifyChargerIssue);
} else if (spring_charger_spoken_notification_count_ >=
kMaxSpringChargerAccessibilityNotifyCount) {
spring_charger_accessility_timer_.Stop();
}
}
base::string16 accessible_name_;
// Tracks how many times the original spring charger accessibility
// notification has been spoken.
int spring_charger_spoken_notification_count_;
base::RepeatingTimer<PowerTrayView> spring_charger_accessility_timer_;
DISALLOW_COPY_AND_ASSIGN(PowerTrayView);
};
class PowerNotificationView : public TrayNotificationView {
public:
explicit PowerNotificationView(TrayPower* owner)
: TrayNotificationView(owner, 0) {
power_status_view_ =
new PowerStatusView(PowerStatusView::VIEW_NOTIFICATION, true);
InitView(power_status_view_);
}
void UpdateStatus() {
SetIconImage(PowerStatus::Get()->GetBatteryImage(PowerStatus::ICON_DARK));
}
private:
PowerStatusView* power_status_view_;
DISALLOW_COPY_AND_ASSIGN(PowerNotificationView);
};
} // namespace tray
using tray::PowerNotificationView;
const int TrayPower::kCriticalMinutes = 5;
const int TrayPower::kLowPowerMinutes = 15;
const int TrayPower::kNoWarningMinutes = 30;
const int TrayPower::kCriticalPercentage = 5;
const int TrayPower::kLowPowerPercentage = 10;
const int TrayPower::kNoWarningPercentage = 15;
TrayPower::TrayPower(SystemTray* system_tray, MessageCenter* message_center)
: SystemTrayItem(system_tray),
message_center_(message_center),
power_tray_(NULL),
notification_view_(NULL),
notification_state_(NOTIFICATION_NONE),
usb_charger_was_connected_(false),
line_power_was_connected_(false) {
PowerStatus::Get()->AddObserver(this);
}
TrayPower::~TrayPower() {
PowerStatus::Get()->RemoveObserver(this);
}
views::View* TrayPower::CreateTrayView(user::LoginStatus status) {
// There may not be enough information when this is created about whether
// there is a battery or not. So always create this, and adjust visibility as
// necessary.
CHECK(power_tray_ == NULL);
power_tray_ = new tray::PowerTrayView();
power_tray_->UpdateStatus(false);
return power_tray_;
}
views::View* TrayPower::CreateDefaultView(user::LoginStatus status) {
// Make sure icon status is up-to-date. (Also triggers stub activation).
PowerStatus::Get()->RequestStatusUpdate();
return NULL;
}
views::View* TrayPower::CreateNotificationView(user::LoginStatus status) {
CHECK(notification_view_ == NULL);
if (!PowerStatus::Get()->IsBatteryPresent())
return NULL;
notification_view_ = new PowerNotificationView(this);
notification_view_->UpdateStatus();
return notification_view_;
}
void TrayPower::DestroyTrayView() {
power_tray_ = NULL;
}
void TrayPower::DestroyDefaultView() {
}
void TrayPower::DestroyNotificationView() {
notification_view_ = NULL;
}
void TrayPower::UpdateAfterLoginStatusChange(user::LoginStatus status) {
}
void TrayPower::UpdateAfterShelfAlignmentChange(ShelfAlignment alignment) {
SetTrayImageItemBorder(power_tray_, alignment);
}
void TrayPower::OnPowerStatusChanged() {
RecordChargerType();
if (PowerStatus::Get()->IsOriginalSpringChargerConnected()) {
if (ash::Shell::GetInstance()->system_tray_delegate()->
ShowSpringChargerReplacementDialog()) {
power_tray_->SetupNotifyBadCharger();
}
}
bool battery_alert = UpdateNotificationState();
if (power_tray_)
power_tray_->UpdateStatus(battery_alert);
if (notification_view_)
notification_view_->UpdateStatus();
// Factory testing may place the battery into unusual states.
if (CommandLine::ForCurrentProcess()->HasSwitch(
ash::switches::kAshHideNotificationsForFactory))
return;
MaybeShowUsbChargerNotification();
if (battery_alert)
ShowNotificationView();
else if (notification_state_ == NOTIFICATION_NONE)
HideNotificationView();
usb_charger_was_connected_ = PowerStatus::Get()->IsUsbChargerConnected();
line_power_was_connected_ = PowerStatus::Get()->IsLinePowerConnected();
}
bool TrayPower::MaybeShowUsbChargerNotification() {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
const char kNotificationId[] = "usb-charger";
bool usb_charger_is_connected = PowerStatus::Get()->IsUsbChargerConnected();
// Check for a USB charger being connected.
if (usb_charger_is_connected && !usb_charger_was_connected_) {
scoped_ptr<Notification> notification(new Notification(
message_center::NOTIFICATION_TYPE_SIMPLE,
kNotificationId,
rb.GetLocalizedString(IDS_ASH_STATUS_TRAY_LOW_POWER_CHARGER_TITLE),
rb.GetLocalizedString(
IDS_ASH_STATUS_TRAY_LOW_POWER_CHARGER_MESSAGE_SHORT),
rb.GetImageNamed(IDR_AURA_NOTIFICATION_LOW_POWER_CHARGER),
base::string16(),
message_center::NotifierId(
message_center::NotifierId::SYSTEM_COMPONENT,
system_notifier::kNotifierPower),
message_center::RichNotificationData(),
NULL));
message_center_->AddNotification(notification.Pass());
return true;
}
// Check for unplug of a USB charger while the USB charger notification is
// showing.
if (!usb_charger_is_connected && usb_charger_was_connected_) {
message_center_->RemoveNotification(kNotificationId, false);
return true;
}
return false;
}
bool TrayPower::UpdateNotificationState() {
const PowerStatus& status = *PowerStatus::Get();
if (!status.IsBatteryPresent() ||
status.IsBatteryTimeBeingCalculated() ||
status.IsMainsChargerConnected() ||
status.IsOriginalSpringChargerConnected()) {
notification_state_ = NOTIFICATION_NONE;
return false;
}
return status.IsUsbChargerConnected() ?
UpdateNotificationStateForRemainingPercentage() :
UpdateNotificationStateForRemainingTime();
}
bool TrayPower::UpdateNotificationStateForRemainingTime() {
// The notification includes a rounded minutes value, so round the estimate
// received from the power manager to match.
const int remaining_minutes = static_cast<int>(
PowerStatus::Get()->GetBatteryTimeToEmpty().InSecondsF() / 60.0 + 0.5);
if (remaining_minutes >= kNoWarningMinutes ||
PowerStatus::Get()->IsBatteryFull()) {
notification_state_ = NOTIFICATION_NONE;
return false;
}
switch (notification_state_) {
case NOTIFICATION_NONE:
if (remaining_minutes <= kCriticalMinutes) {
notification_state_ = NOTIFICATION_CRITICAL;
return true;
}
if (remaining_minutes <= kLowPowerMinutes) {
notification_state_ = NOTIFICATION_LOW_POWER;
return true;
}
return false;
case NOTIFICATION_LOW_POWER:
if (remaining_minutes <= kCriticalMinutes) {
notification_state_ = NOTIFICATION_CRITICAL;
return true;
}
return false;
case NOTIFICATION_CRITICAL:
return false;
}
NOTREACHED();
return false;
}
bool TrayPower::UpdateNotificationStateForRemainingPercentage() {
// The notification includes a rounded percentage, so round the value received
// from the power manager to match.
const int remaining_percentage =
PowerStatus::Get()->GetRoundedBatteryPercent();
if (remaining_percentage >= kNoWarningPercentage ||
PowerStatus::Get()->IsBatteryFull()) {
notification_state_ = NOTIFICATION_NONE;
return false;
}
switch (notification_state_) {
case NOTIFICATION_NONE:
if (remaining_percentage <= kCriticalPercentage) {
notification_state_ = NOTIFICATION_CRITICAL;
return true;
}
if (remaining_percentage <= kLowPowerPercentage) {
notification_state_ = NOTIFICATION_LOW_POWER;
return true;
}
return false;
case NOTIFICATION_LOW_POWER:
if (remaining_percentage <= kCriticalPercentage) {
notification_state_ = NOTIFICATION_CRITICAL;
return true;
}
return false;
case NOTIFICATION_CRITICAL:
return false;
}
NOTREACHED();
return false;
}
void TrayPower::RecordChargerType() {
if (!PowerStatus::Get()->IsLinePowerConnected() ||
line_power_was_connected_)
return;
ChargerType current_charger = UNKNOWN_CHARGER;
if (PowerStatus::Get()->IsMainsChargerConnected()) {
current_charger = MAINS_CHARGER;
} else if (PowerStatus::Get()->IsUsbChargerConnected()) {
current_charger = USB_CHARGER;
} else if (PowerStatus::Get()->IsOriginalSpringChargerConnected()) {
current_charger =
ash::Shell::GetInstance()->system_tray_delegate()->
HasUserConfirmedSafeSpringCharger() ?
SAFE_SPRING_CHARGER : UNCONFIRMED_SPRING_CHARGER;
}
if (current_charger != UNKNOWN_CHARGER) {
UMA_HISTOGRAM_ENUMERATION("Power.ChargerType",
current_charger,
CHARGER_TYPE_COUNT);
}
}
} // namespace ash