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