tray_sms.cc revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
1// Copyright (c) 2012 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 "ash/system/chromeos/network/tray_sms.h"
6
7#include "ash/shell.h"
8#include "ash/system/tray/fixed_sized_scroll_view.h"
9#include "ash/system/tray/system_tray.h"
10#include "ash/system/tray/system_tray_bubble.h"
11#include "ash/system/tray/system_tray_notifier.h"
12#include "ash/system/tray/tray_constants.h"
13#include "ash/system/tray/tray_details_view.h"
14#include "ash/system/tray/tray_item_more.h"
15#include "ash/system/tray/tray_item_view.h"
16#include "ash/system/tray/tray_notification_view.h"
17#include "base/strings/string_number_conversions.h"
18#include "base/strings/utf_string_conversions.h"
19#include "chromeos/network/network_event_log.h"
20#include "chromeos/network/network_handler.h"
21#include "grit/ash_resources.h"
22#include "grit/ash_strings.h"
23#include "ui/base/l10n/l10n_util.h"
24#include "ui/base/resource/resource_bundle.h"
25#include "ui/views/bubble/tray_bubble_view.h"
26#include "ui/views/controls/image_view.h"
27#include "ui/views/controls/label.h"
28#include "ui/views/layout/box_layout.h"
29#include "ui/views/layout/fill_layout.h"
30#include "ui/views/layout/grid_layout.h"
31#include "ui/views/view.h"
32
33namespace {
34
35// Min height of the list of messages in the popup.
36const int kMessageListMinHeight = 200;
37// Top/bottom padding of the text items.
38const int kPaddingVertical = 10;
39
40const char kSmsNumberKey[] = "number";
41const char kSmsTextKey[] = "text";
42
43bool GetMessageFromDictionary(const base::DictionaryValue* message,
44                              std::string* number,
45                              std::string* text) {
46  if (!message->GetStringWithoutPathExpansion(kSmsNumberKey, number))
47    return false;
48  if (!message->GetStringWithoutPathExpansion(kSmsTextKey, text))
49    return false;
50  return true;
51}
52
53}  // namespace
54
55namespace ash {
56namespace internal {
57
58class TraySms::SmsDefaultView : public TrayItemMore {
59 public:
60  explicit SmsDefaultView(TraySms* owner)
61      : TrayItemMore(owner, true) {
62    SetImage(ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
63        IDR_AURA_UBER_TRAY_SMS));
64    Update();
65  }
66
67  virtual ~SmsDefaultView() {}
68
69  void Update() {
70    int message_count = static_cast<TraySms*>(owner())->messages().GetSize();
71    base::string16 label = l10n_util::GetStringFUTF16(
72        IDS_ASH_STATUS_TRAY_SMS_MESSAGES, base::IntToString16(message_count));
73    SetLabel(label);
74    SetAccessibleName(label);
75  }
76
77 private:
78  DISALLOW_COPY_AND_ASSIGN(SmsDefaultView);
79};
80
81// An entry (row) in SmsDetailedView or NotificationView.
82class TraySms::SmsMessageView : public views::View,
83                                public views::ButtonListener {
84 public:
85  enum ViewType {
86    VIEW_DETAILED,
87    VIEW_NOTIFICATION
88  };
89
90  SmsMessageView(TraySms* owner,
91                 ViewType view_type,
92                 size_t index,
93                 const std::string& number,
94                 const std::string& message)
95      : owner_(owner),
96        index_(index) {
97    number_label_ = new views::Label(
98        l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_SMS_NUMBER,
99                                   base::UTF8ToUTF16(number)),
100        ui::ResourceBundle::GetSharedInstance().GetFontList(
101            ui::ResourceBundle::BoldFont));
102    number_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
103
104    message_label_ = new views::Label(base::UTF8ToUTF16(message));
105    message_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
106    message_label_->SetMultiLine(true);
107
108    if (view_type == VIEW_DETAILED)
109      LayoutDetailedView();
110    else
111      LayoutNotificationView();
112  }
113
114  virtual ~SmsMessageView() {
115  }
116
117  // Overridden from ButtonListener.
118  virtual void ButtonPressed(views::Button* sender,
119                             const ui::Event& event) OVERRIDE {
120    owner_->RemoveMessage(index_);
121    owner_->Update(false);
122  }
123
124 private:
125  void LayoutDetailedView() {
126    views::ImageButton* close_button = new views::ImageButton(this);
127    close_button->SetImage(
128        views::CustomButton::STATE_NORMAL,
129        ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
130            IDR_AURA_UBER_TRAY_SMS_DISMISS));
131    const int msg_width = owner_->system_tray()->GetSystemBubble()->
132        bubble_view()->GetPreferredSize().width() -
133            (kNotificationIconWidth + kTrayPopupPaddingHorizontal * 2);
134    message_label_->SizeToFit(msg_width);
135
136    views::GridLayout* layout = new views::GridLayout(this);
137    SetLayoutManager(layout);
138
139    views::ColumnSet* columns = layout->AddColumnSet(0);
140
141    // Message
142    columns->AddPaddingColumn(0, kTrayPopupPaddingHorizontal);
143    columns->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
144                       0 /* resize percent */,
145                       views::GridLayout::FIXED, msg_width, msg_width);
146
147    // Close button
148    columns->AddColumn(views::GridLayout::TRAILING, views::GridLayout::CENTER,
149                       0, /* resize percent */
150                       views::GridLayout::FIXED,
151                       kNotificationIconWidth, kNotificationIconWidth);
152
153
154    layout->AddPaddingRow(0, kPaddingVertical);
155    layout->StartRow(0, 0);
156    layout->AddView(number_label_);
157    layout->AddView(close_button, 1, 2);  // 2 rows for icon
158    layout->StartRow(0, 0);
159    layout->AddView(message_label_);
160
161    layout->AddPaddingRow(0, kPaddingVertical);
162  }
163
164  void LayoutNotificationView() {
165    SetLayoutManager(
166        new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 1));
167    AddChildView(number_label_);
168    message_label_->SizeToFit(kTrayNotificationContentsWidth);
169    AddChildView(message_label_);
170  }
171
172  TraySms* owner_;
173  size_t index_;
174  views::Label* number_label_;
175  views::Label* message_label_;
176
177  DISALLOW_COPY_AND_ASSIGN(SmsMessageView);
178};
179
180class TraySms::SmsDetailedView : public TrayDetailsView,
181                                 public ViewClickListener {
182 public:
183  explicit SmsDetailedView(TraySms* owner)
184      : TrayDetailsView(owner) {
185    Init();
186    Update();
187  }
188
189  virtual ~SmsDetailedView() {
190  }
191
192  void Init() {
193    CreateScrollableList();
194    CreateSpecialRow(IDS_ASH_STATUS_TRAY_SMS, this);
195  }
196
197  void Update() {
198    UpdateMessageList();
199    Layout();
200    SchedulePaint();
201  }
202
203  // Overridden from views::View.
204  virtual gfx::Size GetPreferredSize() OVERRIDE {
205    gfx::Size preferred_size = TrayDetailsView::GetPreferredSize();
206    if (preferred_size.height() < kMessageListMinHeight)
207      preferred_size.set_height(kMessageListMinHeight);
208    return preferred_size;
209  }
210
211 private:
212  void UpdateMessageList() {
213    const base::ListValue& messages =
214        static_cast<TraySms*>(owner())->messages();
215    scroll_content()->RemoveAllChildViews(true);
216    for (size_t index = 0; index < messages.GetSize(); ++index) {
217      const base::DictionaryValue* message = NULL;
218      if (!messages.GetDictionary(index, &message)) {
219        LOG(ERROR) << "SMS message not a dictionary at: " << index;
220        continue;
221      }
222      std::string number, text;
223      if (!GetMessageFromDictionary(message, &number, &text)) {
224        LOG(ERROR) << "Error parsing SMS message";
225        continue;
226      }
227      SmsMessageView* msgview = new SmsMessageView(
228          static_cast<TraySms*>(owner()), SmsMessageView::VIEW_DETAILED, index,
229          number, text);
230      scroll_content()->AddChildView(msgview);
231    }
232    scroller()->Layout();
233  }
234
235  // Overridden from ViewClickListener.
236  virtual void OnViewClicked(views::View* sender) OVERRIDE {
237    if (sender == footer()->content())
238      TransitionToDefaultView();
239  }
240
241  DISALLOW_COPY_AND_ASSIGN(SmsDetailedView);
242};
243
244class TraySms::SmsNotificationView : public TrayNotificationView {
245 public:
246  SmsNotificationView(TraySms* owner,
247                      size_t message_index,
248                      const std::string& number,
249                      const std::string& text)
250      : TrayNotificationView(owner, IDR_AURA_UBER_TRAY_SMS),
251        message_index_(message_index) {
252    SmsMessageView* message_view = new SmsMessageView(
253        owner, SmsMessageView::VIEW_NOTIFICATION, message_index_, number, text);
254    InitView(message_view);
255  }
256
257  void Update(size_t message_index,
258              const std::string& number,
259              const std::string& text) {
260    SmsMessageView* message_view = new SmsMessageView(
261        tray_sms(), SmsMessageView::VIEW_NOTIFICATION,
262        message_index_, number, text);
263    UpdateView(message_view);
264  }
265
266  // Overridden from TrayNotificationView:
267  virtual void OnClose() OVERRIDE {
268    tray_sms()->RemoveMessage(message_index_);
269  }
270
271  virtual void OnClickAction() OVERRIDE {
272    owner()->PopupDetailedView(0, true);
273  }
274
275 private:
276  TraySms* tray_sms() {
277    return static_cast<TraySms*>(owner());
278  }
279
280  size_t message_index_;
281
282  DISALLOW_COPY_AND_ASSIGN(SmsNotificationView);
283};
284
285TraySms::TraySms(SystemTray* system_tray)
286    : SystemTrayItem(system_tray),
287      default_(NULL),
288      detailed_(NULL),
289      notification_(NULL) {
290  // TODO(armansito): SMS could be a special case for cellular that requires a
291  // user (perhaps the owner) to be logged in. If that is the case, then an
292  // additional check should be done before subscribing for SMS notifications.
293  if (chromeos::NetworkHandler::IsInitialized())
294    chromeos::NetworkHandler::Get()->network_sms_handler()->AddObserver(this);
295}
296
297TraySms::~TraySms() {
298  if (chromeos::NetworkHandler::IsInitialized()) {
299    chromeos::NetworkHandler::Get()->network_sms_handler()->RemoveObserver(
300        this);
301  }
302}
303
304views::View* TraySms::CreateDefaultView(user::LoginStatus status) {
305  CHECK(default_ == NULL);
306  default_ = new SmsDefaultView(this);
307  default_->SetVisible(!messages_.empty());
308  return default_;
309}
310
311views::View* TraySms::CreateDetailedView(user::LoginStatus status) {
312  CHECK(detailed_ == NULL);
313  HideNotificationView();
314  if (messages_.empty())
315    return NULL;
316  detailed_ = new SmsDetailedView(this);
317  return detailed_;
318}
319
320views::View* TraySms::CreateNotificationView(user::LoginStatus status) {
321  CHECK(notification_ == NULL);
322  if (detailed_)
323    return NULL;
324  size_t index;
325  std::string number, text;
326  if (GetLatestMessage(&index, &number, &text))
327    notification_ = new SmsNotificationView(this, index, number, text);
328  return notification_;
329}
330
331void TraySms::DestroyDefaultView() {
332  default_ = NULL;
333}
334
335void TraySms::DestroyDetailedView() {
336  detailed_ = NULL;
337}
338
339void TraySms::DestroyNotificationView() {
340  notification_ = NULL;
341}
342
343void TraySms::MessageReceived(const base::DictionaryValue& message) {
344
345  std::string message_text;
346  if (!message.GetStringWithoutPathExpansion(
347          chromeos::NetworkSmsHandler::kTextKey, &message_text)) {
348    NET_LOG_ERROR("SMS message contains no content.", "");
349    return;
350  }
351  // TODO(armansito): A message might be due to a special "Message Waiting"
352  // state that the message is in. Once SMS handling moves to shill, such
353  // messages should be filtered there so that this check becomes unnecessary.
354  if (message_text.empty()) {
355    NET_LOG_DEBUG("SMS has empty content text. Ignoring.", "");
356    return;
357  }
358  std::string message_number;
359  if (!message.GetStringWithoutPathExpansion(
360          chromeos::NetworkSmsHandler::kNumberKey, &message_number)) {
361    NET_LOG_DEBUG("SMS contains no number. Ignoring.", "");
362    return;
363  }
364
365  NET_LOG_DEBUG("Received SMS from: " + message_number + " with text: " +
366                message_text, "");
367
368  base::DictionaryValue* dict = new base::DictionaryValue();
369  dict->SetString(kSmsNumberKey, message_number);
370  dict->SetString(kSmsTextKey, message_text);
371  messages_.Append(dict);
372  Update(true);
373}
374
375bool TraySms::GetLatestMessage(size_t* index,
376                               std::string* number,
377                               std::string* text) {
378  if (messages_.empty())
379    return false;
380  base::DictionaryValue* message;
381  size_t message_index = messages_.GetSize() - 1;
382  if (!messages_.GetDictionary(message_index, &message))
383    return false;
384  if (!GetMessageFromDictionary(message, number, text))
385    return false;
386  *index = message_index;
387  return true;
388}
389
390void TraySms::RemoveMessage(size_t index) {
391  if (index < messages_.GetSize())
392    messages_.Remove(index, NULL);
393}
394
395void TraySms::Update(bool notify) {
396  if (messages_.empty()) {
397    if (default_)
398      default_->SetVisible(false);
399    if (detailed_)
400      HideDetailedView();
401    HideNotificationView();
402  } else {
403    if (default_) {
404      default_->SetVisible(true);
405      default_->Update();
406    }
407    if (detailed_)
408      detailed_->Update();
409    if (notification_) {
410      size_t index;
411      std::string number, text;
412      if (GetLatestMessage(&index, &number, &text))
413        notification_->Update(index, number, text);
414    } else if (notify) {
415      ShowNotificationView();
416    }
417  }
418}
419
420}  // namespace internal
421}  // namespace ash
422