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 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: 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. 81 class 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 179 class 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 243 class 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 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 296 TraySms::~TraySms() { 297 if (chromeos::NetworkHandler::IsInitialized()) { 298 chromeos::NetworkHandler::Get()->network_sms_handler()->RemoveObserver( 299 this); 300 } 301 } 302 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 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 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 330 void TraySms::DestroyDefaultView() { 331 default_ = NULL; 332 } 333 334 void TraySms::DestroyDetailedView() { 335 detailed_ = NULL; 336 } 337 338 void TraySms::DestroyNotificationView() { 339 notification_ = NULL; 340 } 341 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 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 389 void TraySms::RemoveMessage(size_t index) { 390 if (index < messages_.GetSize()) 391 messages_.Remove(index, NULL); 392 } 393 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