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/profiles/profile.h" 16 #include "chrome/browser/status_icons/status_icon.h" 17 #include "chrome/browser/status_icons/status_tray.h" 18 #include "chrome/browser/tab_contents/tab_util.h" 19 #include "chrome/common/pref_names.h" 20 #include "chrome/grit/chromium_strings.h" 21 #include "content/public/browser/browser_thread.h" 22 #include "content/public/browser/content_browser_client.h" 23 #include "content/public/browser/invalidate_type.h" 24 #include "content/public/browser/web_contents.h" 25 #include "content/public/browser/web_contents_delegate.h" 26 #include "content/public/browser/web_contents_observer.h" 27 #include "grit/theme_resources.h" 28 #include "net/base/net_util.h" 29 #include "ui/base/l10n/l10n_util.h" 30 #include "ui/base/resource/resource_bundle.h" 31 #include "ui/gfx/image/image_skia.h" 32 33 #if defined(ENABLE_EXTENSIONS) 34 #include "chrome/common/extensions/extension_constants.h" 35 #include "extensions/browser/extension_registry.h" 36 #include "extensions/common/extension.h" 37 #endif 38 39 using content::BrowserThread; 40 using content::WebContents; 41 42 namespace { 43 44 #if defined(ENABLE_EXTENSIONS) 45 const extensions::Extension* GetExtension(WebContents* web_contents) { 46 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 47 48 if (!web_contents) 49 return NULL; 50 51 extensions::ExtensionRegistry* registry = 52 extensions::ExtensionRegistry::Get(web_contents->GetBrowserContext()); 53 return registry->enabled_extensions().GetExtensionOrAppByURL( 54 web_contents->GetURL()); 55 } 56 57 bool IsWhitelistedExtension(const extensions::Extension* extension) { 58 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 59 60 static const char* const kExtensionWhitelist[] = { 61 extension_misc::kHotwordExtensionId, 62 }; 63 64 for (size_t i = 0; i < arraysize(kExtensionWhitelist); ++i) { 65 if (extension->id() == kExtensionWhitelist[i]) 66 return true; 67 } 68 69 return false; 70 } 71 #endif // defined(ENABLE_EXTENSIONS) 72 73 // Gets the security originator of the tab. It returns a string with no '/' 74 // at the end to display in the UI. 75 base::string16 GetSecurityOrigin(WebContents* web_contents) { 76 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 77 78 if (!web_contents) 79 return base::string16(); 80 81 std::string security_origin = web_contents->GetURL().GetOrigin().spec(); 82 83 // Remove the last character if it is a '/'. 84 if (!security_origin.empty()) { 85 std::string::iterator it = security_origin.end() - 1; 86 if (*it == '/') 87 security_origin.erase(it); 88 } 89 90 return base::UTF8ToUTF16(security_origin); 91 } 92 93 base::string16 GetTitle(WebContents* web_contents) { 94 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 95 96 if (!web_contents) 97 return base::string16(); 98 99 #if defined(ENABLE_EXTENSIONS) 100 const extensions::Extension* const extension = GetExtension(web_contents); 101 if (extension) 102 return base::UTF8ToUTF16(extension->name()); 103 #endif 104 105 base::string16 tab_title = web_contents->GetTitle(); 106 107 if (tab_title.empty()) { 108 // If the page's title is empty use its security originator. 109 tab_title = GetSecurityOrigin(web_contents); 110 } else { 111 // If the page's title matches its URL, use its security originator. 112 Profile* profile = 113 Profile::FromBrowserContext(web_contents->GetBrowserContext()); 114 std::string languages = 115 profile->GetPrefs()->GetString(prefs::kAcceptLanguages); 116 if (tab_title == net::FormatUrl(web_contents->GetURL(), languages)) 117 tab_title = GetSecurityOrigin(web_contents); 118 } 119 120 return tab_title; 121 } 122 123 } // namespace 124 125 // Stores usage counts for all the capture devices associated with a single 126 // WebContents instance. Instances of this class are owned by 127 // MediaStreamCaptureIndicator. They also observe for the destruction of the 128 // WebContents instances and delete themselves when corresponding WebContents is 129 // deleted. 130 class MediaStreamCaptureIndicator::WebContentsDeviceUsage 131 : public content::WebContentsObserver { 132 public: 133 explicit WebContentsDeviceUsage( 134 scoped_refptr<MediaStreamCaptureIndicator> indicator, 135 WebContents* web_contents) 136 : WebContentsObserver(web_contents), 137 indicator_(indicator), 138 audio_ref_count_(0), 139 video_ref_count_(0), 140 mirroring_ref_count_(0), 141 weak_factory_(this) { 142 } 143 144 bool IsCapturingAudio() const { return audio_ref_count_ > 0; } 145 bool IsCapturingVideo() const { return video_ref_count_ > 0; } 146 bool IsMirroring() const { return mirroring_ref_count_ > 0; } 147 148 scoped_ptr<content::MediaStreamUI> RegisterMediaStream( 149 const content::MediaStreamDevices& devices); 150 151 // Increment ref-counts up based on the type of each device provided. 152 void AddDevices(const content::MediaStreamDevices& devices); 153 154 // Decrement ref-counts up based on the type of each device provided. 155 void RemoveDevices(const content::MediaStreamDevices& devices); 156 157 private: 158 // content::WebContentsObserver overrides. 159 virtual void WebContentsDestroyed() OVERRIDE { 160 indicator_->UnregisterWebContents(web_contents()); 161 delete this; 162 } 163 164 scoped_refptr<MediaStreamCaptureIndicator> indicator_; 165 int audio_ref_count_; 166 int video_ref_count_; 167 int mirroring_ref_count_; 168 169 base::WeakPtrFactory<WebContentsDeviceUsage> weak_factory_; 170 171 DISALLOW_COPY_AND_ASSIGN(WebContentsDeviceUsage); 172 }; 173 174 // Implements MediaStreamUI interface. Instances of this class are created for 175 // each MediaStream and their ownership is passed to MediaStream implementation 176 // in the content layer. Each UIDelegate keeps a weak pointer to the 177 // corresponding WebContentsDeviceUsage object to deliver updates about state of 178 // the stream. 179 class MediaStreamCaptureIndicator::UIDelegate 180 : public content::MediaStreamUI { 181 public: 182 UIDelegate(base::WeakPtr<WebContentsDeviceUsage> device_usage, 183 const content::MediaStreamDevices& devices) 184 : device_usage_(device_usage), 185 devices_(devices), 186 started_(false) { 187 DCHECK(!devices_.empty()); 188 } 189 190 virtual ~UIDelegate() { 191 if (started_ && device_usage_.get()) 192 device_usage_->RemoveDevices(devices_); 193 } 194 195 private: 196 // content::MediaStreamUI interface. 197 virtual gfx::NativeViewId OnStarted(const base::Closure& close_callback) 198 OVERRIDE { 199 DCHECK(!started_); 200 started_ = true; 201 if (device_usage_.get()) 202 device_usage_->AddDevices(devices_); 203 return 0; 204 } 205 206 base::WeakPtr<WebContentsDeviceUsage> device_usage_; 207 content::MediaStreamDevices devices_; 208 bool started_; 209 210 DISALLOW_COPY_AND_ASSIGN(UIDelegate); 211 }; 212 213 214 scoped_ptr<content::MediaStreamUI> 215 MediaStreamCaptureIndicator::WebContentsDeviceUsage::RegisterMediaStream( 216 const content::MediaStreamDevices& devices) { 217 return scoped_ptr<content::MediaStreamUI>(new UIDelegate( 218 weak_factory_.GetWeakPtr(), devices)); 219 } 220 221 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::AddDevices( 222 const content::MediaStreamDevices& devices) { 223 for (content::MediaStreamDevices::const_iterator it = devices.begin(); 224 it != devices.end(); ++it) { 225 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE || 226 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) { 227 ++mirroring_ref_count_; 228 } else if (content::IsAudioInputMediaType(it->type)) { 229 ++audio_ref_count_; 230 } else if (content::IsVideoMediaType(it->type)) { 231 ++video_ref_count_; 232 } else { 233 NOTIMPLEMENTED(); 234 } 235 } 236 237 if (web_contents()) 238 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB); 239 240 indicator_->UpdateNotificationUserInterface(); 241 } 242 243 void MediaStreamCaptureIndicator::WebContentsDeviceUsage::RemoveDevices( 244 const content::MediaStreamDevices& devices) { 245 for (content::MediaStreamDevices::const_iterator it = devices.begin(); 246 it != devices.end(); ++it) { 247 if (it->type == content::MEDIA_TAB_AUDIO_CAPTURE || 248 it->type == content::MEDIA_TAB_VIDEO_CAPTURE) { 249 --mirroring_ref_count_; 250 } else if (content::IsAudioInputMediaType(it->type)) { 251 --audio_ref_count_; 252 } else if (content::IsVideoMediaType(it->type)) { 253 --video_ref_count_; 254 } else { 255 NOTIMPLEMENTED(); 256 } 257 } 258 259 DCHECK_GE(audio_ref_count_, 0); 260 DCHECK_GE(video_ref_count_, 0); 261 DCHECK_GE(mirroring_ref_count_, 0); 262 263 web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB); 264 indicator_->UpdateNotificationUserInterface(); 265 } 266 267 MediaStreamCaptureIndicator::MediaStreamCaptureIndicator() 268 : status_icon_(NULL), 269 mic_image_(NULL), 270 camera_image_(NULL) { 271 } 272 273 MediaStreamCaptureIndicator::~MediaStreamCaptureIndicator() { 274 // The user is responsible for cleaning up by reporting the closure of any 275 // opened devices. However, there exists a race condition at shutdown: The UI 276 // thread may be stopped before CaptureDevicesClosed() posts the task to 277 // invoke DoDevicesClosedOnUIThread(). In this case, usage_map_ won't be 278 // empty like it should. 279 DCHECK(usage_map_.empty() || 280 !BrowserThread::IsMessageLoopValid(BrowserThread::UI)); 281 282 // Free any WebContentsDeviceUsage objects left over. 283 for (UsageMap::const_iterator it = usage_map_.begin(); it != usage_map_.end(); 284 ++it) { 285 delete it->second; 286 } 287 } 288 289 scoped_ptr<content::MediaStreamUI> 290 MediaStreamCaptureIndicator::RegisterMediaStream( 291 content::WebContents* web_contents, 292 const content::MediaStreamDevices& devices) { 293 WebContentsDeviceUsage*& usage = usage_map_[web_contents]; 294 if (!usage) 295 usage = new WebContentsDeviceUsage(this, web_contents); 296 return usage->RegisterMediaStream(devices); 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 base::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<StatusIconMenuModel> menu(new StatusIconMenuModel(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 if (!usage.IsCapturingAudio() && !usage.IsCapturingVideo()) 424 continue; 425 426 WebContents* const web_contents = iter->first; 427 428 // The audio/video icon is shown only for non-whitelisted extensions or on 429 // Android. For regular tabs on desktop, we show an indicator in the tab 430 // icon. 431 #if defined(ENABLE_EXTENSIONS) 432 const extensions::Extension* extension = GetExtension(web_contents); 433 if (!extension || IsWhitelistedExtension(extension)) 434 continue; 435 #endif 436 437 audio = audio || usage.IsCapturingAudio(); 438 video = video || usage.IsCapturingVideo(); 439 440 command_targets_.push_back(web_contents); 441 menu->AddItem(command_id, GetTitle(web_contents)); 442 443 // If the menu item is not a label, enable it. 444 menu->SetCommandIdEnabled(command_id, 445 command_id != IDC_MinimumLabelValue); 446 447 // If reaching the maximum number, no more item will be added to the menu. 448 if (command_id == IDC_MEDIA_CONTEXT_MEDIA_STREAM_CAPTURE_LIST_LAST) 449 break; 450 ++command_id; 451 } 452 453 if (command_targets_.empty()) { 454 MaybeDestroyStatusTrayIcon(); 455 return; 456 } 457 458 // The icon will take the ownership of the passed context menu. 459 MaybeCreateStatusTrayIcon(audio, video); 460 if (status_icon_) { 461 status_icon_->SetContextMenu(menu.Pass()); 462 } 463 } 464 465 void MediaStreamCaptureIndicator::GetStatusTrayIconInfo( 466 bool audio, 467 bool video, 468 gfx::ImageSkia* image, 469 base::string16* tool_tip) { 470 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 471 DCHECK(audio || video); 472 473 int message_id = 0; 474 if (audio && video) { 475 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_AND_VIDEO; 476 *image = *camera_image_; 477 } else if (audio && !video) { 478 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_AUDIO_ONLY; 479 *image = *mic_image_; 480 } else if (!audio && video) { 481 message_id = IDS_MEDIA_STREAM_STATUS_TRAY_TEXT_VIDEO_ONLY; 482 *image = *camera_image_; 483 } 484 485 *tool_tip = l10n_util::GetStringUTF16(message_id); 486 } 487