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