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
33 namespace {
34
35 // Min height of the list of messages in the popup.
36 const int kMessageListMinHeight = 200;
37 // Top/bottom padding of the text items.
38 const int kPaddingVertical = 10;
39
40 const char kSmsNumberKey[] = "number";
41 const char kSmsTextKey[] = "text";
42
GetMessageFromDictionary(const base::DictionaryValue * message,std::string * number,std::string * text)43 bool 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
55 namespace ash {
56
57 class TraySms::SmsDefaultView : public TrayItemMore {
58 public:
SmsDefaultView(TraySms * owner)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
~SmsDefaultView()66 virtual ~SmsDefaultView() {}
67
Update()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.
81 class TraySms::SmsMessageView : public views::View,
82 public views::ButtonListener {
83 public:
84 enum ViewType {
85 VIEW_DETAILED,
86 VIEW_NOTIFICATION
87 };
88
SmsMessageView(TraySms * owner,ViewType view_type,size_t index,const std::string & number,const std::string & message)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
~SmsMessageView()113 virtual ~SmsMessageView() {
114 }
115
116 // Overridden from ButtonListener.
ButtonPressed(views::Button * sender,const ui::Event & event)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:
LayoutDetailedView()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
LayoutNotificationView()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
179 class TraySms::SmsDetailedView : public TrayDetailsView,
180 public ViewClickListener {
181 public:
SmsDetailedView(TraySms * owner)182 explicit SmsDetailedView(TraySms* owner)
183 : TrayDetailsView(owner) {
184 Init();
185 Update();
186 }
187
~SmsDetailedView()188 virtual ~SmsDetailedView() {
189 }
190
Init()191 void Init() {
192 CreateScrollableList();
193 CreateSpecialRow(IDS_ASH_STATUS_TRAY_SMS, this);
194 }
195
Update()196 void Update() {
197 UpdateMessageList();
198 Layout();
199 SchedulePaint();
200 }
201
202 // Overridden from views::View.
GetPreferredSize() const203 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:
UpdateMessageList()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.
OnViewClicked(views::View * sender)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
243 class TraySms::SmsNotificationView : public TrayNotificationView {
244 public:
SmsNotificationView(TraySms * owner,size_t message_index,const std::string & number,const std::string & text)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
Update(size_t message_index,const std::string & number,const std::string & text)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:
OnClose()266 virtual void OnClose() OVERRIDE {
267 tray_sms()->RemoveMessage(message_index_);
268 }
269
OnClickAction()270 virtual void OnClickAction() OVERRIDE {
271 owner()->PopupDetailedView(0, true);
272 }
273
274 private:
tray_sms()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
TraySms(SystemTray * system_tray)284 TraySms::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
~TraySms()296 TraySms::~TraySms() {
297 if (chromeos::NetworkHandler::IsInitialized()) {
298 chromeos::NetworkHandler::Get()->network_sms_handler()->RemoveObserver(
299 this);
300 }
301 }
302
CreateDefaultView(user::LoginStatus status)303 views::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
CreateDetailedView(user::LoginStatus status)310 views::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
CreateNotificationView(user::LoginStatus status)319 views::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
DestroyDefaultView()330 void TraySms::DestroyDefaultView() {
331 default_ = NULL;
332 }
333
DestroyDetailedView()334 void TraySms::DestroyDetailedView() {
335 detailed_ = NULL;
336 }
337
DestroyNotificationView()338 void TraySms::DestroyNotificationView() {
339 notification_ = NULL;
340 }
341
MessageReceived(const base::DictionaryValue & message)342 void 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
GetLatestMessage(size_t * index,std::string * number,std::string * text)374 bool 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
RemoveMessage(size_t index)389 void TraySms::RemoveMessage(size_t index) {
390 if (index < messages_.GetSize())
391 messages_.Remove(index, NULL);
392 }
393
Update(bool notify)394 void 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