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