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 "chrome/browser/media/media_stream_capture_indicator.h" 6 7 #include "base/bind.h" 8 #include "base/i18n/rtl.h" 9 #include "base/logging.h" 10 #include "base/memory/scoped_ptr.h" 11 #include "base/prefs/pref_service.h" 12 #include "base/strings/utf_string_conversions.h" 13 #include "chrome/app/chrome_command_ids.h" 14 #include "chrome/browser/browser_process.h" 15 #include "chrome/browser/extensions/extension_service.h" 16 #include "chrome/browser/profiles/profile.h" 17 #include "chrome/browser/status_icons/status_icon.h" 18 #include "chrome/browser/status_icons/status_tray.h" 19 #include "chrome/browser/tab_contents/tab_util.h" 20 #include "chrome/common/extensions/extension.h" 21 #include "chrome/common/pref_names.h" 22 #include "content/public/browser/browser_thread.h" 23 #include "content/public/browser/content_browser_client.h" 24 #include "content/public/browser/invalidate_type.h" 25 #include "content/public/browser/web_contents.h" 26 #include "content/public/browser/web_contents_delegate.h" 27 #include "content/public/browser/web_contents_observer.h" 28 #include "grit/chromium_strings.h" 29 #include "grit/generated_resources.h" 30 #include "grit/theme_resources.h" 31 #include "net/base/net_util.h" 32 #include "ui/base/l10n/l10n_util.h" 33 #include "ui/base/resource/resource_bundle.h" 34 #include "ui/gfx/image/image_skia.h" 35 36 using content::BrowserThread; 37 using content::WebContents; 38 39 namespace { 40 41 const extensions::Extension* GetExtension(WebContents* web_contents) { 42 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 43 44 if (!web_contents) 45 return NULL; 46 47 Profile* profile = 48 Profile::FromBrowserContext(web_contents->GetBrowserContext()); 49 if (!profile) 50 return NULL; 51 52 ExtensionService* extension_service = profile->GetExtensionService(); 53 if (!extension_service) 54 return NULL; 55 56 return extension_service->extensions()->GetExtensionOrAppByURL( 57 web_contents->GetURL()); 58 } 59 60 // Gets the security originator of the tab. It returns a string with no '/' 61 // at the end to display in the UI. 62 string16 GetSecurityOrigin(WebContents* web_contents) { 63 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 64 65 if (!web_contents) 66 return string16(); 67 68 std::string security_origin = web_contents->GetURL().GetOrigin().spec(); 69 70 // Remove the last character if it is a '/'. 71 if (!security_origin.empty()) { 72 std::string::iterator it = security_origin.end() - 1; 73 if (*it == '/') 74 security_origin.erase(it); 75 } 76 77 return UTF8ToUTF16(security_origin); 78 } 79 80 string16 GetTitle(WebContents* web_contents) { 81 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 82 83 if (!web_contents) 84 return string16(); 85 86 const extensions::Extension* const extension = GetExtension(web_contents); 87 if (extension) 88 return UTF8ToUTF16(extension->name()); 89 90 string16 tab_title = web_contents->GetTitle(); 91 92 if (tab_title.empty()) { 93 // If the page's title is empty use its security originator. 94 tab_title = GetSecurityOrigin(web_contents); 95 } else { 96 // If the page's title matches its URL, use its security originator. 97 Profile* profile = 98 Profile::FromBrowserContext(web_contents->GetBrowserContext()); 99 std::string languages = 100 profile->GetPrefs()->GetString(prefs::kAcceptLanguages); 101 if (tab_title == net::FormatUrl(web_contents->GetURL(), languages)) 102 tab_title = GetSecurityOrigin(web_contents); 103 } 104 105 return tab_title; 106 } 107 108 } // namespace 109 110 // Stores usage counts for all the capture devices associated with a single 111 // WebContents instance. Instances of this class are owned by 112 // MediaStreamCaptureIndicator. They also observe for the destruction of the 113 // WebContents instances and delete themselves when corresponding WebContents is 114 // deleted. 115 class MediaStreamCaptureIndicator::WebContentsDeviceUsage 116 : public content::WebContentsObserver { 117 public: 118 explicit WebContentsDeviceUsage( 119 scoped_refptr<MediaStreamCaptureIndicator> indicator, 120 WebContents* web_contents) 121 : WebContentsObserver(web_contents), 122 indicator_(indicator), 123 audio_ref_count_(0), 124 video_ref_count_(0), 125 mirroring_ref_count_(0), 126 weak_factory_(this) { 127 } 128 129 bool IsCapturingAudio() const { return audio_ref_count_ > 0; } 130 bool IsCapturingVideo() const { return video_ref_count_ > 0; } 131 bool IsMirroring() const { return mirroring_ref_count_ > 0; } 132 133 scoped_ptr<content::MediaStreamUI> RegisterMediaStream( 134 const content::MediaStreamDevices& devices); 135 136 // Increment ref-counts up based on the type of each device provided. 137 void AddDevices(const content::MediaStreamDevices& devices); 138 139 // Decrement ref-counts up based on the type of each device provided. 140 void RemoveDevices(const content::MediaStreamDevices& devices); 141 142 private: 143 // content::WebContentsObserver overrides. 144 virtual void WebContentsDestroyed(WebContents* web_contents) OVERRIDE { 145 indicator_->UnregisterWebContents(web_contents); 146 delete this; 147 } 148 149 scoped_refptr<MediaStreamCaptureIndicator> indicator_; 150 int audio_ref_count_; 151 int video_ref_count_; 152 int mirroring_ref_count_; 153 154 base::WeakPtrFactory<WebContentsDeviceUsage> weak_factory_; 155 156 DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage); 157 }; 158 159 // Implements MediaStreamUI interface. Instances of this class are created for 160 // each MediaStream and their ownership is passed to MediaStream implementation 161 // in the content layer. Each UIDelegate keeps a weak pointer to the 162 // corresponding WebContentsDeviceUsage object to deliver updates about state of 163 // the stream. 164 class MediaStreamCaptureIndicator::UIDelegate 165 : public content::MediaStreamUI { 166 public: 167 UIDelegate(base::WeakPtr<WebContentsDeviceUsage> device_usage, 168 const content::MediaStreamDevices& devices) 169 : device_usage_(device_usage), 170 devices_(devices), 171 started_(false) { 172 DCHECK(!devices_.empty()); 173 } 174 175 virtual ~UIDelegate() { 176 if (started_ && device_usage_.get()) 177 device_usage_->RemoveDevices(devices_); 178 } 179 180 private: 181 // content::MediaStreamUI interface. 182 virtual void OnStarted(const base::Closure& close_callback) OVERRIDE { 183 DCHECK(!started_); 184 started_ = true; 185 if (device_usage_.get()) 186 device_usage_->AddDevices(devices_); 187 } 188 189 base::WeakPtr<WebContentsDeviceUsage> device_usage_; 190 content::MediaStreamDevices devices_; 191 bool started_; 192 193 DISALLOW_COPY_AND_ASSIGN(UIDelegate); 194 }; 195 196 197 scoped_ptr<content::MediaStreamUI> 198 MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream( 199 const content::MediaStreamDevices& devices) { 200 return scoped_ptr<content::MediaStreamUI>(new UIDelegate( 201 weak_factory_.GetWeakPtr(), devices)); 202 } 203 204 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices( 205 const content::MediaStreamDevices& devices) { 206 for (content::MediaStreamDevices::const_iterator it = devices.begin(); 207 it != devices.end(); ++it) { 208 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE || 209 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) { 210 ++mirroring_ref_count_; 211 } else if (content::IsAudioMediaType(it->type)) { 212 ++audio_ref_count_; 213 } else if (content::IsVideoMediaType(it->type)) { 214 ++video_ref_count_; 215 } else { 216 NOTIMPLEMENTED(); 217 } 218 } 219 220 if (web_contents()) 221 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB); 222 223 indicator_->UpdateNotificationUserInterface(); 224 } 225 226 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices( 227 const content::MediaStreamDevices& devices) { 228 for (content::MediaStreamDevices::const_iterator it = devices.begin(); 229 it != devices.end(); ++it) { 230 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE || 231 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) { 232 --mirroring_ref_count_; 233 } else if (content::IsAudioMediaType(it->type)) { 234 --audio_ref_count_; 235 } else if (content::IsVideoMediaType(it->type)) { 236 --video_ref_count_; 237 } else { 238 NOTIMPLEMENTED(); 239 } 240 } 241 242 DCHECK_GE(audio_ref_count_, 0); 243 DCHECK_GE(video_ref_count_, 0); 244 DCHECK_GE(mirroring_ref_count_, 0); 245 246 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB); 247 indicator_->UpdateNotificationUserInterface(); 248 } 249 250 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator() 251 : status_icon_(NULL), 252 mic_image_(NULL), 253 camera_image_(NULL) { 254 } 255 256 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() { 257 // The user is responsible for cleaning up by reporting the closure of any 258 // opened devices. However, there exists a race condition at shutdown: The UI 259 // thread may be stopped before CaptureDevicesClosed() posts the task to 260 // invoke DoDevicesClosedOnUIThread(). In this case, usage_map_ won't be 261 // empty like it should. 262 DCHECK(usage_map_.empty() || 263 !BrowserThread::IsMessageLoopValid(BrowserThread::UI)); 264 265 // Free any WebContentsDeviceUsage objects left over. 266 for (UsageMap::const_iterator it = usage_map_.begin(); it != usage_map_.end(); 267 ++it) { 268 delete it->second; 269 } 270 } 271 272 scoped_ptr<content::MediaStreamUI> 273 MediaStreamCaptureIndicator::RegisterMediaStream( 274 content::WebContents* web_contents, 275 const content::MediaStreamDevices& devices) { 276 WebContentsDeviceUsage*& usage = usage_map_[web_contents]; 277 if (!usage) 278 usage = new WebContentsDeviceUsage(this, web_contents); 279 return usage->RegisterMediaStream(devices); 280 } 281 282 bool MediaStreamCaptureIndicator::IsCommandIdChecked( 283 int command_id) const { 284 NOTIMPLEMENTED() << "There are no checked items in the MediaStream menu."; 285 return false; 286 } 287 288 bool MediaStreamCaptureIndicator::IsCommandIdEnabled( 289 int command_id) const { 290 return command_id != IDC_MinimumLabelValue; 291 } 292 293 bool MediaStreamCaptureIndicator::GetAcceleratorForCommandId( 294 int command_id, ui::Accelerator* accelerator) { 295 // No accelerators for status icon context menu. 296 return false; 297 } 298 299 void MediaStreamCaptureIndicator::ExecuteCommand(int command_id, 300 int event_flags) { 301 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 302 303 const int index = 304 command_id - IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST; 305 DCHECK_LE(0, index); 306 DCHECK_GT(static_cast<int>(command_targets_.size()), index); 307 WebContents* const web_contents = command_targets_[index]; 308 UsageMap::const_iterator it = usage_map_.find(web_contents); 309 if (it == usage_map_.end()) 310 return; 311 web_contents->GetDelegate()->ActivateContents(web_contents); 312 } 313 314 bool MediaStreamCaptureIndicator::IsCapturingUserMedia( 315 content::WebContents* web_contents) const { 316 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 317 318 UsageMap::const_iterator it = usage_map_.find(web_contents); 319 return (it != usage_map_.end() && 320 (it->second->IsCapturingAudio() || it->second->IsCapturingVideo())); 321 } 322 323 bool MediaStreamCaptureIndicator::IsCapturingVideo( 324 content::WebContents* web_contents) const { 325 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 326 327 UsageMap::const_iterator it = usage_map_.find(web_contents); 328 return (it != usage_map_.end() && it->second->IsCapturingVideo()); 329 } 330 331 bool MediaStreamCaptureIndicator::IsCapturingAudio( 332 content::WebContents* web_contents) const { 333 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 334 335 UsageMap::const_iterator it = usage_map_.find(web_contents); 336 return (it != usage_map_.end() && it->second->IsCapturingAudio()); 337 } 338 339 bool MediaStreamCaptureIndicator::IsBeingMirrored( 340 content::WebContents* web_contents) const { 341 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 342 343 UsageMap::const_iterator it = usage_map_.find(web_contents); 344 return it != usage_map_.end() && it->second->IsMirroring(); 345 } 346 347 void MediaStreamCaptureIndicator::UnregisterWebContents( 348 WebContents* web_contents) { 349 usage_map_.erase(web_contents); 350 UpdateNotificationUserInterface(); 351 } 352 353 void MediaStreamCaptureIndicator::MaybeCreateStatusTrayIcon(bool audio, 354 bool video) { 355 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 356 if (status_icon_) 357 return; 358 359 // If there is no browser process, we should not create the status tray. 360 if (!g_browser_process) 361 return; 362 363 StatusTray* status_tray = g_browser_process->status_tray(); 364 if (!status_tray) 365 return; 366 367 EnsureStatusTrayIconResources(); 368 369 gfx::ImageSkia image; 370 string16 tool_tip; 371 GetStatusTrayIconInfo(audio, video, &image, &tool_tip); 372 DCHECK(!image.isNull()); 373 DCHECK(!tool_tip.empty()); 374 375 status_icon_ = status_tray->CreateStatusIcon( 376 StatusTray::MEDIA_STREAM_CAPTURE_ICON, image, tool_tip); 377 } 378 379 void MediaStreamCaptureIndicator::EnsureStatusTrayIconResources() { 380 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 381 if (!mic_image_) { 382 mic_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 383 IDR_INFOBAR_MEDIA_STREAM_MIC); 384 } 385 if (!camera_image_) { 386 camera_image_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 387 IDR_INFOBAR_MEDIA_STREAM_CAMERA); 388 } 389 DCHECK(mic_image_); 390 DCHECK(camera_image_); 391 } 392 393 void MediaStreamCaptureIndicator::MaybeDestroyStatusTrayIcon() { 394 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 395 396 if (!status_icon_) 397 return; 398 399 // If there is no browser process, we should not do anything. 400 if (!g_browser_process) 401 return; 402 403 StatusTray* status_tray = g_browser_process->status_tray(); 404 if (status_tray != NULL) { 405 status_tray->RemoveStatusIcon(status_icon_); 406 status_icon_ = NULL; 407 } 408 } 409 410 void MediaStreamCaptureIndicator::UpdateNotificationUserInterface() { 411 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 412 scoped_ptr<ui::SimpleMenuModel> menu(new ui::SimpleMenuModel(this)); 413 414 bool audio = false; 415 bool video = false; 416 int command_id = IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_FIRST; 417 command_targets_.clear(); 418 419 for (UsageMap::const_iterator iter = usage_map_.begin(); 420 iter != usage_map_.end(); ++iter) { 421 // Check if any audio and video devices have been used. 422 const WebContentsDeviceUsage& usage = *iter->second; 423 WebContents* const web_contents = iter->first; 424 425 // Audio/video icon is shown only for extensions or on Android. 426 // For regular tabs on desktop, we show an indicator in the tab icon. 427 if ((usage.IsCapturingAudio() || usage.IsCapturingVideo()) 428 #if !defined(OS_ANDROID) 429 && GetExtension(web_contents) 430 #endif 431 ) { 432 audio = audio || usage.IsCapturingAudio(); 433 video = video || usage.IsCapturingVideo(); 434 435 command_targets_.push_back(web_contents); 436 menu->AddItem(command_id, GetTitle(web_contents)); 437 438 // If reaching the maximum number, no more item will be added to the menu. 439 if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST) 440 break; 441 ++command_id; 442 } 443 } 444 445 if (command_targets_.empty()) { 446 MaybeDestroyStatusTrayIcon(); 447 return; 448 } 449 450 // The icon will take the ownership of the passed context menu. 451 MaybeCreateStatusTrayIcon(audio, video); 452 if (status_icon_) { 453 status_icon_->SetContextMenu(menu.release()); 454 } 455 } 456 457 void MediaStreamCaptureIndicator::GetStatusTrayIconInfo(bool audio, 458 bool video, 459 gfx::ImageSkia* image, 460 string16* tool_tip) { 461 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 462 DCHECK(audio || video); 463 464 int message_id = 0; 465 if (audio && video) { 466 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO; 467 *image = *camera_image_; 468 } else if (audio && !video) { 469 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY; 470 *image = *mic_image_; 471 } else if (!audio && video) { 472 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY; 473 *image = *camera_image_; 474 } 475 476 *tool_tip = l10n_util::GetStringUTF16(message_id); 477 } 478