OLD | NEW |
| (Empty) |
1 // Copyright 2015 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/common/system/chromeos/cast/tray_cast.h" | |
6 | |
7 #include <map> | |
8 #include <string> | |
9 #include <utility> | |
10 #include <vector> | |
11 | |
12 #include "ash/common/cast_config_controller.h" | |
13 #include "ash/common/material_design/material_design_controller.h" | |
14 #include "ash/common/session/session_state_delegate.h" | |
15 #include "ash/common/shelf/wm_shelf_util.h" | |
16 #include "ash/common/system/chromeos/screen_security/screen_tray_item.h" | |
17 #include "ash/common/system/tray/fixed_sized_image_view.h" | |
18 #include "ash/common/system/tray/hover_highlight_view.h" | |
19 #include "ash/common/system/tray/system_tray.h" | |
20 #include "ash/common/system/tray/system_tray_delegate.h" | |
21 #include "ash/common/system/tray/throbber_view.h" | |
22 #include "ash/common/system/tray/tray_constants.h" | |
23 #include "ash/common/system/tray/tray_details_view.h" | |
24 #include "ash/common/system/tray/tray_item_more.h" | |
25 #include "ash/common/system/tray/tray_item_view.h" | |
26 #include "ash/common/system/tray/tray_utils.h" | |
27 #include "ash/common/wm_shell.h" | |
28 #include "ash/public/cpp/shelf_types.h" | |
29 #include "ash/public/interfaces/cast_config.mojom.h" | |
30 #include "ash/resources/grit/ash_resources.h" | |
31 #include "ash/resources/vector_icons/vector_icons.h" | |
32 #include "ash/strings/grit/ash_strings.h" | |
33 #include "base/bind.h" | |
34 #include "base/strings/utf_string_conversions.h" | |
35 #include "ui/base/l10n/l10n_util.h" | |
36 #include "ui/base/resource/resource_bundle.h" | |
37 #include "ui/gfx/image/image.h" | |
38 #include "ui/gfx/paint_vector_icon.h" | |
39 #include "ui/gfx/text_elider.h" | |
40 #include "ui/views/background.h" | |
41 #include "ui/views/border.h" | |
42 #include "ui/views/controls/button/button.h" | |
43 #include "ui/views/controls/image_view.h" | |
44 #include "ui/views/controls/label.h" | |
45 #include "ui/views/controls/scroll_view.h" | |
46 #include "ui/views/layout/box_layout.h" | |
47 #include "ui/views/layout/fill_layout.h" | |
48 | |
49 namespace ash { | |
50 | |
51 namespace { | |
52 | |
53 const size_t kMaximumStatusStringLength = 100; | |
54 | |
55 // Helper method to elide the given string to the maximum length. If a string is | |
56 // contains user-input and is displayed, we should elide it. | |
57 // TODO(jdufault): This does not properly trim unicode characters. We should | |
58 // implement this properly by using views::Label::SetElideBehavior(...). See | |
59 // crbug.com/532496. | |
60 base::string16 ElideString(const base::string16& text) { | |
61 base::string16 elided; | |
62 gfx::ElideString(text, kMaximumStatusStringLength, &elided); | |
63 return elided; | |
64 } | |
65 | |
66 // Returns a vectorized version of the Cast icon. The icon's interior region is | |
67 // filled in if |is_casting| is true. | |
68 gfx::ImageSkia GetCastIconForSystemMenu(bool is_casting) { | |
69 return gfx::CreateVectorIcon( | |
70 kSystemMenuCastIcon, is_casting ? kMenuIconColor : SK_ColorTRANSPARENT); | |
71 } | |
72 | |
73 } // namespace | |
74 | |
75 namespace tray { | |
76 | |
77 // This view is displayed in the system tray when the cast extension is active. | |
78 // It asks the user if they want to cast the desktop. If they click on the | |
79 // chevron, then a detail view will replace this view where the user will | |
80 // actually pick the cast receiver. | |
81 class CastSelectDefaultView : public TrayItemMore { | |
82 public: | |
83 explicit CastSelectDefaultView(SystemTrayItem* owner); | |
84 ~CastSelectDefaultView() override; | |
85 | |
86 private: | |
87 DISALLOW_COPY_AND_ASSIGN(CastSelectDefaultView); | |
88 }; | |
89 | |
90 CastSelectDefaultView::CastSelectDefaultView(SystemTrayItem* owner) | |
91 : TrayItemMore(owner) { | |
92 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
93 | |
94 // Update the image and label. | |
95 if (MaterialDesignController::IsSystemTrayMenuMaterial()) | |
96 SetImage(GetCastIconForSystemMenu(false)); | |
97 else | |
98 SetImage(*rb.GetImageNamed(IDR_AURA_UBER_TRAY_CAST).ToImageSkia()); | |
99 | |
100 base::string16 label = | |
101 rb.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAST_DESKTOP); | |
102 SetLabel(label); | |
103 SetAccessibleName(label); | |
104 } | |
105 | |
106 CastSelectDefaultView::~CastSelectDefaultView() {} | |
107 | |
108 // This view is displayed when the screen is actively being casted; it allows | |
109 // the user to easily stop casting. It fully replaces the | |
110 // |CastSelectDefaultView| view inside of the |CastDuplexView|. | |
111 class CastCastView : public ScreenStatusView { | |
112 public: | |
113 CastCastView(); | |
114 ~CastCastView() override; | |
115 | |
116 void StopCasting(); | |
117 | |
118 const std::string& displayed_route_id() const { return displayed_route_->id; } | |
119 | |
120 // Updates the label for the stop view to include information about the | |
121 // current device that is being casted. | |
122 void UpdateLabel(const std::vector<mojom::SinkAndRoutePtr>& sinks_routes); | |
123 | |
124 private: | |
125 // Overridden from views::ButtonListener. | |
126 void ButtonPressed(views::Button* sender, const ui::Event& event) override; | |
127 | |
128 // The cast activity id that we are displaying. If the user stops a cast, we | |
129 // send this value to the config delegate so that we stop the right cast. | |
130 mojom::CastRoutePtr displayed_route_; | |
131 | |
132 DISALLOW_COPY_AND_ASSIGN(CastCastView); | |
133 }; | |
134 | |
135 CastCastView::CastCastView() | |
136 : ScreenStatusView( | |
137 nullptr, | |
138 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_CAST_UNKNOWN), | |
139 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_STOP)) { | |
140 if (MaterialDesignController::IsSystemTrayMenuMaterial()) { | |
141 icon()->SetImage(GetCastIconForSystemMenu(true)); | |
142 } else { | |
143 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); | |
144 icon()->SetImage( | |
145 bundle.GetImageNamed(IDR_AURA_UBER_TRAY_CAST_ENABLED).ToImageSkia()); | |
146 } | |
147 } | |
148 | |
149 CastCastView::~CastCastView() {} | |
150 | |
151 void CastCastView::StopCasting() { | |
152 WmShell::Get()->cast_config()->StopCasting(displayed_route_.Clone()); | |
153 WmShell::Get()->RecordUserMetricsAction(UMA_STATUS_AREA_CAST_STOP_CAST); | |
154 } | |
155 | |
156 void CastCastView::UpdateLabel( | |
157 const std::vector<mojom::SinkAndRoutePtr>& sinks_routes) { | |
158 for (auto& i : sinks_routes) { | |
159 const mojom::CastSinkPtr& sink = i->sink; | |
160 const mojom::CastRoutePtr& route = i->route; | |
161 | |
162 // We only want to display casts that came from this machine, since on a | |
163 // busy network many other people could be casting. | |
164 if (!route->id.empty() && route->is_local_source) { | |
165 displayed_route_ = route.Clone(); | |
166 | |
167 // We want to display different labels inside of the title depending on | |
168 // what we are actually casting - either the desktop, a tab, or a fallback | |
169 // that catches everything else (ie, an extension tab). | |
170 switch (route->content_source) { | |
171 case ash::mojom::ContentSource::UNKNOWN: | |
172 label()->SetText( | |
173 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_CAST_UNKNOWN)); | |
174 stop_button()->SetAccessibleName(l10n_util::GetStringUTF16( | |
175 IDS_ASH_STATUS_TRAY_CAST_CAST_UNKNOWN_ACCESSIBILITY_STOP)); | |
176 break; | |
177 case ash::mojom::ContentSource::TAB: | |
178 label()->SetText(ElideString(l10n_util::GetStringFUTF16( | |
179 IDS_ASH_STATUS_TRAY_CAST_CAST_TAB, | |
180 base::UTF8ToUTF16(route->title), base::UTF8ToUTF16(sink->name)))); | |
181 stop_button()->SetAccessibleName( | |
182 ElideString(l10n_util::GetStringFUTF16( | |
183 IDS_ASH_STATUS_TRAY_CAST_CAST_TAB_ACCESSIBILITY_STOP, | |
184 base::UTF8ToUTF16(route->title), | |
185 base::UTF8ToUTF16(sink->name)))); | |
186 break; | |
187 case ash::mojom::ContentSource::DESKTOP: | |
188 label()->SetText(ElideString( | |
189 l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_CAST_CAST_DESKTOP, | |
190 base::UTF8ToUTF16(sink->name)))); | |
191 stop_button()->SetAccessibleName( | |
192 ElideString(l10n_util::GetStringFUTF16( | |
193 IDS_ASH_STATUS_TRAY_CAST_CAST_DESKTOP_ACCESSIBILITY_STOP, | |
194 base::UTF8ToUTF16(sink->name)))); | |
195 break; | |
196 } | |
197 | |
198 PreferredSizeChanged(); | |
199 Layout(); | |
200 // Only need to update labels once. | |
201 break; | |
202 } | |
203 } | |
204 } | |
205 | |
206 void CastCastView::ButtonPressed(views::Button* sender, | |
207 const ui::Event& event) { | |
208 StopCasting(); | |
209 } | |
210 | |
211 // This view by itself does very little. It acts as a front-end for managing | |
212 // which of the two child views (|CastSelectDefaultView| and |CastCastView|) | |
213 // is active. | |
214 class CastDuplexView : public views::View { | |
215 public: | |
216 CastDuplexView(SystemTrayItem* owner, | |
217 bool enabled, | |
218 const std::vector<mojom::SinkAndRoutePtr>& sinks_routes); | |
219 ~CastDuplexView() override; | |
220 | |
221 // Activate either the casting or select view. | |
222 void ActivateCastView(); | |
223 void ActivateSelectView(); | |
224 | |
225 CastSelectDefaultView* select_view() { return select_view_; } | |
226 CastCastView* cast_view() { return cast_view_; } | |
227 | |
228 private: | |
229 // Overridden from views::View. | |
230 void ChildPreferredSizeChanged(views::View* child) override; | |
231 void Layout() override; | |
232 | |
233 // Only one of |select_view_| or |cast_view_| will be displayed at any given | |
234 // time. This will return the view is being displayed. | |
235 views::View* ActiveChildView(); | |
236 | |
237 CastSelectDefaultView* select_view_; | |
238 CastCastView* cast_view_; | |
239 | |
240 DISALLOW_COPY_AND_ASSIGN(CastDuplexView); | |
241 }; | |
242 | |
243 CastDuplexView::CastDuplexView( | |
244 SystemTrayItem* owner, | |
245 bool enabled, | |
246 const std::vector<mojom::SinkAndRoutePtr>& sinks_routes) { | |
247 select_view_ = new CastSelectDefaultView(owner); | |
248 select_view_->SetEnabled(enabled); | |
249 cast_view_ = new CastCastView(); | |
250 cast_view_->UpdateLabel(sinks_routes); | |
251 SetLayoutManager(new views::FillLayout()); | |
252 | |
253 ActivateSelectView(); | |
254 } | |
255 | |
256 CastDuplexView::~CastDuplexView() { | |
257 RemoveChildView(ActiveChildView()); | |
258 delete select_view_; | |
259 delete cast_view_; | |
260 } | |
261 | |
262 void CastDuplexView::ActivateCastView() { | |
263 if (ActiveChildView() == cast_view_) | |
264 return; | |
265 | |
266 RemoveChildView(select_view_); | |
267 AddChildView(cast_view_); | |
268 InvalidateLayout(); | |
269 } | |
270 | |
271 void CastDuplexView::ActivateSelectView() { | |
272 if (ActiveChildView() == select_view_) | |
273 return; | |
274 | |
275 RemoveChildView(cast_view_); | |
276 AddChildView(select_view_); | |
277 InvalidateLayout(); | |
278 } | |
279 | |
280 void CastDuplexView::ChildPreferredSizeChanged(views::View* child) { | |
281 PreferredSizeChanged(); | |
282 } | |
283 | |
284 void CastDuplexView::Layout() { | |
285 views::View::Layout(); | |
286 | |
287 select_view_->SetBoundsRect(GetContentsBounds()); | |
288 cast_view_->SetBoundsRect(GetContentsBounds()); | |
289 } | |
290 | |
291 views::View* CastDuplexView::ActiveChildView() { | |
292 if (cast_view_->parent() == this) | |
293 return cast_view_; | |
294 if (select_view_->parent() == this) | |
295 return select_view_; | |
296 return nullptr; | |
297 } | |
298 | |
299 // Exposes an icon in the tray. |TrayCast| manages the visiblity of this. | |
300 class CastTrayView : public TrayItemView { | |
301 public: | |
302 explicit CastTrayView(SystemTrayItem* tray_item); | |
303 ~CastTrayView() override; | |
304 | |
305 private: | |
306 DISALLOW_COPY_AND_ASSIGN(CastTrayView); | |
307 }; | |
308 | |
309 CastTrayView::CastTrayView(SystemTrayItem* tray_item) | |
310 : TrayItemView(tray_item) { | |
311 CreateImageView(); | |
312 if (MaterialDesignController::UseMaterialDesignSystemIcons()) { | |
313 image_view()->SetImage( | |
314 gfx::CreateVectorIcon(kSystemTrayCastIcon, kTrayIconColor)); | |
315 } else { | |
316 image_view()->SetImage(ui::ResourceBundle::GetSharedInstance() | |
317 .GetImageNamed(IDR_AURA_UBER_TRAY_SCREENSHARE) | |
318 .ToImageSkia()); | |
319 } | |
320 } | |
321 | |
322 CastTrayView::~CastTrayView() {} | |
323 | |
324 // This view displays a list of cast receivers that can be clicked on and casted | |
325 // to. It is activated by clicking on the chevron inside of | |
326 // |CastSelectDefaultView|. | |
327 class CastDetailedView : public TrayDetailsView { | |
328 public: | |
329 CastDetailedView(SystemTrayItem* owner, | |
330 const std::vector<mojom::SinkAndRoutePtr>& sinks_and_routes); | |
331 ~CastDetailedView() override; | |
332 | |
333 // Makes the detail view think the view associated with the given receiver_id | |
334 // was clicked. This will start a cast. | |
335 void SimulateViewClickedForTest(const std::string& receiver_id); | |
336 | |
337 // Updates the list of available receivers. | |
338 void UpdateReceiverList( | |
339 const std::vector<mojom::SinkAndRoutePtr>& sinks_routes); | |
340 | |
341 private: | |
342 void CreateItems(); | |
343 | |
344 void UpdateReceiverListFromCachedData(); | |
345 views::View* AddToReceiverList(const mojom::SinkAndRoutePtr& sink_route); | |
346 | |
347 // TrayDetailsView: | |
348 void HandleViewClicked(views::View* view) override; | |
349 | |
350 // A mapping from the receiver id to the receiver/activity data. | |
351 std::map<std::string, ash::mojom::SinkAndRoutePtr> sinks_and_routes_; | |
352 // A mapping from the view pointer to the associated activity id. | |
353 std::map<views::View*, ash::mojom::CastSinkPtr> view_to_sink_map_; | |
354 | |
355 DISALLOW_COPY_AND_ASSIGN(CastDetailedView); | |
356 }; | |
357 | |
358 CastDetailedView::CastDetailedView( | |
359 SystemTrayItem* owner, | |
360 const std::vector<mojom::SinkAndRoutePtr>& sinks_routes) | |
361 : TrayDetailsView(owner) { | |
362 CreateItems(); | |
363 UpdateReceiverList(sinks_routes); | |
364 } | |
365 | |
366 CastDetailedView::~CastDetailedView() {} | |
367 | |
368 void CastDetailedView::SimulateViewClickedForTest( | |
369 const std::string& receiver_id) { | |
370 for (const auto& it : view_to_sink_map_) { | |
371 if (it.second->id == receiver_id) { | |
372 HandleViewClicked(it.first); | |
373 break; | |
374 } | |
375 } | |
376 } | |
377 | |
378 void CastDetailedView::CreateItems() { | |
379 CreateScrollableList(); | |
380 CreateTitleRow(IDS_ASH_STATUS_TRAY_CAST); | |
381 } | |
382 | |
383 void CastDetailedView::UpdateReceiverList( | |
384 const std::vector<mojom::SinkAndRoutePtr>& sinks_routes) { | |
385 // Add/update existing. | |
386 for (const auto& it : sinks_routes) | |
387 sinks_and_routes_[it->sink->id] = it->Clone(); | |
388 | |
389 // Remove non-existent sinks. Removing an element invalidates all existing | |
390 // iterators. | |
391 auto i = sinks_and_routes_.begin(); | |
392 while (i != sinks_and_routes_.end()) { | |
393 bool has_receiver = false; | |
394 for (auto& receiver : sinks_routes) { | |
395 if (i->first == receiver->sink->id) | |
396 has_receiver = true; | |
397 } | |
398 | |
399 if (has_receiver) | |
400 ++i; | |
401 else | |
402 i = sinks_and_routes_.erase(i); | |
403 } | |
404 | |
405 // Update UI. | |
406 UpdateReceiverListFromCachedData(); | |
407 Layout(); | |
408 } | |
409 | |
410 void CastDetailedView::UpdateReceiverListFromCachedData() { | |
411 // Remove all of the existing views. | |
412 view_to_sink_map_.clear(); | |
413 scroll_content()->RemoveAllChildViews(true); | |
414 | |
415 // Add a view for each receiver. | |
416 for (auto& it : sinks_and_routes_) { | |
417 const ash::mojom::SinkAndRoutePtr& sink_route = it.second; | |
418 views::View* container = AddToReceiverList(sink_route); | |
419 view_to_sink_map_[container] = sink_route->sink.Clone(); | |
420 } | |
421 | |
422 scroll_content()->SizeToPreferredSize(); | |
423 scroller()->Layout(); | |
424 } | |
425 | |
426 views::View* CastDetailedView::AddToReceiverList( | |
427 const ash::mojom::SinkAndRoutePtr& sink_route) { | |
428 const gfx::ImageSkia image = | |
429 MaterialDesignController::IsSystemTrayMenuMaterial() | |
430 ? gfx::CreateVectorIcon(kSystemMenuCastDeviceIcon, kMenuIconColor) | |
431 : *ui::ResourceBundle::GetSharedInstance() | |
432 .GetImageNamed(IDR_AURA_UBER_TRAY_CAST_DEVICE_ICON) | |
433 .ToImageSkia(); | |
434 | |
435 HoverHighlightView* container = new HoverHighlightView(this); | |
436 container->AddIconAndLabelCustomSize( | |
437 image, base::UTF8ToUTF16(sink_route->sink->name), false, | |
438 kTrayPopupDetailsIconWidth, kTrayPopupPaddingHorizontal, | |
439 kTrayPopupPaddingBetweenItems); | |
440 | |
441 scroll_content()->AddChildView(container); | |
442 return container; | |
443 } | |
444 | |
445 void CastDetailedView::HandleViewClicked(views::View* view) { | |
446 // Find the receiver we are going to cast to. | |
447 auto it = view_to_sink_map_.find(view); | |
448 if (it != view_to_sink_map_.end()) { | |
449 WmShell::Get()->cast_config()->CastToSink(it->second.Clone()); | |
450 WmShell::Get()->RecordUserMetricsAction( | |
451 UMA_STATUS_AREA_DETAILED_CAST_VIEW_LAUNCH_CAST); | |
452 } | |
453 } | |
454 | |
455 } // namespace tray | |
456 | |
457 TrayCast::TrayCast(SystemTray* system_tray) | |
458 : SystemTrayItem(system_tray, UMA_CAST) { | |
459 WmShell::Get()->AddShellObserver(this); | |
460 WmShell::Get()->cast_config()->AddObserver(this); | |
461 WmShell::Get()->cast_config()->RequestDeviceRefresh(); | |
462 } | |
463 | |
464 TrayCast::~TrayCast() { | |
465 WmShell::Get()->cast_config()->RemoveObserver(this); | |
466 WmShell::Get()->RemoveShellObserver(this); | |
467 } | |
468 | |
469 void TrayCast::StartCastForTest(const std::string& receiver_id) { | |
470 if (detailed_ != nullptr) | |
471 detailed_->SimulateViewClickedForTest(receiver_id); | |
472 } | |
473 | |
474 void TrayCast::StopCastForTest() { | |
475 default_->cast_view()->StopCasting(); | |
476 } | |
477 | |
478 const std::string& TrayCast::GetDisplayedCastId() { | |
479 return default_->cast_view()->displayed_route_id(); | |
480 } | |
481 | |
482 const views::View* TrayCast::GetDefaultView() const { | |
483 return default_; | |
484 } | |
485 | |
486 views::View* TrayCast::CreateTrayView(LoginStatus status) { | |
487 CHECK(tray_ == nullptr); | |
488 tray_ = new tray::CastTrayView(this); | |
489 tray_->SetVisible(HasActiveRoute()); | |
490 return tray_; | |
491 } | |
492 | |
493 views::View* TrayCast::CreateDefaultView(LoginStatus status) { | |
494 CHECK(default_ == nullptr); | |
495 | |
496 default_ = new tray::CastDuplexView(this, status != LoginStatus::LOCKED, | |
497 sinks_and_routes_); | |
498 default_->set_id(TRAY_VIEW); | |
499 default_->select_view()->set_id(SELECT_VIEW); | |
500 default_->cast_view()->set_id(CAST_VIEW); | |
501 | |
502 UpdatePrimaryView(); | |
503 return default_; | |
504 } | |
505 | |
506 views::View* TrayCast::CreateDetailedView(LoginStatus status) { | |
507 WmShell::Get()->RecordUserMetricsAction(UMA_STATUS_AREA_DETAILED_CAST_VIEW); | |
508 CHECK(detailed_ == nullptr); | |
509 detailed_ = new tray::CastDetailedView(this, sinks_and_routes_); | |
510 return detailed_; | |
511 } | |
512 | |
513 void TrayCast::DestroyTrayView() { | |
514 tray_ = nullptr; | |
515 } | |
516 | |
517 void TrayCast::DestroyDefaultView() { | |
518 default_ = nullptr; | |
519 } | |
520 | |
521 void TrayCast::DestroyDetailedView() { | |
522 detailed_ = nullptr; | |
523 } | |
524 | |
525 void TrayCast::OnDevicesUpdated(std::vector<mojom::SinkAndRoutePtr> devices) { | |
526 sinks_and_routes_ = std::move(devices); | |
527 UpdatePrimaryView(); | |
528 | |
529 if (default_) { | |
530 bool has_receivers = !sinks_and_routes_.empty(); | |
531 default_->SetVisible(has_receivers); | |
532 default_->cast_view()->UpdateLabel(sinks_and_routes_); | |
533 } | |
534 if (detailed_) | |
535 detailed_->UpdateReceiverList(sinks_and_routes_); | |
536 } | |
537 | |
538 void TrayCast::UpdatePrimaryView() { | |
539 if (WmShell::Get()->cast_config()->Connected() && | |
540 !sinks_and_routes_.empty()) { | |
541 if (default_) { | |
542 if (HasActiveRoute()) | |
543 default_->ActivateCastView(); | |
544 else | |
545 default_->ActivateSelectView(); | |
546 } | |
547 | |
548 if (tray_) | |
549 tray_->SetVisible(is_mirror_casting_); | |
550 } else { | |
551 if (default_) | |
552 default_->SetVisible(false); | |
553 if (tray_) | |
554 tray_->SetVisible(false); | |
555 } | |
556 } | |
557 | |
558 bool TrayCast::HasActiveRoute() { | |
559 for (const auto& sr : sinks_and_routes_) { | |
560 if (!sr->route->title.empty() && sr->route->is_local_source) | |
561 return true; | |
562 } | |
563 | |
564 return false; | |
565 } | |
566 | |
567 void TrayCast::OnCastingSessionStartedOrStopped(bool started) { | |
568 is_mirror_casting_ = started; | |
569 UpdatePrimaryView(); | |
570 } | |
571 | |
572 } // namespace ash | |
OLD | NEW |