summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/resources
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2018-05-03 13:42:47 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2018-05-15 10:27:51 +0000
commit8c5c43c7b138c9b4b0bf56d946e61d3bbc111bec (patch)
treed29d987c4d7b173cf853279b79a51598f104b403 /chromium/chrome/browser/resources
parent830c9e163d31a9180fadca926b3e1d7dfffb5021 (diff)
BASELINE: Update Chromium to 66.0.3359.156
Change-Id: I0c9831ad39911a086b6377b16f995ad75a51e441 Reviewed-by: Michal Klocek <michal.klocek@qt.io>
Diffstat (limited to 'chromium/chrome/browser/resources')
-rw-r--r--chromium/chrome/browser/resources/BUILD.gn12
-rw-r--r--chromium/chrome/browser/resources/PRESUBMIT.py2
-rw-r--r--chromium/chrome/browser/resources/about_voicesearch.html38
-rw-r--r--chromium/chrome/browser/resources/about_voicesearch.js37
-rw-r--r--chromium/chrome/browser/resources/app_list/OWNERS2
-rw-r--r--chromium/chrome/browser/resources/app_list/start_page.css32
-rw-r--r--chromium/chrome/browser/resources/app_list/start_page.html23
-rw-r--r--chromium/chrome/browser/resources/app_list/start_page.js120
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js2
-rw-r--r--chromium/chrome/browser/resources/chromeos/chromevox/BUILD.gn1
-rw-r--r--chromium/chrome/browser/resources/chromeos/chromevox/strings/chromevox_strings.grd21
-rw-r--r--chromium/chrome/browser/resources/chromeos/genius_app/manifest.json1
-rw-r--r--chromium/chrome/browser/resources/chromeos/login/BUILD.gn30
-rw-r--r--chromium/chrome/browser/resources/chromeos/login/compiled_resources2.gyp5
-rw-r--r--chromium/chrome/browser/resources/chromeos/quick_unlock/compiled_resources2.gyp25
-rw-r--r--chromium/chrome/browser/resources/chromeos/select_to_speak/BUILD.gn6
-rw-r--r--chromium/chrome/browser/resources/chromeos/select_to_speak/compiled_resources2.gyp83
-rw-r--r--chromium/chrome/browser/resources/chromeos/wallpaper_manager/manifest.json1
-rw-r--r--chromium/chrome/browser/resources/components.js36
-rw-r--r--chromium/chrome/browser/resources/cryptotoken/enroller.js57
-rw-r--r--chromium/chrome/browser/resources/cryptotoken/gnubby-u2f.js4
-rw-r--r--chromium/chrome/browser/resources/cryptotoken/manifest.json2
-rw-r--r--chromium/chrome/browser/resources/download_internals/download_internals.html6
-rw-r--r--chromium/chrome/browser/resources/download_internals/download_internals.js4
-rw-r--r--chromium/chrome/browser/resources/download_internals/download_internals_browser_proxy.js11
-rw-r--r--chromium/chrome/browser/resources/extensions/extensions.js5
-rw-r--r--chromium/chrome/browser/resources/feedback/js/feedback.js7
-rw-r--r--chromium/chrome/browser/resources/feedback/js/take_screenshot.js3
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/saml_handler.js2
-rw-r--r--chromium/chrome/browser/resources/identity_internals.html6
-rw-r--r--chromium/chrome/browser/resources/input_ime/ime_window_close.pngbin270 -> 154 bytes
-rw-r--r--chromium/chrome/browser/resources/input_ime/ime_window_close_click.pngbin299 -> 156 bytes
-rw-r--r--chromium/chrome/browser/resources/input_ime/ime_window_close_hover.pngbin299 -> 156 bytes
-rw-r--r--chromium/chrome/browser/resources/inspect/inspect.css8
-rw-r--r--chromium/chrome/browser/resources/inspect/inspect.js37
-rw-r--r--chromium/chrome/browser/resources/local_discovery/local_discovery.html7
-rw-r--r--chromium/chrome/browser/resources/local_discovery/local_discovery.js10
-rw-r--r--chromium/chrome/browser/resources/local_ntp/local_ntp.css3
-rw-r--r--chromium/chrome/browser/resources/local_ntp/most_visited_single.js45
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/app.html1
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/bookmarks.html5
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/command_manager.html9
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/command_manager.js49
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/edit_dialog.html7
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/folder_node.html6
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/folder_node.js2
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/list.html2
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/list.js8
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/toast_manager.html7
-rw-r--r--chromium/chrome/browser/resources/md_bookmarks/toolbar.html2
-rw-r--r--chromium/chrome/browser/resources/md_downloads/1x/incognito_marker.pngbin334 -> 174 bytes
-rw-r--r--chromium/chrome/browser/resources/md_downloads/1x/no_downloads.pngbin5921 -> 2914 bytes
-rw-r--r--chromium/chrome/browser/resources/md_downloads/2x/incognito_marker.pngbin828 -> 370 bytes
-rw-r--r--chromium/chrome/browser/resources/md_downloads/2x/no_downloads.pngbin12405 -> 5440 bytes
-rw-r--r--chromium/chrome/browser/resources/md_downloads/compiled_resources2.gyp1
-rw-r--r--chromium/chrome/browser/resources/md_downloads/downloads.html3
-rw-r--r--chromium/chrome/browser/resources/md_downloads/item.html23
-rw-r--r--chromium/chrome/browser/resources/md_downloads/manager.html3
-rw-r--r--chromium/chrome/browser/resources/md_downloads/toolbar.html10
-rw-r--r--chromium/chrome/browser/resources/md_extensions/code_section.html25
-rw-r--r--chromium/chrome/browser/resources/md_extensions/code_section.js70
-rw-r--r--chromium/chrome/browser/resources/md_extensions/compiled_resources2.gyp2
-rw-r--r--chromium/chrome/browser/resources/md_extensions/detail_view.html58
-rw-r--r--chromium/chrome/browser/resources/md_extensions/detail_view.js10
-rw-r--r--chromium/chrome/browser/resources/md_extensions/error_page.html22
-rw-r--r--chromium/chrome/browser/resources/md_extensions/extensions.html3
-rw-r--r--chromium/chrome/browser/resources/md_extensions/icons.html2
-rw-r--r--chromium/chrome/browser/resources/md_extensions/install_warnings_dialog.html2
-rw-r--r--chromium/chrome/browser/resources/md_extensions/item.html50
-rw-r--r--chromium/chrome/browser/resources/md_extensions/item.js7
-rw-r--r--chromium/chrome/browser/resources/md_extensions/item_list.html4
-rw-r--r--chromium/chrome/browser/resources/md_extensions/item_util.js1
-rw-r--r--chromium/chrome/browser/resources/md_extensions/keyboard_shortcuts.html8
-rw-r--r--chromium/chrome/browser/resources/md_extensions/kiosk_dialog.html14
-rw-r--r--chromium/chrome/browser/resources/md_extensions/load_error.html4
-rw-r--r--chromium/chrome/browser/resources/md_extensions/options_dialog.html3
-rw-r--r--chromium/chrome/browser/resources/md_extensions/options_dialog.js45
-rw-r--r--chromium/chrome/browser/resources/md_extensions/pack_dialog.html8
-rw-r--r--chromium/chrome/browser/resources/md_extensions/pack_dialog_alert.html4
-rw-r--r--chromium/chrome/browser/resources/md_extensions/shortcut_input.html2
-rw-r--r--chromium/chrome/browser/resources/md_extensions/sidebar.html6
-rw-r--r--chromium/chrome/browser/resources/md_extensions/toolbar.html33
-rw-r--r--chromium/chrome/browser/resources/md_extensions/toolbar.js30
-rw-r--r--chromium/chrome/browser/resources/md_history/app.html2
-rw-r--r--chromium/chrome/browser/resources/md_history/app.js12
-rw-r--r--chromium/chrome/browser/resources/md_history/history.html11
-rw-r--r--chromium/chrome/browser/resources/md_history/history_item.html2
-rw-r--r--chromium/chrome/browser/resources/md_history/history_item.js6
-rw-r--r--chromium/chrome/browser/resources/md_history/history_list.html17
-rw-r--r--chromium/chrome/browser/resources/md_history/history_list.js23
-rw-r--r--chromium/chrome/browser/resources/md_history/side_bar.html2
-rw-r--r--chromium/chrome/browser/resources/md_history/synced_device_card.html4
-rw-r--r--chromium/chrome/browser/resources/md_history/synced_device_card.js3
-rw-r--r--chromium/chrome/browser/resources/md_history/synced_device_manager.html13
-rw-r--r--chromium/chrome/browser/resources/md_user_manager/shared_styles.html2
-rw-r--r--chromium/chrome/browser/resources/md_user_manager/user_manager.html2
-rw-r--r--chromium/chrome/browser/resources/md_user_manager/user_manager_pages.html1
-rw-r--r--chromium/chrome/browser/resources/media/media_engagement.html21
-rw-r--r--chromium/chrome/browser/resources/media/media_engagement.js36
-rw-r--r--chromium/chrome/browser/resources/media/mei_preload/preloaded_data.pbbin14 -> 7857 bytes
-rw-r--r--chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.css2
-rw-r--r--chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.html15
-rw-r--r--chromium/chrome/browser/resources/media_router/elements/route_details/route_details.css4
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/BUILD.gn78
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/assemble_extension.py41
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/concat_js_modules.py87
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/manifest.yaml53
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/prelude.js50
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/background.js9
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/config.js26
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/event_listener.js188
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/event_listener_test.js147
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/extension_selector.js47
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/external_message_listener.js80
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/external_message_listener_test.js67
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/externs.js917
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/files.gni145
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/init.js157
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/init_helper.js61
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/init_test.js82
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/issue.js150
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/media_route_controller.js166
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/media_route_controller_test.js70
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/mojo.js415
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/route.js178
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/route_message.js48
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/route_request_error.js89
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink.js79
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink_list.js34
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink_search_criteria.js30
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/internal_message.js62
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/internal_message_listener.js61
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/internal_message_listener_test.js52
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/log_manager.js241
-rwxr-xr-xchromium/chrome/browser/resources/media_router/extension/src/manager/cancellable_promise.js255
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/route_message_sender.js344
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/route_message_sender_test.js173
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/throttling_sender.js89
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/throttling_sender_test.js69
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/presentation_enums.js35
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/provider.js258
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/provider_events.js46
-rwxr-xr-xchromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager.js1280
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager_callbacks.js223
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager_test.js1076
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/route_id.js114
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/route_id_test.js41
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/route_message_port.js74
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/route_message_port_impl.js107
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/manager/sink_availability.js21
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_activity.js150
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_activity_test.js93
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_analytics.js74
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_analytics_test.js111
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_config.js31
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service.js522
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service_loader.js47
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service_name.js21
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_session.js179
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_session_test.js118
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_settings.js400
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_settings_test.js123
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/capture_parameters.js208
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/mirror_media_stream.js340
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/mirror_media_stream_test.js407
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/module.js247
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/module_test.js90
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/mojo_externs.js522
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/persistent_data.js463
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/persistent_data_test.js361
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/presentation.js191
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/presentation_services/cloud_webrtc/webrtc_presentation_session.js192
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/presentation_services/presentation_session.js31
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/common/id_generator.js80
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/common/id_generator_test.js36
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/common/net_utils.js98
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/common/net_utils_test.js61
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/common/retry.js198
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/common/runtime_error_utils.js38
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/common/sink_utils.js187
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/common/xhr_utils.js72
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity.js22
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity_records.js156
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity_records_test.js100
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_analytics.js119
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_analytics_test.js88
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_app_discovery_service.js480
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_app_discovery_service_test.js362
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_client.js325
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_client_test.js284
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider.js474
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider_callbacks.js65
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider_test.js256
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink.js309
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_discovery_service.js334
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_discovery_service_test.js163
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_test.js127
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/presentation_url.js146
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/presentation_url_test.js75
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/providers/dial/sink_app_status.js23
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/analytics.js305
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/analytics_test.js246
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/assertions.js95
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/base64.js46
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/base64_test.js72
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/device_counts.js18
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/device_counts_provider.js23
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/event_analytics.js57
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/event_analytics_test.js27
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/event_target.js69
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/fixed_size_queue.js126
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/fixed_size_queue_test.js74
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/logger.js261
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/logger_test.js166
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/media_source_utils.js127
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/media_source_utils_test.js66
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/mock_clock.js391
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/mock_promise.js602
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/mock_promise_test.js850
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/mojo_utils.js63
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/object_utils.js31
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/object_utils_test.js33
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/platform_utils.js62
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/promise_resolver.js54
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/promise_utils.js36
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/sha1.js239
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/sha1_test.js60
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/string_utils.js24
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/tab_utils.js28
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/throttle.js93
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/throttle_test.js100
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/unit_test_utils.js307
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/xhr_manager.js191
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/utils/xhr_manager_test.js185
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/webrtc/messages.js200
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection.js570
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection_analytics.js58
-rw-r--r--chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection_test.js215
-rw-r--r--chromium/chrome/browser/resources/media_router/media_router_ui_interface.js15
-rw-r--r--chromium/chrome/browser/resources/net_internals/http_cache_view.html4
-rw-r--r--chromium/chrome/browser/resources/ntp4/new_tab.js3
-rw-r--r--chromium/chrome/browser/resources/offline_pages/offline_internals.html8
-rw-r--r--chromium/chrome/browser/resources/offline_pages/offline_internals.js25
-rw-r--r--chromium/chrome/browser/resources/pdf/elements/viewer-bookmark/viewer-bookmark.html4
-rw-r--r--chromium/chrome/browser/resources/pdf/elements/viewer-page-selector/viewer-page-selector.html17
-rw-r--r--chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar/viewer-pdf-toolbar.html8
-rw-r--r--chromium/chrome/browser/resources/pdf/elements/viewer-toolbar-dropdown/viewer-toolbar-dropdown.html2
-rw-r--r--chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar/viewer-zoom-button.html2
-rw-r--r--chromium/chrome/browser/resources/pdf/open_pdf_params_parser.js100
-rw-r--r--chromium/chrome/browser/resources/pdf/pdf.js58
-rw-r--r--chromium/chrome/browser/resources/plugin_metadata/plugins_linux.json6
-rw-r--r--chromium/chrome/browser/resources/plugin_metadata/plugins_mac.json6
-rw-r--r--chromium/chrome/browser/resources/plugin_metadata/plugins_win.json6
-rw-r--r--chromium/chrome/browser/resources/policy.css5
-rw-r--r--chromium/chrome/browser/resources/policy.html4
-rw-r--r--chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.html49
-rw-r--r--chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.js49
-rw-r--r--chromium/chrome/browser/resources/print_preview/OWNERS4
-rw-r--r--chromium/chrome/browser/resources/print_preview/cloud_print_interface.html9
-rw-r--r--chromium/chrome/browser/resources/print_preview/data/cloud_parsers.html5
-rw-r--r--chromium/chrome/browser/resources/print_preview/data/destination.js43
-rw-r--r--chromium/chrome/browser/resources/print_preview/data/destination_store.html3
-rw-r--r--chromium/chrome/browser/resources/print_preview/data/invitation.html4
-rw-r--r--chromium/chrome/browser/resources/print_preview/images/2x/printer.pngbin1935 -> 706 bytes
-rw-r--r--chromium/chrome/browser/resources/print_preview/images/2x/printer_shared.pngbin2603 -> 1529 bytes
-rw-r--r--chromium/chrome/browser/resources/print_preview/images/third_party.pngbin284 -> 0 bytes
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.html13
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.js19
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/advanced_settings_dialog.html47
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/advanced_settings_dialog.js82
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/advanced_settings_item.html68
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/advanced_settings_item.js154
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/app.html40
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/app.js255
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/button_css.html4
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/checkbox_radio_css.html4
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/color_settings.html3
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/color_settings.js4
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/compiled_resources2.gyp80
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/copies_settings.html7
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/copies_settings.js12
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/destination_dialog.html132
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/destination_dialog.js205
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/destination_list.html102
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/destination_list.js138
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/destination_list_item.html136
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/destination_list_item.js58
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/destination_settings.html24
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/destination_settings.js69
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/dpi_settings.html2
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/dpi_settings.js2
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/header.html15
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/header.js134
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/highlight_utils.html3
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/highlight_utils.js60
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/layout_settings.html3
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/layout_settings.js4
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/margins_settings.html3
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/margins_settings.js4
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/media_size_settings.html2
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/media_size_settings.js2
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/model.html1
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/model.js289
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/number_settings_section.html11
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/number_settings_section.js53
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/other_options_settings.html6
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/other_options_settings.js4
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/pages_settings.html6
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/pages_settings.js87
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/preview_area.html45
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/preview_area.js180
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/print_preview_search_box.html41
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/print_preview_search_box.js42
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/scaling_settings.html6
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/scaling_settings.js32
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/search_dialog_css.html48
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/select_css.html4
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/settings_behavior.js2
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/settings_select.html5
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/settings_select.js22
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/state.html5
-rw-r--r--chromium/chrome/browser/resources/print_preview/new/state.js76
-rw-r--r--chromium/chrome/browser/resources/print_preview/preview_generator.js10
-rw-r--r--chromium/chrome/browser/resources/print_preview/previewarea/preview_area.css1
-rw-r--r--chromium/chrome/browser/resources/print_preview/previewarea/preview_area.js42
-rw-r--r--chromium/chrome/browser/resources/print_preview/print_preview.js5
-rw-r--r--chromium/chrome/browser/resources/print_preview/print_preview_new.html11
-rw-r--r--chromium/chrome/browser/resources/print_preview/print_preview_resources.grd72
-rw-r--r--chromium/chrome/browser/resources/print_preview/print_preview_utils.js2
-rw-r--r--chromium/chrome/browser/resources/print_preview/print_preview_utils_unittest.gtestjs11
-rw-r--r--chromium/chrome/browser/resources/print_preview/search/destination_list_item.js22
-rw-r--r--chromium/chrome/browser/resources/print_preview/search/destination_search.css4
-rw-r--r--chromium/chrome/browser/resources/print_preview/search/destination_search.html3
-rw-r--r--chromium/chrome/browser/resources/print_preview/search/destination_search.js2
-rw-r--r--chromium/chrome/browser/resources/print_preview/settings/advanced_settings/advanced_settings_item.js18
-rw-r--r--chromium/chrome/browser/resources/print_preview/settings/destination_settings.js16
-rw-r--r--chromium/chrome/browser/resources/safe_browsing/download_file_types.asciipb4
-rw-r--r--chromium/chrome/browser/resources/settings/PRESUBMIT.py24
-rw-r--r--chromium/chrome/browser/resources/settings/a11y_page/a11y_page.html2
-rw-r--r--chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.html80
-rw-r--r--chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.js33
-rw-r--r--chromium/chrome/browser/resources/settings/about_page/about_page.html25
-rw-r--r--chromium/chrome/browser/resources/settings/about_page/channel_switcher_dialog.html6
-rw-r--r--chromium/chrome/browser/resources/settings/about_page/detailed_build_info.html2
-rw-r--r--chromium/chrome/browser/resources/settings/about_page/update_warning_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/android_apps_page/android_apps_page.html4
-rw-r--r--chromium/chrome/browser/resources/settings/android_apps_page/android_apps_subpage.html6
-rw-r--r--chromium/chrome/browser/resources/settings/android_apps_page/android_settings_element.html2
-rw-r--r--chromium/chrome/browser/resources/settings/appearance_page/appearance_fonts_page.html2
-rw-r--r--chromium/chrome/browser/resources/settings/appearance_page/appearance_page.html12
-rw-r--r--chromium/chrome/browser/resources/settings/basic_page/basic_page.html4
-rw-r--r--chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_device_list_item.html7
-rw-r--r--chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_page.html6
-rw-r--r--chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_subpage.html8
-rw-r--r--chromium/chrome/browser/resources/settings/change_password_page/change_password_page.html2
-rw-r--r--chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.html8
-rw-r--r--chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.js4
-rw-r--r--chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.html22
-rw-r--r--chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.js44
-rw-r--r--chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.html334
-rw-r--r--chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.js156
-rw-r--r--chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.html311
-rw-r--r--chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.js367
-rw-r--r--chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/compiled_resources2.gyp1
-rw-r--r--chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/history_deletion_dialog.html2
-rw-r--r--chromium/chrome/browser/resources/settings/compiled_resources2.gyp2
-rw-r--r--chromium/chrome/browser/resources/settings/controls/controlled_button.html2
-rw-r--r--chromium/chrome/browser/resources/settings/controls/controlled_button.js2
-rw-r--r--chromium/chrome/browser/resources/settings/controls/controlled_radio_button.html23
-rw-r--r--chromium/chrome/browser/resources/settings/controls/controlled_radio_button.js9
-rw-r--r--chromium/chrome/browser/resources/settings/controls/extension_controlled_indicator.html4
-rw-r--r--chromium/chrome/browser/resources/settings/controls/important_site_checkbox.html4
-rw-r--r--chromium/chrome/browser/resources/settings/controls/settings_checkbox.html4
-rw-r--r--chromium/chrome/browser/resources/settings/controls/settings_checkbox.js2
-rw-r--r--chromium/chrome/browser/resources/settings/controls/settings_toggle_button.html5
-rw-r--r--chromium/chrome/browser/resources/settings/date_time_page/date_time_page.html4
-rw-r--r--chromium/chrome/browser/resources/settings/default_browser_page/default_browser_page.html2
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/device_page.html12
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/device_page_browser_proxy.js2
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/display.html6
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/display.js31
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/display_layout.html12
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/display_layout.js11
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/display_overscan_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/drive_cache_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/keyboard.html4
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/pointers.html2
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/storage.html10
-rw-r--r--chromium/chrome/browser/resources/settings/device_page/stylus.html6
-rw-r--r--chromium/chrome/browser/resources/settings/downloads_page/downloads_page.html4
-rw-r--r--chromium/chrome/browser/resources/settings/google_assistant_page/google_assistant_page.html2
-rw-r--r--chromium/chrome/browser/resources/settings/icons.html5
-rw-r--r--chromium/chrome/browser/resources/settings/images/sync_banner.svg10
-rw-r--r--chromium/chrome/browser/resources/settings/incompatible_applications_page/compiled_resources2.gyp33
-rw-r--r--chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_application_item.html25
-rw-r--r--chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_application_item.js103
-rw-r--r--chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_browser_proxy.html2
-rw-r--r--chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_browser_proxy.js122
-rw-r--r--chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_page.html59
-rw-r--r--chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_page.js138
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/internet_config.html12
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/internet_config.js34
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.html18
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.js1
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/internet_known_networks_page.html20
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/internet_page.html10
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/internet_page.js25
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/internet_subpage.html19
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/internet_subpage.js2
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/network_proxy_section.html4
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/network_summary_item.html8
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/network_summary_item.js20
-rw-r--r--chromium/chrome/browser/resources/settings/internet_page/tether_connection_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/languages_page/add_languages_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/languages_page/edit_dictionary_page.html4
-rw-r--r--chromium/chrome/browser/resources/settings/languages_page/languages_page.html47
-rw-r--r--chromium/chrome/browser/resources/settings/languages_page/languages_page.js4
-rw-r--r--chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.html6
-rw-r--r--chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.js24
-rw-r--r--chromium/chrome/browser/resources/settings/on_startup_page/startup_url_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.html7
-rw-r--r--chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.js2
-rw-r--r--chromium/chrome/browser/resources/settings/on_startup_page/startup_urls_page.html8
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/address_edit_dialog.html6
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/autofill_section.html36
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/credit_card_edit_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_edit_dialog.html8
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_list_item.html6
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_and_forms_page.html4
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.html51
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.js199
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.html40
-rw-r--r--chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.js77
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/change_picture.html2
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/change_picture.js8
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/change_picture_browser_proxy.js5
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/compiled_resources2.gyp14
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/easy_unlock_turn_off_dialog.html5
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/fingerprint_list.html6
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/import_data_dialog.html6
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/lock_screen.html10
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/password_prompt_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/people_page.html153
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/people_page.js68
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/pin_keyboard.html1
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/pin_keyboard.js5
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/profile_info_browser_proxy.js11
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.js2
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/setup_pin_dialog.html13
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/sync_account_control.html200
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/sync_account_control.js222
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/sync_browser_proxy.js63
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/sync_page.html8
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/user_list.html2
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/users_add_user_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/users_page.html9
-rw-r--r--chromium/chrome/browser/resources/settings/people_page/users_page.js9
-rw-r--r--chromium/chrome/browser/resources/settings/prefs/pref_util.js2
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.html40
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.js7
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog_util.html27
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/cups_edit_printer_dialog.html8
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/cups_printer_shared_css.html2
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/cups_printers.html15
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/cups_printers.js14
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/cups_printers_list.html6
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/cups_set_manufacturer_model_behavior.js16
-rw-r--r--chromium/chrome/browser/resources/settings/printing_page/printing_page.html6
-rw-r--r--chromium/chrome/browser/resources/settings/privacy_page/privacy_page.html43
-rw-r--r--chromium/chrome/browser/resources/settings/privacy_page/privacy_page.js32
-rw-r--r--chromium/chrome/browser/resources/settings/privacy_page/privacy_page_browser_proxy.js5
-rw-r--r--chromium/chrome/browser/resources/settings/reset_page/powerwash_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/reset_page/reset_page.html39
-rw-r--r--chromium/chrome/browser/resources/settings/reset_page/reset_page.js14
-rw-r--r--chromium/chrome/browser/resources/settings/reset_page/reset_profile_banner.html4
-rw-r--r--chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.js2
-rw-r--r--chromium/chrome/browser/resources/settings/route.js7
-rw-r--r--chromium/chrome/browser/resources/settings/search_engines_page/omnibox_extension_entry.html8
-rw-r--r--chromium/chrome/browser/resources/settings/search_engines_page/search_engine_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/search_engines_page/search_engine_entry.html8
-rw-r--r--chromium/chrome/browser/resources/settings/search_engines_page/search_engines_list.html2
-rw-r--r--chromium/chrome/browser/resources/settings/search_engines_page/search_engines_page.html4
-rw-r--r--chromium/chrome/browser/resources/settings/search_page/search_page.html6
-rw-r--r--chromium/chrome/browser/resources/settings/search_settings.js117
-rw-r--r--chromium/chrome/browser/resources/settings/settings.html3
-rw-r--r--chromium/chrome/browser/resources/settings/settings_main/settings_main.html1
-rw-r--r--chromium/chrome/browser/resources/settings/settings_page/settings_animated_pages.html1
-rw-r--r--chromium/chrome/browser/resources/settings/settings_page/settings_section.html6
-rw-r--r--chromium/chrome/browser/resources/settings/settings_page/settings_subpage.html8
-rw-r--r--chromium/chrome/browser/resources/settings/settings_page/settings_subpage_search.html8
-rw-r--r--chromium/chrome/browser/resources/settings/settings_resources.grd43
-rw-r--r--chromium/chrome/browser/resources/settings/settings_shared_css.html56
-rw-r--r--chromium/chrome/browser/resources/settings/settings_ui/settings_ui.html4
-rw-r--r--chromium/chrome/browser/resources/settings/settings_ui/settings_ui.js2
-rw-r--r--chromium/chrome/browser/resources/settings/settings_vars_css.html6
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/add_site_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/all_sites.html2
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/category_default_setting.js1
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/constants.js3
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/edit_exception_dialog.html4
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.html35
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.js78
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/site_data.html12
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/site_data_details_subpage.html2
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/site_details.html27
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/site_details.js11
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/site_list.html30
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/site_settings_prefs_browser_proxy.js16
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/usb_devices.html5
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings/zoom_levels.html2
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.html103
-rw-r--r--chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.js10
-rw-r--r--chromium/chrome/browser/resources/settings/system_page/system_page.html2
-rw-r--r--chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google.pngbin617 -> 481 bytes
-rw-r--r--chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google_2x.pngbin1221 -> 857 bytes
-rw-r--r--chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.html31
-rw-r--r--chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.js37
-rw-r--r--chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_browser_proxy.js30
-rw-r--r--chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.html33
-rw-r--r--chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.js33
-rw-r--r--chromium/chrome/browser/resources/snippets_internals.html48
-rw-r--r--chromium/chrome/browser/resources/vr/assets/PRESUBMIT.py30
-rw-r--r--chromium/chrome/browser/resources/vr/assets/VERSION2
-rw-r--r--chromium/chrome/browser/resources/vr/assets/chromium/background.pngbin0 -> 90 bytes
-rw-r--r--chromium/chrome/browser/resources/vr/assets/chromium/fullscreen_gradient.pngbin0 -> 127 bytes
-rw-r--r--chromium/chrome/browser/resources/vr/assets/chromium/incognito_gradient.pngbin0 -> 127 bytes
-rw-r--r--chromium/chrome/browser/resources/vr/assets/chromium/normal_gradient.pngbin0 -> 90 bytes
-rw-r--r--chromium/chrome/browser/resources/vr/assets/fullscreen_gradient.png.sha11
-rw-r--r--chromium/chrome/browser/resources/vr/assets/google_chrome/background.png.sha1 (renamed from chromium/chrome/browser/resources/vr/assets/background.png.sha1)0
-rw-r--r--chromium/chrome/browser/resources/vr/assets/google_chrome/fullscreen_gradient.png.sha11
-rw-r--r--chromium/chrome/browser/resources/vr/assets/google_chrome/incognito_gradient.png.sha11
-rw-r--r--chromium/chrome/browser/resources/vr/assets/google_chrome/normal_gradient.png.sha11
-rw-r--r--chromium/chrome/browser/resources/vr/assets/incognito_gradient.png.sha11
-rw-r--r--chromium/chrome/browser/resources/vr/assets/normal_gradient.png.sha11
-rwxr-xr-xchromium/chrome/browser/resources/vr/assets/push_assets_component.py8
-rw-r--r--chromium/chrome/browser/resources/vr/assets/vr_assets_component_files.json8
-rw-r--r--chromium/chrome/browser/resources/vr_shell/OWNERS3
-rw-r--r--chromium/chrome/browser/resources/vr_shell/ddcontroller.glbbin10900 -> 0 bytes
-rw-r--r--chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_app.pngbin2420 -> 0 bytes
-rw-r--r--chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_idle.pngbin14780 -> 0 bytes
-rw-r--r--chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_system.pngbin1990 -> 0 bytes
-rw-r--r--chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_touchpad.pngbin8941 -> 0 bytes
-rw-r--r--chromium/chrome/browser/resources/vr_shell_resources.grd19
545 files changed, 30511 insertions, 3232 deletions
diff --git a/chromium/chrome/browser/resources/BUILD.gn b/chromium/chrome/browser/resources/BUILD.gn
index f0559f28e62..c9f770b9a8e 100644
--- a/chromium/chrome/browser/resources/BUILD.gn
+++ b/chromium/chrome/browser/resources/BUILD.gn
@@ -166,15 +166,3 @@ if (enable_print_preview) {
output_dir = "$root_gen_dir/chrome"
}
}
-
-if (enable_vr) {
- grit("vr_shell_resources") {
- source = "vr_shell_resources.grd"
- defines = chrome_grit_defines
- outputs = [
- "grit/vr_shell_resources.h",
- "vr_shell_resources.pak",
- ]
- output_dir = "$root_gen_dir/chrome"
- }
-}
diff --git a/chromium/chrome/browser/resources/PRESUBMIT.py b/chromium/chrome/browser/resources/PRESUBMIT.py
index 9ebdb0ab8e6..e2a39c26688 100644
--- a/chromium/chrome/browser/resources/PRESUBMIT.py
+++ b/chromium/chrome/browser/resources/PRESUBMIT.py
@@ -8,8 +8,6 @@ See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
for more details about the presubmit API built into depot_tools.
"""
-import re
-
ACTION_XML_PATH = '../../../tools/metrics/actions/actions.xml'
diff --git a/chromium/chrome/browser/resources/about_voicesearch.html b/chromium/chrome/browser/resources/about_voicesearch.html
deleted file mode 100644
index 93a43dc6485..00000000000
--- a/chromium/chrome/browser/resources/about_voicesearch.html
+++ /dev/null
@@ -1,38 +0,0 @@
-<!doctype html>
-<html i18n-values="dir:textdirection;lang:language">
-<head>
-<meta charset="utf-8">
-<link rel="stylesheet" href="chrome://resources/css/text_defaults.css">
-<style>
-.key {
- font-weight: bold;
-}
-
-.value {
- margin-left: 15px;
-}
-</style>
-</head>
-<body>
-<div id="loading-message" i18n-content="loadingMessage">LOADING_MESSAGE</div>
-<div id="body-container" hidden>
- <div id="header">
- <h1 i18n-content="voiceSearchLongTitle">ABOUT_VOICESEARCH</h1>
- </div>
- <div id="voice-search-info-template">
- <table cellpadding="2" cellspacing="0" border="0">
- <tr jsselect="voiceSearchInfo">
- <td><span dir="ltr" jscontent="key" class="key">KEY</span></td>
- <td><span dir="ltr" jscontent="value" class="value">VALUE</span></td>
- </tr>
- </table>
- </div>
-</div>
-<script src="chrome://resources/js/load_time_data.js"></script>
-<script src="chrome://voicesearch/about_voicesearch.js"></script>
-<script src="chrome://voicesearch/strings.js"></script>
-<script src="chrome://resources/js/i18n_template.js"></script>
-<script src="chrome://resources/js/jstemplate_compiled.js"></script>
-<script src="chrome://resources/js/util.js"></script>
-</body>
-</html>
diff --git a/chromium/chrome/browser/resources/about_voicesearch.js b/chromium/chrome/browser/resources/about_voicesearch.js
deleted file mode 100644
index bcf8cbd5f7b..00000000000
--- a/chromium/chrome/browser/resources/about_voicesearch.js
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2014 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-/**
- * Takes the |moduleListData| input argument which represents data about
- * the currently available modules and populates the html jstemplate
- * with that data. It expects an object structure like the above.
- * @param {Object} moduleListData Information about available modules
- */
-function renderTemplate(moduleListData) {
- var input = new JsEvalContext(moduleListData);
- var output = $('voice-search-info-template');
- jstProcess(input, output);
-}
-
-/**
- * Asks the C++ VoiceSearchUIDOMHandler to get details about voice search and
- * return the data in returnVoiceSearchInfo() (below).
- */
-function requestVoiceSearchInfo() {
- chrome.send('requestVoiceSearchInfo');
-}
-
-/**
- * Called by the WebUI to re-populate the page with data representing the
- * current state of voice search.
- * @param {Object} moduleListData Information about available modules.
- */
-function returnVoiceSearchInfo(moduleListData) {
- $('loading-message').hidden = true;
- $('body-container').hidden = false;
- renderTemplate(moduleListData);
-}
-
-// Get data and have it displayed upon loading.
-document.addEventListener('DOMContentLoaded', requestVoiceSearchInfo);
diff --git a/chromium/chrome/browser/resources/app_list/OWNERS b/chromium/chrome/browser/resources/app_list/OWNERS
deleted file mode 100644
index ed54f09c40c..00000000000
--- a/chromium/chrome/browser/resources/app_list/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-calamity@chromium.org
-khmel@chromium.org
diff --git a/chromium/chrome/browser/resources/app_list/start_page.css b/chromium/chrome/browser/resources/app_list/start_page.css
deleted file mode 100644
index 3efaef1955a..00000000000
--- a/chromium/chrome/browser/resources/app_list/start_page.css
+++ /dev/null
@@ -1,32 +0,0 @@
-/* Copyright 2013 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
-html,
-body {
- height: 100%;
- margin: 0;
- overflow: hidden;
- padding: 0;
- user-select: none;
- width: 100%;
-}
-
-#doodle {
- display: none;
- justify-content: center;
-}
-
-#default_logo {
- background-image: url(../../../../ui/webui/resources/images/google_logo.svg);
- background-repeat: no-repeat;
- height: 92px;
- margin: auto;
- width: 272px;
-}
-
-#logo_container {
- bottom: 0;
- position: absolute;
- width: 100%;
-}
diff --git a/chromium/chrome/browser/resources/app_list/start_page.html b/chromium/chrome/browser/resources/app_list/start_page.html
deleted file mode 100644
index 5388e65d334..00000000000
--- a/chromium/chrome/browser/resources/app_list/start_page.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!doctype html>
-<html i18n-values="dir:textdirection;lang:language">
-<head>
- <meta charset="utf-8">
- <link rel="stylesheet" href="chrome://resources/css/text_defaults.css">
- <link rel="stylesheet" href="chrome://app-list/start_page.css">
- <script src="chrome://resources/js/load_time_data.js"></script>
- <script src="chrome://resources/js/cr.js"></script>
- <script src="chrome://resources/js/cr/event_target.js"></script>
- <script src="chrome://resources/js/cr/ui.js"></script>
- <script src="chrome://resources/js/util.js"></script>
- <script src="chrome://app-list/strings.js"></script>
- <script src="chrome://app-list/start_page.js"></script>
- <base id="base">
-</head>
-
-<body>
- <div id="logo_container">
- <div id="default_logo"></div>
- </div>
- <script src="chrome://resources/js/i18n_template.js"></script>
-</body>
-</html>
diff --git a/chromium/chrome/browser/resources/app_list/start_page.js b/chromium/chrome/browser/resources/app_list/start_page.js
deleted file mode 100644
index 2a01e136998..00000000000
--- a/chromium/chrome/browser/resources/app_list/start_page.js
+++ /dev/null
@@ -1,120 +0,0 @@
-// Copyright 2013 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-/**
- * @fileoverview App launcher start page implementation.
- */
-
-/**
- * The maximum height of the Google Doodle. Note this value should be consistent
- * with kWebViewHeight in start_page_view.cc.
- */
-var doodleMaxHeight = 224;
-
-cr.define('appList.startPage', function() {
- 'use strict';
-
- // The element containing the current Google Doodle.
- var doodle = null;
-
- /**
- * Initialize the page.
- */
- function initialize() {
- chrome.send('initialize');
- }
-
- /**
- * Invoked when the app-list bubble is shown.
- */
- function onAppListShown() {
- chrome.send('appListShown', [this.doodle != null]);
- }
-
- /**
- * Sets the doodle's visibility, hiding or showing the default logo.
- *
- * @param {boolean} visible Whether the doodle should be made visible.
- */
- function setDoodleVisible(visible) {
- var doodle = $('doodle');
- var defaultLogo = $('default_logo');
- if (visible) {
- doodle.style.display = 'flex';
- defaultLogo.style.display = 'none';
- } else {
- if (doodle)
- doodle.style.display = 'none';
-
- defaultLogo.style.display = 'block';
- }
- }
-
- /**
- * Invoked when the app-list doodle is updated.
- *
- * @param {Object} data The data object representing the current doodle.
- */
- function onAppListDoodleUpdated(data, base_url) {
- if (this.doodle) {
- this.doodle.parentNode.removeChild(this.doodle);
- this.doodle = null;
- }
-
- var doodleData = data.ddljson;
- if (!doodleData || !doodleData.transparent_large_image) {
- setDoodleVisible(false);
- return;
- }
-
- // Set the page's base URL so that links will resolve relative to the Google
- // homepage.
- $('base').href = base_url;
-
- this.doodle = document.createElement('div');
- this.doodle.id = 'doodle';
- this.doodle.style.display = 'none';
-
- var doodleImage = document.createElement('img');
- doodleImage.id = 'doodle_image';
- if (doodleData.transparent_large_image.height > doodleMaxHeight)
- doodleImage.setAttribute('height', doodleMaxHeight);
- if (doodleData.alt_text) {
- doodleImage.alt = doodleData.alt_text;
- doodleImage.title = doodleData.alt_text;
- }
-
- doodleImage.onload = function() {
- setDoodleVisible(true);
- };
- doodleImage.src = doodleData.transparent_large_image.url;
-
- if (doodleData.target_url) {
- var doodleLink = document.createElement('a');
- doodleLink.id = 'doodle_link';
- doodleLink.href = doodleData.target_url;
- doodleLink.target = '_blank';
- doodleLink.appendChild(doodleImage);
- doodleLink.onclick = function() {
- chrome.send('doodleClicked');
- return true;
- };
- this.doodle.appendChild(doodleLink);
- } else {
- this.doodle.appendChild(doodleImage);
- }
- $('logo_container').appendChild(this.doodle);
- }
-
- return {
- initialize: initialize,
- onAppListDoodleUpdated: onAppListDoodleUpdated,
- onAppListShown: onAppListShown,
- };
-});
-
-document.addEventListener('contextmenu', function(e) {
- e.preventDefault();
-});
-document.addEventListener('DOMContentLoaded', appList.startPage.initialize);
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js b/chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js
index e357336fe7e..d345167a581 100644
--- a/chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js
+++ b/chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js
@@ -111,7 +111,7 @@ cr.define('adapter_broker', function() {
/**
* The implementation of AdapterClient in
- * device/bluetooth/public/interfaces/adapter.mojom. Dispatches events
+ * device/bluetooth/public/mojom/adapter.mojom. Dispatches events
* through AdapterBroker to notify client objects of changes to the Adapter
* service.
* @constructor
diff --git a/chromium/chrome/browser/resources/chromeos/chromevox/BUILD.gn b/chromium/chrome/browser/resources/chromeos/chromevox/BUILD.gn
index 98fdb591fca..329a2f37113 100644
--- a/chromium/chrome/browser/resources/chromeos/chromevox/BUILD.gn
+++ b/chromium/chrome/browser/resources/chromeos/chromevox/BUILD.gn
@@ -126,6 +126,7 @@ chromevox_modules = [
"cvox2/background/automation_util.js",
"cvox2/background/background.js",
"cvox2/background/base_automation_handler.js",
+ "cvox2/background/braille_command_data.js",
"cvox2/background/braille_command_handler.js",
"cvox2/background/chromevox_state.js",
"cvox2/background/command_handler.js",
diff --git a/chromium/chrome/browser/resources/chromeos/chromevox/strings/chromevox_strings.grd b/chromium/chrome/browser/resources/chromeos/chromevox/strings/chromevox_strings.grd
index ed7b45e0e20..f47bdfdc16b 100644
--- a/chromium/chrome/browser/resources/chromeos/chromevox/strings/chromevox_strings.grd
+++ b/chromium/chrome/browser/resources/chromeos/chromevox/strings/chromevox_strings.grd
@@ -2783,6 +2783,27 @@ If you're done with the tutorial, use ChromeVox to navigate to the Close button
<message desc="Shown to a user when they invoke the read current title command in a context without a title." name="IDS_CHROMEVOX_NO_TITLE">
No title
</message>
+ <message desc="Spoken when a user issues a command when nothing is focused." name="IDS_CHROMEVOX_WARNING_NO_CURRENT_RANGE">
+ No focus. Press Ctrl+T to open a new tab.
+ </message>
+ <message desc="A hint to the user that the current control is checkable." name="IDS_CHROMEVOX_HINT_CHECKABLE">
+ Press Search+Space to toggle.
+ </message>
+ <message desc="A hint to the user that the current control is clickable." name="IDS_CHROMEVOX_HINT_CLICKABLE">
+ Press Search+Space to activate.
+ </message>
+ <message desc="A hint to the user that the current control has a list of auto completions." name="IDS_CHROMEVOX_HINT_AUTOCOMPLETE_LIST">
+ Press up or down arrow for auto completions.
+ </message>
+ <message desc="A hint to the user that the current control has inline auto completions." name="IDS_CHROMEVOX_HINT_AUTOCOMPLETE_INLINE">
+ Type to auto complete.
+ </message>
+ <message desc="A hint to the user for interacting with the table control." name="IDS_CHROMEVOX_HINT_TABLE">
+ Press Search+Ctrl+Alt with arrows to navigate by cell.
+ </message>
+ <message desc="A hint to the user for interacting with the menu control." name="IDS_CHROMEVOX_HINT_MENU">
+ Press up or down arrow to navigate; enter to activate.
+ </message>
</messages>
</release>
</grit>
diff --git a/chromium/chrome/browser/resources/chromeos/genius_app/manifest.json b/chromium/chrome/browser/resources/chromeos/genius_app/manifest.json
index 6a625f766a7..e0f1b720490 100644
--- a/chromium/chrome/browser/resources/chromeos/genius_app/manifest.json
+++ b/chromium/chrome/browser/resources/chromeos/genius_app/manifest.json
@@ -46,6 +46,7 @@
"https://www.googleapis.com/auth/supportcontent",
"https://www.googleapis.com/auth/cases",
"https://www.googleapis.com/auth/cases.readonly",
+ "https://www.googleapis.com/auth/pixelbook.email.preferences",
"https://www.google.com/accounts/OAuthLogin"
]
},
diff --git a/chromium/chrome/browser/resources/chromeos/login/BUILD.gn b/chromium/chrome/browser/resources/chromeos/login/BUILD.gn
deleted file mode 100644
index 8ad38cb8443..00000000000
--- a/chromium/chrome/browser/resources/chromeos/login/BUILD.gn
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright 2017 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import("//third_party/closure_compiler/compile_js.gni")
-
-group("closure_compile") {
- deps = [
- ":offline_ad_login",
- ":oobe_change_picture",
- ]
-}
-
-js_binary("offline_ad_login") {
- deps = [
- "//ui/webui/resources/js:load_time_data",
- ]
-}
-
-js_binary("oobe_change_picture") {
- deps = [
- "//ui/webui/resources/cr_elements/chromeos/cr_picture:cr_picture_list",
- "//ui/webui/resources/cr_elements/chromeos/cr_picture:cr_picture_pane",
- "//ui/webui/resources/cr_elements/chromeos/cr_picture:cr_picture_types",
- "//ui/webui/resources/js:assert",
- "//ui/webui/resources/js:i18n_behavior",
- "//ui/webui/resources/js:load_time_data",
- "//ui/webui/resources/js:util",
- ]
-}
diff --git a/chromium/chrome/browser/resources/chromeos/login/compiled_resources2.gyp b/chromium/chrome/browser/resources/chromeos/login/compiled_resources2.gyp
index de8e38ce6b7..dbe606c4978 100644
--- a/chromium/chrome/browser/resources/chromeos/login/compiled_resources2.gyp
+++ b/chromium/chrome/browser/resources/chromeos/login/compiled_resources2.gyp
@@ -4,9 +4,14 @@
{
'targets': [
{
+ 'target_name': 'oobe_select',
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
'target_name': 'offline_ad_login',
'dependencies': [
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data',
+ ':oobe_select',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
diff --git a/chromium/chrome/browser/resources/chromeos/quick_unlock/compiled_resources2.gyp b/chromium/chrome/browser/resources/chromeos/quick_unlock/compiled_resources2.gyp
deleted file mode 100644
index a953cb9433b..00000000000
--- a/chromium/chrome/browser/resources/chromeos/quick_unlock/compiled_resources2.gyp
+++ /dev/null
@@ -1,25 +0,0 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-{
- 'targets': [
- {
- 'target_name': 'md_pin_keyboard',
- 'dependencies': [
- '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:i18n_behavior',
- '<(DEPTH)/third_party/polymer/v1_0/components-chromium/paper-button/compiled_resources2.gyp:paper-button-extracted',
- '<(DEPTH)/third_party/polymer/v1_0/components-chromium/paper-input/compiled_resources2.gyp:paper-input-extracted',
- ],
- 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
- },
- {
- 'target_name': 'pin_keyboard',
- 'dependencies': [
- '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:i18n_behavior',
- '<(DEPTH)/third_party/polymer/v1_0/components-chromium/paper-button/compiled_resources2.gyp:paper-button-extracted',
- '<(DEPTH)/third_party/polymer/v1_0/components-chromium/paper-input/compiled_resources2.gyp:paper-input-extracted',
- ],
- 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
- },
- ],
-}
diff --git a/chromium/chrome/browser/resources/chromeos/select_to_speak/BUILD.gn b/chromium/chrome/browser/resources/chromeos/select_to_speak/BUILD.gn
index 3a2962e1cab..d01029bf728 100644
--- a/chromium/chrome/browser/resources/chromeos/select_to_speak/BUILD.gn
+++ b/chromium/chrome/browser/resources/chromeos/select_to_speak/BUILD.gn
@@ -32,14 +32,18 @@ run_jsbundler("select_to_speak_copied_files") {
"../chromevox/cvox2/background/tree_walker.js",
"checked.png",
"closure_shim.js",
+ "earcons/null_selection.ogg",
+ "node_utils.js",
"options.css",
"options.html",
"paragraph_utils.js",
+ "rect_utils.js",
"select_to_speak.js",
"select_to_speak_gdocs_script.js",
"select_to_speak_main.js",
"select_to_speak_options.js",
"unchecked.png",
+ "word_utils.js",
]
rewrite_rules = [
rebase_path(".", root_build_dir) + ":",
@@ -146,11 +150,13 @@ js2gtest("select_to_speak_extjs_tests") {
test_type = "extension"
sources = [
"select_to_speak_keystroke_selection_test.extjs",
+ "select_to_speak_mouse_selection_test.extjs",
]
gen_include_files = [
"../chromevox/testing/callback_helper.js",
"mock_tts.js",
"select_to_speak_e2e_test_base.js",
+ "pipe.jpg",
]
defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
}
diff --git a/chromium/chrome/browser/resources/chromeos/select_to_speak/compiled_resources2.gyp b/chromium/chrome/browser/resources/chromeos/select_to_speak/compiled_resources2.gyp
index ec7035886ae..b7a05836e6f 100644
--- a/chromium/chrome/browser/resources/chromeos/select_to_speak/compiled_resources2.gyp
+++ b/chromium/chrome/browser/resources/chromeos/select_to_speak/compiled_resources2.gyp
@@ -8,24 +8,28 @@
'dependencies': [
'../chromevox/cvox2/background/constants',
'../chromevox/cvox2/background/automation_util',
- 'externs',
- 'paragraph_utils',
- '<(EXTERNS_GYP):accessibility_private',
- '<(EXTERNS_GYP):automation',
- '<(EXTERNS_GYP):chrome_extensions',
- '<(EXTERNS_GYP):command_line_private',
- '<(EXTERNS_GYP):metrics_private',
+ 'externs',
+ 'rect_utils',
+ 'paragraph_utils',
+ 'word_utils',
+ 'node_utils',
+ '<(EXTERNS_GYP):accessibility_private',
+ '<(EXTERNS_GYP):automation',
+ '<(EXTERNS_GYP):chrome_extensions',
+ '<(EXTERNS_GYP):clipboard',
+ '<(EXTERNS_GYP):command_line_private',
+ '<(EXTERNS_GYP):metrics_private',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
{
'target_name': 'select_to_speak_options',
'dependencies': [
- 'externs',
- '<(EXTERNS_GYP):accessibility_private',
- '<(EXTERNS_GYP):automation',
- '<(EXTERNS_GYP):chrome_extensions',
- '<(EXTERNS_GYP):metrics_private',
+ 'externs',
+ '<(EXTERNS_GYP):accessibility_private',
+ '<(EXTERNS_GYP):automation',
+ '<(EXTERNS_GYP):chrome_extensions',
+ '<(EXTERNS_GYP):metrics_private',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
@@ -34,42 +38,65 @@
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
{
+ 'target_name': 'node_utils',
+ 'dependencies': [
+ 'externs',
+ 'rect_utils',
+ 'paragraph_utils',
+ '<(EXTERNS_GYP):automation',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
+ 'target_name': 'word_utils',
+ 'dependencies': [
+ 'externs',
+ 'paragraph_utils',
+ '<(EXTERNS_GYP):automation',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
'target_name': 'paragraph_utils',
'dependencies': [
- 'externs',
- '<(EXTERNS_GYP):accessibility_private',
- '<(EXTERNS_GYP):automation',
- '<(EXTERNS_GYP):chrome_extensions',
+ 'externs',
+ '<(EXTERNS_GYP):accessibility_private',
+ '<(EXTERNS_GYP):automation',
+ '<(EXTERNS_GYP):chrome_extensions',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
{
+ 'target_name': 'rect_utils',
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
'target_name': '../chromevox/cvox2/background/automation_util',
'dependencies': [
- '../chromevox/cvox2/background/automation_predicate',
- '../chromevox/cvox2/background/tree_walker',
- '../chromevox/cvox2/background/constants',
- '<(EXTERNS_GYP):automation',
- '<(EXTERNS_GYP):chrome_extensions',
+ '../chromevox/cvox2/background/automation_predicate',
+ '../chromevox/cvox2/background/tree_walker',
+ '../chromevox/cvox2/background/constants',
+ '<(EXTERNS_GYP):automation',
+ '<(EXTERNS_GYP):chrome_extensions',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
{
'target_name': '../chromevox/cvox2/background/tree_walker',
'dependencies': [
- '../chromevox/cvox2/background/automation_predicate',
- '../chromevox/cvox2/background/constants',
- '<(EXTERNS_GYP):automation',
- '<(EXTERNS_GYP):chrome_extensions',
+ '../chromevox/cvox2/background/automation_predicate',
+ '../chromevox/cvox2/background/constants',
+ '<(EXTERNS_GYP):automation',
+ '<(EXTERNS_GYP):chrome_extensions',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
{
'target_name': '../chromevox/cvox2/background/automation_predicate',
'dependencies': [
- '../chromevox/cvox2/background/constants',
- '<(EXTERNS_GYP):automation',
- '<(EXTERNS_GYP):chrome_extensions',
+ '../chromevox/cvox2/background/constants',
+ '<(EXTERNS_GYP):automation',
+ '<(EXTERNS_GYP):chrome_extensions',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
diff --git a/chromium/chrome/browser/resources/chromeos/wallpaper_manager/manifest.json b/chromium/chrome/browser/resources/chromeos/wallpaper_manager/manifest.json
index 8feeb9309b5..d4c6c37d202 100644
--- a/chromium/chrome/browser/resources/chromeos/wallpaper_manager/manifest.json
+++ b/chromium/chrome/browser/resources/chromeos/wallpaper_manager/manifest.json
@@ -20,6 +20,7 @@
"alarms",
"app.window.alpha",
"chrome://resources/",
+ "commandLinePrivate",
"experimental",
"storage",
"unlimitedStorage",
diff --git a/chromium/chrome/browser/resources/components.js b/chromium/chrome/browser/resources/components.js
index f1247bbc0b3..8d29201a3d0 100644
--- a/chromium/chrome/browser/resources/components.js
+++ b/chromium/chrome/browser/resources/components.js
@@ -5,6 +5,13 @@
'use strict';
/**
+ * An array of the latest component data including ID, name, status and
+ * version. This is populated in returnComponentsData() for the convenience of
+ * tests.
+ */
+var currentComponentsData = null;
+
+/**
* Takes the |componentsData| input argument which represents data about the
* currently installed components and populates the html jstemplate with
* that data. It expects an object structure like the above.
@@ -32,7 +39,8 @@ function requestComponentsData() {
/**
* Called by the WebUI to re-populate the page with data representing the
- * current state of installed components.
+ * current state of installed components. The componentsData will also be
+ * stored in currentComponentsData to be available to JS for testing purposes.
* @param {Object} componentsData Detailed info about installed components. The
* template expects each component's format to match the following
* structure to correctly populate the page:
@@ -56,6 +64,10 @@ function returnComponentsData(componentsData) {
bodyContainer.style.visibility = 'hidden';
body.className = '';
+ // Initialize |currentComponentsData|, which can also be updated in
+ // onComponentEvent() later.
+ currentComponentsData = componentsData.components;
+
renderTemplate(componentsData);
// Add handlers to dynamically created HTML elements.
@@ -85,12 +97,24 @@ function returnComponentsData(componentsData) {
* optional.
*/
function onComponentEvent(eventArgs) {
- if (eventArgs['id']) {
- var id = eventArgs['id'];
- $('status-' + id).textContent = eventArgs['event'];
- }
+ if (!eventArgs['id'])
+ return;
+
+ var id = eventArgs['id'];
+
+ var filteredComponents = currentComponentsData.filter(function(entry) {
+ return entry.id === id;
+ });
+ var component = filteredComponents[0];
+
+ var status = eventArgs['event'];
+ $('status-' + id).textContent = status;
+ component['status'] = status;
+
if (eventArgs['version']) {
- $('version-' + id).textContent = eventArgs['version'];
+ var version = eventArgs['version'];
+ $('version-' + id).textContent = version;
+ component['version'] = version;
}
}
diff --git a/chromium/chrome/browser/resources/cryptotoken/enroller.js b/chromium/chrome/browser/resources/cryptotoken/enroller.js
index 9b1349281cd..4bc7bb02d16 100644
--- a/chromium/chrome/browser/resources/cryptotoken/enroller.js
+++ b/chromium/chrome/browser/resources/cryptotoken/enroller.js
@@ -144,7 +144,7 @@ async function makeCertAndKey(original) {
b.addASN1(Tag.SET, (b) => {
b.addASN1(Tag.SEQUENCE, (b) => {
b.addASN1ObjectIdentifier(commonName);
- b.addASN1PrintableString('U2F');
+ b.addASN1PrintableString('U2F Issuer');
});
});
});
@@ -160,7 +160,7 @@ async function makeCertAndKey(original) {
b.addASN1(Tag.SET, (b) => {
b.addASN1(Tag.SEQUENCE, (b) => {
b.addASN1ObjectIdentifier(commonName);
- b.addASN1PrintableString('U2F');
+ b.addASN1PrintableString('U2F Device');
});
});
});
@@ -192,7 +192,21 @@ async function makeCertAndKey(original) {
b.addASN1ObjectIdentifier(ecdsaWithSHA256);
});
b.addASN1(Tag.BITSTRING, (b) => { // Signature
- b.addBytesFromString('\x00'); // (not valid, obviously.)
+ // This signature is obviously not correct since it's constant and the
+ // rest of the certificate is not. However, since the issuer certificate
+ // doesn't exist, there's no way for anyone to check the signature on this
+ // certificate and thus this sufficies. However, at least fastmail.com
+ // expects to be able to parse out a valid ECDSA signature and so one is
+ // provided.
+ b.addBytes(new Uint8Array([
+ 0x00, 0x30, 0x45, 0x02, 0x21, 0x00, 0xc1, 0xa3, 0xa6, 0x8e, 0x2f,
+ 0x16, 0xa7, 0x21, 0x46, 0x27, 0x05, 0x7f, 0x62, 0xbb, 0x72, 0x8c,
+ 0x9e, 0x03, 0xe7, 0xa1, 0xba, 0x62, 0xd0, 0x46, 0x52, 0x4e, 0x45,
+ 0x6d, 0x2c, 0x2f, 0x3f, 0x73, 0x02, 0x20, 0x0b, 0x5f, 0x78, 0xe5,
+ 0x11, 0xaa, 0x18, 0x12, 0x9f, 0x6f, 0x23, 0x6d, 0x92, 0x13, 0x22,
+ 0x7d, 0x92, 0xb4, 0xe6, 0x7e, 0xdf, 0x53, 0xe8, 0x16, 0xdf, 0xb0,
+ 0x5d, 0x9d, 0xc8, 0xb9, 0x0f, 0xde
+ ]));
});
});
return {privateKey: keypair.privateKey, certDER: certBuilder.data};
@@ -207,11 +221,13 @@ const Registration = class {
* @param {string} registrationData the registration response message,
* base64-encoded.
* @param {string} appId the application identifier.
- * @param {string=} opt_clientData the client data, base64-encoded. This
- * field is not really optional; it is an error if it is empty or missing.
+ * @param {string} challenge the server-generated challenge parameter. This
+ * is only used if opt_clientData is null and, in that case, is expected
+ * to be a webSafeBase64-encoded, 32-byte value.
+ * @param {string=} opt_clientData the client data, base64-encoded.
* @throws {Error}
*/
- constructor(registrationData, appId, opt_clientData) {
+ constructor(registrationData, appId, challenge, opt_clientData) {
var data = new ByteString(decodeWebSafeBase64ToArray(registrationData));
var magic = data.getBytes(1);
if (magic[0] != 5) {
@@ -231,12 +247,21 @@ const Registration = class {
throw Error('extra trailing bytes');
}
+ var challengeHash;
if (!opt_clientData) {
- throw Error('missing client data');
+ // U2F_V1 - deprecated
+ challengeHash = decodeWebSafeBase64ToArray(challenge);
+ if (challengeHash.length != 32) {
+ throw Error('bad challenge length for U2F_V1');
+ }
+ } else {
+ // U2F_V2
+ challengeHash =
+ sha256HashOfString(atob(webSafeBase64ToNormal(opt_clientData)));
}
+
/** @private {string} */
- this.clientData_ = atob(webSafeBase64ToNormal(opt_clientData));
- JSON.parse(this.clientData_); // Just checking.
+ this.challengeHash_ = challengeHash;
/** @private {string} */
this.appId_ = appId;
@@ -262,7 +287,7 @@ const Registration = class {
var tbs = new ByteBuilder();
tbs.addBytesFromString('\0');
tbs.addBytes(sha256HashOfString(this.appId_));
- tbs.addBytes(sha256HashOfString(this.clientData_));
+ tbs.addBytes(this.challengeHash_);
tbs.addBytes(this.keyHandle_);
tbs.addBytes(this.publicKey_);
return tbs.data;
@@ -338,10 +363,11 @@ var ConveyancePreference = {
*/
function conveyancePreference(enrollChallenge) {
if (enrollChallenge.hasOwnProperty('attestation') &&
- enrollChallenge['attestation'] == 'none') {
- return ConveyancePreference.NONE;
+ (enrollChallenge['attestation'] == 'direct' ||
+ enrollChallenge['attestation'] == 'indirect')) {
+ return ConveyancePreference.DIRECT;
}
- return ConveyancePreference.DIRECT;
+ return ConveyancePreference.NONE;
}
/**
@@ -379,7 +405,8 @@ function handleU2fEnrollRequest(messageSender, request, sendResponse) {
return registrationData;
}
- const reg = new Registration(registrationData, appId, opt_clientData);
+ const reg = new Registration(
+ registrationData, appId, enrollChallenge['challenge'], opt_clientData);
const keypair = await makeCertAndKey(reg.certificate);
const signature = await reg.sign(keypair.privateKey);
return reg.withReplacement(keypair.certDER, signature);
@@ -538,7 +565,7 @@ function isValidEnrollChallengeArray(enrollChallenges, appIdRequired) {
}
/**
- * Finds the enroll challenge of the given version in the enroll challlenge
+ * Finds the enroll challenge of the given version in the enroll challenge
* array.
* @param {Array<EnrollChallenge>} enrollChallenges The enroll challenges to
* search.
diff --git a/chromium/chrome/browser/resources/cryptotoken/gnubby-u2f.js b/chromium/chrome/browser/resources/cryptotoken/gnubby-u2f.js
index d0335ec9ddf..4d219debe02 100644
--- a/chromium/chrome/browser/resources/cryptotoken/gnubby-u2f.js
+++ b/chromium/chrome/browser/resources/cryptotoken/gnubby-u2f.js
@@ -151,8 +151,8 @@ Gnubby.prototype.version = function(cb) {
cb(-GnubbyDevice.OK, v1.buffer);
return;
}
- if (rc == 0x6700) {
- // Wrong length. Try with non-ISO 7816-4-conforming layout defined in
+ if (rc) {
+ // Error. Try with non-ISO 7816-4-conforming layout defined in
// earlier U2F drafts.
apdu = new Uint8Array(
[0x00, Gnubby.U2F_VERSION, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
diff --git a/chromium/chrome/browser/resources/cryptotoken/manifest.json b/chromium/chrome/browser/resources/cryptotoken/manifest.json
index fecac5426a2..2f86ab26a09 100644
--- a/chromium/chrome/browser/resources/cryptotoken/manifest.json
+++ b/chromium/chrome/browser/resources/cryptotoken/manifest.json
@@ -24,7 +24,7 @@
],
"externally_connectable": {
"matches": [
- "<all_urls>"
+ "https://*/*"
],
"ids": [
"fjajfjhkeibgmiggdfehjplbhmfkialk"
diff --git a/chromium/chrome/browser/resources/download_internals/download_internals.html b/chromium/chrome/browser/resources/download_internals/download_internals.html
index 49e67cdce47..acb85302d75 100644
--- a/chromium/chrome/browser/resources/download_internals/download_internals.html
+++ b/chromium/chrome/browser/resources/download_internals/download_internals.html
@@ -21,6 +21,12 @@
</head>
<body>
<h1>Download Internals</h1>
+ <h4>Start Download</h4>
+ <div id="download-service-request-info">
+ <input id="download-url" type="url"
+ placeholder="http://www.example.com">
+ <button id="start-download">Download</button>
+ </div>
<h4>Service State</h4>
<div>
State: <span id="service-state" class="status"></span>
diff --git a/chromium/chrome/browser/resources/download_internals/download_internals.js b/chromium/chrome/browser/resources/download_internals/download_internals.js
index 62f1f1f0ab8..d46a2106ca7 100644
--- a/chromium/chrome/browser/resources/download_internals/download_internals.js
+++ b/chromium/chrome/browser/resources/download_internals/download_internals.js
@@ -122,6 +122,10 @@ cr.define('downloadInternals', function() {
cr.addWebUIListener('service-download-failed', onServiceDownloadFailed);
cr.addWebUIListener('service-request-made', onServiceRequestMade);
+ $('start-download').onclick = function() {
+ browserProxy.startDownload($('download-url').value);
+ };
+
// Kick off requests for the current system state.
browserProxy.getServiceStatus().then(onServiceStatusChanged);
browserProxy.getServiceDownloads().then(onServiceDownloadsAvailable);
diff --git a/chromium/chrome/browser/resources/download_internals/download_internals_browser_proxy.js b/chromium/chrome/browser/resources/download_internals/download_internals_browser_proxy.js
index 89642e117d5..a79453ac7f3 100644
--- a/chromium/chrome/browser/resources/download_internals/download_internals_browser_proxy.js
+++ b/chromium/chrome/browser/resources/download_internals/download_internals_browser_proxy.js
@@ -105,6 +105,12 @@ cr.define('downloadInternals', function() {
* of downloads is fetched.
*/
getServiceDownloads() {}
+
+ /**
+ * Starts a download with the Download Service.
+ * @param {string} url The download URL.
+ */
+ startDownload(url) {}
}
/**
@@ -120,6 +126,11 @@ cr.define('downloadInternals', function() {
getServiceDownloads() {
return cr.sendWithPromise('getServiceDownloads');
}
+
+ /** @override */
+ startDownload(url) {
+ return cr.sendWithPromise('startDownload', url);
+ }
}
cr.addSingletonGetter(DownloadInternalsBrowserProxyImpl);
diff --git a/chromium/chrome/browser/resources/extensions/extensions.js b/chromium/chrome/browser/resources/extensions/extensions.js
index 663f8382b3d..4f9d1b2beb6 100644
--- a/chromium/chrome/browser/resources/extensions/extensions.js
+++ b/chromium/chrome/browser/resources/extensions/extensions.js
@@ -19,10 +19,6 @@
// <include src="chromeos/kiosk_apps.js">
// </if>
-// Used for observing function of the backend datasource for this page by
-// tests.
-var webuiResponded = false;
-
cr.define('extensions', function() {
var ExtensionList = extensions.ExtensionList;
@@ -185,7 +181,6 @@ cr.define('extensions', function() {
// don't need to display the interstitial spinner.
if (!this.hasLoaded_)
this.setLoading_(true);
- webuiResponded = true;
/** @const */
var supervised = profileInfo.isSupervised;
diff --git a/chromium/chrome/browser/resources/feedback/js/feedback.js b/chromium/chrome/browser/resources/feedback/js/feedback.js
index 909a5e50a68..538d7c825e4 100644
--- a/chromium/chrome/browser/resources/feedback/js/feedback.js
+++ b/chromium/chrome/browser/resources/feedback/js/feedback.js
@@ -334,6 +334,13 @@ function initialize() {
});
chrome.app.window.current().show();
+ // Allow feedback to be sent even if the screenshot failed.
+ if (!screenshotCanvas) {
+ $('screenshot-checkbox').disabled = true;
+ $('screenshot-checkbox').checked = false;
+ return;
+ }
+
screenshotCanvas.toBlob(function(blob) {
$('screenshot-image').src = URL.createObjectURL(blob);
// Only set the alt text when the src url is available, otherwise we'd
diff --git a/chromium/chrome/browser/resources/feedback/js/take_screenshot.js b/chromium/chrome/browser/resources/feedback/js/take_screenshot.js
index 3f099724440..830fd726004 100644
--- a/chromium/chrome/browser/resources/feedback/js/take_screenshot.js
+++ b/chromium/chrome/browser/resources/feedback/js/take_screenshot.js
@@ -5,7 +5,7 @@
/**
* Function to take the screenshot of the current screen.
* @param {function(HTMLCanvasElement)} callback Callback for returning the
- * canvas with the screenshot on it.
+ * canvas with the screenshot. Called with null if the screenshot failed.
*/
function takeScreenshot(callback) {
var screenshotStream = null;
@@ -47,5 +47,6 @@ function takeScreenshot(callback) {
console.error(
'takeScreenshot failed: ' + err.name + '; ' + err.message + '; ' +
err.constraintName);
+ callback(null);
});
}
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/saml_handler.js b/chromium/chrome/browser/resources/gaia_auth_host/saml_handler.js
index 670bf7f90af..ba62489f2e2 100644
--- a/chromium/chrome/browser/resources/gaia_auth_host/saml_handler.js
+++ b/chromium/chrome/browser/resources/gaia_auth_host/saml_handler.js
@@ -469,7 +469,7 @@ cr.define('cr.login', function() {
this.authDomain = extractDomain(msg.url);
this.dispatchEvent(new CustomEvent('authPageLoaded', {
detail: {
- url: url,
+ url: msg.url,
isSAMLPage: this.isSamlPage_,
domain: this.authDomain
}
diff --git a/chromium/chrome/browser/resources/identity_internals.html b/chromium/chrome/browser/resources/identity_internals.html
index fcfcc431028..3be44786a73 100644
--- a/chromium/chrome/browser/resources/identity_internals.html
+++ b/chromium/chrome/browser/resources/identity_internals.html
@@ -1,8 +1,8 @@
<!doctype html>
-<html i18n-values="dir:textdirection;lang:language">
+<html dir="$i18n{textdirection}" lang="$i18n{language}">
<head>
<meta charset="utf-8">
- <title i18n-content="tokenCacheHeader"></title>
+ <title>$i18n{tokenCacheHeader}</title>
<link rel="stylesheet" href="chrome://resources/css/text_defaults.css">
<link rel="stylesheet" href="identity_internals.css">
<script src="chrome://resources/js/cr.js"></script>
@@ -13,7 +13,7 @@
<script src="identity_internals.js"></script>
</head>
<body>
- <h2 class="header" i18n-content="tokenCacheHeader"></h2>
+ <h2 class="header">$i18n{tokenCacheHeader}</h2>
<div id="token-list"></div>
<script src="chrome://resources/js/i18n_template.js"></script>
</body>
diff --git a/chromium/chrome/browser/resources/input_ime/ime_window_close.png b/chromium/chrome/browser/resources/input_ime/ime_window_close.png
index 0010118ca99..6b3bd5d836f 100644
--- a/chromium/chrome/browser/resources/input_ime/ime_window_close.png
+++ b/chromium/chrome/browser/resources/input_ime/ime_window_close.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/input_ime/ime_window_close_click.png b/chromium/chrome/browser/resources/input_ime/ime_window_close_click.png
index a954503b6d3..60a218cf395 100644
--- a/chromium/chrome/browser/resources/input_ime/ime_window_close_click.png
+++ b/chromium/chrome/browser/resources/input_ime/ime_window_close_click.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/input_ime/ime_window_close_hover.png b/chromium/chrome/browser/resources/input_ime/ime_window_close_hover.png
index a954503b6d3..60a218cf395 100644
--- a/chromium/chrome/browser/resources/input_ime/ime_window_close_hover.png
+++ b/chromium/chrome/browser/resources/input_ime/ime_window_close_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/inspect/inspect.css b/chromium/chrome/browser/resources/inspect/inspect.css
index 5bd5a239edb..eefce85827b 100644
--- a/chromium/chrome/browser/resources/inspect/inspect.css
+++ b/chromium/chrome/browser/resources/inspect/inspect.css
@@ -178,6 +178,14 @@ img {
margin-left: 6px;
}
+.browser-fallback-note {
+ display: flex;
+ flex-flow: row wrap;
+ margin-left: 4px;
+ margin-top: 5px;
+ min-height: 15px;
+}
+
.used-for-port-forwarding {
background-image: url(../../../../ui/webui/resources/images/info.svg);
height: 15px;
diff --git a/chromium/chrome/browser/resources/inspect/inspect.js b/chromium/chrome/browser/resources/inspect/inspect.js
index 64288b83e80..1901d601dd6 100644
--- a/chromium/chrome/browser/resources/inspect/inspect.js
+++ b/chromium/chrome/browser/resources/inspect/inspect.js
@@ -7,12 +7,17 @@ var MIN_VERSION_TARGET_ID = 26;
var MIN_VERSION_NEW_TAB = 29;
var MIN_VERSION_TAB_ACTIVATE = 30;
var WEBRTC_SERIAL = 'WEBRTC';
+var HOST_CHROME_VERSION;
var queryParamsObject = {};
var browserInspector;
var browserInspectorTitle;
(function() {
+var chromeMatch = navigator.userAgent.match(/(?:^|\W)Chrome\/(\S+)/);
+if (chromeMatch && chromeMatch.length > 1)
+ HOST_CHROME_VERSION = chromeMatch[1].split('.').map(s => Number(s) || 0);
+
var queryParams = window.location.search;
if (!queryParams)
return;
@@ -31,6 +36,21 @@ if ('trace' in queryParamsObject || 'tracing' in queryParamsObject) {
}
})();
+function isVersionNewerThanHost(version) {
+ if (!HOST_CHROME_VERSION)
+ return false;
+ version = version.split('.').map(s => Number(s) || 0);
+ for (var i = 0; i < HOST_CHROME_VERSION.length; i++) {
+ if (i > version.length)
+ return false;
+ if (HOST_CHROME_VERSION[i] > version[i])
+ return false;
+ if (HOST_CHROME_VERSION[i] < version[i])
+ return true;
+ }
+ return false;
+}
+
function sendCommand(command, args) {
chrome.send(command, Array.prototype.slice.call(arguments, 1));
}
@@ -295,6 +315,8 @@ function populateRemoteTargets(devices) {
var majorChromeVersion = browser.adbBrowserChromeVersion;
var pageList;
var browserSection = $(browser.id);
+ var browserNeedsFallback =
+ isVersionNewerThanHost(browser.adbBrowserVersion);
if (browserSection) {
pageList = browserSection.querySelector('.pages');
} else {
@@ -320,6 +342,15 @@ function populateRemoteTargets(devices) {
}
browserSection.appendChild(browserHeader);
+ if (browserNeedsFallback) {
+ var browserFallbackNote = document.createElement('div');
+ browserFallbackNote.className = 'browser-fallback-note';
+ browserFallbackNote.textContent =
+ '\u26A0 Remote browser is newer than client browser. ' +
+ 'Try `inspect fallback` if inspection fails.';
+ browserSection.appendChild(browserFallbackNote);
+ }
+
if (majorChromeVersion >= MIN_VERSION_NEW_TAB) {
var newPage = document.createElement('div');
newPage.className = 'open';
@@ -401,6 +432,12 @@ function populateRemoteTargets(devices) {
row, 'close', sendTargetCommand.bind(null, 'close', page),
false);
}
+ if (browserNeedsFallback) {
+ addActionLink(
+ row, 'inspect fallback',
+ sendTargetCommand.bind(null, 'inspect-fallback', page),
+ page.hasNoUniqueId || page.adbAttachedForeign);
+ }
}
}
updateBrowserVisibility(browserSection);
diff --git a/chromium/chrome/browser/resources/local_discovery/local_discovery.html b/chromium/chrome/browser/resources/local_discovery/local_discovery.html
index 1ba6126155d..a99ecdcb5c5 100644
--- a/chromium/chrome/browser/resources/local_discovery/local_discovery.html
+++ b/chromium/chrome/browser/resources/local_discovery/local_discovery.html
@@ -26,6 +26,7 @@
<h1>$i18n{confirmRegistration}</h1>
<div class="dialog-contents">
<div id="register-message">
+ $i18nRaw{registerPrinterInformationMessage}
</div>
<div class="button-list">
@@ -38,10 +39,8 @@
</a>
</div>
<button class="register-cancel">$i18n{cancel}</button>
- <button id="register-continue-button">
- $i18n{serviceRegister}
- </button>
- </div>
+ <button id="register-continue">$i18n{confirm}</button>
+ </div>
</div>
</div>
diff --git a/chromium/chrome/browser/resources/local_discovery/local_discovery.js b/chromium/chrome/browser/resources/local_discovery/local_discovery.js
index 6906a948908..0b9bc336cc9 100644
--- a/chromium/chrome/browser/resources/local_discovery/local_discovery.js
+++ b/chromium/chrome/browser/resources/local_discovery/local_discovery.js
@@ -118,6 +118,7 @@ cr.define('local_discovery', function() {
deviceContainer: function() {
return $('register-device-list');
},
+
/**
* Register the device.
*/
@@ -133,11 +134,8 @@ cr.define('local_discovery', function() {
*/
showRegister: function() {
recordUmaEvent(DEVICES_PAGE_EVENTS.REGISTER_CLICKED);
- $('register-message').textContent = loadTimeData.getStringF(
- isPrinter(this.info.type) ? 'registerPrinterConfirmMessage' :
- 'registerDeviceConfirmMessage',
- this.info.display_name);
- $('register-continue-button').onclick = this.register.bind(this);
+ $('register-continue').onclick = this.register.bind(this);
+
showRegisterOverlay();
},
/**
@@ -521,7 +519,7 @@ cr.define('local_discovery', function() {
isUserLoggedIn || isUserSupervisedOrOffTheRecord;
$('register-overlay-login-promo').hidden =
isUserLoggedIn || isUserSupervisedOrOffTheRecord;
- $('register-continue-button').disabled =
+ $('register-continue').disabled =
!isUserLoggedIn || isUserSupervisedOrOffTheRecord;
$('my-devices-container').hidden = userSupervisedOrOffTheRecord;
diff --git a/chromium/chrome/browser/resources/local_ntp/local_ntp.css b/chromium/chrome/browser/resources/local_ntp/local_ntp.css
index 2b38a36fc16..343afa804b9 100644
--- a/chromium/chrome/browser/resources/local_ntp/local_ntp.css
+++ b/chromium/chrome/browser/resources/local_ntp/local_ntp.css
@@ -92,6 +92,8 @@ button {
display: flex;
flex-direction: column;
height: 100%;
+ position: relative;
+ z-index: 1;
}
#logo,
@@ -536,7 +538,6 @@ html[dir=rtl] #attribution,
right: 0;
top: 0;
transition: opacity 130ms;
- z-index: 1;
}
#one-google.hidden {
diff --git a/chromium/chrome/browser/resources/local_ntp/most_visited_single.js b/chromium/chrome/browser/resources/local_ntp/most_visited_single.js
index c4c0bfe2da7..9bdd106296f 100644
--- a/chromium/chrome/browser/resources/local_ntp/most_visited_single.js
+++ b/chromium/chrome/browser/resources/local_ntp/most_visited_single.js
@@ -27,35 +27,6 @@ var LOG_TYPE = {
/**
- * The different sources where an NTP tile's title can originate from.
- * Note: Keep in sync with components/ntp_tiles/tile_title_source.h
- * @enum {number}
- * @const
- */
-var TileTitleSource = {
- UNKNOWN: 0,
- MANIFEST: 1,
- META_TAG: 2,
- TITLE: 3,
- INFERRED: 4
-};
-
-
-/**
- * The different sources that an NTP tile can have.
- * Note: Keep in sync with components/ntp_tiles/tile_source.h
- * @enum {number}
- * @const
- */
-var TileSource = {
- TOP_SITES: 0,
- SUGGESTIONS_SERVICE: 1,
- POPULAR: 3,
- WHITELIST: 4,
-};
-
-
-/**
* The different (visual) types that an NTP tile can have.
* Note: Keep in sync with components/ntp_tiles/tile_visual_type.h
* @enum {number}
@@ -128,9 +99,11 @@ var logEvent = function(eventType) {
/**
* Log impression of an NTP tile.
* @param {number} tileIndex Position of the tile, >= 0 and < NUMBER_OF_TILES.
- * @param {number} tileTitleSource The title's source from TileTitleSource.
- * @param {number} tileSource The source from TileSource.
- * @param {number} tileType The type from TileVisualType.
+ * @param {number} tileTitleSource The source of the tile's title as received
+ * from getMostVisitedItemData.
+ * @param {number} tileSource The tile's source as received from
+ * getMostVisitedItemData.
+ * @param {number} tileType The tile's visual type from TileVisualType.
* @param {Date} dataGenerationTime Timestamp representing when the tile was
* produced by a ranking algorithm.
*/
@@ -143,9 +116,11 @@ function logMostVisitedImpression(
/**
* Log click on an NTP tile.
* @param {number} tileIndex Position of the tile, >= 0 and < NUMBER_OF_TILES.
- * @param {number} tileTitleSource The title's source from TileTitleSource.
- * @param {number} tileSource The source from TileSource.
- * @param {number} tileType The type from TileVisualType.
+ * @param {number} tileTitleSource The source of the tile's title as received
+ * from getMostVisitedItemData.
+ * @param {number} tileSource The tile's source as received from
+ * getMostVisitedItemData.
+ * @param {number} tileType The tile's visual type from TileVisualType.
* @param {Date} dataGenerationTime Timestamp representing when the tile was
* produced by a ranking algorithm.
*/
diff --git a/chromium/chrome/browser/resources/md_bookmarks/app.html b/chromium/chrome/browser/resources/md_bookmarks/app.html
index 64393cde7f8..82c01245dd4 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/app.html
+++ b/chromium/chrome/browser/resources/md_bookmarks/app.html
@@ -30,6 +30,7 @@
display: flex;
flex-direction: row;
flex-grow: 1;
+ overflow: hidden;
}
#splitter {
diff --git a/chromium/chrome/browser/resources/md_bookmarks/bookmarks.html b/chromium/chrome/browser/resources/md_bookmarks/bookmarks.html
index 6b15d439bb8..a12bb061aab 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/bookmarks.html
+++ b/chromium/chrome/browser/resources/md_bookmarks/bookmarks.html
@@ -6,6 +6,11 @@
<link rel="stylesheet" href="chrome://resources/css/text_defaults_md.css">
<link rel="stylesheet" href="chrome://resources/css/md_colors.css">
<style>
+ html {
+ /* Remove 300ms delay for 'click' event, when using touch interface. */
+ touch-action: manipulation;
+ }
+
html,
body {
background: var(--md-background-color);
diff --git a/chromium/chrome/browser/resources/md_bookmarks/command_manager.html b/chromium/chrome/browser/resources/md_bookmarks/command_manager.html
index 508b0501a43..13b59a66782 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/command_manager.html
+++ b/chromium/chrome/browser/resources/md_bookmarks/command_manager.html
@@ -2,6 +2,7 @@
<link rel="import" href="chrome://resources/cr_elements/cr_action_menu/cr_action_menu.html">
<link rel="import" href="chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.html">
+<link rel="import" href="chrome://resources/cr_elements/paper_button_style_css.html">
<link rel="import" href="chrome://resources/html/cr/ui/command.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
<link rel="import" href="chrome://bookmarks/dialog_focus_manager.html">
@@ -11,7 +12,7 @@
<dom-module id="bookmarks-command-manager">
<template>
- <style include="shared-style">
+ <style include="shared-style paper-button-style">
.label {
flex: 1;
}
@@ -30,7 +31,7 @@
<template is="cr-lazy-render" id="dropdown">
<dialog is="cr-action-menu" on-mousedown="onMenuMousedown_">
<template is="dom-repeat" items="[[menuCommands_]]" as="command">
- <button class="dropdown-item"
+ <button slot="item" class="dropdown-item"
command$="[[command]]"
hidden$="[[!isCommandVisible_(command, menuIds_)]]"
disabled$="[[!isCommandEnabled_(command, menuIds_)]]"
@@ -56,10 +57,10 @@
<div slot="title">$i18n{openDialogTitle}</div>
<div slot="body"></div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onOpenCancelTap_">
+ <paper-button class="cancel-button" on-click="onOpenCancelTap_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onOpenConfirmTap_">
+ <paper-button class="action-button" on-click="onOpenConfirmTap_">
$i18n{openDialogConfirm}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/md_bookmarks/command_manager.js b/chromium/chrome/browser/resources/md_bookmarks/command_manager.js
index 668a4497522..900da62a5f6 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/command_manager.js
+++ b/chromium/chrome/browser/resources/md_bookmarks/command_manager.js
@@ -58,21 +58,6 @@ cr.define('bookmarks', function() {
});
this.updateFromStore();
- /** @private {function(!Event)} */
- this.boundOnOpenCommandMenu_ = this.onOpenCommandMenu_.bind(this);
- document.addEventListener(
- 'open-command-menu', this.boundOnOpenCommandMenu_);
-
- /** @private {function()} */
- this.boundOnCommandUndo_ = () => {
- this.handle(Command.UNDO, new Set());
- };
- document.addEventListener('command-undo', this.boundOnCommandUndo_);
-
- /** @private {function(!Event)} */
- this.boundOnKeydown_ = this.onKeydown_.bind(this);
- document.addEventListener('keydown', this.boundOnKeydown_);
-
/** @private {!Map<Command, cr.ui.KeyboardShortcutList>} */
this.shortcuts_ = new Map();
@@ -92,14 +77,40 @@ cr.define('bookmarks', function() {
this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
+
+ /** @private {!Map<string, Function>} */
+ this.boundListeners_ = new Map();
+
+ const addDocumentListener = (eventName, handler) => {
+ assert(!this.boundListeners_.has(eventName));
+ const boundListener = handler.bind(this);
+ this.boundListeners_.set(eventName, boundListener);
+ document.addEventListener(eventName, boundListener);
+ };
+ addDocumentListener('open-command-menu', this.onOpenCommandMenu_);
+ addDocumentListener('keydown', this.onKeydown_);
+
+ const addDocumentListenerForCommand = (eventName, command) => {
+ addDocumentListener(eventName, (e) => {
+ if (e.path[0].tagName == 'INPUT')
+ return;
+
+ const items = this.getState().selection.items;
+ if (this.canExecute(command, items))
+ this.handle(command, items);
+ });
+ };
+ addDocumentListenerForCommand('command-undo', Command.UNDO);
+ addDocumentListenerForCommand('cut', Command.CUT);
+ addDocumentListenerForCommand('copy', Command.COPY);
+ addDocumentListenerForCommand('paste', Command.PASTE);
},
detached: function() {
CommandManager.instance_ = null;
- document.removeEventListener(
- 'open-command-menu', this.boundOnOpenCommandMenu_);
- document.removeEventListener('command-undo', this.boundOnCommandUndo_);
- document.removeEventListener('keydown', this.boundOnKeydown_);
+ this.boundListeners_.forEach(
+ (handler, eventName) =>
+ document.removeEventListener(eventName, handler));
},
/**
diff --git a/chromium/chrome/browser/resources/md_bookmarks/edit_dialog.html b/chromium/chrome/browser/resources/md_bookmarks/edit_dialog.html
index c9e6ce563d9..2be1feb461a 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/edit_dialog.html
+++ b/chromium/chrome/browser/resources/md_bookmarks/edit_dialog.html
@@ -2,6 +2,7 @@
<link rel="import" href="chrome://resources/html/assert.html">
<link rel="import" href="chrome://resources/cr_elements/cr_dialog/cr_dialog.html">
+<link rel="import" href="chrome://resources/cr_elements/paper_button_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/shared_style_css.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-input/paper-input.html">
@@ -9,7 +10,7 @@
<dom-module id="bookmarks-edit-dialog">
<template>
- <style include="cr-shared-style"></style>
+ <style include="cr-shared-style paper-button-style"></style>
<dialog is="cr-dialog" id="dialog">
<div slot="title">
[[getDialogTitle_(isFolder_, isEdit_)]]
@@ -30,11 +31,11 @@
</paper-input>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelButtonTap_">
+ <paper-button class="cancel-button" on-click="onCancelButtonTap_">
$i18n{cancel}
</paper-button>
<paper-button id="saveButton" class="action-button"
- on-tap="onSaveButtonTap_">
+ on-click="onSaveButtonTap_">
$i18n{saveEdit}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/md_bookmarks/folder_node.html b/chromium/chrome/browser/resources/md_bookmarks/folder_node.html
index e8d13978fb4..c1a85652d3b 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/folder_node.html
+++ b/chromium/chrome/browser/resources/md_bookmarks/folder_node.html
@@ -76,7 +76,7 @@
<div id="container"
class="v-centered"
- on-tap="selectFolder_"
+ on-click="selectFolder_"
on-dblclick="toggleFolder_"
on-contextmenu="onContextMenu_"
tabindex$="[[getTabIndex_(selectedFolder_, itemId)]]"
@@ -85,7 +85,7 @@
<template is="dom-if" if="[[hasChildFolder_]]">
<button is="paper-icon-button-light"
id="arrow"
- on-tap="toggleFolder_"
+ on-click="toggleFolder_"
on-mousedown="preventDefault_"
tabindex="-1"
aria-label$="[[getButtonAriaLabel_(isOpen, item_)]]">
@@ -98,7 +98,7 @@
open$="[[isSelectedFolder_]]"
no-children$="[[!hasChildFolder_]]">
</div>
- <div class="menu-label elided-text"" title="[[item_.title]]">
+ <div class="menu-label elided-text" title="[[item_.title]]">
[[item_.title]]
</div>
</div>
diff --git a/chromium/chrome/browser/resources/md_bookmarks/folder_node.js b/chromium/chrome/browser/resources/md_bookmarks/folder_node.js
index 78008a9655f..4b913c3ea09 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/folder_node.js
+++ b/chromium/chrome/browser/resources/md_bookmarks/folder_node.js
@@ -141,7 +141,7 @@ Polymer({
changeKeyboardSelection_: function(xDirection, yDirection, currentFocus) {
let newFocusFolderNode = null;
const isChildFolderNodeFocused =
- currentFocus.tagName == 'BOOKMARKS-FOLDER-NODE';
+ currentFocus && currentFocus.tagName == 'BOOKMARKS-FOLDER-NODE';
if (xDirection == 1) {
// The right arrow opens a folder if closed and goes to the first child
diff --git a/chromium/chrome/browser/resources/md_bookmarks/list.html b/chromium/chrome/browser/resources/md_bookmarks/list.html
index 1927916322b..ddbf28f533c 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/list.html
+++ b/chromium/chrome/browser/resources/md_bookmarks/list.html
@@ -21,7 +21,7 @@
}
#list {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
background-color: #fff;
margin: 0 auto;
max-width: var(--card-max-width);
diff --git a/chromium/chrome/browser/resources/md_bookmarks/list.js b/chromium/chrome/browser/resources/md_bookmarks/list.js
index 3b6aa847a67..11b1580b221 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/list.js
+++ b/chromium/chrome/browser/resources/md_bookmarks/list.js
@@ -142,7 +142,13 @@ Polymer({
/** @private */
emptyListMessage_: function() {
- const emptyListMessage = this.searchTerm_ ? 'noSearchResults' : 'emptyList';
+ let emptyListMessage = 'noSearchResults';
+ if (!this.searchTerm_) {
+ emptyListMessage = bookmarks.util.canReorderChildren(
+ this.getState(), this.getState().selectedFolder) ?
+ 'emptyList' :
+ 'emptyUnmodifiableList';
+ }
return loadTimeData.getString(emptyListMessage);
},
diff --git a/chromium/chrome/browser/resources/md_bookmarks/toast_manager.html b/chromium/chrome/browser/resources/md_bookmarks/toast_manager.html
index f94f6342f80..454d1f1ab70 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/toast_manager.html
+++ b/chromium/chrome/browser/resources/md_bookmarks/toast_manager.html
@@ -2,11 +2,12 @@
<link rel="import" href="chrome://resources/polymer/v1_0/iron-a11y-announcer/iron-a11y-announcer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/cr_elements/cr_toast/cr_toast.html">
+<link rel="import" href="chrome://resources/cr_elements/paper_button_style_css.html">
<link rel="import" href="chrome://bookmarks/shared_style.html">
<dom-module id="bookmarks-toast-manager">
<template>
- <style include="shared-style">
+ <style include="shared-style paper-button-style">
#content {
color: #fff;
display: flex;
@@ -17,9 +18,7 @@
-webkit-margin-end: 0;
-webkit-margin-start: 32px;
color: var(--google-blue-300);
- font-weight: 500;
height: 32px;
- min-width: 52px;
padding: 8px;
}
@@ -34,7 +33,7 @@
</style>
<cr-toast id="toast" duration="[[duration]]">
<div id="content" class="elided-text"></div>
- <paper-button id="button" hidden$="[[!showUndo_]]" on-tap="onUndoTap_">
+ <paper-button id="button" hidden$="[[!showUndo_]]" on-click="onUndoTap_">
$i18n{undo}
</paper-button>
</cr-toast>
diff --git a/chromium/chrome/browser/resources/md_bookmarks/toolbar.html b/chromium/chrome/browser/resources/md_bookmarks/toolbar.html
index 219c383cd75..4f8b2786f45 100644
--- a/chromium/chrome/browser/resources/md_bookmarks/toolbar.html
+++ b/chromium/chrome/browser/resources/md_bookmarks/toolbar.html
@@ -52,7 +52,7 @@
id="menuButton"
class="more-actions more-vert-button"
title="$i18n{organizeButtonTitle}"
- on-tap="onMenuButtonOpenTap_"
+ on-click="onMenuButtonOpenTap_"
aria-haspopup="menu">
<div></div>
<div></div>
diff --git a/chromium/chrome/browser/resources/md_downloads/1x/incognito_marker.png b/chromium/chrome/browser/resources/md_downloads/1x/incognito_marker.png
index 2668850b677..4d7b4f0457a 100644
--- a/chromium/chrome/browser/resources/md_downloads/1x/incognito_marker.png
+++ b/chromium/chrome/browser/resources/md_downloads/1x/incognito_marker.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/md_downloads/1x/no_downloads.png b/chromium/chrome/browser/resources/md_downloads/1x/no_downloads.png
index e445faebe3b..9cb8a7cab08 100644
--- a/chromium/chrome/browser/resources/md_downloads/1x/no_downloads.png
+++ b/chromium/chrome/browser/resources/md_downloads/1x/no_downloads.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/md_downloads/2x/incognito_marker.png b/chromium/chrome/browser/resources/md_downloads/2x/incognito_marker.png
index 3ee7c32a700..af60569dd0d 100644
--- a/chromium/chrome/browser/resources/md_downloads/2x/incognito_marker.png
+++ b/chromium/chrome/browser/resources/md_downloads/2x/incognito_marker.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/md_downloads/2x/no_downloads.png b/chromium/chrome/browser/resources/md_downloads/2x/no_downloads.png
index 90edf87a248..6181df50770 100644
--- a/chromium/chrome/browser/resources/md_downloads/2x/no_downloads.png
+++ b/chromium/chrome/browser/resources/md_downloads/2x/no_downloads.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/md_downloads/compiled_resources2.gyp b/chromium/chrome/browser/resources/md_downloads/compiled_resources2.gyp
index b9e2ca905c3..4adfce2fe86 100644
--- a/chromium/chrome/browser/resources/md_downloads/compiled_resources2.gyp
+++ b/chromium/chrome/browser/resources/md_downloads/compiled_resources2.gyp
@@ -75,7 +75,6 @@
'<(DEPTH)/ui/webui/resources/cr_elements/cr_action_menu/compiled_resources2.gyp:cr_action_menu',
'<(DEPTH)/ui/webui/resources/cr_elements/cr_toolbar/compiled_resources2.gyp:cr_toolbar',
'<(DEPTH)/third_party/polymer/v1_0/components-chromium/iron-a11y-announcer/compiled_resources2.gyp:iron-a11y-announcer-extracted',
- '<(DEPTH)/third_party/polymer/v1_0/components-chromium/paper-menu/compiled_resources2.gyp:paper-menu-extracted',
'browser_proxy',
'search_service',
],
diff --git a/chromium/chrome/browser/resources/md_downloads/downloads.html b/chromium/chrome/browser/resources/md_downloads/downloads.html
index bc126bb68c6..07eaf62e051 100644
--- a/chromium/chrome/browser/resources/md_downloads/downloads.html
+++ b/chromium/chrome/browser/resources/md_downloads/downloads.html
@@ -9,6 +9,9 @@
--downloads-card-margin: 24px;
--downloads-card-width: 680px;
background: #f1f1f1;
+
+ /* Remove 300ms delay for 'click' event, when using touch interface. */
+ touch-action: manipulation;
}
.loading {
diff --git a/chromium/chrome/browser/resources/md_downloads/item.html b/chromium/chrome/browser/resources/md_downloads/item.html
index a230d16a3d3..3696956b9a7 100644
--- a/chromium/chrome/browser/resources/md_downloads/item.html
+++ b/chromium/chrome/browser/resources/md_downloads/item.html
@@ -54,7 +54,7 @@
}
#content.is-active {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
}
#content:not(.is-active) {
@@ -244,7 +244,7 @@
<div id="title-area"><!--
Can't have any line breaks.
--><a is="action-link" id="file-link" href="[[data.url]]"
- on-tap="onFileLinkTap_"
+ on-click="onFileLinkTap_"
hidden="[[!completelyOnDisk_]]">[[data.file_name]]</a><!--
Before #name.
--><span id="name"
@@ -263,20 +263,20 @@
</template>
<div id="safe" class="controls" hidden="[[isDangerous_]]">
- <a is="action-link" id="show" on-tap="onShowTap_"
+ <a is="action-link" id="show" on-click="onShowTap_"
hidden="[[!completelyOnDisk_]]">$i18n{controlShowInFolder}</a>
<template is="dom-if" if="[[data.retry]]">
- <paper-button id="retry" on-tap="onRetryTap_">
+ <paper-button id="retry" on-click="onRetryTap_">
$i18n{controlRetry}
</paper-button>
</template>
<template is="dom-if" if="[[pauseOrResumeText_]]">
- <paper-button id="pause-or-resume" on-tap="onPauseOrResumeTap_">
+ <paper-button id="pause-or-resume" on-click="onPauseOrResumeTap_">
[[pauseOrResumeText_]]
</paper-button>
</template>
<template is="dom-if" if="[[showCancel_]]">
- <paper-button id="cancel" on-tap="onCancelTap_">
+ <paper-button id="cancel" on-click="onCancelTap_">
$i18n{controlCancel}
</paper-button>
</template>
@@ -287,17 +287,17 @@
<div id="dangerous" class="controls">
<!-- Dangerous file types (e.g. .exe, .jar). -->
<template is="dom-if" if="[[!isMalware_]]">
- <paper-button id="discard" on-tap="onDiscardDangerousTap_"
+ <paper-button id="discard" on-click="onDiscardDangerousTap_"
class="discard">$i18n{dangerDiscard}</paper-button>
- <paper-button id="save" on-tap="onSaveDangerousTap_"
+ <paper-button id="save" on-click="onSaveDangerousTap_"
class="keep">$i18n{dangerSave}</paper-button>
</template>
<!-- Things that safe browsing has determined to be dangerous. -->
<template is="dom-if" if="[[isMalware_]]">
- <paper-button id="danger-remove" on-tap="onDiscardDangerousTap_"
+ <paper-button id="danger-remove" on-click="onDiscardDangerousTap_"
class="discard">$i18n{controlRemoveFromList}</paper-button>
- <paper-button id="restore" on-tap="onSaveDangerousTap_"
+ <paper-button id="restore" on-click="onSaveDangerousTap_"
class="keep">$i18n{dangerRestore}</paper-button>
</template>
</div>
@@ -307,8 +307,9 @@
<div id="remove-wrapper" class="icon-wrapper">
<button is="paper-icon-button-light" id="remove"
title="$i18n{controlRemoveFromList}"
+ aria-label="$i18n{controlRemoveFromList}"
style$="[[computeRemoveStyle_(isDangerous_, showCancel_)]]"
- on-tap="onRemoveTap_">&#x2715;</button>
+ on-click="onRemoveTap_">&#x2715;</button>
</div>
<div id="incognito" title="$i18n{inIncognito}" hidden="[[!data.otr]]">
diff --git a/chromium/chrome/browser/resources/md_downloads/manager.html b/chromium/chrome/browser/resources/md_downloads/manager.html
index c8c68366be5..3662b4d2e1a 100644
--- a/chromium/chrome/browser/resources/md_downloads/manager.html
+++ b/chromium/chrome/browser/resources/md_downloads/manager.html
@@ -23,6 +23,7 @@
flex: 1 0;
flex-direction: column;
height: 100%;
+ overflow: hidden;
z-index: 0;
}
@@ -39,7 +40,7 @@
}
#drop-shadow {
- @apply(--cr-container-shadow);
+ @apply --cr-container-shadow;
}
:host([has-shadow_]) #drop-shadow {
diff --git a/chromium/chrome/browser/resources/md_downloads/toolbar.html b/chromium/chrome/browser/resources/md_downloads/toolbar.html
index 24209d11177..a98f6b02ca5 100644
--- a/chromium/chrome/browser/resources/md_downloads/toolbar.html
+++ b/chromium/chrome/browser/resources/md_downloads/toolbar.html
@@ -41,15 +41,17 @@
spinner-active="{{spinnerActive}}" on-search-changed="onSearchChanged_">
<button is="paper-icon-button-light" id="moreActions"
title="$i18n{moreActions}" class="dropdown-trigger"
- on-tap="onMoreActionsTap_">
+ on-click="onMoreActionsTap_">
<iron-icon icon="cr:more-vert"></iron-icon>
</button>
</cr-toolbar>
<dialog is="cr-action-menu" id="moreActionsMenu">
- <button class="dropdown-item clear-all" on-tap="onClearAllTap_">
+ <button slot="item" class="dropdown-item clear-all"
+ on-click="onClearAllTap_">
$i18n{clearAll}
- </div>
- <button class="dropdown-item" on-tap="onOpenDownloadsFolderTap_">
+ </button>
+ <button slot="item" class="dropdown-item"
+ on-click="onOpenDownloadsFolderTap_">
$i18n{openDownloadsFolder}
</button>
</dialog>
diff --git a/chromium/chrome/browser/resources/md_extensions/code_section.html b/chromium/chrome/browser/resources/md_extensions/code_section.html
index 093e9a62042..5d575bae3f5 100644
--- a/chromium/chrome/browser/resources/md_extensions/code_section.html
+++ b/chromium/chrome/browser/resources/md_extensions/code_section.html
@@ -2,6 +2,7 @@
<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
<link rel="import" href="chrome://resources/html/cr.html">
+<link rel="import" href="chrome://resources/html/i18n_behavior.html">
<link rel="import" href="chrome://resources/html/load_time_data.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-styles/color.html">
@@ -54,10 +55,16 @@
.more-code {
color: var(--paper-grey-500);
}
+
+ #highlight-description {
+ height: 0;
+ overflow: hidden;
+ }
</style>
- <div id="scroll-container" hidden="[[!codeText_]]">
+ <div id="scroll-container" hidden="[[!highlighted_]]">
<div id="main">
- <div id="line-numbers">
+ <!-- Line numbers are not useful to a screenreader -->
+ <div id="line-numbers" aria-hidden="true">
<div class="more-code before" hidden="[[!truncatedBefore_]]">
...
</div>
@@ -73,7 +80,14 @@
'$i18nPolymer{errorLinesNotShownSingular}',
'$i18nPolymer{errorLinesNotShownPlural}')]]
</div>
- <span>[[codeText_]]</span>
+ <span><!-- Whitespace is preserved in this span. Ignore new lines.
+ --><span>[[before_]]</span><!--
+ --><mark aria-label$="[[highlighted_]]"
+ aria-describedby="highlight-description"><!--
+ --><span aria-hidden="true">[[highlighted_]]</span><!--
+ --></mark><!--
+ --><span>[[after_]]</span><!--
+ --></span>
<div class="more-code after" hidden="[[!truncatedAfter_]]">
[[getLinesNotShownLabel_(
truncatedAfter_,
@@ -83,7 +97,10 @@
</div>
</div>
</div>
- <div id="no-code" hidden="[[codeText_]]">[[couldNotDisplayCode]]</div>
+ <div id="no-code" hidden="[[highlighted_]]">[[couldNotDisplayCode]]</div>
+ <div id="highlight-description" aria-hidden="true">
+ [[highlightDescription_]]
+ </div>
</template>
<script src="code_section.js"></script>
</dom-module>
diff --git a/chromium/chrome/browser/resources/md_extensions/code_section.js b/chromium/chrome/browser/resources/md_extensions/code_section.js
index 149d5d7d054..001dad3a045 100644
--- a/chromium/chrome/browser/resources/md_extensions/code_section.js
+++ b/chromium/chrome/browser/resources/md_extensions/code_section.js
@@ -21,6 +21,8 @@ cr.define('extensions', function() {
const CodeSection = Polymer({
is: 'extensions-code-section',
+ behaviors: [I18nBehavior],
+
properties: {
/**
* The code this object is displaying.
@@ -31,13 +33,17 @@ cr.define('extensions', function() {
value: null,
},
- /**
- * The text of the entire source file. This value does not update on
- * highlight changes; it only updates if the content of the source
- * changes.
- * @private
- */
- codeText_: String,
+ /** @private Highlighted code. */
+ highlighted_: String,
+
+ /** @private Code before the highlighted section. */
+ before_: String,
+
+ /** @private Code after the highlighted section. */
+ after_: String,
+
+ /** @private Description for the highlighted section. */
+ highlightDescription_: String,
/** @private */
lineNumbers_: String,
@@ -67,7 +73,10 @@ cr.define('extensions', function() {
if (!this.code ||
(!this.code.beforeHighlight && !this.code.highlight &&
!this.code.afterHighlight)) {
- this.codeText_ = '';
+ this.highlighted_ = '';
+ this.highlightDescription_ = '';
+ this.before_ = '';
+ this.after_ = '';
this.lineNumbers_ = '';
return;
}
@@ -91,15 +100,19 @@ cr.define('extensions', function() {
if (visibleAfter.charAt(visibleAfter.length - 1) == '\n')
visibleAfter += ' ';
- this.codeText_ = visibleBefore + highlight + visibleAfter;
+ this.highlighted_ = highlight;
+ this.highlightDescription_ = this.getAccessibilityHighlightDescription_(
+ linesBefore.length, highlight.split('\n').length);
+ this.before_ = visibleBefore;
+ this.after_ = visibleAfter;
this.truncatedBefore_ = linesBefore.length - visibleLineCountBefore;
this.truncatedAfter_ = linesAfter.length - visibleLineCountAfter;
+ let visibleCode = visibleBefore + highlight + visibleAfter;
+
this.setLineNumbers_(
this.truncatedBefore_ + 1,
- this.truncatedBefore_ + this.codeText_.split('\n').length);
- this.createHighlight_(
- visibleBefore.length, visibleBefore.length + highlight.length);
+ this.truncatedBefore_ + visibleCode.split('\n').length);
this.scrollToHighlight_(visibleLineCountBefore);
},
@@ -130,23 +143,6 @@ cr.define('extensions', function() {
},
/**
- * Uses the native text-selection API to highlight desired code.
- * @param {number} start
- * @param {number} end
- * @private
- */
- createHighlight_: function(start, end) {
- const range = document.createRange();
- const node = this.$.source.querySelector('span').firstChild;
- range.setStart(node, start);
- range.setEnd(node, end);
-
- const selection = window.getSelection();
- selection.removeAllRanges();
- selection.addRange(range);
- },
-
- /**
* @param {number} linesBeforeHighlight
* @private
*/
@@ -161,6 +157,22 @@ cr.define('extensions', function() {
this.$['scroll-container'].scrollTo({top: targetTop});
},
+
+ /**
+ * @param {number} lineStart
+ * @param {number} numLines
+ * @return {string}
+ * @private
+ */
+ getAccessibilityHighlightDescription_: function(lineStart, numLines) {
+ if (numLines > 1) {
+ return this.i18n(
+ 'accessibilityErrorMultiLine', lineStart.toString(),
+ (lineStart + numLines - 1).toString());
+ } else {
+ return this.i18n('accessibilityErrorLine', lineStart.toString());
+ }
+ },
});
return {CodeSection: CodeSection};
diff --git a/chromium/chrome/browser/resources/md_extensions/compiled_resources2.gyp b/chromium/chrome/browser/resources/md_extensions/compiled_resources2.gyp
index 74850796458..c4c7924b659 100644
--- a/chromium/chrome/browser/resources/md_extensions/compiled_resources2.gyp
+++ b/chromium/chrome/browser/resources/md_extensions/compiled_resources2.gyp
@@ -7,6 +7,7 @@
'target_name': 'code_section',
'dependencies': [
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:i18n_behavior',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data',
'<(EXTERNS_GYP):developer_private',
],
@@ -50,7 +51,6 @@
{
'target_name': 'error_page',
'dependencies': [
- '<(DEPTH)/third_party/polymer/v1_0/components-chromium/paper-menu/compiled_resources2.gyp:paper-menu-extracted',
'<(DEPTH)/ui/webui/resources/cr_elements/compiled_resources2.gyp:cr_container_shadow_behavior',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr',
'<(DEPTH)/ui/webui/resources/js/cr/ui/compiled_resources2.gyp:focus_outline_manager',
diff --git a/chromium/chrome/browser/resources/md_extensions/detail_view.html b/chromium/chrome/browser/resources/md_extensions/detail_view.html
index a111266af18..f730ba9363d 100644
--- a/chromium/chrome/browser/resources/md_extensions/detail_view.html
+++ b/chromium/chrome/browser/resources/md_extensions/detail_view.html
@@ -55,7 +55,7 @@
}
#main {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
background-color: white;
margin: auto;
min-height: 100%;
@@ -80,7 +80,7 @@
#name {
flex-grow: 1;
- @apply(--cr-title-text);
+ @apply --cr-title-text;
}
#learn-more-link {
@@ -93,7 +93,7 @@
}
.section {
- @apply(--cr-section);
+ @apply --cr-section;
}
.section.block {
@@ -144,6 +144,10 @@
width: 78px;
}
+ #reload-button {
+ color: var(--google-blue-500);
+ }
+
.warning div {
display: flex;
}
@@ -155,9 +159,20 @@
.warning-icon {
--iron-icon-fill-color: var(--paper-red-700);
- --iron-icon-height: 19px;
- --iron-icon-width: 19px;
- margin-right: 16px;
+ -webkit-margin-end: 16px;
+ height: 19px;
+ width: 19px;
+ }
+
+ #error-icon {
+ --iron-icon-fill-color: var(--google-red-700);
+ -webkit-margin-end: 4px;
+ height: 18px;
+ width: 18px;
+ }
+
+ #runtime-warnings {
+ color: var(--google-red-700);
}
ul {
@@ -187,7 +202,7 @@
}
paper-spinner-lite {
- @apply(--cr-icon-height-width);
+ @apply --cr-icon-height-width;
}
</style>
<div id="container">
@@ -195,7 +210,7 @@
<div id="top-bar">
<button id="close-button" is="paper-icon-button-light"
aria-label="$i18n{back}" class="icon-arrow-back no-overlap"
- on-tap="onCloseButtonTap_"></button>
+ on-click="onCloseButtonTap_"></button>
<img id="icon" src="[[data.iconUrl]]"
alt$="[[appOrExtension(
data.type,
@@ -227,6 +242,19 @@
</div>
</div>
<div id="warnings" hidden$="[[!hasWarnings_(data.*)]]">
+ <div id="runtime-warnings" aria-describedby="a11yAssociation"
+ hidden$="[[!data.runtimeWarnings.length]]"
+ class="section continuation control-line">
+ <div>
+ <iron-icon id="error-icon" icon="error"></iron-icon>
+ <template is="dom-repeat" items="[[data.runtimeWarnings]]">
+ [[item]]
+ </template>
+ </div>
+ <paper-button id="reload-button" on-click="onReloadTap_">
+ $i18n{itemReload}
+ </paper-button>
+ </div>
<div class="section continuation warning" id="suspicious-warning"
hidden$="[[!data.disableReasons.suspiciousInstall]]">
<div>
@@ -247,7 +275,7 @@
<span>$i18n{itemCorruptInstall}</span>
</div>
<paper-button id="repair-button" class="action-button"
- on-tap="onRepairTap_">
+ on-click="onRepairTap_">
$i18n{itemRepair}
</paper-button>
</div>
@@ -299,7 +327,7 @@
<template is="dom-repeat" items="[[data.views]]">
<li>
<a is="action-link" class="inspectable-view"
- on-tap="onInspectTap_">
+ on-click="onInspectTap_">
[[computeInspectLabel_(item)]]
</a>
</li>
@@ -382,15 +410,15 @@
disabled="[[!isEnabled_(data.state)]]"
hidden="[[!shouldShowOptionsLink_(data.*)]]"
icon-class="icon-external" label="$i18n{itemOptions}"
- on-tap="onExtensionOptionsTap_">
+ on-click="onExtensionOptionsTap_">
</button>
<button class="hr" hidden="[[!data.manifestHomePageUrl.length]]"
is="cr-link-row" icon-class="icon-external" id="extensionWebsite"
- label="$i18n{extensionWebsite}" on-tap="onExtensionWebSiteTap_">
+ label="$i18n{extensionWebsite}" on-click="onExtensionWebSiteTap_">
</button>
<button class="hr" hidden="[[!data.webStoreUrl.length]]"
is="cr-link-row" icon-class="icon-external" id="viewInStore"
- label="$i18n{viewInStore}" on-tap="onViewInStoreTap_">
+ label="$i18n{viewInStore}" on-click="onViewInStoreTap_">
</button>
<div class="section block">
<div class="section-title">$i18n{itemSource}</div>
@@ -400,7 +428,7 @@
<div id="load-path" class="section-content"
hidden$="[[!data.prettifiedPath]]">
<span>$i18n{itemExtensionPath}</span>
- <a is="action-link" on-tap="onLoadPathTap_">
+ <a is="action-link" on-click="onLoadPathTap_">
[[data.prettifiedPath]]
</a>
</div>
@@ -408,7 +436,7 @@
<button class="hr" is="cr-link-row"
hidden="[[isControlled_(data.controlledInfo)]]"
icon-class="subpage-arrow" id="remove-extension"
- label="$i18n{itemRemoveExtension}" on-tap="onRemoveTap_">
+ label="$i18n{itemRemoveExtension}" on-click="onRemoveTap_">
</button>
</div>
</div>
diff --git a/chromium/chrome/browser/resources/md_extensions/detail_view.js b/chromium/chrome/browser/resources/md_extensions/detail_view.js
index 6115bc07b77..364eb45cf4a 100644
--- a/chromium/chrome/browser/resources/md_extensions/detail_view.js
+++ b/chromium/chrome/browser/resources/md_extensions/detail_view.js
@@ -101,7 +101,8 @@ cr.define('extensions', function() {
hasWarnings_: function() {
return this.data.disableReasons.corruptInstall ||
this.data.disableReasons.suspiciousInstall ||
- this.data.disableReasons.updateRequired || !!this.data.blacklistText;
+ this.data.disableReasons.updateRequired ||
+ !!this.data.blacklistText || this.data.runtimeWarnings.length > 0;
},
/**
@@ -179,6 +180,13 @@ cr.define('extensions', function() {
},
/** @private */
+ onReloadTap_: function() {
+ this.delegate.reloadItem(this.data.id).catch(loadError => {
+ this.fire('load-error', loadError);
+ });
+ },
+
+ /** @private */
onRemoveTap_: function() {
this.delegate.deleteItem(this.data.id);
},
diff --git a/chromium/chrome/browser/resources/md_extensions/error_page.html b/chromium/chrome/browser/resources/md_extensions/error_page.html
index 2cae6446d16..e872d27f2a3 100644
--- a/chromium/chrome/browser/resources/md_extensions/error_page.html
+++ b/chromium/chrome/browser/resources/md_extensions/error_page.html
@@ -31,7 +31,7 @@
iron-icon {
--iron-icon-fill-color: var(--paper-grey-500);
- @apply(--cr-icon-height-width);
+ @apply --cr-icon-height-width;
flex-shrink: 0;
}
@@ -47,7 +47,7 @@
* detail_view.html and error_page.html. Refactor such that no duplication
* happens.*/
#main {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
background-color: white;
margin: auto;
min-height: 100%;
@@ -64,7 +64,7 @@
height: 40px;
margin-bottom: 30px;
padding: 8px 12px 0;
- @apply(--cr-title-text);
+ @apply --cr-title-text;
}
#heading span {
@@ -77,7 +77,7 @@
}
.error-item {
- @apply(--cr-section);
+ @apply --cr-section;
padding-left: 0;
}
@@ -108,7 +108,7 @@
}
.details-heading {
- @apply(--cr-title-text);
+ @apply --cr-title-text;
align-items: center;
display: flex;
height: var(--cr-section-min-height);
@@ -177,10 +177,10 @@
<div id="heading">
<button id="close-button" is="paper-icon-button-light"
aria-label="$i18n{back}"
- class="icon-arrow-back no-overlap" on-tap="onCloseButtonTap_">
+ class="icon-arrow-back no-overlap" on-click="onCloseButtonTap_">
</button>
<span>$i18n{errorsPageHeading}</span>
- <paper-button on-tap="onClearAllTap_" hidden="[[!entries_.length]]">
+ <paper-button on-click="onClearAllTap_" hidden="[[!entries_.length]]">
$i18n{clearAll}
</paper-button>
</div>
@@ -190,7 +190,7 @@
<div class="item-container">
<div class$="error-item
[[computeErrorClass_(item, selectedEntry_)]]">
- <div actionable class=" start" on-tap="onErrorItemAction_"
+ <div actionable class=" start" on-click="onErrorItemAction_"
on-keydown="onErrorItemAction_" tabindex="0"
role="button">
<iron-icon icon$="[[computeErrorIcon_(item)]]"
@@ -204,7 +204,7 @@
</div>
<div class="separator"></div>
<button is="paper-icon-button-light" class="icon-delete-gray"
- on-tap="onDeleteErrorAction_"
+ on-click="onDeleteErrorAction_"
aria-describedby$="[[item.id]]"
aria-label="$i18n{clearEntry}"
on-keydown="onDeleteErrorAction_">
@@ -226,7 +226,7 @@
</div>
<ul class="stack-trace-container">
<template is="dom-repeat" items="[[item.stackTrace]]">
- <li on-tap="onStackFrameTap_"
+ <li on-click="onStackFrameTap_"
hidden="[[!shouldDisplayFrame_(item.url)]]"
class$="[[getStackFrameClass_(item,
selectedStackFrame_)]]">
@@ -244,7 +244,7 @@
<paper-button class="devtool-button action-button"
hidden$="[[!computeIsRuntimeError_(item)]]"
disabled="[[!item.canInspect]]"
- on-tap="onDevToolButtonTap_">
+ on-click="onDevToolButtonTap_">
$i18n{openInDevtool}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/md_extensions/extensions.html b/chromium/chrome/browser/resources/md_extensions/extensions.html
index 5b728b9ca9d..5b851ed3990 100644
--- a/chromium/chrome/browser/resources/md_extensions/extensions.html
+++ b/chromium/chrome/browser/resources/md_extensions/extensions.html
@@ -12,6 +12,9 @@
/* --md-background-color in disguise. Not using the var for increased
* performance. */
background-color: #f1f1f1;
+
+ /* Remove 300ms delay for 'click' event, when using touch interface. */
+ touch-action: manipulation;
}
.loading {
diff --git a/chromium/chrome/browser/resources/md_extensions/icons.html b/chromium/chrome/browser/resources/md_extensions/icons.html
index 34714cc4e7b..f05904be4f3 100644
--- a/chromium/chrome/browser/resources/md_extensions/icons.html
+++ b/chromium/chrome/browser/resources/md_extensions/icons.html
@@ -57,4 +57,4 @@
</g>
</defs>
</svg>
-</iron-icon-set>
+</iron-iconset-svg>
diff --git a/chromium/chrome/browser/resources/md_extensions/install_warnings_dialog.html b/chromium/chrome/browser/resources/md_extensions/install_warnings_dialog.html
index d50ce39ff17..286b25e54d6 100644
--- a/chromium/chrome/browser/resources/md_extensions/install_warnings_dialog.html
+++ b/chromium/chrome/browser/resources/md_extensions/install_warnings_dialog.html
@@ -30,7 +30,7 @@
</ul>
</div>
<div slot="button-container">
- <paper-button class="action-button" on-tap="onOkTap_">
+ <paper-button class="action-button" on-click="onOkTap_">
$i18n{ok}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/md_extensions/item.html b/chromium/chrome/browser/resources/md_extensions/item.html
index f9f2d770906..f2fe3442f46 100644
--- a/chromium/chrome/browser/resources/md_extensions/item.html
+++ b/chromium/chrome/browser/resources/md_extensions/item.html
@@ -60,7 +60,7 @@
}
#card {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
background: white;
border-radius: 2px;
display: flex;
@@ -90,7 +90,7 @@
}
#name-and-version {
- @apply(--cr-primary-text);
+ @apply --cr-primary-text;
margin-bottom: 4px;
}
@@ -108,6 +108,13 @@
margin-bottom: 8px;
}
+ #error-icon {
+ --iron-icon-fill-color: var(--google-red-700);
+ -webkit-margin-end: 4px;
+ height: 18px;
+ width: 18px;
+ }
+
#description,
#version,
#extension-id,
@@ -177,7 +184,7 @@
paper-tooltip {
--paper-tooltip: {
- @apply(--cr-tooltip);
+ @apply --cr-tooltip;
min-width: 0;
};
}
@@ -255,28 +262,29 @@
[[data.description]]
</div>
<template is="dom-if" if="[[hasWarnings_(data.*)]]">
- <div id="warnings" >
- <div id="runtime-warnings" aria-describedby="a11yAssociation"
+ <div id="warnings">
+ <iron-icon id="error-icon" icon="error"></iron-icon>
+ <span id="runtime-warnings" aria-describedby="a11yAssociation"
hidden$="[[!data.runtimeWarnings.length]]">
<template is="dom-repeat" items="[[data.runtimeWarnings]]">
[[item]]
</template>
- </div>
- <div id="suspicious-warning" aria-describedby="a11yAssociation"
+ </span>
+ <span id="suspicious-warning" aria-describedby="a11yAssociation"
hidden$="[[!data.disableReasons.suspiciousInstall]]">
$i18n{itemSuspiciousInstall}
<a target="_blank" id="learn-more-link"
href="$i18n{suspiciousInstallHelpUrl}">
$i18n{learnMore}
</a>
- </div>
- <div id="corrupted-warning" aria-describedby="a11yAssociation"
+ </span>
+ <span id="corrupted-warning" aria-describedby="a11yAssociation"
hidden$="[[!data.disableReasons.corruptInstall]]">
$i18n{itemCorruptInstall}
- </div>
- <div id="blacklisted-warning"><!-- No whitespace
+ </span>
+ <span id="blacklisted-warning"><!-- No whitespace
-->[[data.blacklistText]]<!-- so we can use :empty in css.
- --></div>
+ --></span>
</div>
</template>
<template is="dom-if" if="[[inDevMode]]">
@@ -292,12 +300,12 @@
</span>
<a class="clippable-flex-text" is="action-link"
title="[[computeFirstInspectTitle_(data.views)]]"
- on-tap="onInspectTap_">
+ on-click="onInspectTap_">
[[computeFirstInspectLabel_(data.views)]]
</a>
<a is="action-link"
hidden$="[[computeExtraViewsHidden_(data.views)]]"
- on-tap="onExtraInspectTap_">
+ on-click="onExtraInspectTap_">
[[computeExtraInspectLabel_(data.views)]]
</a>
</div>
@@ -308,17 +316,17 @@
</div>
<div id="button-strip" class="layout horizontal center">
<div class="layout flex horizontal center">
- <paper-button id="details-button" on-tap="onDetailsTap_"
+ <paper-button id="details-button" on-click="onDetailsTap_"
aria-describedby="a11yAssociation">
$i18n{itemDetails}
</paper-button>
- <paper-button id="remove-button" on-tap="onRemoveTap_"
+ <paper-button id="remove-button" on-click="onRemoveTap_"
aria-describedby="a11yAssociation"
hidden="[[isControlled_(data.controlledInfo)]]">
$i18n{itemRemove}
</paper-button>
<template is="dom-if" if="[[shouldShowErrorsButton_(data.*)]]">
- <paper-button id="errors-button" on-tap="onErrorsTap_"
+ <paper-button id="errors-button" on-click="onErrorsTap_"
aria-describedby="a11yAssociation">
$i18n{itemErrors}
</paper-button>
@@ -327,22 +335,22 @@
<template is="dom-if" if="[[!computeDevReloadButtonHidden_(data.*)]]">
<button id="dev-reload-button" is="paper-icon-button-light"
aria-label="$i18n{itemReload}" aria-describedby="a11yAssociation"
- class="icon-refresh no-overlap" on-tap="onReloadTap_">
+ class="icon-refresh no-overlap" on-click="onReloadTap_">
</button>
</template>
<template is="dom-if" if="[[data.disableReasons.corruptInstall]]">
<paper-button id="repair-button" class="action-button"
- aria-describedby="a11yAssociation" on-tap="onRepairTap_">
+ aria-describedby="a11yAssociation" on-click="onRepairTap_">
$i18n{itemRepair}
</paper-button>
</template>
<template is="dom-if" if="[[isTerminated_(data.state)]]">
- <paper-button id="terminated-reload-button" on-tap="onReloadTap_"
+ <paper-button id="terminated-reload-button" on-click="onReloadTap_"
aria-describedby="a11yAssociation" class="action-button">
$i18n{itemReload}
</paper-button>
</template>
- <cr-toggle id="enable-toggle" class="action-button"
+ <cr-toggle id="enable-toggle"
aria-label$="[[appOrExtension(
data.type,
'$i18nPolymer{appEnabled}',
diff --git a/chromium/chrome/browser/resources/md_extensions/item.js b/chromium/chrome/browser/resources/md_extensions/item.js
index 116f96d50ea..bdf69ab5bf5 100644
--- a/chromium/chrome/browser/resources/md_extensions/item.js
+++ b/chromium/chrome/browser/resources/md_extensions/item.js
@@ -109,7 +109,12 @@ cr.define('extensions', function() {
/** @private string */
a11yAssociation_: function() {
- return this.i18n('extensionA11yAssociation', this.data.name);
+ // Don't use I18nBehavior.i18n because of additional checks it performs.
+ // Polymer ensures that this string is not stamped into arbitrary HTML.
+ // |this.data.name| can contain any data including html tags.
+ // ex: "My <video> download extension!"
+ return loadTimeData.getStringF(
+ 'extensionA11yAssociation', this.data.name);
},
/** @private */
diff --git a/chromium/chrome/browser/resources/md_extensions/item_list.html b/chromium/chrome/browser/resources/md_extensions/item_list.html
index 2ac11babaa0..849590719b1 100644
--- a/chromium/chrome/browser/resources/md_extensions/item_list.html
+++ b/chromium/chrome/browser/resources/md_extensions/item_list.html
@@ -49,7 +49,7 @@
}
#app-title {
- @apply(--cr-section-text);
+ @apply --cr-section-text;
margin-bottom: 12px;
margin-top: 21px;
}
@@ -61,7 +61,7 @@
<div id="no-items" class="empty-list-message"
hidden$="[[!shouldShowEmptyItemsMessage_(
apps.length, extensions.length)]]">
- <span on-tap="onNoExtensionsTap_">$i18nRaw{noExtensionsOrApps}</span>
+ <span on-click="onNoExtensionsTap_">$i18nRaw{noExtensionsOrApps}</span>
</div>
<div id="no-search-results" class="empty-list-message"
hidden$="[[!shouldShowEmptySearchMessage_(
diff --git a/chromium/chrome/browser/resources/md_extensions/item_util.js b/chromium/chrome/browser/resources/md_extensions/item_util.js
index aaefd16eb47..5f4aac7e18c 100644
--- a/chromium/chrome/browser/resources/md_extensions/item_util.js
+++ b/chromium/chrome/browser/resources/md_extensions/item_util.js
@@ -24,6 +24,7 @@ cr.define('extensions', function() {
case chrome.developerPrivate.ExtensionState.ENABLED:
case chrome.developerPrivate.ExtensionState.TERMINATED:
return true;
+ case chrome.developerPrivate.ExtensionState.BLACKLISTED:
case chrome.developerPrivate.ExtensionState.DISABLED:
return false;
}
diff --git a/chromium/chrome/browser/resources/md_extensions/keyboard_shortcuts.html b/chromium/chrome/browser/resources/md_extensions/keyboard_shortcuts.html
index cb443c889fc..5bdefc60481 100644
--- a/chromium/chrome/browser/resources/md_extensions/keyboard_shortcuts.html
+++ b/chromium/chrome/browser/resources/md_extensions/keyboard_shortcuts.html
@@ -74,15 +74,15 @@
.icon {
-webkit-margin-end: 20px;
- height: 16px;
- width: 16px;
+ height: 20px;
+ width: 20px;
}
.card-controls {
/* We line up the controls with the name, which is after the
- 20px left padding + 16px icon + 20px margin on the icon. */
+ 20px left padding + 20px icon + 20px margin on the icon. */
-webkit-margin-end: 20px;
- -webkit-margin-start: 56px;
+ -webkit-margin-start: 60px;
}
</style>
<div id="container">
diff --git a/chromium/chrome/browser/resources/md_extensions/kiosk_dialog.html b/chromium/chrome/browser/resources/md_extensions/kiosk_dialog.html
index 2709a38694b..fa86c94ec05 100644
--- a/chromium/chrome/browser/resources/md_extensions/kiosk_dialog.html
+++ b/chromium/chrome/browser/resources/md_extensions/kiosk_dialog.html
@@ -95,13 +95,13 @@
</div>
<div class="item-controls">
<paper-button hidden="[[!canEditAutoLaunch_]]"
- on-tap="onAutoLaunchButtonTap_">
+ on-click="onAutoLaunchButtonTap_">
[[getAutoLaunchButtonLabel_(item.autoLaunch,
'$i18nPolymer{kioskDisableAutoLaunch}',
'$i18nPolymer{kioskEnableAutoLaunch}')]]
</paper-button>
<button is="paper-icon-button-light" class="icon-delete-gray"
- on-tap="onDeleteAppTap_"></button>
+ on-click="onDeleteAppTap_"></button>
</div>
</div>
</template>
@@ -114,7 +114,7 @@
'$i18nPolymer{kioskInvalidApp}', errorAppId_)]]"
on-keydown="clearInputInvalid_">
</paper-input>
- <paper-button id="add-button" on-tap="onAddAppTap_"
+ <paper-button id="add-button" on-click="onAddAppTap_"
disabled="[[!addAppInput_]]">
$i18n{add}
</paper-button>
@@ -126,7 +126,7 @@
</paper-checkbox>
</div>
<div slot="button-container">
- <paper-button class="action-button" on-tap="onDoneTap_">
+ <paper-button class="action-button" on-click="onDoneTap_">
$i18n{done}
</paper-button>
</div>
@@ -136,10 +136,12 @@
<div slot="title">$i18n{kioskDisableBailoutWarningTitle}</div>
<div slot="body">$i18n{kioskDisableBailoutWarningBody}</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onBailoutDialogCancelTap_">
+ <paper-button class="cancel-button"
+ on-click="onBailoutDialogCancelTap_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onBailoutDialogConfirmTap_">
+ <paper-button class="action-button"
+ on-click="onBailoutDialogConfirmTap_">
$i18n{confirm}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/md_extensions/load_error.html b/chromium/chrome/browser/resources/md_extensions/load_error.html
index e5de72a0497..10080e154d5 100644
--- a/chromium/chrome/browser/resources/md_extensions/load_error.html
+++ b/chromium/chrome/browser/resources/md_extensions/load_error.html
@@ -40,11 +40,11 @@
</div>
<div slot="button-container">
<paper-spinner-lite active="[[retrying_]]"></paper-spinner-lite>
- <paper-button class="cancel-button" on-tap="close">
+ <paper-button class="cancel-button" on-click="close">
$i18n{cancel}
</paper-button>
<paper-button class="action-button" disabled="[[retrying_]]"
- on-tap="onRetryTap_">
+ on-click="onRetryTap_">
$i18n{loadErrorRetry}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/md_extensions/options_dialog.html b/chromium/chrome/browser/resources/md_extensions/options_dialog.html
index 3c959b13644..5c5abc51db6 100644
--- a/chromium/chrome/browser/resources/md_extensions/options_dialog.html
+++ b/chromium/chrome/browser/resources/md_extensions/options_dialog.html
@@ -23,12 +23,13 @@
ExtensionOptions {
display: block;
min-width: 300px;
+ overflow: hidden;
}
dialog {
+ --scroll-border: 0;
width: fit-content;
--cr-dialog-body: {
- overflow: hidden;
padding: 0;
};
diff --git a/chromium/chrome/browser/resources/md_extensions/options_dialog.js b/chromium/chrome/browser/resources/md_extensions/options_dialog.js
index adffadd9da2..d9ecf142ee3 100644
--- a/chromium/chrome/browser/resources/md_extensions/options_dialog.js
+++ b/chromium/chrome/browser/resources/md_extensions/options_dialog.js
@@ -5,6 +5,25 @@
cr.define('extensions', function() {
'use strict';
+ /**
+ * @return {!Promise} A signal that the document is ready. Need to wait for
+ * this, otherwise the custom ExtensionOptions element might not have been
+ * registered yet.
+ */
+ function whenDocumentReady() {
+ if (document.readyState == 'complete')
+ return Promise.resolve();
+
+ return new Promise(function(resolve) {
+ document.addEventListener('readystatechange', function f() {
+ if (document.readyState == 'complete') {
+ document.removeEventListener('readystatechange', f);
+ resolve();
+ }
+ });
+ });
+ }
+
const OptionsDialog = Polymer({
is: 'extensions-options-dialog',
@@ -25,21 +44,23 @@ cr.define('extensions', function() {
/** @param {chrome.developerPrivate.ExtensionInfo} data */
show: function(data) {
this.data_ = data;
- if (!this.extensionOptions_)
- this.extensionOptions_ = document.createElement('ExtensionOptions');
- this.extensionOptions_.extension = this.data_.id;
- this.extensionOptions_.onclose = this.close.bind(this);
+ whenDocumentReady().then(() => {
+ if (!this.extensionOptions_)
+ this.extensionOptions_ = document.createElement('ExtensionOptions');
+ this.extensionOptions_.extension = this.data_.id;
+ this.extensionOptions_.onclose = this.close.bind(this);
- const onSizeChanged = e => {
- this.extensionOptions_.style.height = e.height + 'px';
- this.extensionOptions_.style.width = e.width + 'px';
+ const onSizeChanged = e => {
+ this.extensionOptions_.style.height = e.height + 'px';
+ this.extensionOptions_.style.width = e.width + 'px';
- if (!this.$$('dialog').open)
- this.$$('dialog').showModal();
- };
+ if (!this.$$('dialog').open)
+ this.$$('dialog').showModal();
+ };
- this.extensionOptions_.onpreferredsizechanged = onSizeChanged;
- this.$.body.appendChild(this.extensionOptions_);
+ this.extensionOptions_.onpreferredsizechanged = onSizeChanged;
+ this.$.body.appendChild(this.extensionOptions_);
+ });
},
close: function() {
diff --git a/chromium/chrome/browser/resources/md_extensions/pack_dialog.html b/chromium/chrome/browser/resources/md_extensions/pack_dialog.html
index 1ee1ae2c232..e984b85658b 100644
--- a/chromium/chrome/browser/resources/md_extensions/pack_dialog.html
+++ b/chromium/chrome/browser/resources/md_extensions/pack_dialog.html
@@ -36,7 +36,7 @@
<paper-input id="root-dir" label="$i18n{packDialogExtensionRoot}"
always-float-label value="{{packDirectory_}}">
</paper-input>
- <paper-button id="root-dir-browse" on-tap="onRootBrowse_">
+ <paper-button id="root-dir-browse" on-click="onRootBrowse_">
$i18n{packDialogBrowse}
</paper-button>
</div>
@@ -44,16 +44,16 @@
<paper-input id="key-file" label="$i18n{packDialogKeyFile}"
always-float-label value="{{keyFile_}}">
</paper-input>
- <paper-button id="key-file-browse" on-tap="onKeyBrowse_">
+ <paper-button id="key-file-browse" on-click="onKeyBrowse_">
$i18n{packDialogBrowse}
</paper-button>
</div>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelTap_">
+ <paper-button class="cancel-button" on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onConfirmTap_"
+ <paper-button class="action-button" on-click="onConfirmTap_"
disabled="[[!packDirectory_]]">
$i18n{packDialogConfirm}
</paper-button>
diff --git a/chromium/chrome/browser/resources/md_extensions/pack_dialog_alert.html b/chromium/chrome/browser/resources/md_extensions/pack_dialog_alert.html
index 68bc231c581..ebf69a90b1b 100644
--- a/chromium/chrome/browser/resources/md_extensions/pack_dialog_alert.html
+++ b/chromium/chrome/browser/resources/md_extensions/pack_dialog_alert.html
@@ -22,10 +22,10 @@
<div class="body" slot="body">[[model.message]]</div>
<div class="button-container" slot="button-container">
<paper-button class$="[[getCancelButtonClass_(confirmLabel_)]]"
- on-tap="onCancelTap_" hidden="[[!cancelLabel_]]">
+ on-click="onCancelTap_" hidden="[[!cancelLabel_]]">
[[cancelLabel_]]
</paper-button>
- <paper-button class="action-button" on-tap="onConfirmTap_"
+ <paper-button class="action-button" on-click="onConfirmTap_"
hidden="[[!confirmLabel_]]">
[[confirmLabel_]]
</paper-button>
diff --git a/chromium/chrome/browser/resources/md_extensions/shortcut_input.html b/chromium/chrome/browser/resources/md_extensions/shortcut_input.html
index ad5c9bb03b9..94b9d0d31f0 100644
--- a/chromium/chrome/browser/resources/md_extensions/shortcut_input.html
+++ b/chromium/chrome/browser/resources/md_extensions/shortcut_input.html
@@ -46,7 +46,7 @@
no-label-float>
</paper-input>
<button id="clear" is="paper-icon-button-light"
- class="icon-clear no-overlap" on-tap="onClearTap_"
+ class="icon-clear no-overlap" on-click="onClearTap_"
hidden$="[[computeClearHidden_(capturing_, shortcut)]]">
</button>
</div>
diff --git a/chromium/chrome/browser/resources/md_extensions/sidebar.html b/chromium/chrome/browser/resources/md_extensions/sidebar.html
index a8857075a54..5e89ab45503 100644
--- a/chromium/chrome/browser/resources/md_extensions/sidebar.html
+++ b/chromium/chrome/browser/resources/md_extensions/sidebar.html
@@ -62,12 +62,12 @@
<iron-selector id="sectionMenu">
<!-- Values for "data-path" attribute must match the "Page" enum. -->
<a class="section-item" id="sections-extensions" href="/"
- on-tap="onLinkTap_" data-path="items-list">
+ on-click="onLinkTap_" data-path="items-list">
$i18n{sidebarExtensions}
<paper-ripple></paper-ripple>
</a>
<a class="section-item" id="sections-shortcuts" href="/shortcuts"
- on-tap="onLinkTap_" data-path="keyboard-shortcuts">
+ on-click="onLinkTap_" data-path="keyboard-shortcuts">
$i18n{keyboardShortcuts}
<paper-ripple></paper-ripple>
</a>
@@ -75,7 +75,7 @@
<div hidden="[[isSupervised]]">
<div class="separator"></div>
<a class="section-item" id="more-extensions" target="_blank"
- href="$i18n{getMoreExtensionsUrl}" on-tap="onMoreExtensionsTap_">
+ href="$i18n{getMoreExtensionsUrl}" on-click="onMoreExtensionsTap_">
<span>$i18n{openChromeWebStore}</span>
<div class="cr-icon icon-external"></div>
<paper-ripple></paper-ripple>
diff --git a/chromium/chrome/browser/resources/md_extensions/toolbar.html b/chromium/chrome/browser/resources/md_extensions/toolbar.html
index b2d3b2f9af8..8e554185091 100644
--- a/chromium/chrome/browser/resources/md_extensions/toolbar.html
+++ b/chromium/chrome/browser/resources/md_extensions/toolbar.html
@@ -1,5 +1,6 @@
<link rel="import" href="chrome://resources/html/polymer.html">
+<link rel="import" href="chrome://resources/cr_elements/cr_toast/cr_toast.html">
<link rel="import" href="chrome://resources/cr_elements/cr_toggle/cr_toggle.html">
<link rel="import" href="chrome://resources/cr_elements/cr_toolbar/cr_toolbar.html">
<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
@@ -40,7 +41,7 @@
-webkit-margin-end: 20px;
}
- #devDrawer[expanded] #button-strip {
+ #devDrawer[expanded] #buttonStrip {
top: 0;
}
@@ -55,7 +56,7 @@
height: var(--button-row-height);
}
- #button-strip {
+ #buttonStrip {
-webkit-margin-end: auto;
-webkit-margin-start: auto;
background: var(--toolbar-color);
@@ -69,7 +70,7 @@
width: 100%;
}
- #button-strip paper-button {
+ #buttonStrip paper-button {
-webkit-margin-end: 16px;
color: white;
/* Increase contrast compared to default values. */
@@ -87,6 +88,15 @@
.more-actions span {
-webkit-margin-end: 16px;
}
+
+ cr-toast > div {
+ color: #fff;
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
</style>
<cr-toolbar page-name="$i18n{toolbarTitle}"
search-prompt="$i18n{search}"
@@ -101,7 +111,7 @@
icon-class="cr20:domain"
icon-aria-label="$i18n{controlledSettingPolicy}">
</cr-tooltip-icon>
- <cr-toggle id="dev-mode" on-change="onDevModeToggleChange_"
+ <cr-toggle id="devMode" on-change="onDevModeToggleChange_"
disabled="[[shouldDisableDevMode_(
devModeControlledByPolicy, isSupervised)]]"
checked="[[inDevMode]]" aria-labelledby="devModeLabel">
@@ -109,20 +119,23 @@
</div>
</cr-toolbar>
<div id="devDrawer" expanded$="[[expanded_]]">
- <div id="button-strip">
- <paper-button hidden$="[[!canLoadUnpacked]]" id="load-unpacked"
- on-tap="onLoadUnpackedTap_">
+ <div id="buttonStrip">
+ <paper-button hidden$="[[!canLoadUnpacked]]" id="loadUnpacked"
+ on-click="onLoadUnpackedTap_">
$i18n{toolbarLoadUnpacked}
</paper-button>
- <paper-button id="pack-extensions" on-tap="onPackTap_">
+ <paper-button id="packExtensions" on-click="onPackTap_">
$i18n{toolbarPack}
</paper-button>
- <paper-button id="update-now" on-tap="onUpdateNowTap_"
+ <paper-button id="updateNow" on-click="onUpdateNowTap_"
title="$i18n{toolbarUpdateNowTooltip}">
$i18n{toolbarUpdateNow}
</paper-button>
+ <cr-toast duration="3000">
+ <div>[[toastLabel_]]</div>
+ </cr-toast>
<if expr="chromeos">
- <paper-button id="kiosk-extensions" on-tap="onKioskTap_"
+ <paper-button id="kioskExtensions" on-click="onKioskTap_"
hidden$="[[!kioskEnabled]]">
$i18n{manageKioskApp}
</paper-button>
diff --git a/chromium/chrome/browser/resources/md_extensions/toolbar.js b/chromium/chrome/browser/resources/md_extensions/toolbar.js
index 3205f8f4f21..b5d3a5cfd37 100644
--- a/chromium/chrome/browser/resources/md_extensions/toolbar.js
+++ b/chromium/chrome/browser/resources/md_extensions/toolbar.js
@@ -53,6 +53,12 @@ cr.define('extensions', function() {
/** @private */
expanded_: Boolean,
+
+ /**
+ * Text to display in update toast
+ * @private
+ */
+ toastLabel_: String,
},
behaviors: [I18nBehavior],
@@ -130,12 +136,24 @@ cr.define('extensions', function() {
/** @private */
onUpdateNowTap_: function() {
- this.delegate.updateAllExtensions().then(() => {
- Polymer.IronA11yAnnouncer.requestAvailability();
- this.fire('iron-announce', {
- text: this.i18n('toolbarUpdateDone'),
- });
- });
+ const updateButton = this.$.updateNow;
+ assert(!updateButton.disabled);
+ updateButton.disabled = true;
+ const toastElement = this.$$('cr-toast');
+ this.toastLabel_ = this.i18n('toolbarUpdatingToast');
+ toastElement.show();
+ this.delegate.updateAllExtensions()
+ .then(() => {
+ Polymer.IronA11yAnnouncer.requestAvailability();
+ const doneText = this.i18n('toolbarUpdateDone');
+ this.fire('iron-announce', {text: doneText});
+ this.toastLabel_ = doneText;
+ toastElement.show();
+ updateButton.disabled = false;
+ })
+ .catch(function() {
+ updateButton.disabled = false;
+ });
},
});
diff --git a/chromium/chrome/browser/resources/md_history/app.html b/chromium/chrome/browser/resources/md_history/app.html
index 5afaa5d63a1..a6bfb168ffd 100644
--- a/chromium/chrome/browser/resources/md_history/app.html
+++ b/chromium/chrome/browser/resources/md_history/app.html
@@ -49,7 +49,7 @@
}
#drop-shadow {
- @apply(--cr-container-shadow);
+ @apply --cr-container-shadow;
}
:host([toolbar-shadow_]) #drop-shadow {
diff --git a/chromium/chrome/browser/resources/md_history/app.js b/chromium/chrome/browser/resources/md_history/app.js
index c187aca222c..56f975fb23f 100644
--- a/chromium/chrome/browser/resources/md_history/app.js
+++ b/chromium/chrome/browser/resources/md_history/app.js
@@ -168,6 +168,13 @@ Polymer({
.getSelectedItemCount();
},
+ selectOrUnselectAll: function() {
+ const list = /** @type {HistoryListElement} */ (this.$.history);
+ const toolbar = /** @type {HistoryToolbarElement} */ (this.$.toolbar);
+ list.selectOrUnselectAll();
+ toolbar.count = list.getSelectedItemCount();
+ },
+
/**
* Listens for call to cancel selection and loops through all items to set
* checkbox to be unselected.
@@ -218,6 +225,9 @@ Polymer({
case 'delete-command':
e.canExecute = this.$.toolbar.count > 0;
break;
+ case 'select-all-command':
+ e.canExecute = !this.$.toolbar.searchField.isSearchFocused();
+ break;
}
},
@@ -230,6 +240,8 @@ Polymer({
this.focusToolbarSearchField();
else if (e.command.id == 'delete-command')
this.deleteSelected();
+ else if (e.command.id == 'select-all-command')
+ this.selectOrUnselectAll();
},
/**
diff --git a/chromium/chrome/browser/resources/md_history/history.html b/chromium/chrome/browser/resources/md_history/history.html
index cc221baf1bd..bdce86e6070 100644
--- a/chromium/chrome/browser/resources/md_history/history.html
+++ b/chromium/chrome/browser/resources/md_history/history.html
@@ -8,6 +8,11 @@
<link rel="stylesheet" href="chrome://resources/css/md_colors.css">
<style>
+ html {
+ /* Remove 300ms delay for 'click' event, when using touch interface. */
+ touch-action: manipulation;
+ }
+
html,
body {
height: 100%;
@@ -79,6 +84,12 @@
</if>
<command id="delete-command" shortcut="Delete Backspace">
<command id="slash-command" shortcut="/">
+<if expr="is_macosx">
+ <command id="select-all-command" shortcut="Meta|a">
+</if>
+<if expr="not is_macosx">
+ <command id="select-all-command" shortcut="Ctrl|a">
+</if>
<link rel="import" href="chrome://resources/html/util.html">
<link rel="import" href="chrome://resources/html/load_time_data.html">
diff --git a/chromium/chrome/browser/resources/md_history/history_item.html b/chromium/chrome/browser/resources/md_history/history_item.html
index 958032b7386..221f09b7af3 100644
--- a/chromium/chrome/browser/resources/md_history/history_item.html
+++ b/chromium/chrome/browser/resources/md_history/history_item.html
@@ -169,7 +169,7 @@
}
#background {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
background: #fff;
bottom: 0;
left: 0;
diff --git a/chromium/chrome/browser/resources/md_history/history_item.js b/chromium/chrome/browser/resources/md_history/history_item.js
index fa0dd0ec630..3329aa189f7 100644
--- a/chromium/chrome/browser/resources/md_history/history_item.js
+++ b/chromium/chrome/browser/resources/md_history/history_item.js
@@ -278,13 +278,13 @@ cr.define('md_history', function() {
item: this.item,
});
- // Stops the 'tap' event from closing the menu when it opens.
+ // Stops the 'click' event from closing the menu when it opens.
e.stopPropagation();
},
/**
- * Record metrics when a result is clicked. This is deliberately tied to
- * on-click rather than on-tap, as on-click triggers from middle clicks.
+ * Record metrics when a result is clicked.
+ * @private
*/
onLinkClick_: function() {
const browserService = md_history.BrowserService.getInstance();
diff --git a/chromium/chrome/browser/resources/md_history/history_list.html b/chromium/chrome/browser/resources/md_history/history_list.html
index b27ce8d74e2..ed46d9fcda9 100644
--- a/chromium/chrome/browser/resources/md_history/history_list.html
+++ b/chromium/chrome/browser/resources/md_history/history_list.html
@@ -1,6 +1,7 @@
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.html">
+<link rel="import" href="chrome://resources/cr_elements/paper_button_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/shared_style_css.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-a11y-announcer/iron-a11y-announcer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-list/iron-list.html">
@@ -14,7 +15,7 @@
<dom-module id="history-list">
<template>
- <style include="shared-style cr-shared-style">
+ <style include="shared-style cr-shared-style paper-button-style">
:host {
box-sizing: border-box;
display: block;
@@ -22,7 +23,7 @@
}
iron-list {
- @apply(--card-sizing);
+ @apply --card-sizing;
margin-top: var(--first-card-padding-top);
}
@@ -62,10 +63,10 @@
<div slot="title">$i18n{removeSelected}</div>
<div slot="body">$i18n{deleteWarning}</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onDialogCancelTap_">
+ <paper-button class="cancel-button" on-click="onDialogCancelTap_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onDialogConfirmTap_">
+ <paper-button class="action-button" on-click="onDialogConfirmTap_">
$i18n{deleteConfirm}
</paper-button>
</div>
@@ -74,15 +75,15 @@
<template is="cr-lazy-render" id="sharedMenu">
<dialog is="cr-action-menu">
- <button id="menuMoreButton" class="dropdown-item"
+ <button id="menuMoreButton" slot="item" class="dropdown-item"
hidden="[[!canSearchMoreFromSite_(
searchedTerm, actionMenuModel_.item.domain)]]"
- on-tap="onMoreFromSiteTap_">
+ on-click="onMoreFromSiteTap_">
$i18n{moreFromSite}
</button>
- <button id="menuRemoveButton" class="dropdown-item"
+ <button id="menuRemoveButton" slot="item" class="dropdown-item"
hidden="[[!canDeleteHistory_]]"
- on-tap="onRemoveFromHistoryTap_">
+ on-click="onRemoveFromHistoryTap_">
$i18n{removeFromHistory}
</button>
</dialog>
diff --git a/chromium/chrome/browser/resources/md_history/history_list.js b/chromium/chrome/browser/resources/md_history/history_list.js
index 8b94ed48774..9cd061e5c60 100644
--- a/chromium/chrome/browser/resources/md_history/history_list.js
+++ b/chromium/chrome/browser/resources/md_history/history_list.js
@@ -138,6 +138,25 @@ Polymer({
this.fire('query-history', false);
},
+ selectOrUnselectAll: function() {
+ if (this.historyData_.length == this.getSelectedItemCount())
+ this.unselectAllItems();
+ else
+ this.selectAllItems();
+ },
+
+ /**
+ * Select each item in |historyData|.
+ */
+ selectAllItems: function() {
+ if (this.historyData_.length == this.getSelectedItemCount())
+ return;
+
+ this.historyData_.forEach((item, index) => {
+ this.changeSelection_(index, true);
+ });
+ },
+
/**
* Deselect each item in |selectedItems|.
*/
@@ -205,6 +224,10 @@ Polymer({
.then((items) => {
this.removeItemsByIndex_(Array.from(this.selectedItems));
this.fire('unselect-all');
+ if (this.historyData_.length == 0) {
+ // Try reloading if nothing is rendered.
+ this.fire('query-history', false);
+ }
});
},
diff --git a/chromium/chrome/browser/resources/md_history/side_bar.html b/chromium/chrome/browser/resources/md_history/side_bar.html
index b49c3e2fe5c..49b11840b7b 100644
--- a/chromium/chrome/browser/resources/md_history/side_bar.html
+++ b/chromium/chrome/browser/resources/md_history/side_bar.html
@@ -107,7 +107,7 @@
<div class="separator"></div>
<a id="clear-browsing-data"
href="chrome://settings/clearBrowserData"
- on-tap="onClearBrowsingDataTap_"
+ on-click="onClearBrowsingDataTap_"
disabled$="[[guestSession_]]"
tabindex$="[[computeClearBrowsingDataTabIndex_(guestSession_)]]">
$i18n{clearBrowsingData}
diff --git a/chromium/chrome/browser/resources/md_history/synced_device_card.html b/chromium/chrome/browser/resources/md_history/synced_device_card.html
index 708f60bcf3c..a9cc4154ac8 100644
--- a/chromium/chrome/browser/resources/md_history/synced_device_card.html
+++ b/chromium/chrome/browser/resources/md_history/synced_device_card.html
@@ -16,7 +16,7 @@
<template>
<style include="shared-style">
:host {
- @apply(--card-sizing);
+ @apply --card-sizing;
-webkit-tap-highlight-color: transparent;
display: block;
padding-bottom: var(--card-padding-between);
@@ -57,7 +57,7 @@
}
#history-item-container {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
background: #fff;
border-radius: 2px;
}
diff --git a/chromium/chrome/browser/resources/md_history/synced_device_card.js b/chromium/chrome/browser/resources/md_history/synced_device_card.js
index 3206eb236eb..bbd109a64a9 100644
--- a/chromium/chrome/browser/resources/md_history/synced_device_card.js
+++ b/chromium/chrome/browser/resources/md_history/synced_device_card.js
@@ -68,8 +68,7 @@ Polymer({
},
/**
- * Open a single synced tab. Listens to 'click' rather than 'tap'
- * to determine what modifier keys were pressed.
+ * Open a single synced tab.
* @param {DomRepeatClickEvent} e
* @private
*/
diff --git a/chromium/chrome/browser/resources/md_history/synced_device_manager.html b/chromium/chrome/browser/resources/md_history/synced_device_manager.html
index 55e529c83e5..7b10c59e6cb 100644
--- a/chromium/chrome/browser/resources/md_history/synced_device_manager.html
+++ b/chromium/chrome/browser/resources/md_history/synced_device_manager.html
@@ -4,13 +4,14 @@
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/cr_elements/cr_action_menu/cr_action_menu.html">
<link rel="import" href="chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.html">
+<link rel="import" href="chrome://resources/cr_elements/paper_button_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/shared_style_css.html">
<link rel="import" href="chrome://history/shared_style.html">
<link rel="import" href="chrome://history/synced_device_card.html">
<dom-module id="history-synced-device-manager">
<template>
- <style include="shared-style cr-shared-style">
+ <style include="shared-style cr-shared-style paper-button-style">
:host {
display: block;
overflow: auto;
@@ -81,19 +82,19 @@
<div id="sign-in-promo">$i18n{signInPromo}</div>
<div id="sign-in-promo-desc">$i18n{signInPromoDesc}</div>
<paper-button id="sign-in-button" class="action-button"
- on-tap="onSignInTap_">
+ on-click="onSignInTap_">
$i18n{signInButton}
</paper-button>
</div>
<template is="cr-lazy-render" id="menu">
<dialog is="cr-action-menu">
- <button id="menuOpenButton" class="dropdown-item"
- on-tap="onOpenAllTap_">
+ <button id="menuOpenButton" slot="item" class="dropdown-item"
+ on-click="onOpenAllTap_">
$i18n{openAll}
</button>
- <button id="menuDeleteButton" class="dropdown-item"
- on-tap="onDeleteSessionTap_">
+ <button id="menuDeleteButton" slot="item" class="dropdown-item"
+ on-click="onDeleteSessionTap_">
$i18n{deleteSession}
</button>
</dialog>
diff --git a/chromium/chrome/browser/resources/md_user_manager/shared_styles.html b/chromium/chrome/browser/resources/md_user_manager/shared_styles.html
index 7c9d1aa16ab..02eac8fb435 100644
--- a/chromium/chrome/browser/resources/md_user_manager/shared_styles.html
+++ b/chromium/chrome/browser/resources/md_user_manager/shared_styles.html
@@ -26,7 +26,7 @@
}
paper-button.action {
- @apply(--action-button);
+ @apply --action-button;
}
paper-button.action.primary {
diff --git a/chromium/chrome/browser/resources/md_user_manager/user_manager.html b/chromium/chrome/browser/resources/md_user_manager/user_manager.html
index 8dbe1d9def7..244e159517d 100644
--- a/chromium/chrome/browser/resources/md_user_manager/user_manager.html
+++ b/chromium/chrome/browser/resources/md_user_manager/user_manager.html
@@ -322,7 +322,7 @@
--paper-button-flat-keyboard-focus: {
background: rgb(173, 50, 36);
};
- @apply(--action-button);
+ @apply --action-button;
}
#user-manager-prompt-message {
diff --git a/chromium/chrome/browser/resources/md_user_manager/user_manager_pages.html b/chromium/chrome/browser/resources/md_user_manager/user_manager_pages.html
index 8a4acecdaf6..c2f8945b653 100644
--- a/chromium/chrome/browser/resources/md_user_manager/user_manager_pages.html
+++ b/chromium/chrome/browser/resources/md_user_manager/user_manager_pages.html
@@ -7,6 +7,7 @@
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/animations/fade-out-animation.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/neon-animatable.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/neon-animated-pages.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/web-animations.html">
<dom-module id="user-manager-pages">
<template>
diff --git a/chromium/chrome/browser/resources/media/media_engagement.html b/chromium/chrome/browser/resources/media/media_engagement.html
index fd374ce52d3..af4c2d8e1e2 100644
--- a/chromium/chrome/browser/resources/media/media_engagement.html
+++ b/chromium/chrome/browser/resources/media/media_engagement.html
@@ -18,6 +18,10 @@
font-size: 14px;
}
+ button {
+ margin-bottom: 20px;
+ }
+
table {
border-collapse: collapse;
margin-bottom: 20px;
@@ -50,7 +54,6 @@
.origin-cell {
background-color: rgba(230, 230, 230, 0.5);
- min-width: 500px;
}
.visits-count-cell,
@@ -60,6 +63,7 @@
.significant-playbacks-count-cell {
background-color: rgba(230, 230, 230, 0.5);
text-align: right;
+ white-space: nowrap;
}
.base-score-input {
@@ -96,6 +100,7 @@
</head>
<body>
<h1>Media Engagement</h1>
+ <button id="copy-all-to-clipboard">Copy all to clipboard</button>
<table>
<thead>
<tr id="config-table-header">
@@ -110,6 +115,12 @@
<tbody id="config-table-body">
</tbody>
</table>
+ <p>
+ <label>
+ <input id="show-no-playbacks" type="checkbox">
+ Show sessions with no playbacks
+ </label>
+ </p>
<table>
<thead>
<tr id="engagement-table-header">
@@ -117,10 +128,10 @@
Origin
</th>
<th sort-key="visits" sort-reverse>
- Visits
+ Sessions
</th>
<th sort-key="mediaPlaybacks" sort-reverse>
- Playbacks
+ Sessions with playback
</th>
<th sort-key="audiblePlaybacks" sort-reverse>
Audible Playbacks*
@@ -134,6 +145,9 @@
<th sort-key="isHigh" sort-reverse>
Is High
</th>
+ <th sort-key="highScoreChanges" sort-reverse>
+ Is High Changes
+ </th>
<th sort-key="totalScore" class="sort-column" sort-reverse>
Score
</th>
@@ -156,6 +170,7 @@
<td class="significant-playbacks-count-cell"></td>
<td class="last-playback-time-cell"></td>
<td class="is-high-cell"></td>
+ <td class="is-high-changes-cell"></td>
<td class="total-score-cell"></td>
<td class="engagement-bar-cell">
<div class="engagement-bar"></div>
diff --git a/chromium/chrome/browser/resources/media/media_engagement.js b/chromium/chrome/browser/resources/media/media_engagement.js
index 0ca0a3cd94c..a4a8af7841f 100644
--- a/chromium/chrome/browser/resources/media/media_engagement.js
+++ b/chromium/chrome/browser/resources/media/media_engagement.js
@@ -19,6 +19,7 @@ var engagementTableBody = null;
var sortReverse = true;
var sortKey = 'totalScore';
var configTableBody = null;
+var showNoPlaybacks = false;
/**
* Creates a single row in the engagement table.
@@ -37,8 +38,9 @@ function createRow(rowInfo) {
new Date(rowInfo.lastMediaPlaybackTime).toISOString() :
'';
td[6].textContent = rowInfo.isHigh ? 'Yes' : 'No';
- td[7].textContent = rowInfo.totalScore ? rowInfo.totalScore.toFixed(2) : '0';
- td[8].getElementsByClassName('engagement-bar')[0].style.width =
+ td[7].textContent = rowInfo.highScoreChanges;
+ td[8].textContent = rowInfo.totalScore ? rowInfo.totalScore.toFixed(2) : '0';
+ td[9].getElementsByClassName('engagement-bar')[0].style.width =
(rowInfo.totalScore * 50) + 'px';
return document.importNode(template.content, true);
}
@@ -77,7 +79,8 @@ function compareTableItem(sortKey, a, b) {
if (sortKey == 'visits' || sortKey == 'mediaPlaybacks' ||
sortKey == 'lastMediaPlaybackTime' || sortKey == 'totalScore' ||
- sortKey == 'audiblePlaybacks' || sortKey == 'significantPlaybacks') {
+ sortKey == 'audiblePlaybacks' || sortKey == 'significantPlaybacks' ||
+ sortKey == 'highScoreChanges') {
return val1 - val2;
}
@@ -108,7 +111,7 @@ function renderConfigTable(config) {
configTableBody.innerHTML = '';
configTableBody.appendChild(
- createConfigRow('Min Visits', config.scoreMinVisits));
+ createConfigRow('Min Sessions', config.scoreMinVisits));
configTableBody.appendChild(
createConfigRow('Lower Threshold', config.highScoreLowerThreshold));
configTableBody.appendChild(
@@ -121,7 +124,8 @@ function renderConfigTable(config) {
function renderTable() {
clearTable();
sortInfo();
- info.forEach(rowInfo => engagementTableBody.appendChild(createRow(rowInfo)));
+ info.filter(rowInfo => (showNoPlaybacks || rowInfo.mediaPlaybacks > 0))
+ .forEach(rowInfo => engagementTableBody.appendChild(createRow(rowInfo)));
}
/**
@@ -173,5 +177,27 @@ document.addEventListener('DOMContentLoaded', function() {
renderTable();
});
}
+
+ // Add handler to 'copy all to clipboard' button
+ var copyAllToClipboardButton = $('copy-all-to-clipboard');
+ copyAllToClipboardButton.addEventListener('click', (e) => {
+ // Make sure nothing is selected
+ window.getSelection().removeAllRanges();
+
+ document.execCommand('selectAll');
+ document.execCommand('copy');
+
+ // And deselect everything at the end.
+ window.getSelection().removeAllRanges();
+ });
+
+ // Add handler to 'show no playbacks' checkbox
+ var showNoPlaybacksCheckbox = $('show-no-playbacks');
+ showNoPlaybacksCheckbox.addEventListener('change', (e) => {
+ showNoPlaybacks = e.target.checked;
+ renderTable();
+ });
+
});
+
})();
diff --git a/chromium/chrome/browser/resources/media/mei_preload/preloaded_data.pb b/chromium/chrome/browser/resources/media/mei_preload/preloaded_data.pb
index 1302db9033b..1652cafc98a 100644
--- a/chromium/chrome/browser/resources/media/mei_preload/preloaded_data.pb
+++ b/chromium/chrome/browser/resources/media/mei_preload/preloaded_data.pb
Binary files differ
diff --git a/chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.css b/chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.css
index 7ae1c39ff5a..9cc54236cca 100644
--- a/chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.css
+++ b/chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.css
@@ -137,7 +137,7 @@ paper-item:hover {
border: 0;
}
-paper-menu {
+paper-listbox {
color: rgba(0, 0, 0, 0.87);
overflow-x: hidden;
overflow-y: auto;
diff --git a/chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.html b/chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.html
index 9a9e85029f4..6b606df8ece 100644
--- a/chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.html
+++ b/chromium/chrome/browser/resources/media_router/elements/media_router_container/media_router_container.html
@@ -3,7 +3,7 @@
<link rel="import" href="chrome://resources/polymer/v1_0/paper-checkbox/paper-checkbox.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-input/paper-input.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-item/paper-item.html">
-<link rel="import" href="chrome://resources/polymer/v1_0/paper-menu/paper-menu.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/paper-listbox/paper-listbox.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-spinner/paper-spinner-lite.html">
<link rel="import" href="../media_router_header/media_router_header.html">
<link rel="import" href="../route_details/route_details.html">
@@ -57,7 +57,7 @@
</media-router-header>
<div id="content">
<template is="dom-if" if="[[!computeCastModeListHidden_(currentView_)]]">
- <paper-menu id="cast-mode-list" role="presentation"
+ <paper-listbox id="cast-mode-list" role="presentation"
selectable="paper-item" selected="{{selectedCastModeMenuItem_}}">
<template is="dom-repeat" id="presentationCastModeList"
items="[[computePresentationCastModeList_(castModeList)]]">
@@ -94,7 +94,7 @@
<div><span>[[item.description]]</span></div>
</paper-item>
</template>
- </paper-menu>
+ </paper-listbox>
</template>
<template is="dom-if"
if="[[!computeRouteDetailsHidden_(currentView_, issue)]]">
@@ -121,7 +121,7 @@
</div>
<template is="dom-if" if="[[!computeSinkListHidden_(sinksToShow_)]]">
<div id="sink-list" hidden$="[[hideSinkListForAnimation_]]">
- <paper-menu id="sink-list-paper-menu" role="presentation">
+ <paper-listbox id="sink-list-paper-menu" role="presentation">
<template is="dom-repeat" id="sinkList" items="[[sinksToShow_]]">
<paper-item on-tap="onSinkClick_">
<div class="sink-content">
@@ -158,7 +158,7 @@
</div>
</paper-item>
</template>
- </paper-menu>
+ </paper-listbox>
</div>
</template>
<template is="dom-if" if="[[searchEnabled_]]">
@@ -185,7 +185,8 @@
</div>
<div id="search-results"
hidden$="[[computeSearchResultsHidden_(searchResultsToShow_, isSearchListHidden_)]]">
- <paper-menu id="search-results-paper-menu" selected="0" role="presentation">
+ <paper-listbox id="search-results-paper-menu" selected="0"
+ role="presentation">
<template is="dom-repeat" id="searchResults"
items="[[searchResultsToShow_]]">
<paper-item class="search-item" on-tap="onSinkClick_">
@@ -226,7 +227,7 @@
</div>
</paper-item>
</template>
- </paper-menu>
+ </paper-listbox>
</div>
</div>
</template>
diff --git a/chromium/chrome/browser/resources/media_router/elements/route_details/route_details.css b/chromium/chrome/browser/resources/media_router/elements/route_details/route_details.css
index 92f8b1a7dd1..e5b7f3e72da 100644
--- a/chromium/chrome/browser/resources/media_router/elements/route_details/route_details.css
+++ b/chromium/chrome/browser/resources/media_router/elements/route_details/route_details.css
@@ -3,8 +3,8 @@
* found in the LICENSE file. */
#route-action-buttons {
- @apply(--layout-horizontal);
- @apply(--layout-end-justified);
+ @apply --layout-horizontal;
+ @apply --layout-end-justified;
margin: 0 10px;
padding: 0;
white-space: nowrap;
diff --git a/chromium/chrome/browser/resources/media_router/extension/BUILD.gn b/chromium/chrome/browser/resources/media_router/extension/BUILD.gn
new file mode 100644
index 00000000000..3b002567f96
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/BUILD.gn
@@ -0,0 +1,78 @@
+import("src/files.gni")
+
+group("all") {
+ deps = [
+ ":media_router",
+ ]
+}
+
+declare_args() {
+ # Determines whether JSCompiler should be used to typecheck
+ # JavaScript code for the Media Router extension.
+ enable_media_router_jscompile = false
+}
+
+if (enable_media_router_jscompile) {
+ # Run JSCompiler for typechecking.
+ action("media_router_type_check") {
+ script = "//third_party/closure_compiler/compile2.py"
+ inputs = rebase_path(mr_module_files, ".", "src") + [
+ "src/externs.js",
+ "src/mojo_externs.js",
+ ]
+ outputs = [
+ target_gen_dir + "/$target_name.stamp",
+ ]
+ args = [
+ "--out_file",
+ rebase_path(outputs[0], root_build_dir),
+ "--closure_args",
+ "dependency_mode=LOOSE",
+ "checks_only",
+ "--",
+ ] + rebase_path(inputs, root_build_dir)
+ }
+}
+
+# Concatentate JS files to produce "module" JS files that can be
+# loaded at runtime. This could be done by JSCompiler, but it depends
+# on Java, and Java isn't always available.
+action("media_router_modules") {
+ script = "concat_js_modules.py"
+ module_inputs = rebase_path(mr_module_files, ".", "src")
+ inputs = module_inputs + [ "prelude.js" ]
+ outputs = []
+ foreach(module_name, mr_module_names) {
+ outputs += [ "${target_gen_dir}/${module_name}.js" ]
+ }
+ args = [ "--module-specs" ] + mr_module_specs + [
+ "--prelude-file",
+ rebase_path("prelude.js", root_build_dir),
+ "--output-dir",
+ rebase_path(target_gen_dir + "/", root_build_dir),
+ "--",
+ ] + rebase_path(module_inputs, root_build_dir)
+}
+
+# Produce the Media Router extension. At present, the extension isn't
+# included in the Chromium distribution, but it can be sideloaded into
+# Chromium for testing.
+action("media_router") {
+ script = "assemble_extension.py"
+ inputs = [
+ "manifest.yaml",
+ ]
+ outputs = [
+ "$target_gen_dir/manifest.json",
+ ]
+ deps = [
+ ":media_router_modules",
+ ]
+ if (enable_media_router_jscompile) {
+ deps += [ ":media_router_type_check" ]
+ }
+ args = [
+ "--manifest_in=" + rebase_path("manifest.yaml", root_build_dir),
+ "--output_dir=" + rebase_path(target_gen_dir, root_build_dir),
+ ]
+}
diff --git a/chromium/chrome/browser/resources/media_router/extension/assemble_extension.py b/chromium/chrome/browser/resources/media_router/extension/assemble_extension.py
new file mode 100644
index 00000000000..7b2df0a255b
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/assemble_extension.py
@@ -0,0 +1,41 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This script generates manifest.json from manifest.yaml.
+
+In the future it may copy other non-JS files into the target
+directory, hence the name of the script.
+"""
+
+import argparse
+import json
+import re
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--manifest_in")
+ parser.add_argument("--output_dir")
+ args = parser.parse_args()
+
+ # Load the input file, removing YAML-style comment lines to produce
+ # valid JSON.
+ json_data = ''
+ with open(args.manifest_in) as manifest_in:
+ for line in manifest_in:
+ if re.match("^ *#", line):
+ # Insert an empty line so line numbers aren't changed.
+ json_data += '\n'
+ else:
+ json_data += line
+
+ # Verify that the result is valid JSON.
+ json.loads(json_data)
+
+ # Dump the output to the requested location.
+ with open(args.output_dir + "/manifest.json", "w") as manifest_out:
+ manifest_out.writelines(json_data)
+
+
+main()
diff --git a/chromium/chrome/browser/resources/media_router/extension/concat_js_modules.py b/chromium/chrome/browser/resources/media_router/extension/concat_js_modules.py
new file mode 100644
index 00000000000..cda9ca1dd47
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/concat_js_modules.py
@@ -0,0 +1,87 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This script concatenates JS files into a set of output files.
+
+Modules are specified as a list of strings of the form
+module=<name>:<num>[:...], where <name> is the name of the module and
+<num> is the number of JS files that should be concatenates to make
+the module file, and if a second : is present, everything after it is
+ignored. (This strange format is used because it is compatible with
+JSCompiler's --module flag.)
+"""
+
+import argparse
+import os.path
+import re
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--module-specs", nargs="+",
+ help="List of module specifiations.")
+ parser.add_argument(
+ "--output-dir",
+ help="Directory where output files are written.")
+ parser.add_argument(
+ "--prelude-file",
+ help="A prelude file included in every output module.")
+ parser.add_argument(
+ "sources", nargs="+",
+ help="JS input files.")
+ args = parser.parse_args()
+
+ # Read the prelude file, which contains code to be placed at the
+ # start of each module.
+ with open(args.prelude_file, "r") as prelude_in:
+ prelude = prelude_in.read()
+
+ # Loop over all specified modules, and simulaneously traverse the
+ # list of source files using `source_index`.
+ source_index = 0
+ for spec in args.module_specs:
+ # Split the module spec into a module name and a count of source
+ # files to include in the module.
+ pre, sep, post = spec.partition("module=")
+ assert pre == ""
+ assert sep
+ parts = post.split(":")
+ module_name = parts[0]
+ input_count = int(parts[1])
+
+ # Write the module file.
+ module_file = os.path.join(args.output_dir, module_name + ".js")
+ with open(module_file, "w") as module_out:
+ module_out.write(prelude)
+
+ # Append as many input files as requested by the module spec.
+ for i in range(input_count):
+ source_file = args.sources[source_index]
+ source_index += 1
+ module_name = None
+ with open(source_file, "r") as source_in:
+ module_out.write("goog.scope(() => {\n")
+ # Copy input line by line.
+ for line in source_in:
+ m = re.match(r"goog\.module\('([^']+)'\);\n", line)
+ if m:
+ # Handle goog.module statements specially.
+ module_name = m.group(1)
+ module_out.write("let exports = {};\n")
+ else:
+ # Typical case: just copy the line verbatim.
+ module_out.write(line)
+ if module_name:
+ # Put exported names into the global namespace. That's
+ # not now goog.module is supposed to work, but it's close
+ # enough.
+ module_out.write(
+ "__setGlobal('{}', exports);\n".format(module_name));
+ module_out.write("});\n")
+
+ # Check that every source file has been appended to a module.
+ if source_index != len(args.sources):
+ sys.exit("Too many input files.")
+
+main()
diff --git a/chromium/chrome/browser/resources/media_router/extension/manifest.yaml b/chromium/chrome/browser/resources/media_router/extension/manifest.yaml
new file mode 100644
index 00000000000..4eeb52af9d8
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/manifest.yaml
@@ -0,0 +1,53 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# NOTE: Only a subset of YAML syntax is allowed in this file. Lines
+# containing comments are stripped, and the remaining lines must be
+# valid JSON.
+
+{
+ "name": "Chromium Media Router",
+ "version": "0.1",
+ "manifest_version": 2,
+ "description": "Provider for discovery and services for mirroring of Chromium Media Router",
+ "minimum_chrome_version": "37",
+
+ "permissions": [
+ "alarms",
+ "declarativeWebRequest",
+ "desktopCapture",
+ "dial",
+ "http://*/*",
+ "mediaRouterPrivate",
+ "metricsPrivate",
+ "storage",
+ "settingsPrivate",
+ "tabCapture",
+ "tabs"
+ ],
+
+ # Background script.
+ "background": {
+ "scripts": [
+ "common.js",
+ "mirroring_common.js",
+ "background_script.js"
+ ],
+ "persistent": false
+ },
+
+ # Google Feedback requires:
+ # script-src: https://feedback.googleusercontent.com https://www.google.com
+ # https://www.gstatic.com/feedback
+ # child-src: https://www.google.com
+ #
+ # Webview elements are implemented as a custom <object> elements that loads
+ # dynamic plugin data. Without an "object-src 'self'" permission in the CSP,
+ # webview elements fail to attach to extension pages (crbug.com/509854).
+ "content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' https://feedback.googleusercontent.com https://www.google.com https://www.gstatic.com; child-src https://www.google.com; connect-src 'self' http://*:* https://*:*; font-src https://fonts.gstatic.com; object-src 'self';",
+
+ # Setting the public key fixes the extension id to:
+ # enhhojjnijigcajfphajepfemndkmdlo
+ "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+hlN5FB+tjCsBszmBIvIcD/djLLQm2zZfFygP4U4/o++ZM91EWtgII10LisoS47qT2TIOg4Un4+G57elZ9PjEIhcJfANqkYrD3t9dpEzMNr936TLB2u683B5qmbB68Nq1Eel7KVc+F0BqhBondDqhvDvGPEV0vBsbErJFlNH7SQIDAQAB"
+}
diff --git a/chromium/chrome/browser/resources/media_router/extension/prelude.js b/chromium/chrome/browser/resources/media_router/extension/prelude.js
new file mode 100644
index 00000000000..6140042e26d
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/prelude.js
@@ -0,0 +1,50 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Implementation of JSCompiler intrinsics for use when JSCompiler is
+// not available.
+
+/**
+ * Sets the values of a global expression consisting of a
+ * dot-delimited list of identifiers.
+ */
+const __setGlobal = (name, value) => {
+ let parent = window;
+ const parts = name.split('.');
+ for (let i = 0; i < parts.length; i++) {
+ const part = parts[i];
+ if (i == parts.length - 1) {
+ parent[part] = value;
+ } else {
+ if (!parent[part]) {
+ parent[part] = {};
+ }
+ parent = parent[part];
+ }
+ }
+};
+
+const goog = {
+ provide(name) {
+ __setGlobal(name, {});
+ },
+
+ require(name) {
+ let parent = window;
+ name.split('.').forEach(part => {
+ parent = parent[part];
+ });
+ return parent;
+ },
+
+ module: {
+ declareLegacyNamespace() {},
+ },
+
+ forwardDeclare() {},
+
+ scope(body) {
+ body.call(window);
+ },
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/background.js b/chromium/chrome/browser/resources/media_router/extension/src/background.js
new file mode 100644
index 00000000000..e1b87123e7f
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/background.js
@@ -0,0 +1,9 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+goog.require('mr.Init');
+
+
+mr.Init.init().then(undefined, err => window.close());
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/config.js b/chromium/chrome/browser/resources/media_router/extension/src/config.js
new file mode 100644
index 00000000000..fc339414189
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/config.js
@@ -0,0 +1,26 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Configs.
+ */
+
+goog.provide('mr.Config');
+
+
+/**
+ * Compiler flag used to enable debug/testing only components. The default
+ * value defined here is only used in open-source builds.
+ * @define {boolean} True if this extension was released through debug channel.
+ */
+mr.Config.isDebugChannel = true;
+
+
+/**
+ * Compiler flag used to set logging level and other privacy sensitive config
+ * for public release. The default value defined here is only used in
+ * open-source builds.
+ * @define {boolean} True if this extension was released through public channel.
+ */
+mr.Config.isPublicChannel = false;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/event_listener.js b/chromium/chrome/browser/resources/media_router/extension/src/event_listener.js
new file mode 100644
index 00000000000..8cef915b1a3
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/event_listener.js
@@ -0,0 +1,188 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Interface for registration of listeners of events that can
+ * wake the extension. All EventListeners must invoke
+ * |addOnStartup| in the first event loop in order to
+ * properly receive events that woke the extension. When adding a new
+ * EventListener, be sure to also add it to |mr.Init.addEventListeners_|.
+ */
+
+goog.provide('mr.EventListener');
+
+goog.require('mr.EventAnalytics');
+goog.require('mr.Module');
+goog.require('mr.PersistentData');
+goog.require('mr.PersistentDataManager');
+
+
+
+/**
+ * Listens for an extension event conditionally, and forwards the event to a
+ * designated module.
+ *
+ * This is useful for event listeners that are not added when the extension
+ * initially starts up (mDNS listeners, for instance). When an event
+ * listener is ready to be added for the first time (and the module is ready
+ * to handle the event), |addListener()| should be called. By doing so, the
+ * event listener will be automatically added back at the top level the next
+ * time the extension wakes up from suspension, unless |removeListener()| is
+ * called.
+ *
+ * @implements {mr.PersistentData}
+ * @template EVENT
+ */
+mr.EventListener = class {
+ /**
+ * @param {!mr.EventAnalytics.Event} eventType The event type for this
+ * listener to record with analytics.
+ * @param {string} name Name of the handler for PersistentData.
+ * @param {mr.ModuleId} moduleId Name of the module handling the events.
+ * @param {!EVENT} eventHandler The event handler to listen to.
+ * @param {...*} listenerArgs Additional arguments when adding the listener,
+ * such as filters.
+ */
+ constructor(eventType, name, moduleId, eventHandler, ...listenerArgs) {
+ /** @private @const {!mr.EventAnalytics.Event} */
+ this.eventType_ = eventType;
+
+ /** @private @const {string} */
+ this.name_ = name;
+
+ /** @private @const {mr.ModuleId} */
+ this.moduleId_ = moduleId;
+
+ /** @private @const {!EVENT} */
+ this.eventHandler_ = eventHandler;
+
+ /** @private @const {!Array<*>} */
+ this.listenerArgs_ = listenerArgs;
+
+ /**
+ * This field is stored as temporary data. Set to true if the listener was
+ * added, and will be added back the next time the extension wakes up via
+ * |addOnStartup()|.
+ * @private {boolean}
+ */
+ this.hasListener_ = false;
+
+ /** @private @const {?function(*)} */
+ this.listener_ = (...args) => this.dispatchEvent_(...args);
+ }
+
+ /**
+ * Adds back the event listener that was added before the last suspension.
+ * This method is called by mr.Init during the first event loop only.
+ */
+ addOnStartup() {
+ mr.PersistentDataManager.register(this);
+ }
+
+ /**
+ * Helper method to add the listener to the event.
+ * @private
+ */
+ doAddListener_() {
+ this.eventHandler_.addListener(this.listener_, ...this.listenerArgs_);
+ }
+
+ /**
+ * Adds event listener. No-ops if listener is already added. Event
+ * listeners will be added back automatically the next time extension wakes
+ * up, during |addOnStartup|.
+ */
+ addListener() {
+ if (this.hasListener_) {
+ return;
+ }
+ this.hasListener_ = true;
+ this.doAddListener_();
+ }
+
+ /**
+ * Removes event listener. The next time extension wakes up, the event
+ * listener will not be added back during |addOnStartup|.
+ */
+ removeListener() {
+ if (!this.hasListener_) {
+ return;
+ }
+ this.eventHandler_.removeListener(this.listener_);
+ this.hasListener_ = false;
+ }
+
+ /**
+ * Implementations may override this method validate an incoming event before
+ * it is forwarded to the designated module.
+ * @param {...*} args
+ * @return {boolean} true if the event can be forwarded to the module.
+ */
+ validateEvent(...args) {
+ return true;
+ }
+
+ /**
+ * The entry point for incoming events. First the event will be validated.
+ * After that, the event will be forwarded to the module, which is loaded if
+ * necessary. Since asynchronous event handling is required, a value
+ * representing asynchronous handling will be returned synchronously, as
+ * required by the event.
+ * @param {...*} args Parameters for the incoming event.
+ * @return {*} false if the event is invalid. Otherwise, a value that
+ * represents that event will be handled asynchronously will be returned.
+ * @private
+ */
+ dispatchEvent_(...args) {
+ mr.EventAnalytics.recordEvent(this.eventType_);
+ if (!this.validateEvent(...args)) {
+ return false;
+ }
+ mr.Module.load(this.moduleId_)
+ .then(module => module.handleEvent(this.eventHandler_, ...args));
+ return this.deferredReturnValue();
+ }
+
+ /**
+ * Returns |true| if the event listener is already registered.
+ * @return {boolean}
+ */
+ isRegistered() {
+ return this.hasListener_;
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'mr.EventListener.' + this.name_;
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ return [this.hasListener_];
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {
+ const hadListener =
+ /** @type {boolean} */ (
+ mr.PersistentDataManager.getTemporaryData(this));
+ if (hadListener) {
+ this.addListener();
+ }
+ }
+
+ /**
+ * Implementations may override this method to provide a return value in the
+ * case the event is not handled synchronously. The default value is
+ * undefined.
+ * @return {*}
+ */
+ deferredReturnValue() {}
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/event_listener_test.js b/chromium/chrome/browser/resources/media_router/extension/src/event_listener_test.js
new file mode 100644
index 00000000000..a64895f5318
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/event_listener_test.js
@@ -0,0 +1,147 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly('event_listener_test');
+
+goog.require('mr.EventAnalytics');
+goog.require('mr.EventListener');
+goog.require('mr.Module');
+goog.require('mr.PersistentDataManager');
+goog.require('mr.PromiseResolver');
+goog.require('mr.UnitTestUtils');
+
+
+describe('Tests event listeners', function() {
+ let mockEvent;
+
+ beforeEach(function() {
+ mr.UnitTestUtils.mockChromeApi();
+ mockEvent = jasmine.createSpyObj(
+ 'mockEvent', ['addListener', 'hasListener', 'removeListener']);
+ });
+
+ afterEach(function() {
+ mr.Module.clearForTest();
+ mr.PersistentDataManager.clear();
+ mr.UnitTestUtils.restoreChromeApi();
+ });
+
+ it('EventListener addListener no listener args', function() {
+ const listener = new mr.EventListener(
+ mr.EventAnalytics.Event.DIAL_ON_ERROR, 'MockEventListener',
+ 'SomeModule', mockEvent);
+ listener.addListener();
+ expect(mockEvent.addListener).toHaveBeenCalled();
+ expect(listener.isRegistered()).toBe(true);
+
+ listener.removeListener();
+ expect(mockEvent.removeListener).toHaveBeenCalled();
+ expect(listener.isRegistered()).toBe(false);
+ });
+
+ it('EventListener addListener with listener args', function() {
+ const listenerArgs = ['foo', 1];
+ const listener = new mr.EventListener(
+ mr.EventAnalytics.Event.DIAL_ON_ERROR, 'MockEventListener',
+ 'SomeModule', mockEvent, ...listenerArgs);
+ listener.addListener();
+ expect(mockEvent.addListener)
+ .toHaveBeenCalledWith(jasmine.any(Function), ...listenerArgs);
+ expect(listener.isRegistered()).toBe(true);
+
+ listener.removeListener();
+ expect(mockEvent.removeListener).toHaveBeenCalled();
+ expect(listener.isRegistered()).toBe(false);
+ });
+
+ it('EventListener addOnStartup no prior register', function() {
+ const listener = new mr.EventListener(
+ mr.EventAnalytics.Event.DIAL_ON_ERROR, 'MockEventListener',
+ 'SomeModule', mockEvent);
+ listener.addOnStartup();
+ expect(mockEvent.addListener).not.toHaveBeenCalled();
+ });
+
+ it('EventListener addOnStartup registered before', function() {
+ const listener = new mr.EventListener(
+ mr.EventAnalytics.Event.DIAL_ON_ERROR, 'MockEventListener',
+ 'SomeModule', mockEvent);
+ listener.addOnStartup();
+ expect(mockEvent.addListener.calls.count()).toBe(0);
+ listener.addListener();
+ expect(mockEvent.addListener.calls.count()).toBe(1);
+
+ mr.PersistentDataManager.suspendForTest();
+
+ const listener2 = new mr.EventListener(
+ mr.EventAnalytics.Event.DIAL_ON_ERROR, 'MockEventListener',
+ 'SomeModule', mockEvent);
+ // Registers with mr.PersistentDataManager again. It should see that it was
+ // previously registered before suspend, and re-adds the listener
+ // auatomatically.
+ listener2.addOnStartup();
+ expect(mockEvent.addListener.calls.count()).toBe(2);
+ });
+
+ it('EventListener rejected invalid event', function() {
+ let savedListener = null;
+ const listener = new mr.EventListener(
+ mr.EventAnalytics.Event.DIAL_ON_ERROR, 'MockEventListener',
+ 'SomeModule', mockEvent);
+ mockEvent.addListener.and.callFake(listener => {
+ savedListener = listener;
+ });
+ listener.addListener();
+ expect(mockEvent.addListener).toHaveBeenCalled();
+ expect(savedListener).not.toBeNull();
+ spyOn(listener, 'validateEvent').and.returnValue(false);
+
+ // Module won't be loaded since event did not pass validation.
+ spyOn(mr.Module, 'load');
+ let returnedValue = savedListener('foo', 1);
+ expect(returnedValue).toBe(false);
+ expect(mr.Module.load.calls.count()).toBe(0);
+ });
+
+ it('EventListener dispatch event', function(done) {
+ spyOn(mr.EventAnalytics, 'recordEvent');
+ let savedListener = null;
+ const listener = new mr.EventListener(
+ mr.EventAnalytics.Event.DIAL_ON_ERROR, 'MockEventListener',
+ 'SomeModule', mockEvent);
+ spyOn(listener, 'deferredReturnValue').and.returnValue(123);
+ mockEvent.addListener.and.callFake(listener => {
+ savedListener = listener;
+ });
+ listener.addListener();
+ expect(mockEvent.addListener).toHaveBeenCalled();
+ expect(savedListener).not.toBeNull();
+
+ // Module not ready yet; events are queued up, and deferred value is
+ // returned synchronously.
+ let resolver = new mr.PromiseResolver();
+ spyOn(mr.Module, 'load').and.returnValue(resolver.promise);
+ let returnedValue = savedListener('foo', 1);
+ expect(returnedValue).toBe(123);
+ returnedValue = savedListener('bar', 2);
+ expect(returnedValue).toBe(123);
+ expect(mr.Module.load.calls.count()).toBe(2);
+
+ expect(mr.EventAnalytics.recordEvent)
+ .toHaveBeenCalledWith(mr.EventAnalytics.Event.DIAL_ON_ERROR);
+ expect(mr.EventAnalytics.recordEvent.calls.count()).toEqual(2);
+
+ const module = jasmine.createSpyObj('mockModule', ['handleEvent']);
+ module.handleEvent.and.callFake((e, arg1, arg2) => {
+ if (arg1 == 'bar') {
+ expect(module.handleEvent).toHaveBeenCalledWith(mockEvent, 'foo', 1);
+ expect(module.handleEvent).toHaveBeenCalledWith(mockEvent, 'bar', 2);
+ done();
+ }
+ });
+
+ // Fake loading the module.
+ resolver.resolve(module);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/extension_selector.js b/chromium/chrome/browser/resources/media_router/extension/src/extension_selector.js
new file mode 100644
index 00000000000..652a20d0aa6
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/extension_selector.js
@@ -0,0 +1,47 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Selector for picking an MR extension to use.
+
+
+ */
+
+goog.provide('mr.ExtensionId');
+goog.provide('mr.ExtensionSelector');
+
+
+/**
+ * @enum {string}
+ */
+mr.ExtensionId = {
+ PUBLIC: 'pkedcjkdefgpdelpbcmbmeomcjbeemfm',
+ DEV: 'enhhojjnijigcajfphajepfemndkmdlo'
+};
+
+
+/**
+ * @return {Promise} Resolves if this extension should start itself,
+ * rejects otherwise.
+ */
+mr.ExtensionSelector.shouldStart = function() {
+ return new Promise((resolve, reject) => {
+ switch (window.location.host) {
+ case mr.ExtensionId.DEV:
+ resolve();
+ break;
+ case mr.ExtensionId.PUBLIC:
+ chrome.management.get(mr.ExtensionId.DEV, result => {
+ if (chrome.runtime.lastError || !result.enabled) {
+ resolve();
+ } else {
+ reject(Error('Dev extension is enabled'));
+ }
+ });
+ break;
+ default:
+ reject(Error('Unknown extension id'));
+ }
+ });
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/external_message_listener.js b/chromium/chrome/browser/resources/media_router/extension/src/external_message_listener.js
new file mode 100644
index 00000000000..3f4e0800dfa
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/external_message_listener.js
@@ -0,0 +1,80 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Listener for messages received from external apps/extensions or
+ * web pages.
+ *
+
+ */
+
+goog.provide('mr.ExternalMessageListener');
+
+goog.require('mr.EventAnalytics');
+goog.require('mr.EventListener');
+goog.require('mr.InternalMessageType');
+goog.require('mr.ModuleId');
+
+
+mr.ExternalMessageListener = class extends mr.EventListener {
+ constructor() {
+ super(
+ mr.EventAnalytics.Event.RUNTIME_ON_MESSAGE_EXTERNAL,
+ 'ExternalMessageListener', mr.ModuleId.PROVIDER_MANAGER,
+ chrome.runtime.onMessageExternal);
+ }
+
+ /**
+ * @override
+ */
+ validateEvent(message, sender, sendResponse) {
+ // Make sure all messages have a sender |id| and that the ID is whitelisted.
+ // If messages have a |tab| they are from a web page (most likely the Cast
+ // setup page).
+ if (!sender.id ||
+ mr.ExternalMessageListener.WHITELIST_.indexOf(sender.id) == -1) {
+ return false;
+ }
+
+ // Check if message type is valid.
+ return message.type == mr.InternalMessageType.START ||
+ message.type == mr.InternalMessageType.STOP ||
+ message.type == mr.InternalMessageType.SUBSCRIBE_LOG_DATA;
+ }
+
+ /**
+ * @override
+ */
+ deferredReturnValue() {
+ // Indicates the messaging channel should be kept open until
+ // sendResponse() is called.
+ return true;
+ }
+
+ /** @return {!mr.ExternalMessageListener} */
+ static get() {
+ if (!mr.ExternalMessageListener.listener_) {
+ mr.ExternalMessageListener.listener_ = new mr.ExternalMessageListener();
+ }
+ return mr.ExternalMessageListener.listener_;
+ }
+};
+
+
+/** @private {?mr.ExternalMessageListener} */
+mr.ExternalMessageListener.listener_ = null;
+
+
+/**
+ * List of app ids which are allowed to use the command messages.
+ * These must also be included in the manifest 'externally_connectable' list.
+ *
+ * @private @const {!Array<string>}
+ */
+mr.ExternalMessageListener.WHITELIST_ = [
+ // ghire kiosk app
+ 'idmofbkcelhplfjnmmdolenpigiiiecc', // prod
+ 'ggedfkijiiammpnbdadhllnehapomdge', // staging
+ 'njjegkblellcjnakomndbaloifhcoccg' // dev
+];
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/external_message_listener_test.js b/chromium/chrome/browser/resources/media_router/extension/src/external_message_listener_test.js
new file mode 100644
index 00000000000..e1f5480a27a
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/external_message_listener_test.js
@@ -0,0 +1,67 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly('external_message_listener_test');
+
+goog.require('mr.ExternalMessageListener');
+goog.require('mr.InternalMessageType');
+goog.require('mr.UnitTestUtils');
+
+describe('Tests mr.ExternalMessageListener', () => {
+ let listener;
+
+ beforeEach(() => {
+ mr.UnitTestUtils.mockChromeApi();
+ listener = new mr.ExternalMessageListener();
+ });
+
+ afterEach(() => {
+ mr.UnitTestUtils.restoreChromeApi();
+ });
+
+ it('rejects sender not in whitelist', () => {
+ const sender = {'id': 'invalid'};
+ const callback = response => {
+ fail('should not have called back');
+ };
+
+ expect(listener.validateEvent({}, sender, callback)).toBe(false);
+ });
+
+ it('invalid type returns empty and closes channel', () => {
+ const sender = {'id': 'njjegkblellcjnakomndbaloifhcoccg'};
+ const callback = response => {
+ fail('should not have called back');
+ };
+
+ expect(listener.validateEvent({}, sender, callback)).toBe(false);
+ });
+
+ it('valid start message', () => {
+ const sender = {'id': 'njjegkblellcjnakomndbaloifhcoccg'};
+ const callback = response => {
+ fail('should not have called back');
+ };
+ const message = {'type': mr.InternalMessageType.START};
+ expect(listener.validateEvent(message, sender, callback)).toBe(true);
+ });
+
+ it('valid stop message', () => {
+ const sender = {'id': 'njjegkblellcjnakomndbaloifhcoccg'};
+ const callback = response => {
+ fail('should not have called back');
+ };
+ const message = {'type': mr.InternalMessageType.STOP};
+ expect(listener.validateEvent(message, sender, callback)).toBe(true);
+ });
+
+ it('valid log subscription message', () => {
+ const sender = {'id': 'njjegkblellcjnakomndbaloifhcoccg'};
+ const callback = response => {
+ fail('should not have called back');
+ };
+ const message = {'type': mr.InternalMessageType.SUBSCRIBE_LOG_DATA};
+ expect(listener.validateEvent(message, sender, callback)).toBe(true);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/externs.js b/chromium/chrome/browser/resources/media_router/extension/src/externs.js
new file mode 100644
index 00000000000..eac0a60d527
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/externs.js
@@ -0,0 +1,917 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// NOTE: Througout this file, we use constructors instead of @typedefs to
+// declare object properties. This prevents the JSCompiler from renaming these
+// properties. These can be converted to shorter @typedef declarations when the
+// JSCompiler adds full support for @typedef in externs.
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for WebRTC PeerConnection as in
+// http://www.w3.org/TR/2012/WD-webrtc-20120821/
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @type {string} */
+MediaStreamEvent.prototype.type;
+
+
+/** @type {string} */
+RTCPeerConnection.prototype.iceConnectionState;
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for processes API
+// See: https://developer.chrome.com/extensions/processes
+//////////////////////////////////////////////////////////////////////////////
+
+
+/**
+ * @const
+ */
+chrome.processes = {};
+
+
+/**
+ * @param {number} tabId
+ * @param {Function} callback
+ */
+chrome.processes.getProcessIdForTab = function(tabId, callback) {};
+
+
+/**
+ * @const
+ */
+chrome.processes.onUpdated = {};
+
+
+/**
+ * @param {Function} callback
+ */
+chrome.processes.onUpdated.addListener = function(callback) {};
+
+
+/**
+ * @param {Function} callback
+ */
+chrome.processes.onUpdated.removeListener = function(callback) {};
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for Tab Capture API
+// See: https://developer.chrome.com/extensions/tabCapture.html
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @const {*} */
+chrome.tabCapture = {};
+
+
+/**
+ * @param {MediaConstraints} constraints
+ * @param {function(MediaStream)} callback
+ */
+chrome.tabCapture.capture = function(constraints, callback) {};
+
+
+/**
+ * @param {string} startUrl
+ * @param {MediaConstraints} constraints
+ * @param {function(MediaStream)} callback
+ */
+chrome.tabCapture.captureOffscreenTab = function(
+ startUrl, constraints, callback) {};
+
+
+/**
+ * @type {ChromeEvent}
+ */
+chrome.tabCapture.onStatusChanged;
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for Desktop Capture API
+// See: https://developer.chrome.com/extensions/desktopCapture.html
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @const */
+chrome.desktopCapture = {};
+
+
+/**
+ * @param {Array<string>} sources
+ * @param {function(string)} callback
+ * @return {number} desktopMediaRequestId
+ */
+chrome.desktopCapture.chooseDesktopMedia = function(sources, callback) {};
+
+
+/**
+ * @param {number} desktopMediaRequestId
+ */
+chrome.desktopCapture.cancelChooseDesktopMedia = function(
+ desktopMediaRequestId) {};
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for chrome.dial API
+// IDL: http://goo.gl/qmKqro
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @const {*} */
+chrome.dial = {};
+
+
+
+/** @constructor */
+chrome.dial.DialDevice = function() {};
+
+
+/** @type {string} */
+chrome.dial.DialDevice.prototype.deviceLabel;
+
+
+/** @type {string} */
+chrome.dial.DialDevice.prototype.deviceDescriptionUrl;
+
+
+/** @type {number} */
+chrome.dial.DialDevice.prototype.configId;
+
+
+/** @constructor */
+chrome.dial.DialDeviceDescription = function() {};
+
+
+/** @type {string} */
+chrome.dial.DialDeviceDescription.prototype.deviceLabel;
+
+
+/** @type {string} */
+chrome.dial.DialDeviceDescription.prototype.appUrl;
+
+
+/** @type {string} */
+chrome.dial.DialDeviceDescription.prototype.deviceDescription;
+
+
+
+/** @constructor */
+chrome.dial.DialError = function() {};
+
+
+/** @type {string} */
+chrome.dial.DialError.prototype.code;
+
+
+/**
+ * @param {function(boolean)} callback The result (true if successful).
+ */
+chrome.dial.discoverNow = function(callback) {};
+
+/**
+ * @param {string} deviceLabel
+ * @param {function(?chrome.dial.DialDeviceDescription)} callback
+ */
+chrome.dial.fetchDeviceDescription = function(deviceLabel, callback) {};
+
+
+/** @type {ChromeEvent} */
+chrome.dial.onDeviceList;
+
+
+/** @type {ChromeEvent} */
+chrome.dial.onError;
+
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for the chrome.cast.channel API
+// IDL: http://goo.gl/G1hmAI
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @const */
+chrome.cast = chrome.cast || {};
+
+
+/** @const */
+chrome.cast.channel = {};
+
+
+
+/** @constructor */
+chrome.cast.channel.ChannelInfo = function() {};
+
+
+/** @type {number} */
+chrome.cast.channel.ChannelInfo.prototype.channelId;
+
+
+/** @type {string} */
+chrome.cast.channel.ChannelInfo.prototype.readyState;
+
+
+/** @type {?string} */
+chrome.cast.channel.ChannelInfo.prototype.errorState;
+
+
+/** @type {?boolean} */
+chrome.cast.channel.ChannelInfo.prototype.keepAlive;
+
+
+/** @type {?boolean} */
+chrome.cast.channel.ChannelInfo.prototype.audioOnly;
+
+
+/** @type {!chrome.cast.channel.ConnectInfo} */
+chrome.cast.channel.ChannelInfo.prototype.connectInfo;
+
+
+
+/** @constructor */
+chrome.cast.channel.MessageInfo = function() {};
+
+
+/** @type {string} */
+chrome.cast.channel.MessageInfo.prototype.namespace_;
+
+
+/** @type {*} */
+chrome.cast.channel.MessageInfo.prototype.data;
+
+
+/** @type {string} */
+chrome.cast.channel.MessageInfo.prototype.sourceId;
+
+
+/** @type {string} */
+chrome.cast.channel.MessageInfo.prototype.destinationId;
+
+
+
+/** @constructor */
+chrome.cast.channel.ConnectInfo = function() {};
+
+
+/** @type {string} */
+chrome.cast.channel.ConnectInfo.prototype.ipAddress;
+
+
+/** @type {number} */
+chrome.cast.channel.ConnectInfo.prototype.port;
+
+
+/** @type {string} */
+chrome.cast.channel.ConnectInfo.prototype.auth;
+
+
+/** @type {number} */
+chrome.cast.channel.ConnectInfo.prototype.timeout;
+
+
+/** @type {number} */
+chrome.cast.channel.ConnectInfo.prototype.livenessTimeout;
+
+
+/** @type {number} */
+chrome.cast.channel.ConnectInfo.prototype.pingInterval;
+
+
+
+/** @constructor */
+chrome.cast.channel.ErrorInfo = function() {};
+
+
+/** @type {string} */
+chrome.cast.channel.ErrorInfo.prototype.errorState;
+
+
+/** @type {?number} */
+chrome.cast.channel.ErrorInfo.prototype.eventType;
+
+
+/** @type {?number} */
+chrome.cast.channel.ErrorInfo.prototype.challengeReplyErrorType;
+
+
+/** @type {?number} */
+chrome.cast.channel.ErrorInfo.prototype.netReturnValue;
+
+
+/** @type {?number} */
+chrome.cast.channel.ErrorInfo.prototype.nssErrorCode;
+
+
+/**
+ * @param {!chrome.cast.channel.ConnectInfo} connectInfo
+ * @param {function(!chrome.cast.channel.ChannelInfo=)} callback
+ */
+chrome.cast.channel.open = function(connectInfo, callback) {};
+
+
+/**
+ * @param {!chrome.cast.channel.ChannelInfo} channel
+ * @param {!chrome.cast.channel.MessageInfo} message
+ * @param {function(!chrome.cast.channel.ChannelInfo=)} callback
+ */
+chrome.cast.channel.send = function(channel, message, callback) {};
+
+
+/**
+ * @param {!chrome.cast.channel.ChannelInfo} channel
+ * @param {function(!chrome.cast.channel.ChannelInfo=)} callback
+ */
+chrome.cast.channel.close = function(channel, callback) {};
+
+
+
+/** @const */
+chrome.cast.channel.onMessage;
+
+
+/**
+ * @param {!function(!chrome.cast.channel.ChannelInfo,
+ * !chrome.cast.channel.MessageInfo)} listener
+ */
+chrome.cast.channel.onMessage.addListener = function(listener) {};
+
+
+/**
+ * @param {!function(!chrome.cast.channel.ChannelInfo,
+ * !chrome.cast.channel.MessageInfo)} listener
+ */
+chrome.cast.channel.onMessage.removeListener = function(listener) {};
+
+
+/** @const */
+chrome.cast.channel.onError;
+
+
+/**
+ * @param {!function(!chrome.cast.channel.ChannelInfo,
+ * (!chrome.cast.channel.ErrorInfo|undefined))} listener
+ */
+chrome.cast.channel.onError.addListener = function(listener) {};
+
+
+/**
+ * @param {!function(!chrome.cast.channel.ChannelInfo,
+ * (!chrome.cast.channel.ErrorInfo|undefined))} listener
+ */
+chrome.cast.channel.onError.removeListener = function(listener) {};
+
+
+/** @const */
+chrome.cast.media = {};
+
+
+/**
+ * @param {function(ChromeWindow)} callback
+ */
+chrome.browserAction.openPopup = function(callback) {};
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for the chrome.cast.streaming APIs
+// See: http://goo.gl/yInHUU
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @const {*} */
+chrome.cast.streaming = {};
+
+
+/** @const {*} */
+chrome.cast.streaming.session = {};
+
+
+/**
+ * @param {?MediaStreamTrack} audio
+ * @param {?MediaStreamTrack} video
+ * @param {!function(?number, ?number, !number)} callback
+ */
+chrome.cast.streaming.session.create = function(audio, video, callback) {};
+
+
+/** @const {*} */
+chrome.cast.streaming.rtpStream = {};
+
+
+
+/** @constructor */
+chrome.cast.streaming.rtpStream.CodecSpecificParams = function() {};
+
+
+
+/** @constructor */
+chrome.cast.streaming.rtpStream.RtpPayloadParams = function() {};
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.maxLatency;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.minLatency;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.animatedLatency;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.payloadType;
+
+
+/** @type {string} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.codecName;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.ssrc;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.feedbackSsrc;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.clockRate;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.minBitrate;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.maxBitrate;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.channels;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.maxFrameRate;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.width;
+
+
+/** @type {number} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.height;
+
+
+/** @type {string} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.aesKey;
+
+
+/** @type {string} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.aesIvMask;
+
+
+/** @type {Array.<chrome.cast.streaming.rtpStream.CodecSpecificParams>} */
+chrome.cast.streaming.rtpStream.RtpPayloadParams.prototype.codecSpecificParams;
+
+
+
+/** @constructor */
+chrome.cast.streaming.rtpStream.RtpParams = function() {};
+
+
+/** @type {chrome.cast.streaming.rtpStream.RtpPayloadParams} */
+chrome.cast.streaming.rtpStream.RtpParams.prototype.payload;
+
+
+/** @type {Array.<string>} */
+chrome.cast.streaming.rtpStream.RtpParams.prototype.rtcpFeatures;
+
+
+/**
+ * @param {!number} streamId
+ */
+chrome.cast.streaming.rtpStream.destroy = function(streamId) {};
+
+
+/**
+ * @param {!number} streamId
+ * @return {!Array.<!chrome.cast.streaming.rtpStream.RtpParams>}
+ */
+chrome.cast.streaming.rtpStream.getSupportedParams = function(streamId) {
+ return [new chrome.cast.streaming.rtpStream.RtpParams];
+};
+
+
+/**
+ * @param {!number} streamId
+ * @param {!chrome.cast.streaming.rtpStream.RtpParams} params
+ */
+chrome.cast.streaming.rtpStream.start = function(streamId, params) {};
+
+
+/**
+ * @param {!number} streamId
+ */
+chrome.cast.streaming.rtpStream.stop = function(streamId) {};
+
+
+/**
+ * @param {!number} streamId
+ * @param {!boolean} enable
+ */
+chrome.cast.streaming.rtpStream.toggleLogging = function(streamId, enable) {};
+
+
+/**
+ * @param {!number} streamId
+ * @param {!string} extraData
+ * @param {!function(ArrayBuffer)} callback
+ */
+chrome.cast.streaming.rtpStream.getRawEvents = function(
+ streamId, extraData, callback) {};
+
+
+/**
+ * @param {!number} streamId
+ * @param {!function(Object)} callback
+ */
+chrome.cast.streaming.rtpStream.getStats = function(streamId, callback) {};
+
+
+/** @const {*} */
+chrome.cast.streaming.rtpStream.onStarted = {};
+
+
+/**
+ * @param {!function(!number)} listener
+ */
+chrome.cast.streaming.rtpStream.onStarted.addListener = function(listener) {};
+
+
+/**
+ * @param {!function(!number)} listener
+ */
+chrome.cast.streaming.rtpStream.onStarted.removeListener = function(listener) {
+};
+
+
+/** @const {*} */
+chrome.cast.streaming.rtpStream.onStopped = {};
+
+
+/**
+ * @param {!function(!number)} listener
+ */
+chrome.cast.streaming.rtpStream.onStopped.addListener = function(listener) {};
+
+
+/**
+ * @param {!function(!number)} listener
+ */
+chrome.cast.streaming.rtpStream.onStopped.removeListener = function(listener) {
+};
+
+
+/** @const {*} */
+chrome.cast.streaming.rtpStream.onError = {};
+
+
+/**
+ * @param {!function(number, string)} listener
+ */
+chrome.cast.streaming.rtpStream.onError.addListener = function(listener) {};
+
+
+/**
+ * @param {!function(number, string)} listener
+ */
+chrome.cast.streaming.rtpStream.onError.removeListener = function(listener) {};
+
+
+
+/** @constructor */
+chrome.cast.streaming.udpTransport.IPEndPoint = function() {};
+
+
+/** @type {string} */
+chrome.cast.streaming.udpTransport.IPEndPoint.prototype.address;
+
+
+/** @type {number} */
+chrome.cast.streaming.udpTransport.IPEndPoint.prototype.port;
+
+
+/**
+ * @param {!number} streamId
+ */
+chrome.cast.streaming.udpTransport.destroy = function(streamId) {};
+
+
+/** @const {*} */
+chrome.cast.streaming.udpTransport = {};
+
+
+/**
+ * @param {!number} transportId
+ * @param {!chrome.cast.streaming.udpTransport.IPEndPoint} ipEndPoint
+ */
+chrome.cast.streaming.udpTransport.setDestination = function(
+ transportId, ipEndPoint) {};
+
+
+/**
+ * @param {!number} transportId
+ * @param {!Object} options
+ */
+chrome.cast.streaming.udpTransport.setOptions = function(transportId, options) {
+};
+
+
+/** @const {*} */
+chrome.mojoPrivate = {};
+
+
+/**
+ * @param {!string} moduleName
+ * @return {!Promise.<T>}
+ * @template T
+ */
+chrome.mojoPrivate.requireAsync = function(moduleName) {
+ return Promise.resolve(Object.create(null));
+};
+
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for UMA analytics
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @const {*} */
+chrome.metricsPrivate = {};
+
+
+/**
+ * Records an elapsed time of no more than 10 seconds. The sample value is
+ * specified in milliseconds.
+ * @param {string} metricName
+ * @param {number} value
+ */
+chrome.metricsPrivate.recordTime = function(metricName, value) {};
+
+
+/**
+ * Records an elapsed time of no more than 3 minutes. The sample value is
+ * specified in milliseconds.
+ * @param {string} metricName
+ * @param {number} value
+ */
+chrome.metricsPrivate.recordMediumTime = function(metricName, value) {};
+
+
+/**
+ * Records an elapsed time of no more than 1 hour. The sample value is
+ * specified in milliseconds.
+ * @param {string} metricName
+ * @param {number} value
+ */
+chrome.metricsPrivate.recordLongTime = function(metricName, value) {};
+
+
+/**
+ * Records an action performed by the user.
+ * @param {string} name
+ */
+chrome.metricsPrivate.recordUserAction = function(name) {};
+
+
+/**
+ * Records a value than can range from 1 to 100.
+ * @param {string} metricName
+ * @param {number} count
+ */
+chrome.metricsPrivate.recordSmallCount = function(metricName, count) {};
+
+
+/**
+ * Returns true if the user opted in to sending crash reports.
+ * @param {!function(boolean)} callback
+ */
+chrome.metricsPrivate.getIsCrashReportingEnabled = function(callback) {};
+
+
+/**
+ * Describes the type of metric that is to be collected.
+ * @typedef {{
+ * metricName: string,
+ * type: string,
+ * min: number,
+ * max: number,
+ * buckets: number
+ * }}
+ */
+var MetricType;
+
+
+/**
+ * Adds a value to the given metric.
+ * @param {MetricType} metricType
+ * @param {number} value
+ */
+chrome.metricsPrivate.recordValue = function(metricType, value) {};
+
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs subset for settingsPrivate API.
+// see chromium/src/third_party/closure_compiler/externs/settings_private.js
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @const {*} */
+chrome.settingsPrivate = {};
+
+
+/**
+ * @enum {string}
+ */
+chrome.settingsPrivate.PrefType = {
+ BOOLEAN: 'BOOLEAN',
+ NUMBER: 'NUMBER',
+ STRING: 'STRING',
+ URL: 'URL',
+ LIST: 'LIST',
+ DICTIONARY: 'DICTIONARY',
+};
+
+
+/**
+ * @typedef {{
+ * key: string,
+ * type: !chrome.settingsPrivate.PrefType,
+ * value: *,
+ * }}
+ */
+chrome.settingsPrivate.PrefObject;
+
+
+/**
+ * @param {string} name
+ * @param {function(!chrome.settingsPrivate.PrefObject):void} callback
+ */
+chrome.settingsPrivate.getPref = function(name, callback) {};
+
+
+/**
+ * @const
+ */
+chrome.settingsPrivate.onPrefsChanged = {};
+
+
+/**
+ * @param {function(!Array<!Object>):void} callback
+ */
+chrome.settingsPrivate.onPrefsChanged.hasListener = function(callback) {};
+
+
+/**
+ * @param {function(!Array<!Object>):void} callback
+ */
+chrome.settingsPrivate.onPrefsChanged.addListener = function(callback) {};
+
+
+/**
+ * @param {function(!Array<!Object>):void} callback
+ */
+chrome.settingsPrivate.onPrefsChanged.removeListener = function(callback) {};
+
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for Google Calendar v3 API as in
+// developers.google.com/google-apps/calendar/v3/reference/events#resource
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @const */
+chrome.cast.calendar = {};
+
+
+
+/** @constructor */
+chrome.cast.calendar.Calendar = function() {};
+
+
+/** @type {string} */
+chrome.cast.calendar.Calendar.prototype.id;
+
+
+
+/** @constructor */
+chrome.cast.calendar.Event = function() {};
+
+
+/** @type {string} */
+chrome.cast.calendar.Event.prototype.summary;
+
+
+/** @type {string} */
+chrome.cast.calendar.Event.prototype.hangoutLink;
+
+
+/**
+ * @typedef {{
+ * date: string,
+ * dateTime: string
+ * }}
+ */
+chrome.cast.calendar.Event.prototype.start;
+
+
+/**
+ * @typedef {{
+ * date: string,
+ * dateTime: string
+ * }}
+ */
+chrome.cast.calendar.Event.prototype.end;
+
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for Google Hangouts v1 API
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @const */
+chrome.cast.hangout = {};
+
+
+/**
+ * @typedef {{
+ * 'service': (string|undefined),
+ * 'value': (string|undefined)
+ * }}
+ */
+chrome.cast.hangout.ExternalKey;
+
+
+/**
+ * @typedef {{
+ * 'hangout_id': (string|undefined),
+ * 'participant_id': (string|undefined),
+ * 'user_id': (string|undefined),
+ * 'display_name': (string|undefined),
+ * 'role': (string|undefined),
+ * 'client_type': (string|undefined),
+ * 'participant_state': (string|undefined),
+ * 'joined': (boolean|undefined)
+ * }}
+ */
+chrome.cast.hangout.Participant;
+
+
+/**
+ * @typedef {{
+ * 'hangout_id': (string|undefined),
+ * 'type': (string|undefined),
+ * 'external_key': (chrome.cast.hangout.ExternalKey|undefined),
+ * 'company_title': (string|undefined),
+ * 'meeting_room_name': (string|undefined),
+ * 'meeting_domain': (string|undefined),
+ * "sharing_url": (string|undefined),
+ * }}
+ */
+chrome.cast.hangout.Hangout;
+
+
+
+/**
+
+ * @see https://developer.chrome.com/extensions/tabs#event-onUpdated
+ * @constructor
+ */
+function TabChangeInfo() {}
+
+
+/** @type {string} */
+TabChangeInfo.prototype.status;
+
+
+/** @type {string} */
+TabChangeInfo.prototype.url;
+
+
+/** @type {boolean} */
+TabChangeInfo.prototype.pinned;
+
+
+/** @type {boolean} */
+TabChangeInfo.prototype.audible;
+
+
+/** @type {string} */
+TabChangeInfo.prototype.favIconUrl;
+
+//////////////////////////////////////////////////////////////////////////////
+// Externs for declarativeWebRequest (not used except for channel checking)
+//////////////////////////////////////////////////////////////////////////////
+
+
+/** @type {Object} */
+chrome.declarativeWebRequest;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/files.gni b/chromium/chrome/browser/resources/media_router/extension/src/files.gni
new file mode 100644
index 00000000000..3770bbca1cd
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/files.gni
@@ -0,0 +1,145 @@
+# Generated file. Do not modify!
+
+mr_module_names = [
+ "common",
+ "mirroring_common",
+ "background_script",
+]
+mr_module_specs = [
+ "module=common:8",
+ "module=mirroring_common:73:common",
+ "module=background_script:4:mirroring_common",
+]
+mr_module_files = [
+ # common files
+ "config.js",
+ "utils/assertions.js",
+ "utils/logger.js",
+ "utils/analytics.js",
+ "utils/promise_resolver.js",
+ "providers/common/retry.js",
+ "utils/xhr_manager.js",
+ "internal_message.js",
+
+ # mirroring_common files
+ "manager/route_message_port.js",
+ "interface_data/issue.js",
+ "manager/cancellable_promise.js",
+ "mirror_services/mirror_analytics.js",
+ "utils/platform_utils.js",
+ "mirror_services/mirror_config.js",
+ "mirror_services/stream_capture/capture_parameters.js",
+ "mirror_services/stream_capture/mirror_media_stream.js",
+ "module.js",
+ "utils/event_analytics.js",
+ "utils/media_source_utils.js",
+ "mirror_services/mirror_service.js",
+ "mirror_services/mirror_service_name.js",
+ "mirror_services/mirror_activity.js",
+ "utils/tab_utils.js",
+ "mirror_services/mirror_session.js",
+ "mirror_services/mirror_settings.js",
+ "webrtc/messages.js",
+ "webrtc/peer_connection_analytics.js",
+ "webrtc/peer_connection.js",
+ "interface_data/sink.js",
+ "persistent_data.js",
+ "utils/base64.js",
+ "utils/sha1.js",
+ "utils/string_utils.js",
+ "providers/common/sink_utils.js",
+ "providers/dial/sink_app_status.js",
+ "providers/dial/dial_sink.js",
+ "event_listener.js",
+ "utils/device_counts.js",
+ "utils/device_counts_provider.js",
+ "utils/promise_utils.js",
+ "providers/common/id_generator.js",
+ "interface_data/media_route_controller.js",
+ "utils/mojo_utils.js",
+ "manager/route_id.js",
+ "interface_data/route.js",
+ "manager/provider.js",
+ "providers/common/runtime_error_utils.js",
+ "interface_data/route_request_error.js",
+ "interface_data/sink_list.js",
+ "manager/presentation_enums.js",
+ "manager/sink_availability.js",
+ "providers/dial/dial_analytics.js",
+ "providers/dial/dial_provider_callbacks.js",
+ "utils/event_target.js",
+ "manager/provider_manager_callbacks.js",
+ "providers/common/net_utils.js",
+ "providers/common/xhr_utils.js",
+ "providers/dial/dial_client.js",
+ "providers/dial/dial_activity.js",
+ "providers/dial/dial_activity_records.js",
+ "providers/dial/dial_sink_discovery_service.js",
+ "providers/dial/dial_app_discovery_service.js",
+ "providers/dial/presentation_url.js",
+ "providers/dial/dial_provider.js",
+ "interface_data/sink_search_criteria.js",
+ "utils/fixed_size_queue.js",
+ "log_manager.js",
+ "utils/object_utils.js",
+ "presentation_services/presentation_session.js",
+ "presentation_services/cloud_webrtc/webrtc_presentation_session.js",
+ "external_message_listener.js",
+ "internal_message_listener.js",
+ "init_helper.js",
+ "interface_data/route_message.js",
+ "interface_data/mojo.js",
+ "manager/mr_event_senders/throttling_sender.js",
+ "manager/mr_event_senders/route_message_sender.js",
+ "manager/provider_events.js",
+ "manager/route_message_port_impl.js",
+ "utils/throttle.js",
+ "manager/provider_manager.js",
+
+ # background_script files
+ "extension_selector.js",
+ "providers/test/test_provider.js",
+ "init.js",
+ "background.js",
+]
+mr_test_files = [
+ "event_listener_test.js",
+ "external_message_listener_test.js",
+ "init_test.js",
+ "interface_data/media_route_controller_test.js",
+ "internal_message_listener_test.js",
+ "manager/mr_event_senders/route_message_sender_test.js",
+ "manager/mr_event_senders/throttling_sender_test.js",
+ "manager/provider_manager_test.js",
+ "manager/route_id_test.js",
+ "mirror_services/mirror_activity_test.js",
+ "mirror_services/mirror_analytics_test.js",
+ "mirror_services/mirror_session_test.js",
+ "mirror_services/mirror_settings_test.js",
+ "mirror_services/stream_capture/mirror_media_stream_test.js",
+ "module_test.js",
+ "persistent_data_test.js",
+ "providers/common/id_generator_test.js",
+ "providers/common/net_utils_test.js",
+ "providers/dial/dial_activity_records_test.js",
+ "providers/dial/dial_analytics_test.js",
+ "providers/dial/dial_app_discovery_service_test.js",
+ "providers/dial/dial_client_test.js",
+ "providers/dial/dial_provider_test.js",
+ "providers/dial/dial_sink_discovery_service_test.js",
+ "providers/dial/dial_sink_test.js",
+ "providers/dial/presentation_url_test.js",
+ "providers/test/test_provider_test.js",
+ "utils/analytics_test.js",
+ "utils/base64_test.js",
+ "utils/event_analytics_test.js",
+ "utils/fixed_size_queue_test.js",
+ "utils/logger_test.js",
+ "utils/media_source_utils_test.js",
+ "utils/mock_promise_test.js",
+ "utils/object_utils_test.js",
+ "utils/sha1_test.js",
+ "utils/throttle_test.js",
+ "utils/xhr_manager_test.js",
+ "webrtc/peer_connection_test.js",
+]
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/init.js b/chromium/chrome/browser/resources/media_router/extension/src/init.js
new file mode 100644
index 00000000000..6e6140e6c94
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/init.js
@@ -0,0 +1,157 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.provide('mr.Init');
+
+goog.require('mr.Config');
+goog.require('mr.ExtensionSelector');
+goog.require('mr.InitHelper');
+goog.require('mr.LogManager');
+goog.require('mr.Logger');
+goog.require('mr.MediaRouterService');
+goog.require('mr.MediumTiming');
+goog.require('mr.PersistentDataManager');
+goog.require('mr.ProviderManager');
+goog.require('mr.TestProvider');
+
+
+/** @private {mr.Logger} */
+mr.Init.logger_ = mr.Logger.getInstance('mr.Init');
+
+
+/** @type {string} */
+mr.Init.FIRST_WAKE_DURATION = 'MediaRouter.Provider.FirstWakeDuration';
+
+
+/** @type {string} */
+mr.Init.WAKE_DURATION = 'MediaRouter.Provider.WakeDuration';
+
+
+/** @private {?mr.MediumTiming} */
+mr.Init.wakeTiming_;
+
+
+/** @private {?mr.ProviderManager} */
+mr.Init.providerManager_;
+
+
+/**
+ * @param {!mr.ProviderManager} providerManager
+ * @return {!Array.<!mr.Provider>}
+ * @private
+ */
+mr.Init.getProviders_ = function(providerManager) {
+ const providers = mr.InitHelper.getProviders(providerManager);
+ if (!mr.Config.isPublicChannel) {
+ providers.push(new mr.TestProvider(providerManager));
+ }
+ return providers;
+};
+
+
+/**
+ * @return {!Promise}
+ * @private
+ */
+mr.Init.initProviderManager_ = function() {
+ return mr.ExtensionSelector.shouldStart()
+ .then(mr.MediaRouterService.getInstance)
+ .then(result => {
+ if (!result['mrService']) {
+ throw Error('Failed to get MR service');
+ }
+ const mrInstanceId = result['mrInstanceId'];
+ if (!mrInstanceId) {
+ throw Error('Failed to get MR instance ID.');
+ }
+ mr.Init.logger_.info('MR instance ID: ' + mrInstanceId);
+ const mediaRouterService =
+ /** @type {!mr.MediaRouterService} */ (result['mrService']);
+ if (!mr.Init.providerManager_) {
+ throw Error('providerManager not initialized.');
+ }
+ /** @type {!mr.ProviderManager} */
+ const providerManager = mr.Init.providerManager_;
+ mediaRouterService.setHandlers(providerManager);
+
+ if (mr.PersistentDataManager.isChromeReloaded(mrInstanceId)) {
+ mr.Init.wakeTiming_.setName(mr.Init.FIRST_WAKE_DURATION);
+ }
+ chrome.runtime.onSuspend.addListener(
+ mr.Init.wakeTiming_.end.bind(mr.Init.wakeTiming_));
+
+ mr.PersistentDataManager.initialize(mrInstanceId);
+
+ mr.LogManager.getInstance().registerDataManager();
+
+ const providers = mr.Init.getProviders_(providerManager);
+ if (!mr.Config.isDebugChannel) {
+ // Log unhandled promise rejections for external channels,
+ // but leave them as thrown exceptions for internal.
+ window.addEventListener('unhandledrejection', event => {
+ let e = event.reason;
+ if (!e.stack) {
+ e = Error(e);
+ }
+ mr.Init.logger_.error(
+ 'Unhandled promise rejection.',
+ /** @type {Error} */ (e));
+ });
+ }
+ providerManager.initialize(
+ mediaRouterService, providers, result['mrConfig']);
+ })
+ .then(undefined, error => {
+ mr.Init.logger_.warning(error.message);
+ throw error;
+ });
+};
+
+
+/**
+ * Exposed for testing.
+
+ * @return {!Array<!mr.EventListener>}
+ * @private
+ */
+mr.Init.getAllListeners_ = function() {
+ return [
+ ...mr.InitHelper.getListeners(),
+ ];
+};
+
+
+/**
+ * Registers all event listeners.
+ * @private
+ */
+mr.Init.addEventListeners_ = function() {
+ mr.Init.getAllListeners_().forEach(
+ eventListener => eventListener.addOnStartup());
+ mr.InitHelper.addEventListeners();
+
+ // Listen for an event that always get invoked on browser startup. This is
+ // necessary because Media Router must know the extension ID in order to wake
+ // the extension up, and MR gets the ID when the event page activates for the
+ // first time. If the event page never activates, then MR will never be able
+ // to connect to it.
+
+ chrome.runtime.onStartup.addListener(() => {});
+};
+
+
+/**
+ * @return {!Promise}
+ */
+mr.Init.init = function() {
+ mr.LogManager.getInstance().init();
+ mr.Init.wakeTiming_ = new mr.MediumTiming(mr.Init.WAKE_DURATION);
+
+ /** @type {!mr.ProviderManager} */
+ const providerManager = new mr.ProviderManager();
+ mr.Init.providerManager_ = providerManager;
+ const promise = mr.Init.initProviderManager_();
+ mr.Init.addEventListeners_();
+ return promise;
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/init_helper.js b/chromium/chrome/browser/resources/media_router/extension/src/init_helper.js
new file mode 100644
index 00000000000..320629a8547
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/init_helper.js
@@ -0,0 +1,61 @@
+/**
+ * @fileoverview Definitions that are specific to the open-source
+ * version of the extension. The functions here don't do anything
+ * useful, but they need to be here because they're called by code
+ * that's shared with the closed-source version.
+ */
+goog.module('mr.InitHelper');
+goog.module.declareLegacyNamespace();
+
+const DialProvider = goog.require('mr.DialProvider');
+const EventListener = goog.require('mr.EventListener');
+const Provider = goog.require('mr.Provider');
+const ProviderManager = goog.forwardDeclare('mr.ProviderManager');
+
+
+/**
+ * @param {!ProviderManager} providerManager
+ * @return {!Array<!Provider>}
+ */
+function getProviders(providerManager) {
+ return [new DialProvider(providerManager)];
+}
+
+
+/**
+ * @return {!Array<!EventListener>}
+ */
+function getListeners() {
+ return [];
+}
+
+
+/**
+ * @return {void}
+ */
+function addEventListeners() {}
+
+
+/**
+ * @return {!Function}
+ */
+function getInternalMessageHandler() {
+ return () => {};
+}
+
+
+/**
+ * @return {!Function}
+ */
+function getExternalMessageHandler() {
+ return () => {};
+}
+
+
+exports = {
+ getProviders,
+ getListeners,
+ addEventListeners,
+ getInternalMessageHandler,
+ getExternalMessageHandler,
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/init_test.js b/chromium/chrome/browser/resources/media_router/extension/src/init_test.js
new file mode 100644
index 00000000000..25ba7d36419
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/init_test.js
@@ -0,0 +1,82 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly('init_test');
+
+goog.require('mr.Init');
+goog.require('mr.MockClock');
+goog.require('mr.Module');
+goog.require('mr.UnitTestUtils');
+
+
+
+describe('Tests init', function() {
+ let mockClock;
+ let savedCallback;
+
+ beforeEach(function() {
+ mr.Module.clearForTest();
+ mockClock = new mr.MockClock(true);
+ mr.UnitTestUtils.mockChromeApi();
+
+ for (let listener of mr.Init.getAllListeners_()) {
+ spyOn(listener, 'addOnStartup').and.callThrough();
+ }
+
+ spyOn(mr.ExtensionSelector, 'shouldStart')
+ .and.returnValue(Promise.resolve());
+ spyOn(mr.MediaRouterService, 'getInstance').and.returnValue({
+ 'mrService': jasmine.createSpyObj(
+ 'mrService', ['setHandlers', 'onRouteMessagesReceived']),
+ 'mrInstanceId': 'mrInstanceId'
+ });
+
+ spyOn(mr.PersistentDataManager, 'initialize');
+ spyOn(mr.PersistentDataManager, 'register');
+ spyOn(mr.Init, 'getProviders_').and.returnValue([]);
+
+ savedCallback = null;
+ chrome.runtime.onSuspend.addListener.and.callFake(callback => {
+ savedCallback = callback;
+ });
+ });
+
+ afterEach(function() {
+ mockClock.uninstall();
+ mr.UnitTestUtils.restoreChromeApi();
+ });
+
+ it('records first wake duration after Chrome reload', function(done) {
+ spyOn(mr.PersistentDataManager, 'isChromeReloaded').and.returnValue(true);
+ mr.Init.init().then(() => {
+ expect(chrome.runtime.onSuspend.addListener).toHaveBeenCalled();
+ expect(savedCallback).not.toBeNull();
+ mockClock.tick(12345);
+ savedCallback();
+ expect(chrome.metricsPrivate.recordMediumTime)
+ .toHaveBeenCalledWith(mr.Init.FIRST_WAKE_DURATION, 12345);
+ done();
+ });
+ });
+
+ it('records wake duration after Chrome reload', function(done) {
+ spyOn(mr.PersistentDataManager, 'isChromeReloaded').and.returnValue(false);
+ mr.Init.init().then(() => {
+ expect(chrome.runtime.onSuspend.addListener).toHaveBeenCalled();
+ expect(savedCallback).not.toBeNull();
+ mockClock.tick(54321);
+ savedCallback();
+ expect(chrome.metricsPrivate.recordMediumTime)
+ .toHaveBeenCalledWith(mr.Init.WAKE_DURATION, 54321);
+ done();
+ });
+ });
+
+ it('Registers event listeners on bootstrap', function(done) {
+ mr.Init.init().then(done, done.fail);
+ for (let listener of mr.Init.getAllListeners_()) {
+ expect(listener.addOnStartup).toHaveBeenCalled();
+ }
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/issue.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/issue.js
new file mode 100644
index 00000000000..c0da74b3fd1
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/issue.js
@@ -0,0 +1,150 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview The issue object shared between component extension
+ * and Chrome media router.
+ */
+
+goog.provide('mr.Issue');
+goog.provide('mr.IssueAction');
+goog.provide('mr.IssueSeverity');
+
+goog.require('mr.Assertions');
+
+/**
+ * Note: keep synced with the issue severity supported by MR's issue manager.
+ * @enum {string}
+ * @export
+ */
+mr.IssueSeverity = {
+ FATAL: 'fatal',
+ WARNING: 'warning',
+ NOTIFICATION: 'notification'
+};
+
+
+/**
+ * Note: keep synced with the issue actions supported by MR's issue manager.
+ * @enum {string}
+ * @export
+ */
+mr.IssueAction = {
+ DISMISS: 'dismiss',
+ LEARN_MORE: 'learn_more'
+};
+
+mr.Issue = class {
+ /**
+ * @param {string} title
+ * @param {mr.IssueSeverity} severity
+ */
+ constructor(title, severity) {
+ /**
+ * @type {?string}
+ * @export
+ */
+ this.routeId = null;
+
+ /**
+ * @type {mr.IssueSeverity}
+ * @export
+ */
+ this.severity = severity;
+
+ /**
+ * When true, this issue takes the whole dialog.
+ * @type {boolean}
+ * @export
+ */
+ this.isBlocking = this.severity == mr.IssueSeverity.FATAL ? true : false;
+
+ /**
+ * Short description about the issue. Localized string.
+ * @type {string}
+ * @export
+ */
+ this.title = title;
+
+ /**
+ * Message about issue detail or how to handle issue.
+ * Messages should be suitable for end users to decide which actions to
+ * take.
+ * @type {?string}
+ * @export
+ */
+ this.message = null;
+
+ /**
+ * @type {mr.IssueAction}
+ * @export
+ */
+ this.defaultAction = mr.IssueAction.DISMISS;
+
+ /**
+ * @type {Array.<mr.IssueAction>}
+ * @export
+ */
+ this.secondaryActions = null;
+
+ /**
+ * Required if one action is LEARN_MORE.
+ * @type {?number}
+ * @export
+ */
+ this.helpPageId = null;
+ }
+
+ /**
+ * Sets the action to LEARN_MORE and sets the pageId that is required by the
+ * action for targeting.
+ * @param {number} pageId
+ * @return {!mr.Issue} This object.
+ */
+ setDefaultActionLearnMore(pageId) {
+ mr.Assertions.assert(pageId > 0);
+ this.helpPageId = pageId;
+ this.defaultAction = mr.IssueAction.LEARN_MORE;
+ return this;
+ }
+
+ /**
+ * @param {Array.<mr.IssueAction>} secondaryActions
+ * @return {!mr.Issue} This object.
+ */
+ setSecondaryActions(secondaryActions) {
+ this.secondaryActions = secondaryActions;
+ return this;
+ }
+
+ /**
+ * @param {string} message
+ * @return {!mr.Issue} This object.
+ */
+ setMessage(message) {
+ this.message = message;
+ return this;
+ }
+
+ /**
+ * @param {boolean} isBlocking
+ * @return {!mr.Issue} This object.
+ */
+ setIsBlocking(isBlocking) {
+ if (!isBlocking && this.severity == mr.IssueSeverity.FATAL) {
+ throw Error('All FATAL issues must be blocking.');
+ }
+ this.isBlocking = isBlocking;
+ return this;
+ }
+
+ /**
+ * @param {string} routeId
+ * @return {!mr.Issue} This object.
+ */
+ setRouteId(routeId) {
+ this.routeId = routeId;
+ return this;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/media_route_controller.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/media_route_controller.js
new file mode 100644
index 00000000000..0b9494b4194
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/media_route_controller.js
@@ -0,0 +1,166 @@
+goog.module('mr.MediaRouteController');
+goog.module.declareLegacyNamespace();
+
+const Logger = goog.require('mr.Logger');
+
+
+/**
+ * Controls a media that is being routed. This base class defines the same set
+ * of APIs as the MediaController Mojo interface defined in
+ * media_controller.mojom in Chromium, and contains a MojoBinding to the
+ * browser (so that MediaController commands from the browser are routed here).
+ * MRPs may extend this class with additional commands.
+ */
+const MediaRouteController = class {
+ /**
+ * @param {string} name Name of the controller (e.g. HangoutsRouteController).
+ * @param {!mojo.InterfaceRequest} controllerRequest The Mojo interface
+ * request to be bound to this controller.
+ * @param {!mojo.MediaStatusObserverPtr} observer The Mojo pointer to the
+ * MediaStatusObserver.
+ */
+ constructor(name, controllerRequest, observer) {
+ /** @protected @const {!Logger} */
+ this.logger = Logger.getInstance('mr.MediaRouteController.' + name);
+
+ /**
+ * The binding used to maintain a Mojo connection with the browser process.
+ * @private {?mojo.Binding}
+ */
+ this.binding_ =
+ new mojo.Binding(mojo.MediaController, this, controllerRequest);
+
+ /**
+ * The observer to send media status updates to.
+ * @private {?mojo.MediaStatusObserverPtr}
+ */
+ this.observer_ = observer;
+
+ /**
+ * @protected @const {!mojo.MediaStatus}
+ */
+ this.currentMediaStatus = new mojo.MediaStatus({
+ title: '',
+ description: '',
+ duration: new mojo.TimeDelta({microseconds: 0}),
+ current_time: new mojo.TimeDelta({microseconds: 0})
+ });
+
+ /**
+ * @private {boolean}
+ */
+ this.disposed_ = false;
+
+ this.binding_.setConnectionErrorHandler(
+ this.onMojoConnectionError.bind(this));
+ this.observer_.ptr.setConnectionErrorHandler(
+ this.onMojoConnectionError.bind(this));
+ }
+
+ /**
+ * Drops the reference to the binding.
+ * @protected
+ */
+ onMojoConnectionError() {
+ this.dispose();
+ this.onControllerInvalidated();
+ }
+
+ /**
+ * Closes the connection in the binding and the observer. There will be no
+ * more incoming or outgoing calls after this.
+ * @final
+ */
+ dispose() {
+ if (this.disposed_) {
+ return;
+ }
+ this.disposed_ = true;
+ this.disposeInternal();
+ if (this.binding_) {
+ this.binding_.close();
+ this.binding_ = null;
+ }
+ if (this.observer_) {
+ this.observer_.ptr.reset();
+ this.observer_ = null;
+ }
+ }
+
+ /**
+ * Notifies the observer of media status update, if it exists.
+ * @protected
+ */
+ notifyObserver() {
+ if (this.observer_) {
+ this.observer_.onMediaStatusUpdated(this.currentMediaStatus);
+ }
+ }
+
+ /**
+ * Performs additional cleanup when the controller is being disposed.
+ * @protected
+ */
+ disposeInternal() {}
+
+ /**
+ * Performs final cleanup after the controller is invalidated by a Mojo
+ * connection error. By the time this is called, dispose() has already
+ * happened. This gives an opportunity for clients to drop their references
+ * to the controller.
+ * @protected
+ */
+ onControllerInvalidated() {}
+
+ // The following are methods for handling incoming media commands. The method
+ // names must match the ones defined in media_controller.mojom in Chromium
+ // and be exported.
+
+ /**
+ * Plays the media.
+ * @export
+ */
+ play() {}
+
+ /**
+ * Pauses the media.
+ * @export
+ */
+ pause() {}
+
+ /**
+ * Mutes or unmutes the media.
+ * @param {boolean} mute
+ * @export
+ */
+ setMute(mute) {}
+
+ /**
+ * Sets the volume on the media. The given volume must be between 0 and 1.
+ * @param {number} volume
+ * @export
+ */
+ setVolume(volume) {}
+
+ /**
+ * Seeks to the given time. The given time must be non-negative and less than
+ * or equal to the duration of the media.
+ * @param {!mojo.TimeDelta} time
+ * @export
+ */
+ seek(time) {}
+
+ /**
+ * Binds the given request to an implementation that accepts Hangouts-specific
+ * commands.
+ * @param {!mojo.InterfaceRequest} hangoutsControllerRequest Interface request
+ * for Hangouts controller.
+ * @export
+ */
+ connectHangoutsMediaRouteController(hangoutsControllerRequest) {
+ hangoutsControllerRequest.close();
+ throw new Error('Not implemented');
+ }
+};
+
+exports = MediaRouteController;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/media_route_controller_test.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/media_route_controller_test.js
new file mode 100644
index 00000000000..b0fd150f7e3
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/media_route_controller_test.js
@@ -0,0 +1,70 @@
+goog.module('mr.MediaRouteControllerTest');
+goog.setTestOnly('mr.MediaRouteControllerTest');
+
+const MediaRouteController = goog.require('mr.MediaRouteController');
+const UnitTestUtils = goog.require('mr.UnitTestUtils');
+
+describe('MediaRouteController test', () => {
+ let binding;
+ let observer;
+
+ beforeEach(() => {
+ UnitTestUtils.mockMojoApi();
+ binding = UnitTestUtils.createMojoBindingSpyObj();
+ observer = UnitTestUtils.createMojoMediaStatusObserverSpyObj();
+ spyOn(mojo, 'Binding').and.returnValue(binding);
+ });
+
+ function createController() {
+ const controllerRequest = {};
+ const controller =
+ new MediaRouteController('TestController', controllerRequest, observer);
+ expect(mojo.Binding)
+ .toHaveBeenCalledWith(
+ mojo.MediaController, controller, controllerRequest);
+ expect(binding.setConnectionErrorHandler).toHaveBeenCalled();
+ expect(observer.ptr.setConnectionErrorHandler).toHaveBeenCalled();
+ return controller;
+ }
+
+ it('Sets things up on construction', () => {
+ createController();
+ });
+
+ it('Cleans up on Mojo connection error', () => {
+ const controller = createController();
+ spyOn(controller, 'onControllerInvalidated').and.callThrough();
+ const onMojoConnectionError =
+ binding.setConnectionErrorHandler.calls.argsFor(0)[0];
+ onMojoConnectionError();
+ expect(binding.close).toHaveBeenCalled();
+ expect(observer.ptr.reset).toHaveBeenCalled();
+ expect(controller.onControllerInvalidated.calls.count()).toBe(1);
+ });
+
+ it('Cleans up on calling dispose()', () => {
+ const controller = createController();
+ spyOn(controller, 'disposeInternal').and.callThrough();
+ controller.dispose();
+ expect(binding.close).toHaveBeenCalled();
+ expect(observer.ptr.reset).toHaveBeenCalled();
+ expect(controller.disposeInternal.calls.count()).toBe(1);
+ // Calling dispose twice has no effect.
+ controller.dispose();
+ expect(controller.disposeInternal.calls.count()).toBe(1);
+ });
+
+ it('Notifies observer', () => {
+ const controller = createController();
+ controller.notifyObserver();
+ expect(observer.onMediaStatusUpdated)
+ .toHaveBeenCalledWith(controller.currentMediaStatus);
+ });
+
+ it('Does not notify observer after cleanup', () => {
+ const controller = createController();
+ controller.dispose();
+ controller.notifyObserver();
+ expect(observer.onMediaStatusUpdated).not.toHaveBeenCalled();
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/mojo.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/mojo.js
new file mode 100644
index 00000000000..3cacd0478af
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/mojo.js
@@ -0,0 +1,415 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Closure definitions of Mojo service objects.
+ */
+
+goog.provide('mr.MediaRouterRequestHandler');
+goog.provide('mr.MediaRouterService');
+
+goog.require('mr.RouteMessage');
+
+
+/**
+ * @interface
+ *
+ * Don't convert this interface to an ES6 class! Doing so causes things to
+ * break in strange ways.
+ */
+mr.MediaRouterService = function() {};
+
+
+
+/**
+ * @return {!Promise<!Object>}
+ */
+mr.MediaRouterService.getInstance = function() {
+ if (!chrome.mojoPrivate || !chrome.mojoPrivate.requireAsync) {
+ return Promise.reject(Error('No mojo service loaded'));
+ }
+ return new Promise((resolve, reject) => {
+ // requireAsync does not return failures by design, so there is no error
+ // handler here.
+ chrome.mojoPrivate.requireAsync('media_router_bindings').then(mr => {
+ const mediaRouter = /** @type {!mr.MediaRouterService} */ (mr);
+ // Import all definitions into mojo namespace.
+
+ mojo = mediaRouter.getMojoExports && mediaRouter.getMojoExports();
+ mediaRouter.start().then(result => {
+ resolve({
+ 'mrService': mediaRouter,
+
+ 'mrInstanceId': result['instance_id'] || result,
+ 'mrConfig': result['config']
+ });
+ });
+ });
+ });
+};
+
+
+/**
+ * @param {!mr.MediaRouterRequestHandler} handlers
+ * @export
+ */
+mr.MediaRouterService.prototype.setHandlers;
+
+
+/**
+ * Starts the MediaRouterService.
+ * @return {!Promise<!{
+ * instance_id: string,
+ * config: !mojo.MediaRouteProviderConfig
+ * }>} Resolved with an Object containing the result when MediaRouterService is
+ * started.
+ * @export
+ */
+mr.MediaRouterService.prototype.start;
+
+
+/**
+ * Returns an Object containing Mojo definitions exported by
+ * media_router_bindings.
+ * @return {!Object}
+ * @export
+ */
+mr.MediaRouterService.prototype.getMojoExports;
+
+
+/**
+ * @param {string} source
+ * @param {!Array.<!mr.Sink>} sinks
+ * @param {!Array<!mojo.Origin>} origins
+ * @export
+ */
+mr.MediaRouterService.prototype.onSinksReceived;
+
+
+/**
+ * @param {string} pseudoSinkId
+ * @param {string} sinkId
+ * @export
+ */
+mr.MediaRouterService.prototype.onSearchSinkIdReceived;
+
+
+/**
+ * Used by Media Router Provider Manager to inform Media Router that there is an
+ * issue.
+ * @param {!mr.Issue} issue
+ * @export
+ */
+mr.MediaRouterService.prototype.onIssue;
+
+
+/**
+ * Used by Media Router Provider Manager to inform Media Router that the list
+ * of routes has been updated.
+ * @param {Array.<mr.Route>} routes
+ * @param {string=} opt_source
+ * @param {Array.<string>=} opt_nonLocalJoinableRouteIds
+ * @export
+ */
+mr.MediaRouterService.prototype.onRoutesUpdated;
+
+
+/**
+ * Used by Media Router Provider Manager to inform Media Router that sink
+ * availability has changed.
+ * @param {!mr.SinkAvailability} availability
+ * @export
+ */
+mr.MediaRouterService.prototype.onSinkAvailabilityUpdated;
+
+
+/**
+ * Used by Media Router Provider Manager to inform Media Router that the state
+ * of a presentation connected to a route has changed.
+ * @param {string} routeId
+ * @param {string} state
+ * @export
+ */
+mr.MediaRouterService.prototype.onPresentationConnectionStateChanged;
+
+
+/**
+ * Used by Media Router Provider Manager to inform Media Router that the
+ * presentation connected to a route has closed.
+ * @param {string} routeId
+ * @param {string} reason
+ * @param {string} message
+ * @export
+ */
+mr.MediaRouterService.prototype.onPresentationConnectionClosed;
+
+
+/**
+ * Informs Media Router of route messages received from the media sink to which
+ * the route is connected.
+ * @param {string} routeId
+ * @param {!Array<!mr.RouteMessage>} messages
+ * @export
+ */
+mr.MediaRouterService.prototype.onRouteMessagesReceived;
+
+
+/**
+ * Disables or enables extension event page suspension.
+ * @param {boolean} keepAlive
+ * @export
+ */
+mr.MediaRouterService.prototype.setKeepAlive;
+
+
+/**
+ * Gets the current keep alive state.
+ * @return {boolean}
+ * @export
+ */
+mr.MediaRouterService.prototype.getKeepAlive;
+
+
+/**
+ * Informs Media Router that a MirrorServiceRemoter is created for the given
+ * tab. The Media Router may use remoterPtr to control media remoting. The Media
+ * Router should also bind sourceRequest to an implementation to receive updates
+ * on the remoting session.
+ * @param {number} tabId
+ * @param {!mojo.MirrorServiceRemoterPtr} remoterPtr
+ * @param {!mojo.InterfaceRequest} sourceRequest
+ * @export
+ */
+mr.MediaRouterService.prototype.onMediaRemoterCreated;
+
+
+/**
+ * @interface
+ */
+mr.MediaRouterRequestHandler = function() {};
+
+
+/**
+ * Will be called immediately before any other handler method is invoked.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.onBeforeInvokeHandler;
+
+
+/**
+ * Creates a route for |mediaSource| to |sinkId|.
+ * @param {string} sourceUrn The URN of the media being displayed.
+ * @param {string} sinkId
+ * @param {string} presentationId A presentation ID to use.
+
+ * @param {!mojo.Origin|string=} origin
+ * @param {number=} tabId
+ * @param {number=} timeoutMillis If positive, the timeout to use in place
+ * of default timeout.
+ * @param {boolean=} offTheRecord If true, the request is from an off the
+ * record (incognito) browser profile.
+ * @return {!Promise<!mr.Route>} Fulfilled with route created if successful.
+ * Rejected otherwise.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.createRoute;
+
+
+/**
+ * Joins an existing route.
+ *
+ * @param {string} sourceUrn
+ * @param {string} presentationId A presentation ID for Presentation API
+ * client; A Cast session ID for Cast join; and 'autojoin' for Cast auto Join
+ * case;
+ * @param {!mojo.Origin|string} origin
+ * @param {number} tabId
+ * @param {number=} timeoutMillis If positive, the timeout to use in place
+ * of default timeout.
+ * @param {boolean=} offTheRecord If true, the request is from an off the
+ * record (incognito) browser profile.
+ * @return {!Promise<!mr.Route>} Fulfilled with route joined if successful.
+ * Rejected otherwise.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.joinRoute;
+
+
+/**
+ * Joins an existing route by route Id.
+ *
+ * @param {string} sourceUrn
+ * @param {string} routeId An existing route Id to join.
+ * @param {string} presentationId The presentation Id of the route being
+ * created.
+ * @param {!mojo.Origin|string} origin
+ * @param {number} tabId
+ * @param {number=} timeoutMillis If positive, the timeout to use in place
+ * of default timeout.
+ * @return {!Promise<!mr.Route>} Fulfilled with route connected if successful.
+ * Rejected otherwise.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.connectRouteByRouteId;
+
+
+/**
+ * Terminates the route specified by |routeId|.
+ * @param {string} routeId The ID of the route to be terminated.
+ * @return {!Promise<void>} Resolved if the route was terminated, rejected
+ * otherwise.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.terminateRoute;
+
+
+/**
+ * Starts querying for sinks capable of displaying |sourceUrn|.
+ * @param {string} sourceUrn The URN of the media.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.startObservingMediaSinks;
+
+
+/**
+ * Stops querying for sinks capable of displaying |sourceUrn|.
+ * @param {string} sourceUrn The URN of the media.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.stopObservingMediaSinks;
+
+
+/**
+ * Sends a message to the sink via a media route.
+ * @param {string} routeId
+ * @param {!Object|string} message The message to post. Object will be
+ * serialized to a JSON string.
+ * @param {Object=} opt_extraInfo Extra info about how to send a message.
+ * @return {!Promise} Fulfilled when the message is posted, or rejected
+ * if there an error.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.sendRouteMessage;
+
+
+/**
+ * Sends a binary message to the sink via a media route.
+ * @param {string} routeId
+ * @param {!Uint8Array} data The binary message to send.
+ * @return {!Promise} Fulfilled when the message is sent, or rejected
+ * if there an error.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.sendRouteBinaryMessage;
+
+
+/**
+ * Called when the MediaRouter wants to start receiving messages from the media
+ * sink for the route identified by |routeId|.
+ * @param {!string} routeId
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.startListeningForRouteMessages;
+
+
+/**
+ * Called when the MediaRouter wants to stop getting further messages
+ * associated with the routeId.
+ * @param {!string} routeId
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.stopListeningForRouteMessages;
+
+
+/**
+ * Informs the Media Router Provider Manager to send updates on routes list.
+ * @param {string} sourceUrn The URN of the media.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.startObservingMediaRoutes;
+
+
+/**
+ * Informs the Media Router Provider Manager to stop sending updates on routes
+ * list.
+ * @param {string} sourceUrn The URN of the media.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.stopObservingMediaRoutes;
+
+
+/**
+ * Informs the Media Router Provider Manager that a presentation connection has
+ * detached from its underlying media route due to garbage collection or
+ * explicit close().
+ * @param {!string} routeId
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.detachRoute;
+
+
+/**
+ * Enables mDNS discovery. No-ops if it is already enabled. Calling this will
+ * trigger a firewall prompt on Windows if there is not already a firewall rule
+ * for mDNS.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.enableMdnsDiscovery;
+
+
+/**
+ * Searches the appropriate provider for a sink matching |searchCriteria| that
+ * is compatible with |sourceUrn|. The provider that is searched is chosen to be
+ * the one that owns the pseudo sink identified by |sinkId|. If any sinks are
+ * found, sinks observers for |sourceUrn| will be invoked via onSinksReceived()
+ * with those sinks included. The function will return the ID of a sink to which
+ * a route can be created or the empty string if no such sink exists.
+ *
+ * Note that returning the empty string does not necessarily mean no matching
+ * sinks were found. The provider could find multiple matching sinks and not
+ * know how to choose a single one for the route creation. The MRPM will return
+ * the sink on which the user is most likely to create the next route. However,
+ * the manager does not enforce that the provider creates at most one sink in
+ * response to a search.
+ *
+ * @param {string} sinkId Sink ID of the pseudo sink that generated the request.
+ * @param {string} sourceUrn Source to be used with the sink.
+ * @param {!mr.SinkSearchCriteria} searchCriteria Sink search criteria for the
+ * MRP's which includes the user's current domain.
+ * @return {!Promise<string>} Fulfilled with the ID of a sink to which a route
+ * can be created. Rejected otherwise.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.searchSinks;
+
+/**
+ * Called when Media Router finishes sink discovery.
+ * @param {string} providerName Name of provider where the sinks come from.
+ * @param {!Array<!mojo.Sink>} list of sinks discovered by Media Router.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.provideSinks;
+
+/**
+ * Updates sinks even if sinkAvailability is UNAVAILABLE to allow for query
+ * based discovery providers an opportuntity to find sinks.
+ * @param {string} sourceUrn The URN of the media.
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.updateMediaSinks;
+
+
+/**
+ * Creates and returns the MediaRouteController instance for the given route.
+ * Also sets the media status observer for the given route.
+ * Rejects if the controller cannot be created, or if the controller
+ * already exists.
+ * @param {string} routeId
+ * @param {!mojo.InterfaceRequest} controllerRequest The Mojo request object to
+ * be bound to the controller created.
+ * @param {!mojo.MediaStatusObserverPtr} observer The observer's Mojo pointer.
+ * @return {!Promise<void>}
+ * @export
+ */
+mr.MediaRouterRequestHandler.prototype.createMediaRouteController;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/route.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/route.js
new file mode 100644
index 00000000000..b42e4fe42b1
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/route.js
@@ -0,0 +1,178 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview The media route data object shared between component extension
+ * and Chrome media router.
+ */
+
+goog.provide('mr.Route');
+goog.require('mr.RouteId');
+
+mr.Route = class {
+ /**
+ * @param {string} id The route ID.
+ * @param {string} presentationId The ID of the associated presentation.
+ * @param {string} sinkId The ID of the sink running this route.
+ * @param {?string} sourceUrn The media source being sent through this route.
+ * @param {boolean} isLocal Whether the route is requested locally.
+ * @param {string} description
+ * @param {?string} iconUrl
+ */
+ constructor(
+ id, presentationId, sinkId, sourceUrn, isLocal, description, iconUrl) {
+ /**
+ * @type {string}
+ * @export
+ */
+ this.id = id;
+
+ /**
+ * The ID of the presentation associated with this route.
+ * @type {string}
+ * @export
+ */
+ this.presentationId = presentationId;
+
+ /**
+ * The ID of the sink associated with this route.
+ * @type {string}
+ * @export
+ */
+ this.sinkId = sinkId;
+
+ /**
+ * The media source being sent through this route.
+ * Non-null if |isLocal| is true.
+ * For discovered routes, the media source may not be available.
+ * @type {?string}
+ * @export
+ */
+ this.mediaSource = sourceUrn;
+
+ /**
+ * Whether the route was created locally or is associated with a Cast
+ * session
+ * that was created locally.
+ * @type {boolean}
+ * @export
+ */
+ this.isLocal = isLocal;
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.description = description;
+
+ /**
+ * @type {?string}
+ * @export
+ */
+ this.iconUrl = iconUrl;
+
+ /**
+ * When false, this route cannot be stopped by the provider managing it.
+ * @type {boolean}
+ * @export
+ */
+ this.allowStop = true;
+
+ /**
+
+ * @type {?string}
+ * @export
+ */
+ this.customControllerPath = null;
+
+ /**
+
+ * @type {boolean}
+ * @export
+ */
+ this.supportsMediaRouteController = false;
+
+ /**
+ * The type of controller associated with this route, or kNone if controller
+ * is not supported for the route.
+
+ * @type {mojo.RouteControllerType}
+ * @export
+ */
+ this.controllerType =
+ mojo && mojo.RouteControllerType && mojo.RouteControllerType.kNone;
+
+ /**
+ * If set to true, this route should be displayed for |sinkId| in UI.
+ * @type {boolean}
+ * @export
+ */
+ this.forDisplay = true;
+
+ /**
+ * If true, the route was created by an off the record (incognito) browser
+ * profile.
+ * @type {boolean}
+ * @export
+ */
+ this.offTheRecord = false;
+
+ /**
+ * This field is used to identify routes that were created locally as
+ * opposed
+ * to isLocal which can identify both locally created routes and non-local
+ * routes associated with a Cast session that was created locally. The
+ * initial
+ * value of this field is the same as isLocal.
+ * @type {boolean}
+ */
+ this.createdLocally = isLocal;
+
+ /**
+ * If true, the route was created for offscreen presentation (1-UA mode).
+ * @type {boolean}
+ * @export
+ */
+ this.isOffscreenPresentation = false;
+ }
+
+ /**
+ * @param {!string} presentationId
+ * @param {!string} providerName
+ * @param {!string} sinkId The ID of the sink running this route.
+ * @param {?string} source The media source being sent through this route.
+ * @param {!boolean} isLocal Whether the route is requested locally.
+ * @param {!string} description
+ * @param {?string} iconUrl
+ * @return {!mr.Route}
+ */
+ static createRoute(
+ presentationId, providerName, sinkId, source, isLocal, description,
+ iconUrl) {
+ return new mr.Route(
+ mr.RouteId.getRouteId(presentationId, providerName, sinkId, source),
+ presentationId, sinkId, source, isLocal, description, iconUrl);
+ }
+};
+
+/**
+ * @typedef {{
+ * tabId: (?number|undefined),
+ * sessionId: string,
+ * sinkIpAddress: string,
+ * sinkModelName: string,
+ * sinkFriendlyName: (string|undefined),
+ * activity: (!mr.mirror.Activity|undefined)
+ * }}
+ */
+mr.Route.MirrorInitData;
+
+
+/**
+ * The field is only used inside component extension to pass MRP specific data
+ * to the corresponding mirroring service. For example, cast streaming needs
+ * sink IP address and session ID.
+ * @type {mr.Route.MirrorInitData}
+ */
+mr.Route.prototype.mirrorInitData;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/route_message.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/route_message.js
new file mode 100644
index 00000000000..a796a271b80
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/route_message.js
@@ -0,0 +1,48 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview The route message object shared between component extension
+ * and Chrome media router.
+ */
+
+goog.provide('mr.RouteMessage');
+
+mr.RouteMessage = class {
+ /**
+ * @param {string} routeId
+ * @param {string|!Uint8Array} message
+ */
+ constructor(routeId, message) {
+ /**
+ * @type {string}
+ * @export
+ */
+ this.routeId = routeId;
+
+ /**
+ * @type {string|!Uint8Array}
+ * @export
+ */
+ this.message = message;
+ }
+
+ /**
+ * @param {!mr.RouteMessage} routeMessage
+ * @return {boolean} true if the message is in binary format.
+ */
+ static isBinary(routeMessage) {
+ return typeof routeMessage.message != 'string';
+ }
+
+ /**
+ * @param {!mr.RouteMessage} routeMessage
+ * Returns the length of the message if it is a string, otherwise 0.
+ * @return {number}
+ */
+ static stringLength(routeMessage) {
+ return mr.RouteMessage.isBinary(routeMessage) ? 0 :
+ routeMessage.message.length;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/route_request_error.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/route_request_error.js
new file mode 100644
index 00000000000..ac0092f098e
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/route_request_error.js
@@ -0,0 +1,89 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Extension of Error for route request errors.
+ */
+
+goog.provide('mr.RouteRequestError');
+goog.provide('mr.RouteRequestResultCode');
+
+
+/**
+ * Keep in sync with:
+ * - RouteRequestResultCode in media_router.mojom
+ * - RouteRequestResult::ResultCode in route_request_result.h
+ * - MediaRouteProviderResult enum in tools/metrics/histograms.xml
+ * @enum {number}
+ */
+mr.RouteRequestResultCode = {
+ UNKNOWN_ERROR: 0,
+ OK: 1,
+ TIMED_OUT: 2,
+ ROUTE_NOT_FOUND: 3,
+ SINK_NOT_FOUND: 4,
+ INVALID_ORIGIN: 5,
+ OFF_THE_RECORD_MISMATCH: 6,
+ NO_SUPPORTED_PROVIDER: 7,
+ CANCELLED: 8,
+};
+
+mr.RouteRequestError = class extends Error {
+ /**
+ * @param {!mr.RouteRequestResultCode} errorCode
+ * @param {string=} opt_message
+ * @param {string=} opt_stack
+ */
+ constructor(errorCode, opt_message, opt_stack) {
+ super();
+
+ this.name = 'RouteRequestError';
+ this.message = opt_message || '';
+ if (opt_stack) {
+ this.stack = opt_stack;
+ } else {
+ // Attempt to ensure there is a stack trace.
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, mr.RouteRequestError);
+ } else {
+ const stack = new Error().stack;
+ if (stack) {
+ this.stack = stack;
+ }
+ }
+ }
+
+ /** @type {!mr.RouteRequestResultCode} */
+ this.errorCode = errorCode;
+ }
+
+ /**
+ * If the given error is a mr.RouteRequestError, returns it. Otherwise, a
+ * returns a mr.RouteRequestError with the error's message and UNKNOWN error
+ * code.
+ * @param {*} error
+ * @return {!mr.RouteRequestError}
+ */
+ static wrap(error) {
+ if (error instanceof mr.RouteRequestError) {
+ return error;
+ } else if (error instanceof Error) {
+ return new mr.RouteRequestError(
+ mr.RouteRequestResultCode.UNKNOWN_ERROR, error.message, error.stack);
+ } else {
+ return new mr.RouteRequestError(mr.RouteRequestResultCode.UNKNOWN_ERROR);
+ }
+ }
+
+ /**
+ * @param {*} error Possibly an instance of mr.RouteRequestError.
+ * @return {boolean} True if the argument represents a timeout.
+ */
+ static isTimeout(error) {
+ if (error instanceof mr.RouteRequestError) {
+ return error.errorCode == mr.RouteRequestResultCode.TIMED_OUT;
+ }
+ return false;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink.js
new file mode 100644
index 00000000000..e11b6dcc597
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink.js
@@ -0,0 +1,79 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.provide('mr.Sink');
+goog.provide('mr.SinkIconType');
+
+
+/**
+ * Sink icon types defined in Chrome.
+ * To define a new icon, add it to Chrome first and then here.
+ * @enum {string}
+ */
+mr.SinkIconType = {
+ CAST: 'cast',
+ CAST_AUDIO_GROUP: 'cast_audio_group',
+ CAST_AUDIO: 'cast_audio',
+ MEETING: 'meeting',
+ HANGOUT: 'hangout',
+ EDUCATION: 'education',
+ GENERIC: 'generic',
+};
+
+
+
+/**
+ * Represents a device which can be a destination for a media route.
+ */
+mr.Sink = class {
+ /**
+ * @param {string} id The ID of the sink.
+ * @param {string} friendlyName The human readable name of the sink.
+ * @param {mr.SinkIconType=} iconType
+ * @param {?string=} description The human readable description of
+ * the sink. The description is displayed below the sink name,
+ * and may contain more detailed information about the sink
+ * (e.g., meeting description).
+ * @param {?string=} domain The Dasher domain associated with the
+ * sink.
+ */
+ constructor(
+ id, friendlyName, iconType = undefined, description = null,
+ domain = null) {
+ // For some reason the current public release of jscompiler gets upset if
+ // mr.SinkIconType.GENERIC is specified as a default argument.
+ iconType = iconType || mr.SinkIconType.GENERIC;
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.id = id;
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.friendlyName = friendlyName;
+
+ /**
+ * @type {mr.SinkIconType}
+ * @export
+ */
+ this.iconType = iconType;
+
+ /**
+ * @type {?string}
+ * @export
+ */
+ this.description = description;
+
+ /**
+ * A non-null domain signifies the sink is tied to a Dasher domain.
+ * @type {?string}
+ * @export
+ */
+ this.domain = domain;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink_list.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink_list.js
new file mode 100644
index 00000000000..f430d2f0428
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink_list.js
@@ -0,0 +1,34 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Data structure returned by mr.Provider.getAvailableSinks.
+ */
+
+goog.provide('mr.SinkList');
+
+mr.SinkList = class {
+ /**
+ * @param {!Array<mr.Sink>} sinks
+ * @param {Array<string>=} opt_origins List of origins that have access to the
+ * sink list. If not provided, the sink list is accessible from all origins.
+ */
+ constructor(sinks, opt_origins) {
+ /**
+ * @type {!Array<!mr.Sink>}
+ * @export
+ */
+ this.sinks = sinks;
+
+ /**
+ * @type {?Array<string>}
+ * @export
+ */
+ this.origins = opt_origins || null;
+ }
+};
+
+
+/** @const {!mr.SinkList} */
+mr.SinkList.EMPTY = new mr.SinkList([]);
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink_search_criteria.js b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink_search_criteria.js
new file mode 100644
index 00000000000..9dd8ea094af
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/interface_data/sink_search_criteria.js
@@ -0,0 +1,30 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview The sink search criteria shared between component extension and
+ * Chrome media router.
+ */
+
+goog.provide('mr.SinkSearchCriteria');
+
+mr.SinkSearchCriteria = class {
+ /**
+ * @param {string} input
+ * @param {?string} domain
+ */
+ constructor(input, domain) {
+ /**
+ * @type {string}
+ * @export
+ */
+ this.input = input;
+
+ /**
+ * @type {?string}
+ * @export
+ */
+ this.domain = domain;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/internal_message.js b/chromium/chrome/browser/resources/media_router/extension/src/internal_message.js
new file mode 100644
index 00000000000..debdaab4d6f
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/internal_message.js
@@ -0,0 +1,62 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Internal messages between parts of the extension, e.g., event
+ * page, background page, e2e test page.
+ *
+
+ */
+
+goog.provide('mr.InternalMessage');
+goog.provide('mr.InternalMessageType');
+
+
+/**
+ * @enum {string}
+ */
+mr.InternalMessageType = {
+ // Feedback ==> event page
+ SUBSCRIBE_LOG_DATA: 'subscribe_log_data',
+ RETRIEVE_LOG_DATA: 'retrieve_log_data',
+ // Control Messages
+ // App ==> Cloud MRP
+ START: 'start',
+ STOP: 'stop',
+ // Responses
+ // Cloud MRP ==> App
+ ROUTE: 'route',
+ STOPPED: 'stopped',
+ ERROR: 'error',
+ // App ==> Log Subscribers
+ SUBSCRIBED: 'subscribed',
+ LOG_MESSAGE: 'log_message',
+};
+
+mr.InternalMessage = class {
+ /**
+ * @param {string} source ID of source of message.
+ * @param {mr.InternalMessageType} type The message type.
+ * @param {*=} opt_message
+ */
+ constructor(source, type, opt_message) {
+ /**
+ * @type {string}
+ * @export
+ */
+ this.source = source;
+
+ /**
+ * @type {mr.InternalMessageType}
+ * @export
+ */
+ this.type = type;
+
+ /**
+ * @type {*}
+ * @export
+ */
+ this.message = opt_message;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/internal_message_listener.js b/chromium/chrome/browser/resources/media_router/extension/src/internal_message_listener.js
new file mode 100644
index 00000000000..c55869c2b4a
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/internal_message_listener.js
@@ -0,0 +1,61 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Handles internal messages that arrive in the
+ * chrome.runtime.onMessage event. It is assumed that all incoming messages have
+ * type mr.InternalMessage.
+ */
+
+goog.provide('mr.InternalMessageListener');
+
+goog.require('mr.EventAnalytics');
+goog.require('mr.EventListener');
+goog.require('mr.InternalMessage');
+goog.require('mr.InternalMessageType');
+
+
+/**
+ * @extends {mr.EventListener<ChromeEvent>}
+ */
+mr.InternalMessageListener = class extends mr.EventListener {
+ constructor() {
+ super(
+ mr.EventAnalytics.Event.RUNTIME_ON_MESSAGE, 'InternalMessageListener',
+ mr.ModuleId.PROVIDER_MANAGER, chrome.runtime.onMessage);
+ }
+
+ /**
+ * @override
+ */
+ validateEvent(message, sender, sendResponse) {
+ const internalMessage = /** @type {mr.InternalMessage} */ (message);
+ return internalMessage.type == mr.InternalMessageType.RETRIEVE_LOG_DATA &&
+ sender.id == chrome.runtime.id &&
+ sender.url == `chrome-extension://${sender.id}/feedback.html`;
+ }
+
+ /**
+ * @override
+ */
+ deferredReturnValue() {
+ // Indicates the messaging channel should be kept open until
+ // sendResponse() is called.
+ return true;
+ }
+
+ /**
+ * @return {!mr.InternalMessageListener}
+ */
+ static get() {
+ if (!mr.InternalMessageListener.listener_) {
+ mr.InternalMessageListener.listener_ = new mr.InternalMessageListener();
+ }
+ return mr.InternalMessageListener.listener_;
+ }
+};
+
+
+/** @private {?mr.InternalMessageListener} */
+mr.InternalMessageListener.listener_ = null;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/internal_message_listener_test.js b/chromium/chrome/browser/resources/media_router/extension/src/internal_message_listener_test.js
new file mode 100644
index 00000000000..f9e64ae45cb
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/internal_message_listener_test.js
@@ -0,0 +1,52 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly('internal_message_listener_test');
+
+goog.require('mr.InternalMessageListener');
+
+describe('Tests mr.InternalMessageListener', () => {
+ let listener;
+
+ const failCallback = response => {
+ fail('should not have called back');
+ };
+ const validEvent = {'type': mr.InternalMessageType.RETRIEVE_LOG_DATA};
+ const validSender = {
+ 'id': 'foo',
+ 'url': 'chrome-extension://foo/feedback.html'
+ };
+ beforeEach(() => {
+ chrome.runtime = {id: 'foo'};
+ listener = new mr.InternalMessageListener();
+ });
+
+ it('rejects invalid extension id', () => {
+ const sender = {
+ 'id': 'invalid',
+ 'url': 'chrome-extension://invalid/feedback.html'
+ };
+
+ const result = listener.validateEvent(validEvent, sender, failCallback);
+ expect(result).toBe(false);
+ });
+
+ it('rejects invalid extension url', () => {
+ const sender = {'id': 'foo', 'url': 'chrome-extension://foo/invalid.html'};
+
+ const result = listener.validateEvent(validEvent, sender, failCallback);
+ expect(result).toBe(false);
+ });
+
+ it('rejects invalid message type', () => {
+ const result = listener.validateEvent(
+ {'type': mr.InternalMessageType.START}, validSender, failCallback);
+ expect(result).toBe(false);
+ });
+
+ it('passes validation', () => {
+ const result = listener.validateEvent(validEvent, validSender, () => {});
+ expect(result).toBe(true);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/log_manager.js b/chromium/chrome/browser/resources/media_router/extension/src/log_manager.js
new file mode 100644
index 00000000000..791aed8c247
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/log_manager.js
@@ -0,0 +1,241 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Log manager which enables and collects both fine and info logs,
+ * and provides method to get logs with parameter to include or exclude
+ * fine logs.
+
+ */
+
+goog.provide('mr.LogManager');
+
+goog.require('mr.Config');
+goog.require('mr.FixedSizeQueue');
+goog.require('mr.Logger');
+goog.require('mr.PersistentData');
+goog.require('mr.PersistentDataManager');
+
+
+/**
+ * @implements {mr.PersistentData}
+ */
+mr.LogManager = class {
+ constructor() {
+ /** @private @const */
+ this.buffer_ = new mr.FixedSizeQueue(mr.LogManager.BUFFER_SIZE);
+
+ /** @private @const */
+ this.startTime_ = Date.now();
+ }
+
+ /**
+ * @return {!mr.LogManager}
+ */
+ static getInstance() {
+ if (mr.LogManager.instance_ == null) {
+ mr.LogManager.instance_ = new mr.LogManager();
+ }
+ return mr.LogManager.instance_;
+ }
+
+ /**
+ * Init
+ */
+ init() {
+ mr.Logger.level = this.getDefaultLogLevel_();
+ const browserLogger = mr.Logger.getInstance('browser');
+ const oldErrorHandler = window.onerror;
+ /**
+ * @param {string} message
+ * @param {string} url
+ * @param {number} line
+ * @param {number=} col
+ * @param {*=} error
+ */
+ window.onerror = (message, url, line, col, error) => {
+ if (oldErrorHandler) {
+ oldErrorHandler(message, url, line, col, error);
+ }
+ browserLogger.error(`Error: ${message} (${url} @ Line: ${line})`, error);
+ };
+ mr.Logger.addHandler(this.onNewLog_.bind(this));
+
+ // Override log level via localStorage setting
+ const debugKey = 'debug.logs';
+ const debugLevel = window.localStorage[debugKey];
+ if (debugLevel) {
+ mr.Logger.level = mr.Logger.stringToLevel(
+ debugLevel.toUpperCase(), mr.Logger.Level.FINE);
+ } else if (!mr.Config.isPublicChannel) {
+ // Record the default local level in local settings so developers can
+ // easily change it without having to look up the name of the setting.
+ window.localStorage[debugKey] =
+ mr.Logger.levelToString(mr.Logger.DEFAULT_LEVEL);
+ }
+
+ const consoleKey = 'debug.console';
+ if (!mr.Config.isPublicChannel && window.localStorage[consoleKey] == null) {
+ // Enable console logging by default in internal builds. Any value other
+ // than 'false' or '' is treated as true.
+ window.localStorage[consoleKey] = 'true';
+ }
+ const consoleValue = window.localStorage[consoleKey];
+ if (consoleValue && consoleValue.toLowerCase() != 'false') {
+ mr.Logger.addHandler(this.logToConsole_.bind(this));
+ }
+ }
+
+ /**
+ * Saves logs in the internal buffer.
+ *
+ * @param {mr.Logger.Record} logRecord The log entry.
+ * @private
+ */
+ onNewLog_(logRecord) {
+ this.buffer_.enqueue(this.formatRecord_(logRecord, false));
+ const exception = logRecord.exception;
+ if (exception instanceof Error && exception.stack) {
+ this.buffer_.enqueue(exception.stack);
+ }
+ }
+
+ /**
+ * @param {mr.Logger.Record} logRecord The log entry.
+ * @private
+ */
+ logToConsole_(logRecord) {
+ const args = [this.formatRecord_(logRecord, true)];
+ if (logRecord.exception) {
+ args.push(logRecord.exception);
+ }
+ switch (logRecord.level) {
+ case mr.Logger.Level.SEVERE:
+ console.error(...args);
+ break;
+ case mr.Logger.Level.WARNING:
+ console.warn(...args);
+ break;
+ case mr.Logger.Level.INFO:
+ console.log(...args);
+ break;
+ default:
+ console.debug(...args);
+ }
+ }
+
+ /**
+ * @param {!mr.Logger.Record} record
+ * @param {boolean} forConsole
+ * @return {string}
+ * @private
+ */
+ formatRecord_(record, forConsole) {
+ const sb = ['['];
+ if (forConsole) {
+ // Format relative timestamp.
+ const seconds = (Date.now() - this.startTime_) / 1000;
+ sb.push((' ' + seconds.toFixed(3)).slice(-7));
+ } else {
+ // Format absolute timestamp.
+ const date = new Date(record.time);
+ const twoDigitStr = num => num < 10 ? '0' + num : num;
+ sb.push(
+ date.getFullYear().toString(), '-', twoDigitStr(date.getMonth() + 1),
+ '-', twoDigitStr(date.getDate()), ' ', twoDigitStr(date.getHours()),
+ ':', twoDigitStr(date.getMinutes()), ':',
+ twoDigitStr(date.getSeconds()), '.',
+ twoDigitStr(Math.floor(date.getMilliseconds() / 10)));
+ }
+ sb.push(
+ '][', mr.Logger.levelToString(record.level), '][', record.logger, '] ',
+ record.message);
+ // Don't append the exception when logging to the console, because it will
+ // be handled specially later.
+ if (!forConsole && record.exception != null) {
+ sb.push('\n');
+ if (record.exception instanceof Error) {
+ sb.push(record.exception.message);
+ } else {
+ try {
+ sb.push(JSON.stringify(record.exception));
+ } catch (e) {
+ sb.push(record.exception.toString());
+ }
+ }
+ }
+ sb.push('\n');
+ return sb.join('');
+ }
+
+ /**
+ * Get the logs in log buffer.
+ * @return {string}
+ */
+ getLogs() {
+ if (this.buffer_.getCount() == 0) {
+ return 'NA';
+ }
+
+ return this.buffer_.getValues().join('');
+ }
+
+ /**
+ * @return {!mr.Logger.Level} The default log level.
+ * @private
+ */
+ getDefaultLogLevel_() {
+ return mr.Config.isPublicChannel ? mr.Logger.Level.INFO :
+ mr.Logger.Level.FINE;
+ }
+
+ /**
+ * Registers with the data manager and loads any previous logs.
+ */
+ registerDataManager() {
+ mr.PersistentDataManager.register(this);
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'LogManager';
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ return [this.buffer_.getValues()];
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {
+ const currentLogs = this.buffer_.getValues();
+ this.buffer_.clear();
+ for (let log of mr.PersistentDataManager.getTemporaryData(this) || []) {
+ this.buffer_.enqueue(log);
+ }
+ for (let log of currentLogs) {
+ this.buffer_.enqueue(log);
+ }
+ }
+};
+
+
+/**
+ * @private {mr.LogManager}
+ */
+mr.LogManager.instance_ = null;
+
+
+/**
+ * The max number of logs in buffer. The old logs get pushed out when the buffer
+ * is full.
+ * @const
+ */
+mr.LogManager.BUFFER_SIZE = 1000;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/cancellable_promise.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/cancellable_promise.js
new file mode 100755
index 00000000000..0e0807b4575
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/cancellable_promise.js
@@ -0,0 +1,255 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.CancellablePromise');
+goog.module.declareLegacyNamespace();
+
+
+/**
+ * A promise packaged together with a cancel() method.
+ *
+ * To understand cancellation, for consider a chain of ordinary Promise objects
+ * P, Q, and R, where Q is the result of calling P.then, and R is the result of
+ * calling Q.then. Whenever Q is rejected, the rejection is propagated "down"
+ * the chain to R, but P is unaffected.
+ *
+ * Now consider a chain of CancellablePromise objects P, Q, and R, created using
+ * the chain() method below. Rejection behaves the same as with ordinary
+ * Promise objects, but whenever Q is cancelled, the cancellation is propagated
+ * "up" the chain to P. Because a cancelled promise is also rejected, calling
+ * Q.cancel() also causes the rejection to be propagated to R. In this way,
+ * cancellation propagates both up and down a chain of related promises.
+ *
+ * For an explanation of how this class is used in practice (in the interaction
+ * between ProviderManager and RouteProvider classes), see the diagram in
+ * <route-creation-timeout.svg.gz>.
+ *
+ * @template T
+ */
+class CancellablePromise {
+ /**
+ * @param {function(function(T), function(*))} init Function
+ * called immediatiately with resolve and reject functions passed as
+ * arguments.
+ * @param {?function(*)=} onCancelled Optional function to be called when this
+ * CancellablePromise is cancelled.
+ */
+ constructor(init, onCancelled = null) {
+ /**
+ * @private {function(*)}
+ */
+ this.reject_;
+
+ /**
+ * @private {?function(*)}
+ */
+ this.onCancelled_ = onCancelled;
+
+ /**
+ * @const
+ */
+ this.promise = new Promise((resolve, reject) => {
+ const resolve1 = value => {
+ this.onCancelled_ = null;
+ resolve(value);
+ };
+ const reject1 = reason => {
+ this.onCancelled_ = null;
+ reject(reason);
+ };
+ this.reject_ = reject1;
+ init(resolve1, reject1);
+ });
+ }
+
+ /**
+ * Cancels this promise.
+ *
+ * Does nothing if |this.promise| is already settled. Otherwise, causes
+ * |this.promise| to be rejected with |reason|, and causes the |onHandled|
+ * function passed to this class's constructor to be called with |reason|.
+ *
+ * @param {*} reason
+ */
+ cancel(reason) {
+ this.reject_(reason);
+ if (this.onCancelled_) {
+ const onCancelled = this.onCancelled_;
+ this.onCancelled_ = null; // ensure cancel() method is idempotent
+ setTimeout(() => onCancelled(reason), 0);
+ }
+ }
+
+ /**
+ * Chains CancellablePromises together like the then() method of ordinary
+ * promises, with one extra feature: if the "child" promise returned by this
+ * method is cancelled, then this promise is automatically cancelled as well.
+ *
+ * @param {?function(T):U} onResolved Function called when this promise is
+ * resolved. The argument is the value with which this promise was
+ * resolved. If the function returns, the return value is used to resolve
+ * the child promise. If the function throws an error, the child promise
+ * is rejected with that error. If this argument is null, it is
+ * equivalent to passing a function that returns its argument.
+ * @param {?function(*):U=} onRejected Function called when this promise is
+ * rejected. The argument is the reason with which this promise was
+ * rejected. If the function returns, the return value is used to resolve
+ * the child promise. If the function throws an error, the child promise
+ * is rejected with that error. If this argument is missing or null, it
+ * is equivalent to passing a function that throws its argument.
+ * @return {!CancellablePromise<U>} A child promise which is resolved or
+ * rejected depending on the result of calling |onResolved| or
+ * |onRejected|.
+ * @template U
+ */
+ chain(onResolved, onRejected = null) {
+ return new CancellablePromise(
+ (resolve, reject) => {
+ this.promise.then(
+ value => {
+ if (onResolved) {
+ try {
+ resolve(onResolved(value));
+ } catch (reason) {
+ reject(reason);
+ }
+ } else {
+ resolve(value);
+ }
+ },
+ reason => {
+ if (onRejected) {
+ try {
+ resolve(onRejected(reason));
+ } catch (reason2) {
+ reject(reason2);
+ }
+ } else {
+ reject(reason);
+ }
+ });
+ },
+ reason => {
+ this.cancel(reason);
+ });
+ }
+
+ /**
+ * Make it Promise by expose then.
+ * @param {?function(T):U} onResolved Function called when this promise is
+ * resolved. The argument is the value with which this promise was
+ * resolved. If the function returns, the return value is used to resolve
+ * the child promise. If the function throws an error, the child promise
+ * is rejected with that error. If this argument is null, it is
+ * equivalent to passing a function that returns its argument.
+ * @param {?function(*):U=} onRejected Function called when this promise is
+ * rejected. The argument is the reason with which this promise was
+ * rejected. If the function returns, the return value is used to resolve
+ * the child promise. If the function throws an error, the child promise
+ * is rejected with that error. If this argument is missing or null, it
+ * is equivalent to passing a function that throws its argument.
+ * @return {!CancellablePromise<U>} A child promise which is resolved or
+ * rejected depending on the result of calling |onResolved| or
+ * |onRejected|.
+ * @template U
+ */
+ then(onResolved, onRejected = null) {
+ return this.chain(onResolved, onRejected);
+ }
+
+ /**
+ * Shorthand for .chain(null, onRejected).
+ * @param {?function(*):T} onRejected
+ * @return {!CancellablePromise<T>}
+ */
+ catch(onRejected) {
+ return this.chain(null, onRejected);
+ }
+
+ /**
+ * Utility function create a promise in a resolved state.
+ * @param {T} value
+ * @return {!CancellablePromise<T>}
+ * @template T
+ */
+ static resolve(value) {
+ return new CancellablePromise((resolve, reject) => {
+ resolve(value);
+ });
+ }
+
+ /**
+ * Utility function create a promise in a rejected state.
+ * @param {*} reason
+ * @return {!CancellablePromise<T>}
+ * @template T
+ */
+ static reject(reason) {
+ return new CancellablePromise((resolve, reject) => {
+ reject(reason);
+ });
+ }
+
+ /**
+ * Utility function to wrap a Promise.
+ *
+
+ *
+ * @param {!Promise<T>} promise
+ * @return {!CancellablePromise<T>}
+ * @template T
+ */
+ static forPromise(promise) {
+ return new CancellablePromise((resolve, reject) => {
+ promise.then(resolve, reject);
+ });
+ }
+
+ /**
+ * Produces a CancellablePromise |outer| that runs the following steps:
+ *
+ * In the normal case, |outer| waits for a regular (non-cancellable) Promise
+ * |promise| to resolve to |value|. Then it calls |startCancellableStep|,
+ * passing |value| as the argument, to produce a CancellablePromise |inner|.
+ * When |inner| is settled, the result is used to settle |outer|.
+ *
+ * If |outer| is cancelled before |promise| is resolved, then |value| is
+ * discarded and |startCancellableStep| is not called.
+ *
+ * If |outer| is cancelled after |promise| is resolved, then |inner| is
+ * cancelled as well.
+ *
+ * If |promise| is rejected, then |outer| is rejected as well.
+ *
+ * @param {!Promise<A>} promise
+ * @param {function(A):!CancellablePromise<B>} startCancellableStep
+ * @return {!CancellablePromise<B>}
+ * @template A, B
+ */
+ static withUncancellableStep(promise, startCancellableStep) {
+ /** @type {boolean} */
+ let wasCancelled = false;
+ /** @type {CancellablePromise} */
+ let innerPromise = null;
+
+ return new CancellablePromise(
+ (resolve, reject) => {
+ promise.then(value => {
+ if (!wasCancelled) {
+ innerPromise = startCancellableStep(value);
+ innerPromise.promise.then(resolve, reject);
+ }
+ }, reject);
+ },
+ reason => {
+ if (innerPromise) {
+ innerPromise.cancel(reason);
+ } else {
+ wasCancelled = true;
+ }
+ });
+ }
+}
+
+exports = CancellablePromise;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/route_message_sender.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/route_message_sender.js
new file mode 100644
index 00000000000..ca16b6a578c
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/route_message_sender.js
@@ -0,0 +1,344 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview A sender for sending route message to MR. When MR asks for
+ * the next batch of messages, this sender sends matching messages if
+ * available, or buffer the request till message arrives.
+ *
+
+ */
+
+goog.provide('mr.RouteMessageSender');
+
+goog.require('mr.Assertions');
+goog.require('mr.Logger');
+goog.require('mr.PersistentData');
+goog.require('mr.PersistentDataManager');
+goog.require('mr.RouteMessage');
+goog.require('mr.ThrottlingSender');
+
+
+/**
+ * @implements {mr.PersistentData}
+ */
+mr.RouteMessageSender = class extends mr.ThrottlingSender {
+ /**
+ * @param {!mr.ProviderManagerCallbacks} providerManagerCallbacks
+ * @param {number} messageSizeKeepAliveThreshold
+ * @final
+ */
+ constructor(providerManagerCallbacks, messageSizeKeepAliveThreshold) {
+ super(mr.RouteMessageSender.SEND_MESSAGE_INTERVAL_MILLIS);
+
+ /**
+ * Route ID to queue map. One queue per route.
+ * @private {!Map<string, !Array<!mr.RouteMessage>>}
+ */
+ this.queues_ = new Map();
+
+ /**
+ * Set of routes being listened for messages.
+ * @private {!Set<string>}
+ */
+ this.listeningRouteIds_ = new Set();
+
+ /**
+ * Callback to invoke to send a batch of route messages. Set during
+ * |init()|. The first argument is the route id, and the second argument is
+ * the batch of messages to send.
+ * @private {?function(string, !Array<!mr.RouteMessage>)}
+ */
+ this.sendMessagesCallback_ = null;
+
+ /**
+ * Sum of sizes of all string messages currently queued up. When this is
+ * above a certain threshold, the extension will be kept alive due to
+ * performance reasons.
+ * @private {number}
+ */
+ this.totalMessageSize_ = 0;
+
+ /**
+ * Number of enqueued binary messages. The extension will be kept alive
+ * while there are enqueued binary messages.
+ * @private {number}
+ */
+ this.binaryMessageCount_ = 0;
+
+ /**
+ * See |totalMessageSize_| and |binaryMessageCount_|.
+ * @private {boolean}
+ */
+ this.shouldKeepAlive_ = false;
+
+ /**
+ * @private {!mr.ProviderManagerCallbacks}
+ */
+ this.providerManagerCallbacks_ = providerManagerCallbacks;
+
+ /**
+ * @private {number}
+ */
+ this.messageSizeKeepAliveThreshold_ = messageSizeKeepAliveThreshold;
+
+ /** @private {mr.Logger} */
+ this.logger_ = mr.Logger.getInstance('mr.RouteMessageSender');
+ }
+
+ /**
+ * @param {!function(string, !Array<!mr.RouteMessage>)} sendMessagesCallback
+ * The callback to invoke to send a batch of route messages. See comments
+ * on |sendMessagesCallback_|.
+ */
+ init(sendMessagesCallback) {
+ this.sendMessagesCallback_ = sendMessagesCallback;
+ mr.PersistentDataManager.register(this);
+ }
+
+ /**
+ * Starts listening for route messages associated with |routeId|, and schedule
+ * a task to send any available messages to the Media Router.
+ *
+ * @param {string} routeId
+ */
+ listenForRouteMessages(routeId) {
+ if (this.listeningRouteIds_.has(routeId)) {
+ return;
+ }
+
+ this.listeningRouteIds_.add(routeId);
+ if (this.hasMessageFrom_(routeId)) {
+ this.scheduleSend();
+ }
+ }
+
+ /**
+ * The media router wants to stop getting further messages associated with the
+ * routeId until it issues listenForRouteMessages again.
+ *
+ * @param {string} routeId
+ */
+ stopListeningForRouteMessages(routeId) {
+ this.listeningRouteIds_.delete(routeId);
+ }
+
+ /**
+ * Called when there is a |message| for |routeId| available to be sent.
+ * @param {string} routeId
+ * @param {string|!Uint8Array} message
+ */
+ send(routeId, message) {
+ let queue = this.queues_.get(routeId);
+ if (!queue) {
+ queue = [];
+ this.queues_.set(routeId, queue);
+ }
+
+ const routeMessage = new mr.RouteMessage(routeId, message);
+ queue.push(routeMessage);
+
+ // If the queue size for this route has grown suspiciously large, log
+ // warnings as the queue size grows past the warning threshold.
+ //
+
+ if (queue.length > mr.RouteMessageSender.QUEUE_SIZE_WARNING_THRESHOLD_ &&
+ queue.length % mr.RouteMessageSender.QUEUE_SIZE_WARNING_THRESHOLD_ ==
+ 1) {
+ this.logger_.warning(
+ () => `Message queue length is excessively large ` +
+ `(${queue.length}) for route ${routeId}`);
+ }
+
+ this.totalMessageSize_ += mr.RouteMessage.stringLength(routeMessage);
+ if (mr.RouteMessage.isBinary(routeMessage)) {
+ this.binaryMessageCount_++;
+ }
+
+ this.updateShouldKeepAlive_();
+ if (this.listeningRouteIds_.has(routeId)) {
+ this.scheduleSend();
+ }
+ }
+
+ /**
+ * Removes queue on route removal.
+ * @param {string} routeId
+ */
+ onRouteRemoved(routeId) {
+ this.listeningRouteIds_.delete(routeId);
+ const queue = this.queues_.get(routeId);
+ if (queue) {
+ this.queues_.delete(routeId);
+ this.onMessagesRemoved_(queue);
+ this.updateShouldKeepAlive_();
+ }
+ }
+
+ /**
+ * Update message size and binary message counters as |messages| are being
+ * removed from the queue.
+ * @param {!Array<!mr.RouteMessage>} messages
+ * @private
+ */
+ onMessagesRemoved_(messages) {
+ if (messages.length == 0) {
+ return;
+ }
+
+ for (let message of messages) {
+ this.totalMessageSize_ -= mr.RouteMessage.stringLength(message);
+ if (mr.RouteMessage.isBinary(message)) {
+ this.binaryMessageCount_--;
+ }
+ }
+ }
+
+ /**
+ * @param {string} routeId
+ * @return {boolean} True if there is at least one message from the route.
+ * @private
+ */
+ hasMessageFrom_(routeId) {
+ const queue = this.queues_.get(routeId);
+ return !!queue && queue.length > 0;
+ }
+
+ /**
+ * Computes whether the extension should be kept alive, and informs the
+ * Provider Manager if that value changed.
+ * @private
+ */
+ updateShouldKeepAlive_() {
+ const newShouldKeepAlive = this.binaryMessageCount_ > 0 ||
+ this.totalMessageSize_ > this.messageSizeKeepAliveThreshold_;
+ if (newShouldKeepAlive != this.shouldKeepAlive_) {
+ this.shouldKeepAlive_ = newShouldKeepAlive;
+ this.providerManagerCallbacks_.requestKeepAlive(
+ this.getStorageKey(), newShouldKeepAlive);
+ }
+ }
+
+ /**
+ * @override
+ */
+ doSend() {
+ if (!this.sendMessagesCallback_) {
+ this.logger_.error(
+ 'sendMessagesCallback not set. Messages not delivered.');
+ return;
+ }
+
+ for (const routeId of this.listeningRouteIds_) {
+ const queue = this.queues_.get(routeId);
+ if (!queue || (queue.length == 0)) {
+ continue;
+ }
+ this.sendMessagesCallback_(routeId, queue);
+ this.onMessagesRemoved_(queue);
+ this.queues_.set(routeId, []);
+ }
+ this.updateShouldKeepAlive_();
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'mr.RouteMessageSender';
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ // Assumption: While there are binary messages in any queue, the extension
+ // keep-alive should be turned on. Thus, we should not encounter binary
+ // messages while persisting the queues here.
+ const persistableQueues = [...this.queues_.entries()].map(entry => {
+ return [
+ entry[0],
+ entry[1].map(
+ message => mr.Assertions.assertString(
+ message.message, 'No support for persisting binary messages'))
+ ];
+ });
+ return [new mr.RouteMessageSender.PersistentData_(
+ persistableQueues, Array.from(this.listeningRouteIds_),
+ this.totalMessageSize_)];
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {
+ const savedData = /** @type {?mr.RouteMessageSender.PersistentData_} */
+ (mr.PersistentDataManager.getTemporaryData(this));
+ if (savedData) {
+ this.queues_ = new Map();
+ for (const entry of savedData.queues) {
+ const routeId = /** @type {string} */ (entry[0]);
+ // Assumption: In getData(), there should not have been any binary
+ // messages persisted. Therefore, only string messages should be
+ // restored here.
+ const queue = (/** @type {!Array<*>} */ (entry[1])).map(message => {
+ return new mr.RouteMessage(
+ routeId,
+ mr.Assertions.assertString(
+ message, 'No support for restoring binary messages'));
+ });
+ this.queues_.set(routeId, queue);
+ }
+ this.listeningRouteIds_ = new Set(savedData.listeningRouteIds);
+ this.totalMessageSize_ = savedData.totalMessageSize;
+ }
+ }
+};
+
+
+/**
+ * The interval at which messages will be sent back to Media Router.
+ * @const {number}
+ */
+mr.RouteMessageSender.SEND_MESSAGE_INTERVAL_MILLIS = 20;
+
+
+/**
+ * Generally, no route should have more than 50 messages in queue. However,
+ * there may be momentary spikes for high-volume communications (e.g., RPC
+ * traffic).
+ * @private @const {number}
+ */
+mr.RouteMessageSender.QUEUE_SIZE_WARNING_THRESHOLD_ = 50;
+
+
+/**
+ * If the total number of characters in all enqueued string messages exceeds
+ * this threshold, the extension will be kept alive for performance reasons.
+ * @const {number}
+ */
+mr.RouteMessageSender.MESSAGE_SIZE_KEEP_ALIVE_THRESHOLD = 512 * 1024;
+
+
+/**
+ * @private
+ */
+mr.RouteMessageSender.PersistentData_ = class {
+ /**
+ * @param {!Array<Array<*>>} queues An array where each element is an
+ * 2-element array of [routeId, messages].
+ * @param {!Array<string>} listeningRouteIds
+ * @param {number} totalMessageSize
+ */
+ constructor(queues, listeningRouteIds, totalMessageSize) {
+ /** @type {!Array<Array<*>>} */
+ this.queues = queues;
+
+ /** @type {!Array<string>} */
+ this.listeningRouteIds = listeningRouteIds;
+
+ /** @type {number} */
+ this.totalMessageSize = totalMessageSize;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/route_message_sender_test.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/route_message_sender_test.js
new file mode 100644
index 00000000000..31b885e82dc
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/route_message_sender_test.js
@@ -0,0 +1,173 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly();
+goog.require('mr.MockClock');
+goog.require('mr.PersistentDataManager');
+goog.require('mr.RouteMessage');
+goog.require('mr.RouteMessageSender');
+goog.require('mr.UnitTestUtils');
+
+
+
+describe('Tests RouteMessageSender', function() {
+ let mockClock;
+ let providerManagerCallbacks;
+ let callback;
+ let sender;
+ const routeId1 = 'r1';
+ const routeId2 = 'r2';
+ const text1 = 'message1';
+ const messageSizeThreshold = 100;
+
+ beforeEach(function() {
+ mr.UnitTestUtils.mockChromeApi();
+ chrome.runtime.onSuspend.addListener = l => {
+ onSuspendListener = l;
+ };
+
+ mr.PersistentDataManager.clear();
+ mockClock = new mr.MockClock(true);
+ providerManagerCallbacks =
+ jasmine.createSpyObj('pmCallbacks', ['requestKeepAlive']);
+ sender = new mr.RouteMessageSender(
+ providerManagerCallbacks, messageSizeThreshold);
+ callback = jasmine.createSpy('sendCallback');
+ sender.init(callback);
+ });
+
+ afterEach(function() {
+ mr.PersistentDataManager.clear();
+ mockClock.uninstall();
+ mr.UnitTestUtils.restoreChromeApi();
+ });
+
+ it('hasMessageFrom_', function() {
+ expect(sender.hasMessageFrom_(routeId1)).toBe(false);
+ sender.send(routeId1, text1);
+ expect(sender.hasMessageFrom_(routeId1)).toBe(true);
+ });
+
+ it('No msg when requested; new msg sent when arrives', function() {
+ sender.listenForRouteMessages(routeId1);
+ sender.send(routeId1, text1);
+ expect(callback).toHaveBeenCalledWith(
+ routeId1, [new mr.RouteMessage(routeId1, text1)]);
+ expect(sender.hasMessageFrom_(routeId1)).toBe(false);
+ });
+
+ it('Has msg when requested', function() {
+ sender.send(routeId1, text1);
+ expect(callback).not.toHaveBeenCalled();
+
+ sender.listenForRouteMessages(routeId1);
+ expect(callback).toHaveBeenCalledWith(
+ routeId1, [new mr.RouteMessage(routeId1, text1)]);
+ mockClock.tick(mr.RouteMessageSender.SEND_MESSAGE_INTERVAL_MILLIS);
+ expect(sender.hasMessageFrom_(routeId1)).toBe(false);
+ });
+
+ it('onRouteRemoved removes messages', function() {
+ sender.send(routeId1, text1);
+ expect(sender.hasMessageFrom_(routeId1)).toBe(true);
+ sender.onRouteRemoved(routeId1);
+ expect(sender.hasMessageFrom_(routeId1)).toBe(false);
+
+ expect(sender.totalMessageSize_).toEqual(0);
+ expect(sender.binaryMessageCount_).toEqual(0);
+ expect(sender.shouldKeepAlive_).toBe(false);
+ });
+
+ it('stopListeningForRouteMessages does not remove messages', function() {
+ sender.send(routeId1, text1);
+ expect(sender.hasMessageFrom_(routeId1)).toBe(true);
+ sender.stopListeningForRouteMessages(routeId1);
+ expect(sender.hasMessageFrom_(routeId1)).toBe(true);
+ });
+
+ it('requestKeepAlive due to binary message', function() {
+ const binaryArray1 = new Uint8Array(12);
+ const binaryArray2 = new Uint8Array(34);
+ sender.send(routeId1, binaryArray1);
+ expect(providerManagerCallbacks.requestKeepAlive)
+ .toHaveBeenCalledWith(sender.getStorageKey(), true);
+ providerManagerCallbacks.requestKeepAlive.calls.reset();
+
+ sender.send(routeId2, binaryArray2);
+ expect(providerManagerCallbacks.requestKeepAlive).not.toHaveBeenCalled();
+
+ sender.listenForRouteMessages(routeId1);
+ mockClock.tick(mr.RouteMessageSender.SEND_MESSAGE_INTERVAL_MILLIS);
+ expect(callback).toHaveBeenCalledWith(
+ routeId1, [new mr.RouteMessage(routeId1, binaryArray1)]);
+ expect(providerManagerCallbacks.requestKeepAlive).not.toHaveBeenCalled();
+
+ sender.listenForRouteMessages(routeId2);
+ mockClock.tick(mr.RouteMessageSender.SEND_MESSAGE_INTERVAL_MILLIS);
+ expect(callback).toHaveBeenCalledWith(
+ routeId2, [new mr.RouteMessage(routeId2, binaryArray2)]);
+ expect(providerManagerCallbacks.requestKeepAlive)
+ .toHaveBeenCalledWith(sender.getStorageKey(), false);
+
+ expect(sender.totalMessageSize_).toEqual(0);
+ expect(sender.binaryMessageCount_).toEqual(0);
+ expect(sender.shouldKeepAlive_).toBe(false);
+ });
+
+ it('requestKeepAlive due to message size threshold', function() {
+ const message1 = 'a'.repeat(messageSizeThreshold / 2);
+ const message2 = 'b'.repeat(messageSizeThreshold / 2 + 1);
+
+ sender.send(routeId1, message1);
+ expect(providerManagerCallbacks.requestKeepAlive).not.toHaveBeenCalled();
+
+ sender.send(routeId2, message2);
+ expect(providerManagerCallbacks.requestKeepAlive)
+ .toHaveBeenCalledWith(sender.getStorageKey(), true);
+ providerManagerCallbacks.requestKeepAlive.calls.reset();
+
+ sender.listenForRouteMessages(routeId1);
+ mockClock.tick(mr.RouteMessageSender.SEND_MESSAGE_INTERVAL_MILLIS);
+ expect(callback).toHaveBeenCalledWith(
+ routeId1, [new mr.RouteMessage(routeId1, message1)]);
+ expect(providerManagerCallbacks.requestKeepAlive)
+ .toHaveBeenCalledWith(sender.getStorageKey(), false);
+
+ sender.listenForRouteMessages(routeId2);
+ mockClock.tick(mr.RouteMessageSender.SEND_MESSAGE_INTERVAL_MILLIS);
+ expect(callback).toHaveBeenCalledWith(
+ routeId2, [new mr.RouteMessage(routeId2, message2)]);
+
+ expect(sender.totalMessageSize_).toEqual(0);
+ expect(sender.binaryMessageCount_).toEqual(0);
+ expect(sender.shouldKeepAlive_).toBe(false);
+ });
+
+ it('saves pending messages, then restores and sends them', function() {
+ const textMessage = 'this is a text message';
+ const textMessage2 = 'this is another text message';
+
+ // Queue up the messages in the RouteMessageSender. They will not be sent
+ // because listenForRouteMessages() has not been called yet.
+ sender.send(routeId1, textMessage);
+ sender.send(routeId1, textMessage2);
+
+ // Persists pending messages.
+ mr.PersistentDataManager.suspendForTest();
+
+ // Re-creating a new RouteMessageSender restores the pending messages.
+ sender = new mr.RouteMessageSender(
+ providerManagerCallbacks, messageSizeThreshold);
+ sender.init(callback);
+
+ // Now, call listenForRouteMessages() and the restored pending messages
+ // should be processed.
+ sender.listenForRouteMessages(routeId1);
+ mockClock.tick(mr.RouteMessageSender.SEND_MESSAGE_INTERVAL_MILLIS);
+ expect(callback).toHaveBeenCalledWith(routeId1, [
+ new mr.RouteMessage(routeId1, textMessage),
+ new mr.RouteMessage(routeId1, textMessage2),
+ ]);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/throttling_sender.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/throttling_sender.js
new file mode 100644
index 00000000000..730959a9b91
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/throttling_sender.js
@@ -0,0 +1,89 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview An abstract throttling sender. It sends right away if there is
+ * no sending in the past 'interval' time. Otherwise, it waits till 'interval'
+ * is passed.
+ *
+
+ */
+
+goog.provide('mr.ThrottlingSender');
+
+
+/**
+ * Note: Strictly speaking, this class should implement PersistentData to store
+ * lastSendTime_. But since we never specify an interval large enough for the
+ * extension to become suspended in between, it is not needed in practice.
+ */
+mr.ThrottlingSender = class {
+ /**
+ * @param {number} interval in milliseconds.
+ */
+ constructor(interval) {
+ /** @private {number} */
+ this.interval_ = interval;
+
+ /** @private {?number} */
+ this.lastSendTime_ = null;
+
+ /** @private {?number} */
+ this.timerId_ = null;
+ }
+
+ /**
+ * Clears the sender timer and sets it to null.
+ * @private
+ */
+ clearTimer_() {
+ if (this.timerId_ != null) {
+ clearTimeout(this.timerId_);
+ this.timerId_ = null;
+ }
+ }
+
+ /**
+ * Schedule a send.
+ * @protected
+ */
+ scheduleSend() {
+ if (this.timerId_ != null) {
+ return;
+ }
+ if (this.lastSendTime_ == null ||
+ Date.now() - this.lastSendTime_ >= this.interval_) {
+ // Send right away
+ this.send_();
+ } else {
+ // Delay a while
+ const delay =
+ Math.max(this.lastSendTime_ + this.interval_ - Date.now(), 5);
+ this.timerId_ = setTimeout(this.send_.bind(this), delay);
+ }
+ }
+
+ /**
+ * Sends messages immediately.
+ */
+ sendImmediately() {
+ this.send_();
+ }
+
+ /**
+ * Sends right away and schedule another send if there is more to send.
+ * @private
+ */
+ send_() {
+ this.clearTimer_();
+ this.doSend();
+ this.lastSendTime_ = Date.now();
+ }
+
+ /**
+ * Sends the message if available.
+ * @protected
+ */
+ doSend() {}
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/throttling_sender_test.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/throttling_sender_test.js
new file mode 100644
index 00000000000..fd3de97a161
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/mr_event_senders/throttling_sender_test.js
@@ -0,0 +1,69 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly();
+goog.require('mr.MockClock');
+goog.require('mr.ThrottlingSender');
+
+describe('Tests ThrottlingSender', function() {
+ let mockClock;
+ let sender;
+ const interval = 50;
+ let numOfMessages;
+
+ beforeEach(function() {
+ mockClock = new mr.MockClock(true);
+ sender = new mr.ThrottlingSender(interval);
+ numOfMessages = 0;
+ sender.doSend = jasmine.createSpy('doSend').and.callFake(() => {
+ numOfMessages--;
+ });
+ });
+
+ afterEach(function() {
+ mockClock.uninstall();
+ });
+
+ it('first msg is sent right away', function() {
+ numOfMessages++;
+ sender.scheduleSend();
+ expect(sender.doSend.calls.count()).toEqual(1);
+ expect(numOfMessages).toEqual(0);
+ mockClock.tick(interval);
+ expect(sender.doSend.calls.count()).toEqual(1);
+ expect(numOfMessages).toEqual(0);
+ });
+
+ it('messages are throttled', function() {
+ numOfMessages++;
+ sender.scheduleSend();
+ expect(sender.doSend.calls.count()).toEqual(1);
+ expect(numOfMessages).toEqual(0);
+
+ numOfMessages++;
+ sender.scheduleSend();
+ expect(sender.doSend.calls.count()).toEqual(1);
+ expect(numOfMessages).toEqual(1);
+
+ mockClock.tick(interval);
+ expect(sender.doSend.calls.count()).toEqual(2);
+ expect(numOfMessages).toEqual(0);
+ });
+
+ it('messages are sent gradually 1', function() {
+ numOfMessages++;
+ sender.scheduleSend();
+ expect(sender.doSend.calls.count()).toEqual(1);
+ mockClock.tick(interval / 2);
+ expect(sender.doSend.calls.count()).toEqual(1);
+
+ numOfMessages++;
+ sender.scheduleSend();
+ expect(sender.doSend.calls.count()).toEqual(1);
+ mockClock.tick(interval / 2);
+ expect(sender.doSend.calls.count()).toEqual(2);
+ mockClock.tick(interval);
+ expect(sender.doSend.calls.count()).toEqual(2);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/presentation_enums.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/presentation_enums.js
new file mode 100644
index 00000000000..3ce170b66ea
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/presentation_enums.js
@@ -0,0 +1,35 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Defines presentation connection related enums.
+ */
+
+goog.provide('mr.PresentationConnectionCloseReason');
+goog.provide('mr.PresentationConnectionState');
+
+
+/**
+ * Presentation connection states. Keep in sync with PresentationConnection.idl
+ * in the Chromium code base.
+ *
+ * @enum {string}
+ */
+mr.PresentationConnectionState = {
+ CONNECTED: 'connected',
+ TERMINATED: 'terminated',
+ CLOSED: 'closed'
+};
+
+
+/**
+ * Keep in sync with PresentationConnectionCloseEvent.idl in Chromium code base.
+ *
+ * @enum {string}
+ */
+mr.PresentationConnectionCloseReason = {
+ ERROR: 'error',
+ CLOSED: 'closed',
+ WENT_AWAY: 'went_away'
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/provider.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider.js
new file mode 100644
index 00000000000..fe0217eff71
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider.js
@@ -0,0 +1,258 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview API for media route providers.
+ *
+
+ */
+
+goog.provide('mr.Provider');
+goog.provide('mr.ProviderName');
+
+goog.require('mr.CancellablePromise');
+
+/**
+ * @enum {string}
+ */
+mr.ProviderName = {
+ CAST: 'cast',
+ DIAL: 'dial',
+ CLOUD: 'cloud',
+ TEST: 'test'
+};
+
+
+
+/**
+ * @record
+ */
+mr.Provider = class {
+ /**
+ * Gets provider name.
+ * @return {string}
+ */
+ getName() {}
+
+ /**
+ * Called to send a message to the sink via a media route.
+ * @param {string} routeId
+ * @param {!Object|string} message The message to send. Object will be
+ * serialized to a JSON string.
+ * @param {Object=} opt_extraInfo Extra info about how to send a message.
+ * @return {!Promise<void>} Fulfilled when the message is posted, or rejected
+ * if there an error.
+ */
+ sendRouteMessage(routeId, message, opt_extraInfo) {}
+
+ /**
+ * Called to send a binary message to the sink via a media route.
+ * @param {string} routeId
+ * @param {!Uint8Array} data The message to send.
+ * @return {!Promise<void>} Fulfilled when the message is posted, or rejected
+ * if there an error.
+ */
+ sendRouteBinaryMessage(routeId, data) {}
+
+ /**
+ * Called the first time the provider is loaded.
+ * Should do any one-time initialization (i.e. register event filters, etc.)
+ * @param {!mojo.MediaRouteProviderConfig=} config The config object from the
+ * browser to initialize the provider with.
+ */
+ initialize(config = undefined) {}
+
+ /**
+ * Queries this provider for existing routes.
+ *
+ * @return {!Array.<!mr.Route>}
+ */
+ getRoutes() {}
+
+ /**
+ * Queries this provider for available sinks.
+ *
+ * @param {string} sourceUrn
+ * @return {!mr.SinkList}
+ */
+ getAvailableSinks(sourceUrn) {}
+
+ /**
+ * Starts querying for sinks capable of displaying |sourceUrn|.
+ * @param {string} sourceUrn The URN of the media.
+ */
+ startObservingMediaSinks(sourceUrn) {}
+
+ /**
+ * Stops querying for sinks capable of displaying |sourceUrn|.
+ * @param {string} sourceUrn The URN of the media.
+ */
+ stopObservingMediaSinks(sourceUrn) {}
+
+ /**
+ * Informs the provider to send updates on routes list.
+ * @param {string} sourceUrn The URN of the media.
+ */
+ startObservingMediaRoutes(sourceUrn) {}
+
+ /**
+ * Informs the provider to stop sending updates on routes list.
+ * @param {string} sourceUrn The URN of the media.
+ */
+ stopObservingMediaRoutes(sourceUrn) {}
+
+ /**
+ * Queries this provider for a sink by id.
+ *
+ * @param {string} sinkId
+ * @return {?mr.Sink}
+ * Null if no sink exists with sinkId.
+ */
+ getSinkById(sinkId) {}
+
+ /**
+ * Creates a new route to the sink.
+ * @param {string} sourceUrn
+ * @param {string} sinkId The ID of the target sink.
+ * @param {string} presentationId A presentation ID to use.
+ * @param {boolean} offTheRecord True if the request is from an
+ * off the record (incognito) browser profile.
+ * @param {number} timeoutMillis Request timeout in milliseconds.
+ * @param {string=} opt_origin
+ * @param {number=} opt_tabId
+ * @return {!mr.CancellablePromise<!mr.Route>} Fulfilled with route created if
+ * successful. Rejected otherwise.
+ */
+ createRoute(
+ sourceUrn, sinkId, presentationId, offTheRecord, timeoutMillis,
+ opt_origin, opt_tabId) {}
+
+ /**
+ * Terminates the media route owned by this provider.
+ * @param {string} routeId The media route id.
+ * @return {!Promise} Fulfilled when route is terminated, or rejected with
+ * an error.
+ */
+ terminateRoute(routeId) {}
+
+ /**
+ * Creates and computes mirror settings appropriate for the given sink (and
+ * the sender's capabilities). See class comments for mr.mirror.Settings when
+ * overriding this method. Returns null if this provider does not support the
+ * sink.
+ *
+ * The provider must return valid, frozen settings if
+ * provider.canRoute(sourceUrn, sinkId) is true.
+ *
+ * @param {string} sinkId The ID of the sink to mirror to.
+ * @return {!mr.mirror.Settings}
+ */
+ getMirrorSettings(sinkId) {}
+
+ /**
+ * Gets the name of the best mirror service supported by this provider
+ * on the sink |sinkId|.
+ *
+ * Note that provider must return a valid mirror service name if
+ * provider.canRoute(sourceUrn, sinkId) is true, where sourcerUrn is any
+ * valid mirroring URN.
+ *
+ * @param {string} sinkId
+ * @return {?mr.mirror.ServiceName}
+ * Null if no mirror service is supported on the sink.
+ */
+ getMirrorServiceName(sinkId) {}
+
+ /**
+ * Tells the provider that the mirroring activity description for the
+ * mirroring route |routeId| has changed. The provider can synchronize this
+ * with its own state.
+ * @param {string} routeId
+ */
+ onMirrorActivityUpdated(routeId) {}
+
+ /**
+ * Whether this provider can route media |sourceUrn| to sink |sinkId|.
+ * @param {string} sourceUrn The URN of the media being displayed.
+ * @param {string} sinkId
+ * @return {boolean} True if the provider can handle it.
+ */
+ canRoute(sourceUrn, sinkId) {}
+
+ /**
+ * Whether this provider can join a given route from |sourceUrn| and,
+ * optionally, the specific |route| in question.
+ * @param {string} sourceUrn The URN of the media being displayed.
+ * @param {string=} presentationId The presentation ID to join.
+ * @param {mr.Route=} route The route to join.
+ * @return {boolean} True if the provider can handle it.
+ */
+ canJoin(sourceUrn, presentationId = undefined, route = undefined) {}
+
+ /**
+ * Joins a route identified by by |sourceUrn| and |presentationId|.
+ * @param {string} sourceUrn
+ * @param {string} presentationId A presentation ID for Presentation API
+ * client; A Cast session ID for Cast join; and 'autojoin' for Cast auto Join
+ * case;
+ * @param {boolean} offTheRecord True if the request is from an
+ * off the record (incognito) browser profile.
+ * @param {number} timeoutMillis Request timeout in milliseconds.
+ * @param {string} origin
+ * @param {?number} tabId null for packaged app.
+ * @return {!mr.CancellablePromise<!mr.Route>} Fulfilled with the route if
+ * joined; Rejected otherwise.
+ */
+ joinRoute(
+ sourceUrn, presentationId, offTheRecord, timeoutMillis, origin, tabId) {}
+
+ /**
+ * Joins a route identified by by |sourceUrn| and |routeId|.
+ * @param {string} sourceUrn
+ * @param {string} routeId A route ID to join.
+ * @param {string} presentationId The presentation ID of the route to be
+ * created.
+ * @param {string} origin
+ * @param {?number} tabId null for packaged app.
+ * @param {number=} opt_timeoutMillis If positive, the timeout to use in place
+ * of default timeout.
+ * @return {!mr.CancellablePromise<!mr.Route>} Fulfilled with the route if
+ * joined; Rejected otherwise.
+ */
+ connectRouteByRouteId(
+ sourceUrn, routeId, presentationId, origin, tabId, opt_timeoutMillis) {}
+
+ /**
+ * Detaches a presentation connection from the underlying media route given by
+ * |routeId|.
+ * @param {!string} routeId
+ */
+ detachRoute(routeId) {}
+
+ /**
+ * Searches this provider for a sink that matches |searchCriteria| that is
+ * compatible with the source |sourceUrn|. Returns a promise which resolves
+ * to the matching sink, or rejected if not found.
+ * @param {string} sourceUrn
+ * @param {!mr.SinkSearchCriteria} searchCriteria
+ * @return {!Promise<!mr.Sink>}
+ */
+ searchSinks(sourceUrn, searchCriteria) {}
+
+ /**
+ * Called when Media Router finishes sink discovery. Store |sinks| in this
+ * provider.
+ * @param {!Array<!mojo.Sink>} sinks list of discovered sinks
+ */
+ provideSinks(sinks) {}
+
+ /**
+ * See documentation in interface_data/mojo.js.
+ * @param {string} routeId
+ * @param {!mojo.InterfaceRequest} controllerRequest
+ * @param {!mojo.MediaStatusObserverPtr} observer
+ * @return {!Promise<void>}
+ */
+ createMediaRouteController(routeId, controllerRequest, observer) {}
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_events.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_events.js
new file mode 100644
index 00000000000..564180e723f
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_events.js
@@ -0,0 +1,46 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Provider events.
+
+ */
+
+goog.provide('mr.InternalMessageEvent');
+goog.provide('mr.ProviderEventType');
+
+
+/**
+ * @enum {string}
+ */
+mr.ProviderEventType = {
+ INTERNAL_MESSAGE: 'internal_message'
+};
+
+
+/**
+ * @template T
+ */
+mr.InternalMessageEvent = class {
+ /**
+ * @param {string} routeId
+ * @param {T} message
+ */
+ constructor(routeId, message) {
+ /**
+ * @const
+ */
+ this.type = mr.ProviderEventType.INTERNAL_MESSAGE;
+
+ /**
+ * @type {string}
+ */
+ this.routeId = routeId;
+
+ /**
+ * @type {T}
+ */
+ this.message = message;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager.js
new file mode 100755
index 00000000000..94e50f9792f
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager.js
@@ -0,0 +1,1280 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview API for registering Media Route Providers.
+ *
+
+ *
+ * For now, we will always load every provider each time the extension is
+ * loaded.
+ */
+
+goog.provide('mr.ProviderManager');
+
+goog.require('mr.Assertions');
+goog.require('mr.CancellablePromise');
+goog.require('mr.EventAnalytics');
+goog.require('mr.EventTarget');
+goog.require('mr.InitHelper');
+goog.require('mr.InternalMessageEvent');
+goog.require('mr.Logger');
+goog.require('mr.MediaRouterRequestHandler');
+goog.require('mr.MessagePortService');
+goog.require('mr.MessagePortServiceImpl');
+goog.require('mr.MirrorAnalytics');
+goog.require('mr.Module');
+goog.require('mr.MojoUtils');
+goog.require('mr.PersistentData');
+goog.require('mr.PersistentDataManager');
+goog.require('mr.ProviderManagerCallbacks');
+goog.require('mr.RouteMessageSender');
+goog.require('mr.RouteRequestError');
+goog.require('mr.RouteRequestResultCode');
+goog.require('mr.Sink');
+goog.require('mr.SinkAvailability');
+goog.require('mr.SinkSearchCriteria');
+goog.require('mr.Throttle');
+goog.require('mr.mirror.Error');
+goog.require('mr.mirror.ServiceName');
+
+
+/**
+ * Tracks registered MediaRouteProviders and loads them on-demand.
+ * @implements {mr.MediaRouterRequestHandler}
+ * @implements {mr.PersistentData}
+ * @implements {mr.ProviderManagerCallbacks}
+ */
+mr.ProviderManager = class extends mr.Module {
+ constructor() {
+ super();
+
+ /**
+ * @private {mr.Logger}
+ */
+ this.logger_ = mr.Logger.getInstance('mr.ProviderManager');
+
+ /**
+ * Holds a an array of registered Media Route Providers.
+ * @private {!Array<!mr.Provider>}
+ */
+ this.providers_ = [];
+
+ /**
+ * Holds a map of route ID to the provider that is managing the route.
+ * @private {!Map<string, mr.Provider>}
+ */
+ this.routeIdToProvider_ = new Map();
+
+ /**
+ * Holds a map of route ID to the mirror service name if the route is for
+ * tab/desktop mirroring.
+ * @private {!Map<string, mr.mirror.ServiceName>}
+ */
+ this.routeIdToMirrorServiceName_ = new Map();
+
+ /**
+ * Holds a set of active sink queries.
+ * @private {!Set<string>}
+ */
+ this.sinkQueries_ = new Set();
+
+ /**
+ * Holds a set of active route queries.
+ * @private {!Set<string>}
+ */
+ this.routeQueries_ = new Set();
+
+ /**
+ * Holds a map of mirror service names to modules.
+ * @private {!Map<mr.mirror.ServiceName, mr.ModuleId>}
+ */
+ this.mirrorServiceModules_ = new Map();
+
+ /**
+ * Keeps track of last used mirror service for gathering logs for feedback.
+ * @private {?mr.mirror.ServiceName}
+ */
+ this.lastUsedMirrorService_ = null;
+
+ /** @private {?mr.MediaRouterService} */
+ this.mediaRouterService_ = null;
+
+ /** @private {!mr.EventTarget} */
+ this.routeMessageEventTarget_ = new mr.EventTarget();
+
+
+ /** @private {!mr.Throttle} */
+ this.routeUpdateEventThrottle_ = new mr.Throttle(
+ this.sendRoutesQueryResultToMr_,
+ mr.ProviderManager.ALL_QUERIES_INTERVAL_MS_, this);
+
+ /** @private {!mr.Throttle} */
+ this.sinkUpdateEventThrottle_ = new mr.Throttle(
+ this.executeSinkQueries_, mr.ProviderManager.ALL_QUERIES_INTERVAL_MS_,
+ this);
+
+ /**
+ * MR message sender with build-in rate throttle.
+ * @private {!mr.RouteMessageSender}
+ */
+ this.mrRouteMessageSender_ = new mr.RouteMessageSender(
+ this, mr.RouteMessageSender.MESSAGE_SIZE_KEEP_ALIVE_THRESHOLD);
+
+ /**
+ * The number of pending createRoute
+ * @private {number}
+ */
+ this.pendingRequestRoutes_ = 0;
+
+ /**
+ * Maps provider name to that provider's last reported sink availability.
+ * @private {!Map<string, mr.SinkAvailability>}
+ */
+ this.sinkAvailabilityMap_ = new Map();
+
+ /**
+ * Whether mDNS is currently enabled.
+ * @private {boolean}
+ */
+ this.mdnsEnabled_ = window.navigator.userAgent.indexOf('Windows') == -1;
+
+ /**
+ * Functions that should be executed when |mdnsEnabled_| goes from |false|
+ * to
+ * |true|.
+ * @private {!Array<function()>}
+ */
+ this.mdnsEnabledCallbacks_ = [];
+
+ /**
+ * Names of components that have requested to keep the extension alive.
+ * @private {!Array<string>}
+ */
+ this.keepAliveComponents_ = [];
+
+ /**
+ * Handler for chrome.runtime.onMessage events.
+ * @private @const
+ */
+ this.internalMessageHandler_ =
+ mr.InitHelper.getInternalMessageHandler(this);
+
+ /**
+ * Handler for chrome.runtime.onMessageExternal events.
+ * @private @const
+ */
+ this.externalMessageHandler_ =
+ mr.InitHelper.getExternalMessageHandler(this);
+
+ mr.ProviderManager.exportProperties_(this);
+ }
+
+ /**
+ * @override
+ */
+ handleEvent(event, ...args) {
+ if (event == chrome.runtime.onMessage) {
+ this.internalMessageHandler_(...args);
+ } else if (event == chrome.runtime.onMessageExternal) {
+ this.externalMessageHandler_(...args);
+ } else {
+ throw new Error('Unhandled event');
+ }
+ }
+
+ /**
+ * Gets the mirror service with the given name.
+ * @param {mr.mirror.ServiceName} serviceName Name of the mirror service.
+ * @return {!Promise<!mr.mirror.Service>} Resolved with the requested
+ * mirror service.
+ */
+ getMirrorService(serviceName) {
+ const moduleId = this.mirrorServiceModules_.get(serviceName);
+ return mr.Module.load(moduleId).then(module => {
+ let service = /** @type {!mr.mirror.Service} */ (module);
+ service.initialize(this);
+ return service;
+ });
+ }
+
+ /**
+ * Registers and initalizes providers.
+ *
+ * @param {!Array<!mr.Provider>} providers
+ * @param {!mojo.MediaRouteProviderConfig=} config
+ */
+ registerAllProviders(providers, config = undefined) {
+ providers.forEach(provider => {
+ this.registerProvider_(provider, config);
+ });
+ }
+
+ /**
+ * Initializes the provider manager, register / initialize given providers,
+ * and registers itself with PersistentDataManager.
+ * @param {!mr.MediaRouterService} mediaRouterService
+ * @param {!Array<!mr.Provider>} providers
+ * @param {!mojo.MediaRouteProviderConfig=} config
+ */
+ initialize(mediaRouterService, providers, config = undefined) {
+ this.mirrorServiceModules_.set(
+ mr.mirror.ServiceName.WEBRTC, mr.ModuleId.WEBRTC_STREAMING_SERVICE);
+ this.mirrorServiceModules_.set(
+ mr.mirror.ServiceName.CAST_STREAMING,
+ mr.ModuleId.CAST_STREAMING_SERVICE);
+ this.mirrorServiceModules_.set(
+ mr.mirror.ServiceName.HANGOUTS, mr.ModuleId.HANGOUTS_SERVICE);
+ this.mirrorServiceModules_.set(
+ mr.mirror.ServiceName.MEETINGS, mr.ModuleId.MEETINGS_SERVICE);
+
+ mr.MessagePortService.setService(new mr.MessagePortServiceImpl(this));
+
+ this.mediaRouterService_ = mediaRouterService;
+ this.mrRouteMessageSender_.init(
+ this.mediaRouterService_.onRouteMessagesReceived.bind(
+ this.mediaRouterService_));
+ this.registerAllProviders(providers, config);
+
+ mr.PersistentDataManager.register(this);
+ mr.Module.onModuleLoaded(mr.ModuleId.PROVIDER_MANAGER, this);
+ }
+
+ /**
+ * Registers a provider and initializes it.
+ * @param {!mr.Provider} provider
+ * @param {!mojo.MediaRouteProviderConfig=} config
+ * @private
+ */
+ registerProvider_(provider, config = undefined) {
+ if (this.getProviderByName(provider.getName())) {
+ this.logger_.warning(
+ 'Provider ' + provider.getName() + ' already registered.');
+ return;
+ }
+
+ try {
+ provider.initialize(config);
+ this.providers_.push(provider);
+ this.sinkAvailabilityMap_.set(
+ provider.getName(), mr.SinkAvailability.UNAVAILABLE);
+ } catch (/** Error */ error) {
+ this.logger_.warning(
+ 'Provider ' + provider.getName() + ' failed to initialize.', error);
+ }
+ }
+
+ /**
+ * @return {!Array<!mr.Provider>} Registered Media Route Providers.
+ */
+ getProviders() {
+ return this.providers_;
+ }
+
+ /**
+ * @param {!mr.CancellablePromise<!mr.Route>} routePromise
+ * @param {number} timeout Timeout in milliseconds. When timeout
+ * fires, the return value of this method is rejected. Must be a positive
+ * number.
+ * @return {!Promise<!mr.Route>}
+ * @private
+ */
+ addTimeout_(routePromise, timeout) {
+ return new Promise((resolve, reject) => {
+ let timerId = null;
+ this.preventSuspend_();
+ timerId = window.setTimeout(() => {
+ timerId = null;
+ // Refer to the CancellablePromise class for more details on the
+ // cancel() method, and <route-creation-timeout.svg.gz> for a diagram of
+ // how promise cancellation works with this class.
+ routePromise.cancel(new mr.RouteRequestError(
+ mr.RouteRequestResultCode.TIMED_OUT,
+ 'timeout after ' + timeout + ' ms.'));
+ }, timeout);
+ const cleanup = () => {
+ this.allowSuspend_();
+ if (timerId != null) {
+ window.clearTimeout(timerId);
+ }
+ };
+ routePromise.promise.then(
+ route => {
+ cleanup();
+ resolve(route);
+ },
+ err => {
+ cleanup();
+ reject(mr.RouteRequestError.wrap(err));
+ });
+ });
+ }
+
+ /**
+ * @param {!Promise<T>} promise
+ * @param {number} timeout Timeout in milliseconds. When timeout
+ * fires, the return value of this method is rejected. Must be a positive
+ * number.
+ * @return {!Promise<T>}
+ * @template T
+ * @private
+ */
+ addIgnoredTimeout_(promise, timeout) {
+ return this.addTimeout_(mr.CancellablePromise.forPromise(promise), timeout);
+ }
+
+ /**
+ * Increments the number of pending request for routes and checks if the
+ * extension should be kept alive.
+ * @private
+ */
+ preventSuspend_() {
+ this.pendingRequestRoutes_++;
+ this.maybeUpdateKeepAlive_();
+ }
+
+ /**
+ * Decrements the number of pending request for routes and checks if the
+ * extension should be kept alive.
+ * @private
+ */
+ allowSuspend_() {
+ this.pendingRequestRoutes_--;
+ this.maybeUpdateKeepAlive_();
+ }
+
+ /**
+ * If override timeout is provided and is positive, returns it.
+ * Otherwise, returns the default timeout.
+ * @param {number} defaultTimeoutMillis Default timeout.
+ * @param {number=} opt_overrideTimeoutMillis Optional override timeout.
+ * @return {number}
+ * @private
+ */
+ static getTimeoutToUse_(defaultTimeoutMillis, opt_overrideTimeoutMillis) {
+ return opt_overrideTimeoutMillis && opt_overrideTimeoutMillis > 0 ?
+ opt_overrideTimeoutMillis :
+ defaultTimeoutMillis;
+ }
+
+ /** @override */
+ onBeforeInvokeHandler() {
+ mr.EventAnalytics.recordEvent(mr.EventAnalytics.Event.MEDIA_ROUTER);
+ }
+
+ /**
+ * Helper method to return a string origin from a string or a mojo.Origin
+ * object.
+
+ * @param {!mojo.Origin|string} origin
+ * @return {string}
+ * @private
+ */
+ mojoOriginToString_(origin) {
+ if (typeof origin == 'string') {
+ return origin;
+ }
+ return mr.MojoUtils.mojoOriginToString(/** @type{!mojo.Origin} */ (origin));
+ }
+
+ /**
+ * @override
+ */
+ createRoute(
+ sourceUrn, sinkId, presentationId, origin = undefined, tabId = undefined,
+ timeoutMillis = undefined, offTheRecord = false) {
+ const provider = this.getProviderFor_(sourceUrn, sinkId);
+ if (!provider) {
+ return Promise.reject(new mr.RouteRequestError(
+ mr.RouteRequestResultCode.NO_SUPPORTED_PROVIDER,
+ 'No provider supports createRoute with source: ' + sourceUrn +
+ ' and sink: ' + sinkId));
+ }
+ let originString = undefined;
+ if (origin !== undefined) {
+ originString = this.mojoOriginToString_(origin);
+ }
+ timeoutMillis = mr.ProviderManager.getTimeoutToUse_(
+ mr.ProviderManager.CREATE_ROUTE_TIMEOUT_MS, timeoutMillis);
+ const routePromise = provider.createRoute(
+ sourceUrn, sinkId, presentationId, offTheRecord, timeoutMillis,
+ originString, tabId);
+ return this.addTimeout_(routePromise, timeoutMillis)
+ .then(
+ route => route,
+ /** Error */ err => {
+ this.logger_.error('Error creating route.', err);
+ throw err;
+ });
+ }
+
+ /**
+ * @override
+ */
+ connectRouteByRouteId(
+ sourceUrn, routeId, presentationId, origin, tabId,
+ timeoutMillis = undefined) {
+ const provider = this.routeIdToProvider_.get(routeId);
+ if (!provider) {
+ return Promise.reject(new mr.RouteRequestError(
+ mr.RouteRequestResultCode.NO_SUPPORTED_PROVIDER,
+ 'No provider supports join ' + routeId));
+ }
+
+ // connectRouteByRouteId may take a while. Prevent extension suspension.
+ const routePromise = provider.connectRouteByRouteId(
+ sourceUrn, routeId, presentationId, this.mojoOriginToString_(origin),
+ tabId);
+ timeoutMillis = mr.ProviderManager.getTimeoutToUse_(
+ mr.ProviderManager.JOIN_ROUTE_TIMEOUT_MS_, timeoutMillis);
+ return this.addTimeout_(routePromise, timeoutMillis);
+ }
+
+ /**
+ * @override
+ */
+ joinRoute(
+ sourceUrn, presentationId, origin, tabId, timeoutMillis = undefined,
+ offTheRecord = false) {
+ const provider = this.getProviderForJoin_(sourceUrn, presentationId);
+ if (!provider) {
+ return Promise.reject(new mr.RouteRequestError(
+ mr.RouteRequestResultCode.NO_SUPPORTED_PROVIDER,
+ 'No provider supports join ' + presentationId));
+ }
+
+ // joinRoute may take a while. Prevent extension suspension.
+ timeoutMillis = mr.ProviderManager.getTimeoutToUse_(
+ mr.ProviderManager.JOIN_ROUTE_TIMEOUT_MS_, timeoutMillis);
+ const routePromise = provider.joinRoute(
+ sourceUrn, presentationId, offTheRecord, timeoutMillis,
+ this.mojoOriginToString_(origin), tabId);
+ return this.addTimeout_(routePromise, timeoutMillis);
+ }
+
+ /**
+ * @override
+ */
+ terminateRoute(routeId) {
+ const provider = this.routeIdToProvider_.get(routeId);
+ if (!provider) {
+ return Promise.reject(new mr.RouteRequestError(
+ mr.RouteRequestResultCode.ROUTE_NOT_FOUND,
+ 'Route not found for routeId ' + routeId));
+ }
+ return this.maybeStopMirrorSession_(routeId).then(
+ () => provider.terminateRoute(routeId));
+ }
+
+ /**
+ * @override
+ */
+ startObservingMediaSinks(sourceUrn) {
+ if (!this.sinkQueries_.has(sourceUrn)) {
+ this.sinkQueries_.add(sourceUrn);
+ this.providers_.forEach(p => {
+ p.startObservingMediaSinks(sourceUrn);
+ });
+ }
+ this.querySinks_(sourceUrn);
+ }
+
+ /**
+ * @override
+ */
+ stopObservingMediaSinks(sourceUrn) {
+ if (!this.sinkQueries_.delete(sourceUrn)) {
+ this.logger_.info('No existing query ' + sourceUrn);
+ } else {
+ this.doStopObservingMediaSinks_(sourceUrn);
+ }
+ }
+
+ /**
+ * Helper method to stop a sink query on all providers.
+ * @param {string} sourceUrn The URN of the media.
+ * @private
+ */
+ doStopObservingMediaSinks_(sourceUrn) {
+ this.providers_.forEach(p => {
+ p.stopObservingMediaSinks(sourceUrn);
+ });
+ }
+
+ /**
+ * Queries for sinks capable of displaying |sourceUrn|.
+ * @param {string} sourceUrn The URN of the media.
+ * @private
+ */
+ querySinks_(sourceUrn) {
+
+ if (sourceUrn == 'urn:x-org.chromium.media:source:tab:-1') {
+ this.logger_.warning('No sinks for sourceUrn: ' + sourceUrn);
+ } else {
+ /** @type {!Map<string, !mr.Sink>} */
+ const sinks = new Map();
+ let origins = [];
+ // Get available sinks from current providers.
+ this.providers_.forEach(p => {
+ const sinkList = p.getAvailableSinks(sourceUrn);
+ if (sinkList.sinks.length > 0) {
+ origins = sinkList.origins;
+ }
+ sinkList.sinks.forEach(sink => {
+ // There shouldn't be duplicate sinks. Log a message if we encounter
+ // it.
+ if (sinks.has(sink.id)) {
+ this.logger_.warning(
+ 'Detected duplicate sink ' + sink.id +
+ ' from provider: ' + p.getName());
+ } else {
+ sinks.set(sink.id, sink);
+ }
+ });
+ });
+
+ this.sendSinksToMr_(sourceUrn, Array.from(sinks.values()), origins || []);
+ }
+ }
+
+ /**
+ * Sends sinks that support |sourceUrn| to media router.
+ * @param {string} sourceUrn
+ * @param {!Array<!mr.Sink>} sinkList Sinks that support |sourceUrn|
+ * @param {!Array<string>} origins Origins that can access the sink list.
+ * @private
+ */
+ sendSinksToMr_(sourceUrn, sinkList, origins) {
+ this.logger_.info(
+ 'Sending ' + sinkList.length + ' sinks to MR for ' + sourceUrn);
+ this.mediaRouterService_.onSinksReceived(
+ sourceUrn, sinkList, origins.map(mr.MojoUtils.stringToMojoOrigin));
+ }
+
+ /**
+ * @override
+ */
+ sendRouteMessage(routeId, message, opt_extraInfo) {
+ const provider = this.routeIdToProvider_.get(routeId);
+ if (!provider) {
+ return Promise.reject(Error(`Invalid route ID ${routeId}`));
+ }
+ return this.addIgnoredTimeout_(
+ provider.sendRouteMessage(routeId, message, opt_extraInfo),
+ mr.ProviderManager.SEND_MESSAGE_TIMEOUT_MS_);
+ }
+
+ /**
+ * @override
+ */
+ sendRouteBinaryMessage(routeId, data) {
+ const provider = this.routeIdToProvider_.get(routeId);
+ if (!provider) {
+ return Promise.reject(Error(`Invalid route ID ${routeId}`));
+ }
+ return this.addIgnoredTimeout_(
+ provider.sendRouteBinaryMessage(routeId, data),
+ mr.ProviderManager.SEND_MESSAGE_TIMEOUT_MS_);
+ }
+
+ /**
+ * @override
+ */
+ startListeningForRouteMessages(routeId) {
+ this.mrRouteMessageSender_.listenForRouteMessages(routeId);
+ }
+
+ /**
+ * @override
+ */
+ stopListeningForRouteMessages(routeId) {
+ this.mrRouteMessageSender_.stopListeningForRouteMessages(routeId);
+ }
+
+ /**
+ * @override
+ */
+ detachRoute(routeId) {
+ const provider = this.routeIdToProvider_.get(routeId);
+ if (!provider) {
+ this.logger_.info('Route ' + routeId + ' does not exist.');
+ return;
+ }
+ provider.detachRoute(routeId);
+ }
+
+ /**
+ * @override
+ */
+ enableMdnsDiscovery() {
+ this.mdnsEnabled_ = true;
+ this.mdnsEnabledCallbacks_.forEach(callback => {
+ callback();
+ });
+ this.mdnsEnabledCallbacks_.length = 0;
+ }
+
+ /**
+ * @param {string} sourceUrn The URN of the media being displayed.
+ * @param {string} sinkId
+ * @return {?mr.Provider} The provider that can handle |sourceUrn| on |sinkId|.
+ * Null if none exists.
+ * @private
+ */
+ getProviderFor_(sourceUrn, sinkId) {
+ return this.providers_.find(
+ provider => provider.canRoute(sourceUrn, sinkId)) ||
+ null;
+ }
+
+ /**
+ * @param {string} sourceUrn The URN of the media being displayed.
+ * @param {string} presentationId The presentation ID to join.
+ * @return {?mr.Provider} The provider that can join a route identified by
+ * |sourceUrn| and |presentationId|. Null if none exists.
+ * @private
+ */
+ getProviderForJoin_(sourceUrn, presentationId) {
+ return this.providers_.find(
+ provider => provider.canJoin(sourceUrn, presentationId)) ||
+ null;
+ }
+
+ /**
+ * @private
+ */
+ executeSinkQueries_() {
+ this.sinkQueries_.forEach(sourceUrn => {
+ this.querySinks_(sourceUrn);
+ });
+ }
+
+ /**
+ * @private
+ */
+ maybeUpdateKeepAlive_() {
+ this.mediaRouterService_.setKeepAlive(
+ this.pendingRequestRoutes_ > 0 || this.keepAliveComponents_.length > 0);
+ }
+
+ /**
+ * @override
+ */
+ startObservingMediaRoutes(sourceUrn) {
+ if (!this.routeQueries_.has(sourceUrn)) {
+ this.routeQueries_.add(sourceUrn);
+ this.providers_.forEach(p => {
+ p.startObservingMediaRoutes(sourceUrn);
+ });
+ }
+ this.routeUpdateEventThrottle_.fire();
+ }
+
+ /**
+ * @override
+ */
+ stopObservingMediaRoutes(sourceUrn) {
+ if (!this.routeQueries_.delete(sourceUrn)) {
+ this.logger_.info('No existing route query ' + sourceUrn);
+ } else {
+ this.providers_.forEach(provider => {
+ provider.stopObservingMediaRoutes(sourceUrn);
+ });
+ }
+ }
+
+ /**
+ * Send routes query result to MR immediately.
+ * @private
+ */
+ sendRoutesQueryResultToMr_() {
+ if (this.routeQueries_.size == 0) return;
+
+ let routeList = [];
+ this.providers_.forEach(p => {
+ routeList = routeList.concat(p.getRoutes());
+ });
+
+ this.routeQueries_.forEach(sourceUrn => {
+ const nonLocalJoinableRouteIds = [];
+ this.providers_.forEach(p => {
+ const routes = p.getRoutes();
+ routes.forEach(route => {
+ if (!route.createdLocally && p.canJoin(sourceUrn, undefined, route)) {
+ nonLocalJoinableRouteIds.push(route.id);
+ }
+ });
+ });
+ this.mediaRouterService_.onRoutesUpdated(
+ routeList, sourceUrn, nonLocalJoinableRouteIds);
+ });
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'ProviderManager';
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ return [new mr.ProviderManager.PersistentData_(
+ this.providers_.map(p => p.getName()), Array.from(this.sinkQueries_),
+ Array.from(this.routeQueries_),
+ Array.from(
+ this.routeIdToProvider_,
+ ([routeId, provider]) => [routeId, provider.getName()]),
+ Array.from(this.sinkAvailabilityMap_), this.mdnsEnabled_,
+ this.lastUsedMirrorService_)];
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {
+ const savedData = /** @type {mr.ProviderManager.PersistentData_} */
+ (mr.PersistentDataManager.getTemporaryData(this));
+ if (savedData) {
+ this.sinkQueries_ = new Set(savedData.sinkQueries);
+ this.routeQueries_ = new Set(savedData.routeQueries);
+
+ for (let [routeId, providerName] of savedData.routeIdToProviderName) {
+ const provider = this.getProviderByName(providerName);
+ mr.Assertions.assert(provider, 'Provider not found: ' + providerName);
+ this.routeIdToProvider_.set(routeId, provider);
+ }
+ this.sinkAvailabilityMap_ = new Map(savedData.sinkAvailabilityMap);
+ this.lastUsedMirrorService_ = savedData.lastUsedMirrorService || null;
+ if (savedData.mdnsEnabled) {
+ this.enableMdnsDiscovery();
+ }
+ }
+ }
+
+ /**
+ * @override
+ */
+ getProviderFromRouteId(routeId) {
+ return this.routeIdToProvider_.get(routeId);
+ }
+
+ /**
+ * @override
+ */
+ onRouteAdded(provider, route) {
+ this.routeIdToProvider_.set(route.id, provider);
+ this.onRouteUpdated(provider, route);
+ }
+
+ /**
+ * @override
+ */
+ onRouteRemoved(provider, route) {
+ // Stopping mirroring does not affect message delivery.
+ this.maybeStopMirrorSession_(route.id);
+
+ this.routeIdToProvider_.delete(route.id);
+ this.mrRouteMessageSender_.onRouteRemoved(route.id);
+ this.onRouteUpdated(provider, route);
+ }
+
+ /**
+ * @override
+ */
+ onRouteUpdated(provider, route) {
+ if (this.routeQueries_.size > 0) this.routeUpdateEventThrottle_.fire();
+ }
+
+ /**
+ * @override
+ */
+ handleMirrorActivityUpdate(route, mirrorActivity) {
+ const provider = this.routeIdToProvider_.get(route.id);
+ if (provider) {
+ provider.onMirrorActivityUpdated(route.id);
+ // Bypass the throttle, since an update to the route description may have
+ // happened before the throttle interval has elapsed.
+ if (this.routeQueries_.size > 0) this.sendRoutesQueryResultToMr_();
+ }
+ }
+
+ /**
+ * @override
+ */
+ onRouteMessage(provider, routeId, message) {
+ if (!this.routeIdToProvider_.has(routeId)) {
+ this.logger_.warning('Got route message for closed route ' + routeId);
+ return;
+ }
+ // If message is not a string or an Uint8Array, encode it into a JSON string
+ // for mr.cast.InternalMessage.
+ if ((typeof message !== 'string') && !(message instanceof Uint8Array)) {
+ message = JSON.stringify(message);
+ }
+ this.mrRouteMessageSender_.send(routeId, message);
+ }
+
+ /**
+ * @override
+ */
+ onPresentationConnectionStateChanged(routeId, state) {
+ if (state == mr.PresentationConnectionState.TERMINATED) {
+ this.mrRouteMessageSender_.sendImmediately();
+ }
+ this.mediaRouterService_.onPresentationConnectionStateChanged(
+ routeId, /** @type {string} */ (state));
+ }
+
+ /**
+ * @override
+ */
+ onPresentationConnectionClosed(routeId, reason, message) {
+ this.mrRouteMessageSender_.sendImmediately();
+ this.mediaRouterService_.onPresentationConnectionClosed(
+ routeId, /** @type {string} */ (reason), message);
+ }
+
+ /**
+ * @override
+ */
+ onMirrorSessionEnded(routeId) {
+ // When mirror service invokes this method, it already cleaned its session.
+ // So provider manager only needs to close the route via provider and does
+ // not
+ // need to invoke mirrorService.stopCurrentMirroring.
+ this.routeIdToMirrorServiceName_.delete(routeId);
+ const provider = this.routeIdToProvider_.get(routeId);
+ if (provider) {
+ provider.terminateRoute(routeId);
+ }
+ }
+
+ /**
+ * @override
+ */
+ onInternalMessage(provider, routeId, message) {
+ this.routeMessageEventTarget_.dispatchEvent(
+ new mr.InternalMessageEvent(routeId, message));
+ }
+
+ /**
+ * @override
+ */
+ onSinksUpdated() {
+ this.sinkUpdateEventThrottle_.fire();
+ }
+
+ /**
+ * @override
+ */
+ onSinkAvailabilityUpdated(provider, availability) {
+ const oldValue = this.sinkAvailabilityMap_.get(provider.getName());
+ mr.Assertions.assert(oldValue !== undefined, 'oldValue != undefined');
+ if (oldValue == availability) {
+ return;
+ }
+
+ const currentAvailability = this.computeSinkAvailability_();
+ this.sinkAvailabilityMap_.set(provider.getName(), availability);
+ const newAvailability = this.computeSinkAvailability_();
+ if (currentAvailability == newAvailability) {
+ return;
+ }
+
+ // When the overall SinkAvailability becomes UNAVAILABLE, all sink queries
+ // will be removed. Before that happens, we need to update the sink query
+ // results with empty results. Importantly, however, this also allows
+ // pseudo sinks to be sent as well, which clearing the sinks on the browser
+ // side would not maintain.
+ if (newAvailability == mr.SinkAvailability.UNAVAILABLE) {
+ this.executeSinkQueries_();
+ }
+ this.mediaRouterService_.onSinkAvailabilityUpdated(newAvailability);
+ if (newAvailability == mr.SinkAvailability.UNAVAILABLE) {
+ this.sinkQueries_.forEach(sourceUrn => {
+ this.doStopObservingMediaSinks_(sourceUrn);
+ });
+ this.sinkQueries_.clear();
+ }
+ }
+
+ /**
+ * @return {!mr.SinkAvailability} Overall sink availability based on providers'
+ * availability.
+ * @private
+ */
+ computeSinkAvailability_() {
+ return Array.from(this.sinkAvailabilityMap_.values())
+ .reduce(
+ (prev, cur) => Math.max(prev, cur),
+ mr.SinkAvailability.UNAVAILABLE);
+ }
+
+ /**
+ * @override
+ */
+ getRouteMessageEventTarget() {
+ return this.routeMessageEventTarget_;
+ }
+
+ /**
+ * @override
+ */
+ sendIssue(issue) {
+ this.mediaRouterService_.onIssue(issue);
+ }
+
+ /**
+ * @param {string} routeId
+ * @return {!Promise<boolean>} Fulfilled with true if the route is for
+ * a mirror session and the mirroring was stopped, and with false
+ * otherwise.
+ * @private
+ */
+ maybeStopMirrorSession_(routeId) {
+ if (this.routeIdToMirrorServiceName_.has(routeId)) {
+ // Stop mirroring first
+ return this
+ .getMirrorService(this.routeIdToMirrorServiceName_.get(routeId))
+ .then(mirrorService => {
+ this.routeIdToMirrorServiceName_.delete(routeId);
+ return mirrorService.stopCurrentMirroring();
+ });
+ }
+ return Promise.resolve(false);
+ }
+
+ /**
+ * @override
+ */
+ startMirroring(
+ provider, route, opt_presentationId, opt_streamStartedCallback) {
+ // The provider can handle the mirroring URN, thus the mirror
+ // service name is non-null.
+ const mirrorServiceName =
+ /** @type {mr.mirror.ServiceName} */ (mr.Assertions.assertString(
+ provider.getMirrorServiceName(route.sinkId)));
+ this.logger_.info(`Starting mirroring using service: ${mirrorServiceName}`);
+ this.routeIdToMirrorServiceName_.set(route.id, mirrorServiceName);
+ let innerPromise = null;
+ let cancellationReason = null;
+ return mr.CancellablePromise.withUncancellableStep(
+ this.getMirrorService(mirrorServiceName), mirrorService => {
+ this.lastUsedMirrorService_ = mirrorServiceName;
+ return mirrorService
+ .startMirroring(
+
+ route, mr.Assertions.assertString(route.mediaSource),
+ provider.getMirrorSettings(route.sinkId), opt_presentationId,
+ opt_streamStartedCallback)
+ .catch(err => {
+ if (err instanceof mr.mirror.Error &&
+ err.reason ==
+ mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_USER_CANCEL) {
+ throw new mr.RouteRequestError(
+ mr.RouteRequestResultCode.CANCELLED);
+ }
+ throw err;
+ });
+ });
+ }
+
+ /**
+ * @override
+ */
+ updateMirroring(
+ provider, route, sourceUrn, opt_presentationId, opt_tabId,
+ opt_streamStartedCallback) {
+ const mirrorServiceName = this.routeIdToMirrorServiceName_.get(route.id);
+ if (!mirrorServiceName) {
+ return mr.CancellablePromise.reject(
+ Error('Route ' + route.id + ' is not mirroring'));
+ }
+ return mr.CancellablePromise.withUncancellableStep(
+ this.getMirrorService(mirrorServiceName), mirrorService => {
+ this.lastUsedMirrorService_ = mirrorServiceName;
+ return mirrorService.updateMirroring(
+ route, sourceUrn, provider.getMirrorSettings(route.sinkId),
+ opt_presentationId, opt_tabId, opt_streamStartedCallback);
+ });
+ }
+
+ /**
+ * @override
+ */
+ registerMdnsDiscoveryEnabledCallback(callback) {
+ if (this.mdnsEnabled_) {
+ callback();
+ return;
+ }
+ if (this.mdnsEnabledCallbacks_.indexOf(callback) != -1) {
+ return;
+ }
+ this.mdnsEnabledCallbacks_.push(callback);
+ }
+
+ /**
+ * @override
+ */
+ isMdnsDiscoveryEnabled() {
+ return this.mdnsEnabled_;
+ }
+
+ /**
+ * @override
+ */
+ requestKeepAlive(componentId, keepAlive) {
+ const index = this.keepAliveComponents_.indexOf(componentId);
+ const componentKeepAlive = (index >= 0);
+ if (keepAlive && !componentKeepAlive) {
+ this.keepAliveComponents_.push(componentId);
+ } else if (!keepAlive && componentKeepAlive) {
+ this.keepAliveComponents_.splice(index, 1);
+ }
+ this.maybeUpdateKeepAlive_();
+ }
+
+ /**
+ * @param {!string} name
+ * @return {?mr.Provider}
+ */
+ getProviderByName(name) {
+ return this.providers_.find(p => p.getName() == name) || null;
+ }
+
+ /**
+ * Returns the name of the provider that created the pseudo sink ID |sinkId|
+ * or null if it is not a valid pseudo sink ID.
+ * @param {string} sinkId
+ * @return {?string}
+ * @private
+ */
+ getProviderNameFromPseudoSinkId_(sinkId) {
+ if (!sinkId.startsWith(mr.ProviderManager.PSEUDO_SINK_NAME_PREFIX_)) {
+ return null;
+ }
+ return sinkId.substring(mr.ProviderManager.PSEUDO_SINK_NAME_PREFIX_.length);
+ }
+
+ /**
+ * Get the rejection promise when sink is not found.
+ * @param {string} sinkId Sink ID of the pseudo sink that generated the
+ * request.
+ * @param {string} sourceUrn Source to be used with the sink.
+ * @param {!mr.SinkSearchCriteria} searchCriteria Sink search criteria for the
+ * MRP's which includes the user's current domain.
+ * @return {!Promise}
+ * @private
+ */
+ rejectWithSinkNotFoundError_(sinkId, sourceUrn, searchCriteria) {
+ this.mediaRouterService_.onSearchSinkIdReceived(sinkId, '');
+ return Promise.reject(new mr.RouteRequestError(
+ mr.RouteRequestResultCode.UNKNOWN_ERROR,
+ 'No sink found for search input: ' + searchCriteria.input +
+ ' and source: ' + sourceUrn));
+ }
+
+ /**
+ * @override
+ */
+ searchSinks(sinkId, sourceUrn, searchCriteria) {
+ const providerName = this.getProviderNameFromPseudoSinkId_(sinkId);
+ const searchOwner =
+ providerName ? this.getProviderByName(providerName) : null;
+ if (!searchOwner) {
+ return Promise.reject(new Error('No provider supports ' + sinkId));
+ }
+ return searchOwner.searchSinks(sourceUrn, searchCriteria)
+ .then((sink) => sink.id);
+ }
+
+ /**
+ * @override
+ */
+ provideSinks(providerName, sinks) {
+ const provider = this.getProviderByName(providerName);
+ if (!provider) {
+ this.logger_.error(
+ `provideSinks: Provider not found for providerName ${providerName}`);
+ return;
+ }
+ provider.provideSinks(sinks);
+ }
+
+ /**
+ * @override
+ */
+ updateMediaSinks(sourceUrn) {
+ // Don't add to sinkQueries as the providers may already be unavailable, but
+ // if the sink queries already contain the sourceUrn just return as
+ // discovery
+ // is already ongoing.
+ if (this.sinkQueries_.has(sourceUrn)) {
+ return;
+ }
+ this.querySinks_(sourceUrn);
+ }
+
+ /**
+ * @override
+ */
+ createMediaRouteController(routeId, controllerRequest, observer) {
+ const cleanUpOnError = () => {
+ controllerRequest.close();
+ observer.ptr.reset();
+ };
+ const provider = this.routeIdToProvider_.get(routeId);
+ if (!provider) {
+ const errorMessage =
+ `createMediaRouteController: Provider not found for ${routeId}`;
+ this.logger_.error(errorMessage);
+ cleanUpOnError();
+ return Promise.reject(new Error(errorMessage));
+ }
+ return provider
+ .createMediaRouteController(routeId, controllerRequest, observer)
+ .catch(e => {
+ this.logger_.error(`createMediaRouteController failed: ${e.message}`);
+ cleanUpOnError();
+ throw e;
+ });
+ }
+
+ /**
+ * @param {number} tabId
+ * @param {!mojo.MirrorServiceRemoterPtr} remoter
+ * @param {!mojo.InterfaceRequest} remotingSourceRequest
+ */
+ onMediaRemoterCreated(tabId, remoter, remotingSourceRequest) {
+ this.mediaRouterService_.onMediaRemoterCreated(
+ tabId, remoter, remotingSourceRequest);
+ }
+
+ /**
+ * @return {?mr.mirror.ServiceName} The name of most recently used mirror
+ * service, or null if mirror service has not been used.
+ */
+ getLastUsedMirrorService() {
+ return this.lastUsedMirrorService_;
+ }
+
+ /**
+ * Exports methods for Mojo handler.
+ * @param {!mr.ProviderManager} providerManager
+ * @private
+ */
+ static exportProperties_(providerManager) {
+ // Cast to {!Object} so the compiler doesn't complain about accessing fields
+ // using subscript notation.
+ const obj = /** @type {!Object} */ (providerManager);
+ obj['onBeforeInvokeHandler'] = providerManager.onBeforeInvokeHandler;
+ obj['createRoute'] = providerManager.createRoute;
+ obj['joinRoute'] = providerManager.joinRoute;
+ obj['connectRouteByRouteId'] = providerManager.connectRouteByRouteId;
+ obj['terminateRoute'] = providerManager.terminateRoute;
+ obj['startObservingMediaSinks'] = providerManager.startObservingMediaSinks;
+ obj['stopObservingMediaSinks'] = providerManager.stopObservingMediaSinks;
+ obj['sendRouteMessage'] = providerManager.sendRouteMessage;
+ obj['sendRouteBinaryMessage'] = providerManager.sendRouteBinaryMessage;
+ obj['startListeningForRouteMessages'] =
+ providerManager.startListeningForRouteMessages;
+ obj['stopListeningForRouteMessages'] =
+ providerManager.stopListeningForRouteMessages;
+ obj['startObservingMediaRoutes'] =
+ providerManager.startObservingMediaRoutes;
+ obj['stopObservingMediaRoutes'] = providerManager.stopObservingMediaRoutes;
+ obj['detachRoute'] = providerManager.detachRoute;
+ obj['enableMdnsDiscovery'] = providerManager.enableMdnsDiscovery;
+ obj['searchSinks'] = providerManager.searchSinks;
+ obj['provideSinks'] = providerManager.provideSinks;
+ obj['updateMediaSinks'] = providerManager.updateMediaSinks;
+ obj['createMediaRouteController'] =
+ providerManager.createMediaRouteController;
+ }
+};
+
+/**
+ * Provider may fire sink/route list change event frequently. To avoid run all
+ * sink queries too frequently, only one set of queries at the end of each
+ * interval if there is an update event in the interval.
+ * @private @const {number}
+ */
+mr.ProviderManager.ALL_QUERIES_INTERVAL_MS_ = 500;
+
+// The timeout values below are to ensure provider manager resolve or reject
+// a pending MR request after a reasonable delay. This is a safe guard in case
+// provider has some unforeseen error.
+
+/** @const {number} */
+mr.ProviderManager.CREATE_ROUTE_TIMEOUT_MS = 60 * 1000;
+
+/** @private @const {number} */
+mr.ProviderManager.JOIN_ROUTE_TIMEOUT_MS_ = 30 * 1000;
+
+/** @private @const {number} */
+mr.ProviderManager.SEND_MESSAGE_TIMEOUT_MS_ = 30 * 1000;
+
+/** @private @const {string} */
+mr.ProviderManager.PSEUDO_SINK_NAME_PREFIX_ = 'pseudo:';
+
+
+/**
+ * The Data to be saved in storage when event page suspends.
+ * @private
+ */
+mr.ProviderManager.PersistentData_ = class {
+ /**
+ * @param {!Array<string>} providerNames
+ * @param {!Array<string>} sinkQueries
+ * @param {!Array<string>} routeQueries
+ * @param {!Array<!Array>} routeIdToProviderName
+ * @param {!Array<!Array>} sinkAvailabilityMap
+ * @param {boolean} mdnsEnabled
+ * @param {?mr.mirror.ServiceName} lastUsedMirrorService
+ */
+ constructor(
+ providerNames, sinkQueries, routeQueries, routeIdToProviderName,
+ sinkAvailabilityMap, mdnsEnabled, lastUsedMirrorService) {
+ /**
+ * @type {!Array<string>}
+ */
+ this.providerNames = providerNames;
+
+ /**
+ * @type {!Array<string>}
+ */
+ this.sinkQueries = sinkQueries;
+
+ /**
+ * @type {!Array<string>}
+ */
+ this.routeQueries = routeQueries;
+
+ /**
+ * Map encoded as an array.
+ * @type {!Array<!Array>}
+ */
+ this.routeIdToProviderName = routeIdToProviderName;
+
+ /**
+ * Map encoded as an array.
+ * @type {!Array<!Array>}
+ */
+ this.sinkAvailabilityMap = sinkAvailabilityMap;
+
+ /**
+ * @type {boolean}
+ */
+ this.mdnsEnabled = mdnsEnabled;
+
+ /**
+ * @type {?mr.mirror.ServiceName}
+ */
+ this.lastUsedMirrorService = lastUsedMirrorService;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager_callbacks.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager_callbacks.js
new file mode 100644
index 00000000000..01ff1f5d3fa
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager_callbacks.js
@@ -0,0 +1,223 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview API used by Media Route Providers to interact with the Media
+ * Route Provider Manager.
+ */
+
+goog.provide('mr.ProviderManagerCallbacks');
+goog.provide('mr.ProviderManagerIssueCallbacks');
+goog.provide('mr.ProviderManagerMirrorServiceCallbacks');
+goog.provide('mr.ProviderManagerRouteCallbacks');
+goog.provide('mr.ProviderManagerSinkCallbacks');
+
+goog.require('mr.EventTarget');
+goog.require('mr.mirror.Activity');
+
+
+/**
+ * @record
+ */
+mr.ProviderManagerSinkCallbacks = class {
+ /**
+ * Called to notify that sinks have been added, removed, or updated.
+ */
+ onSinksUpdated() {}
+
+ /**
+ * Called to notify that sink availability has changed.
+ *
+
+ *
+ * @param {!mr.Provider} provider
+ * @param {!mr.SinkAvailability} availability
+ */
+ onSinkAvailabilityUpdated(provider, availability) {}
+};
+
+
+
+/**
+ * @record
+ */
+mr.ProviderManagerRouteCallbacks = class {
+ /**
+ * @param {!mr.Provider} provider
+ * @param {!mr.Route} route
+ */
+ onRouteAdded(provider, route) {}
+
+ /**
+ * @param {!mr.Provider} provider
+ * @param {!mr.Route} route
+ */
+ onRouteRemoved(provider, route) {}
+
+ /**
+ * @param {!mr.Provider} provider
+ * @param {!mr.Route} route
+ */
+ onRouteUpdated(provider, route) {}
+
+ /**
+ * Sends the provided message to the web app. If the message should also be
+ * sent internally via the MessagePort, also call onInternalMessage.
+ * @param {!mr.Provider} provider
+ * @param {string} routeId
+ * @param {string|!Uint8Array|!Object} message If not a string (text message)
+ * or Uint8Array (binary message), then the message will be serialized to
+ * a JSON string and sent as a text message.
+ */
+ onRouteMessage(provider, routeId, message) {}
+
+ /**
+ * Called to notify the presentation connected to a route has changed state.
+ * If the state is CLOSED, onPresentationConnectionClosed is called instead of
+ * this method.
+ * @param {!string} routeId
+ * @param {!mr.PresentationConnectionState} state
+ */
+ onPresentationConnectionStateChanged(routeId, state) {}
+
+ /**
+ * Called to notify the presentation connected to a route has closed.
+ * @param {string} routeId
+ * @param {!mr.PresentationConnectionCloseReason} reason
+ * @param {string} message
+ */
+ onPresentationConnectionClosed(routeId, reason, message) {}
+};
+
+
+
+/**
+ * @record
+ */
+mr.ProviderManagerIssueCallbacks = class {
+ /**
+ * Sends the given issue to MediaRouter.
+ * @param {!mr.Issue} issue
+ */
+ sendIssue(issue) {}
+};
+
+
+
+/**
+ * @record
+ * @extends {mr.ProviderManagerIssueCallbacks}
+ */
+mr.ProviderManagerMirrorServiceCallbacks = class {
+ /**
+ * Invoked by mirror service when it stops a mirror session due to error,
+ * the end of a stream etc. Because provider manager is unaware of mirror
+ * session's internal state, this method is used by mirror service to tell
+ * provider manager to close the corresponding route.
+ * @param {string} routeId
+ */
+ onMirrorSessionEnded(routeId) {}
+
+ /**
+ * Invoked by the mirror service when the mirroring activity description for
+ * mirroring route |route| has changed.
+ *
+ * @param {!mr.Route} route
+ * @param {!mr.mirror.Activity} mirrorActivity
+ */
+ handleMirrorActivityUpdate(route, mirrorActivity) {}
+};
+
+
+
+/**
+ * @record
+ * @extends {mr.ProviderManagerIssueCallbacks}
+ * @extends {mr.ProviderManagerSinkCallbacks}
+ * @extends {mr.ProviderManagerRouteCallbacks}
+ * @extends {mr.ProviderManagerMirrorServiceCallbacks}
+ */
+mr.ProviderManagerCallbacks = class {
+ /**
+ * @param {string} routeId
+ * @return {mr.Provider}
+ */
+ getProviderFromRouteId(routeId) {}
+
+ /**
+ * @return {!mr.EventTarget}
+ */
+ getRouteMessageEventTarget() {}
+
+ /**
+ * Sends the message internally, to the mirror session associated with the
+ * provided route ID, via the MessagePort.
+ * @param {!mr.Provider} provider
+ * @param {!string} routeId
+ * @param {!Object|string} message
+ */
+ onInternalMessage(provider, routeId, message) {}
+
+ /**
+ * Called by a provider to request the mirroring service to start mirroring.
+ * @param {!mr.Provider} provider
+ * @param {!mr.Route} route
+ * @param {string=} opt_presentationId
+ * @param {(function(!mr.Route): !mr.CancellablePromise<!mr.Route>)=}
+ * opt_streamStartedCallback Callback to invoke after stream capture
+ * succeeded and before the mirror session is created. This allows the
+ * provider to perform additional setup and update the route for the
+ * mirror session.
+ * @return {!mr.CancellablePromise<!mr.Route>}
+ */
+ startMirroring(
+ provider, route, opt_presentationId, opt_streamStartedCallback) {}
+
+ /**
+ * Called by a provider to request the mirroring service to update |route| to
+ * the new source |sourceUrn|.
+ * @param {!mr.Provider} provider
+ * @param {!mr.Route} route
+ * @param {string} sourceUrn
+ * @param {string=} opt_presentationId
+ * @param {number=} opt_tabId
+ * @param {(function(!mr.Route): !mr.CancellablePromise)=}
+ * opt_streamStartedCallback Callback to invoke after stream capture
+ * succeeded and before the mirror session is created. This allows the
+ * provider to perform additional setup and update the route for the
+ * mirror session.
+ * @return {!mr.CancellablePromise<!mr.Route>}
+ */
+ updateMirroring(
+ provider, route, sourceUrn, opt_presentationId, opt_tabId,
+ opt_streamStartedCallback) {}
+
+ /**
+ * Register a callback with the provider manager that will either be executed
+ * immediately if mDNS discovery is currently enabled or saved to be executed
+ * when mDNS discovery becomes enabled. This should allow duplicate calls to
+ * be
+ * made with the same function address but only call the function once.
+ * @param {function()} callback Callback that depends on mDNS discovery.
+ */
+ registerMdnsDiscoveryEnabledCallback(callback) {}
+
+ /**
+ * Called by a component with |keepAlive| set to true when it requires the
+ * extension to be kept alive. When a component no longer requires the
+ * extension
+ * to be kept alive, this method should be called with |keepAlive| set to
+ * false.
+ * The extension will be prevented from suspending as long as at least one
+ * component requested keep alive.
+ * @param {string} componentId Globally unique id of the requesting component.
+ * @param {boolean} keepAlive
+ */
+ requestKeepAlive(componentId, keepAlive) {}
+
+ /**
+ * @return {boolean} Whether mDNS discovery is currently enabled.
+ */
+ isMdnsDiscoveryEnabled() {}
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager_test.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager_test.js
new file mode 100644
index 00000000000..9453ed678b2
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/provider_manager_test.js
@@ -0,0 +1,1076 @@
+/**
+ * @fileoverview Tests for provider_manager.
+ */
+goog.setTestOnly('provider_manager_test');
+
+goog.require('mr.CancellablePromise');
+goog.require('mr.MediaSourceUtils');
+goog.require('mr.MirrorAnalytics');
+goog.require('mr.Module');
+goog.require('mr.PersistentDataManager');
+goog.require('mr.PresentationConnectionCloseReason');
+goog.require('mr.PresentationConnectionState');
+goog.require('mr.ProviderManager');
+goog.require('mr.Route');
+goog.require('mr.RouteMessage');
+goog.require('mr.RouteRequestError');
+goog.require('mr.RouteRequestResultCode');
+goog.require('mr.Sink');
+goog.require('mr.SinkAvailability');
+goog.require('mr.SinkList');
+goog.require('mr.UnitTestUtils');
+goog.require('mr.mirror.Error');
+goog.require('mr.mirror.ServiceName');
+
+describe('Tests ProviderManager', function() {
+ let sourceUrn;
+ let mockMediaRouterService;
+ let providerManager;
+ let mockProvider1;
+ let mockProvider1Name;
+ let mockProvider2;
+ let mockBrokenProvider;
+ let mockCastMirrorService;
+ let mockWebrtcMirrorService;
+ let mirrorServiceMap;
+ const provider1Routes = [];
+ const provider2Routes = [];
+ const providerMethods = [
+ 'getName', 'initialize', 'getAvailableSinks', 'createRoute',
+ 'terminateRoute', 'sendRouteMessage', 'getSinkById', 'getMirrorSettings',
+ 'getMirrorServiceName', 'canRoute', 'startObservingMediaSinks',
+ 'stopObservingMediaSinks', 'startObservingMediaRoutes',
+ 'stopObservingMediaRoutes', 'getRoutes', 'canJoin', 'searchSinks',
+ 'createMediaRouteController'
+ ];
+ const mirrorServiceMethods = [
+ 'initialize',
+ 'getName',
+ 'startMirroring',
+ 'stopCurrentMirroring',
+ 'createMirrorSession',
+ 'updateMirroring',
+ ];
+ const castMirrorServiceMethods = mirrorServiceMethods.slice();
+ const presentationId = 'presentationId';
+ const routeId = '123';
+
+ let mockClock;
+
+ beforeEach(function() {
+ mr.PersistentDataManager.clear();
+ mr.Module.clearForTest();
+ mr.UnitTestUtils.mockMojoApi();
+ sourceUrn = 'urn:dial-multiscreen-org:dial:application:YouTube';
+ mockClock = null;
+ window['chrome'] = chrome || {};
+ chrome['runtime'] = chrome.runtime || {};
+ chrome.runtime.id = '123';
+ chrome.runtime.getManifest = function() {
+ return {version: 'fakeVersion'};
+ };
+ mockProvider1 = jasmine.createSpyObj('provider1', providerMethods);
+ mockProvider1Name = 'p1';
+ mockProvider1.getName.and.returnValue(mockProvider1Name);
+ mockProvider1.getRoutes.and.callFake(() => provider1Routes);
+ mockProvider2 = jasmine.createSpyObj('provider2', providerMethods);
+ mockProvider2.getName.and.returnValue('p2');
+ mockProvider2.getRoutes.and.callFake(() => provider2Routes);
+ mockBrokenProvider =
+ jasmine.createSpyObj('brokenProvider', providerMethods);
+ mockBrokenProvider.initialize.and.throwError(
+ new Error('I forgot how to initialize. @_@'));
+ mockCastMirrorService =
+ jasmine.createSpyObj('castMirrorService', castMirrorServiceMethods);
+ mockCastMirrorService.getName.and.returnValue(
+ mr.mirror.ServiceName.CAST_STREAMING);
+ mockWebrtcMirrorService =
+ jasmine.createSpyObj('webrtcMirrorService', mirrorServiceMethods);
+ mockWebrtcMirrorService.getName.and.returnValue(
+ mr.mirror.ServiceName.WEBRTC);
+ mirrorServiceMap = new Map();
+ mirrorServiceMap.set(
+ mr.mirror.ServiceName.CAST_STREAMING, mockCastMirrorService);
+ mirrorServiceMap.set(mr.mirror.ServiceName.WEBRTC, mockWebrtcMirrorService);
+
+ // These two not needed?
+ spyOn(mr.mirror.cast, 'Service').and.returnValue(mockCastMirrorService);
+ spyOn(mr.mirror.webrtc, 'WebRtcService')
+ .and.returnValue(mockWebrtcMirrorService);
+ mockMediaRouterService = jasmine.createSpyObj('mrService', [
+ 'setKeepAlive', 'getKeepAlive', 'setHandlers',
+ 'onPresentationConnectionClosed', 'onPresentationConnectionStateChanged',
+ 'onRoutesUpdated', 'onSinkAvailabilityUpdated', 'onSinksReceived',
+ 'start', 'onSearchSinkIdReceived', 'onRouteMessagesReceived'
+ ]);
+ const mockCloudComponentProvider = {
+ getIdentityService: function() {
+ return {};
+ }
+ };
+ spyOn(mr.cloud.CloudComponentProvider, 'getInstance')
+ .and.returnValue(mockCloudComponentProvider);
+ providerManager = new mr.ProviderManager();
+ expect(mockMediaRouterService.start.calls.count()).toBe(0);
+ providerManager.initialize(mockMediaRouterService, []);
+ mockMediaRouterService.start.and.returnValue('instance123');
+ spyOn(providerManager.mrRouteMessageSender_, 'send').and.callThrough();
+ spyOn(providerManager, 'getMirrorService').and.callFake(serviceName => {
+ return Promise.resolve(mirrorServiceMap.get(serviceName));
+ });
+ });
+
+ afterEach(function() {
+ if (mockClock) {
+ mr.UnitTestUtils.restoreRealClockAndPromises();
+ }
+ });
+
+ describe('Test presentation connection state changes', function() {
+ it('onPresentationConnectionStateChanged', function() {
+ const state = mr.PresentationConnectionState.TERMINATED;
+ providerManager.onPresentationConnectionStateChanged(routeId, state);
+ expect(mockMediaRouterService.onPresentationConnectionStateChanged)
+ .toHaveBeenCalledWith(routeId, state);
+ });
+
+ it('onPresentationConnectionStateClosed', function() {
+ const closeReason = mr.PresentationConnectionCloseReason.WENT_AWAY;
+ const message = 'Connection went away';
+ providerManager.onPresentationConnectionClosed(
+ routeId, closeReason, message);
+ expect(mockMediaRouterService.onPresentationConnectionClosed)
+ .toHaveBeenCalledWith(routeId, closeReason, message);
+ });
+ });
+
+ describe('Test registerAllProviders', function() {
+ it('Provider is initialized', function() {
+ providerManager.registerAllProviders(
+ [mockProvider1, mockProvider2, mockBrokenProvider]);
+ expect(mockProvider1.initialize.calls.count()).toBe(1);
+ expect(mockProvider2.initialize.calls.count()).toBe(1);
+ expect(mockBrokenProvider.initialize.calls.count()).toBe(1);
+ expect(providerManager.getProviders().some(
+ p => p.getName() == mockProvider1.getName()))
+ .toBe(true);
+ expect(providerManager.getProviders().some(
+ p => p.getName() == mockProvider2.getName()))
+ .toBe(true);
+ expect(!providerManager.getProviders().some(
+ p => p.getName() == mockBrokenProvider.getName()))
+ .toBe(true);
+ });
+
+ it('Route message event is listened to', function() {
+ mockClock = mr.UnitTestUtils.useMockClockAndPromises();
+ const route = new mr.Route(routeId, 'pId', '0', null, false, '', null);
+ const message = 'msg';
+ providerManager.registerAllProviders([mockProvider1]);
+ providerManager.onRouteAdded(mockProvider1, route);
+ providerManager.onRouteMessage(mockProvider1, routeId, message, true);
+ providerManager.startListeningForRouteMessages(routeId);
+ mockClock.tick(mr.RouteMessageSender.SEND_MESSAGE_INTERVAL_MILLIS);
+ expect(mockMediaRouterService.onRouteMessagesReceived)
+ .toHaveBeenCalledWith(
+ routeId, [new mr.RouteMessage(routeId, message)]);
+ });
+
+ it('Message of a closed route is not forwarded', function() {
+ const message = 'msg';
+ providerManager.registerAllProviders([mockProvider1]);
+ providerManager.onRouteMessage(mockProvider1, routeId, message);
+ expect(providerManager.mrRouteMessageSender_.send).not.toHaveBeenCalled();
+ });
+
+ it('InternalMessage sends and is not forwarded to app', function() {
+ const route = new mr.Route(routeId, 'pId', '0', null, false, '', null);
+ const message = 'msg';
+ providerManager.registerAllProviders([mockProvider1]);
+ providerManager.onRouteAdded(mockProvider1, route);
+ providerManager.onInternalMessage(mockProvider1, routeId, message);
+ expect(providerManager.mrRouteMessageSender_.send).not.toHaveBeenCalled();
+ });
+
+ it('start/stopObservingMediaRoutes is passed to provider', function() {
+ mockProvider1.getRoutes.and.returnValue([]);
+ mockProvider2.getRoutes.and.returnValue([]);
+ providerManager.registerAllProviders([mockProvider1, mockProvider2]);
+
+ expect(mockProvider1.startObservingMediaRoutes.calls.count()).toBe(0);
+ expect(mockProvider2.startObservingMediaRoutes.calls.count()).toBe(0);
+ expect(mockProvider1.stopObservingMediaRoutes.calls.count()).toBe(0);
+ expect(mockProvider2.stopObservingMediaRoutes.calls.count()).toBe(0);
+
+ providerManager.startObservingMediaRoutes(sourceUrn);
+ expect(mockProvider1.startObservingMediaRoutes.calls.count()).toBe(1);
+ expect(mockProvider2.startObservingMediaRoutes.calls.count()).toBe(1);
+ expect(mockProvider1.stopObservingMediaRoutes.calls.count()).toBe(0);
+ expect(mockProvider2.stopObservingMediaRoutes.calls.count()).toBe(0);
+
+ providerManager.stopObservingMediaRoutes(sourceUrn);
+ expect(mockProvider1.startObservingMediaRoutes.calls.count()).toBe(1);
+ expect(mockProvider2.startObservingMediaRoutes.calls.count()).toBe(1);
+ expect(mockProvider1.stopObservingMediaRoutes.calls.count()).toBe(1);
+ expect(mockProvider2.stopObservingMediaRoutes.calls.count()).toBe(1);
+ });
+
+ it('Routes query result is sent back to MR initially', function() {
+ providerManager.startObservingMediaRoutes(sourceUrn);
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+ providerManager.stopObservingMediaRoutes(sourceUrn);
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+ });
+
+ it('Routes query result is sent back to MR on events', function() {
+ mockClock = mr.UnitTestUtils.useMockClockAndPromises();
+
+ const route = new mr.Route(routeId, 'pId', '0', null, false, '', null);
+
+ providerManager.startObservingMediaRoutes(sourceUrn);
+ // Send routes right away
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+
+ // Call to MR is throttled.
+ mockClock.tick(mr.ProviderManager.ALL_QUERIES_INTERVAL_MS_ / 2);
+ providerManager.onRouteAdded(mockProvider1, route);
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+ providerManager.onRouteRemoved(mockProvider1, route);
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+ providerManager.onRouteAdded(mockProvider1, route);
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+ mockClock.tick(mr.ProviderManager.ALL_QUERIES_INTERVAL_MS_ + 1);
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(2);
+ });
+
+ it('Routes query result is not sent back to MR if stopped', function() {
+ mockClock = mr.UnitTestUtils.useMockClockAndPromises();
+
+ const route = new mr.Route(routeId, 'pId', '0', null, false, '', null);
+
+ providerManager.startObservingMediaRoutes(sourceUrn);
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+
+ mockClock.tick(mr.ProviderManager.ALL_QUERIES_INTERVAL_MS_ / 2);
+ providerManager.onRouteAdded(mockProvider1, route);
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+ providerManager.onRouteRemoved(mockProvider1, route);
+ providerManager.onRouteAdded(mockProvider1, route);
+ providerManager.stopObservingMediaRoutes(sourceUrn);
+ // Query was stopped to MR is not called.
+ mockClock.tick(mr.ProviderManager.ALL_QUERIES_INTERVAL_MS_ / 2 + 1);
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+
+ });
+
+ it('Multiple route queries do not cause duplicate routes', function() {
+ mockClock = mr.UnitTestUtils.useMockClockAndPromises();
+
+ const route = new mr.Route(routeId, 'pId', '0', null, false, '', null);
+ const sourceUrn2 = '';
+ // Only one route will be returned.
+ mockProvider1.getRoutes.and.returnValue([route]);
+ mockProvider1.canJoin.and.returnValue(false);
+ providerManager.registerAllProviders([mockProvider1]);
+
+ // Multiple route queries means the one route should be returned for
+ // multiple route queries.
+ providerManager.startObservingMediaRoutes(sourceUrn);
+ providerManager.startObservingMediaRoutes(sourceUrn2);
+ // Call to MR is throttled - so this will only happen once.
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(1);
+ expect(mockMediaRouterService.onRoutesUpdated)
+ .toHaveBeenCalledWith([route], sourceUrn, []);
+ mockClock.tick(mr.ProviderManager.ALL_QUERIES_INTERVAL_MS_ + 1);
+ // This will fire twice because there are two route queries, bringing the
+ // total to 3 times.
+ expect(mockMediaRouterService.onRoutesUpdated.calls.count()).toBe(3);
+ expect(mockMediaRouterService.onRoutesUpdated)
+ .toHaveBeenCalledWith([route], sourceUrn, []);
+ expect(mockMediaRouterService.onRoutesUpdated)
+ .toHaveBeenCalledWith([route], sourceUrn2, []);
+ });
+ });
+
+ describe('Test keep alive', function() {
+ it('Keep alive for 1 provider', function() {
+ providerManager.requestKeepAlive(mockProvider1, true);
+ expect(mockMediaRouterService.setKeepAlive).toHaveBeenCalledWith(true);
+ providerManager.requestKeepAlive(mockProvider1, false);
+ expect(mockMediaRouterService.setKeepAlive).toHaveBeenCalledWith(false);
+ });
+
+ it('Keep alive for more than 1 provider', function() {
+ providerManager.requestKeepAlive(mockProvider1, true);
+ expect(mockMediaRouterService.setKeepAlive).toHaveBeenCalledWith(true);
+ providerManager.requestKeepAlive(mockProvider2, true);
+ expect(mockMediaRouterService.setKeepAlive).toHaveBeenCalledWith(true);
+ providerManager.requestKeepAlive(mockProvider1, false);
+ expect(mockMediaRouterService.setKeepAlive).toHaveBeenCalledWith(true);
+ providerManager.requestKeepAlive(mockProvider2, false);
+ expect(mockMediaRouterService.setKeepAlive).toHaveBeenCalledWith(false);
+ });
+ });
+
+ describe('Test createRoute', function() {
+ let sinkId;
+ let localRoute;
+
+ beforeEach(function() {
+ sourceUrn = 'urn:dial-multiscreen-org:dial:application:YouTube';
+ sinkId = 'sink1';
+ localRoute = new mr.Route('r2', 'pId', sinkId, sourceUrn, true, '', null);
+ providerManager.registerAllProviders([mockProvider1, mockProvider2]);
+ });
+
+ it('No provider can handle it', function(done) {
+ mockProvider1.canRoute.and.returnValue(false);
+ mockProvider2.canRoute.and.returnValue(false);
+ providerManager.createRoute(sourceUrn, sinkId, presentationId)
+ .catch(e => {
+ expect(e instanceof mr.RouteRequestError).toBe(true);
+ expect(e.errorCode)
+ .toEqual(mr.RouteRequestResultCode.NO_SUPPORTED_PROVIDER);
+ expect(mockProvider1.canRoute)
+ .toHaveBeenCalledWith(sourceUrn, sinkId);
+ expect(mockProvider2.canRoute)
+ .toHaveBeenCalledWith(sourceUrn, sinkId);
+
+ expect(mockMediaRouterService.setKeepAlive.calls.count()).toBe(0);
+
+ done();
+ });
+ });
+
+ it('One provider can handle, but fail to create session', function(done) {
+ mockProvider1.canRoute.and.returnValue(true);
+ mockProvider2.canRoute.and.returnValue(false);
+ mockProvider1.createRoute.and.returnValue(
+ mr.CancellablePromise.reject(Error('err')));
+ providerManager.createRoute(sourceUrn, sinkId, presentationId)
+ .catch(e => {
+ expect(e instanceof mr.RouteRequestError).toBe(true);
+ expect(e.errorCode)
+ .toEqual(mr.RouteRequestResultCode.UNKNOWN_ERROR);
+ expect(mockProvider1.createRoute.calls.count()).toBe(1);
+ expect(mockProvider1.createRoute)
+ .toHaveBeenCalledWith(
+ sourceUrn, sinkId, presentationId, false,
+ mr.ProviderManager.CREATE_ROUTE_TIMEOUT_MS, undefined,
+ undefined);
+ expect(mockProvider2.createRoute.calls.count()).toBe(0);
+
+ expect(mockMediaRouterService.setKeepAlive.calls.argsFor(0))
+ .toEqual([true]);
+ expect(mockMediaRouterService.setKeepAlive.calls.argsFor(1))
+ .toEqual([false]);
+
+ done();
+ });
+ });
+
+ it('One provider can handle it, and created session', function(done) {
+ mockProvider1.canRoute.and.returnValue(true);
+ mockProvider2.canRoute.and.returnValue(false);
+ mockProvider1.createRoute.and.callFake(() => {
+ providerManager.onRouteAdded(mockProvider1, localRoute);
+ return mr.CancellablePromise.resolve(localRoute);
+ });
+ providerManager.createRoute(sourceUrn, sinkId, presentationId).then(r => {
+ expect(r).toEqual(localRoute);
+ expect(mockProvider1.createRoute.calls.count()).toBe(1);
+ expect(mockProvider1.createRoute)
+ .toHaveBeenCalledWith(
+ sourceUrn, sinkId, presentationId, false,
+ mr.ProviderManager.CREATE_ROUTE_TIMEOUT_MS, undefined,
+ undefined);
+ expect(mockProvider2.createRoute.calls.count()).toBe(0);
+
+ done();
+ });
+ });
+
+ it('createRoute with custom timeout', function(done) {
+ mockProvider1.canRoute.and.returnValue(true);
+ mockProvider2.canRoute.and.returnValue(false);
+ mockProvider1.createRoute.and.callFake(() => {
+ providerManager.onRouteAdded(mockProvider1, localRoute);
+ return mr.CancellablePromise.resolve(localRoute);
+ });
+ const timeoutMillis = 12345;
+ providerManager
+ .createRoute(
+ sourceUrn, sinkId, presentationId, undefined, undefined,
+ timeoutMillis)
+ .then(r => {
+ expect(r).toEqual(localRoute);
+ expect(mockProvider1.createRoute.calls.count()).toBe(1);
+ expect(mockProvider1.createRoute)
+ .toHaveBeenCalledWith(
+ sourceUrn, sinkId, presentationId, false, timeoutMillis,
+ undefined, undefined);
+ expect(mockProvider2.createRoute.calls.count()).toBe(0);
+
+ done();
+ });
+ });
+
+ it('a mirror session is created', function(done) {
+ sourceUrn = mr.MediaSourceUtils.DESKTOP_MIRROR_URN;
+ localRoute = new mr.Route('r2', 'pId', sinkId, sourceUrn, true, '', null);
+ mockProvider1.canRoute.and.returnValue(true);
+ mockProvider2.canRoute.and.returnValue(false);
+ const mirrorSettings = {};
+ mockProvider1.getMirrorSettings.and.returnValue(mirrorSettings);
+ mockProvider1.getMirrorServiceName.and.returnValue(
+ mr.mirror.ServiceName.CAST_STREAMING);
+ mockProvider1.createRoute.and.callFake(() => {
+ providerManager.onRouteAdded(this, localRoute);
+ return mr.CancellablePromise.resolve(localRoute);
+ });
+ mockCastMirrorService.startMirroring.and.returnValue(
+ mr.CancellablePromise.resolve(localRoute));
+ expect(providerManager.getLastUsedMirrorService()).toBeNull();
+ providerManager.createRoute(sourceUrn, sinkId, presentationId)
+ .then(route => {
+ providerManager.startMirroring(mockProvider1, route, presentationId)
+ .promise.then(r => {
+ expect(mockProvider1.createRoute.calls.count()).toBe(1);
+ expect(mockProvider1.createRoute)
+ .toHaveBeenCalledWith(
+ sourceUrn, sinkId, presentationId, false,
+ mr.ProviderManager.CREATE_ROUTE_TIMEOUT_MS, undefined,
+ undefined);
+ expect(mockProvider2.createRoute.calls.count()).toBe(0);
+ expect(mockCastMirrorService.startMirroring.calls.count())
+ .toBe(1);
+ expect(mockCastMirrorService.startMirroring)
+ .toHaveBeenCalledWith(
+ localRoute, sourceUrn, mirrorSettings, presentationId,
+ undefined);
+ expect(providerManager.routeIdToProvider_.has(localRoute.id))
+ .toBe(true);
+ expect(providerManager.routeIdToMirrorServiceName_.has(
+ localRoute.id))
+ .toBe(true);
+ expect(providerManager.getLastUsedMirrorService())
+ .toBe(mr.mirror.ServiceName.CAST_STREAMING);
+ done();
+ });
+ });
+ });
+
+ it('a mirror session is not created', function(done) {
+ sourceUrn = mr.MediaSourceUtils.DESKTOP_MIRROR_URN;
+ localRoute = new mr.Route('r2', 'pId', sinkId, sourceUrn, true, '', null);
+ mockProvider1.canRoute.and.returnValue(true);
+ mockProvider2.canRoute.and.returnValue(false);
+ const mirrorSettings = {};
+ mockProvider1.getMirrorSettings.and.returnValue(mirrorSettings);
+ mockProvider1.getMirrorServiceName.and.returnValue(
+ mr.mirror.ServiceName.CAST_STREAMING);
+ mockProvider1.createRoute.and.callFake(() => {
+ providerManager.onRouteAdded(mockProvider1, localRoute);
+ return mr.CancellablePromise.resolve(localRoute);
+ });
+ const error = Error('failed to mirror');
+ mockCastMirrorService.startMirroring.and.callFake(
+ () => mr.CancellablePromise.reject(error));
+
+ providerManager.createRoute(sourceUrn, sinkId, presentationId)
+ .then(route => {
+ providerManager.startMirroring(mockProvider1, route, presentationId)
+ .promise.catch(err => {
+ expect(err).toEqual(error);
+ expect(mockProvider1.createRoute.calls.count()).toBe(1);
+ expect(mockProvider1.createRoute)
+ .toHaveBeenCalledWith(
+ sourceUrn, sinkId, presentationId, false,
+ mr.ProviderManager.CREATE_ROUTE_TIMEOUT_MS, undefined,
+ undefined);
+ expect(mockProvider2.createRoute.calls.count()).toBe(0);
+ expect(mockCastMirrorService.startMirroring.calls.count())
+ .toBe(1);
+ expect(mockCastMirrorService.startMirroring)
+ .toHaveBeenCalledWith(
+ localRoute, sourceUrn, mirrorSettings, presentationId,
+ undefined);
+ // Call onMirrorSessionEnded as the mirror service would.
+
+ providerManager.onMirrorSessionEnded(route.id);
+ // Call onRouteRemoved as the provider would.
+
+ providerManager.onRouteRemoved(mockProvider1, route);
+ expect(providerManager.routeIdToProvider_.has(localRoute.id))
+ .toBe(false);
+ expect(providerManager.routeIdToMirrorServiceName_.has(
+ localRoute.id))
+ .toBe(false);
+
+ done();
+ });
+ });
+ });
+
+ it('an mr.RouteRequestError is returned for user cancellation', (done) => {
+ mockProvider1.canRoute.and.returnValue(true);
+ mockProvider1.getMirrorSettings.and.returnValue({});
+ mockProvider1.getMirrorServiceName.and.returnValue(
+ mr.mirror.ServiceName.CAST_STREAMING);
+ const error = new mr.mirror.Error(
+ '',
+ mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_USER_CANCEL);
+ mockCastMirrorService.startMirroring.and.callFake(
+ () => mr.CancellablePromise.reject(error));
+
+ providerManager
+ .startMirroring(
+ mockProvider1,
+ new mr.Route(
+ 'r2', 'pId', sinkId, mr.MediaSourceUtils.DESKTOP_MIRROR_URN,
+ true, '', null),
+ presentationId)
+ .promise.catch(err => {
+ expect(err instanceof mr.RouteRequestError).toBe(true);
+ expect(err.errorCode).toBe(mr.RouteRequestResultCode.CANCELLED);
+ done();
+ });
+ });
+
+ it('createRoute with default timeout', function(done) {
+ mockClock = mr.UnitTestUtils.useMockClockAndPromises();
+
+ mockProvider1.canRoute.and.returnValue(true);
+ mockProvider2.canRoute.and.returnValue(false);
+ // The provider will never resolve the promise. Provider manager will
+ // reject it on timeout.
+ mockProvider1.createRoute.and.returnValue(
+ new mr.CancellablePromise(() => {}));
+ providerManager.createRoute(sourceUrn, sinkId, presentationId)
+ .then(
+ r => {
+ fail('Route unexpectedly created');
+ },
+ e => {
+ expect(e instanceof mr.RouteRequestError).toBe(true);
+ expect(e.errorCode)
+ .toEqual(mr.RouteRequestResultCode.TIMED_OUT);
+ expect(e.message).toMatch('timeout');
+ done();
+ });
+ mockClock.tick(mr.ProviderManager.CREATE_ROUTE_TIMEOUT_MS + 1);
+ });
+
+ it('createRoute with custom timeout', function(done) {
+ mockClock = mr.UnitTestUtils.useMockClockAndPromises();
+
+ mockProvider1.canRoute.and.returnValue(true);
+ mockProvider2.canRoute.and.returnValue(false);
+ // The provider will never resolve the promise. Provider manager will
+ // reject it on timeout.
+ mockProvider1.createRoute.and.returnValue(
+ new mr.CancellablePromise(() => {}));
+ const timeoutMillis = 20 * 1000;
+ providerManager
+ .createRoute(
+ sourceUrn, sinkId, 'presentationId', 'origin', 0, timeoutMillis)
+ .then(
+ r => {
+ fail('Route unexpectedly created');
+ },
+ e => {
+ expect(e instanceof mr.RouteRequestError).toBe(true);
+ expect(e.errorCode)
+ .toEqual(mr.RouteRequestResultCode.TIMED_OUT);
+ expect(e.message).toMatch('timeout');
+ done();
+ });
+ mockClock.tick(timeoutMillis + 1);
+ });
+
+ it('update mirror session', function(done) {
+ localRoute = new mr.Route('r2', 'pId', sinkId, sourceUrn, true, '', null);
+ mockProvider1.canRoute.and.returnValue(true);
+ mockProvider2.canRoute.and.returnValue(false);
+ const mirrorSettings = {};
+ mockProvider1.getMirrorSettings.and.returnValue(mirrorSettings);
+ mockProvider1.getMirrorServiceName.and.returnValue(
+ mr.mirror.ServiceName.CAST_STREAMING);
+ mockProvider1.createRoute.and.callFake(() => {
+ providerManager.onRouteAdded(this, localRoute);
+ return mr.CancellablePromise.resolve(localRoute);
+ });
+ mockCastMirrorService.startMirroring.and.returnValue(
+ mr.CancellablePromise.resolve(localRoute));
+ mockCastMirrorService.updateMirroring.and.returnValue(
+ mr.CancellablePromise.resolve(localRoute));
+ providerManager.createRoute(sourceUrn, sinkId, presentationId)
+ .then(route => {
+ providerManager.startMirroring(mockProvider1, route, presentationId)
+ .promise.then(r => {
+ expect(mockProvider1.createRoute.calls.count()).toBe(1);
+ expect(mockProvider1.createRoute)
+ .toHaveBeenCalledWith(
+ sourceUrn, sinkId, presentationId, false,
+ mr.ProviderManager.CREATE_ROUTE_TIMEOUT_MS, undefined,
+ undefined);
+ expect(mockProvider2.createRoute.calls.count()).toBe(0);
+ expect(mockCastMirrorService.startMirroring.calls.count())
+ .toBe(1);
+ expect(mockCastMirrorService.startMirroring)
+ .toHaveBeenCalledWith(
+ localRoute, sourceUrn, mirrorSettings, presentationId,
+ undefined);
+ expect(providerManager.routeIdToProvider_.has(localRoute.id))
+ .toBe(true);
+ expect(providerManager.routeIdToMirrorServiceName_.has(
+ localRoute.id))
+ .toBe(true);
+
+ const newSourceUrn = mr.MediaSourceUtils.DESKTOP_MIRROR_URN;
+ const callback = function() {};
+ providerManager
+ .updateMirroring(
+ mockProvider1, r, newSourceUrn, presentationId,
+ undefined, callback)
+ .promise.then(updatedRoute => {
+ expect(
+ mockCastMirrorService.updateMirroring.calls.count())
+ .toBe(1);
+ expect(mockCastMirrorService.updateMirroring)
+ .toHaveBeenCalledWith(
+ r, newSourceUrn, mirrorSettings, presentationId,
+ undefined, callback);
+ done();
+ });
+ });
+ });
+ });
+
+ it('updateMirroring before startMirroring fails', function(done) {
+ localRoute = new mr.Route('r2', 'pId', sinkId, sourceUrn, true, '', null);
+ const callback = function() {};
+
+ providerManager
+ .updateMirroring(
+ mockProvider1, localRoute, sourceUrn, undefined, callback)
+ .promise.catch(done);
+ });
+ });
+
+ describe('Test PersistentData', function() {
+ it('ProviderManager is restored properly as PersistentData', function() {
+ providerManager.registerAllProviders([mockProvider1]);
+ providerManager.sinkQueries_.add(sourceUrn);
+ providerManager.routeQueries_.add(sourceUrn);
+ providerManager.routeIdToProvider_.set(routeId, mockProvider1);
+ providerManager.sinkAvailabilityMap_.set(
+ mockProvider1Name, mr.SinkAvailability.AVAILABLE);
+
+ const expectedSinkQueries = new Set(providerManager.sinkQueries_);
+ const expectedRouteQueries = new Set(providerManager.routeQueries_);
+
+ const data = providerManager.getData();
+ expect(data.length).toEqual(1);
+ const tempData = data[0];
+ expect(tempData.providerNames).toEqual([mockProvider1Name]);
+ expect(tempData.sinkQueries).toEqual([sourceUrn]);
+ expect(tempData.routeQueries).toEqual([sourceUrn]);
+ expect(tempData.routeIdToProviderName).toEqual([
+ [routeId, mockProvider1Name]
+ ]);
+ expect(tempData.sinkAvailabilityMap).toEqual([
+ [mockProvider1Name, mr.SinkAvailability.AVAILABLE]
+ ]);
+
+ // Data is saved to localStorage.
+ mr.PersistentDataManager.onSuspend_();
+
+ // Make PersistentDataManager forget providerManager, so it can be
+ // registered again.
+ mr.PersistentDataManager.dataInstances_.clear();
+
+ providerManager.routeIdToProvider_.clear();
+ providerManager.sinkQueries_.clear();
+ providerManager.routeQueries_.clear();
+ providerManager.sinkAvailabilityMap_.clear();
+
+ // Load data back to providerManager.
+ mockProvider1.getAvailableSinks.and.returnValue(mr.SinkList.EMPTY);
+ mr.PersistentDataManager.register(providerManager);
+ expect(providerManager.sinkQueries_).toEqual(expectedSinkQueries);
+ expect(providerManager.routeQueries_).toEqual(expectedRouteQueries);
+ expect(providerManager.routeIdToProvider_.get(routeId))
+ .toEqual(mockProvider1);
+ expect(providerManager.sinkAvailabilityMap_.get(mockProvider1Name))
+ .toEqual(mr.SinkAvailability.AVAILABLE);
+ });
+
+ it('ProviderManager calls mDNS callbacks if mDNS enabled', function() {
+ let callbackRuns = 0;
+ const callback = function() {
+ ++callbackRuns;
+ };
+ providerManager.mdnsEnabled_ = true;
+
+ // Data is saved to localStorage.
+ mr.PersistentDataManager.onSuspend_();
+
+ // Prevent immediate callback so we are sure it runs in loadSavedData
+ providerManager.mdnsEnabled_ = false;
+ providerManager.registerMdnsDiscoveryEnabledCallback(callback);
+ expect(callbackRuns).toBe(0);
+
+ // Make PersistentDataManager forget providerManager, so it can be
+ // registered again.
+ mr.PersistentDataManager.dataInstances_.clear();
+
+ // Load data back to providerManager.
+ mockProvider1.getAvailableSinks.and.returnValue(mr.SinkList.EMPTY);
+ mr.PersistentDataManager.register(providerManager);
+ expect(callbackRuns).toBe(1);
+ });
+ });
+
+ describe('Test onRouteRemoved', function() {
+ let sourceUrn;
+ let sinkId;
+ let localRoute;
+
+ beforeEach(function() {
+ sourceUrn = 'urn:dial-multiscreen-org:dial:application:YouTube';
+ sinkId = 'sink1';
+ localRoute = new mr.Route('r2', 'pId', sinkId, sourceUrn, true, '', null);
+ providerManager.registerAllProviders([mockProvider1]);
+ spyOn(providerManager.mrRouteMessageSender_, 'onRouteRemoved')
+ .and.callThrough();
+ sourceUrn = mr.MediaSourceUtils.DESKTOP_MIRROR_URN;
+ localRoute = new mr.Route('r2', 'pId', sinkId, sourceUrn, true, '', null);
+ mockProvider1.canRoute.and.returnValue(true);
+ const mirrorSettings = {};
+ mockProvider1.getMirrorSettings.and.returnValue(mirrorSettings);
+ mockProvider1.getMirrorServiceName.and.returnValue(
+ mr.mirror.ServiceName.CAST_STREAMING);
+ mockProvider1.createRoute.and.callFake(() => {
+ providerManager.onRouteAdded(mockProvider1, localRoute);
+ return mr.CancellablePromise.resolve(localRoute);
+ });
+ mockCastMirrorService.startMirroring.and.returnValue(
+ mr.CancellablePromise.resolve(localRoute));
+ });
+
+ it('On a mirror session stopped', function(done) {
+ let count = 0;
+ const checkDone = () => {
+ if (++count == 2) {
+ expect(providerManager.routeIdToProvider_.has(localRoute.id))
+ .toBe(false);
+ expect(providerManager.routeIdToMirrorServiceName_.has(localRoute.id))
+ .toBe(false);
+ expect(mockCastMirrorService.stopCurrentMirroring).toHaveBeenCalled();
+ expect(providerManager.mrRouteMessageSender_.onRouteRemoved)
+ .toHaveBeenCalledWith(localRoute.id);
+ done();
+ }
+ };
+ mockCastMirrorService.stopCurrentMirroring.and.callFake(checkDone);
+ providerManager.mrRouteMessageSender_.onRouteRemoved.and.callFake(
+ checkDone);
+ providerManager.createRoute(sourceUrn, sinkId, presentationId)
+ .then(route => {
+ providerManager.startMirroring(mockProvider1, route)
+ .promise.then(r => {
+ expect(route.id).toEqual(localRoute.id);
+ expect(providerManager.routeIdToProvider_.has(localRoute.id))
+ .toBe(true);
+ expect(providerManager.routeIdToMirrorServiceName_.has(
+ localRoute.id))
+ .toBe(true);
+ providerManager.onRouteRemoved(mockProvider1, localRoute);
+ });
+ });
+ });
+ });
+
+ it('Test add/remove MediaSinksQuery', function() {
+ const sourceUrn = 'urn:x-org.chromium.media:source:desktop';
+ const sink1 = new mr.Sink('s1', 'sink1');
+ const sink2 = new mr.Sink('s2', 'sink2');
+ const origins = ['https://www.google.com', 'https://youtube.com'];
+ providerManager.registerAllProviders([mockProvider1, mockProvider2]);
+ mockProvider1.getAvailableSinks.and.returnValue(
+ new mr.SinkList([sink1, sink2], origins));
+ mockProvider2.getAvailableSinks.and.returnValue(mr.SinkList.EMPTY);
+ providerManager.startObservingMediaSinks(sourceUrn);
+ expect(mockProvider1.startObservingMediaSinks)
+ .toHaveBeenCalledWith(sourceUrn);
+ expect(mockProvider2.startObservingMediaSinks)
+ .toHaveBeenCalledWith(sourceUrn);
+ expect(mockMediaRouterService.onSinksReceived)
+ .toHaveBeenCalledWith(sourceUrn, [sink1, sink2], jasmine.any(Object));
+ const originList =
+ mockMediaRouterService.onSinksReceived.calls.argsFor(0)[2];
+ expect(originList.length).toBe(2);
+ expect(originList[0].scheme).toBe('https');
+ expect(originList[0].host).toBe('www.google.com');
+ expect(originList[1].scheme).toBe('https');
+ expect(originList[1].host).toBe('youtube.com');
+ // add again
+ providerManager.startObservingMediaSinks(sourceUrn);
+ expect(mockProvider1.startObservingMediaSinks.calls.count()).toBe(1);
+ expect(mockProvider2.startObservingMediaSinks.calls.count()).toBe(1);
+ expect(mockMediaRouterService.onSinksReceived.calls.count()).toBe(2);
+ // remove
+ providerManager.stopObservingMediaSinks(sourceUrn);
+ expect(mockProvider1.stopObservingMediaSinks)
+ .toHaveBeenCalledWith(sourceUrn);
+ expect(mockProvider2.stopObservingMediaSinks)
+ .toHaveBeenCalledWith(sourceUrn);
+ // add again
+ providerManager.startObservingMediaSinks(sourceUrn);
+ expect(mockProvider1.startObservingMediaSinks.calls.count()).toBe(2);
+ expect(mockProvider2.startObservingMediaSinks.calls.count()).toBe(2);
+ expect(mockMediaRouterService.onSinksReceived.calls.count()).toBe(3);
+ });
+
+ it('Test onSinkAvailabilityUpdated', function() {
+ providerManager.registerAllProviders([mockProvider1, mockProvider2]);
+
+ providerManager.onSinkAvailabilityUpdated(
+ mockProvider1, mr.SinkAvailability.UNAVAILABLE);
+ expect(mockMediaRouterService.onSinkAvailabilityUpdated)
+ .not.toHaveBeenCalled();
+
+ providerManager.onSinkAvailabilityUpdated(
+ mockProvider1, mr.SinkAvailability.PER_SOURCE);
+ expect(mockMediaRouterService.onSinkAvailabilityUpdated)
+ .toHaveBeenCalledWith(mr.SinkAvailability.PER_SOURCE);
+
+ providerManager.onSinkAvailabilityUpdated(
+ mockProvider2, mr.SinkAvailability.PER_SOURCE);
+ expect(mockMediaRouterService.onSinkAvailabilityUpdated.calls.count())
+ .toBe(1);
+
+ providerManager.onSinkAvailabilityUpdated(
+ mockProvider2, mr.SinkAvailability.AVAILABLE);
+ expect(mockMediaRouterService.onSinkAvailabilityUpdated)
+ .toHaveBeenCalledWith(mr.SinkAvailability.AVAILABLE);
+
+ providerManager.onSinkAvailabilityUpdated(
+ mockProvider1, mr.SinkAvailability.UNAVAILABLE);
+ expect(mockMediaRouterService.onSinkAvailabilityUpdated.calls.count())
+ .toBe(2);
+
+ providerManager.onSinkAvailabilityUpdated(
+ mockProvider2, mr.SinkAvailability.UNAVAILABLE);
+ expect(mockMediaRouterService.onSinkAvailabilityUpdated)
+ .toHaveBeenCalledWith(mr.SinkAvailability.UNAVAILABLE);
+ });
+
+ it('Test mDNS callbacks wait until mDNS is enabled', function() {
+ providerManager.mdnsEnabled_ = true;
+ let callbackRuns = 0;
+ const callback = function() {
+ ++callbackRuns;
+ };
+ providerManager.registerMdnsDiscoveryEnabledCallback(callback);
+ // Execute immediately when mDNS discovery is enabled.
+ expect(callbackRuns).toBe(1);
+
+ callbackRuns = 0;
+ providerManager.mdnsEnabled_ = false;
+ providerManager.registerMdnsDiscoveryEnabledCallback(callback);
+ // Defer execution until mDNS discovery is enabled.
+ expect(callbackRuns).toBe(0);
+ providerManager.enableMdnsDiscovery();
+ expect(callbackRuns).toBe(1);
+ });
+
+ it('Test mDNS callback registration disallows duplicates', function() {
+ providerManager.mdnsEnabled_ = true;
+ let callbackRuns = 0;
+ const callback = function() {
+ ++callbackRuns;
+ };
+
+ providerManager.mdnsEnabled_ = false;
+ providerManager.registerMdnsDiscoveryEnabledCallback(callback);
+ providerManager.registerMdnsDiscoveryEnabledCallback(callback);
+ // Defer execution until mDNS discovery is enabled and ensure only called
+ // once.
+ expect(callbackRuns).toBe(0);
+ providerManager.enableMdnsDiscovery();
+ expect(callbackRuns).toBe(1);
+ });
+
+ describe('searchSinks', function() {
+ const getSearchCriteria = function(input) {
+ return {'input': input, 'domain': 'google.com'};
+ };
+ let pseudoId;
+
+ beforeEach(function() {
+ sourceUrn = 'urn:x-org.chromium.media:source:desktop';
+ pseudoId = 'pseudo:' + mockProvider1Name;
+ providerManager.registerAllProviders([mockProvider1, mockProvider2]);
+ });
+
+ it('returns the sink id if the provider returns a sink', done => {
+ const sinkId = 'sinkId1';
+ const sinkName = 'sink name';
+ const foundSink = new mr.Sink(sinkId, sinkName);
+ mockProvider1.searchSinks.and.returnValue(Promise.resolve(foundSink));
+
+ providerManager
+ .searchSinks(pseudoId, sourceUrn, getSearchCriteria(sinkName))
+ .then((resolvedSinkId) => {
+ expect(resolvedSinkId).toBe(sinkId);
+ done();
+ });
+ });
+
+ it('returns nothing if the provider returns nothing', done => {
+ mockProvider1.searchSinks.and.returnValue(
+ Promise.resolve(new mr.Sink('', '')));
+ providerManager
+ .searchSinks(pseudoId, sourceUrn, getSearchCriteria('sink name'))
+ .then((resolvedSinkId) => expect(resolvedSinkId).toBe(''))
+ .then(done, done.fail);
+ });
+
+ it('returns nothing when no provider owns the pseudo sink', done => {
+ pending(
+ 'This may never have worked, because the test ' +
+ 'wasn\'t checking what it was supposed to check.');
+ pseudoId = 'pseudo:bad';
+ providerManager
+ .searchSinks(pseudoId, sourceUrn, getSearchCriteria('sink name'))
+ .then((resolvedSinkId) => expect(resolvedSinkId).toBe(''))
+ .then(done, done.fail);
+ });
+ });
+
+ describe('updateMediaSinks', function() {
+ const sink1 = new mr.Sink('s1', 'sink1');
+ const sink2 = new mr.Sink('s2', 'sink2');
+
+ beforeEach(function() {
+ sourceUrn = 'urn:x-org.chromium.media:source:desktop';
+ providerManager.registerAllProviders([mockProvider1, mockProvider2]);
+ mockProvider1.getAvailableSinks.and.returnValue(
+ new mr.SinkList([sink1, sink2], ['https://www.google.com']));
+ mockProvider2.getAvailableSinks.and.returnValue(mr.SinkList.EMPTY);
+ });
+
+ it('queries providers with desktop source', function() {
+ providerManager.updateMediaSinks(sourceUrn);
+ expect(mockProvider1.getAvailableSinks).toHaveBeenCalledWith(sourceUrn);
+ expect(mockProvider2.getAvailableSinks).toHaveBeenCalledWith(sourceUrn);
+ expect(mockMediaRouterService.onSinksReceived)
+ .toHaveBeenCalledWith(
+ sourceUrn, [sink1, sink2], [jasmine.any(Object)]);
+ const originList =
+ mockMediaRouterService.onSinksReceived.calls.argsFor(0)[2];
+ expect(originList.length).toBe(1);
+ expect(originList[0].scheme).toBe('https');
+ expect(originList[0].host).toBe('www.google.com');
+ });
+
+ it('returns immediately if query already exists', function() {
+ providerManager.sinkQueries_.add(sourceUrn);
+ // updateMediaSinks should return and not touch providers
+ providerManager.updateMediaSinks(sourceUrn);
+ expect(mockProvider1.getAvailableSinks).not.toHaveBeenCalled();
+ expect(mockProvider2.getAvailableSinks).not.toHaveBeenCalled();
+ expect(mockMediaRouterService.onSinksReceived).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('calls terminateRoute', function() {
+ const routeIdToTerminate = 'deadbeef';
+
+ beforeEach(function() {
+ providerManager.registerAllProviders([mockProvider1]);
+ providerManager.routeIdToProvider_.set(routeIdToTerminate, mockProvider1);
+ });
+
+ it('with correct routeId', function(done) {
+ mockProvider1.terminateRoute.and.callFake(routeId => {
+ expect(routeId).toBe(routeIdToTerminate);
+ done();
+ });
+ providerManager.terminateRoute(routeIdToTerminate);
+ });
+
+ it('and returns a rejected promise when it fails', function(done) {
+ mockProvider1.terminateRoute.and.callFake(routeId => {
+ expect(routeId).toBe(routeIdToTerminate);
+ return Promise.reject(new Error('Some error'));
+ });
+ providerManager.terminateRoute(routeIdToTerminate).then(done.fail, done);
+ });
+
+ it('and terminates a mirroring route', function(done) {
+ providerManager.routeIdToMirrorServiceName_.set(
+ routeIdToTerminate, mr.mirror.ServiceName.CAST_STREAMING);
+ providerManager.terminateRoute(routeIdToTerminate).then(() => {
+ expect(mockProvider1.terminateRoute)
+ .toHaveBeenCalledWith(routeIdToTerminate);
+ expect(mockCastMirrorService.stopCurrentMirroring).toHaveBeenCalled();
+ expect(
+ providerManager.routeIdToMirrorServiceName_.has(routeIdToTerminate))
+ .toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('createMediaRouteController tests', () => {
+ const routeId = 'deadbeef';
+ let controller;
+ let observer;
+
+ beforeEach(() => {
+ controller = mr.UnitTestUtils.createMojoRequestSpyObj();
+ observer = mr.UnitTestUtils.createMojoMediaStatusObserverSpyObj();
+ providerManager.registerAllProviders([mockProvider1]);
+ providerManager.routeIdToProvider_.set(routeId, mockProvider1);
+ });
+
+ it('succeeds', done => {
+ mockProvider1.createMediaRouteController.and.callFake(
+ () => Promise.resolve());
+ providerManager.createMediaRouteController(routeId, controller, observer)
+ .then(() => {
+ expect(mockProvider1.createMediaRouteController).toHaveBeenCalled();
+ expect(controller.close).not.toHaveBeenCalled();
+ expect(observer.ptr.reset).not.toHaveBeenCalled();
+ done();
+ }, fail);
+ });
+
+ it('fails if route does not exist', done => {
+ providerManager
+ .createMediaRouteController(
+ 'nonexistentRouteId', controller, observer)
+ .then(fail, () => {
+ expect(controller.close).toHaveBeenCalled();
+ expect(observer.ptr.reset).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('fails if provider fails', done => {
+ mockProvider1.createMediaRouteController.and.callFake(
+ () => Promise.reject('Foo'));
+ providerManager.createMediaRouteController(routeId, controller, observer)
+ .then(fail, () => {
+ expect(mockProvider1.createMediaRouteController).toHaveBeenCalled();
+ expect(controller.close).toHaveBeenCalled();
+ expect(observer.ptr.reset).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/route_id.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/route_id.js
new file mode 100644
index 00000000000..a5d9f37a9d0
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/route_id.js
@@ -0,0 +1,114 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview A routeId includes presentation ID, provider name,
+ * sink ID and media source. See mr.RouteId.getRouteId method for its
+ * format (note the different formatting of routeId for 2UA cases).
+ */
+
+goog.provide('mr.RouteId');
+
+goog.require('mr.MediaSourceUtils');
+
+/**
+ * @final
+ */
+mr.RouteId = class {
+ constructor() {
+ /** @private {string} */
+ this.routeId_;
+
+ /** @private {string} */
+ this.presentationId_;
+
+ /** @private {string} */
+ this.providerName_;
+
+ /** @private {string} */
+ this.sinkId_;
+
+ /** @private {string} */
+ this.source_;
+ }
+
+ /**
+ * @param {string} routeId
+ * @return {?mr.RouteId} A route ID object if the input string is valid;
+ * null otherwise.
+ */
+ static create(routeId) {
+ if (!routeId.startsWith(mr.RouteId.PREFIX_)) {
+ return null;
+ }
+ const suffix = routeId.substring(mr.RouteId.PREFIX_.length);
+ if (!suffix) {
+ return null;
+ }
+ const match = suffix.match(/([^/]*)\/([^-/]*)-([^/]*)\/(.*)/);
+ if (!match) {
+ return null;
+ }
+ const obj = new mr.RouteId();
+ obj.routeId_ = routeId;
+ [, obj.presentationId_, obj.providerName_, obj.sinkId_, obj.source_] =
+ match;
+ return obj;
+ }
+
+ /**
+ * @param {string} presentationId
+ * @param {string} providerName
+ * @param {string} sinkId
+ * @param {?string} source
+ * @return {string}
+ */
+ static getRouteId(presentationId, providerName, sinkId, source) {
+ // In a 2UA mode, the routeId should be the presentationId.
+ if (source && mr.MediaSourceUtils.isPresentationSource(source)) {
+ return presentationId;
+ }
+ return mr.RouteId.PREFIX_ + presentationId + '/' + providerName + '-' +
+ sinkId + '/' + source;
+ }
+
+ /**
+ * @return {string}
+ */
+ getRouteId() {
+ return this.routeId_;
+ }
+
+ /**
+ * @return {string}
+ */
+ getPresentationId() {
+ return this.presentationId_;
+ }
+
+ /**
+ * @return {string}
+ */
+ getProviderName() {
+ return this.providerName_;
+ }
+
+ /**
+ * @return {string}
+ */
+ getSinkId() {
+ return this.sinkId_;
+ }
+
+ /**
+ * @return {string}
+ */
+ getSource() {
+ return this.source_;
+ }
+};
+
+
+/** @private @const {string} */
+mr.RouteId.PREFIX_ = 'urn:x-org.chromium:media:route:';
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/route_id_test.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/route_id_test.js
new file mode 100644
index 00000000000..daff81e818f
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/route_id_test.js
@@ -0,0 +1,41 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.require('mr.RouteId');
+
+describe('Tests RouteId', function() {
+ it('Test getRouteId', function() {
+ expect(mr.RouteId.getRouteId('123', 'cast', 'sink1', 'some-source'))
+ .toEqual(mr.RouteId.PREFIX_ + '123/cast-sink1/some-source');
+ });
+
+ it('Test getRouteId in 2UA mode', function() {
+ expect(mr.RouteId.getRouteId('123', 'cast', 'sink1', 'http://mysite.com'))
+ .toEqual('123');
+ });
+
+ it('Test getRouteId with valid input', function() {
+ let routeId = mr.RouteId.PREFIX_ + '123/cast-sink1/some-source';
+ let obj = mr.RouteId.create(routeId);
+ expect(obj.getRouteId()).toEqual(routeId);
+ expect(obj.getPresentationId()).toEqual('123');
+ expect(obj.getProviderName()).toEqual('cast');
+ expect(obj.getSinkId()).toEqual('sink1');
+ expect(obj.getSource()).toEqual('some-source');
+
+ routeId = mr.RouteId.PREFIX_ + '/cast-sink1/some-source';
+ obj = mr.RouteId.create(routeId);
+ expect(obj.getRouteId()).toEqual(routeId);
+ expect(obj.getPresentationId()).toEqual('');
+ expect(obj.getProviderName()).toEqual('cast');
+ expect(obj.getSinkId()).toEqual('sink1');
+ expect(obj.getSource()).toEqual('some-source');
+ });
+
+ it('Test getRouteId with invalid input 1', function() {
+ expect(mr.RouteId.create('123')).toBe(null);
+ expect(mr.RouteId.create(mr.RouteId.PREFIX_ + '123/cast-sink1')).toBe(null);
+ expect(mr.RouteId.create(mr.RouteId.PREFIX_ + '123/cast-')).toBe(null);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/route_message_port.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/route_message_port.js
new file mode 100644
index 00000000000..d6cae814fe9
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/route_message_port.js
@@ -0,0 +1,74 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview API for posting messages to a route and getting messages from
+ * the route.
+ */
+
+goog.provide('mr.MessagePort');
+goog.provide('mr.MessagePortService');
+
+
+
+/**
+ * @record
+ */
+mr.MessagePort = class {
+ /**
+ * Called to post a message to the sink via a media route.
+ * @param {!Object|string} message The message to post. Object will be
+ * serialized to a JSON string.
+ * @param {Object=} opt_extraInfo Extra info about how to send a message.
+ * @return {!Promise} Fulfilled when the message is posted, or rejected
+ * if there an error.
+ */
+ sendMessage(message, opt_extraInfo) {}
+
+ /**
+ * Releases any resources used by this object.
+ */
+ dispose() {}
+};
+
+
+/**
+ * The message handler to invoke when messages associated
+ * with the route arrives.
+ * @type {function((!Object|string))}
+ */
+mr.MessagePort.prototype.onMessage;
+
+
+
+/**
+ * @record
+ */
+mr.MessagePortService = class {
+ /**
+ * Returns the MessagePort for internal communication with the provider.
+ * @param {string} routeId
+ * @return {!mr.MessagePort}
+ */
+ getInternalMessenger(routeId) {}
+ /**
+ * @return {!mr.MessagePortService}
+ */
+ static getService() {
+ return mr.MessagePortService.service_;
+ }
+
+ /**
+ * @param {!mr.MessagePortService} service
+ */
+ static setService(service) {
+ if (!mr.MessagePortService.service_) {
+ mr.MessagePortService.service_ = service;
+ }
+ }
+};
+
+
+/** @private {!mr.MessagePortService} */
+mr.MessagePortService.service_;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/route_message_port_impl.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/route_message_port_impl.js
new file mode 100644
index 00000000000..78efd2d018c
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/route_message_port_impl.js
@@ -0,0 +1,107 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+
+goog.provide('mr.MessagePortServiceImpl');
+
+goog.require('mr.EventTarget');
+goog.require('mr.MessagePort');
+goog.require('mr.MessagePortService');
+goog.require('mr.ProviderEventType');
+
+
+/**
+ * @template T
+ * @implements {mr.MessagePort}
+ * @private
+ */
+mr.MessagePortImpl_ = class {
+ /**
+ * @param {string} routeId
+ * @param {function(string, (!Object|string), Object=): !Promise}
+ * sendMessage
+ * @param {!mr.EventTarget} messageEventTarget
+ */
+ constructor(routeId, sendMessage, messageEventTarget) {
+ /** @private {string} */
+ this.routeId_ = routeId;
+
+ /** @private {function(string, (!Object|string), Object=): !Promise} */
+ this.sendMessage_ = sendMessage;
+
+ /** @private {!mr.EventTarget} */
+ this.messageEventTarget_ = messageEventTarget;
+
+ /** @type {function((!Object|string))} */
+ this.onMessage = () => {};
+
+ messageEventTarget.listen(
+ mr.ProviderEventType.INTERNAL_MESSAGE, this.onRouteMessage_, this);
+ }
+
+ /**
+ * @override
+ */
+ sendMessage(message, opt_extraInfo) {
+ return this.sendMessage_(this.routeId_, message, opt_extraInfo);
+ }
+
+ /**
+ * Called when a message is received.
+ *
+ * @param {mr.InternalMessageEvent.<!Object|string>} event
+ * @private
+ */
+ onRouteMessage_(event) {
+ if (event.routeId != this.routeId_) {
+ return;
+ }
+ this.onMessage(event.message);
+ }
+
+ /**
+ * @override
+ */
+ dispose() {
+
+ this.onMessage = () => {};
+ this.messageEventTarget_.unlisten(
+ mr.ProviderEventType.INTERNAL_MESSAGE, this.onRouteMessage_, this);
+ }
+};
+
+
+/** @private @const {!mr.MessagePort} */
+mr.MessagePortImpl_.NULL_INSTANCE_ =
+ new mr.MessagePortImpl_('', () => Promise.resolve(), new mr.EventTarget());
+
+
+/**
+ * @implements {mr.MessagePortService}
+ */
+mr.MessagePortServiceImpl = class {
+ /**
+ * @param {!mr.ProviderManagerCallbacks} providerManagerCallbacks
+ */
+ constructor(providerManagerCallbacks) {
+ /**
+ * @private {!mr.ProviderManagerCallbacks}
+ */
+ this.providerManagerCallbacks_ = providerManagerCallbacks;
+ }
+
+ /**
+ * @override
+ */
+ getInternalMessenger(routeId) {
+ const provider =
+ this.providerManagerCallbacks_.getProviderFromRouteId(routeId);
+ return !provider ?
+ mr.MessagePortImpl_.NULL_INSTANCE_ :
+ new mr.MessagePortImpl_(
+ routeId, provider.sendRouteMessage.bind(provider),
+ this.providerManagerCallbacks_.getRouteMessageEventTarget());
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/manager/sink_availability.js b/chromium/chrome/browser/resources/media_router/extension/src/manager/sink_availability.js
new file mode 100644
index 00000000000..5c3c1ce0862
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/manager/sink_availability.js
@@ -0,0 +1,21 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Enum for per-media-route-provider sink availability.
+ */
+
+goog.provide('mr.SinkAvailability');
+
+
+/**
+ * Per-provider sink availability.
+ * Keep in sync with MediaRouter.SinkAvailability in media_router.mojom.
+ * @enum {number}
+ */
+mr.SinkAvailability = {
+ UNAVAILABLE: 0,
+ PER_SOURCE: 1,
+ AVAILABLE: 2
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_activity.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_activity.js
new file mode 100644
index 00000000000..df658d67a66
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_activity.js
@@ -0,0 +1,150 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Mirroring activities for display in the UI.
+ */
+
+goog.module('mr.mirror.Activity');
+goog.module.declareLegacyNamespace();
+
+const Assertions = goog.require('mr.Assertions');
+const MediaSourceUtils = goog.require('mr.MediaSourceUtils');
+
+
+/**
+ * Possible mirroring activities.
+ * @enum {string}
+ */
+const Type = {
+ MIRROR_TAB: 'mirror_tab',
+ MIRROR_DESKTOP: 'mirror_desktop',
+ MIRROR_FILE: 'mirror_file',
+ MEDIA_REMOTING: 'media_remoting',
+ PRESENTATION: 'presentation'
+};
+
+
+/**
+ * Describes a mirroring activity for display in the UI.
+ */
+const Activity = class {
+ /**
+ * Constructs a new Activity describing a mirroring route. origin is
+ * required, unless the activityType is MIRROR_DESKTOP. Both contentTitle and
+ * origin may change over the course of a mirroring activity from tab
+ * navigation.
+ *
+ * @param {!Type} type
+ * @param {boolean} incognito
+ * @param {?string=} origin The top level origin of the tab being cast.
+ */
+ constructor(type, incognito, origin = null) {
+ /** @private {!Type} */
+ this.type_ = type;
+
+ /** @private {boolean} */
+ this.incognito_ = incognito;
+
+ /** @private {?string} */
+ this.origin_ = origin;
+
+ /** @private {?string} */
+ this.contentTitle_ = null;
+ }
+
+ /**
+ * @param {!mr.Route} route A mirroring route
+ * @return {!Activity} Intital activity object for route
+ */
+ static createFromRoute(route) {
+ let type;
+ let origin = null;
+ const source = /** @type {string} */ (Assertions.assert(route.mediaSource));
+ if (MediaSourceUtils.isPresentationSource(source)) {
+ type = Type.PRESENTATION;
+ origin = new URL(source).origin;
+ } else if (MediaSourceUtils.isTabMirrorSource(source)) {
+ // Tab mirroring routes may switch to MEDIA_REMOTING or MIRROR_FILE after
+ // they have begun.
+ type = Type.MIRROR_TAB;
+ } else if (MediaSourceUtils.isDesktopMirrorSource(source)) {
+ type = Type.MIRROR_DESKTOP;
+ }
+ Assertions.assert(type, `Unexpected mediaSource ${source}`);
+ return new Activity(
+ /** @type {Type} */ (type), route.offTheRecord, origin);
+ }
+
+ /** @param {Type} type Sets the activity type. */
+ setType(type) {
+ this.type_ = type;
+ }
+
+ /** @param {?string} origin Sets the current origin of the activity. */
+ setOrigin(origin) {
+ this.origin_ = origin;
+ }
+
+ /** @param {?string} contentTitle Sets the content title of the activity. */
+ setContentTitle(contentTitle) {
+ this.contentTitle_ = contentTitle;
+ }
+
+ /** @param {boolean} incognito Sets the incognito status of the activity. */
+ setIncognito(incognito) {
+ this.incognito_ = incognito;
+ }
+
+ /** @return {string} The string to use for the route's description. */
+ getRouteDescription() {
+ switch (this.type_) {
+ case Type.MIRROR_TAB:
+ return this.origin_ ? `Casting tab (${this.origin_})` : 'Casting tab';
+ case Type.MIRROR_DESKTOP:
+ return 'Casting desktop';
+ case Type.MIRROR_FILE:
+ return 'Casting local content';
+ case Type.MEDIA_REMOTING:
+ return this.origin_ ? `Casting media (${this.origin_})` :
+ `Casting media`;
+ case Type.PRESENTATION:
+ return `Casting ${this.origin_ || 'site'}`;
+ default:
+ Assertions.assert(false, 'Unexpected type ' + this.type_);
+ return '';
+ }
+ }
+
+ /** @return {string} The string to use for the route's media status. */
+ getRouteMediaStatus() {
+ if (this.type_ == Type.MIRROR_DESKTOP) {
+ return '';
+ }
+ return this.contentTitle_ || '';
+ }
+
+ /** @return {string} The string to broadcast to other Cast senders. */
+ getCastRemoteTitle() {
+ if (this.incognito_) return 'Casting active';
+ switch (this.type_) {
+ case Type.MIRROR_TAB:
+ return 'Casting tab';
+ case Type.MIRROR_DESKTOP:
+ return 'Casting desktop';
+ case Type.MIRROR_FILE:
+ return 'Casting local content';
+ case Type.MEDIA_REMOTING:
+ return 'Casting media';
+ case Type.PRESENTATION:
+ return 'Casting site';
+ default:
+ Assertions.assert(false, 'Unexpected type ' + this.type_);
+ return '';
+ }
+ }
+};
+
+exports = Activity;
+exports.Type = Type;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_activity_test.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_activity_test.js
new file mode 100644
index 00000000000..e431bd7dd14
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_activity_test.js
@@ -0,0 +1,93 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly();
+goog.require('mr.Route');
+goog.require('mr.mirror.Activity');
+
+describe('Tests mr.mirror.Activity', () => {
+ let tabMirrorRoute;
+ let desktopMirrorRoute;
+ let presentationRoute;
+
+ beforeEach(() => {
+ tabMirrorRoute = new mr.Route(
+ 'routeId', 'presentationId', 'sinkId',
+ 'urn:x-org.chromium.media:source:tab:47', true, null);
+ desktopMirrorRoute = new mr.Route(
+ 'routeId2', 'presentationId2', 'sinkId2',
+ 'urn:x-org.chromium.media:source:desktop', true, null);
+ presentationRoute = new mr.Route(
+ 'routeId3', 'presentationId3', 'sinkId3', 'https://www.example.com',
+ true, null);
+ });
+
+ it('creates activity from a tab mirroring route', () => {
+ let activity = mr.mirror.Activity.createFromRoute(tabMirrorRoute);
+ expect(activity.getRouteDescription()).toBe('Casting tab');
+ expect(activity.getRouteMediaStatus()).toBe('');
+ expect(activity.getCastRemoteTitle()).toBe('Casting tab');
+ });
+
+ it('creates activity from a desktop mirroring route', () => {
+ let activity = mr.mirror.Activity.createFromRoute(desktopMirrorRoute);
+ expect(activity.getRouteDescription()).toBe('Casting desktop');
+ expect(activity.getRouteMediaStatus()).toBe('');
+ expect(activity.getCastRemoteTitle()).toBe('Casting desktop');
+ });
+
+ it('creates activity from a presentation route', () => {
+ let activity = mr.mirror.Activity.createFromRoute(presentationRoute);
+ expect(activity.getRouteDescription())
+ .toBe('Casting https://www.example.com');
+ expect(activity.getRouteMediaStatus()).toBe('');
+ expect(activity.getCastRemoteTitle()).toBe('Casting site');
+ });
+
+ it('uses the tab origin and page title for tab mirroring', () => {
+ let activity = mr.mirror.Activity.createFromRoute(tabMirrorRoute);
+ activity.setOrigin('news.google.com');
+ activity.setContentTitle('Google News');
+ expect(activity.getRouteDescription())
+ .toBe('Casting tab (news.google.com)');
+ expect(activity.getRouteMediaStatus()).toBe('Google News');
+ expect(activity.getCastRemoteTitle()).toBe('Casting tab');
+ });
+
+ it('uses the tab origin and page title for presentation', () => {
+ let activity = mr.mirror.Activity.createFromRoute(presentationRoute);
+ activity.setOrigin('www.example.com');
+ activity.setContentTitle('Some Presentation');
+ expect(activity.getRouteDescription()).toBe('Casting www.example.com');
+ expect(activity.getRouteMediaStatus()).toBe('Some Presentation');
+ expect(activity.getCastRemoteTitle()).toBe('Casting site');
+ });
+
+ it('updates the remote title for incognito', () => {
+ let activity = mr.mirror.Activity.createFromRoute(tabMirrorRoute);
+ activity.setIncognito(true);
+ expect(activity.getCastRemoteTitle()).toBe('Casting active');
+ });
+
+ it('updates the activity for remoting', () => {
+ let activity = mr.mirror.Activity.createFromRoute(tabMirrorRoute);
+ activity.setOrigin('www.vimeo.com');
+ activity.setContentTitle('Vimeo');
+ activity.setType(mr.mirror.Activity.Type.MEDIA_REMOTING);
+ expect(activity.getRouteDescription())
+ .toBe('Casting media (www.vimeo.com)');
+ expect(activity.getRouteMediaStatus()).toBe('Vimeo');
+ expect(activity.getCastRemoteTitle()).toBe('Casting media');
+ });
+
+ it('updates the activity for mirroring local media', () => {
+ let activity = mr.mirror.Activity.createFromRoute(tabMirrorRoute);
+ activity.setOrigin('');
+ activity.setContentTitle('some_file.mp4');
+ activity.setType(mr.mirror.Activity.Type.MIRROR_FILE);
+ expect(activity.getRouteDescription()).toBe('Casting local content');
+ expect(activity.getRouteMediaStatus()).toBe('some_file.mp4');
+ expect(activity.getCastRemoteTitle()).toBe('Casting local content');
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_analytics.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_analytics.js
new file mode 100644
index 00000000000..20ea156371e
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_analytics.js
@@ -0,0 +1,74 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Defines UMA analytics specific to Mirroring.
+ */
+
+goog.provide('mr.MirrorAnalytics');
+goog.provide('mr.mirror.Error');
+
+goog.require('mr.Analytics');
+
+
+/**
+ * Contains all common analytics logic for Mirroring.
+ * @const {*}
+ */
+mr.MirrorAnalytics = {};
+
+
+/**
+ * Histogram name for mirroring start failure.
+ * @private @const {string}
+ */
+mr.MirrorAnalytics.CAPTURING_FAILURE_HISTOGRAM_ =
+ 'MediaRouter.Mirror.Capturing.Failure';
+
+
+/**
+ * Possible values for the start failure analytics.
+ * @enum {number}
+ */
+mr.MirrorAnalytics.CapturingFailure = {
+ CAPTURE_TAB_FAIL_EMPTY_STREAM: 0,
+ CAPTURE_DESKTOP_FAIL_ERROR_TIMEOUT: 1,
+ CAPTURE_TAB_TIMEOUT: 2,
+ CAPTURE_DESKTOP_FAIL_ERROR_USER_CANCEL: 3,
+ ANSWER_NOT_RECEIVED: 4,
+ CAPTURE_TAB_FAIL_ERROR_TIMEOUT: 5,
+ ICE_CONNECTION_CLOSED: 6,
+ TAB_FAIL: 7,
+ DESKTOP_FAIL: 8,
+ UNKNOWN: 9,
+};
+
+
+/**
+ * Records a mirroring start failure.
+ * @param {mr.MirrorAnalytics.CapturingFailure} reason The type of failure.
+ * @param {string} name The name of the histogram.
+ */
+mr.MirrorAnalytics.recordCapturingFailureWithName = function(reason, name) {
+ mr.Analytics.recordEnum(name, reason, mr.MirrorAnalytics.CapturingFailure);
+};
+
+
+/**
+ * Error thrown when initiating a mirroring session.
+ */
+mr.mirror.Error = class extends Error {
+ /**
+ * @param {string} message The error message.
+ * @param {mr.MirrorAnalytics.CapturingFailure=} reason The failure reason.
+ */
+ constructor(message, reason = undefined) {
+ super(message);
+ /** {!mr.MirrorAnalytics.CapturingFailure} The failure reason. */
+ this.reason =
+ (reason >= 0 && reason <= mr.MirrorAnalytics.CapturingFailure.UNKNOWN) ?
+ reason :
+ mr.MirrorAnalytics.CapturingFailure.UNKNOWN;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_analytics_test.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_analytics_test.js
new file mode 100644
index 00000000000..5bc5596eb43
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_analytics_test.js
@@ -0,0 +1,111 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly();
+goog.require('mr.MirrorAnalytics');
+
+describe('Tests Analytics', function() {
+ const metricName = 'MediaRouter.Fake.Start.Failure';
+
+ beforeEach(function() {
+ chrome.metricsPrivate = {
+ recordTime: jasmine.createSpy('recordTime'),
+ recordMediumTime: jasmine.createSpy('recordMediumTime'),
+ recordLongTime: jasmine.createSpy('recordLongTime'),
+ recordUserAction: jasmine.createSpy('recordUserAction'),
+ recordValue: jasmine.createSpy('recordValue'),
+ };
+ });
+
+ describe('Test Mirror Analytics', function() {
+ describe('Test recordCapturingFailure', function() {
+ const testConfig = {
+ 'metricName': metricName,
+ 'type': 'histogram-linear',
+ 'min': 1,
+ 'max': 10,
+ 'buckets': 11
+ };
+ it('Should record an empty stream error', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure.CAPTURE_TAB_FAIL_EMPTY_STREAM,
+ metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 0);
+ });
+ it('Should record a desktop timeout error', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_TIMEOUT,
+ metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 1);
+ });
+ it('Should record a tab timeout error', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure.CAPTURE_TAB_TIMEOUT,
+ metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 2);
+ });
+ it('Should record an user cancel error', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_USER_CANCEL,
+ metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 3);
+ });
+ it('Should record an answer not received error', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure.ANSWER_NOT_RECEIVED,
+ metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 4);
+ });
+ it('Should record a tab fail error', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure.CAPTURE_TAB_FAIL_ERROR_TIMEOUT,
+ metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 5);
+ });
+ it('Should record an ice connection closed error', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure.ICE_CONNECTION_CLOSED,
+ metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 6);
+ });
+ it('Should record a tab failure', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure.TAB_FAIL, metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 7);
+ });
+ it('Should record a desktop failure', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure.DESKTOP_FAIL, metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 8);
+ });
+ it('Should record an unknown error', function() {
+ mr.MirrorAnalytics.recordCapturingFailureWithName(
+ mr.MirrorAnalytics.CapturingFailure.UNKNOWN, metricName);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 9);
+ });
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_config.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_config.js
new file mode 100644
index 00000000000..fe079b2053b
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_config.js
@@ -0,0 +1,31 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Mirroring configs.
+ */
+
+goog.provide('mr.mirror.Config');
+goog.require('mr.PlatformUtils');
+
+
+/**
+ * True if TDLS is supported by the browser and platform.
+ * @const {boolean}
+ */
+mr.mirror.Config.isTDLSSupportedByPlatform = Boolean(
+ typeof chrome != 'undefined' && chrome.networkingPrivate &&
+ chrome.networkingPrivate.setWifiTDLSEnabledState &&
+ mr.PlatformUtils.getCurrentOS() == mr.PlatformUtils.OS.CHROMEOS);
+
+
+/**
+ * True if desktop audio capture is available.
+ * @const {boolean}
+ */
+mr.mirror.Config.isDesktopAudioCaptureAvailable =
+ // Audio capture is supported on ChromeOS with Chrome version
+ // 30.0.1584.0 and up, and Chrome 31 on Windows.
+ [mr.PlatformUtils.OS.CHROMEOS, mr.PlatformUtils.OS.WINDOWS].includes(
+ mr.PlatformUtils.getCurrentOS());
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service.js
new file mode 100644
index 00000000000..c1bc326c9cf
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service.js
@@ -0,0 +1,522 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Service supporting tab and screen mirroring.
+ *
+ * This object is a singleton that controls all capture MediaStreams and all
+ * instances of MirrorSession.
+ *
+
+ */
+
+goog.provide('mr.mirror.Service');
+
+goog.require('mr.Assertions');
+goog.require('mr.CancellablePromise');
+goog.require('mr.EventAnalytics');
+goog.require('mr.Issue');
+goog.require('mr.IssueSeverity');
+goog.require('mr.Logger');
+goog.require('mr.MediaSourceUtils');
+goog.require('mr.MirrorAnalytics');
+goog.require('mr.Module');
+goog.require('mr.mirror.CaptureParameters');
+goog.require('mr.mirror.CaptureSurfaceType');
+goog.require('mr.mirror.Error');
+goog.require('mr.mirror.Messages');
+goog.require('mr.mirror.MirrorMediaStream');
+
+mr.mirror.Service = class extends mr.Module {
+ /**
+ * @param {!mr.mirror.ServiceName} serviceName
+ * @param {mr.ProviderManagerMirrorServiceCallbacks=} mirrorServiceCallbacks
+ */
+ constructor(serviceName, mirrorServiceCallbacks) {
+ super();
+
+ /** @private @const {!mr.mirror.ServiceName} */
+ this.serviceName_ = serviceName;
+
+ /** @private {?mr.ProviderManagerMirrorServiceCallbacks} */
+ this.mirrorServiceCallbacks_ = mirrorServiceCallbacks || null;
+
+ /** @protected {?mr.mirror.Session} */
+ this.currentSession = null;
+
+ /** @private {?mr.mirror.MirrorMediaStream} */
+ this.currentMediaStream_ = null;
+
+ /** @protected @const */
+ this.logger = mr.Logger.getInstance('mr.mirror.Service.' + serviceName);
+
+ /** @private @const */
+ this.onTabUpdated_ = this.handleTabUpdate_.bind(this);
+
+ /** @private {boolean} */
+ this.initialized_ = false;
+ }
+
+ /**
+ * Initializes the service. Sets callbacks to provider manager. No-ops if
+ * already initialized.
+ * @param {!mr.ProviderManagerMirrorServiceCallbacks} mirrorServiceCallbacks
+ * Callbacks to provider manager.
+ */
+ initialize(mirrorServiceCallbacks) {
+ if (this.initialized_) {
+ return;
+ }
+ this.mirrorServiceCallbacks_ = mirrorServiceCallbacks;
+ this.initialized_ = true;
+ this.doInitialize();
+ }
+
+ /**
+ * Called during initialization to perform service-specific initialization.
+ * @protected
+ */
+ doInitialize() {}
+
+ /**
+ * @return {mr.mirror.ServiceName}
+ */
+ getName() {
+ return this.serviceName_;
+ }
+
+ /**
+ * @param {!mr.Route} route
+ * @param {string} sourceUrn
+ * @param {!mr.mirror.Settings} mirrorSettings
+ * @param {string=} opt_presentationId
+ * @param {(function(!mr.Route): !mr.CancellablePromise)=}
+ * opt_streamStartedCallback Callback to invoke after stream capture
+ * succeeded and before the mirror session is created. The callback
+ * may update the route.
+ * @return {!mr.CancellablePromise<!mr.Route>} A promise fulfilled
+ * when mirroring has started successfully.
+ */
+ startMirroring(
+ route, sourceUrn, mirrorSettings, opt_presentationId,
+ opt_streamStartedCallback) {
+ this.logger.info('Start mirroring on route ' + route.id);
+ if (!this.initialized_) {
+ return mr.CancellablePromise.reject(Error('Not initialized'));
+ }
+ const promise = new Promise((resolve, reject) => {
+ this.stopCurrentMirroring()
+ .then(() => {
+ const captureParams = mr.mirror.Service.createCaptureParameters_(
+ sourceUrn, mirrorSettings, opt_presentationId);
+ return new mr.mirror.MirrorMediaStream(captureParams).start();
+ })
+ .then(stream => {
+ if (this.currentMediaStream_) {
+ stream.stop();
+ throw new mr.mirror.Error('Cannot start multiple streams');
+ }
+ this.currentMediaStream_ = stream;
+ this.currentMediaStream_.setOnStreamEnded(this.cleanup_.bind(this));
+ if (opt_streamStartedCallback) {
+ // Yuck. Converting a CancellablePromise to a plain Promise
+ // prevents cancellation from propagating correctly.
+ return opt_streamStartedCallback(route).promise;
+ }
+ return route;
+ })
+ .then(updatedRoute => {
+ if (this.currentSession) {
+ throw new mr.mirror.Error('Cannot start multiple sessions');
+ }
+ if (!this.currentMediaStream_) {
+ throw new mr.mirror.Error(
+ 'Media stream ended before session could start.');
+ }
+ this.currentSession =
+ this.createMirrorSession(mirrorSettings, updatedRoute);
+ updatedRoute.mirrorInitData.activity =
+ this.currentSession.getActivity();
+ this.currentSession.setOnActivityUpdate(
+ this.mirrorServiceCallbacks_.handleMirrorActivityUpdate.bind(
+ this.mirrorServiceCallbacks_));
+ return this.currentSession.start(/** @type {!MediaStream} */ (
+ this.currentMediaStream_.getMediaStream()));
+ })
+ .then(() => {
+ if (mr.MediaSourceUtils.isTabMirrorSource(sourceUrn) &&
+ !chrome.tabs.onUpdated.hasListener(this.onTabUpdated_)) {
+ chrome.tabs.onUpdated.addListener(this.onTabUpdated_);
+ }
+ return this.postProcessMirroring_(route, sourceUrn, mirrorSettings);
+ })
+ .then(() => {
+ resolve(route);
+ })
+ .catch(err => {
+
+ this.onStartError_(/** @type {!Error} */ (err));
+ return this.cleanup_().then(() => {
+ reject(err);
+ });
+ });
+ });
+ return mr.CancellablePromise.forPromise(promise);
+ }
+
+ /**
+ * @param {!mr.Route} route
+ * @param {string} sourceUrn
+ * @param {!mr.mirror.Settings} mirrorSettings
+ * @param {string=} opt_presentationId
+ * @param {number=} opt_tabId
+ * @param {(function(!mr.Route): !mr.CancellablePromise<!mr.Route>)=}
+ * opt_streamStartedCallback Callback to invoke after stream capture
+ * succeeded and before the mirror session is created. The callback may
+ * update the route.
+ * @return {!mr.CancellablePromise<!mr.Route>} A promise fulfilled
+ * when mirroring has started successfully.
+ */
+ updateMirroring(
+ route, sourceUrn, mirrorSettings, opt_presentationId, opt_tabId,
+ opt_streamStartedCallback) {
+ this.logger.info('Update mirroring on route ' + route.id);
+ if (!this.initialized_) {
+ return mr.CancellablePromise.reject(Error('Not initialized'));
+ }
+ return mr.CancellablePromise.forPromise(this.doUpdateMirroring_(
+ route, sourceUrn, mirrorSettings, opt_presentationId,
+ opt_streamStartedCallback));
+ }
+
+ /**
+ * A helper method for updateMirroring.
+ * @param {!mr.Route} route
+ * @param {string} sourceUrn
+ * @param {!mr.mirror.Settings} mirrorSettings
+ * @param {string=} opt_presentationId
+ * @param {(function(!mr.Route): !mr.CancellablePromise<!mr.Route>)=}
+ * opt_streamStartedCallback Callback to invoke after stream capture
+ * succeeded and before the mirror session is created. The callback may
+ * update the route.
+ * @return {!Promise<!mr.Route>} A promise fulfilled
+ * when mirroring has started successfully.
+ * @private
+ */
+ doUpdateMirroring_(
+ route, sourceUrn, mirrorSettings, opt_presentationId,
+ opt_streamStartedCallback) {
+ if (!this.currentSession) {
+ return Promise.reject(new mr.mirror.Error(
+ 'No session to update streams on',
+ mr.MirrorAnalytics.CapturingFailure.TAB_FAIL));
+ }
+ if (!this.currentSession.supportsUpdateStream()) {
+ return Promise.reject(new mr.mirror.Error(
+ 'Session does not support updating stream',
+ mr.MirrorAnalytics.CapturingFailure.TAB_FAIL));
+ }
+
+ let streamSwapped = false;
+ return new Promise((resolve, reject) => {
+ const captureParams = mr.mirror.Service.createCaptureParameters_(
+ sourceUrn, mirrorSettings, opt_presentationId);
+ new mr.mirror.MirrorMediaStream(captureParams)
+ .start()
+ .then(stream => {
+ if (this.currentMediaStream_) {
+ this.currentMediaStream_.setOnStreamEnded(null);
+ this.currentMediaStream_.stop();
+ this.recordStreamEnded();
+ }
+ this.currentMediaStream_ = stream;
+ this.currentMediaStream_.setOnStreamEnded(this.cleanup_.bind(this));
+ streamSwapped = true;
+
+ if (opt_streamStartedCallback) {
+ return opt_streamStartedCallback(route).promise;
+ }
+ return route;
+ })
+ .then(_ => {
+ if (!this.currentSession) {
+ throw new mr.mirror.Error('Session ended while updating stream');
+ }
+ if (!this.currentMediaStream_) {
+ throw new mr.mirror.Error(
+ 'Media stream ended before session could be updated.');
+ }
+ return this.currentSession.updateStream(
+ /** @type {!MediaStream} */ (
+ this.currentMediaStream_.getMediaStream()));
+ })
+ .then(this.postProcessMirroring_.bind(
+ this, route, sourceUrn, mirrorSettings))
+ .then(() => resolve(route))
+ .catch(err => {
+ this.onStartError_(/** @type {!Error} */ (err));
+ if (streamSwapped) {
+ return this.cleanup_().then(() => {
+ reject(err);
+ });
+ } else {
+ reject(err);
+ }
+ });
+ });
+ }
+
+ /**
+ * @param {!mr.Route} route
+ * @param {string} sourceUrn
+ * @param {!mr.mirror.Settings} mirrorSettings
+ * @return {!Promise<void>} Resolves when done.
+ * @private
+ */
+ postProcessMirroring_(route, sourceUrn, mirrorSettings) {
+ return new Promise((resolve, reject) => {
+ if (!this.currentSession) {
+ reject(new mr.mirror.Error(
+ 'Session gone before executing post-startup steps',
+ mr.MirrorAnalytics.CapturingFailure.TAB_FAIL));
+ return;
+ }
+ if (mr.MediaSourceUtils.isTabMirrorSource(sourceUrn)) {
+ this.currentSession.setTabId(
+ /** @type {number} */ (route.mirrorInitData.tabId));
+ this.recordTabMirrorStartSuccess();
+ } else if (mr.MediaSourceUtils.isPresentationSource(sourceUrn)) {
+ this.currentSession.setTabId(
+ /** @type {number} */ (route.mirrorInitData.tabId));
+ this.recordOffscreenTabMirrorStartSuccess();
+ } else {
+ this.recordDesktopMirrorStartSuccess();
+ }
+ this.checkCaptureIssues_(
+ mirrorSettings,
+ /** @type {!mr.mirror.MirrorMediaStream} */
+ (this.currentMediaStream_));
+ this.currentSession.onActivityUpdated();
+ resolve();
+ });
+ }
+
+ /**
+ * @param {!Error|!mr.mirror.Error} error The error that occurred when
+ * starting mirroring.
+ * @private
+ */
+ onStartError_(error) {
+ error.reason = (error.reason != null) ?
+ error.reason :
+ mr.MirrorAnalytics.CapturingFailure.UNKNOWN;
+
+ this.logger.error(
+ `Failed to start mirroring: ${error.message}` +
+ `, reason = ${error.reason}: ${error.stack}`);
+ this.recordMirrorStartFailure(error.reason);
+ }
+
+ /**
+ * @return {!Promise<boolean>} Fulfilled with true if there was a session
+ * and it was stopped, and with false if there is no session to stop.
+ */
+ stopCurrentMirroring() {
+ if (!this.initialized_) {
+ return Promise.reject('Not initialized');
+ }
+ return this.cleanup_().then(hadSession => {
+ if (hadSession) this.recordMirrorSessionEnded();
+
+ return hadSession;
+ });
+ }
+
+ /**
+ * @return {!Promise<boolean>} Fulfilled with true if there was a session
+ * and it was stopped. This promise never rejects.
+ * @private
+ */
+ cleanup_() {
+ // No-op if the listener was already removed.
+ chrome.tabs.onUpdated.removeListener(this.onTabUpdated_);
+
+ const streamToCleanUp = this.currentMediaStream_;
+ this.currentMediaStream_ = null;
+ if (streamToCleanUp) {
+ // Clear the "on ended" callback to prevent recursive calls to this method
+ // while the session and MediaStream are being torn down (below).
+ streamToCleanUp.setOnStreamEnded(null);
+ }
+
+ const sessionToCleanUp = this.currentSession;
+ this.currentSession = null;
+
+ // Create a promise chain to execute the stopping of the session, invoke the
+ // before/after stop callbacks, and thereafter stop the MediaStream. The
+ // MediaStream is stopped after the session to avoid any extra churn in the
+ // session shutdown process. All of this is conditional on whether a session
+ // and/or MediaStream was ever started since cleanup_() is also used as an
+ // all-purpose failed-start recovery handler.
+ let cleanupPromise;
+ if (sessionToCleanUp) {
+ cleanupPromise =
+ this.beforeCleanUpSession(sessionToCleanUp)
+ .catch(
+ err =>
+ this.logger.error('Error in before-cleanup steps', err))
+ .then(() => sessionToCleanUp.stop())
+ .catch(err => this.logger.error('Error stopping session', err))
+ .then(() => {
+ this.mirrorServiceCallbacks_.onMirrorSessionEnded(
+ sessionToCleanUp.getRoute().id);
+ })
+ .catch(err => this.logger.error('Error in ended callbacks', err))
+ .then(() => true);
+ } else {
+ cleanupPromise = Promise.resolve(false);
+ }
+ if (streamToCleanUp) {
+ cleanupPromise = cleanupPromise.then(hadSession => {
+ streamToCleanUp.stop();
+ this.recordStreamEnded();
+ return hadSession;
+ });
+ }
+
+ return cleanupPromise;
+ }
+
+ /**
+ * @param {!mr.mirror.Session} session the session about to be cleaned up.
+ * @return {!Promise<void>} Fulfilled when session has been cleaned up.
+ * @protected
+ */
+ beforeCleanUpSession(session) {
+ return Promise.resolve();
+ }
+
+ /**
+ * Handles tab updated event.
+ *
+ * @param {number} tabId the ID of the tab.
+ * @param {!TabChangeInfo} changeInfo The changes to the state of the tab.
+ * @param {!Tab} tab The tab.
+ * @private
+ */
+ handleTabUpdate_(tabId, changeInfo, tab) {
+ mr.EventAnalytics.recordEvent(mr.EventAnalytics.Event.TABS_ON_UPDATED);
+ this.currentSession &&
+ this.currentSession.onTabUpdated(tabId, changeInfo, tab);
+ }
+
+ /**
+ * Creates a new mr.mirror.CaptureParameters from the given inputs.
+ *
+ * @param {string} sourceUrn
+ * @param {!mr.mirror.Settings} mirrorSettings
+ * @param {string=} opt_presentationId
+ * @return {!mr.mirror.CaptureParameters}
+ * @private
+ */
+ static createCaptureParameters_(
+ sourceUrn, mirrorSettings, opt_presentationId) {
+ if (mr.MediaSourceUtils.isTabMirrorSource(sourceUrn)) {
+ return new mr.mirror.CaptureParameters(
+ mr.mirror.CaptureSurfaceType.TAB, mirrorSettings);
+ } else if (mr.MediaSourceUtils.isDesktopMirrorSource(sourceUrn)) {
+ return new mr.mirror.CaptureParameters(
+ mr.mirror.CaptureSurfaceType.DESKTOP, mirrorSettings);
+ } else if (mr.MediaSourceUtils.isPresentationSource(sourceUrn)) {
+ if (!opt_presentationId) {
+ throw new mr.mirror.Error('Missing offscreen tab presentation id');
+ }
+ return new mr.mirror.CaptureParameters(
+ mr.mirror.CaptureSurfaceType.OFFSCREEN_TAB, mirrorSettings, sourceUrn,
+ /** @type {!string} */ (opt_presentationId));
+ } else {
+ throw new mr.mirror.Error(
+ 'Source URN does not suggest a known capture type.');
+ }
+ }
+
+ /**
+ * Checks for any capture issues after mirroring has started successfully.
+ *
+ * @param {!mr.mirror.Settings} settings The requested settings.
+ * @param {!mr.mirror.MirrorMediaStream} mediaStream The captured stream.
+ * @private
+ */
+ checkCaptureIssues_(settings, mediaStream) {
+ if (settings.shouldCaptureAudio && mediaStream.getMediaStream() &&
+ !mediaStream.getMediaStream().getAudioTracks().length) {
+ this.mirrorServiceCallbacks_.sendIssue(new mr.Issue(
+ mr.mirror.Messages.MSG_MR_MIRROR_NO_AUDIO_CAPTURED,
+ mr.IssueSeverity.NOTIFICATION));
+ }
+ }
+
+ /**
+ * @return {?mr.mirror.Session}
+ * @deprecated Use of this getter is dangerous, since mr.mirror.Service could
+ * be in the middle of a sequence of asynchronous steps to start up or
+ * shut down the current session.
+ */
+ getCurrentSession() {
+ return this.currentSession;
+ }
+
+ /**
+ * @param {!mr.mirror.Settings} mirrorSettings
+ * @param {!mr.Route} route
+ * @return {!mr.mirror.Session}
+ */
+ createMirrorSession(mirrorSettings, route) {}
+ /**
+ * Records that Tab Mirroring successfully started.
+ */
+ recordTabMirrorStartSuccess() {}
+ /**
+ * Records that Desktop Mirroring successfully started.
+ */
+ recordDesktopMirrorStartSuccess() {}
+ /**
+ * Records that Offscreen Tab (1UA mode) Mirroring successfully started.
+ */
+ recordOffscreenTabMirrorStartSuccess() {}
+ /**
+ * Records that the session has ended.
+ */
+ recordMirrorSessionEnded() {}
+ /**
+ * Records that a Mirroring session failed to start.
+ * @param {mr.MirrorAnalytics.CapturingFailure} reason
+ * The reason for the failure.
+ */
+ recordMirrorStartFailure(reason) {}
+ /**
+ * Records that a Mirroring stream ended.
+ */
+ recordStreamEnded() {}
+ /**
+ * Asynchronously uploads logs for the most recent mirroring session.
+ * @param {string} feedbackId ID of feedback requesting log upload.
+ * @return {!Promise<string|undefined>} Resolved with an identifier (e.g.,
+ * URL) of the log that will be uploaded (which will be undefined if there
+ * is no such identifier), or rejected if upload failed.
+ */
+ requestLogUpload(feedbackId) {
+ return mr.Assertions.rejectNotImplemented();
+ }
+ /**
+ * See documentation in interface_data/mojo.js.
+ * @param {string} routeId
+ * @param {!mojo.InterfaceRequest} controllerRequest
+ * @param {!mojo.MediaStatusObserverPtr} observer
+ * @return {!Promise<void>}
+ */
+ createMediaRouteController(routeId, controllerRequest, observer) {
+ return mr.Assertions.rejectNotImplemented();
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service_loader.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service_loader.js
new file mode 100644
index 00000000000..4d0eefeb307
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service_loader.js
@@ -0,0 +1,47 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.provide('mr.mirror.DefaultServiceLoader');
+goog.provide('mr.mirror.ServiceLoader');
+
+
+
+/**
+ * Loads a mirror.Service. Note that this loader does not need to handle event
+ * page suspending and waking up (the event page is running when there is a
+ * local mirroring route).
+ * @record
+ */
+mr.mirror.ServiceLoader = class {
+ /**
+ * Loads and returns the service.
+ * @return {!Promise<!mr.mirror.Service>}
+ */
+ loadService() {}
+};
+
+
+/**
+ * A lightweight implementation of ServiceLoader which just returns the instance
+ * provided to the constructor.
+ * @implements {mr.mirror.ServiceLoader}
+ */
+mr.mirror.DefaultServiceLoader = class {
+ /**
+ * @param {!mr.mirror.Service} serviceInstance
+ */
+ constructor(serviceInstance) {
+ /**
+ * @private {!mr.mirror.Service}
+ */
+ this.serviceInstance_ = serviceInstance;
+ }
+
+ /**
+ * @override
+ */
+ loadService() {
+ return Promise.resolve(this.serviceInstance_);
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service_name.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service_name.js
new file mode 100644
index 00000000000..0a588727732
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_service_name.js
@@ -0,0 +1,21 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Service name uniquely identifying each service.
+ */
+
+
+goog.provide('mr.mirror.ServiceName');
+
+
+/**
+ * @enum {string}
+ */
+mr.mirror.ServiceName = {
+ CAST_STREAMING: 'cast_streaming',
+ HANGOUTS: 'hangouts',
+ MEETINGS: 'meetings',
+ WEBRTC: 'webrtc'
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_session.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_session.js
new file mode 100644
index 00000000000..7c3a0a3442c
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_session.js
@@ -0,0 +1,179 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Interface to a mirroring session.
+
+ */
+
+goog.provide('mr.mirror.Session');
+
+goog.require('mr.TabUtils');
+goog.require('mr.mirror.Activity');
+
+
+/**
+ * Creates a new MirrorSession. Do not call, as this is an abstract base class.
+ */
+mr.mirror.Session = class {
+ /**
+ * @param {!mr.Route} route
+ * @param {?function(!mr.Route, !mr.mirror.Activity)=} onActivityUpdate
+ */
+ constructor(route, onActivityUpdate = null) {
+ /**
+ * The media route associated with this mirror session.
+ * @protected @const {!mr.Route}
+ */
+ this.route = route;
+
+ /**
+ * Called when this.activity_ has changed.
+ * @private {?function(!mr.Route, !mr.mirror.Activity)}
+ */
+ this.onActivityUpdate_ = onActivityUpdate;
+
+ /**
+ * The activity description for keeping UI strings up to date.
+ * @protected {!mr.mirror.Activity}
+ */
+ this.activity = mr.mirror.Activity.createFromRoute(route);
+
+ /** @type {?number} */
+ this.tabId = null;
+
+ /** @type {?Tab} */
+ this.tab = null;
+
+ /** @type {boolean} */
+ this.isRemoting = false;
+ }
+
+ /**
+ * Sets the callback for handling activity updates.
+ * @param {!function(!mr.Route, !mr.mirror.Activity)} onActivityUpdate
+ */
+ setOnActivityUpdate(onActivityUpdate) {
+ this.onActivityUpdate_ = onActivityUpdate;
+ }
+
+ /**
+ * @param {number} tabId The id of the tab being captured, if any.
+ * @return {!Promise<void>}
+ */
+ setTabId(tabId) {
+ if (this.tabId != tabId) {
+ this.tabId = tabId;
+ return mr.TabUtils.getTab(tabId).then((tab) => {
+ this.tab = tab;
+ this.onActivityUpdated();
+ });
+ } else {
+ return Promise.resolve();
+ }
+ }
+
+ /**
+ * Handles tab updated event.
+ *
+ * @param {number} tabId the ID of the tab.
+ * @param {!TabChangeInfo} changeInfo The changes to the state of the tab.
+ * @param {!Tab} tab The tab.
+ */
+ onTabUpdated(tabId, changeInfo, tab) {
+ if (tabId != this.tabId) return;
+ if (changeInfo.status == 'complete' ||
+ (!!changeInfo.favIconUrl && tab.status == 'complete')) {
+ this.tab = tab;
+ this.onActivityUpdated();
+ }
+ }
+
+ /**
+ * Called when the activity object for the mirror session needs to be updated.
+ * This happens when e.g. the tab being mirrored navigates, changes title, or
+ * switches in or out of media remoting.
+ */
+ onActivityUpdated() {
+ if (this.tab) {
+ this.activity.setContentTitle(this.tab.title);
+ this.activity.setIncognito(this.tab.incognito);
+ const url = new URL(this.tab.url);
+ if (url.protocol == 'file:') {
+ this.activity.setType(mr.mirror.Activity.Type.MIRROR_FILE);
+ this.activity.setOrigin(null);
+ } else {
+ this.activity.setType(
+ this.isRemoting ? mr.mirror.Activity.Type.MEDIA_REMOTING :
+ mr.mirror.Activity.Type.MIRROR_TAB);
+ if (url.protocol == 'https:') {
+ // OK to drop the protocol for secure origins.
+ this.activity.setOrigin(url.origin.substr(8));
+ } else {
+ this.activity.setOrigin(url.origin);
+ }
+ }
+ }
+ this.route.description = this.activity.getRouteDescription();
+ this.onActivityUpdate_ && this.onActivityUpdate_(this.route, this.activity);
+ this.sendActivityToSink();
+ }
+
+ /**
+ * @return {!mr.Route}
+ */
+ getRoute() {
+ return this.route;
+ }
+
+ /**
+ * @return {!mr.mirror.Activity}
+ */
+ getActivity() {
+ return this.activity;
+ }
+
+ /**
+ * Starts the mirroring session. The |mediaStream| must provide one audio
+ * and/or one video track. It is illegal to call start() more than once on the
+ * same session, even after stop() has been called. See updateStream(), or
+ * else create a new instance to re-start a session.
+ * @param {!MediaStream} mediaStream The media stream that has the audio
+ * and/or video track.
+ * @return {!Promise<mr.mirror.Session>} Fulfilled when the
+ * session has been created and transports have started for the streams.
+ */
+ start(mediaStream) {}
+
+ /**
+ * @return {boolean} Whether the session supports updating its media stream
+ * while it is active.
+ */
+ supportsUpdateStream() {
+ return false;
+ }
+
+ /**
+ * Updates the media stream that the session is using.
+ * @param {!MediaStream} mediaStream
+ * @return {!Promise}
+ */
+ updateStream(mediaStream) {}
+
+ /**
+ * Stops the mirroring session. The underlying streams and transports are
+ * stopped and destroyed. Sessions cannot be re-started. Instead, either use
+ * updateStream() or create a new instance to re-start a session.
+ * @return {!Promise<void>} Fulfilled with once the session has been stopped.
+ * The promise is rejected on error.
+ */
+ stop() {
+ return Promise.resolve();
+ }
+
+ /**
+ * Sends updated activity info directly to the sink.
+ */
+ sendActivityToSink() {}
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_session_test.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_session_test.js
new file mode 100644
index 00000000000..52f0536c11a
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_session_test.js
@@ -0,0 +1,118 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly();
+goog.require('mr.Route');
+goog.require('mr.TabUtils');
+goog.require('mr.mirror.Session');
+
+describe('Tests mr.mirror.Session', () => {
+ let mirrorRoute;
+ let session;
+ let onActivityUpdated;
+
+ function expectNormalSession(s) {
+ expect(s.tabId).toBe(47);
+ expect(s.tab).not.toBe(null);
+ expect(s.isRemoting).toBe(false);
+ expect(s.activity.getRouteDescription())
+ .toBe('Casting tab (news.google.com)');
+ expect(s.activity.getRouteMediaStatus()).toBe('Google News');
+ expect(s.activity.getCastRemoteTitle()).toBe('Casting tab');
+ }
+
+ function expectIncognitoSession(s) {
+ expect(s.tabId).toBe(47);
+ expect(s.tab).not.toBe(null);
+ expect(s.isRemoting).toBe(false);
+ expect(s.activity.getRouteDescription())
+ .toBe('Casting tab (news.google.com)');
+ expect(s.activity.getRouteMediaStatus()).toBe('Google News');
+ expect(s.activity.getCastRemoteTitle()).toBe('Casting active');
+ }
+
+ beforeEach(() => {
+ window['mojo'] = null; // Workaround to allow tests to run in Jasmine
+ // without mojo bindings
+ mirrorRoute = new mr.Route(
+ 'routeId', 'presentationId', 'sinkId',
+ 'urn:x-org.chromium.media:source:tab:47', true, null);
+ onActivityUpdated = jasmine.createSpy();
+ session = new mr.mirror.Session(mirrorRoute, onActivityUpdated);
+ session.tabId = 47;
+ spyOn(session, 'sendActivityToSink');
+ });
+
+ it('has default data for a tab mirroring route', () => {
+ expect(session.route).toBe(mirrorRoute);
+ expect(session.tabId).toBe(47);
+ expect(session.tab).toBe(null);
+ expect(session.isRemoting).toBe(false);
+ expect(session.activity.getRouteDescription()).toBe('Casting tab');
+ expect(session.activity.getRouteMediaStatus()).toBe('');
+ expect(session.activity.getCastRemoteTitle()).toBe('Casting tab');
+ });
+
+ describe('onTabUpdated', () => {
+ it('sets all fields with normal tab', () => {
+ session.onTabUpdated(47, {'status': 'complete'}, {
+ 'title': 'Google News',
+ 'url': 'https://news.google.com',
+ 'incognito': false
+ });
+ expect(session.sendActivityToSink).toHaveBeenCalled();
+ expect(onActivityUpdated).toHaveBeenCalled();
+ expectNormalSession(session);
+ });
+ it('sets some fields with incognito tab', () => {
+ session.onTabUpdated(47, {'status': 'complete'}, {
+ 'title': 'Google News',
+ 'url': 'https://news.google.com',
+ 'incognito': true
+ });
+ expect(session.sendActivityToSink).toHaveBeenCalled();
+ expect(onActivityUpdated).toHaveBeenCalled();
+ expectIncognitoSession(session);
+ });
+ it('sets some fields with OTR route', () => {
+ mirrorRoute.offTheRecord = true;
+ otrSession = new mr.mirror.Session(mirrorRoute, onActivityUpdated);
+ otrSession.tabId = 47;
+ spyOn(otrSession, 'sendActivityToSink');
+ otrSession.onTabUpdated(47, {'status': 'complete'}, {
+ 'title': 'Google News',
+ 'url': 'https://news.google.com',
+ 'incognito': true
+ });
+ expect(otrSession.sendActivityToSink).toHaveBeenCalled();
+ expect(onActivityUpdated).toHaveBeenCalled();
+ expectIncognitoSession(otrSession);
+ });
+ });
+
+ describe('setTabId', () => {
+ beforeEach((done) => {
+ spyOn(mr.TabUtils, 'getTab').and.returnValue(Promise.resolve({
+ 'title': 'CNN',
+ 'url': 'https://www.cnn.com',
+ 'incognito': false
+ }));
+ session.setTabId(48);
+ done();
+ });
+
+ it('updates the tab', (done) => {
+ expect(session.sendActivityToSink).toHaveBeenCalled();
+ expect(onActivityUpdated).toHaveBeenCalled();
+ expect(session.tabId).toBe(48);
+ expect(session.tab).not.toBe(null);
+ expect(session.isRemoting).toBe(false);
+ expect(session.activity.getRouteDescription())
+ .toBe('Casting tab (www.cnn.com)');
+ expect(session.activity.getRouteMediaStatus()).toBe('CNN');
+ expect(session.activity.getCastRemoteTitle()).toBe('Casting tab');
+ done();
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_settings.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_settings.js
new file mode 100644
index 00000000000..74499bbacbc
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_settings.js
@@ -0,0 +1,400 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Tab-mirroring settings, including video bitrate, video
+ * resolution, and audio bitrate.
+ *
+
+ */
+
+goog.provide('mr.mirror.Settings');
+goog.provide('mr.mirror.VideoCodec');
+
+goog.require('mr.Logger');
+goog.require('mr.PlatformUtils');
+
+
+/**
+ * @enum {string}
+ */
+mr.mirror.VideoCodec = {
+ VP8: 'VP8',
+ // This is the internal codename for hardware-encoded H264. for V1 mirroring.
+ CAST1: 'CAST1',
+ H264: 'H264',
+
+ // This is a fake codec for retransmissions used in WebRTC.
+ RTX: 'rtx'
+};
+
+
+
+/**
+ * Settings that affect capture and transport (via Cast Streaming or WebRTC).
+ * Generally, these should all be left unchanged from their defaults.
+ * Overriding them is only meant for development or user experiments.
+ *
+ * WARNING: If making any changes to defaults here, it is on *you* to confirm
+ * that all downstream consumers of these settings will behave correctly with
+ * the new values. If not, see usage notes below for per-sink adjustments.
+ *
+ * Usage: Generally, this class should be instantiated by mr.Provider
+ * implementations only. The provider creates a new instance, and then must
+ * adjust any properties based on both the sender's and sink's capabilities.
+ * It should also, as a final step, freeze the settings object to prevent any
+ * downstream code from making changes. Example provider code:
+ *
+ * getMirrorSettings(sinkId) {
+ * // Constrain default settings to a sink that is only capable of standard
+ * // definition (lower resolution and no high frame rate support).
+ * const settings = new mr.mirror.Settings();
+ * settings.maxWidth = Math.min(settings.maxWidth, 640);
+ * settings.maxHeight = Math.min(settings.maxWidth, 360);
+ * settings.maxFrameRate = Math.min(settings.maxFrameRate, 30);
+ *
+ * // Override: Some sinks might require the sender to do the letterboxing.
+ * settings.senderSideLetterboxing =
+ * !this.canSinkHandleLetterboxing_(sinkId);
+ *
+ * settings.makeFinalAdjustmentsAndFreeze();
+ * return settings;
+ * }
+ *
+ */
+mr.mirror.Settings = class {
+ constructor() {
+ /**
+ * Maximum video width in pixels.
+ *
+ * @export {number}
+ */
+ this.maxWidth = 1920;
+
+ /**
+ * Maximum video height in pixels.
+ *
+ * @export {number}
+ */
+ this.maxHeight = 1080;
+
+ /**
+ * Minimum video width in pixels.
+ *
+ * @export {number}
+ */
+ this.minWidth = 180;
+
+ /**
+ * Minimum video height in pixels.
+ *
+ * @export {number}
+ */
+ this.minHeight = 180;
+
+ /**
+ * Whether the screen capture must handle letterboxing/pillarboxing. If
+ * false (more desired), the receiver will handle it. When setting this to
+ * true, please see comments for getMinDimensionsToMatchAspectRatio().
+ *
+ * @export {boolean}
+ */
+ this.senderSideLetterboxing = false;
+
+ /**
+ * The minimum frame rate for captures. Well-behaved clients can handle a
+ * minimum frame rate of zero, which prevents wasting system resources
+ * sender-side. Unfortunately, not all clients are well-behaved...
+ *
+ * @export {number}
+ */
+ this.minFrameRate = 0;
+
+ /**
+ * The maximum frame rate for captures.
+ *
+ * @export {number}
+ */
+ this.maxFrameRate = 30;
+
+ /**
+ * Minimum video bitrate in kbps.
+ *
+ * @export {number}
+ */
+ this.minVideoBitrate = 300;
+
+ /**
+ * Maximum video bitrate in kbps.
+ *
+ * @export {number}
+ */
+ this.maxVideoBitrate = 5000;
+
+ /**
+ * Target audio bitrate in kbps (0 means automatic).
+ *
+ * @export {number}
+ */
+ this.audioBitrate = 0;
+
+ /**
+ * Maximum end-to-end latency (in milliseconds).
+ *
+ * @export {number}
+ */
+ this.maxLatencyMillis = 800;
+
+ /**
+ * Minimum end-to-end latency (in milliseconds). This allows cast streaming
+ * to adaptively lower latency in interactive streaming scenarios.
+ * This setting currently applies to cast streaming only.
+ *
+
+ *
+ * @export {number}
+ */
+ this.minLatencyMillis = 400;
+
+ /**
+ * Starting end-to-end latency for animated content (in milliseconds).
+ *
+ * @export {number}
+ */
+ this.animatedLatencyMillis = 400;
+
+ /**
+ * Enable DSCP?
+ * This setting currently applies to cast streaming only.
+ *
+ * @export {boolean}
+ */
+ this.dscpEnabled =
+ [
+ mr.PlatformUtils.OS.MAC, mr.PlatformUtils.OS.LINUX,
+ mr.PlatformUtils.OS.CHROMEOS
+ ].includes(mr.PlatformUtils.getCurrentOS()) ||
+ mr.PlatformUtils.isWindows8OrNewer();
+
+ /**
+ * Whether to enable network transport logging.
+ *
+ * @export {boolean}
+ */
+ this.enableLogging = true;
+
+ /**
+ * Whether an attempt should be made to use TDLS.
+ * This setting currently applies to cast streaming only.
+ *
+ * @export {boolean}
+ */
+ this.useTdls = false;
+
+
+ /**
+ * Whether video should be captured. This could be influenced by the
+ * application and/or the sink's capabilities.
+ * @export {boolean}
+ */
+ this.shouldCaptureVideo = true;
+
+ /**
+ * Whether audio should be captured. This could be influenced by the
+ * application and/or the sink's capabilities.
+ * @export {boolean}
+ */
+ this.shouldCaptureAudio = true;
+
+ // For development, debugging, or integration testing use only!
+ const overrides = window.localStorage ?
+ window.localStorage.getItem(mr.mirror.Settings.OverridesKey) :
+ null;
+ if (overrides) {
+ try {
+ const parsedOverrides = JSON.parse(String(overrides));
+ if (parsedOverrides instanceof Object) {
+ this.update_(/** @type {!Object} */ (parsedOverrides));
+ mr.Logger.getInstance('mr.mirror.Settings')
+ .warning(
+ () => 'Initial mr.mirror.Settings overridden to: ' +
+ this.toJsonString());
+ } else {
+ throw Error(
+ `localStorage[${mr.mirror.Settings.OverridesKey}] ` +
+ `does not parse as an Object: ${overrides}`);
+ }
+ } catch (exception) {
+ mr.Logger.getInstance('mr.mirror.Settings')
+ .error(
+ mr.mirror.Settings.OverridesKey + ' must be of the form ' +
+ '\'{"maxWidth":640, "maxHeight":360}\'.',
+ exception);
+ // Prevent mirroring from starting if overrides are present and not
+ // parseable.
+ throw new Error('Overrides not parseable. See ERROR log for details.');
+ }
+ }
+ }
+
+ /**
+ * @return {!mr.mirror.Settings}
+ */
+ clone() {
+ const settings = new mr.mirror.Settings();
+ settings.update_(this);
+ return settings;
+ }
+
+ /**
+ * Returns the properties of this Settings object as a JSON-formatted string.
+ * @return {!string}
+ */
+ toJsonString() {
+ return JSON.stringify(this, (key, value) => {
+ // Only public fields are included in the stringified output.
+ if (key.length == 0 || !key.endsWith('_')) {
+ return value;
+ }
+ return undefined;
+ });
+ }
+
+ /**
+ * Update this object to have the same settings as another object.
+ * @param {!Object} settings The properties to apply, which may be any subset
+ * of all the public fields of this class.
+ * @private
+ */
+ update_(settings) {
+ // Override Closure-compiler "access on a struct" error.
+ const self = /** @type {!Object} */ (this);
+ for (const key of Object.keys(settings)) {
+ if (key.endsWith('_') || (typeof settings[key] !== typeof self[key])) {
+ continue;
+ }
+ self[key] = settings[key];
+ }
+ }
+
+ /**
+ * Make mandatory system-wide bounds adjustments and then freeze this Settings
+ * object. See example usage in class-level comments.
+ */
+ makeFinalAdjustmentsAndFreeze() {
+ this.clampMaxDimensionsToScreenSize_();
+ Object.freeze(this);
+ }
+
+ /**
+ * Adjusts the maxWidth/maxHeight to within the size of the user's screen, and
+ * rounds down to a standard 16:9 resolution (i.e., width is 0 modulo 160 and
+ * height is 0 modulo 90). This prevents performance problems due to:
+ * 1. The pre-capture fullscreen size being something way larger than the
+ * system was designed for (e.g., 1080p on a Daisy Chromebook).
+ * 2. The receiver dealing with scaling from an oddball resolution to a
+ * standard resolution (e.g., 1366x768 --> 1280x720).
+ * @private
+ */
+ clampMaxDimensionsToScreenSize_() {
+ const widthStep = 160;
+ const heightStep = 90;
+ const screenWidth = mr.mirror.Settings.getScreenWidth();
+ const screenHeight = mr.mirror.Settings.getScreenHeight();
+ const x = this.maxWidth * screenHeight;
+ const y = this.maxHeight * screenWidth;
+ let clampedWidth = 0;
+ let clampedHeight = 0;
+ if (y < x) {
+ clampedWidth = Math.min(this.maxWidth, screenWidth);
+ clampedWidth = clampedWidth - (clampedWidth % widthStep);
+ clampedHeight = clampedWidth * heightStep / widthStep;
+ } else {
+ clampedHeight = Math.min(this.maxHeight, screenHeight);
+ clampedHeight = clampedHeight - (clampedHeight % heightStep);
+ clampedWidth = clampedHeight * widthStep / heightStep;
+ }
+ if (clampedWidth < Math.max(widthStep, this.minWidth) ||
+ clampedHeight < Math.max(heightStep, this.minHeight)) {
+ clampedWidth = Math.max(widthStep, this.minWidth);
+ clampedHeight = Math.max(heightStep, this.minHeight);
+ }
+
+ this.maxWidth = clampedWidth;
+ this.maxHeight = clampedHeight;
+ }
+
+ /**
+ * Returns alternate |minWidth| and |minHeight| values that match the aspect
+ * ratio of |maxWidth| and |maxHeight| to the nearest integer. Some of the
+ * capture APIs will then interpret the matching aspect ratios to mean that
+ * the sender should letterbox/pillarbox the video, rather than allowing the
+ * receiver to handle it. This method does NOT modify any properties of this
+ * Settings object.
+ * @return {!{width: number, height: number}} New minimum width/height values,
+ * as described.
+ */
+ getMinDimensionsToMatchAspectRatio() {
+ if (this.minAndMaxAspectRatiosAreSimilar()) {
+ return {width: this.minWidth, height: this.minHeight};
+ }
+
+ let a = this.maxWidth;
+ let b = this.maxHeight;
+ while (b != 0) {
+ const remainder = a % b;
+ a = b;
+ b = remainder;
+ }
+ // Note: |a| now contains the greatest common divisor.
+ let width = this.maxWidth / a;
+ let height = this.maxHeight / a;
+ if (width < this.minWidth || height < this.minHeight) {
+ // Increase to respect the current min width/Height setting.
+ const upFactor = Math.max(this.minWidth / width, this.minHeight / height);
+ width *= upFactor;
+ height *= upFactor;
+ // ...and make sure the increase does not now exceed the max width/height.
+ if (width > this.maxWidth || height > this.maxHeight) {
+ width = this.maxWidth;
+ height = this.maxHeight;
+ }
+ }
+ width = Math.round(width);
+ height = Math.round(height);
+ return {width, height};
+ }
+
+ /**
+ * @return {boolean} Returns true if the aspect ratios of the min and max
+ * dimensions are within one part in one hundred of each other.
+ */
+ minAndMaxAspectRatiosAreSimilar() {
+ if (this.minHeight == 0 || this.maxHeight == 0) {
+ return false;
+ }
+ // Source: content/renderer/media/media_stream_video_capturer_source.cc (in
+ // Chromium project, circa 2016).
+ const ratioOfMinSize = Math.floor(100.0 * this.minWidth / this.minHeight);
+ const ratioOfMaxSize = Math.floor(100.0 * this.maxWidth / this.maxHeight);
+ return ratioOfMinSize == ratioOfMaxSize;
+ }
+
+ /** @return {number} */
+ static getScreenWidth() {
+ return screen.width;
+ }
+
+ /** @return {number} */
+ static getScreenHeight() {
+ return screen.height;
+ }
+};
+
+
+/**
+ * The key for retrieving settings overrides from localStorage.
+ * @type {string}
+ */
+mr.mirror.Settings.OverridesKey = 'mr.mirror.Settings.Overrides';
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_settings_test.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_settings_test.js
new file mode 100644
index 00000000000..066e92f3198
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/mirror_settings_test.js
@@ -0,0 +1,123 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.require('mr.mirror.Settings');
+
+describe('Tests Mirror Settings', function() {
+ it('produces JSON strings containing public fields', function() {
+ // Emit Settings as a JSON-format string.
+ const settings = new mr.mirror.Settings();
+ const jsonString = settings.toJsonString();
+
+ // The string should be parseable as JSON and contain only public fields.
+ const parsed = JSON.parse(jsonString);
+ expect(typeof parsed['maxWidth']).toBe('number');
+ expect(typeof parsed['enableLogging']).toBe('boolean');
+ expect(Object.keys(parsed).sort())
+ .toEqual(Object.keys(settings).filter(x => !x.endsWith('_')).sort());
+ });
+
+ it('only updates public fields from localStorage overrides', function() {
+ const settings = new mr.mirror.Settings();
+
+ // Normal case: A public field is updated with a new value of the same type.
+ const originalMaxWidth = settings.maxWidth;
+ settings.update_({'maxWidth': originalMaxWidth / 2});
+ expect(settings.maxWidth).toBe(originalMaxWidth / 2);
+
+ // Bad case: A public field being set to a value of a different type.
+ const jsonBefore = settings.toJsonString();
+ settings.update_({'maxWidth': true});
+ expect(settings.toJsonString()).toEqual(jsonBefore);
+
+ // Bad case: A non-existant field.
+ settings.update_({'fooey': true});
+ expect(settings.toJsonString()).toEqual(jsonBefore);
+
+ // Bad case: Attempt to set private field.
+ const loggerBefore = settings.logger_;
+ const injectionAttackFunc = function() {
+ return 'MUAHAHAHAHA!';
+ };
+ settings.update_({'logger_': injectionAttackFunc});
+ expect(settings.logger_).not.toBe(injectionAttackFunc);
+ expect(settings.logger_).toBe(loggerBefore);
+ expect(settings.toJsonString()).toEqual(jsonBefore);
+ });
+
+ it('clamps max dimensions to 1920x1080 screen size', function() {
+ spyOn(mr.mirror.Settings, 'getScreenWidth').and.returnValue(1920);
+ spyOn(mr.mirror.Settings, 'getScreenHeight').and.returnValue(1080);
+ const settings = new mr.mirror.Settings();
+ settings.makeFinalAdjustmentsAndFreeze();
+ expect(settings.maxWidth).toBe(1920);
+ expect(settings.maxHeight).toBe(1080);
+ });
+
+ it('clamps max dimensions to 1366x768 screen size', function() {
+ spyOn(mr.mirror.Settings, 'getScreenWidth').and.returnValue(1366);
+ spyOn(mr.mirror.Settings, 'getScreenHeight').and.returnValue(768);
+ const settings = new mr.mirror.Settings();
+ settings.makeFinalAdjustmentsAndFreeze();
+ expect(settings.maxWidth).toBe(1280);
+ expect(settings.maxHeight).toBe(720);
+ });
+
+ it('returns min size matching aspect ratio of max size', function() {
+ const settings = new mr.mirror.Settings();
+ settings.maxWidth = 1920;
+ settings.maxHeight = 1080;
+ settings.minWidth = 320;
+ settings.minHeight = 180;
+ expect(settings.minAndMaxAspectRatiosAreSimilar()).toBe(true);
+ expect(settings.getMinDimensionsToMatchAspectRatio())
+ .toEqual({width: 320, height: 180});
+
+ settings.minWidth = 0;
+ settings.minHeight = 0;
+ expect(settings.minAndMaxAspectRatiosAreSimilar()).toBe(false);
+ expect(settings.getMinDimensionsToMatchAspectRatio())
+ .toEqual({width: 16, height: 9});
+ settings.minWidth = 1;
+ settings.minHeight = 1;
+ expect(settings.minAndMaxAspectRatiosAreSimilar()).toBe(false);
+ expect(settings.getMinDimensionsToMatchAspectRatio())
+ .toEqual({width: 16, height: 9});
+ settings.minWidth = 16;
+ settings.minHeight = 9;
+ expect(settings.minAndMaxAspectRatiosAreSimilar()).toBe(true);
+
+ settings.minWidth = 320;
+ settings.minHeight = 240;
+ expect(settings.minAndMaxAspectRatiosAreSimilar()).toBe(false);
+ expect(settings.getMinDimensionsToMatchAspectRatio())
+ .toEqual({width: 427, height: 240});
+
+ settings.maxWidth = 1000;
+ settings.maxHeight = 1000;
+ settings.minWidth = 48;
+ settings.minHeight = 27;
+ expect(settings.minAndMaxAspectRatiosAreSimilar()).toBe(false);
+ expect(settings.getMinDimensionsToMatchAspectRatio())
+ .toEqual({width: 48, height: 48});
+
+ settings.maxWidth = 1001;
+ settings.maxHeight = 999;
+ settings.minWidth = 0;
+ settings.minHeight = 0;
+ expect(settings.getMinDimensionsToMatchAspectRatio())
+ .toEqual({width: 1001, height: 999});
+ });
+
+ it('is frozen after final adjustments are made', function() {
+ 'use strict';
+ const settings = new mr.mirror.Settings();
+ expect(Object.isFrozen(settings)).toBe(false);
+ settings.makeFinalAdjustmentsAndFreeze();
+ expect(Object.isFrozen(settings)).toBe(true);
+ expect(() => {
+ settings.maxWidth = 999;
+ }).toThrow();
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/capture_parameters.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/capture_parameters.js
new file mode 100644
index 00000000000..dbf1d77fa8c
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/capture_parameters.js
@@ -0,0 +1,208 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Stream capture parameters.
+ */
+
+goog.provide('mr.mirror.CaptureParameters');
+goog.provide('mr.mirror.CaptureSurfaceType');
+
+goog.require('mr.Assertions');
+goog.require('mr.mirror.Config');
+
+
+/**
+ * Parameters that configure and control local media capture.
+ */
+mr.mirror.CaptureParameters = class {
+ /**
+ * @param {!mr.mirror.CaptureSurfaceType} captureSurface
+ * @param {!mr.mirror.Settings} mirrorSettings
+ * @param {string=} opt_offscreenTabUrl
+ * @param {string=} opt_presentationId
+ */
+ constructor(
+ captureSurface, mirrorSettings, opt_offscreenTabUrl, opt_presentationId) {
+ if (opt_offscreenTabUrl) {
+ mr.Assertions.assert(
+ captureSurface == mr.mirror.CaptureSurfaceType.OFFSCREEN_TAB);
+ mr.Assertions.assert(opt_presentationId);
+ }
+
+ /** @type {!mr.mirror.CaptureSurfaceType} */
+ this.captureSurface = captureSurface;
+
+ /** @type {!mr.mirror.Settings} */
+ this.mirrorSettings = mirrorSettings;
+
+ /** @type {?string} */
+ this.offscreenTabUrl = opt_offscreenTabUrl || null;
+
+ /** @type {?string} */
+ this.presentationId = opt_presentationId || null;
+ }
+
+ /**
+ * @return {boolean} True if this is for tab capture
+ */
+ isTab() {
+ return this.captureSurface == mr.mirror.CaptureSurfaceType.TAB;
+ }
+
+ /**
+ * @return {boolean} True if this is for desktop capture
+ */
+ isDesktop() {
+ return this.captureSurface == mr.mirror.CaptureSurfaceType.DESKTOP;
+ }
+
+ /**
+ * @return {boolean} True if this is for offscreen tab capture
+ */
+ isOffscreenTab() {
+ return this.captureSurface == mr.mirror.CaptureSurfaceType.OFFSCREEN_TAB;
+ }
+
+ /**
+ * @param {string=} sourceId The source ID of the desktop media.
+ * @return {!MediaStreamConstraints|!Object} Media constraints for use with
+ * platform capture APIs.
+ */
+ toMediaConstraints(sourceId = undefined) {
+ if (this.isTab()) {
+ return this.toTabMediaConstraints_();
+ } else if (this.isOffscreenTab()) {
+ return this.toOffscreenTabMediaConstraints_();
+ } else {
+ return this.toDesktopMediaConstraints_(sourceId);
+ }
+ }
+
+ /**
+ * @return {!MediaConstraints} Media constraints for use with platform
+ * capture APIs.
+ * @private
+ */
+ toTabMediaConstraints_() {
+
+ //
+ // Also the tabCapture API shape doesn't match getUserMedia, wich combines
+ // audio and video constraints into a single dictionary.
+ //
+ // Both of these issues make it hard to type toMediaConstraints() properly.
+ const constraints = /** @type {!MediaConstraints} */
+ ({
+ 'audio': this.mirrorSettings.shouldCaptureAudio,
+ 'video': this.mirrorSettings.shouldCaptureVideo
+ });
+
+ if (this.mirrorSettings.shouldCaptureVideo) {
+ constraints['videoConstraints'] = {
+ 'mandatory': {'enableAutoThrottling': true}
+ };
+ this.setCommonVideoConstraints_(
+ constraints['videoConstraints']['mandatory']);
+ }
+ return constraints;
+ }
+
+ /**
+ * @return {!MediaConstraints} Media constraints for use with offscreen
+ * capture APIs.
+ * @private
+ */
+ toOffscreenTabMediaConstraints_() {
+ mr.Assertions.assert(
+ this.presentationId, 'Missing offscreen capture presentation id');
+ const constraints = this.toTabMediaConstraints_();
+ constraints['presentationId'] = this.presentationId;
+ return constraints;
+ };
+
+ /**
+ * @param {string=} sourceId The source id of the desktop media. Only required
+ * if capturing video.
+ * @return {!MediaStreamConstraints} Media constraints for use with platform
+ * capture APIs.
+ * @private
+ */
+ toDesktopMediaConstraints_(sourceId = undefined) {
+ const constraints = /** @type {!MediaStreamConstraints} */
+ ({'audio': false, 'video': false});
+
+ if (this.mirrorSettings.shouldCaptureVideo) {
+ mr.Assertions.assert(sourceId);
+ constraints['video'] = {
+ 'mandatory': {
+ 'chromeMediaSource': 'desktop',
+ 'chromeMediaSourceId': sourceId,
+ }
+ };
+ this.setCommonVideoConstraints_(constraints['video']['mandatory']);
+
+ if (mr.mirror.Config.isDesktopAudioCaptureAvailable &&
+ this.mirrorSettings.shouldCaptureAudio) {
+ // NOTE(mfoltz): Nothing in Chrome seems to consume the audio sourceId;
+ // however, continuing to pass it in case something changes in the
+ // future.
+ constraints['audio'] = {
+ 'mandatory': {
+ 'chromeMediaSource': 'system',
+ 'chromeMediaSourceId': sourceId,
+ }
+ };
+ }
+ } else if (this.mirrorSettings.shouldCaptureAudio) {
+ mr.Assertions.assert(!sourceId);
+ mr.Assertions.assert(mr.mirror.Config.isDesktopAudioCaptureAvailable);
+ constraints['audio'] = {
+ 'mandatory': {
+ 'chromeMediaSource': 'system',
+ }
+ };
+ }
+
+ return constraints;
+ }
+
+ /**
+ * Helper to populate common video constraints fields for both capture APIs.
+ * @param {!MediaStreamConstraints|!MediaConstraints} constraints
+ * @private
+ */
+ setCommonVideoConstraints_(constraints) {
+ // If sender-side letterboxing is being used, pass altered min dimensions to
+ // the capture API which match the aspect ratio of the max dimensions. The
+ // capture API will interpret this to mean that it must perform
+ // letterboxing/pillarboxing.
+ let minWidth = this.mirrorSettings.minWidth;
+ let minHeight = this.mirrorSettings.minHeight;
+ if (this.mirrorSettings.senderSideLetterboxing) {
+ const altMin = this.mirrorSettings.getMinDimensionsToMatchAspectRatio();
+ minWidth = altMin.width;
+ minHeight = altMin.height;
+ }
+
+ Object.assign(constraints, {
+ 'minWidth': minWidth,
+ 'minHeight': minHeight,
+ 'maxWidth': this.mirrorSettings.maxWidth,
+ 'maxHeight': this.mirrorSettings.maxHeight,
+ 'minFrameRate': this.mirrorSettings.minFrameRate,
+ 'maxFrameRate': this.mirrorSettings.maxFrameRate,
+ });
+ }
+};
+
+
+/**
+ * Possible capture modes.
+ * @enum {string}
+ */
+mr.mirror.CaptureSurfaceType = {
+ TAB: 'tab',
+ DESKTOP: 'desktop',
+ OFFSCREEN_TAB: 'offscreen_tab'
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/mirror_media_stream.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/mirror_media_stream.js
new file mode 100644
index 00000000000..370b91fcb15
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/mirror_media_stream.js
@@ -0,0 +1,340 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Media stream for use with tab and desktop capture.
+ *
+ * A media stream has a video and/or audio track. Each capture source (i.e.,
+ * tab) should use its own MediaStream object and call start() to initiate
+ * capture.
+ */
+
+goog.provide('mr.mirror.MirrorMediaStream');
+
+goog.require('mr.Assertions');
+goog.require('mr.Logger');
+goog.require('mr.MirrorAnalytics');
+goog.require('mr.PlatformUtils');
+goog.require('mr.mirror.Config');
+goog.require('mr.mirror.Error');
+
+/**
+ * Constructs a new MediaStream that will capture media according to
+ * captureParams.
+ */
+mr.mirror.MirrorMediaStream = class {
+ /**
+ * @param {!mr.mirror.CaptureParameters} captureParams
+ */
+ constructor(captureParams) {
+ /** @private {!mr.mirror.CaptureParameters} */
+ this.captureParams_ = captureParams;
+
+ /** @private {?function()} */
+ this.onStreamEnded_ = null;
+
+ /** @private {?MediaStream} */
+ this.mediaStream_ = null;
+
+ /** @private {mr.Logger} */
+ this.logger_ = mr.Logger.getInstance('mr.mirror.MirrorMediaStream');
+ }
+
+ /**
+ * @param {?function()} onStreamEnded Invoked when the stream fires an
+ * onended event.
+ */
+ setOnStreamEnded(onStreamEnded) {
+ this.onStreamEnded_ = onStreamEnded;
+ }
+
+ /**
+ * @return {!mr.mirror.CaptureParameters}
+ */
+ getCaptureParams() {
+ return this.captureParams_;
+ }
+
+ /**
+ * @return {?MediaStream}
+ */
+ getMediaStream() {
+ return this.mediaStream_;
+ }
+
+ /**
+ * Starts capturing media and sets audioTrack and videoTrack.
+ * @return {!Promise<!mr.mirror.MirrorMediaStream>} Fulfilled when capture
+ * has started.
+ */
+ start() {
+ if (this.captureParams_.isTab()) {
+ return this.startTabCapturing_();
+ } else if (this.captureParams_.isOffscreenTab()) {
+ return this.startOffscreenTabCapturing_();
+ } else {
+ return this.startDesktopCapturing_();
+ }
+ }
+
+ /**
+ * @return {!Promise<!mr.mirror.MirrorMediaStream>} Fulfilled when capture
+ * has started.
+ * @private
+ */
+ startTabCapturing_() {
+ const constraints = this.captureParams_.toMediaConstraints();
+ this.logger_.info(
+ 'Starting tab capture with constraints ' + JSON.stringify(constraints));
+
+ return new Promise((resolve, reject) => {
+ // Note: There's a subtle reason to NOT pass |resolve| as the
+ // callback function to the chrome.tabCapture.capture() call here.
+ // Because this is an extension API, when an error occurs, the value
+ // in chrome.runtime.lastError is only available during the call to
+ // the callback function. However, the Promise.then() method runs
+ // its function at a later time, when chrome.runtime.lastError is no
+ // longer set.
+ chrome.tabCapture.capture(constraints, stream => {
+ if (stream) {
+ this.setStream_(stream);
+ resolve(this);
+ } else {
+ reject(this.createTabCaptureError_());
+ }
+ });
+
+ // Set a timer to reject the promise after a delay.
+ //
+
+ window.setTimeout(() => {
+ // In normal usage, this will be a no-op because this promise will
+ // already have been resolved by the call to
+ // chrome.tabCapture.capture above.
+ reject(new mr.mirror.Error(
+ 'chrome.tabCapture.capture failed to call its callback',
+ mr.MirrorAnalytics.CapturingFailure.CAPTURE_TAB_TIMEOUT));
+ }, mr.mirror.MirrorMediaStream.TAB_CAPTURE_TIMEOUT_);
+ });
+ }
+
+ /**
+ * @param {?Event} event The ended event.
+ * @private
+ */
+ handleTrackEnded_(event) {
+ if (event) {
+ this.logger_.info(
+ () => 'Track ' + JSON.stringify(event.target) + ' ended');
+ }
+ this.stop();
+ }
+
+ /**
+ * Returns an Error corresponding to a chrome.tabCapture error.
+ * @return {!mr.mirror.Error} The corresponding error.
+ * @private
+ */
+ createTabCaptureError_() {
+ // As of Chrome 51, when |stream| is null, chrome.runtime.lastError.message
+ // should always be set to a non-empty string. If it is not, fall back to
+ // the default error message so everyone can yell at miu@.
+ if (chrome.runtime.lastError && chrome.runtime.lastError.message) {
+ return new mr.mirror.Error(
+ chrome.runtime.lastError.message,
+ mr.MirrorAnalytics.CapturingFailure.TAB_FAIL);
+ } else {
+ return new mr.mirror.Error(
+ mr.mirror.MirrorMediaStream.EMPTY_STREAM_,
+ mr.MirrorAnalytics.CapturingFailure.CAPTURE_TAB_FAIL_EMPTY_STREAM);
+ }
+ }
+
+ /**
+ * Requests a screen capture source from the user via a native dialog and
+ * returns the source ID, or rejects if a timeout is reached or the user
+ * cancels.
+ * @param {number=} timeoutMillis The timeout in milliseconds.
+ * @return {!Promise<string>} Fulfilled with the source ID.
+ * @private
+ */
+ requestScreenCaptureSourceId_(
+ timeoutMillis = mr.mirror.MirrorMediaStream.WINDOW_PICKER_TIMEOUT_) {
+ return new Promise((resolve, reject) => {
+ const desktopChooserConfig = ['screen', 'audio'];
+ if (mr.PlatformUtils.getCurrentOS() == mr.PlatformUtils.OS.LINUX) {
+ desktopChooserConfig.push('window');
+ }
+ let requestId;
+ // Wait 60 seconds and then cancel the picker and reject the
+ // promise.
+
+ const timeoutId = window.setTimeout(() => {
+ if (requestId) {
+ chrome.desktopCapture.cancelChooseDesktopMedia(requestId);
+ }
+ reject(new mr.mirror.Error(
+ 'timeout',
+ mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_TIMEOUT));
+ }, timeoutMillis);
+ // https://developer.chrome.com/extensions/desktopCapture#method-chooseDesktopMedia
+ requestId = chrome.desktopCapture.chooseDesktopMedia(
+ desktopChooserConfig, sourceId => {
+ window.clearTimeout(timeoutId);
+ if (!sourceId) {
+ // User cancelled the desktop media selector prompt.
+ reject(new mr.mirror.Error(
+ 'User cancelled capture dialog',
+ mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_USER_CANCEL));
+ } else {
+ resolve(sourceId);
+ }
+ });
+ });
+ }
+
+ /**
+ * Generates a screen capture MediaStream from the given
+ * MediaStreamConstraints.
+ * @param {!MediaStreamConstraints} constraints The constraints object to use.
+ * @return {!Promise<MediaStream>} Fulfilled with the MediaStream from
+ * capture.
+ * @private
+ */
+ generateScreenCaptureStream_(constraints) {
+ return new Promise((resolve, reject) => {
+ this.logger_.info(
+ () => 'Starting desktop capture with constraints ' +
+ JSON.stringify(constraints));
+ navigator.mediaDevices.getUserMedia(constraints)
+ .then(
+ stream => {
+ if (!stream) {
+ // NOTE(miu): This implies that getUserMedia is broken, and it
+ // may also be breaking chrome.tabCapture.
+ reject(new mr.mirror.Error(
+ mr.mirror.MirrorMediaStream.EMPTY_STREAM_,
+ mr.MirrorAnalytics.CapturingFailure.DESKTOP_FAIL));
+ }
+ this.setStream_(stream);
+ resolve(stream);
+ },
+ error => {
+ let errorReason =
+ mr.MirrorAnalytics.CapturingFailure.DESKTOP_FAIL;
+ // Certain errors indicate the user cancelled the request.
+ // https://www.w3.org/TR/mediacapture-streams/#methods-5
+ if (error.name == 'NotAllowedError') {
+ errorReason = mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_USER_CANCEL;
+ }
+ reject(new mr.mirror.Error(
+ `${error.name} ${error.constraintName}: ${error.message}`,
+ errorReason));
+ });
+ });
+ }
+
+ /**
+ * @return {!Promise<!mr.mirror.MirrorMediaStream>} Fulfilled when capture
+ * has started.
+ * @private
+ */
+ startDesktopCapturing_() {
+ if (mr.mirror.Config.isDesktopAudioCaptureAvailable &&
+ this.captureParams_.mirrorSettings.shouldCaptureAudio &&
+ !this.captureParams_.mirrorSettings.shouldCaptureVideo) {
+ return this
+ .generateScreenCaptureStream_(
+ this.captureParams_.toMediaConstraints())
+ .then(_ => this);
+ }
+
+ // Video capture requires asking the user to pick which screen to capture.
+
+ return this.requestScreenCaptureSourceId_().then(sourceId => {
+ const constraints = this.captureParams_.toMediaConstraints(sourceId);
+ return this.generateScreenCaptureStream_(constraints).then(_ => this);
+ });
+ }
+
+ /**
+ * @return {!Promise<!mr.mirror.MirrorMediaStream>} Fulfilled when capture
+ * has started.
+ * @private
+ */
+ startOffscreenTabCapturing_() {
+ mr.Assertions.assert(!!this.captureParams_.offscreenTabUrl);
+ const constraints = this.captureParams_.toMediaConstraints();
+ this.logger_.info(
+ () => 'Starting offscreen tab capture with constraints ' +
+ JSON.stringify(constraints));
+ return new Promise((resolve, reject) => {
+ chrome.tabCapture.captureOffscreenTab(
+ this.captureParams_.offscreenTabUrl.toString(), constraints,
+ stream => {
+ if (stream) {
+ this.setStream_(stream);
+ resolve(this);
+ } else {
+ reject(this.createTabCaptureError_());
+ }
+ });
+ });
+ }
+
+ /**
+ * @param {!MediaStream} stream
+ * @private
+ */
+ setStream_(stream) {
+ this.mediaStream_ = stream;
+ mr.Assertions.assert(
+ stream.getAudioTracks().length || stream.getVideoTracks().length,
+ 'Expecting at least one audio or video track.');
+ // For desktop capturing, users may stop capturing via desktop capturing's
+ // own stop button, which triggers onended event.
+ stream.getTracks().forEach(track => {
+ track.onended = this.handleTrackEnded_.bind(this);
+ });
+ }
+
+ /**
+ * Stops captured streams.
+ */
+ stop() {
+ if (!this.mediaStream_) return;
+ this.mediaStream_.getTracks().forEach(track => {
+ track.onended = null;
+ track.stop();
+ });
+ this.mediaStream_ = null;
+ if (this.onStreamEnded_) {
+ this.onStreamEnded_();
+ }
+ }
+};
+
+
+/**
+ * The number of milliseconds to wait after for the browser to call the callback
+ * function after calling chrome.tabCapture.capture.
+ * @private @const {number}
+ */
+mr.mirror.MirrorMediaStream.TAB_CAPTURE_TIMEOUT_ = 5000;
+
+
+/**
+ * @private @const {number}
+ */
+mr.mirror.MirrorMediaStream.WINDOW_PICKER_TIMEOUT_ = 60000;
+
+
+/**
+ * Error messaging for reporting of empty streams.
+ * @private @const {string}
+ */
+mr.mirror.MirrorMediaStream.EMPTY_STREAM_ = 'empty_stream';
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/mirror_media_stream_test.js b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/mirror_media_stream_test.js
new file mode 100644
index 00000000000..45746847799
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mirror_services/stream_capture/mirror_media_stream_test.js
@@ -0,0 +1,407 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly();
+
+goog.require('mr.PlatformUtils');
+goog.require('mr.mirror.CaptureParameters');
+goog.require('mr.mirror.CaptureSurfaceType');
+goog.require('mr.mirror.Config');
+goog.require('mr.mirror.Error');
+goog.require('mr.mirror.MirrorMediaStream');
+goog.require('mr.mirror.Settings');
+
+describe('mr.mirror.MirrorMediaStream', () => {
+ let captureParams;
+ let instance;
+ let mediaStream;
+ let mirrorSettings;
+
+ beforeEach(() => {
+ chrome.runtime.lastError = null;
+ chrome.tabCapture =
+ jasmine.createSpyObj('tabCapture', ['capture', 'captureOffscreenTab']);
+ chrome.desktopCapture = jasmine.createSpyObj(
+ 'desktopCapture', ['chooseDesktopMedia', 'cancelChooseDesktopMedia']);
+ spyOn(navigator.mediaDevices, 'getUserMedia');
+ spyOn(mr.PlatformUtils, 'getCurrentOS');
+
+ mediaStream = jasmine.createSpyObj(
+ 'mediaStream', ['getAudioTracks', 'getVideoTracks', 'getTracks']);
+ mirrorSettings = new mr.mirror.Settings();
+ captureParams = new mr.mirror.CaptureParameters(
+ mr.mirror.CaptureSurfaceType.DESKTOP, mirrorSettings);
+ instance = new mr.mirror.MirrorMediaStream(captureParams);
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ it('accessor for capture params returns initial value', () => {
+ expect(instance.getCaptureParams()).toBe(captureParams);
+ });
+
+ describe('when capturing a tab', () => {
+ beforeEach(() => {
+ captureParams.captureSurface = mr.mirror.CaptureSurfaceType.TAB;
+ });
+
+ it('stores the stream upon successful capture', (done) => {
+ mediaStream.getVideoTracks.and.returnValue([{}]);
+ mediaStream.getAudioTracks.and.returnValue([{}]);
+ mediaStream.getTracks.and.returnValue([{}]);
+ chrome.tabCapture.capture.and.callFake((constraints, callback) => {
+ callback(mediaStream);
+ });
+
+ instance.start()
+ .then(() => {
+ expect(chrome.tabCapture.capture).toHaveBeenCalled();
+ expect(instance.getMediaStream()).toBe(mediaStream);
+ done();
+ })
+ .catch(fail);
+ });
+
+ it('rejects with an error upon empty stream with error message', (done) => {
+ chrome.runtime.lastError = {message: 'expected-message'};
+ chrome.tabCapture.capture.and.callFake((constraints, callback) => {
+ callback();
+ });
+
+ instance.start().catch((err) => {
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason).toBe(mr.MirrorAnalytics.CapturingFailure.TAB_FAIL);
+ expect(err.message).toBe('expected-message');
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+ });
+
+ it('rejects with an error upon empty stream with empty message', (done) => {
+ chrome.tabCapture.capture.and.callFake((constraints, callback) => {
+ callback();
+ });
+
+ instance.start().catch((err) => {
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason)
+ .toBe(mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_TAB_FAIL_EMPTY_STREAM);
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+ });
+
+ it('rejects with an error upon timeout', (done) => {
+ instance.start().catch((err) => {
+ expect(window.chrome.tabCapture.capture).toHaveBeenCalled();
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason)
+ .toBe(mr.MirrorAnalytics.CapturingFailure.CAPTURE_TAB_TIMEOUT);
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+ jasmine.clock().tick(5001);
+ });
+ });
+
+ describe('when capturing an off-screen tab', () => {
+ beforeEach(() => {
+ captureParams.offscreenTabUrl = 'offscreen-tab-url';
+ captureParams.presentationId = 'offscreen-tab-presentation-id';
+ captureParams.captureSurface = mr.mirror.CaptureSurfaceType.OFFSCREEN_TAB;
+ });
+
+ it('stores the stream upon successful capture', (done) => {
+ mediaStream.getVideoTracks.and.returnValue([{}]);
+ mediaStream.getAudioTracks.and.returnValue([{}]);
+ mediaStream.getTracks.and.returnValue([{}]);
+ chrome.tabCapture.captureOffscreenTab.and.callFake(
+ (tabUrl, constraints, callback) => {
+ expect(tabUrl).toBe('offscreen-tab-url');
+ callback(mediaStream);
+ });
+
+ instance.start()
+ .then(() => {
+ expect(window.chrome.tabCapture.captureOffscreenTab)
+ .toHaveBeenCalled();
+ expect(instance.getMediaStream()).toBe(mediaStream);
+ done();
+ })
+ .catch(fail);
+ });
+
+ it('rejects with an error upon empty stream with error message', (done) => {
+ chrome.runtime.lastError = {message: 'expected-message'};
+ chrome.tabCapture.captureOffscreenTab.and.callFake(
+ (tabUrl, constraints, callback) => callback());
+
+ instance.start().catch((err) => {
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason).toBe(mr.MirrorAnalytics.CapturingFailure.TAB_FAIL);
+ expect(err.message).toBe('expected-message');
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+ });
+
+ it('rejects with an error upon empty stream with empty message', (done) => {
+ chrome.tabCapture.captureOffscreenTab.and.callFake(
+ (tabUrl, constraints, callback) => callback());
+
+ instance.start().catch((err) => {
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason)
+ .toBe(mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_TAB_FAIL_EMPTY_STREAM);
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+ });
+ });
+
+ describe('when capturing the desktop', () => {
+ const supportsAudioCapture =
+ mr.mirror.Config.isDesktopAudioCaptureAvailable;
+
+ beforeEach(() => {
+ captureParams.captureSurface = mr.mirror.CaptureSurfaceType.DESKTOP;
+ });
+
+ afterEach(() => {
+ mr.mirror.Config.isDesktopAudioCaptureAvailable = supportsAudioCapture;
+ });
+
+ it('stores the audio and video streams upon successful capture', (done) => {
+ mediaStream.getVideoTracks.and.returnValue([{}]);
+ mediaStream.getAudioTracks.and.returnValue([{}]);
+ mediaStream.getTracks.and.returnValue([{}]);
+
+ chrome.desktopCapture.chooseDesktopMedia.and.callFake(
+ (config, callback) => {
+ callback('source-id');
+ });
+ navigator.mediaDevices.getUserMedia.and.callFake(
+ constraints => Promise.resolve(mediaStream));
+
+ instance.start()
+ .then(() => {
+ expect(chrome.desktopCapture.chooseDesktopMedia).toHaveBeenCalled();
+ expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalled();
+ expect(instance.getMediaStream()).toBe(mediaStream);
+ done();
+ })
+ .catch(fail);
+ });
+
+ it('stores the audio stream upon successful capture', (done) => {
+ mr.mirror.Config.isDesktopAudioCaptureAvailable = true;
+ const audioOnlyMirrorSettings = new mr.mirror.Settings();
+ audioOnlyMirrorSettings.shouldCaptureVideo = false;
+ const audioOnlyCaptureParams = new mr.mirror.CaptureParameters(
+ mr.mirror.CaptureSurfaceType.DESKTOP, audioOnlyMirrorSettings);
+ const audioOnlyInstance =
+ new mr.mirror.MirrorMediaStream(audioOnlyCaptureParams);
+
+ mediaStream.getVideoTracks.and.returnValue([{}]);
+ mediaStream.getAudioTracks.and.returnValue([{}]);
+ mediaStream.getTracks.and.returnValue([{}]);
+
+ navigator.mediaDevices.getUserMedia.and.callFake(
+ constraints => Promise.resolve(mediaStream));
+
+ audioOnlyInstance.start()
+ .then(() => {
+ expect(chrome.desktopCapture.chooseDesktopMedia)
+ .not.toHaveBeenCalled();
+ expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalled();
+ expect(audioOnlyInstance.getMediaStream()).toBe(mediaStream);
+ done();
+ })
+ .catch(fail);
+ });
+
+ it('allows choosing only screen, audio for non-linux platforms', (done) => {
+ mr.PlatformUtils.getCurrentOS.and.returnValue(
+ mr.PlatformUtils.OS.WINDOWS);
+ chrome.desktopCapture.chooseDesktopMedia.and.callFake(
+ (config, callback) => {
+ expect(config).toContain('screen');
+ expect(config).toContain('audio');
+ expect(config).not.toContain('window');
+ done();
+ });
+ instance.start();
+ });
+
+ it('allows choosing screen, audio, window for linux platforms', (done) => {
+ mr.PlatformUtils.getCurrentOS.and.returnValue(mr.PlatformUtils.OS.LINUX);
+ chrome.desktopCapture.chooseDesktopMedia.and.callFake(
+ (config, callback) => {
+ expect(config).toContain('screen');
+ expect(config).toContain('audio');
+ expect(config).toContain('window');
+ done();
+ });
+ instance.start();
+ });
+
+ it('rejects with an error upon timeout in desktop chooser', (done) => {
+ chrome.desktopCapture.chooseDesktopMedia.and.returnValue('expected-id');
+
+ instance.start().catch((err) => {
+ expect(chrome.desktopCapture.chooseDesktopMedia).toHaveBeenCalled();
+ expect(chrome.desktopCapture.cancelChooseDesktopMedia)
+ .toHaveBeenCalledWith('expected-id');
+ expect(navigator.mediaDevices.getUserMedia).not.toHaveBeenCalled();
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason)
+ .toBe(mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_TIMEOUT);
+ expect(err.message).toBe('timeout');
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+
+ jasmine.clock().tick(60001);
+ });
+
+ it('rejects with an error when user cancels desktop picker', (done) => {
+ chrome.desktopCapture.chooseDesktopMedia.and.callFake(
+ (config, callback) => {
+ callback(/* no source id */);
+ });
+
+ instance.start().catch((err) => {
+ expect(chrome.desktopCapture.chooseDesktopMedia).toHaveBeenCalled();
+ expect(navigator.mediaDevices.getUserMedia).not.toHaveBeenCalled();
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason)
+ .toBe(mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_USER_CANCEL);
+ expect(err.message).toMatch(/cancelled/i);
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+ });
+
+ it('rejects with an error upon getUserMedia error', (done) => {
+ chrome.desktopCapture.chooseDesktopMedia.and.callFake(
+ (config, callback) => {
+ callback('source-id');
+ });
+
+ navigator.mediaDevices.getUserMedia.and.callFake(
+ constraints => Promise.reject(
+ new DOMException('expected-message', 'SecurityError')));
+
+ instance.start().catch((err) => {
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason)
+ .toBe(mr.MirrorAnalytics.CapturingFailure.DESKTOP_FAIL);
+ expect(err.message).toMatch(/\bSecurityError\b/);
+ expect(err.message).toMatch(/\bexpected-message\b/);
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+ });
+
+ it('rejects with an cancelled error upon NotAllowedError', (done) => {
+ chrome.desktopCapture.chooseDesktopMedia.and.callFake(
+ (config, callback) => {
+ callback('source-id');
+ });
+ navigator.mediaDevices.getUserMedia.and.callFake(
+ constraints => Promise.reject(
+ new DOMException('expected-message', 'NotAllowedError')));
+
+ instance.start().catch((err) => {
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason)
+ .toBe(mr.MirrorAnalytics.CapturingFailure
+ .CAPTURE_DESKTOP_FAIL_ERROR_USER_CANCEL);
+ expect(err.message).toMatch(/\bNotAllowedError\b/);
+ expect(err.message).toMatch(/\bexpected-message\b/);
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+ });
+
+ it('rejects with an error upon empty stream object', (done) => {
+ chrome.desktopCapture.chooseDesktopMedia.and.callFake(
+ (config, callback) => {
+ callback('source-id');
+ });
+ navigator.mediaDevices.getUserMedia.and.callFake(
+ constraints => Promise.resolve(null));
+
+ instance.start().catch((err) => {
+ expect(err instanceof mr.mirror.Error).toBe(true);
+ expect(err.reason)
+ .toBe(mr.MirrorAnalytics.CapturingFailure.DESKTOP_FAIL);
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ });
+ });
+ });
+
+ describe('when stopping a stream', () => {
+ let startPromise;
+ let track;
+
+ beforeEach(() => {
+ track = jasmine.createSpyObj('track', ['stop']);
+ mediaStream.getVideoTracks.and.returnValue([{}]);
+ mediaStream.getAudioTracks.and.returnValue([{}]);
+ mediaStream.getTracks.and.returnValue([track]);
+ captureParams.captureSurface = mr.mirror.CaptureSurfaceType.TAB;
+ chrome.tabCapture.capture.and.callFake((constraints, callback) => {
+ callback(mediaStream);
+ });
+
+ startPromise = instance.start();
+ });
+
+ it('calls stop() on the tracks', (done) => {
+ startPromise
+ .then(() => {
+ instance.stop();
+ expect(track.stop).toHaveBeenCalled();
+ expect(track.onended).toBe(null);
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ })
+ .catch(fail);
+ });
+
+ it('automatically stops when track ends', (done) => {
+ startPromise
+ .then(() => {
+ track.onended();
+ expect(track.stop).toHaveBeenCalled();
+ expect(track.onended).toBe(null);
+ expect(instance.getMediaStream()).toBe(null);
+ done();
+ })
+ .catch(fail);
+ });
+
+ it('calls the onStreamEnded callback if it exists', (done) => {
+ const onStreamEndedSpy = jasmine.createSpy('onStreamEnded');
+ instance.setOnStreamEnded(onStreamEndedSpy);
+
+ startPromise
+ .then(() => {
+ instance.stop();
+ expect(onStreamEndedSpy).toHaveBeenCalled();
+ done();
+ })
+ .catch(fail);
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/module.js b/chromium/chrome/browser/resources/media_router/extension/src/module.js
new file mode 100644
index 00000000000..248c6cb6097
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/module.js
@@ -0,0 +1,247 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Contains definition for modules and the module loader.
+ * The Media Router extension is logically separated into modules. Each module
+ * and their corresponding bundle path are registered at startup
+ * time. When a module is required, they will be loaded on-demand.
+ */
+
+goog.provide('mr.Module');
+goog.provide('mr.ModuleId');
+
+goog.require('mr.Logger');
+goog.require('mr.PromiseResolver');
+
+
+/**
+ * Identifier for each module.
+ * @enum {string}
+ */
+mr.ModuleId = {
+ CAST_CHANNEL_SERVICE: 'mr.cast.ChannelService',
+ CAST_SINK_DISCOVERY_SERVICE: 'mr.cast.SinkDiscoveryService',
+ CAST_STREAMING_SERVICE: 'mr.mirror.cast.Service',
+ SLARTI_SINK_DISCOVERY_SERVICE: 'mr.cloud.slarti.SinkDiscoveryService',
+ WEAVE_SINK_DISCOVERY_SERVICE: 'mr.cloud.discovery.WeaveSinkDiscoveryService',
+ HANGOUTS_SERVICE: 'mr.mirror.hangouts.HangoutsService',
+ MEETINGS_SERVICE: 'mr.mirror.hangouts.MeetingsService',
+ PROVIDER_MANAGER: 'mr.ProviderManager',
+ WEBRTC_STREAMING_SERVICE: 'mr.mirror.webrtc.WebRtcService'
+};
+
+
+/**
+ * Identifier for bundles. A bundle is a js file containing a collection of
+ * modules.
+
+ * @enum {string}
+ */
+mr.Bundle = {
+ MAIN: 'background_script.js',
+ MIRRORING_CAST_STREAMING: 'mirroring_cast_streaming.js',
+ MIRRORING_HANGOUTS: 'mirroring_hangouts.js',
+ MIRRORING_WEBRTC: 'mirroring_webrtc.js'
+};
+
+
+/**
+ * Base class for a module. When a module is loaded and initialized, it should
+ * call mr.Module.onModuleLoaded to inform its dependencies that it is ready.
+ */
+mr.Module = class {
+ /**
+ * Returns the module with the given ID if it is already initialized, null
+ * otherwise.
+ * @param {mr.ModuleId} moduleId
+ * @return {?mr.Module}
+ */
+ static get(moduleId) {
+ return mr.Module.moduleById_.get(moduleId) || null;
+ }
+
+ /**
+ * Loads the module with the given ID. If the module is already loaded, the
+ * Promise is resolved immediately.
+ * @param {mr.ModuleId} moduleId
+ * @return {!Promise<!mr.Module>} Resolved with the module when
+ * it is loaded.
+ */
+ static load(moduleId) {
+ const module = mr.Module.get(moduleId);
+ if (module) {
+ return Promise.resolve(module);
+ }
+ let resolver = mr.Module.resolverByModuleId_.get(moduleId);
+ if (!resolver) {
+ resolver = new mr.PromiseResolver();
+ mr.Module.resolverByModuleId_.set(moduleId, resolver);
+ mr.Module.loadBundleForModule_(moduleId, resolver);
+ }
+
+ return resolver.promise;
+ }
+
+ /**
+ * Loads the bundle corresponding to the given module. Called the first time
+ * a module is requested.
+ * @param {mr.ModuleId} moduleId
+ * @param {!mr.PromiseResolver<!mr.Module>} resolver Rejected if the bundle
+ * associated with the module won't be loaded due to permanent error.
+ * @private
+ */
+ static loadBundleForModule_(moduleId, resolver) {
+ const bundle = mr.Module.getBundle_(moduleId);
+ if (!bundle) {
+ resolver.reject(new Error(`No corresponding bundle for ${moduleId}`));
+ return;
+ }
+
+ if (mr.Module.DEFAULT_LOAD_BUNDLES_.has(bundle)) {
+ return;
+ }
+
+ // Check if the bundle has been not requested previously.
+ let bundlePromise = mr.Module.bundlePromises_.get(bundle);
+ if (!bundlePromise) {
+
+ mr.Module.logger_.info(`Loading bundle ${bundle} for module ${moduleId}`);
+ bundlePromise = mr.Module.doLoadBundle_(bundle);
+ mr.Module.bundlePromises_.set(bundle, bundlePromise);
+ }
+
+ // Add an error handler to reject the module load request.
+ bundlePromise.catch(e => {
+ resolver.reject(e);
+ });
+ }
+
+ /**
+ * Returns the bundle associated with a module.
+ * @param {mr.ModuleId} moduleId
+ * @return {?mr.Bundle}
+ * @private
+ */
+ static getBundle_(moduleId) {
+ return mr.Module.MODULE_TO_BUNDLE_MAPPING_.get(moduleId) || null;
+ }
+
+ /**
+ * Called when a bundle needs to be loaded.
+ * @param {mr.Bundle} bundle Name of bundle to load.
+ * @return {!Promise<void>} Resolves when the bundle is loaded or rejected if
+ * it failed to load.
+ * @private
+ */
+ static doLoadBundle_(bundle) {
+ let resolver = new mr.PromiseResolver();
+ resolver.promise.then(
+ () => {
+ mr.Module.logger_.info(`Bundle ${bundle} loaded`);
+ },
+ e => {
+ mr.Module.logger_.error(`Failed to load bundle ${bundle}`);
+ throw e;
+ });
+ let script = document.createElement('script');
+ script.src = chrome.extension.getURL(bundle);
+ script.setAttribute('type', 'text/javascript');
+ script.async = true;
+
+ script.onload = () => resolver.resolve(undefined);
+ script.onerror = () =>
+ resolver.reject(new Error(`Failed to load bundle ${bundle}`));
+ document.head.appendChild(script);
+ return resolver.promise;
+ }
+
+ /**
+ * Called by a module when it is done loading and initializing. Registers the
+ * module and resolves the outstanding promise returned by |load(moduleId)|.
+ * @param {mr.ModuleId} moduleId Identifier of the module. No two modules can
+ * have the same identifier.
+ * @param {!mr.Module} module The module that is ready.
+ */
+ static onModuleLoaded(moduleId, module) {
+ if (mr.Module.moduleById_.has(moduleId)) {
+ throw new Error('Duplicate module ' + moduleId);
+ }
+ mr.Module.moduleById_.set(moduleId, module);
+ const resolver = mr.Module.resolverByModuleId_.get(moduleId);
+ if (resolver) {
+ resolver.resolve(module);
+ }
+ }
+
+ /**
+ * Used for testing only.
+ */
+ static clearForTest() {
+ mr.Module.moduleById_.clear();
+ mr.Module.resolverByModuleId_.clear();
+ mr.Module.bundlePromises_.clear();
+ }
+
+ /**
+ * Subclasses should override this if a mr.EventListener designated this
+ * module to forward the events to.
+ * @param {*} event The event delivered to the handler. It is the handler's
+ * responsibility to verify that it can handle the event.
+ * @param {...*} args Arguments for the event.
+ */
+ handleEvent(event, ...args) {
+ throw new Error('Not implemented');
+ }
+};
+
+
+/**
+ * Maps a module ID to a bundle ID. Used for loading the bundle that contains
+ * a required module.
+ * @private @const {!Map<mr.ModuleId, mr.Bundle>}
+ */
+mr.Module.MODULE_TO_BUNDLE_MAPPING_ = new Map([
+ [mr.ModuleId.CAST_CHANNEL_SERVICE, mr.Bundle.MAIN],
+ [mr.ModuleId.CAST_SINK_DISCOVERY_SERVICE, mr.Bundle.MAIN],
+ [mr.ModuleId.CAST_STREAMING_SERVICE, mr.Bundle.MIRRORING_CAST_STREAMING],
+ [mr.ModuleId.SLARTI_SINK_DISCOVERY_SERVICE, mr.Bundle.MAIN],
+ [mr.ModuleId.WEAVE_SINK_DISCOVERY_SERVICE, mr.Bundle.MAIN],
+ [mr.ModuleId.HANGOUTS_SERVICE, mr.Bundle.MIRRORING_HANGOUTS],
+ [mr.ModuleId.MEETINGS_SERVICE, mr.Bundle.MIRRORING_HANGOUTS],
+ [mr.ModuleId.PROVIDER_MANAGER, mr.Bundle.MAIN],
+ [mr.ModuleId.WEBRTC_STREAMING_SERVICE, mr.Bundle.MIRRORING_WEBRTC]
+]);
+
+
+/**
+ * Set of bundles that are loaded by default.
+ * @private @const {!Set<mr.Bundle>}
+ */
+mr.Module.DEFAULT_LOAD_BUNDLES_ = new Set([mr.Bundle.MAIN]);
+
+
+/** @private {mr.Logger} */
+mr.Module.logger_ = mr.Logger.getInstance('mr.Module');
+
+/**
+ * The set of modules currently loaded and initialized in the extension, keyed
+ * by their IDs.
+ * @private {!Map<mr.ModuleId, !mr.Module>}
+ */
+mr.Module.moduleById_ = new Map();
+
+
+/**
+ * Holds the outstanding promise while a module is being loaded.
+ * @private {!Map<mr.ModuleId, !mr.PromiseResolver<!mr.Module>>}
+ */
+mr.Module.resolverByModuleId_ = new Map();
+
+
+/**
+ * Holds the outstanding promise while a bundle is being loaded.
+ * @private {!Map<mr.Bundle, !Promise<void>>}
+ */
+mr.Module.bundlePromises_ = new Map();
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/module_test.js b/chromium/chrome/browser/resources/media_router/extension/src/module_test.js
new file mode 100644
index 00000000000..1d2fed8c118
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/module_test.js
@@ -0,0 +1,90 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly('module_test');
+
+goog.require('mr.Module');
+goog.require('mr.PromiseUtils');
+
+
+
+describe('Tests modules', function() {
+ let mockModule;
+
+ beforeEach(function() {
+ mockModule = {'id': 1};
+ });
+
+ afterEach(function() {
+ mr.Module.clearForTest();
+ });
+
+ it('gets a module before and after it is loaded', function() {
+ let module = mr.Module.get('SomeModule');
+ expect(module).toBeNull();
+
+ const mockModule2 = {'id': 2};
+ mr.Module.onModuleLoaded('AnotherModule', mockModule2);
+
+ module = mr.Module.get('SomeModule');
+ expect(module).toBeNull();
+
+ mr.Module.onModuleLoaded('SomeModule', mockModule);
+ module = mr.Module.get('SomeModule');
+ expect(module).toBe(mockModule);
+
+ module = mr.Module.get('AnotherModule');
+ expect(module).toBe(mockModule2);
+ });
+
+ it('loads a module which loads a bundle', function(done) {
+ spyOn(mr.Module, 'getBundle_').and.returnValue('SomeBundle');
+ spyOn(mr.Module, 'doLoadBundle_').and.returnValue(Promise.resolve());
+
+ const promise = mr.Module.load('SomeModule');
+ const promise2 = mr.Module.load('SomeModule');
+
+ expect(mr.Module.getBundle_).toHaveBeenCalledWith('SomeModule');
+ expect(mr.Module.doLoadBundle_).toHaveBeenCalledWith('SomeBundle');
+ expect(mr.Module.doLoadBundle_.calls.count()).toBe(1);
+
+ mr.Module.onModuleLoaded('SomeModule', mockModule);
+
+ const promise3 = mr.Module.load('SomeModule');
+ Promise.all([promise, promise2, promise3]).then(modules => {
+ for (let module of modules) {
+ expect(module).toBe(mockModule);
+ }
+ done();
+ });
+ });
+
+ it('load rejects if failed to load a bundle', function(done) {
+ spyOn(mr.Module, 'getBundle_').and.returnValue('SomeBundle');
+ spyOn(mr.Module, 'doLoadBundle_')
+ .and.returnValue(Promise.reject(new Error('failed to load bundle')));
+
+ const promise = mr.Module.load('SomeModule');
+ const promise2 = mr.Module.load('SomeModule');
+
+ expect(mr.Module.getBundle_).toHaveBeenCalledWith('SomeModule');
+ expect(mr.Module.doLoadBundle_).toHaveBeenCalledWith('SomeBundle');
+ expect(mr.Module.doLoadBundle_.calls.count()).toBe(1);
+
+ const promise3 = mr.Module.load('SomeModule');
+ mr.PromiseUtils.allSettled([promise, promise2, promise3]).then(results => {
+ for (let result of results) {
+ expect(result.fulfilled).toBe(false);
+ }
+ done();
+ });
+ });
+
+ it('each module is mapped to a bundle', () => {
+ for (const key in mr.ModuleId) {
+ expect(mr.Module.getBundle_(mr.ModuleId[key]))
+ .not.toBeNull(`${mr.ModuleId[key]} does not map to a bundle`);
+ }
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/mojo_externs.js b/chromium/chrome/browser/resources/media_router/extension/src/mojo_externs.js
new file mode 100644
index 00000000000..93f4805ccde
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/mojo_externs.js
@@ -0,0 +1,522 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Closure definitions of Mojo service objects. Note that these
+ * definitions are bound only after chrome.mojoPrivate.requireAsync resolves
+ * in mojo.js.
+ */
+
+
+var mojo = {};
+
+
+
+// Core Mojo.
+
+/**
+ * @param {!Object} interfaceType
+ * @param {!Object} impl
+ * @param {mojo.InterfaceRequest=} request
+ * @constructor
+ */
+mojo.Binding = function(interfaceType, impl, request) {};
+
+
+/**
+ * Closes the message pipe. The bound object will no longer receive messages.
+ */
+mojo.Binding.prototype.close = function() {};
+
+
+/**
+ * Binds to the given request.
+ * @param {!mojo.InterfaceRequest} request
+ */
+mojo.Binding.prototype.bind = function(request) {};
+
+
+/** @param {function()} callback */
+mojo.Binding.prototype.setConnectionErrorHandler = function(callback) {};
+
+
+/**
+ * Creates an interface ptr and bind it to this instance.
+ * @return {?} The interface ptr.
+ */
+mojo.Binding.prototype.createInterfacePtrAndBind = function() {};
+
+
+
+/** @constructor */
+mojo.InterfaceRequest = function() {};
+
+
+/**
+ * Closes the message pipe. The object can no longer be bound to an
+ * implementation.
+ */
+mojo.InterfaceRequest.prototype.close = function() {};
+
+
+
+/** @constructor */
+mojo.InterfacePtrController = function() {};
+
+
+/**
+ * Closes the message pipe. Messages can no longer be sent with this object.
+ */
+mojo.InterfacePtrController.prototype.reset = function() {};
+
+
+/** @param {function()} callback */
+mojo.InterfacePtrController.prototype.setConnectionErrorHandler = function(
+ callback) {};
+
+
+
+/**
+ * @param {!Object} interfacePtr
+ */
+mojo.makeRequest = function(interfacePtr) {};
+
+
+// Mojom structs.
+
+/**
+ * @constructor
+ * @struct
+ */
+mojo.IPAddress = function() {};
+
+
+
+/** @type {!Array<number>} */
+mojo.IPAddress.prototype.address;
+
+
+/** @type {!Array<number>} */
+mojo.IPAddress.prototype.address_bytes;
+
+
+
+/**
+ * @constructor
+ * @struct
+ */
+mojo.IPEndPoint = function() {};
+
+
+/** @type {mojo.IPAddress} */
+mojo.IPEndPoint.prototype.address;
+
+
+/** @type {number} */
+mojo.IPEndPoint.prototype.port;
+
+
+
+/** @constructor */
+mojo.Url = function() {};
+
+
+/** @type {string} */
+mojo.Url.prototype.url;
+
+
+
+/**
+ * @constructor
+ * @struct
+ */
+mojo.DialMediaSink = function() {};
+
+
+/** @type {mojo.IPAddress} */
+mojo.DialMediaSink.prototype.ip_address;
+
+
+/** @type {string} */
+mojo.DialMediaSink.prototype.model_name;
+
+
+/** @type {mojo.Url} */
+mojo.DialMediaSink.prototype.app_url;
+
+
+
+/**
+ * @constructor
+ * @struct
+ */
+mojo.CastMediaSink = function() {};
+
+
+
+/** @type {mojo.IPAddress} */
+mojo.CastMediaSink.prototype.ip_address;
+
+
+/** @type {mojo.IPEndPoint} */
+mojo.CastMediaSink.prototype.ip_endpoint;
+
+
+/** @type {string} */
+mojo.CastMediaSink.prototype.model_name;
+
+
+/** @type {number} */
+mojo.CastMediaSink.prototype.capabilities;
+
+
+/** @type {number} */
+mojo.CastMediaSink.prototype.cast_channel_id;
+
+
+
+/**
+ * @constructor
+ * @struct
+ */
+mojo.SinkExtraData = function() {};
+
+
+/** @type {mojo.DialMediaSink} */
+mojo.SinkExtraData.prototype.dial_media_sink;
+
+
+/** @type {mojo.CastMediaSink} */
+mojo.SinkExtraData.prototype.cast_media_sink;
+
+
+
+/**
+ * @constructor
+ * @struct
+ */
+mojo.Sink = function() {};
+
+
+/** @constructor */
+mojo.SinkIconType = function() {};
+mojo.SinkIconType.CAST = 0;
+mojo.SinkIconType.CAST_AUDIO_GROUP = 1;
+mojo.SinkIconType.CAST_AUDIO = 2;
+mojo.SinkIconType.MEETING = 3;
+mojo.SinkIconType.HANGOUT = 4;
+mojo.SinkIconType.EDUCATION = 5;
+mojo.SinkIconType.GENERIC = 6;
+
+
+/** @type {string} */
+mojo.Sink.prototype.sink_id;
+
+
+/** @type {string} */
+mojo.Sink.prototype.name;
+
+
+/** @type {?string} */
+mojo.Sink.prototype.description;
+
+
+/** @type {?string} */
+mojo.Sink.prototype.domain;
+
+
+/** @type {?mojo.SinkIconType} */
+mojo.Sink.prototype.icon_type;
+
+
+/** @type {?mojo.SinkExtraData} */
+mojo.Sink.prototype.extra_data;
+
+
+
+/**
+ * @param {!Object} values An object mapping from property names to values.
+ * @constructor
+ * @struct
+ */
+mojo.TimeDelta = function(values) {};
+
+
+/** @type {number} */
+mojo.TimeDelta.prototype.microseconds;
+
+
+
+/**
+ * @param {!Object} values An object mapping from property names to values.
+ * @constructor
+ * @struct
+ */
+mojo.MediaStatus = function(values) {};
+
+
+/** @constructor */
+mojo.MediaStatus.PlayState = function() {};
+/** @type {!mojo.MediaStatus.PlayState} */
+mojo.MediaStatus.PlayState.PLAYING;
+/** @type {!mojo.MediaStatus.PlayState} */
+mojo.MediaStatus.PlayState.PAUSED;
+/** @type {!mojo.MediaStatus.PlayState} */
+mojo.MediaStatus.PlayState.BUFFERING;
+
+
+/** @type {?string} */
+mojo.MediaStatus.prototype.title;
+
+
+/** @type {?string} */
+mojo.MediaStatus.prototype.description;
+
+
+/** @type {boolean} */
+mojo.MediaStatus.prototype.can_play_pause;
+
+
+/** @type {boolean} */
+mojo.MediaStatus.prototype.can_mute;
+
+
+/** @type {boolean} */
+mojo.MediaStatus.prototype.can_set_volume;
+
+
+/** @type {boolean} */
+mojo.MediaStatus.prototype.can_seek;
+
+
+/** @type {?mojo.MediaStatus.PlayState} */
+mojo.MediaStatus.prototype.play_state;
+
+
+/** @type {boolean} */
+mojo.MediaStatus.prototype.is_muted;
+
+
+/** @type {number} */
+mojo.MediaStatus.prototype.volume;
+
+
+/** @type {?mojo.TimeDelta} */
+mojo.MediaStatus.prototype.duration;
+
+
+/** @type {?mojo.TimeDelta} */
+mojo.MediaStatus.prototype.current_time;
+
+
+/** @type {mojo.HangoutsMediaStatusExtraData} */
+mojo.MediaStatus.prototype.hangouts_extra_data;
+
+
+
+/**
+ * @param {!Object} values
+ * @constructor
+ * @struct
+ */
+mojo.HangoutsMediaStatusExtraData = function(values) {};
+
+
+/** @type {boolean} */
+mojo.HangoutsMediaStatusExtraData.prototype.local_present;
+
+
+
+/**
+ * @param {!Object} values An object mapping from property names to values.
+ * @constructor
+ * @struct
+ */
+mojo.Origin = function(values) {};
+
+
+/** @type {string} */
+mojo.Origin.prototype.scheme;
+
+
+/** @type {string} */
+mojo.Origin.prototype.host;
+
+
+/** @type {number} */
+mojo.Origin.prototype.port;
+
+
+/** @type {string} */
+mojo.Origin.prototype.suborigin;
+
+
+/** @type {boolean} */
+mojo.Origin.prototype.unique;
+
+
+
+/** @constructor */
+mojo.RouteControllerType = function() {};
+/** @type {mojo.RouteControllerType} */
+mojo.RouteControllerType.kNone;
+/** @type {mojo.RouteControllerType} */
+mojo.RouteControllerType.kGeneric;
+/** @type {mojo.RouteControllerType} */
+mojo.RouteControllerType.kHangouts;
+/** @type {mojo.RouteControllerType} */
+mojo.RouteControllerType.kMirroring;
+
+
+// Mojom interfaces.
+
+/**
+ * @constructor
+ * @struct
+ */
+mojo.MediaStatusObserverPtr = function() {};
+
+
+/** @param {!mojo.MediaStatus} status */
+mojo.MediaStatusObserverPtr.prototype.onMediaStatusUpdated = function(status) {
+};
+
+
+/** @type {!mojo.InterfacePtrController} */
+mojo.MediaStatusObserverPtr.prototype.ptr;
+
+
+
+/** @type {!Object} */
+mojo.MediaController;
+
+
+/** @type {!Object} */
+mojo.HangoutsMediaRouteController;
+
+
+/** @constructor */
+mojo.MediaRouteProviderConfig = function() {};
+
+
+/** @type {boolean} */
+mojo.MediaRouteProviderConfig.prototype.enable_dial_discovery;
+
+
+/** @type {boolean} */
+mojo.MediaRouteProviderConfig.prototype.enable_dial_sink_query;
+
+
+/** @type {boolean} */
+mojo.MediaRouteProviderConfig.prototype.enable_cast_discovery;
+
+
+
+/** @constructor */
+mojo.RemotingStopReason = function() {};
+/** @type {!mojo.RemotingStopReason} */
+mojo.RemotingStopReason.ROUTE_TERMINATED;
+/** @type {!mojo.RemotingStopReason} */
+mojo.RemotingStopReason.USER_DISABLED;
+
+
+
+/** @constructor */
+mojo.RemotingSinkFeature = function() {};
+
+
+
+/** @constructor */
+mojo.RemotingSinkAudioCapability = function() {};
+/** @type {!mojo.RemotingSinkAudioCapability} */
+mojo.RemotingSinkAudioCapability.CODEC_BASELINE_SET;
+/** @type {!mojo.RemotingSinkAudioCapability} */
+mojo.RemotingSinkAudioCapability.CODEC_AAC;
+/** @type {!mojo.RemotingSinkAudioCapability} */
+mojo.RemotingSinkAudioCapability.CODEC_OPUS;
+
+
+
+/** @constructor */
+mojo.RemotingSinkVideoCapability = function() {};
+/** @type {!mojo.RemotingSinkVideoCapability} */
+mojo.RemotingSinkVideoCapability.SUPPORT_4K;
+/** @type {!mojo.RemotingSinkVideoCapability} */
+mojo.RemotingSinkVideoCapability.CODEC_BASELINE_SET;
+/** @type {!mojo.RemotingSinkVideoCapability} */
+mojo.RemotingSinkVideoCapability.CODEC_H264;
+/** @type {!mojo.RemotingSinkVideoCapability} */
+mojo.RemotingSinkVideoCapability.CODEC_VP8;
+/** @type {!mojo.RemotingSinkVideoCapability} */
+mojo.RemotingSinkVideoCapability.CODEC_VP9;
+/** @type {!mojo.RemotingSinkVideoCapability} */
+mojo.RemotingSinkVideoCapability.CODEC_HEVC;
+
+
+
+/**
+ * @constructor
+ * @struct
+ */
+mojo.RemotingSinkMetadata = function() {};
+
+
+/** @type {!Array<mojo.RemotingSinkFeature>} */
+mojo.RemotingSinkMetadata.prototype.features;
+
+
+/** @type {!Array<mojo.RemotingSinkAudioCapability>} */
+mojo.RemotingSinkMetadata.prototype.audio_capabilities;
+
+
+/** @type {!Array<mojo.RemotingSinkVideoCapability>} */
+mojo.RemotingSinkMetadata.prototype.video_capabilities;
+
+
+/** @type {!string} */
+mojo.RemotingSinkMetadata.prototype.friendly_name;
+
+
+
+/** @type {!Object} */
+mojo.MirrorServiceRemoter;
+
+
+
+/**
+ * @constructor
+ */
+mojo.MirrorServiceRemoterPtr = function() {};
+
+
+/** @type {!mojo.InterfacePtrController} */
+mojo.MirrorServiceRemoterPtr.prototype.ptr;
+
+
+
+/**
+ * @constructor
+ */
+mojo.MirrorServiceRemotingSourcePtr = function() {};
+
+
+/** @param {!mojo.RemotingSinkMetadata} capabilities */
+mojo.MirrorServiceRemotingSourcePtr.prototype.onSinkAvailable = function(
+ capabilities) {};
+
+/** @param {!Uint8Array} message */
+mojo.MirrorServiceRemotingSourcePtr.prototype.onMessageFromSink = function(
+ message) {};
+
+
+/** @param {!mojo.RemotingStopReason} reason */
+mojo.MirrorServiceRemotingSourcePtr.prototype.onStopped = function(reason) {};
+
+
+/** Notifies the remoting source when streaming error occurs. */
+mojo.MirrorServiceRemotingSourcePtr.prototype.onError = function() {};
+
+
+/** @type {!mojo.InterfacePtrController} */
+mojo.MirrorServiceRemotingSourcePtr.prototype.ptr;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/persistent_data.js b/chromium/chrome/browser/resources/media_router/extension/src/persistent_data.js
new file mode 100644
index 00000000000..a7490f357e6
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/persistent_data.js
@@ -0,0 +1,463 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview API for classes to save data and cleanup before the event page
+ * is shut down.
+ *
+ * Temporary data is removed once the extension version is changed, and thus
+ * objects should never write anything to temporary data that needs to survive
+ * an extension update.
+ *
+ * Persistent data is retained across extension versions and browser restarts;
+ * it can be removed when the user clears local storage from their profile. Do
+ * not store any sensitive or per-route data persistently.
+ */
+
+goog.provide('mr.PersistentData');
+goog.provide('mr.PersistentDataManager');
+goog.require('mr.Logger');
+
+
+
+/**
+ * @interface
+ */
+mr.PersistentData = class {};
+
+
+/**
+ * Alias for localStorage to deal with broken Storage interface.
+ * @const @private {!Object}
+ */
+mr.PersistentData.storageObj_ = /** @type {!Object} */ (window.localStorage);
+
+
+/**
+ * Get the unique name of the object instance that has data to be saved.
+ * The name is used to isolate data from different objects.
+ * @return {string}
+ */
+mr.PersistentData.prototype.getStorageKey;
+
+
+/**
+ * Invoked by persistent data manager when an object registers itself with the
+ * manager. The object should load its saved data in this method.
+ */
+mr.PersistentData.prototype.loadSavedData;
+
+
+/**
+ * Implements this method to cleanup and return data that needs to be saved
+ * before the event page is shut down. The method should normally return a one-
+ * or two-element array of temporary and optional persistent data to save.
+ *
+ * Temporary data is cleared on browser restart or extension version change.
+ * Persistent data is retained until local storage is cleared by the browser
+ * profile.
+ *
+ * Any Objects must be serializable with JSON.stringify.
+ *
+ * The implementation of getData should not make any asynchronous
+ * calls; otherwise, there is no guarantee that asynchronous calls can finish.
+ *
+ * @return {!Array<!Object>} A one- or two-element array of data that needs to
+ * be saved. The first element is temporary data and the second element is
+ * persistent data. Return an empty array if there is no data to save. If
+ * only persistent data needs to be persisted, pass in undefined for the
+ * first element in the two-element array.
+ */
+mr.PersistentData.prototype.getData;
+
+
+/**
+ * The total number of characters that can be stored in localStorage,
+ * approximately.
+ * @const {number}
+ */
+mr.PersistentDataManager.QUOTA_CHARS = 5200000;
+
+
+/**
+ * The total number of characters used by persistent data. Note that writes that
+ * access localStorage directly may not be counted here.
+
+ * @private {number}
+ */
+mr.PersistentDataManager.charsUsed_ = 0;
+
+
+/**
+ * @param {!mr.PersistentData} obj The object that may have temporary data.
+ * @return {T} The data saved before. Null if no data is saved.
+ * @template T
+ */
+mr.PersistentDataManager.getTemporaryData = function(obj) {
+ const data = window.localStorage.getItem(
+ mr.PersistentDataManager.getStorageKey_(obj, false));
+ return data ? JSON.parse(data) : null;
+};
+
+
+/**
+ * @param {!mr.PersistentData} obj The object that may have peristent data.
+ * @return {T} The data saved before. Null if no data is saved.
+ * @template T
+ */
+mr.PersistentDataManager.getPersistentData = function(obj) {
+ const data = window.localStorage.getItem(
+ mr.PersistentDataManager.getStorageKey_(obj, true));
+ return data ? JSON.parse(data) : null;
+};
+
+
+/**
+ * Registers an object so that it gets informed about onSuspend events.
+ *
+ * @param {!mr.PersistentData} obj An object that has data to save.
+ */
+mr.PersistentDataManager.register = function(obj) {
+ if (mr.PersistentDataManager.dataInstances_.has(obj.getStorageKey())) {
+ throw Error('Duplicate instance name ' + obj.getStorageKey());
+ }
+ mr.PersistentDataManager.dataInstances_.set(obj.getStorageKey(), obj);
+ obj.loadSavedData();
+};
+
+
+/**
+ * Un-Registers an object from being informed of onSuspend/Resume events.
+ *
+ * @param {!mr.PersistentData} obj An object to remove from tracking.
+ */
+mr.PersistentDataManager.unregister = function(obj) {
+ mr.PersistentDataManager.dataInstances_.delete(obj.getStorageKey());
+};
+
+
+/**
+ * @param {string} mrInstanceId The media router instance ID, which stays the
+ * same till Chrome restarts.
+ */
+mr.PersistentDataManager.initialize = function(mrInstanceId) {
+ let otherChars = 0;
+ for (let key of Object.keys(mr.PersistentData.storageObj_)) {
+ const itemSize = key.length + window.localStorage.getItem(key).length;
+ if (key.startsWith(mr.PersistentDataManager.KEY_PREFIX_)) {
+ mr.PersistentDataManager.charsUsed_ += itemSize;
+ } else {
+ otherChars += itemSize;
+ }
+ }
+ mr.PersistentDataManager.mrInstanceId_ = mrInstanceId;
+ if (mr.PersistentDataManager.isVersionChanged_() ||
+ mr.PersistentDataManager.isChromeReloaded(mrInstanceId)) {
+ mr.PersistentDataManager.removeTemporary_();
+ }
+ mr.PersistentDataManager.logger_.info(
+ 'initialize: ' + mr.PersistentDataManager.charsUsed_ + ' chars used, ' +
+ otherChars + ' other chars');
+ chrome.runtime.onSuspend.addListener(mr.PersistentDataManager.onSuspend_);
+};
+
+
+/**
+ * @private {?string}
+ */
+mr.PersistentDataManager.mrInstanceId_ = null;
+
+
+/**
+ * @const {mr.Logger}
+ * @private
+ */
+mr.PersistentDataManager.logger_ =
+ mr.Logger.getInstance('mr.PersistentDataManager');
+
+
+/**
+ * A map from object's instance name to the object.
+ * @private @const {!Map<string, !mr.PersistentData>}
+ */
+mr.PersistentDataManager.dataInstances_ = new Map();
+
+
+/** @private @const {string} */
+mr.PersistentDataManager.KEY_PREFIX_ = 'mr.';
+
+
+/**
+ * @param {!mr.PersistentData} obj
+ * @param {boolean} persistent
+ * @return {string}
+ * @private
+ */
+mr.PersistentDataManager.getStorageKey_ = function(obj, persistent) {
+ return mr.PersistentDataManager.KEY_PREFIX_ +
+ (persistent ? 'persistent.' : 'temp.') + obj.getStorageKey();
+};
+
+
+/**
+ * Checks if the extension version has changed.
+ * @return {boolean} True if current extension has a different version as
+ * the saved version.
+ * @private
+ */
+mr.PersistentDataManager.isVersionChanged_ = function() {
+ return !!window.localStorage.getItem('version') &&
+ window.localStorage.getItem('version') !==
+ chrome.runtime.getManifest().version;
+};
+
+
+/**
+ * Checks if the chrome has been reloaded since the last time the extension is
+ * loaded.
+ * @param {string} mrInstanceId The media router instance ID, which stays the
+ * same till Chrome restarts.
+ * @return {boolean} True if Chrome was reloaded.
+ */
+mr.PersistentDataManager.isChromeReloaded = function(mrInstanceId) {
+ return !!window.localStorage.getItem('mrInstanceId') &&
+ window.localStorage.getItem('mrInstanceId') !== mrInstanceId;
+};
+
+
+/**
+ * Handles onSuspend event.
+ * @private
+ */
+mr.PersistentDataManager.onSuspend_ = function() {
+ mr.PersistentDataManager.logger_.info('onSuspend');
+
+ mr.PersistentDataManager.write(
+ 'version', chrome.runtime.getManifest().version);
+ if (mr.PersistentDataManager.mrInstanceId_) {
+ mr.PersistentDataManager.write(
+ 'mrInstanceId', mr.PersistentDataManager.mrInstanceId_);
+ }
+ const logManager = mr.PersistentDataManager.dataInstances_.get('LogManager');
+ for (const [key, obj] of mr.PersistentDataManager.dataInstances_) {
+ if (obj != logManager) {
+ mr.PersistentDataManager.saveData(
+ /** @type {!mr.PersistentData} */ (obj));
+ }
+ }
+ // Save the data for LogManager last, so that we save the logs generated
+ // during saveData() calls.
+ if (logManager) {
+ mr.PersistentDataManager.saveData(logManager);
+ }
+};
+
+
+/**
+ * Save PersistentData object to local storage.
+ * @param {!mr.PersistentData} obj
+ */
+mr.PersistentDataManager.saveData = function(obj) {
+ try {
+ const data = obj.getData();
+ if (data && data[0] != undefined) {
+ mr.PersistentDataManager.write(
+ mr.PersistentDataManager.getStorageKey_(obj, false),
+ JSON.stringify(data[0]));
+ }
+ if (data && data[1] != undefined) {
+ mr.PersistentDataManager.write(
+ mr.PersistentDataManager.getStorageKey_(obj, true),
+ JSON.stringify(data[1]));
+ }
+ } catch (e) {
+ mr.PersistentDataManager.logger_.error(
+ `Error while saving data for ${obj.getStorageKey()}: ${e.message}`);
+ }
+};
+
+
+/**
+ * Writes value to localStorage under key. If the value is too large to fit
+ * into the remaining localStorage quota, temporary data is first removed. If
+ * the value still won't fit, an exception is thrown.
+
+ * @param {string} key The localStorage key.
+ * @param {string} value The value to write.
+ */
+mr.PersistentDataManager.write = function(key, value) {
+ const dm = mr.PersistentDataManager;
+ let sizeDelta = 0;
+ const currentValue = window.localStorage.getItem(key);
+ if (currentValue != null) {
+ sizeDelta = value.length - currentValue.length;
+ } else {
+ sizeDelta = key.length + value.length;
+ }
+
+ if (dm.charsUsed_ + sizeDelta > dm.QUOTA_CHARS) {
+ mr.PersistentDataManager.logger_.warning(
+ 'Unable to write ' + sizeDelta + ' chars');
+ dm.removeTemporary_();
+ }
+
+ if (dm.charsUsed_ + sizeDelta > dm.QUOTA_CHARS) {
+ dm.logger_.error(
+ 'Unable to write ' + sizeDelta + ' chars after clearing temporary');
+ throw Error(
+ `Setting the value of '${key}' would exceed the quota, ` +
+ 'according to accounting.');
+ }
+
+ try {
+ window.localStorage.setItem(key, value);
+ } catch (error) {
+ throw Error(
+ `Setting the value of '${key}' would exceed the quota, ` +
+ 'according to the browser.');
+ }
+ // Adjusting dm.charsUsed_ only after the call to setItem() has succeeded.
+ dm.charsUsed_ += sizeDelta;
+};
+
+
+/**
+ * Writes a Blob to localStorage under the given key, making space-efficient use
+ * of localStorage by encoding two of the Blob's bytes into each DOMString
+ * character. Use readBlob() to read the value back.
+ * @param {string} key The localStorage key.
+ * @param {!Blob} value The value to write.
+ * @return {!Promise<void>} Resolves once the Blob has been written; or rejects
+ * if it won't fit.
+ */
+mr.PersistentDataManager.writeBlob = function(key, value) {
+ // The byte size needs to be a multiple of two, since each string character
+ // code is a 16-bit unsigned integer. Thus, append padding byte(s) to the end
+ // of the Blob. These will also be used to indicate whether the original Blob
+ // was of even or odd length when reading back later.
+ if (value.size % 2 == 0) {
+ value = new Blob([value, new Uint8Array([0, 0])]);
+ } else {
+ value = new Blob([value, new Uint8Array([1])]);
+ }
+
+ return new Promise((resolve, reject) => {
+ // Use FileReader to gain access to the Blob content via an ArrayBuffer.
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ if (reader.error) {
+ reject(reader.error);
+ return;
+ }
+
+ try {
+ const buffer = /** @type {!ArrayBuffer} */ (reader.result);
+
+ // Convert the buffer bytes into a string, storing two bytes in each of
+ // the string's characters for space efficiency. This is done in batches
+ // to avoid smashing the call stack when calling String.fromCharCode().
+ const batchSize = 8192;
+ const pieces = [];
+ for (let pos = 0, end = buffer.byteLength; pos < end;
+ pos += batchSize) {
+ // Note: The byteLength will always be an even number since all input
+ // values to the following expression must be even numbers:
+ const byteLengthOfChunk = Math.min(end - pos, batchSize);
+ pieces.push(String.fromCharCode.apply(
+ null, new Uint16Array(buffer, pos, byteLengthOfChunk / 2)));
+ }
+
+ // Finally, join the pieces into a single string and attempt to store
+ // the string using the quota management heuristics in write().
+ mr.PersistentDataManager.write(key, pieces.join(''));
+
+ resolve();
+ } catch (error) {
+ reject(error);
+ }
+ };
+ reader.readAsArrayBuffer(value);
+ });
+};
+
+
+/**
+ * Reads a Blob from localStorage under the given key. Returns null if it does
+ * not exist.
+ * @param {string} key The localStorage key.
+ * @param {Object=} blobOptions The options for the reconstituted Blob (e.g.,
+ * {'type': 'application/gzip'}).
+ * @return {?Blob}
+ */
+mr.PersistentDataManager.readBlob = function(key, blobOptions) {
+ const asString = window.localStorage.getItem(key);
+ if (asString == null || asString.length < 1) {
+ return null;
+ }
+ const charCodes = new Uint16Array(asString.length);
+ for (let i = 0; i < asString.length; ++i) {
+ charCodes[i] = asString.charCodeAt(i);
+ }
+ // Determine, from the last byte value, whether the original Blob was of even
+ // or odd length (see writeBlob()). Create a Blob from a view of all but the
+ // padding byte(s) at the end of the buffer.
+ const buffer = charCodes.buffer;
+ const flagByte = (new Uint8Array(buffer, buffer.byteLength - 1, 1))[0];
+ const viewOfPayload =
+ new Uint8Array(buffer, 0, buffer.byteLength - ((flagByte == 0) ? 2 : 1));
+ return new Blob([viewOfPayload], blobOptions);
+};
+
+
+/**
+ * Removes temporary data.
+ * @private
+ */
+mr.PersistentDataManager.removeTemporary_ = function() {
+ for (let key of Object.keys(mr.PersistentData.storageObj_)) {
+ if (key.startsWith(mr.PersistentDataManager.KEY_PREFIX_ + 'temp.')) {
+ mr.PersistentDataManager.charsUsed_ -=
+ (key.length + window.localStorage.getItem(key).length);
+ delete window.localStorage[key];
+ }
+ }
+ mr.PersistentDataManager.logger_.info(
+ 'removeTemporary_: ' + mr.PersistentDataManager.charsUsed_ +
+ ' chars used');
+};
+
+
+/**
+ * Removes all data.
+ * @private
+ */
+mr.PersistentDataManager.removeAll_ = function() {
+ for (let key of Object.keys(mr.PersistentData.storageObj_)) {
+ if (key.startsWith(mr.PersistentDataManager.KEY_PREFIX_))
+ window.localStorage.removeItem(key);
+ }
+ mr.PersistentDataManager.charsUsed_ = 0;
+};
+
+
+/**
+ * Clears internal state and remove all saved data.
+ * Utility method for unit test.
+
+ */
+mr.PersistentDataManager.clear = function() {
+ mr.PersistentDataManager.removeAll_();
+ mr.PersistentDataManager.dataInstances_.clear();
+};
+
+
+/**
+ * Simulates suspend.
+ * Utility method for unit test.
+
+ */
+mr.PersistentDataManager.suspendForTest = function() {
+ mr.PersistentDataManager.onSuspend_();
+ mr.PersistentDataManager.dataInstances_.clear();
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/persistent_data_test.js b/chromium/chrome/browser/resources/media_router/extension/src/persistent_data_test.js
new file mode 100644
index 00000000000..6708e9fdb4f
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/persistent_data_test.js
@@ -0,0 +1,361 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly('persistent_data_test');
+
+goog.require('mr.PersistentData');
+goog.require('mr.PersistentDataManager');
+goog.require('mr.UnitTestUtils');
+
+
+/**
+ * @implements {mr.PersistentData}
+ * @private
+ */
+DummyData_ = class {
+ /**
+ * @param {string} id
+ */
+ constructor(id) {
+ /** @private {string} */
+ this.id_ = id;
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ return [];
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'dummy-data' + this.id_;
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {}
+};
+
+
+describe('Tests PersistentDataManager', () => {
+ let dataInstance1;
+ let dataInstance2;
+ let onSuspendListener;
+ let version;
+ let mrInstanceId;
+ const originalQuota = mr.PersistentDataManager.QUOTA_CHARS;
+
+ beforeEach(() => {
+ window.localStorage.clear();
+ mr.PersistentDataManager.charsUsed_ = 0;
+ dataInstance1 = new DummyData_('1');
+ dataInstance2 = new DummyData_('2');
+ version = '1.0';
+ mrInstanceId = '123';
+ mr.UnitTestUtils.mockChromeApi();
+ chrome.runtime.onSuspend = {
+ addListener: l => {
+ onSuspendListener = l;
+ }
+ };
+ chrome.runtime.getManifest = () => ({'version': version});
+ mr.PersistentDataManager.initialize(mrInstanceId);
+ dataInstance1.loadSavedData = jasmine.createSpy('loadSavedData');
+ dataInstance2.loadSavedData = jasmine.createSpy('loadSavedData');
+ mr.PersistentDataManager.register(dataInstance1);
+ mr.PersistentDataManager.register(dataInstance2);
+ expect(dataInstance1.loadSavedData).toHaveBeenCalled();
+ expect(dataInstance2.loadSavedData).toHaveBeenCalled();
+ });
+
+ afterEach(() => {
+ mr.PersistentDataManager.clear();
+ mr.PersistentDataManager.QUOTA_CHARS = originalQuota;
+ mr.UnitTestUtils.restoreChromeApi();
+ });
+
+ it('returns null with no saved data', () => {
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toBe(null);
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance2)).toBe(null);
+ });
+
+ describe('handles onSuspend', () => {
+ it('with one instance with data', () => {
+ dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
+ dataInstance2.getData = () => null;
+ onSuspendListener();
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
+ 'd': 1
+ });
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance1))
+ .toEqual({'e': 1});
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance2))
+ .toBe(null);
+ expect(mr.PersistentDataManager.charsUsed_).toBe(83);
+ });
+
+ it('with two instances with data', () => {
+ dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
+ dataInstance2.getData = () => [{'d': 2}, {'e': 2}];
+ onSuspendListener();
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
+ 'd': 1
+ });
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance1))
+ .toEqual({'e': 1});
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance2)).toEqual({
+ 'd': 2
+ });
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance2))
+ .toEqual({'e': 2});
+ expect(mr.PersistentDataManager.charsUsed_).toBe(141);
+ });
+
+ it('with no instance with data', () => {
+ dataInstance1.getData = () => null;
+ dataInstance2.getData = () => null;
+ onSuspendListener();
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance1))
+ .toBe(null);
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance2))
+ .toBe(null);
+ expect(mr.PersistentDataManager.charsUsed_).toBe(25);
+ });
+
+ it('with an instance with a circular dependency', () => {
+ const data1 = {};
+ data1.data = data1;
+ dataInstance1.getData = () => [{'d': data1}, {'e': data1}];
+ dataInstance2.getData = () => [{'d': 2}, {'e': 2}];
+ onSuspendListener();
+
+ // If there is a circular dependency of objects, that data cannot be
+ // converted into JSON or saved. However other data should still be saved.
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance2)).toEqual({
+ 'd': 2
+ });
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance2))
+ .toEqual({'e': 2});
+ });
+ });
+
+ it('Test saveData', () => {
+ dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
+ dataInstance2.getData = () => [{'d': 2}, {'e': 2}];
+ const dataInstance3 = new DummyData_('3');
+ dataInstance3.getData = () => [false, {'e': 3}];
+ const dataInstance4 = new DummyData_('4');
+ dataInstance4.getData = () => [undefined, 0];
+
+ // Note that dataInstance2 is not saved.
+ mr.PersistentDataManager.saveData(dataInstance1);
+ mr.PersistentDataManager.saveData(dataInstance3);
+ mr.PersistentDataManager.saveData(dataInstance4);
+
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
+ 'd': 1
+ });
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
+ 'e': 1
+ });
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance2)).toBeNull();
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance2))
+ .toBeNull();
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance3))
+ .toEqual(false);
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance3)).toEqual({
+ 'e': 3
+ });
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance4)).toBeNull();
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance4))
+ .toEqual(0);
+ });
+
+ it('Test unregister', () => {
+ dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
+ dataInstance2.getData = () => [{'d': 2}, {'e': 2}];
+ const dataInstance3 = new DummyData_('3');
+ dataInstance3.getData = () => [{'d': 3}, {'e': 3}];
+ mr.PersistentDataManager.register(dataInstance3);
+ onSuspendListener();
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance3)).toEqual({
+ 'd': 3
+ });
+ mr.PersistentDataManager.unregister(dataInstance3);
+ // Get data should not be called again.
+ dataInstance3.getData = () => {
+ fail();
+ };
+ onSuspendListener();
+ });
+
+ it('handles version change', () => {
+ dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
+ onSuspendListener();
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
+ 'd': 1
+ });
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
+ 'e': 1
+ });
+ version = '1.1';
+ expect(mr.PersistentDataManager.isChromeReloaded(mrInstanceId)).toBe(false);
+ mr.PersistentDataManager.initialize(mrInstanceId);
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toBe(null);
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
+ 'e': 1
+ });
+ });
+
+ it('handles mrInstanceId change', () => {
+ dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
+ onSuspendListener();
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
+ 'd': 1
+ });
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
+ 'e': 1
+ });
+ mrInstanceId = '321';
+ expect(mr.PersistentDataManager.isChromeReloaded(mrInstanceId)).toBe(true);
+ mr.PersistentDataManager.initialize(mrInstanceId);
+ expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toBe(null);
+ expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
+ 'e': 1
+ });
+ });
+
+ describe('allows writes', () => {
+ it('of small values', () => {
+ mr.PersistentDataManager.write('mr.temp.Buckaroo', 'Bonzai');
+ expect(window.localStorage.getItem('mr.temp.Buckaroo')).toBe('Bonzai');
+ expect(mr.PersistentDataManager.charsUsed_).toBe(22);
+ });
+
+ it('of large values over quota', () => {
+ mr.PersistentDataManager.QUOTA_CHARS = 100;
+ [1, 2, 3, 4].forEach(index => {
+ mr.PersistentDataManager.write(
+ 'mr.temp.' + index, 'Only the dead have seen the end of war.');
+ });
+ expect(mr.PersistentDataManager.charsUsed_).toBe(96);
+ // Normally this would go over QUOTA_CHARS, but clearing temporary
+ // values allows it to succeed.
+ mr.PersistentDataManager.write(
+ 'mr.persistent.5', 'Courage is knowing what not to fear.');
+ expect(mr.PersistentDataManager.charsUsed_).toBe(51);
+ [1, 2, 3, 4].forEach(index => {
+ expect(window.localStorage.getItem('mr.temp.' + index)).toBeNull();
+ });
+ expect(window.localStorage.getItem('mr.persistent.5'))
+ .toBe('Courage is knowing what not to fear.');
+ });
+ });
+
+ describe('stores and reads Blobs', () => {
+ const dm = mr.PersistentDataManager;
+ const testKey = 'test';
+
+ const allUint16Values = [];
+ for (let i = 0; i < (1 << 16); ++i) {
+ allUint16Values.push(i);
+ }
+
+ // Takes an array of byte values, creates a Blob, stores and retrieves it
+ // back, and then maps the bytes of the Blob back to an array of byte
+ // values.
+ function writeThenReadByteValues(values) {
+ return new Promise((resolve, reject) => {
+ dm.writeBlob(testKey, new Blob([new Uint8Array(values)])).then(() => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ if (reader.error) {
+ reject(reader.error);
+ } else {
+ resolve(Array.from(new Uint8Array(reader.result)));
+ }
+ };
+ reader.readAsArrayBuffer(dm.readBlob(testKey));
+ }, reject);
+ });
+ }
+
+ // Same as writeThenReadByteValues(), except operate on an array of uint16.
+ function writeThenReadShortValues(values) {
+ return new Promise((resolve, reject) => {
+ dm.writeBlob(testKey, new Blob([new Uint16Array(values)])).then(() => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ if (reader.error) {
+ reject(reader.error);
+ } else {
+ resolve(Array.from(new Uint16Array(reader.result)));
+ }
+ };
+ reader.readAsArrayBuffer(dm.readBlob(testKey));
+ }, reject);
+ });
+ }
+
+ it('of zero size', done => {
+ writeThenReadByteValues([]).then(values => {
+ expect(values).toEqual([]);
+ done();
+ }, fail);
+ });
+
+ it('of even size', done => {
+ writeThenReadByteValues([42, 1]).then(values => {
+ expect(values).toEqual([42, 1]);
+ done();
+ }, fail);
+ });
+
+ it('of odd size', done => {
+ writeThenReadByteValues([42, 1, 0]).then(values => {
+ expect(values).toEqual([42, 1, 0]);
+ done();
+ }, fail);
+ });
+
+ it('containing all possible uint16 values', done => {
+ const original = allUint16Values.concat(allUint16Values.reverse());
+ writeThenReadShortValues(original).then(values => {
+ expect(values).toEqual(original);
+ done();
+ }, fail);
+ });
+
+ it('that are just within quota', done => {
+ // Note on quota space needed: The string length of the key plus value
+ // must be available. A blob of 7 uint16 values will have a byte size of
+ // 14. The impl will add two padding bytes to the end, making the total 16
+ // bytes. Thus, 16/2 = 8 chars of quota must be available for the value.
+ // In total, 4 + 8 (key + value) chars must be available.
+ mr.PersistentDataManager.QUOTA_CHARS =
+ mr.PersistentDataManager.charsUsed_ + testKey.length + 8;
+ const original = allUint16Values.slice(0, 7);
+ writeThenReadShortValues(original).then(values => {
+ expect(values).toEqual(original);
+ done();
+ }, fail);
+ });
+
+ it('that exceed quota', done => {
+ // See note above on how quota accounting works.
+ mr.PersistentDataManager.QUOTA_CHARS =
+ mr.PersistentDataManager.charsUsed_ + testKey.length + 8;
+ const original = allUint16Values.slice(0, 8);
+ writeThenReadShortValues(original).then(fail, error => {
+ expect(dm.readBlob(testKey)).toBeNull();
+ done();
+ });
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/presentation.js b/chromium/chrome/browser/resources/media_router/extension/src/presentation.js
new file mode 100644
index 00000000000..07ed021276b
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/presentation.js
@@ -0,0 +1,191 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Presentation API.
+ * @externs
+ * @see http://w3c.github.io/presentation-api
+ * @see https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/modules/presentation/
+ */
+
+
+
+/**
+ * @interface
+ * @see http://w3c.github.io/presentation-api/#idl-def-presentationconnection
+ */
+function PresentationConnection() {}
+
+
+/**
+ * @type {string}
+ * @see http://w3c.github.io/presentation-api/#dom-presentationconnection-id
+ */
+PresentationConnection.prototype.id;
+
+
+/**
+ * @type {string}
+ * @see http://w3c.github.io/presentation-api/#dom-presentationconnection-url
+ */
+PresentationConnection.prototype.url;
+
+
+/**
+ * @type {string}
+ * @see http://w3c.github.io/presentation-api/#dom-presentationconnection-state
+ */
+PresentationConnection.prototype.state;
+
+
+/**
+ * @type {?function(!MessageEvent)}
+ * @see http://w3c.github.io/presentation-api/#dom-presentationconnection-onmessage
+ */
+PresentationConnection.prototype.onmessage;
+
+
+/**
+ * @type {?function(!PresentationConnectionCloseEvent)}
+ * @see http://w3c.github.io/presentation-api/#dom-presentationconnection-onclose
+ */
+PresentationConnection.prototype.onclose;
+
+
+/**
+ * @type {?function()}
+ * @see https://www.w3.org/TR/presentation-api/#dom-presentationconnection-onterminate
+ */
+PresentationConnection.prototype.onterminate;
+
+
+/**
+ * @param {string} message
+ * @see http://w3c.github.io/presentation-api/#dom-presentationconnection-send
+ */
+PresentationConnection.prototype.send = function(message) {};
+
+/**
+ * @see https://w3c.github.io/presentation-api/#dom-presentationconnection-terminate
+ */
+PresentationConnection.prototype.terminate = function() {};
+
+/**
+ * @see http://w3c.github.io/presentation-api/#dom-presentationconnection-close
+ */
+PresentationConnection.prototype.close = function() {};
+
+
+
+/**
+ * @constructor
+ * @extends {Event}
+ */
+function PresentationConnectionCloseEvent() {}
+
+
+/** @type {string} */
+PresentationConnectionCloseEvent.prototype.reason;
+
+
+/** @type {string} */
+PresentationConnectionCloseEvent.prototype.message;
+
+
+
+/**
+ * @constructor
+ * @extends {Event}
+ * @see http://w3c.github.io/presentation-api/#presentationavailability
+ */
+function PresentationAvailability() {}
+
+
+/** @type {boolean} */
+PresentationAvailability.prototype.value;
+
+
+/** @type {?function()} */
+PresentationAvailability.prototype.onchange;
+
+
+
+/**
+ * @constructor
+ * @extends {Event}
+ * @see http://w3c.github.io/presentation-api/#availablechangeevent
+ */
+function AvailableChangeEvent() {}
+
+
+/**
+ * @type {boolean}
+ */
+AvailableChangeEvent.prototype.available;
+
+
+
+/**
+ * @constructor
+ * @extends {Event}
+ */
+function PresentationConnectionAvailableEvent() {}
+
+
+/**
+ * @type {!PresentationConnection}
+ */
+PresentationConnectionAvailableEvent.prototype.connection;
+
+
+
+/**
+ * @param {string|!Array<string>} url
+ * @constructor
+ */
+function PresentationRequest(url) {}
+
+
+/**
+ * @return {!Promise<!PresentationConnection>}
+ */
+PresentationRequest.prototype.start = function() {};
+
+
+/**
+ * @param {string} presentationId
+ * @return {!Promise<!PresentationConnection>}
+ */
+PresentationRequest.prototype.reconnect = function(presentationId) {};
+
+
+/**
+ * @return {!Promise<!PresentationAvailability>}
+ */
+PresentationRequest.prototype.getAvailability = function() {};
+
+
+/**
+ * @type {?function(!PresentationConnectionAvailableEvent)}
+ */
+PresentationRequest.prototype.onconnectionavailable;
+
+
+
+/**
+ * @interface
+ */
+function Presentation() {}
+
+
+/**
+ * @type {PresentationRequest}
+ */
+Presentation.prototype.defaultRequest;
+
+
+/**
+ * @type {!Presentation}
+ */
+Navigator.prototype.presentation;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/presentation_services/cloud_webrtc/webrtc_presentation_session.js b/chromium/chrome/browser/resources/media_router/extension/src/presentation_services/cloud_webrtc/webrtc_presentation_session.js
new file mode 100644
index 00000000000..907ac9b5aab
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/presentation_services/cloud_webrtc/webrtc_presentation_session.js
@@ -0,0 +1,192 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Implementation of PresentationSession that uses the WebRTC
+ * PeerConnection.
+ */
+goog.provide('mr.presentation.webrtc.CloudWebRtcSession');
+
+goog.require('mr.MessagePortService');
+goog.require('mr.PromiseResolver');
+goog.require('mr.presentation.Session');
+goog.require('mr.webrtc.Message');
+goog.require('mr.webrtc.MessageType');
+goog.require('mr.webrtc.OfferMessageData');
+goog.require('mr.webrtc.PeerConnection');
+
+goog.scope(function() {
+
+
+
+
+/**
+ * Constructs a new WebRTC presentation session.
+ * @implements {mr.presentation.Session}
+ */
+mr.presentation.webrtc.CloudWebRtcSession = class {
+ /**
+ * @param {!mr.Route} route
+ * @param {!string} sourceUrn
+ */
+ constructor(route, sourceUrn) {
+ /** @type {!mr.Route} */
+ this.route = route;
+
+ /** @type {!string} */
+ this.sourceUrn = sourceUrn;
+
+ /** @private {!mr.PromiseResolver<!mr.webrtc.PeerConnection>} */
+ this.peerConnectionResolver_ = new mr.PromiseResolver();
+
+ /** @private {!Promise<!mr.webrtc.PeerConnection>} */
+ this.peerConnection_ = this.peerConnectionResolver_.promise;
+
+ /** @private {!mr.MessagePort} */
+ this.messagePort_ =
+ mr.MessagePortService.getService().getInternalMessenger(route.id);
+
+ /** @private {!mr.PromiseResolver<!mr.Route>} */
+ this.startResolver_ = new mr.PromiseResolver();
+
+ /** @private {boolean} */
+ this.started_ = false;
+
+ this.setUpMessagePort_();
+ this.setUpPeerConnection_();
+
+ // Send a request for TURN credentials, then expect a response message with
+ // type TURN_CREDENTIALS.
+ this.sendMessageToMrp_(
+ new mr.webrtc.Message(mr.webrtc.MessageType.GET_TURN_CREDENTIALS));
+ }
+
+ /**
+ * @override
+ */
+ start() {
+ return this.peerConnection_.then(pc => {
+ if (pc.isStarted()) {
+ return Promise.reject(Error('Presentation already started'));
+ }
+
+ pc.start();
+ return this.startResolver_.promise;
+ });
+ }
+
+ /**
+ * @override
+ */
+ stop() {
+ this.started_ = false;
+ return this.peerConnection_.then(pc => {
+ pc.stop();
+ this.peerConnection_ =
+ Promise.reject(Error('Peer connection has already been stopped'));
+ });
+ }
+
+ /**
+ * Sets up the message port to receive incoming messages.
+ * @private
+ */
+ setUpMessagePort_() {
+ this.messagePort_.onMessage = message => {
+ if (!message.type) {
+ // Wrap message and send it along as a presentation message.
+ this.peerConnection_.then(pc => {
+ pc.sendDataChannelMessage({
+ type: mr.webrtc.MessageType.PRESENTATION_CONNECTION_MESSAGE,
+ data: message
+ });
+ });
+ return;
+ }
+ switch (message.type) {
+ case mr.webrtc.MessageType.TURN_CREDENTIALS:
+ // Response to a GET_TURN_CREDENTIALS message. This causes the
+ // PeerConnection to be created.
+ this.peerConnectionResolver_.resolve(new mr.webrtc.PeerConnection(
+ this.route.id,
+ /** @type {!Array<!mr.webrtc.TurnCredential>} */
+ (message.data['credentials'])));
+ break;
+ case mr.webrtc.MessageType.ANSWER:
+ this.peerConnection_.then(pc => {
+ pc.setRemoteDescription(message.data);
+ });
+ break;
+ case mr.webrtc.MessageType.STOP:
+ this.startResolver_.reject('Stop signal received');
+ this.stop();
+ break;
+ default:
+ throw Error('Unknown message type: ' + message.type);
+ }
+ };
+ }
+
+ /**
+ * Sets up the peer connection (with callbacks).
+ * @private
+ */
+ setUpPeerConnection_() {
+ this.peerConnection_.then(pc => {
+ // Pass the description up the MessagePort.
+ pc.setOnOfferDescriptionReady(description => {
+ const offerData = new mr.webrtc.OfferMessageData(
+ description,
+ /* opt_settings_ */ null,
+ /* opt_mediaConstraints */ null, this.sourceUrn, this.route.id);
+ const message =
+ new mr.webrtc.Message(mr.webrtc.MessageType.OFFER, offerData);
+ this.sendMessageToMrp_(message);
+ });
+ // Pass along the data channel message up the MessagePort.
+ pc.setOnDataChannelMessage(message => {
+ // Check if message is a STOP message and calls stop() before sending it
+ // through the message port to MRP.
+ const webRtcMessage = mr.webrtc.Message.fromString(message);
+ if (webRtcMessage.type == mr.webrtc.MessageType.STOP) {
+ this.stop();
+ }
+ this.sendMessageToMrp_(webRtcMessage);
+ });
+
+ // Send the connection success/closed/failed events up the MessagePort,
+ // and
+ // also resolve or reject the start() promise.
+ pc.setOnConnectionSuccess(event => {
+ this.started_ = true;
+ this.sendMessageToMrp_(
+ new mr.webrtc.Message(mr.webrtc.MessageType.SESSION_START_SUCCESS));
+ this.startResolver_.resolve(this.route);
+ });
+ pc.setOnConnectionClosed(event => {
+ this.sendMessageToMrp_(
+ new mr.webrtc.Message(mr.webrtc.MessageType.SESSION_END));
+ });
+ pc.setOnConnectionFailure(error => {
+ // If we haven't started yet, reject the start promise.
+ if (!this.started_) {
+ this.startResolver_.reject(error);
+ }
+ this.sendMessageToMrp_(
+ new mr.webrtc.Message(mr.webrtc.MessageType.SESSION_FAILURE));
+ });
+ });
+ }
+
+ /**
+ * Sends the provided message to the MRP via the message port.
+ * @param {!Object} message
+ * @private
+ */
+ sendMessageToMrp_(message) {
+ this.messagePort_.sendMessage(message);
+ }
+};
+
+}); // goog.scope
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/presentation_services/presentation_session.js b/chromium/chrome/browser/resources/media_router/extension/src/presentation_services/presentation_session.js
new file mode 100644
index 00000000000..02b2ad5253b
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/presentation_services/presentation_session.js
@@ -0,0 +1,31 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Interface to a presentation session.
+ */
+
+goog.provide('mr.presentation.Session');
+
+
+
+/**
+ * Creates a new PresentationSession.
+ * @record
+ */
+mr.presentation.Session = class {
+ /**
+ * Starts the presentation session.
+ * @return {!Promise<!mr.Route>} Fulfilled when the
+ * session has been created and transports have started.
+ */
+ start() {}
+ /**
+ * Stops the presentation session. The underlying streams and transports are
+ * stopped and destroyed.
+ *
+ * @return {!Promise} Promise that resolves when the session is stopped.
+ */
+ stop() {}
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/common/id_generator.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/id_generator.js
new file mode 100644
index 00000000000..91096c736f8
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/id_generator.js
@@ -0,0 +1,80 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview An ID generator that keeps its state across extension
+ * suspend/wakeup cycles.
+ *
+
+ */
+
+goog.provide('mr.IdGenerator');
+
+goog.require('mr.PersistentData');
+goog.require('mr.PersistentDataManager');
+
+
+/**
+ * @implements {mr.PersistentData}
+ */
+mr.IdGenerator = class {
+ /**
+ * @param {!string} storageKey
+ */
+ constructor(storageKey) {
+ /** @private @const {!string} */
+ this.storageKey_ = storageKey;
+
+ /** @private {!number} */
+ this.nextId_ = Math.floor(Math.random() * 1e6) * 1000;
+ }
+
+ /**
+ * Enables persistent
+ */
+ enablePersistent() {
+ mr.PersistentDataManager.register(this);
+ }
+
+ /**
+ * @return {!number} The next ID. 0 will never be returned.
+ */
+ getNext() {
+ let nextId = this.nextId_++;
+ if (nextId == 0) {
+ // Skip 0, which is used by Cast receiver to
+ // indicate that the broadcast status message is not coming from a
+ // specific
+ // sender (it is an autonomous status change, not triggered by a command
+ // from any sender). Strange usage of 0 though; could be a null / optional
+ // field.
+ nextId = this.nextId_++;
+ }
+ return nextId;
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'IdGenerator.' + this.storageKey_;
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ return [this.nextId_];
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {
+ const savedData = mr.PersistentDataManager.getTemporaryData(this);
+ if (savedData) {
+ this.nextId_ = savedData;
+ }
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/common/id_generator_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/id_generator_test.js
new file mode 100644
index 00000000000..1e9e37a220d
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/id_generator_test.js
@@ -0,0 +1,36 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.require('mr.IdGenerator');
+goog.require('mr.PersistentDataManager');
+
+describe('IdGenerator Tests', function() {
+ let generator;
+
+ beforeEach(function() {
+ spyOn(Math, 'random').and.returnValue(0);
+ generator = new mr.IdGenerator('test');
+ generator.enablePersistent();
+ });
+
+ afterEach(function() {
+ mr.PersistentDataManager.clear();
+ });
+
+ it('getNext', function() {
+ expect(generator.getNext()).toBe(1);
+ expect(generator.getNext()).toBe(2);
+ expect(generator.getNext()).toBe(3);
+ });
+
+ it('Persistent', function() {
+ expect(generator.getNext()).toBe(1);
+ expect(generator.getNext()).toBe(2);
+ mr.PersistentDataManager.suspendForTest();
+ generator = new mr.IdGenerator('test');
+ generator.loadSavedData();
+ expect(generator.getNext()).toBe(3);
+ });
+
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/common/net_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/net_utils.js
new file mode 100644
index 00000000000..fe52d1d5ce2
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/net_utils.js
@@ -0,0 +1,98 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Utilites for handling network data.
+
+ */
+
+goog.module('mr.NetUtils');
+goog.module.declareLegacyNamespace();
+
+
+
+/** @const @private {!RegExp} */
+exports.IPV4_REGEXP_ =
+ new RegExp('^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$');
+
+/** @const @private {!Array<number>} */
+exports.IPV4_PRIVATE_ADDRESS_MASKS_ = [
+ // 10.0.0.0/8
+ 4278190080,
+ // 172.16.0.0/12
+ 4293918720,
+ // 192.168.0.0/16
+ 4294901760
+];
+
+/** @const @private {!Array<number>} */
+exports.IPV4_PRIVATE_ADDRESS_SUBNETS_ = [
+ // 10.0.0.0/8
+ 167772160,
+ // 172.16.0.0/12
+ 2886729728,
+ // 192.168.0.0/16
+ 3232235520
+];
+
+/**
+ * Parses an IPv4 dotted-quad address into an array of four numbers, or
+ * returns null if the argument cannot be parsed.
+ *
+ * @param {string} ipAddress
+ * @return {?Array<number>} Array of four integers 0-255 or null.
+ */
+exports.parseIPv4Address = function(ipAddress) {
+ const matches = ipAddress.match(exports.IPV4_REGEXP_);
+ if (!matches || matches.length != 5) return null;
+ const result = [];
+ for (let i = 0; i < 4; i++) {
+ result[i] = Number.parseInt(matches[i + 1], 10);
+ if (result[i] < 0 || result[i] > 255) return null;
+ }
+ return result;
+};
+
+/**
+ * Returns true if ipAddress can be parsed into a valid IPv4 private network
+ * address.
+ *
+ * @param {string} ipAddress
+ * @return {boolean} True if ipAddress is a valid IPv4 private network address.
+ */
+exports.isPrivateIPv4Address = function(ipAddress) {
+ const parsedAddress = exports.parseIPv4Address(ipAddress);
+ if (!parsedAddress) return false;
+ // >>> 0 converts a signed integer to unsigned, so bitwise operations are
+ // sensible.
+ const addressValue = (parsedAddress[0] << 24 | parsedAddress[1] << 16 |
+ parsedAddress[2] << 8 | parsedAddress[0]) >>>
+ 0;
+ for (let i = 0; i < 3; i++) {
+ if ((addressValue & exports.IPV4_PRIVATE_ADDRESS_MASKS_[i]) >>> 0 ==
+ exports.IPV4_PRIVATE_ADDRESS_SUBNETS_[i])
+ return true;
+ }
+ return false;
+};
+
+/**
+ * @param {string} url A url.
+ * @return {!HTMLAnchorElement} The result of parsing url.
+ */
+exports.parseUrl = function(url) {
+ const a = document.createElement('a');
+ a.href = url;
+ return /** @type {!HTMLAnchorElement} */ (a);
+};
+
+/**
+ * Non-exhaustive list of HTTP status codes. Add new codes as needed here.
+ * @enum {number}
+ */
+const HttpStatus = {
+ NOT_FOUND: 404,
+};
+
+exports.HttpStatus = HttpStatus;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/common/net_utils_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/net_utils_test.js
new file mode 100644
index 00000000000..4143201990b
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/net_utils_test.js
@@ -0,0 +1,61 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Unit tests for mr.NetUtils.
+
+ */
+
+goog.module('mr.NetUtilsTest');
+goog.setTestOnly('mr.NetUtilsTest');
+
+const n = goog.require('mr.NetUtils');
+
+describe('mr.NetUtils', () => {
+
+ it('parses a valid IPv4 address', () => {
+ expect(n.parseIPv4Address('128.164.100.104')).toEqual([128, 164, 100, 104]);
+ expect(n.parseIPv4Address('0.0.0.0')).toEqual([0, 0, 0, 0]);
+ expect(n.parseIPv4Address('255.255.255.255')).toEqual([255, 255, 255, 255]);
+ });
+
+ it('does not parse an invalid IPv4 address', () => {
+ expect(n.parseIPv4Address('')).toBeNull();
+ expect(n.parseIPv4Address('deadbeef')).toBeNull();
+ expect(n.parseIPv4Address('128.164.100')).toBeNull();
+ expect(n.parseIPv4Address('128.164.100.104.333')).toBeNull();
+ expect(n.parseIPv4Address('256.164.100.104')).toBeNull();
+ expect(n.parseIPv4Address('-1.164.100.104')).toBeNull();
+ });
+
+ it('validates an IPv4 private network address', () => {
+ expect(n.isPrivateIPv4Address('10.0.0.0')).toBe(true);
+ expect(n.isPrivateIPv4Address('10.255.255.255')).toBe(true);
+ expect(n.isPrivateIPv4Address('172.16.0.0')).toBe(true);
+ expect(n.isPrivateIPv4Address('172.31.255.255')).toBe(true);
+ expect(n.isPrivateIPv4Address('192.168.0.0')).toBe(true);
+ expect(n.isPrivateIPv4Address('192.168.255.255')).toBe(true);
+ });
+
+ it('does not validate an IPv4 public network address', () => {
+ expect(n.isPrivateIPv4Address('9.255.255.255')).toBe(false);
+ expect(n.isPrivateIPv4Address('11.0.0.0')).toBe(false);
+ expect(n.isPrivateIPv4Address('172.15.255.255')).toBe(false);
+ expect(n.isPrivateIPv4Address('172.32.0.0')).toBe(false);
+ expect(n.isPrivateIPv4Address('193.167.255.255')).toBe(false);
+ expect(n.isPrivateIPv4Address('193.169.0.0')).toBe(false);
+ });
+
+ it('parses a URL', () => {
+ const url =
+ n.parseUrl('https://www.example.com:8080/a/path?a_query#a_fragment');
+ expect(url.protocol).toBe('https:');
+ expect(url.hostname).toBe('www.example.com');
+ expect(url.port).toBe('8080');
+ expect(url.pathname).toBe('/a/path');
+ expect(url.search).toBe('?a_query');
+ expect(url.hash).toBe('#a_fragment');
+ expect(url.host).toBe('www.example.com:8080');
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/common/retry.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/retry.js
new file mode 100644
index 00000000000..326dbfa1d54
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/retry.js
@@ -0,0 +1,198 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview
+ * A class for attempting some unreliable operation until it succeeds. Sort of
+ * an asynchronous while loop.
+ *
+ * (new mr.Retry(attempt, 500, 10)).start().then(onSuccess, onFailure);
+ *
+ * The next attempt is driven by the failure of current attempt. Thus, there is
+ * at most one attempt invocation at any time.
+ */
+
+goog.provide('mr.Retry');
+
+goog.require('mr.Assertions');
+goog.require('mr.PromiseResolver');
+
+
+/**
+ * An object that attempts some operation until it succeeds.
+ *
+ * @template R
+ */
+mr.Retry = class {
+ /**
+ * @param {function():!Promise<R>} onAttempt An
+ * idempotent function to call repeatedly until it succeeds.
+ * @param {number} retryDelay The number of milliseconds to wait between the
+ * start of one attempt and the start of the next. It must be positive.
+ * More precisely, this is the amount of time to wait between the failure
+ * of one call to onAttempt().
+ * @param {number} maxAttempts The maximum number of attempts.
+ * It must be positive.
+ */
+ constructor(onAttempt, retryDelay, maxAttempts) {
+ /**
+ * @private {!function():!Promise<R>}
+ */
+ this.onAttempt_ = onAttempt;
+
+ /**
+ * @private {number}
+ */
+ this.retryDelay_ = retryDelay > 0 ? retryDelay : 10;
+
+ /**
+ * @private {number}
+ */
+ this.maxAttempts_ = maxAttempts > 0 ? maxAttempts : 1;
+
+ /**
+ * @private {number}
+ */
+ this.maxRetryDelay_ = 0;
+
+ /**
+ * @private {number}
+ */
+ this.backoffFactor_ = 1;
+
+ /**
+ * The number of times `onAttempt_` has been called.
+ * @private {number}
+ */
+ this.numAttemptsStarted_ = 0;
+
+ /**
+ * @private {boolean}
+ */
+ this.isFinished_ = false;
+
+ /**
+ * The ID of the most recently created timer.
+ * @private {?number}
+ */
+ this.timerId_ = null;
+
+ /** @private {mr.PromiseResolver} */
+ this.resolver_ = null;
+ }
+
+ /**
+ * Starts running this object.
+ *
+ * This method starts an asynchronous process that repeatedly calls
+ * `onAttempt`.
+ *
+ * For each attempt, `onAttempt` is called. When `onAttempt`
+ * resolves, the returned promise is resolved with the same result.
+ * The returned promise rejects if `abort` is called on this object,
+ * or the number of attempts specified by `setMaxAttempts` is reached.
+ *
+ * @return {!Promise<R>}
+ * @template R
+ */
+ start() {
+ if (this.resolver_ != null) {
+ return Promise.reject(Error('Cannot call Retry.start more than once.'));
+ }
+ this.resolver_ = new mr.PromiseResolver();
+ this.retryOnce_();
+ return this.resolver_.promise;
+ }
+
+ /**
+ * Makes the next call to `onAttempt_`.
+ * @private
+ */
+ retryOnce_() {
+ this.timerId_ = null;
+ if (this.isFinished_) {
+ // The abort method has been called, don't start a new attempt.
+ return;
+ }
+
+ this.numAttemptsStarted_++;
+ this.onAttempt_().then(
+ result => {
+ this.cleanup_();
+ this.resolver_.resolve(result);
+ },
+ error => {
+ if (this.numAttemptsStarted_ >= this.maxAttempts_) {
+ // Maximum number of attempts has been reached, do not try again.
+ this.cleanup_();
+ this.resolver_.reject(Error('Max attempts reached'));
+ } else {
+ this.timerId_ =
+ setTimeout(this.retryOnce_.bind(this), this.retryDelay_);
+ this.updateRetryDelay_();
+ }
+ });
+ }
+
+ /**
+ * Implement exponential backoff.
+ * @private
+ */
+ updateRetryDelay_() {
+ let newRetryDelay = this.retryDelay_ * this.backoffFactor_;
+ if (this.maxRetryDelay_ > 0) {
+ newRetryDelay = Math.min(newRetryDelay, this.maxRetryDelay_);
+ }
+ this.retryDelay_ = newRetryDelay;
+ }
+
+ /**
+ * Sets the backoff factor. After each attempt, the retry delay is
+ * multiplied by the backoff factor, which must be at least 1.
+ *
+ * @param {number} backoffFactor The factor by which the retry delay should be
+ * increased after each attempt.
+ * @return {!mr.Retry} this
+ */
+ setBackoffFactor(backoffFactor) {
+ mr.Assertions.assert(backoffFactor >= 1);
+ this.backoffFactor_ = backoffFactor;
+ return this;
+ }
+
+ /**
+ * Sets the maximum retry delay that can be used as a result of the backoff
+ * factor. If 0, there is no maximum.
+ * @param {number} maxRetryDelay The maximum number of milliseconds to wait
+ * before retrying.
+ * @return {!mr.Retry} this
+ */
+ setMaxRetryDelay(maxRetryDelay) {
+ mr.Assertions.assert(maxRetryDelay >= 0);
+ this.maxRetryDelay_ = maxRetryDelay;
+ return this;
+ }
+
+ /**
+ * Causes this object to stop making attempts and puts it in a
+ * finished state. May be called any time after `start`.
+ */
+ abort() {
+ this.cleanup_();
+ this.resolver_.reject(Error('abort'));
+ }
+
+ /**
+ * @private
+ */
+ cleanup_() {
+ if (this.timerId_ != null) {
+ // Clean up any timer, because it will be a no-op when it fires.
+ clearTimeout(this.timerId_);
+ this.timerId_ = null;
+ }
+
+ this.isFinished_ = true;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/common/runtime_error_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/runtime_error_utils.js
new file mode 100644
index 00000000000..f3eab537456
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/runtime_error_utils.js
@@ -0,0 +1,38 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Utilities for handling extension API errors.
+ */
+
+goog.module('mr.RunTimeErrorUtils');
+goog.module.declareLegacyNamespace();
+
+const Logger = goog.require('mr.Logger');
+
+
+/**
+ * Converts chrome.runtime.lastError into an Error object. Should only be
+ * called from an extension function callback that returns null or undefined.
+ * By accessing chrome.runtime.lastError, this method has a side effect of
+ * "handling" the error by preventing it from turning into an unchecked error.
+
+ *
+ * @param {string} functionName The name of the extension API function.
+ * @param {!Logger=} logger If there is an error, logs a FINE error message
+ * with the logger, if provided.
+ * @return {?Error} An Error object with chrome.runtime.lastError.message, if
+ * any.
+ */
+exports.getError = function(functionName, logger = undefined) {
+ if (!chrome.runtime.lastError) {
+ return null;
+ }
+ const message = functionName + ' failed, chrome.runtime.lastError: ' +
+ (chrome.runtime.lastError.message || 'Unknown error');
+ if (logger) {
+ logger.fine(message);
+ }
+ return new Error(message);
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/common/sink_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/sink_utils.js
new file mode 100644
index 00000000000..830ecbe5895
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/sink_utils.js
@@ -0,0 +1,187 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Utilites and settings for media sinks.
+
+ */
+
+goog.module('mr.SinkUtils');
+goog.module.declareLegacyNamespace();
+
+const PersistentData = goog.require('mr.PersistentData');
+const PersistentDataManager = goog.require('mr.PersistentDataManager');
+const Sha1 = goog.require('mr.Sha1');
+const StringUtils = goog.require('mr.StringUtils');
+const base64 = goog.require('mr.base64');
+
+
+/**
+ * @implements {PersistentData}
+ */
+class SinkUtils {
+ constructor() {
+ /**
+ * Used to generate per-profile media sink ids. Persistent data.
+ * @private {string}
+ */
+ this.receiverIdToken_ = StringUtils.getRandomString();
+
+ /**
+ * The model name of the device that was most recently used to start a media
+ * route. Temporary data.
+ * @type {!SinkUtils.DeviceData}
+ */
+ this.recentLaunchedDevice = new SinkUtils.DeviceData(null, null);
+
+ /**
+ * The model name of the device that was most recently discovered. Temporary
+ * data.
+ * @type {!SinkUtils.DeviceData}
+ */
+ this.recentDiscoveredDevice = new SinkUtils.DeviceData(null, null);
+
+ /**
+ * List of IP addresses of devices that are assumed to exist (and not
+ * discvered on the LAN), for debugging purposes. Persistent data.
+ * @type {!Array<string>}
+ */
+ this.fixedIpList = [];
+
+ /**
+ * Persistent data.
+ * @type {number}
+ */
+ this.castControlPort = 0;
+
+ /**
+ * The last time cloud sinks were checked.
+ * @type {number}
+ */
+ this.lastCloudSinkCheckTimeMillis = 0;
+
+ PersistentDataManager.register(this);
+ }
+
+ /**
+ * @return {!SinkUtils}
+ */
+ static getInstance() {
+ if (!SinkUtils.instance_) {
+ SinkUtils.instance_ = new SinkUtils();
+ }
+ return SinkUtils.instance_;
+ }
+
+ /**
+ * Generates ID from the receiver UUID and a per-profile token saved in
+ * localStorage.
+ *
+ * Both DIAL and mDNS use this to generate receiver ID so that it is
+ * consistent and can be used to deduplicate receivers. For a given token, the
+ * ID is the same for the same device no matter when it is discovered.
+ *
+ * @param {string} uniqueId
+ * @return {string} receiver ID.
+ */
+ generateId(uniqueId) {
+ uniqueId = uniqueId.toLowerCase();
+ const sha1 = new Sha1();
+ sha1.update(uniqueId);
+ sha1.update(this.receiverIdToken_);
+ return 'r' + base64.encodeArray(sha1.digest(), true);
+ }
+
+ /**
+ * @return {!SinkUtils.DeviceData} Most recent device launched or
+ * discovered.
+ */
+ getRecentDevice() {
+ if (this.recentLaunchedDevice.model) return this.recentLaunchedDevice;
+
+ if (this.recentDiscoveredDevice.model) return this.recentDiscoveredDevice;
+
+ return new SinkUtils.DeviceData(null, null);
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'SinkUtils';
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ return [
+ {
+ 'recentLaunchedDevice': this.recentLaunchedDevice,
+ 'recentDiscoveredDevice': this.recentDiscoveredDevice
+ },
+ {
+ 'receiverIdToken': this.receiverIdToken_,
+ 'fixedIpList': this.fixedIpList.join(','),
+ 'castControlPort': this.castControlPort,
+ 'lastCloudSinkCheckTimeMillis': this.lastCloudSinkCheckTimeMillis
+ }
+ ];
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {
+ const tempData = PersistentDataManager.getTemporaryData(this);
+ if (tempData) {
+ this.recentLaunchedDevice = tempData['recentLaunchedDevice'] ||
+ new SinkUtils.DeviceData(null, null);
+ this.recentDiscoveredDevice = tempData['recentDiscoveredDevice'] ||
+ new SinkUtils.DeviceData(null, null);
+ }
+
+ const persistentData = PersistentDataManager.getPersistentData(this);
+ if (persistentData) {
+ this.receiverIdToken_ =
+ persistentData['receiverIdToken'] || StringUtils.getRandomString();
+ this.fixedIpList = (persistentData['fixedIpList'] &&
+ persistentData['fixedIpList'].split(',')) ||
+ [];
+ this.castControlPort = persistentData['castControlPort'] || 0;
+ this.lastCloudSinkCheckTimeMillis =
+ persistentData['lastCloudSinkCheckTimeMillis'] || 0;
+ }
+ }
+}
+
+
+/** @private {SinkUtils} */
+SinkUtils.instance_ = null;
+
+
+/**
+ * The device data to keep track of.
+ */
+SinkUtils.DeviceData = class {
+ /**
+ * @param {?string} modelName
+ * @param {?string} ipAddress
+ */
+ constructor(modelName, ipAddress) {
+ /**
+ * @type {?string}
+ * @export
+ */
+ this.model = modelName;
+
+ /**
+ * @type {?string}
+ * @export
+ */
+ this.ip = ipAddress;
+ }
+};
+
+exports = SinkUtils;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/common/xhr_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/xhr_utils.js
new file mode 100644
index 00000000000..0ce839efb46
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/common/xhr_utils.js
@@ -0,0 +1,72 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Utilities for dealing with XmlHttpRequests.
+ */
+
+goog.provide('mr.XhrUtils');
+
+goog.require('mr.Logger');
+
+
+/**
+ * Logs the outcome of a XmlHttpRequest.
+ * @param {mr.Logger} logger Where to log.
+ * @param {string} action The operation that created the Fetch.
+ * @param {string} method The HTTP method.
+ * @param {!XMLHttpRequest} xhr The response from Fetch.
+ */
+mr.XhrUtils.logRawXhr = function(logger, action, method, xhr) {
+ const logString = mr.XhrUtils.getStatusString_(
+ action, method, xhr.responseURL, xhr.status, xhr.statusText);
+ if (mr.XhrUtils.isSuccess(xhr)) {
+ logger.info(logString);
+ } else {
+ logger.fine(logString);
+ }
+};
+
+
+/**
+ * Returns true if the given XMLHttpRequest represents a successful response.
+ * @param {!XMLHttpRequest} xhr
+ * @return {boolean}
+ */
+mr.XhrUtils.isSuccess = function(xhr) {
+ return xhr.status >= 200 && xhr.status <= 299;
+};
+
+
+/**
+ * Returns a loggable string with the given parameters.
+ * @param {string} action
+ * @param {string} method
+ * @param {string} url
+ * @param {number} status
+ * @param {string} statusText
+ * @return {string}
+ * @private
+ */
+mr.XhrUtils.getStatusString_ = function(
+ action, method, url, status, statusText) {
+ return `[${action}]: ${method} ${url} => ${status} (${statusText})`;
+};
+
+
+/**
+ * Parses xmlText into an XML document.
+ * @param {string} xmlText A serialized XML document.
+ * @return {?Document} An XML document if the parse was successful, null
+ * otherwise.
+ */
+mr.XhrUtils.parseXml = function(xmlText) {
+ const xml = new DOMParser().parseFromString(xmlText, 'text/xml');
+ // A failed parse returns an HTML document with a <parsererror> element.
+ return xml.getElementsByTagNameNS(
+ 'http://www.w3.org/1999/xhtml', 'parsererror')
+ .length ?
+ null :
+ xml;
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity.js
new file mode 100644
index 00000000000..264e20928c6
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity.js
@@ -0,0 +1,22 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview DIAL activity (locally launched or discovered).
+ */
+
+goog.provide('mr.dial.Activity');
+
+mr.dial.Activity = class {
+ /**
+ * @param {!mr.Route} route
+ * @param {!string} appName
+ */
+ constructor(route, appName) {
+ /** @type {!mr.Route} */
+ this.route = route;
+ /** @type {string} */
+ this.appName = appName;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity_records.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity_records.js
new file mode 100644
index 00000000000..112ec31bd15
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity_records.js
@@ -0,0 +1,156 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.ActivityRecords');
+
+const ActivityCallbacks = goog.require('mr.dial.ActivityCallbacks');
+const PersistentData = goog.require('mr.PersistentData');
+const PersistentDataManager = goog.require('mr.PersistentDataManager');
+
+
+/**
+ * Keeps track of DIAL activities.
+ * @implements {PersistentData}
+ */
+const ActivityRecords = class {
+ /**
+ * @param {!ActivityCallbacks} activityCallbacks
+ */
+ constructor(activityCallbacks) {
+ /**
+ * Maps route ID to the corresponding Activity.
+ * @private {!Map<string, !mr.dial.Activity>}
+ */
+ this.routeIdToAppInfo_ = new Map();
+
+ /** @private @const {!ActivityCallbacks} */
+ this.activityCallbacks_ = activityCallbacks;
+ }
+
+ /**
+ * Restores routeIdToAppInfo_ from saved data.
+ */
+ init() {
+ PersistentDataManager.register(this);
+ }
+
+ /**
+ * Removes all activities.
+ */
+ clear() {
+ this.routeIdToAppInfo_.clear();
+ }
+
+ /**
+ * Adds a new activity. If the activity already exists, this is no-op.
+ * @param {!mr.dial.Activity} activity
+ */
+ add(activity) {
+ if (this.getByRouteId(activity.route.id)) {
+ return;
+ }
+ this.routeIdToAppInfo_.set(activity.route.id, activity);
+ this.activityCallbacks_.onActivityAdded(activity);
+ }
+
+ /**
+ * Returns the activity corresponding to the given route ID, or null if there
+ * is none.
+ * @param {string} routeId
+ * @return {?mr.dial.Activity}
+ */
+ getByRouteId(routeId) {
+ return this.routeIdToAppInfo_.get(routeId) || null;
+ }
+
+ /**
+ * Returns the activity corresponding to the given sink ID, or null if there
+ * is none.
+ * @param {string} sinkId
+ * @return {?mr.dial.Activity}
+ */
+ getBySinkId(sinkId) {
+ for (let [routeId, activity] of this.routeIdToAppInfo_) {
+ if (activity.route.sinkId == sinkId) {
+ return /** @type {!mr.dial.Activity} */ (activity);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Removes the activity associated with the given sink ID.
+ * @param {string} sinkId
+ */
+ removeBySinkId(sinkId) {
+ const activity = this.getBySinkId(sinkId);
+ if (activity) {
+ this.routeIdToAppInfo_.delete(activity.route.id);
+ this.activityCallbacks_.onActivityRemoved(activity);
+ }
+ }
+
+ /**
+ * Removes the activity associated with the given route ID.
+ * @param {string} routeId
+ */
+ removeByRouteId(routeId) {
+ const activity = this.routeIdToAppInfo_.get(routeId);
+ if (activity) {
+ this.routeIdToAppInfo_.delete(routeId);
+ this.activityCallbacks_.onActivityRemoved(activity);
+ }
+ }
+
+ /**
+ * Returns the list of routes associated with the current list of activities.
+ * @return {!Array<!mr.Route>}
+ */
+ getRoutes() {
+ return Array.from(
+ this.routeIdToAppInfo_.values(), activity => activity.route);
+ }
+
+ /**
+ * Returns the current list of acitvities.
+ * @return {!Array<!mr.dial.Activity>}
+ */
+ getActivities() {
+ return Array.from(this.routeIdToAppInfo_.values());
+ }
+
+ /**
+ * Returns the number of activities.
+ * @return {number}
+ */
+ getActivityCount() {
+ return this.routeIdToAppInfo_.size;
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'dial.ActivityRecords';
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ return [Array.from(this.routeIdToAppInfo_)];
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {
+ const savedData = PersistentDataManager.getTemporaryData(this);
+ if (savedData) {
+ this.routeIdToAppInfo_ = new Map(savedData);
+ }
+ }
+};
+
+exports = ActivityRecords;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity_records_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity_records_test.js
new file mode 100644
index 00000000000..38ff262fba0
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_activity_records_test.js
@@ -0,0 +1,100 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.ActivityRecordsTest');
+goog.setTestOnly('mr.dial.ActivityRecordsTest');
+
+const Activity = goog.require('mr.dial.Activity');
+const ActivityRecords = goog.require('mr.dial.ActivityRecords');
+const PersistentDataManager = goog.require('mr.PersistentDataManager');
+const Route = goog.require('mr.Route');
+const UnitTestUtils = goog.require('mr.UnitTestUtils');
+
+describe('DIAL ActivityRecords Tests', function() {
+ let records;
+ let mockCallbacks;
+ let activity1;
+ let activity2;
+
+ beforeEach(function() {
+ UnitTestUtils.mockMojoApi();
+ UnitTestUtils.mockChromeApi();
+ let route = new Route(
+ 'routeId1', 'presentationId1', 'sinkId1', null, false, 'description1',
+ 'imageUrl1');
+ activity1 = new Activity(route, 'app1');
+ route = new Route(
+ 'routeId2', 'presentationId2', 'sinkId2', null, true, 'description2',
+ 'imageUrl2');
+ activity2 = new Activity(route, 'app2');
+ mockCallbacks = jasmine.createSpyObj(
+ 'ActivityCallbacks',
+ ['onActivityAdded', 'onActivityRemoved', 'onActivityUpdated']);
+ records = new ActivityRecords(mockCallbacks);
+ records.init();
+ });
+
+ afterEach(function() {
+ PersistentDataManager.clear();
+ UnitTestUtils.restoreChromeApi();
+ });
+
+ it('Add activity', function() {
+ records.add(activity1);
+ expect(records.getByRouteId(activity1.route.id)).toEqual(activity1);
+ expect(mockCallbacks.onActivityAdded.calls.count()).toBe(1);
+ expect(mockCallbacks.onActivityAdded).toHaveBeenCalledWith(activity1);
+ records.add(activity1);
+ expect(mockCallbacks.onActivityAdded.calls.count()).toBe(1);
+ });
+
+ it('Get activity and route', function() {
+ records.add(activity1);
+ expect(records.getByRouteId(activity1.route.id)).toEqual(activity1);
+ expect(records.getBySinkId(activity1.route.sinkId)).toEqual(activity1);
+ expect(records.getRoutes()).toEqual([activity1.route]);
+ records.add(activity2);
+ expect(records.getByRouteId(activity2.route.id)).toEqual(activity2);
+ expect(records.getBySinkId(activity2.route.sinkId)).toEqual(activity2);
+ expect(records.getRoutes()).toEqual([activity1.route, activity2.route]);
+ });
+
+ it('Remove activity', function() {
+ records.add(activity1);
+ records.add(activity2);
+ records.removeByRouteId(activity2.route.id);
+ expect(mockCallbacks.onActivityRemoved.calls.count()).toBe(1);
+ expect(mockCallbacks.onActivityRemoved).toHaveBeenCalledWith(activity2);
+ records.removeByRouteId(activity2.route.id);
+ expect(mockCallbacks.onActivityRemoved.calls.count()).toBe(1);
+ expect(records.getByRouteId(activity2.route.id)).toEqual(null);
+ expect(records.getBySinkId(activity2.route.sinkId)).toEqual(null);
+ expect(records.getRoutes()).toEqual([activity1.route]);
+
+ records.removeBySinkId(activity1.route.sinkId);
+ expect(mockCallbacks.onActivityRemoved.calls.count()).toBe(2);
+ expect(mockCallbacks.onActivityRemoved).toHaveBeenCalledWith(activity1);
+ expect(records.getRoutes()).toEqual([]);
+ });
+
+ it('Save without any data', function() {
+ expect(records.getRoutes()).toEqual([]);
+ PersistentDataManager.suspendForTest();
+ records = new ActivityRecords(mockCallbacks);
+ records.loadSavedData();
+ expect(records.getRoutes()).toEqual([]);
+ });
+
+ it('Save with data', function() {
+ records.add(activity1);
+ records.add(activity2);
+ PersistentDataManager.suspendForTest();
+ records = new ActivityRecords(mockCallbacks);
+ records.loadSavedData();
+ expect(JSON.stringify(records.getRoutes())).toEqual(JSON.stringify([
+ activity1.route, activity2.route
+ ]));
+ });
+
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_analytics.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_analytics.js
new file mode 100644
index 00000000000..9abe0f835a5
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_analytics.js
@@ -0,0 +1,119 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Defines UMA analytics specific to the DIAL provider.
+ */
+
+goog.provide('mr.DialAnalytics');
+
+goog.require('mr.Analytics');
+
+
+/**
+ * Contains all analytics logic for the DIAL provider.
+ * @const {*}
+ */
+mr.DialAnalytics = {};
+
+
+/** @enum {string} */
+mr.DialAnalytics.Metric = {
+ DEVICE_DESCRIPTION_FAILURE: 'MediaRouter.Dial.Device.Description.Failure',
+ DEVICE_DESCRIPTION_FROM_CACHE: 'MediaRouter.Dial.Device.Description.Cached',
+ DIAL_CREATE_ROUTE: 'MediaRouter.Dial.Create.Route',
+ NON_CAST_DISCOVERY: 'MediaRouter.Dial.Sink.Discovered.NonCast'
+};
+
+
+/**
+ * Possible values for the route creation analytics.
+ * @enum {number}
+ */
+mr.DialAnalytics.DialRouteCreation = {
+ FAILED_NO_SINK: 0,
+ ROUTE_CREATED: 1,
+ NO_APP_INFO: 2,
+ FAILED_LAUNCH_APP: 3
+};
+
+
+/**
+ * Possible values for device description failures.
+ * @enum {number}
+ */
+mr.DialAnalytics.DeviceDescriptionFailures = {
+ ERROR: 0,
+ PARSE: 1,
+ EMPTY: 2
+};
+
+
+/**
+ * Records analytics around route creation.
+ * @param {mr.DialAnalytics.DialRouteCreation} value
+ */
+mr.DialAnalytics.recordCreateRoute = function(value) {
+ mr.Analytics.recordEnum(
+ mr.DialAnalytics.Metric.DIAL_CREATE_ROUTE, value,
+ mr.DialAnalytics.DialRouteCreation);
+};
+
+
+/**
+ * Records a failure with the device description.
+ * @param {!mr.DialAnalytics.DeviceDescriptionFailures} value The failure
+ * reason.
+ */
+mr.DialAnalytics.recordDeviceDescriptionFailure = function(value) {
+ mr.Analytics.recordEnum(
+ mr.DialAnalytics.Metric.DEVICE_DESCRIPTION_FAILURE, value,
+ mr.DialAnalytics.DeviceDescriptionFailures);
+};
+
+
+/**
+ * Records that device description was retreived from the cache.
+ */
+mr.DialAnalytics.recordDeviceDescriptionFromCache = function() {
+ mr.Analytics.recordEvent(
+ mr.DialAnalytics.Metric.DEVICE_DESCRIPTION_FROM_CACHE);
+};
+
+
+/**
+ * Records that a device was discovered by DIAL that didn't support the cast
+ * protocol.
+ */
+mr.DialAnalytics.recordNonCastDiscovery = function() {
+ mr.Analytics.recordEvent(mr.DialAnalytics.Metric.NON_CAST_DISCOVERY);
+};
+
+
+/**
+ * Histogram name for available DIAL devices count.
+ * @private @const {string}
+ */
+mr.DialAnalytics.AVAILABLE_DEVICES_COUNT_ =
+ 'MediaRouter.Dial.AvailableDevicesCount';
+
+
+/**
+ * Histogram name for known DIAL devices count.
+ * @private @const {string}
+ */
+mr.DialAnalytics.KNOWN_DEVICES_COUNT_ = 'MediaRouter.Dial.KnownDevicesCount';
+
+
+/**
+ * Records device counts.
+ * @param {!mr.DeviceCounts} deviceCounts
+ */
+mr.DialAnalytics.recordDeviceCounts = function(deviceCounts) {
+ mr.Analytics.recordSmallCount(
+ mr.DialAnalytics.AVAILABLE_DEVICES_COUNT_,
+ deviceCounts.availableDeviceCount);
+ mr.Analytics.recordSmallCount(
+ mr.DialAnalytics.KNOWN_DEVICES_COUNT_, deviceCounts.knownDeviceCount);
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_analytics_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_analytics_test.js
new file mode 100644
index 00000000000..37a762b4bf4
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_analytics_test.js
@@ -0,0 +1,88 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly();
+goog.require('mr.DialAnalytics');
+
+describe('Dial Analytics', function() {
+
+ beforeEach(function() {
+ chrome.metricsPrivate = jasmine.createSpyObj(
+ ['recordSmallCount', 'recordUserAction', 'recordValue']);
+ });
+
+ it('should record a Dial.Create.Route result', function() {
+ const testConfig = {
+ 'metricName': 'MediaRouter.Dial.Create.Route',
+ 'type': 'histogram-linear',
+ 'min': 1,
+ 'max': 4,
+ 'buckets': 5
+ };
+ let numCalls = 0;
+ for (key in mr.DialAnalytics.DialRouteCreation) {
+ const value = mr.DialAnalytics.DialRouteCreation[key];
+ mr.DialAnalytics.recordCreateRoute(value);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(++numCalls);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, value);
+ }
+ });
+
+ it('should not record an unknown Dial.Create.Route result', function() {
+ mr.DialAnalytics.recordCreateRoute('test');
+ expect(chrome.metricsPrivate.recordValue).not.toHaveBeenCalled();
+ });
+
+ it('should record a Dial.Device.Description.Failure result', function() {
+ const testConfig = {
+ 'metricName': 'MediaRouter.Dial.Device.Description.Failure',
+ 'type': 'histogram-linear',
+ 'min': 1,
+ 'max': 3,
+ 'buckets': 4
+ };
+ let numCalls = 0;
+ for (key in mr.DialAnalytics.DeviceDescriptionFailures) {
+ const value = mr.DialAnalytics.DeviceDescriptionFailures[key];
+ mr.DialAnalytics.recordDeviceDescriptionFailure(value);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(++numCalls);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, value);
+ }
+ });
+
+ it('should not record an unknown Dial.Device.Description.Failure',
+ function() {
+ mr.DialAnalytics.recordDeviceDescriptionFailure('test');
+ expect(chrome.metricsPrivate.recordValue).not.toHaveBeenCalled();
+ });
+
+ it('should record a Device Description From Cache action', function() {
+ mr.DialAnalytics.recordDeviceDescriptionFromCache();
+ expect(chrome.metricsPrivate.recordUserAction.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordUserAction)
+ .toHaveBeenCalledWith('MediaRouter.Dial.Device.Description.Cached');
+ });
+
+ it('should record a non-Cast Sink Discovery action', function() {
+ mr.DialAnalytics.recordNonCastDiscovery();
+ expect(chrome.metricsPrivate.recordUserAction.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordUserAction)
+ .toHaveBeenCalledWith('MediaRouter.Dial.Sink.Discovered.NonCast');
+ });
+
+ it('DeviceCounts is recorded with recordSmallCount', () => {
+ const deviceCounts = {availableDeviceCount: 5, knownDeviceCount: 8};
+ mr.DialAnalytics.recordDeviceCounts(deviceCounts);
+ expect(chrome.metricsPrivate.recordSmallCount.calls.count()).toBe(2);
+ let args = chrome.metricsPrivate.recordSmallCount.calls.argsFor(0);
+ expect(args[0]).toBe(mr.DialAnalytics.AVAILABLE_DEVICES_COUNT_);
+ expect(args[1]).toBe(deviceCounts.availableDeviceCount);
+
+ args = chrome.metricsPrivate.recordSmallCount.calls.argsFor(1);
+ expect(args[0]).toBe(mr.DialAnalytics.KNOWN_DEVICES_COUNT_);
+ expect(args[1]).toBe(deviceCounts.knownDeviceCount);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_app_discovery_service.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_app_discovery_service.js
new file mode 100644
index 00000000000..d612fa90702
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_app_discovery_service.js
@@ -0,0 +1,480 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.AppDiscoveryService');
+
+const ActivityRecords = goog.require('mr.dial.ActivityRecords');
+const DialClient = goog.require('mr.dial.Client');
+const DialSink = goog.require('mr.dial.Sink');
+const DialSinkAppStatus = goog.require('mr.dial.SinkAppStatus');
+const Logger = goog.require('mr.Logger');
+const PersistentData = goog.require('mr.PersistentData');
+const PersistentDataManager = goog.require('mr.PersistentDataManager');
+const PromiseUtils = goog.require('mr.PromiseUtils');
+const SinkDiscoveryService = goog.require('mr.dial.SinkDiscoveryService');
+
+
+/**
+ * Service for determining whether a sink supports a given DIAL app.
+ * @implements {PersistentData}
+ */
+const AppDiscoveryService = class {
+ /**
+ * @param {!SinkDiscoveryService} discoveryService
+ * @param {!ActivityRecords} activityRecords
+ */
+ constructor(discoveryService, activityRecords) {
+ /** @private @const {?Logger} */
+ this.logger_ = Logger.getInstance('mr.dial.AppDiscoveryService');
+
+ /**
+ * Names of applications that are actively being queried for.
+ * @private {!Set<string>}
+ */
+ this.registeredApps_ = new Set();
+
+ /**
+ * @private @const {!SinkDiscoveryService}
+ */
+ this.discoveryService_ = discoveryService;
+
+ /**
+ * @private @const {!ActivityRecords}
+ */
+ this.activityRecords_ = activityRecords;
+
+ /**
+ * Keeps track of pending requests to avoid duplication. Values are
+ * Promises returning strings of the form <sink.getId()>:<appName>.
+ * @private @const {!Map<string, !Promise<!DialClient.AppInfo>>}
+ */
+ this.pendingRequests_ = new Map();
+
+ /**
+ * DIAL clients used by this service. Keys are sink ids.
+ * @private @const {!Map<string, !DialClient.Client>}
+ */
+ this.clientsBySinkId_ = new Map();
+
+ /**
+ * The timeout ID for the next scan.
+ * @private {?number}
+ */
+ this.timeoutId_ = null;
+
+ /**
+ * Whether the service is running.
+ * @private {boolean}
+ */
+ this.running_ = false;
+ }
+
+ init() {
+ PersistentDataManager.register(this);
+ }
+
+ /**
+ * Starts discovery.
+ */
+ start() {
+ this.logger_.info('Starting periodic scanning.');
+ if (!this.running_) {
+ this.running_ = true;
+ this.doScan_();
+ }
+ }
+
+ /**
+ * Stops discovery. Also aborts all outstanding discovery requests.
+ */
+ stop() {
+ this.logger_.info('Stopping periodic scanning.');
+ if (!this.running_) return;
+ this.running_ = false;
+ if (this.timeoutId_) {
+ clearTimeout(this.timeoutId_);
+ this.timeoutId_ = null;
+ }
+ this.pendingRequests_.clear();
+ }
+
+ /**
+ * Registers an application name that should be queried. Starts periodic
+ * scanning if it hasn't already started. Otherwise, issues an out-of-band
+ * query for the app against all discovered sinks.
+ * @param {string} appName
+ */
+ registerApp(appName) {
+ if (this.registeredApps_.has(appName) &&
+ !this.anyUnknownAppStatus_(appName)) {
+ // No need to scan since status for appName is known for all sinks.
+ return;
+ }
+
+ this.registeredApps_.add(appName);
+
+ if (this.discoveryService_.getSinkCount() == 0) {
+ // No sinks, no need to scan.
+ return;
+ }
+ if (!this.running_) {
+ this.start();
+ } else {
+ this.discoveryService_.getSinks().forEach(
+ this.doScanSinkForApp_.bind(this, appName));
+ }
+ }
+
+ /**
+ * Unregisters an application name.
+ * @param {string} appName
+ */
+ unregisterApp(appName) {
+ this.registeredApps_.delete(appName);
+ }
+
+ /**
+ * @return {!Array<string>}
+ */
+ getRegisteredApps() {
+ return Array.from(this.registeredApps_);
+ }
+
+ /**
+ * @return {number}
+ */
+ getAppCount() {
+ return this.registeredApps_.size;
+ }
+
+ /**
+ * Issues app info queries and updates sink app status and activities
+ * according to results. Queries will be made for the following:
+ * 1) All registered apps, against all discovered sinks. Note that a (app,
+ * sink) combination is only queried if its status is unknown, or if enough
+ * time has elapsed since its previous status was known.
+ * 2) Each activity's app against its sink.
+ * Once all queries have returned and all updates have been made, schedules a
+ * scan in the future.
+ * @private
+ */
+ doScan_() {
+ this.logger_.info('Start app status scan.');
+ const promises = [];
+
+ // Scans all sinks against all registered apps.
+ this.discoveryService_.getSinks().forEach(sink => {
+ promises.push.apply(promises, this.scanSink(sink));
+ });
+
+ // Scans all activities.
+ this.activityRecords_.getActivities().forEach(activity => {
+ promises.push(this.scanActivity_(activity));
+ });
+
+ PromiseUtils.allSettled(promises).then(() => {
+ if (this.running_ && !this.timeoutId_) {
+ this.logger_.fine('Scan complete; scheduling for next scan.');
+ // NOTE(imcheng): setTimeout with a large delay does not work well in
+ // event pages. Instead we should be using chrome.alarms API, but note
+ // that it is subject to a minmum delay of 1 minute.
+ this.timeoutId_ = setTimeout(() => {
+ this.doRescan_();
+ }, AppDiscoveryService.CHECK_INTERVAL_MILLIS);
+ }
+ });
+ }
+
+ /**
+ * Clears the timer and starts a round of scanning. Only valid when called
+ * from a timer.
+ * @private
+ */
+ doRescan_() {
+ this.logger_.fine('Start app status scan (timer-based)');
+ this.timeoutId_ = null;
+ this.doScan_();
+ }
+
+ /**
+ * Asynchronously scans a sink against all registered apps and updates its app
+ * status map if needed.
+ * @param {!DialSink} sink
+ * @return {!Array<!Promise<void>>} If the sink does not support app
+ * the getting app info, returns an empty array. Otherwise, returns a list
+ * of Promises, each corresponding to a registered app, resolved when the
+ * sink's app status has been updated.
+ */
+ scanSink(sink) {
+ if (!sink.supportsAppAvailability()) {
+ return [];
+ }
+
+ const promises = [];
+ for (let appName of this.registeredApps_) {
+ promises.push(this.doScanSinkForApp_(appName, sink));
+ }
+ return promises;
+ }
+
+ /**
+ * Issues an app info query for the given app to the given sink, and updates
+ * the sink's app status map with the response.
+ * @param {string} appName
+ * @param {!DialSink} sink
+ * @return {!Promise<void>} Resolved when the sink's app status has been
+ * updated.
+ * @private
+ */
+ doScanSinkForApp_(appName, sink) {
+ if (sink.getAppStatus(appName) != DialSinkAppStatus.UNKNOWN &&
+ Date.now() - sink.getAppStatusTimeStamp(appName) <
+ AppDiscoveryService.CACHE_PERIOD_) {
+ // App status already known and not expired.
+ return Promise.resolve();
+ }
+ if (!sink.supportsAppAvailability()) {
+ return Promise.resolve();
+ }
+ this.logger_.fine(
+ 'Querying ' + sink.getId() + ' for ' + appName +
+ ' to update app status');
+ return this.getAppInfo_(sink, appName)
+ .then(
+ appInfo => {
+ const newAppStatus = this.getAvailabilityFromAppInfo_(appInfo);
+ this.maybeUpdateAppStatus_(sink, appName, newAppStatus);
+ },
+ e => {
+ // Some devices return NOT_FOUND for GetAppInfo to indicate the
+ // app is unavailable.
+ if (e instanceof DialClient.AppInfoNotFoundError) {
+ this.maybeUpdateAppStatus_(
+ sink, appName, DialSinkAppStatus.UNAVAILABLE);
+ return;
+ }
+ this.logger_.warning(
+ 'Failed to process app availability; ' + sink.getId() +
+ ' does not support app availability');
+ sink.setSupportsAppAvailability(false);
+ });
+ }
+
+ /**
+ * Issues an app info query for the given activity's app to the activity's
+ * sink, and updates the activity records if the app is no longer running.
+ * @param {!mr.dial.Activity} activity
+ * @return {!Promise<void>} Resolved when the activity record has possibly
+ * been updated.
+ * @private
+ */
+ scanActivity_(activity) {
+ const sink = this.discoveryService_.getSinkById(activity.route.sinkId);
+ if (!sink) {
+ this.logger_.warning(
+ 'Activity refers to nonexistent sink: ' + activity.route.id);
+ return Promise.resolve();
+ }
+ if (!sink.supportsAppAvailability()) {
+ return Promise.resolve();
+ }
+ const appName = activity.appName;
+ this.logger_.fine(
+ 'Querying ' + sink.getId() + ' for ' + appName + ' to update activity');
+ return this.getAppInfo_(sink, appName)
+ .then(
+ appInfo => this.maybeUpdateActivityRecord_(
+ /** @type {!DialSink} */ (sink), appName, appInfo),
+ e => this.doRemoveActivityRecord_(
+ /** @type {!DialSink} */ (sink), appName));
+ }
+
+ /**
+ * Gets the DIAL client associated with the given sink, or creates one if it
+ * does not exist.
+ * @param {!DialSink} sink
+ * @return {!DialClient.Client} A client instance for the given sink.
+ * @private
+ */
+ getDialClient_(sink) {
+ let client = this.clientsBySinkId_.get(sink.getId());
+ if (!client) {
+ client = new DialClient.Client(sink);
+ this.logger_.fine('Created DIAL client for ' + sink.getId());
+ this.clientsBySinkId_.set(sink.getId(), client);
+ }
+ return client;
+ }
+
+ /**
+ * Issues an app info query with the given sink's DIAL client. A query will
+ * not be issued if there is already a same one pending.
+ * @param {!DialSink} sink
+ * @param {string} appName
+ * @return {!Promise<!DialClient.AppInfo>} Resolved with the query response,
+ * or rejected on error.
+ * @private
+ */
+ getAppInfo_(sink, appName) {
+ const requestId = AppDiscoveryService.getRequestId_(sink, appName);
+ let promise = this.pendingRequests_.get(requestId);
+ if (promise) {
+ return promise;
+ }
+
+ promise = this.getDialClient_(sink).getAppInfo(appName);
+ this.pendingRequests_.set(requestId, promise);
+ const cleanup = () => {
+ this.pendingRequests_.delete(requestId);
+ };
+ promise.then(cleanup, cleanup);
+ return promise;
+ }
+
+ /**
+ * Translates the given app info result into a DialSinkAppStatus value.
+ * @param {!DialClient.AppInfo} appInfo
+ * @return {DialSinkAppStatus}
+ * @private
+ */
+ getAvailabilityFromAppInfo_(appInfo) {
+ if (appInfo.name == 'Netflix') {
+ return this.getAppStatusFromNetflixAppInfo_(appInfo);
+ } else {
+ switch (appInfo.state) {
+ case DialClient.DialAppState.RUNNING:
+ case DialClient.DialAppState.STOPPED:
+ return DialSinkAppStatus.AVAILABLE;
+ default:
+ return DialSinkAppStatus.UNAVAILABLE;
+ }
+ }
+ }
+
+ /**
+ * Returns app status from a Netflix app info.
+ * @param {!DialClient.AppInfo} appInfo The app info from DIAL GET.
+ * @return {DialSinkAppStatus}
+ * @private
+ */
+ getAppStatusFromNetflixAppInfo_(appInfo) {
+ const isNetflixWebsocket =
+ appInfo.extraData && appInfo.extraData['capabilities'] == 'websocket';
+ if (isNetflixWebsocket &&
+ (appInfo.state == DialClient.DialAppState.RUNNING ||
+ appInfo.state == DialClient.DialAppState.STOPPED)) {
+ return DialSinkAppStatus.AVAILABLE;
+ } else {
+ return DialSinkAppStatus.UNAVAILABLE;
+ }
+ }
+
+ /**
+ * Returns the request ID to use for an app info request.
+ * @param {!DialSink} sink
+ * @param {string} appName
+ * @return {string}
+ * @private
+ */
+ static getRequestId_(sink, appName) {
+ return sink.getId() + ':' + appName;
+ }
+
+ /**
+ * Checks whether the status of an app on a sink has changed, and if so
+ * notifies discovery service.
+ * @param {!DialSink} sink
+ * @param {string} appName
+ * @param {DialSinkAppStatus} newAppStatus
+ * @private
+ */
+ maybeUpdateAppStatus_(sink, appName, newAppStatus) {
+ this.logger_.fine(
+ 'Got app status ' + newAppStatus + ' from ' + sink.getId() + ' for ' +
+ appName);
+ const oldAppStatus = sink.getAppStatus(appName);
+ sink.setAppStatus(appName, newAppStatus);
+ if (newAppStatus != oldAppStatus) {
+ this.discoveryService_.onAppStatusChanged(appName, sink);
+ }
+ }
+
+ /**
+ * Checks whether the given app is no longer running on the given sink, and if
+ * so notifies activity records.
+ * @param {!DialSink} sink
+ * @param {string} appName
+ * @param {!DialClient.AppInfo} appInfo
+ * @private
+ */
+ maybeUpdateActivityRecord_(sink, appName, appInfo) {
+ if (appInfo.state != DialClient.DialAppState.RUNNING) {
+ this.doRemoveActivityRecord_(sink, appName);
+ }
+ }
+
+ /**
+ * Removes the activity record with the given sink and app name, if it exists.
+ * @param {!DialSink} sink
+ * @param {string} appName
+ * @private
+ */
+ doRemoveActivityRecord_(sink, appName) {
+ const activity = this.activityRecords_.getBySinkId(sink.getId());
+ if (activity && activity.appName == appName) {
+ this.activityRecords_.removeByRouteId(activity.route.id);
+ }
+ }
+
+ /**
+ * Checks if there is a sink whose status of appName is unknown.
+ * @param {string} appName
+ * @return {boolean}
+ * @private
+ */
+ anyUnknownAppStatus_(appName) {
+ return this.discoveryService_.getSinks().some(
+ s => s.getAppStatus(appName) == DialSinkAppStatus.UNKNOWN);
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'dial.AppDiscoveryService';
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ return [this.getRegisteredApps()];
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {
+ const savedData = PersistentDataManager.getTemporaryData(this);
+ this.registeredApps_ = new Set(savedData || []);
+ }
+};
+
+
+/**
+ * The interval for periodic scanning.
+ * @package @const {number}
+ */
+AppDiscoveryService.CHECK_INTERVAL_MILLIS = 60 * 1000;
+
+
+/**
+ * The amount of time an availability result for an app (both AVAILABLE and
+ * UNAVAILABLE) will be cached for.
+ * @private @const {number}
+ */
+AppDiscoveryService.CACHE_PERIOD_ = 60 * 60 * 1000;
+
+
+exports = AppDiscoveryService;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_app_discovery_service_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_app_discovery_service_test.js
new file mode 100644
index 00000000000..cd7edeac16b
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_app_discovery_service_test.js
@@ -0,0 +1,362 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.AppDiscoveryServiceTest');
+goog.setTestOnly('mr.dial.AppDiscoveryServiceTest');
+
+const Activity = goog.require('mr.dial.Activity');
+const ActivityRecords = goog.require('mr.dial.ActivityRecords');
+const AppDiscoveryService = goog.require('mr.dial.AppDiscoveryService');
+const DialClient = goog.require('mr.dial.Client');
+const DialSink = goog.require('mr.dial.Sink');
+const DialSinkAppStatus = goog.require('mr.dial.SinkAppStatus');
+const PersistentDataManager = goog.require('mr.PersistentDataManager');
+const Route = goog.require('mr.Route');
+const UnitTestUtils = goog.require('mr.UnitTestUtils');
+
+describe('DIAL AppDiscoveryService Tests', function() {
+ let activityRecords;
+ let service;
+ let mockClock;
+ let mockDiscoveryService;
+ let mockActivityCallbacks;
+ let sink1;
+ let sink2;
+ let sink3;
+ let mockDialClient;
+ let stoppedAppInfo;
+ let runningAppInfo;
+ let installableAppInfo;
+ let stoppedAppInfoWithWebsocket;
+
+ const setUpGetAppInfoResponse = function(appInfo) {
+ mockDialClient.getAppInfo.and.callFake(() => Promise.resolve(appInfo));
+ };
+
+ const setUpGetAppInfoError = function() {
+ mockDialClient.getAppInfo.and.callFake(
+ () => Promise.reject(new Error('getAppInfo failed')));
+ };
+
+ const setUpGetAppInfoNotFoundError = function() {
+ mockDialClient.getAppInfo.and.callFake(
+ () => Promise.reject(new DialClient.AppInfoNotFoundError()));
+ };
+
+ beforeEach(function() {
+ mockClock = UnitTestUtils.useMockClockAndPromises();
+
+ mockDiscoveryService = jasmine.createSpyObj(
+ 'discoveryService',
+ ['getSinks', 'getSinkById', 'getSinkCount', 'onAppStatusChanged']);
+ mockActivityCallbacks = jasmine.createSpyObj(
+ 'activityCallbacks',
+ ['onActivityAdded', 'onActivityRemoved', 'onActivityUpdated']);
+ activityRecords = new ActivityRecords(mockActivityCallbacks);
+ service = new AppDiscoveryService(mockDiscoveryService, activityRecords);
+
+ sink1 = new DialSink('sink1', '1').setSupportsAppAvailability(true);
+ sink2 = new DialSink('sink2', '2').setSupportsAppAvailability(true);
+ sink3 = new DialSink('sink3', '3').setSupportsAppAvailability(false);
+ mockDialClient = jasmine.createSpyObj('dialClient', ['getAppInfo']);
+ spyOn(AppDiscoveryService.prototype, 'getDialClient_')
+ .and.returnValue(mockDialClient);
+ stoppedAppInfo = {'state': DialClient.DialAppState.STOPPED};
+ runningAppInfo = {'state': DialClient.DialAppState.RUNNING};
+ installableAppInfo = {'state': DialClient.DialAppState.INSTALLABLE};
+ stoppedAppInfoWithWebsocket = {
+ 'name': 'Netflix',
+ 'state': DialClient.DialAppState.STOPPED,
+ 'extraData': {'capabilities': 'websocket'}
+ };
+
+ chrome.runtime = {
+ id: 'fakeId',
+ getManifest: function() {
+ return {version: 'fakeVersion'};
+ }
+ };
+ });
+
+ afterEach(function() {
+ service.stop();
+ UnitTestUtils.restoreRealClockAndPromises();
+ PersistentDataManager.clear();
+ });
+
+ describe('Tests registerApp', function() {
+ beforeEach(function() {
+ mockDiscoveryService.getSinks.and.returnValue([sink1, sink2, sink3]);
+ mockDiscoveryService.getSinkCount.and.returnValue(3);
+ });
+
+ const expectAppStatus = function(expectedAppStatus, appName) {
+ service.init();
+
+ expect(sink1.getAppStatus(appName)).toEqual(DialSinkAppStatus.UNKNOWN);
+ expect(sink2.getAppStatus(appName)).toEqual(DialSinkAppStatus.UNKNOWN);
+ expect(sink3.getAppStatus(appName)).toEqual(DialSinkAppStatus.UNKNOWN);
+
+ service.registerApp(appName);
+
+ service.start();
+ // Let internal promises to resolve or reject.
+ mockClock.tick(1);
+
+ expect(sink1.getAppStatus(appName)).toEqual(expectedAppStatus);
+ expect(sink2.getAppStatus(appName)).toEqual(expectedAppStatus);
+ expect(sink3.getAppStatus(appName)).toEqual(DialSinkAppStatus.UNKNOWN);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(2);
+ };
+
+ it('Response indicates app was stopped', function() {
+ setUpGetAppInfoResponse(stoppedAppInfo);
+ expectAppStatus(DialSinkAppStatus.AVAILABLE, 'YouTube');
+ });
+
+ it('Response indicates app was running', function() {
+ setUpGetAppInfoResponse(runningAppInfo);
+ expectAppStatus(DialSinkAppStatus.AVAILABLE, 'YouTube');
+ });
+
+ it('Response indicates app was installable', function() {
+ setUpGetAppInfoResponse(installableAppInfo);
+ expectAppStatus(DialSinkAppStatus.UNAVAILABLE, 'YouTube');
+ });
+
+ it('Response has invalid app info', function() {
+ setUpGetAppInfoError();
+ expectAppStatus(DialSinkAppStatus.UNKNOWN, 'YouTube');
+ expect(sink1.supportsAppAvailability()).toBe(false);
+ expect(sink2.supportsAppAvailability()).toBe(false);
+ });
+
+ it('Response indicates not found', function() {
+ setUpGetAppInfoNotFoundError();
+ expectAppStatus(DialSinkAppStatus.UNAVAILABLE, 'YouTube');
+ });
+
+ it('Netflix with special stopped info', function() {
+ setUpGetAppInfoResponse(stoppedAppInfoWithWebsocket);
+ expectAppStatus(DialSinkAppStatus.AVAILABLE, 'Netflix');
+ });
+
+ it('Netflix with normal stopped info', function() {
+ stoppedAppInfo.name = 'Netflix';
+ setUpGetAppInfoResponse(stoppedAppInfo);
+ expectAppStatus(DialSinkAppStatus.UNAVAILABLE, 'Netflix');
+ });
+
+ it('No new query generated when registering existing app with known status',
+ function() {
+ setUpGetAppInfoResponse(stoppedAppInfo);
+ expectAppStatus(DialSinkAppStatus.AVAILABLE, 'YouTube');
+ // register again
+ service.registerApp('YouTube');
+ mockClock.tick(1);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(2);
+ // unregister does nothing because sink already had the app status.
+ service.unregisterApp('YouTube');
+ service.registerApp('YouTube');
+ mockClock.tick(1);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(2);
+ // unless clear app status first.
+ sink1.clearAppStatus();
+ sink2.clearAppStatus();
+ service.unregisterApp('YouTube');
+ service.registerApp('YouTube');
+ mockClock.tick(1);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(4);
+ });
+
+ it('No new query generated when register existing app with unknown status',
+ function() {
+ setUpGetAppInfoError();
+ expectAppStatus(DialSinkAppStatus.UNKNOWN, 'YouTube');
+ // Make sink1 have known status. But sink2 has unknown status.
+ sink1.setAppStatus('YouTube', DialSinkAppStatus.UNAVAILABLE);
+ // register again
+ service.registerApp('YouTube');
+ mockClock.tick(1);
+ // No re-query
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(2);
+ });
+ });
+
+ describe('Tests app status caching and onAppStatusChanged', function() {
+ beforeEach(function() {
+ mockDiscoveryService.getSinks.and.returnValue([sink1]);
+ mockDiscoveryService.getSinkCount.and.returnValue(1);
+ });
+
+ it('Known app status does not change during caching period', function() {
+ service.init();
+ setUpGetAppInfoResponse(stoppedAppInfo);
+ expect(sink1.getAppStatus('YouTube')).toEqual(DialSinkAppStatus.UNKNOWN);
+ service.registerApp('YouTube');
+ service.start();
+ mockClock.tick(1);
+ expect(sink1.getAppStatus('YouTube'))
+ .toEqual(DialSinkAppStatus.AVAILABLE);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(1);
+ expect(mockDiscoveryService.onAppStatusChanged.calls.count()).toBe(1);
+
+ // Make app unavailable
+ setUpGetAppInfoNotFoundError();
+ service.doScan_();
+ mockClock.tick(1);
+ // No new query and app is still available since cache is not expired.
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(1);
+ expect(sink1.getAppStatus('YouTube'))
+ .toEqual(DialSinkAppStatus.AVAILABLE);
+ expect(mockDiscoveryService.onAppStatusChanged.calls.count()).toBe(1);
+ });
+
+ it('Does not re-query app status when getAppInfo throws error', function() {
+ service.init();
+ setUpGetAppInfoError();
+ expect(sink1.getAppStatus('YouTube')).toEqual(DialSinkAppStatus.UNKNOWN);
+ service.registerApp('YouTube');
+ service.start();
+ mockClock.tick(1);
+ expect(sink1.getAppStatus('YouTube')).toEqual(DialSinkAppStatus.UNKNOWN);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(1);
+ expect(mockDiscoveryService.onAppStatusChanged.calls.count()).toBe(0);
+
+ service.doScan_();
+ mockClock.tick(1);
+ // No new query
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(1);
+ expect(sink1.getAppStatus('YouTube')).toEqual(DialSinkAppStatus.UNKNOWN);
+ expect(mockDiscoveryService.onAppStatusChanged.calls.count()).toBe(0);
+ });
+
+ it('Cache expires, re-query, different status', function() {
+ service.init();
+ setUpGetAppInfoResponse(stoppedAppInfo);
+ expect(sink1.getAppStatus('YouTube')).toEqual(DialSinkAppStatus.UNKNOWN);
+ service.registerApp('YouTube');
+ service.start();
+ mockClock.tick(1);
+ expect(sink1.getAppStatus('YouTube'))
+ .toEqual(DialSinkAppStatus.AVAILABLE);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(1);
+ expect(mockDiscoveryService.onAppStatusChanged.calls.count()).toBe(1);
+
+ // Make app unavailable
+ setUpGetAppInfoNotFoundError();
+ // Make cache expire
+ mockClock.tick(AppDiscoveryService.CACHE_PERIOD_);
+ service.doScan_();
+ mockClock.tick(1);
+ // new query and app becomes unavailable
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(2);
+ expect(sink1.getAppStatus('YouTube'))
+ .toEqual(DialSinkAppStatus.UNAVAILABLE);
+ expect(mockDiscoveryService.onAppStatusChanged.calls.count()).toBe(2);
+ });
+
+ it('Cache expires, re-query, same status', function() {
+ service.init();
+ setUpGetAppInfoResponse(stoppedAppInfo);
+ expect(sink1.getAppStatus('YouTube')).toEqual(DialSinkAppStatus.UNKNOWN);
+ service.registerApp('YouTube');
+ service.start();
+ mockClock.tick(1);
+ expect(sink1.getAppStatus('YouTube'))
+ .toEqual(DialSinkAppStatus.AVAILABLE);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(1);
+ expect(mockDiscoveryService.onAppStatusChanged.calls.count()).toBe(1);
+
+ // Make cache expire
+ mockClock.tick(AppDiscoveryService.CACHE_PERIOD_);
+ service.doScan_();
+ mockClock.tick(1);
+ // New query and app becomes unavailable
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(2);
+ expect(sink1.getAppStatus('YouTube'))
+ .toEqual(DialSinkAppStatus.AVAILABLE);
+ // Did not trigger app status changed event
+ expect(mockDiscoveryService.onAppStatusChanged.calls.count()).toBe(1);
+ });
+ });
+
+ describe('Tests activity scanning', function() {
+ beforeEach(function() {
+ mockDiscoveryService.getSinks.and.returnValue([sink1, sink2, sink3]);
+ mockDiscoveryService.getSinkCount.and.returnValue(3);
+ mockDiscoveryService.getSinkById.and.returnValue(sink1);
+
+ service.init();
+ const route = Route.createRoute(
+ 'presentationId', 'providerName', sink1.getId(), 'source', true,
+ 'description', null);
+ const activity = new Activity(route, 'YouTube');
+ activityRecords.add(activity);
+ expect(mockActivityCallbacks.onActivityAdded).toHaveBeenCalled();
+ });
+
+ it('Activity is removed when app is no longer running', function() {
+ setUpGetAppInfoResponse(stoppedAppInfo);
+ service.start();
+ mockClock.tick(1);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(1);
+ expect(mockActivityCallbacks.onActivityRemoved.calls.count()).toBe(1);
+ expect(activityRecords.getActivityCount()).toBe(0);
+ });
+
+ it('Activity is not removed when app is still running', function() {
+ setUpGetAppInfoResponse(runningAppInfo);
+ service.start();
+ mockClock.tick(1);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(1);
+ expect(mockActivityCallbacks.onActivityRemoved).not.toHaveBeenCalled();
+ expect(activityRecords.getActivityCount()).toBe(1);
+
+ // Trigger periodic rescan
+ mockClock.tick(AppDiscoveryService.CHECK_INTERVAL_MILLIS + 1);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(2);
+ expect(mockActivityCallbacks.onActivityRemoved).not.toHaveBeenCalled();
+ expect(activityRecords.getActivityCount()).toBe(1);
+
+ // No more periodic rescan.
+ service.stop();
+ mockClock.tick(AppDiscoveryService.CHECK_INTERVAL_MILLIS + 1);
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(2);
+ });
+
+ it('Activity scanning not duplicated with app scanning', function() {
+ setUpGetAppInfoResponse(stoppedAppInfo);
+ service.registerApp('YouTube');
+ mockClock.tick(1);
+ // One for sink1 and one for sink2. Activity scanning does not issue a
+ // duplicated request. Both app status and activity status can be updated
+ // with the same response.
+ expect(mockDialClient.getAppInfo.calls.count()).toBe(2);
+ expect(sink1.getAppStatus('YouTube'))
+ .toEqual(DialSinkAppStatus.AVAILABLE);
+ expect(sink2.getAppStatus('YouTube'))
+ .toEqual(DialSinkAppStatus.AVAILABLE);
+ expect(mockActivityCallbacks.onActivityRemoved.calls.count()).toBe(1);
+ expect(activityRecords.getActivityCount()).toBe(0);
+ });
+ });
+
+ it('Persistent with data', function() {
+ mockDiscoveryService.getSinks.and.returnValue([]);
+ mockDiscoveryService.getSinkCount.and.returnValue(0);
+ const mockServiceToSuspend = new AppDiscoveryService(
+ mockDiscoveryService, new ActivityRecords(mockActivityCallbacks));
+ mockServiceToSuspend.init();
+ mockServiceToSuspend.registerApp('app1');
+ mockServiceToSuspend.registerApp('app2');
+ PersistentDataManager.suspendForTest();
+
+ const mockServiceToLoad = new AppDiscoveryService(
+ mockDiscoveryService, new ActivityRecords(mockActivityCallbacks));
+ mockServiceToLoad.loadSavedData();
+ expect(mockServiceToLoad.getRegisteredApps()).toEqual(['app1', 'app2']);
+ expect(mockServiceToLoad.getAppCount()).toEqual(2);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_client.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_client.js
new file mode 100644
index 00000000000..52759a070fd
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_client.js
@@ -0,0 +1,325 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Client wrapper for interacting with DIAL devices and data
+ * structures for responses.
+ */
+
+goog.module('mr.dial.Client');
+goog.module.declareLegacyNamespace();
+
+const Assertions = goog.require('mr.Assertions');
+const Logger = goog.require('mr.Logger');
+const NetUtils = goog.require('mr.NetUtils');
+const XhrManager = goog.require('mr.XhrManager');
+const XhrUtils = goog.require('mr.XhrUtils');
+
+/**
+ * Possible states of a DIAL application.
+ * @enum {string}
+ */
+const DialAppState = {
+ /** The app is running. */
+ RUNNING: 'running',
+ /** The app is not running. */
+ STOPPED: 'stopped',
+ /** The app can be installed. */
+ INSTALLABLE: 'installable',
+ /**
+ * An error was encountered getting the state.
+
+ */
+ ERROR: 'error'
+};
+
+
+/**
+ * Holds data parsed from a DIAL GET response.
+ */
+const AppInfo = class {
+ constructor() {
+ /**
+ * The application name. Mandatory.
+ * @type {string}
+ */
+ this.name = 'unknown';
+
+ /**
+ * The reported state of the application.
+ * @type {DialAppState}
+ */
+ this.state = DialAppState.ERROR;
+
+ /**
+ * If the application's state is INSTALLABLE, then the URL where the app
+ * can be installed.
+ * @type {?string}
+ */
+ this.installUrl = null;
+
+ /**
+ * Whether the DELETE operation is supported.
+ * @type {boolean}
+ */
+ this.allowStop = true;
+
+ /**
+ * If the applications's state is RUNNING, a resource identifier for the
+ * running application.
+ * @type {?string}
+ */
+ this.resource = null;
+
+ /**
+ * Application-specific data included with the GET response that is not part
+ * of the official specifciations.
+ * @type {!Object<string, string>}
+ */
+ this.extraData = {};
+ }
+};
+
+
+/**
+ * Indicates that the DIAL sink returned NOT_FOUND in response to a GET request.
+ */
+const AppInfoNotFoundError = class extends Error {
+ constructor() {
+ super();
+ }
+};
+
+
+/**
+ * Wrapper for a DIAL sink used for communicating with it.
+ */
+const Client = class {
+ /**
+ * @param {!mr.dial.Sink} sink
+ * @param {!XhrManager=} xhrManager Manager for all requests.
+ */
+ constructor(sink, xhrManager = Client.getXhrManager_()) {
+ // NOTE(mfoltz,haibinlu): We do not assert if the sink supports DIAL,
+ // since combined discovery uses DialClient to check app info to see if a
+ // mDNS-discovered sink is also a DIAL sink.
+ Assertions.assert(
+ sink.getDialAppUrl(), 'Receiver must have a DIAL app URL set.');
+
+ /** @private @const {!mr.dial.Sink} */
+ this.sink_ = sink;
+
+ /** @private @const {!XhrManager} */
+ this.xhrManager_ = xhrManager;
+
+ /** @private @const {?Logger} */
+ this.logger_ = Logger.getInstance('mr.dial.Client');
+ }
+
+ /**
+ * @param {!mr.dial.Sink} sink
+ * @return {!Client}
+ */
+ static create(sink) {
+ return new Client(sink);
+ }
+
+ /**
+ * Returns the default XhrManager, creating it if necessary.
+ * @return {!XhrManager}
+ * @private
+ */
+ static getXhrManager_() {
+ if (!Client.xhrManager_) {
+ Client.xhrManager_ = new XhrManager(
+ /* maxRequests */ 10,
+ /* defaultTimeoutMillis */ 2000,
+ /* defaultNumAttempts */ 1);
+ }
+ return Client.xhrManager_;
+ }
+
+ /**
+ * @param {string} state A string representing a DIAL application state.
+ * @return {DialAppState} The corresponding state or ERROR if the
+ * state is invalid.
+ * @private
+ */
+ static parseDialAppState_(state) {
+ switch (state) {
+ case 'running':
+ return DialAppState.RUNNING;
+ case 'stopped':
+ return DialAppState.STOPPED;
+ default:
+ return DialAppState.ERROR;
+ }
+ }
+
+ /**
+ * Launches an application on the sink.
+ * @param {string} appName Name of the DIAL application to launch.
+ * @param {string} postData Data to include in the HTTP POST request.
+ * @return {!Promise<void>} Fulfilled when the operation completes
+ * successfully. Rejected otherwise.
+ */
+ launchApp(appName, postData) {
+ return this.xhrManager_
+ .send(
+ this.getAppUrl_(appName), 'POST', postData, {timeoutMillis: 15000})
+ .then(xhr => this.handleResponse_('launchApp', 'POST', xhr));
+ }
+
+ /**
+ * Stops a running application on the sink.
+ * @param {string} appName Name of the DIAL application to stop.
+ * @return {!Promise<void>} Fulfilled when the operation completes
+ * successfully. Rejected otherwise.
+ */
+ stopApp(appName) {
+ return this.xhrManager_.send(this.getAppUrl_(appName), 'DELETE')
+ .then(xhr => this.handleResponse_('stopApp', 'DELETE', xhr));
+ }
+
+ /**
+ * Gets information about a running application on the sink.
+ * @param {string} appName Name of the DIAL application to get info from.
+ * @return {!Promise<!AppInfo>} Fulfilled with AppInfo. Rejected if the
+ * operation did not complete successfully. In the case of the sink
+ * returning NOT_FOUND for the request, AppInfoNotFoundError will be
+ * thrown.
+ */
+ getAppInfo(appName) {
+ return this.xhrManager_
+ .send(this.getAppUrl_(appName), 'GET', undefined, {numAttempts: 3})
+ .then(xhr => this.handleGetAppInfoResponse_(appName, xhr));
+ }
+
+ /**
+ * Parses the response from a Xhr GET request.
+ * @param {string} appName App nam used in the request.
+ * @param {!XMLHttpRequest} xhr
+ * @return {!AppInfo}
+ * @private
+ */
+ handleGetAppInfoResponse_(appName, xhr) {
+ XhrUtils.logRawXhr(this.logger_, 'GetAppInfo', 'GET', xhr);
+ if (!XhrUtils.isSuccess(xhr)) {
+ if (xhr.status == NetUtils.HttpStatus.NOT_FOUND) {
+ throw new AppInfoNotFoundError();
+ } else {
+ throw new Error(`Response error: ${xhr.status}`);
+ }
+ }
+
+ const xml = XhrUtils.parseXml(xhr.responseText);
+ if (!xml) {
+ this.logger_.info('Invalid or empty response');
+ throw new Error('Invalid or empty response');
+ }
+
+ const service = xml.getElementsByTagName('service');
+ if (!service || service.length != 1) {
+ this.logger_.info('Invalid GET response (invalid service)');
+ throw new Error('Invalid GET response (invalid service)');
+ }
+ const appInfo = new AppInfo();
+ for (var i = 0, l = service[0].childNodes.length; i < l; i++) {
+ const node = service[0].childNodes[i];
+ if (node.nodeName == 'state') {
+ appInfo.state = Client.parseDialAppState_(node.textContent);
+ } else if (node.nodeName == 'name') {
+ appInfo.name = node.textContent;
+ } else if (node.nodeName == 'link') {
+ appInfo.resource = node.getAttribute('href');
+ } else if (node.nodeName == 'options') {
+ // The default value for allowStop is true per DIAL spec.
+ appInfo.allowStop = (node.getAttribute('allowStop') != 'false');
+ } else {
+ appInfo.extraData[node.nodeName] = node.innerHTML;
+ }
+ }
+
+ // Validate mandatory fields (name, state).
+ if (appInfo.name == 'unknown') {
+ this.logger_.info('GET response missing name value');
+ throw new Error('GET response missing name value');
+ }
+
+ if (appInfo.name != appName) {
+ this.logger_.info('GET app name mismatch');
+ throw new Error('GET app name mismatch');
+ }
+
+ if (appInfo.state == DialAppState.ERROR) {
+ this.logger_.info('GET response missing state value');
+ throw new Error('GET response missing state value');
+ }
+
+ // Parse state.
+ const installable = /installable=(.+)/.exec(appInfo.state);
+ if (installable && installable[1]) {
+ appInfo.state = DialAppState.INSTALLABLE;
+ appInfo.installUrl = installable[1];
+ } else if (
+ appInfo.state == DialAppState.RUNNING ||
+ appInfo.state == DialAppState.STOPPED) {
+ // Valid state. Continue.
+ } else {
+ this.logger_.info('GET response has invalid state value');
+ throw new Error('GET response has invalid state value');
+ }
+
+ // Success!
+ return appInfo;
+ }
+
+ /**
+ * Returns the URL used to communicate with a given DIAL application.
+ * @param {string} appName The name of the DIAL application.
+ * @return {string} The URL for the activity.
+ * @private
+ */
+ getAppUrl_(appName) {
+ let appUrl = this.sink_.getDialAppUrl();
+ if (appUrl.charAt(appUrl.length - 1) != '/') {
+ appUrl += '/';
+ }
+ return appUrl + appName;
+ }
+
+ /**
+ * Logs the given response and returns a Promise that resolves if it indicates
+ * success.
+ * @param {string} action Name of the operation that created the request.
+ * @param {string} method The HTTP method.
+ * @param {!XMLHttpRequest} xhr
+ * @return {!Promise<void>} Resolves if the response indicates success,
+ * rejected otherwise.
+ * @private
+ */
+ handleResponse_(action, method, xhr) {
+ return new Promise((resolve, reject) => {
+ XhrUtils.logRawXhr(this.logger_, action, method, xhr);
+ if (XhrUtils.isSuccess(xhr)) {
+ resolve();
+ } else {
+ reject(Error(xhr.statusText));
+ }
+ });
+ }
+};
+
+
+/**
+ * Lazily instantiated and shared between DialClient instances.
+ * @private {?XhrManager}
+ */
+Client.xhrManager_ = null;
+
+
+exports.AppInfo = AppInfo;
+exports.AppInfoNotFoundError = AppInfoNotFoundError;
+exports.Client = Client;
+exports.DialAppState = DialAppState;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_client_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_client_test.js
new file mode 100644
index 00000000000..0c1979d405f
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_client_test.js
@@ -0,0 +1,284 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.ClientTest');
+goog.setTestOnly('mr.dial.ClientTest');
+
+const DialClient = goog.require('mr.dial.Client');
+const DialSink = goog.require('mr.dial.Sink');
+const XhrUtils = goog.require('mr.XhrUtils');
+
+describe('Dial Client Tests', function() {
+ let client;
+ let mockXhrManager;
+ let sink;
+ const appUrl = 'http://198.0.0.100/apps';
+
+
+ beforeEach(function() {
+ sink = new DialSink('sink', 'sinkid');
+ sink.setDialAppUrl(appUrl);
+ mockXhrManager = jasmine.createSpyObj('XhrManager', ['send']);
+ spyOn(XhrUtils, 'logRawXhr');
+ client = new DialClient.Client(sink, mockXhrManager);
+ });
+
+ const setMockXhrResponse = function(xml) {
+ mockXhrManager.send.and.returnValue(
+ Promise.resolve({responseText: xml, status: 200}));
+ };
+
+ const setMockXhrErrorResponse = function() {
+ mockXhrManager.send.and.returnValue(
+ Promise.resolve({responseText: null, status: 403 /* Forbidden */}));
+ };
+
+ const setMockXhrNotFoundResponse = function() {
+ mockXhrManager.send.and.returnValue(
+ Promise.resolve({responseText: null, status: 404}));
+ };
+
+ const setMockXhrReject = function() {
+ mockXhrManager.send.and.returnValue(
+ Promise.reject(new Error('send failed')));
+ };
+
+ // Suppress Jasmine warning about a spec with no expectations.
+ const noExpectations = function() {
+ expect(true).toBe(true);
+ };
+
+ describe('Tests launchApp', function() {
+ const expectLaunchAppFails = function(done) {
+ client.launchApp('YouTube', 'v=12345678').then(() => {
+ fail('launchApp unexpectedly succeeded.');
+ }, done);
+ };
+
+ it('Resolves', done => {
+ setMockXhrResponse('');
+ client.launchApp('YouTube', 'v=12345678').then(() => {
+ expect(mockXhrManager.send)
+ .toHaveBeenCalledWith(
+ appUrl + '/YouTube', 'POST', 'v=12345678', jasmine.any(Object));
+ done();
+ });
+ });
+
+ it('Rejects on error response', done => {
+ setMockXhrErrorResponse();
+ expectLaunchAppFails(done);
+ noExpectations();
+ });
+
+ it('Rejects on send rejection', done => {
+ setMockXhrReject();
+ expectLaunchAppFails(done);
+ noExpectations();
+ });
+ });
+
+ describe('Tests stopApp', function() {
+ const expectStopAppFails = function(done) {
+ client.stopApp('YouTube').then(() => {
+ fail('stopApp unexpectedly succeeded.');
+ }, done);
+ };
+
+ it('Resolves', done => {
+ setMockXhrResponse('');
+ client.stopApp('YouTube').then(() => {
+ expect(mockXhrManager.send)
+ .toHaveBeenCalledWith(appUrl + '/YouTube', 'DELETE');
+ done();
+ });
+ });
+
+ it('Rejects on error response', done => {
+ setMockXhrErrorResponse();
+ expectStopAppFails(done);
+ noExpectations();
+ });
+
+ it('Rejects on send rejection', done => {
+ setMockXhrReject();
+ expectStopAppFails(done);
+ noExpectations();
+ });
+ });
+
+ const expectMockSendGet = function() {
+ expect(mockXhrManager.send)
+ .toHaveBeenCalledWith(
+ 'http://198.0.0.100/apps/YouTube', 'GET', undefined,
+ jasmine.any(Object));
+ };
+
+ const VALID_GET_RESPONSE_ = '<?xml version="1.0" encoding="UTF-8"?>' +
+ '<service xmlns="urn:dial-multiscreen-org:schemas:dial">' +
+ '<name>YouTube</name>' +
+ '<options allowStop="false"/>' +
+ '<state>running</state>' +
+ '<link rel="run" href="run"/>' +
+ '</service>';
+
+ const VALID_GET_RESPONSE_EXTRA_DATA_ =
+ '<?xml version="1.0" encoding="UTF-8"?>' +
+ '<service xmlns="urn:dial-multiscreen-org:schemas:dial">' +
+ '<name>YouTube</name>' +
+ '<state>running</state>' +
+ '<link rel="run" href="run"/>' +
+ '<port>8080</port>' +
+ '<additionalData>' +
+ '<screenId>e5n3112oskr42pg0td55b38nh4</screenId>' +
+ '<otherField>2</otherField>' +
+ '</additionalData>' +
+ '</service>';
+
+ const INVALID_GET_RESPONSE_NO_SERVICE_ =
+ '<?xml version="1.0" encoding="UTF-8"?>';
+
+ const INVALID_GET_RESPONSE_NO_STATE_ =
+ '<?xml version="1.0" encoding="UTF-8"?>' +
+ '<service xmlns="urn:dial-multiscreen-org:schemas:dial">' +
+ '<name>YouTube</name>' +
+ '<options allowStop="true"/>' +
+ '<link rel="run" href="run"/>' +
+ '</service>';
+
+ const INVALID_GET_RESPONSE_INVALID_STATE_ =
+ '<?xml version="1.0" encoding="UTF-8"?>' +
+ '<service xmlns="urn:dial-multiscreen-org:schemas:dial">' +
+ '<name>YouTube</name>' +
+ '<options allowStop="true"/>' +
+ '<state>xyzzy</state>' +
+ '<link rel="run" href="run"/>' +
+ '</service>';
+
+ const INVALID_GET_RESPONSE_INSTALLABLE_ =
+ '<?xml version="1.0" encoding="UTF-8"?>' +
+ '<service xmlns="urn:dial-multiscreen-org:schemas:dial">' +
+ '<name>YouTube</name>' +
+ '<options allowStop="true"/>' +
+ '<state>installable=http://play.google.com/youtube</state>' +
+ '<link rel="run" href="run"/>' +
+ '</service>';
+
+ const INVALID_GET_RESPONSE_NO_NAME_ =
+ '<?xml version="1.0" encoding="UTF-8"?>' +
+ '<service xmlns="urn:dial-multiscreen-org:schemas:dial">' +
+ '<options allowStop="true"/>' +
+ '<state>running</state>' +
+ '<link rel="run" href="run"/>' +
+ '</service>';
+
+ const INVALID_GET_RESPONSE_WRONG_APP_NAME_ =
+ '<?xml version="1.0" encoding="UTF-8"?>' +
+ '<service xmlns="urn:dial-multiscreen-org:schemas:dial">' +
+ '<name>WrongAppName</name>' +
+ '<options allowStop="true"/>' +
+ '<state>running</state>' +
+ '<link rel="run" href="run"/>' +
+ '</service>';
+
+ describe('Tests getAppInfo', function() {
+ it('Returns info from valid response', done => {
+ setMockXhrResponse(VALID_GET_RESPONSE_);
+ client.getAppInfo('YouTube').then(appInfo => {
+ expect(appInfo.name).toEqual('YouTube');
+ expect(appInfo.state).toEqual('running');
+ expect(appInfo.allowStop).toBe(false);
+ expect(appInfo.resource).toEqual('run');
+ expectMockSendGet();
+ done();
+ });
+ });
+
+ it('Returns info with extraData', done => {
+ setMockXhrResponse(VALID_GET_RESPONSE_EXTRA_DATA_);
+ client.getAppInfo('YouTube').then(appInfo => {
+ expect(appInfo.name).toEqual('YouTube');
+ expect(appInfo.state).toEqual('running');
+ expect(appInfo.allowStop).toBe(true);
+ expect(appInfo.resource).toEqual('run');
+ expect(appInfo.extraData.port).toEqual('8080');
+ expect(appInfo.extraData.additionalData)
+ .toEqual(
+ '<screenId xmlns="urn:dial-multiscreen-org:schemas:dial">' +
+ 'e5n3112oskr42pg0td55b38nh4</screenId>' +
+ '<otherField xmlns="urn:dial-multiscreen-org:schemas:dial">2' +
+ '</otherField>');
+ expectMockSendGet();
+ done();
+ });
+ });
+
+ const expectGetAppInfoFails = function(done) {
+ client.getAppInfo('YouTube').then(
+ _ => {
+ fail('getAppInfo unexpectedly succeeded.');
+ },
+ e => {
+ expectMockSendGet();
+ done();
+ });
+ };
+
+ const testInvalidResponse = function(response, done) {
+ setMockXhrResponse(response);
+ expectGetAppInfoFails(done);
+ };
+
+ it('Rejects on invalid response 1', done => {
+ testInvalidResponse('blarg', done);
+ });
+
+ it('Rejects on invalid response 2', done => {
+ testInvalidResponse(INVALID_GET_RESPONSE_NO_SERVICE_, done);
+ });
+
+ it('Rejects on invalid response 3', done => {
+ testInvalidResponse(INVALID_GET_RESPONSE_NO_STATE_, done);
+ });
+
+ it('Rejects on invalid response 4', done => {
+ testInvalidResponse(INVALID_GET_RESPONSE_NO_NAME_, done);
+ });
+
+ it('Rejects on invalid response 5', done => {
+ testInvalidResponse(INVALID_GET_RESPONSE_INVALID_STATE_, done);
+ });
+
+ it('Rejects on invalid response 6', done => {
+ testInvalidResponse(INVALID_GET_RESPONSE_INSTALLABLE_, done);
+ });
+
+ it('Rejects on mismatched app name', done => {
+ testInvalidResponse(INVALID_GET_RESPONSE_WRONG_APP_NAME_, done);
+ });
+
+ it('Rejects on error response', done => {
+ setMockXhrErrorResponse();
+ expectGetAppInfoFails(done);
+ });
+
+ it('Rejects on send rejection', done => {
+ setMockXhrReject();
+ expectGetAppInfoFails(done);
+ });
+
+ it('Rejects with AppInfoNotFoundError', done => {
+ setMockXhrNotFoundResponse();
+ client.getAppInfo('YouTube').then(
+ _ => {
+ fail('getAppInfo unexpectedly succeeded.');
+ },
+ e => {
+ expect(e instanceof DialClient.AppInfoNotFoundError).toBe(true);
+ expectMockSendGet();
+ done();
+ });
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider.js
new file mode 100644
index 00000000000..f5c471a82a5
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider.js
@@ -0,0 +1,474 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.DialProvider');
+goog.module.declareLegacyNamespace();
+
+const Activity = goog.require('mr.dial.Activity');
+const ActivityRecords = goog.require('mr.dial.ActivityRecords');
+const AppDiscoveryService = goog.require('mr.dial.AppDiscoveryService');
+const Assertions = goog.require('mr.Assertions');
+const CancellablePromise = goog.require('mr.CancellablePromise');
+const DeviceCountsProvider = goog.require('mr.DeviceCountsProvider');
+const DialAnalytics = goog.require('mr.DialAnalytics');
+const DialClient = goog.require('mr.dial.Client');
+const DialPresentationUrl = goog.require('mr.dial.PresentationUrl');
+const DialSink = goog.require('mr.dial.Sink');
+const Logger = goog.require('mr.Logger');
+const MediaSourceUtils = goog.require('mr.MediaSourceUtils');
+const PresentationConnectionState = goog.require('mr.PresentationConnectionState');
+const Provider = goog.require('mr.Provider');
+const ProviderCallbacks = goog.require('mr.dial.ProviderCallbacks');
+const ProviderManagerCallbacks = goog.require('mr.ProviderManagerCallbacks');
+const ProviderName = goog.require('mr.ProviderName');
+const Route = goog.require('mr.Route');
+const RouteRequestError = goog.require('mr.RouteRequestError');
+const RouteRequestResultCode = goog.require('mr.RouteRequestResultCode');
+const SinkAppStatus = goog.require('mr.dial.SinkAppStatus');
+const SinkAvailability = goog.require('mr.SinkAvailability');
+const SinkDiscoveryService = goog.require('mr.dial.SinkDiscoveryService');
+const SinkList = goog.require('mr.SinkList');
+const SinkUtils = goog.require('mr.SinkUtils');
+
+/**
+ * DIAL implementation of Media Route Provider.
+ * @implements {Provider}
+ * @implements {ProviderCallbacks}
+ * @implements {DeviceCountsProvider}
+ */
+const DialProvider = class {
+ /**
+ * @param {!ProviderManagerCallbacks} providerManagerCallbacks
+ * @param {!SinkDiscoveryService=} sinkDiscoveryService
+ * @param {!AppDiscoveryService=} appDiscoveryService
+ * @final
+ */
+ constructor(
+ providerManagerCallbacks, sinkDiscoveryService = undefined,
+ appDiscoveryService = undefined) {
+ /** @private @const {!ProviderManagerCallbacks} */
+ this.providerManagerCallbacks_ = providerManagerCallbacks;
+
+ /** @private @const {!SinkDiscoveryService} */
+ this.sinkDiscoveryService_ =
+ sinkDiscoveryService || new SinkDiscoveryService(this);
+
+ /** @private @const {!ActivityRecords} */
+ this.activityRecords_ = new ActivityRecords(this);
+
+ /** @private {?AppDiscoveryService} */
+ this.appDiscoveryService_ = appDiscoveryService || null;
+
+ /** @private @const {?Logger} */
+ this.logger_ = Logger.getInstance('mr.DialProvider');
+ }
+
+ /**
+ * @override
+ */
+ getName() {
+ return ProviderName.DIAL;
+ }
+
+ /**
+ * @override
+ */
+ getDeviceCounts() {
+ return this.sinkDiscoveryService_.getDeviceCounts();
+ }
+
+ /**
+ * @override
+ */
+ initialize(config) {
+ const sinkQueryEnabled =
+ (config && config.enable_dial_sink_query == false) ? false : true;
+ this.logger_.info('Dial sink query enabled: ' + sinkQueryEnabled + '...');
+
+ this.activityRecords_.init();
+ this.sinkDiscoveryService_.init();
+
+ if (sinkQueryEnabled) {
+ this.appDiscoveryService_ = this.appDiscoveryService_ ||
+ new AppDiscoveryService(this.sinkDiscoveryService_,
+ this.activityRecords_);
+ this.appDiscoveryService_.init();
+ } else {
+ this.appDiscoveryService_ = null;
+ }
+
+ this.maybeStartAppDiscovery_();
+ }
+
+ /**
+ * @override
+ */
+ getAvailableSinks(sourceUrn) {
+ // Prevent SinkDiscoveryService to return cached available sinks.
+ if (!this.appDiscoveryService_) {
+ return SinkList.EMPTY;
+ }
+
+ this.logger_.fine('GetAvailableSinks for ' + sourceUrn);
+ const dialMediaSource = DialPresentationUrl.create(sourceUrn);
+ return dialMediaSource ?
+ this.sinkDiscoveryService_.getSinksByAppName(dialMediaSource.appName) :
+ SinkList.EMPTY;
+ }
+
+ /**
+ * @override
+ */
+ startObservingMediaSinks(sourceUrn) {
+ if (!this.appDiscoveryService_) {
+ return;
+ }
+
+ const dialMediaSource = DialPresentationUrl.create(sourceUrn);
+ if (dialMediaSource) {
+ this.appDiscoveryService_.registerApp(dialMediaSource.appName);
+ this.maybeStartAppDiscovery_();
+ }
+ }
+
+ /**
+ * @override
+ */
+ stopObservingMediaSinks(sourceUrn) {
+ if (!this.appDiscoveryService_) {
+ return;
+ }
+
+ const dialMediaSource = DialPresentationUrl.create(sourceUrn);
+ if (dialMediaSource) {
+ this.appDiscoveryService_.unregisterApp(dialMediaSource.appName);
+ this.maybeStopAppDiscovery_();
+ }
+ }
+
+ /**
+ * @override
+ */
+ startObservingMediaRoutes(sourceUrn) {
+ this.maybeStartAppDiscovery_();
+ }
+
+ /**
+ * @override
+ */
+ stopObservingMediaRoutes(sourceUrn) {
+ this.maybeStopAppDiscovery_();
+ }
+
+ /**
+ * @private
+ */
+ maybeStopAppDiscovery_() {
+ if (!this.appDiscoveryService_) {
+ return;
+ }
+
+ if (this.sinkDiscoveryService_.getSinkCount() == 0 ||
+ (this.appDiscoveryService_.getAppCount() == 0 &&
+ this.activityRecords_.getActivityCount() == 0)) {
+ this.appDiscoveryService_.stop();
+ }
+ }
+
+ /**
+ * @private
+ */
+ maybeStartAppDiscovery_() {
+ if (!this.appDiscoveryService_) {
+ return;
+ }
+
+ if (this.sinkDiscoveryService_.getSinkCount() > 0 &&
+ (this.appDiscoveryService_.getAppCount() > 0 ||
+ this.activityRecords_.getActivityCount() > 0)) {
+ this.appDiscoveryService_.start();
+ }
+ }
+
+ /**
+ * @override
+ */
+ getSinkById(id) {
+ const dialSink = this.sinkDiscoveryService_.getSinkById(id);
+ return dialSink ? dialSink.getMrSink() : null;
+ }
+
+ /**
+ * @override
+ */
+ getRoutes() {
+ return this.activityRecords_.getRoutes();
+ }
+
+ /**
+ * @override
+ */
+ createRoute(
+ sourceUrn, sinkId, presentationId, offTheRecord, timeoutMillis,
+ opt_origin, opt_tabId) {
+ const sink = this.sinkDiscoveryService_.getSinkById(sinkId);
+ if (!sink) {
+ DialAnalytics.recordCreateRoute(
+ DialAnalytics.DialRouteCreation.FAILED_NO_SINK);
+ return CancellablePromise.reject(Error('Unkown sink: ' + sinkId));
+ }
+ SinkUtils.getInstance().recentLaunchedDevice =
+ new SinkUtils.DeviceData(sink.getModelName(), sink.getIpAddress());
+ const dialMediaSource = DialPresentationUrl.create(sourceUrn);
+ if (!dialMediaSource) {
+ return CancellablePromise.reject(Error('No app name set.'));
+ }
+ const appName = dialMediaSource.appName;
+ const dialClient = this.newClient_(sink);
+
+ return CancellablePromise.forPromise(
+ /** @type {!Promise<!Route>} */
+ (dialClient.getAppInfo(appName)
+ .then(appInfo => {
+ if (appInfo.state == DialClient.DialAppState.RUNNING) {
+ return dialClient.stopApp(appName);
+ }
+ })
+ .then(() => {
+ return dialClient.launchApp(
+ appName, dialMediaSource.launchParameter);
+ })
+ .then(() => {
+ return this.addRoute(
+ sinkId, sourceUrn, true, appName, presentationId,
+ offTheRecord);
+ })
+ .catch(err => {
+ DialAnalytics.recordCreateRoute(
+ DialAnalytics.DialRouteCreation.FAILED_LAUNCH_APP);
+ throw err;
+ })));
+ }
+
+ /**
+ * @override
+ */
+ joinRoute(
+ sourceUrn, presentationId, offTheRecord, timeoutMillis, origin, tabId) {
+ return CancellablePromise.reject(Error('Not supported'));
+ }
+
+ /**
+ * @override
+ */
+ connectRouteByRouteId(sourceUrn, routeId, presentationId, origin, tabId) {
+ return CancellablePromise.reject(Error('Not supported'));
+ }
+
+ /**
+ * @override
+ */
+ detachRoute(routeId) {}
+
+ /**
+ * @param {string} sinkId
+ * @param {?string} sourceUrn
+ * @param {boolean} isLocal
+ * @param {string} appName
+ * @param {string} presentationId
+ * @param {boolean} offTheRecord
+ * @return {!Route} The route that was just added.
+ */
+ addRoute(sinkId, sourceUrn, isLocal, appName, presentationId, offTheRecord) {
+ DialAnalytics.recordCreateRoute(
+ DialAnalytics.DialRouteCreation.ROUTE_CREATED);
+ const route = Route.createRoute(
+ presentationId, this.getName(), sinkId, sourceUrn, isLocal, appName,
+ null);
+ route.offTheRecord = offTheRecord;
+ this.activityRecords_.add(new Activity(route, appName));
+ return route;
+ }
+
+ /**
+ * @override
+ */
+ onSinkAdded(sink) {
+ this.providerManagerCallbacks_.onSinkAvailabilityUpdated(
+ this, SinkAvailability.PER_SOURCE);
+ if (this.appDiscoveryService_) {
+ this.maybeStartAppDiscovery_();
+ this.appDiscoveryService_.scanSink(sink);
+ }
+ this.providerManagerCallbacks_.onSinksUpdated();
+ SinkUtils.getInstance().recentDiscoveredDevice =
+ new SinkUtils.DeviceData(sink.getModelName(), sink.getIpAddress());
+ }
+
+ /**
+ * @override
+ */
+ onSinksRemoved(sinks) {
+ if (this.sinkDiscoveryService_.getSinkCount() == 0) {
+ this.providerManagerCallbacks_.onSinkAvailabilityUpdated(
+ this, SinkAvailability.UNAVAILABLE);
+ }
+ this.maybeStopAppDiscovery_();
+ sinks.forEach(sink => {
+ this.activityRecords_.removeBySinkId(sink.getId());
+ });
+ this.providerManagerCallbacks_.onSinksUpdated();
+ }
+
+ /**
+ * @override
+ */
+ onSinkUpdated(sink) {
+ this.providerManagerCallbacks_.onSinksUpdated();
+ }
+
+ /**
+ * @override
+ */
+ onActivityAdded(activity) {
+ this.maybeStartAppDiscovery_();
+ this.providerManagerCallbacks_.onRouteAdded(this, activity.route);
+ }
+
+ /**
+ * @override
+ */
+ onActivityRemoved(activity) {
+ const route = activity.route;
+ if (route.isLocal) {
+ this.providerManagerCallbacks_.onPresentationConnectionStateChanged(
+ route.id, PresentationConnectionState.TERMINATED);
+ }
+ this.maybeStopAppDiscovery_();
+ this.providerManagerCallbacks_.onRouteRemoved(this, route);
+ }
+
+ /**
+ * @override
+ */
+ onActivityUpdated(activity) {
+ this.providerManagerCallbacks_.onRouteUpdated(this, activity.route);
+ }
+
+ /**
+ * @override
+ */
+ terminateRoute(routeId) {
+ const activity = this.activityRecords_.getByRouteId(routeId);
+ if (!activity) {
+ return Promise.reject(new RouteRequestError(
+ RouteRequestResultCode.ROUTE_NOT_FOUND,
+ 'Route in DIAL provider not found for routeId ' + routeId));
+ }
+ this.activityRecords_.removeByRouteId(routeId);
+ const sink = this.sinkDiscoveryService_.getSinkById(activity.route.sinkId);
+ if (!sink) {
+ return Promise.reject(new RouteRequestError(
+ RouteRequestResultCode.ROUTE_NOT_FOUND,
+ 'Sink in DIAL provider not found for sinkId ' +
+ activity.route.sinkId));
+ }
+ return this.newClient_(sink).stopApp(activity.appName);
+ }
+
+ /**
+ * @override
+ */
+ getMirrorSettings(sinkId) {
+ throw new Error('Not implemented.');
+ }
+
+ /**
+ * @override
+ */
+ getMirrorServiceName(sinkId) {
+ return null;
+ }
+
+ /**
+ * @override
+ */
+ onMirrorActivityUpdated(routeId) {}
+
+ /**
+ * @override
+ */
+ sendRouteMessage(routeId, message, opt_extraInfo) {
+ return Promise.reject(Error('DIAL sending messages is not supported'));
+ }
+
+ /**
+ * @override
+ */
+ sendRouteBinaryMessage(routeId, message) {
+ return Promise.reject(Error('DIAL sending messages is not supported'));
+ }
+
+ /**
+ * @override
+ */
+ canRoute(sourceUrn, sinkId) {
+ const sink = this.sinkDiscoveryService_.getSinkById(sinkId);
+ if (!sink) {
+ return false;
+ }
+
+ if (MediaSourceUtils.isMirrorSource(sourceUrn)) {
+ return false;
+ }
+
+ const dialMediaSource = DialPresentationUrl.create(sourceUrn);
+ if (!dialMediaSource) {
+ return false;
+ }
+ return sink.getAppStatus(dialMediaSource.appName) ==
+ SinkAppStatus.AVAILABLE;
+ }
+
+ /**
+ * @override
+ */
+ canJoin(sourceUrn, presentationId, route) {
+ return false;
+ }
+
+ /**
+ * @override
+ */
+ searchSinks(sourceUrn, searchCriteria) {
+ // Not implemented.
+ return Assertions.rejectNotImplemented();
+ }
+
+ /**
+ * @override
+ */
+ createMediaRouteController(routeId, controllerRequest, observer) {
+ // Not implemented.
+ return Assertions.rejectNotImplemented();
+ }
+
+ /**
+ * @override
+ */
+ provideSinks(sinks) {
+ this.sinkDiscoveryService_.addSinks(sinks);
+ }
+
+ /**
+ * @param {!DialSink} sink
+ * @return {!DialClient.Client}
+ * @private
+ */
+ newClient_(sink) {
+ return new DialClient.Client(sink);
+ }
+};
+
+exports = DialProvider;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider_callbacks.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider_callbacks.js
new file mode 100644
index 00000000000..2c7284922b0
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider_callbacks.js
@@ -0,0 +1,65 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview DIAL provider callbacks used for other DIAL services to inform
+ * activity or sink updates.
+ */
+
+goog.provide('mr.dial.ActivityCallbacks');
+goog.provide('mr.dial.ProviderCallbacks');
+goog.provide('mr.dial.SinkDiscoveryCallbacks');
+
+
+
+/**
+ * @record
+ */
+mr.dial.ActivityCallbacks = class {
+ /**
+ * @param {!mr.dial.Activity } activity
+ */
+ onActivityAdded(activity) {}
+
+ /**
+ * @param {!mr.dial.Activity } activity
+ */
+ onActivityRemoved(activity) {}
+
+ /**
+ * @param {!mr.dial.Activity } activity
+ */
+ onActivityUpdated(activity) {}
+};
+
+
+
+/**
+ * @record
+ */
+mr.dial.SinkDiscoveryCallbacks = class {
+ /**
+ * @param {!mr.dial.Sink} sink Sink that has been added.
+ */
+ onSinkAdded(sink) {}
+
+ /**
+ * @param {!Array.<!mr.dial.Sink>} sinks Sinks that have been removed.
+ */
+ onSinksRemoved(sinks) {}
+
+ /**
+ * @param {!mr.dial.Sink} sink Sink that has been updated.
+ */
+ onSinkUpdated(sink) {}
+};
+
+
+
+/**
+ * @record
+ * @extends {mr.dial.ActivityCallbacks}
+ * @extends {mr.dial.SinkDiscoveryCallbacks}
+ */
+mr.dial.ProviderCallbacks = class {};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider_test.js
new file mode 100644
index 00000000000..c82f8b9931e
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_provider_test.js
@@ -0,0 +1,256 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.DialProviderTest');
+goog.setTestOnly('mr.DialProviderTest');
+
+const Activity = goog.require('mr.dial.Activity');
+const DialClient = goog.require('mr.dial.Client');
+const DialProvider = goog.require('mr.DialProvider');
+const DialSink = goog.require('mr.dial.Sink');
+const PersistentDataManager = goog.require('mr.PersistentDataManager');
+const PresentationConnectionState = goog.require('mr.PresentationConnectionState');
+const Route = goog.require('mr.Route');
+const SinkAvailability = goog.require('mr.SinkAvailability');
+
+
+describe('DialProvider tests', function() {
+ let provider;
+ let mockPmCallbacks;
+ const pmCallbackMethods = [
+ 'getRouteMessageEventTarget', 'getProviderFromRouteId',
+ 'onPresentationConnectionClosed', 'onPresentationConnectionStateChanged',
+ 'onRouteMessage', 'onRouteRemoved', 'onRouteAdded', 'onSinksUpdated',
+ 'onSinkAvailabilityUpdated'
+ ];
+ let mockSinkDiscoveryService;
+ const sinkDiscoveryServiceMethods =
+ ['init', 'getSinkCount', 'init', 'getSinksByAppName', 'getSinkById'];
+ let mockAppDiscoveryService;
+ const appDiscoveryServiceMethods = [
+ 'init', 'start', 'stop', 'registerApp', 'unregisterApp', 'getAppCount',
+ 'scanSink'
+ ];
+ let mockDialClient;
+
+ const appInfo = new DialClient.AppInfo();
+ const youTubeUrl = 'dial:YouTube?postData=dj0xMjM=';
+
+ beforeEach(function() {
+ mockPmCallbacks =
+ jasmine.createSpyObj('ProviderManagerCallbacks', pmCallbackMethods);
+
+ mockSinkDiscoveryService = jasmine.createSpyObj(
+ 'SinkDiscoveryService', sinkDiscoveryServiceMethods);
+ mockAppDiscoveryService =
+ jasmine.createSpyObj('AppDiscoveryService', appDiscoveryServiceMethods);
+
+ provider = new DialProvider(
+ mockPmCallbacks, mockSinkDiscoveryService, mockAppDiscoveryService);
+ provider.initialize({enable_dial_discovery: true});
+ expect(mockAppDiscoveryService.init).toHaveBeenCalled();
+
+ const fakeDialSink = new DialSink('Fake DIAL sink', 'uniqueId');
+ fakeDialSink.setDialAppUrl(youTubeUrl);
+ mockDialClient = jasmine.createSpyObj(
+ 'dialClient', ['getAppInfo', 'launchApp', 'stopApp']);
+ spyOn(DialProvider.prototype, 'newClient_').and.returnValue(mockDialClient);
+ mockSinkDiscoveryService.getSinkById.and.returnValue(fakeDialSink);
+ appInfo.name = 'YouTube';
+ appInfo.state = DialClient.DialAppState.STOPPED;
+ mr.DialAnalytics.recordCreateRoute = jasmine.createSpy('recordCreateRoute');
+ });
+
+ afterEach(function() {
+ PersistentDataManager.clear();
+ });
+
+ describe('startObservingMediaSinks Test', function() {
+ it('Handles non-dial sink query', function() {
+ provider.startObservingMediaSinks('urn:not-dial:YouTube');
+ expect(mockAppDiscoveryService.registerApp).not.toHaveBeenCalled();
+ });
+
+ it('Handles valid dial sink query, no sinks', function() {
+ mockSinkDiscoveryService.getSinkCount.and.returnValue(0);
+ mockAppDiscoveryService.getAppCount.and.returnValue(1);
+ provider.startObservingMediaSinks(youTubeUrl);
+ expect(mockAppDiscoveryService.registerApp)
+ .toHaveBeenCalledWith('YouTube');
+ expect(mockAppDiscoveryService.start).not.toHaveBeenCalled();
+ });
+
+ it('Handles valid dial sink query, at least one sink', function() {
+ mockSinkDiscoveryService.getSinkCount.and.returnValue(1);
+ mockAppDiscoveryService.getAppCount.and.returnValue(1);
+ provider.startObservingMediaSinks(youTubeUrl);
+ expect(mockAppDiscoveryService.registerApp)
+ .toHaveBeenCalledWith('YouTube');
+ expect(mockAppDiscoveryService.start).toHaveBeenCalled();
+ });
+ });
+
+ describe('stopObservingMediaSinks Test', function() {
+ it('Handles non-dial sink query', function() {
+ provider.stopObservingMediaSinks('urn:not-dial:YouTube');
+ expect(mockAppDiscoveryService.unregisterApp).not.toHaveBeenCalled();
+ });
+
+ it('Handles valid dial sink query', function() {
+ mockAppDiscoveryService.getAppCount.and.returnValue(0);
+ provider.stopObservingMediaSinks(youTubeUrl);
+ expect(mockAppDiscoveryService.unregisterApp)
+ .toHaveBeenCalledWith('YouTube');
+ });
+
+ it('Handles valid dial sink query, app query remains', function() {
+ mockAppDiscoveryService.getAppCount.and.returnValue(1);
+ provider.stopObservingMediaSinks(youTubeUrl);
+ expect(mockAppDiscoveryService.unregisterApp)
+ .toHaveBeenCalledWith('YouTube');
+ });
+ });
+
+ describe('onPresentationConnectionStateChanged Test', function() {
+ it('Changes presentation state to terminated', function() {
+ const route = provider.addRoute(
+ 'sink1', youTubeUrl, true, 'app1', 'presentationId1');
+ provider.onActivityRemoved(new Activity(route, 'app1'));
+ expect(mockPmCallbacks.onPresentationConnectionStateChanged)
+ .toHaveBeenCalledWith(
+ route.id, PresentationConnectionState.TERMINATED);
+ expect(mockPmCallbacks.onRouteRemoved).toHaveBeenCalled();
+ });
+
+ it('Does not change state for non-local presentation', function() {
+ const route = provider.addRoute(
+ 'sink1', youTubeUrl, false, 'app1', 'presentationId1');
+ provider.onActivityRemoved(new Activity(route, 'app1'));
+ expect(mockPmCallbacks.onPresentationConnectionStateChanged)
+ .not.toHaveBeenCalled();
+ expect(mockPmCallbacks.onRouteRemoved).toHaveBeenCalled();
+ });
+ });
+
+ describe('createRoute Test', function() {
+ it('Creates a route', function(done) {
+ mockDialClient.getAppInfo.and.returnValue(Promise.resolve(appInfo));
+ mockDialClient.launchApp.and.returnValue(Promise.resolve());
+ provider.createRoute(youTubeUrl, 'sink1', 'presentationId1', false)
+ .promise.then(
+ route => {
+ expect(route.sinkId).toBe('sink1');
+ expect(route.mediaSource).toBe(youTubeUrl);
+ expect(route.offTheRecord).toBe(false);
+ expect(mr.DialAnalytics.recordCreateRoute)
+ .toHaveBeenCalledWith(
+ mr.DialAnalytics.DialRouteCreation.ROUTE_CREATED);
+ done();
+ },
+ e => {
+ done.fail('Unexpected error: ' + e.message);
+ });
+ });
+
+ it('Creates an off-the-record route', function(done) {
+ mockDialClient.getAppInfo.and.returnValue(Promise.resolve(appInfo));
+ mockDialClient.launchApp.and.returnValue(Promise.resolve());
+ provider.createRoute(youTubeUrl, 'sink1', 'presentationId1', true)
+ .promise.then(
+ route => {
+ expect(route.sinkId).toBe('sink1');
+ expect(route.mediaSource).toBe(youTubeUrl);
+ expect(route.offTheRecord).toBe(true);
+ expect(mr.DialAnalytics.recordCreateRoute)
+ .toHaveBeenCalledWith(
+ mr.DialAnalytics.DialRouteCreation.ROUTE_CREATED);
+ done();
+ },
+ e => {
+ done.fail('Unexpected error: ' + e.message);
+ });
+ });
+
+ it('Fails to create a route when get app info fails', function(done) {
+ mockDialClient.getAppInfo.and.returnValue(Promise.reject('fail'));
+ provider.createRoute(youTubeUrl, 'sink1', 'presentationId1', true)
+ .promise.then(done.fail, e => {
+ expect(mr.DialAnalytics.recordCreateRoute)
+ .toHaveBeenCalledWith(
+ mr.DialAnalytics.DialRouteCreation.FAILED_LAUNCH_APP);
+ done();
+ });
+ });
+
+ it('Fails to create a route when launch app fails', function(done) {
+ mockDialClient.getAppInfo.and.returnValue(Promise.resolve(appInfo));
+ mockDialClient.launchApp.and.returnValue(Promise.reject('fail'));
+ provider.createRoute(youTubeUrl, 'sink1', 'presentationId1', true)
+ .promise.then(done.fail, e => {
+ expect(mr.DialAnalytics.recordCreateRoute)
+ .toHaveBeenCalledWith(
+ mr.DialAnalytics.DialRouteCreation.FAILED_LAUNCH_APP);
+ done();
+ });
+ });
+
+ it('Starts and stop app discovery', function() {
+ mockSinkDiscoveryService.getSinkCount.and.returnValue(1);
+ let appCount = 0;
+ mockAppDiscoveryService.getAppCount.and.callFake(() => appCount);
+ const route = Route.createRoute(
+ 'presentationId', 'providerName', 'sinkId', 'source', true,
+ 'description', null);
+ const activity = new Activity(route, 'YouTube');
+ provider.activityRecords_.add(activity);
+ expect(mockAppDiscoveryService.start).toHaveBeenCalled();
+
+ appCount++;
+ provider.startObservingMediaSinks(youTubeUrl);
+ expect(mockAppDiscoveryService.registerApp)
+ .toHaveBeenCalledWith('YouTube');
+
+ appCount--;
+ provider.stopObservingMediaSinks(youTubeUrl);
+ expect(mockAppDiscoveryService.stop).not.toHaveBeenCalled();
+
+ provider.activityRecords_.removeByRouteId(route.id);
+ expect(mockAppDiscoveryService.stop).toHaveBeenCalled();
+ });
+
+ it('Sets SinkAvailability to UNAVAILABLE if no more sinks', () => {
+ mockSinkDiscoveryService.getSinkCount.and.returnValue(0);
+ // Note: This should also work if an non-empty list if passed in. For
+ // simplicity, an empty list is used here.
+ provider.onSinksRemoved([]);
+ expect(mockPmCallbacks.onSinkAvailabilityUpdated)
+ .toHaveBeenCalledWith(provider, SinkAvailability.UNAVAILABLE);
+ });
+ });
+
+ describe('Disables Dial sink query', function() {
+ beforeEach(function() {
+ PersistentDataManager.clear();
+ provider.initialize({enable_dial_sink_query: false});
+ expect(mockAppDiscoveryService.start).not.toHaveBeenCalled();
+ });
+
+ it('Starting and stopping observing media sinks does nothing', function() {
+ provider.startObservingMediaSinks(youTubeUrl);
+ expect(mockAppDiscoveryService.registerApp).not.toHaveBeenCalled();
+
+ provider.stopObservingMediaSinks(youTubeUrl);
+ expect(mockAppDiscoveryService.unregisterApp).not.toHaveBeenCalled();
+ });
+
+ it('onSinkAdded does not start app discovery', function() {
+ const sink = new DialSink('s1', 'sink1');
+ provider.onSinkAdded(sink);
+ expect(mockAppDiscoveryService.scanSink).not.toHaveBeenCalled();
+
+ const sinkList = provider.getAvailableSinks();
+ expect(sinkList.sinks.length).toBe(0);
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink.js
new file mode 100644
index 00000000000..8cf0e1fe39d
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink.js
@@ -0,0 +1,309 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.Sink');
+goog.module.declareLegacyNamespace();
+
+const Sink = goog.require('mr.Sink');
+const SinkAppStatus = goog.require('mr.dial.SinkAppStatus');
+
+
+/**
+ * A wrapper for Sink containing DIAL specific data.
+ */
+const DialSink = class {
+ /**
+ * @param {string} friendlyName
+ * @param {string} uniqueId
+ * @final
+ */
+ constructor(friendlyName, uniqueId) {
+ /** @private {?string} */
+ this.ipAddress_ = null;
+
+ /** @private {?number} */
+ this.port_ = null;
+
+ /** @private {?string} */
+ this.dialAppUrl_ = null;
+
+ /** @private {?string} */
+ this.deviceDescriptionUrl_ = null;
+
+ /** @private {?string} */
+ this.modelName_ = null;
+
+ /** @private @const {!Sink} */
+ this.mrSink_ = new Sink(uniqueId, friendlyName);
+
+ /**
+ * Holds the status of applications that may be available on the sink.
+ * Keys are application Names.
+ * @private {!Object<string, SinkAppStatus>}
+ */
+ this.appStatusMap_ = {};
+
+ /**
+ * Holds the timestamp when the status of applications was set.
+ * @private {!Object<string, number>}
+ */
+ this.appStatusTimeStamp_ = {};
+
+ /** @private {boolean} */
+ this.supportsAppAvailability_ = false;
+ }
+
+ /**
+ * @return {!Sink}
+ */
+ getMrSink() {
+ return this.mrSink_;
+ }
+
+ /**
+ * @return {string} A human readable name for the sink.
+ */
+ getFriendlyName() {
+ return this.mrSink_.friendlyName;
+ }
+
+ /**
+ * @param {string} friendlyName
+ * @return {!mr.dial.Sink} This sink.
+ */
+ setFriendlyName(friendlyName) {
+ this.mrSink_.friendlyName = friendlyName;
+ return this;
+ }
+
+ /**
+ * @return {?string} sink model name if known.
+ */
+ getModelName() {
+ return this.modelName_;
+ }
+
+ /**
+ * @param {?string} modelName
+ * @return {!mr.dial.Sink} This sink.
+ */
+ setModelName(modelName) {
+ this.modelName_ = modelName;
+ return this;
+ }
+
+ /**
+ * @return {string} An identifier for this sink.
+ */
+ getId() {
+ return this.mrSink_.id;
+ }
+
+ /**
+ * @param {string} id
+ * @return {!mr.dial.Sink} This sink.
+ */
+ setId(id) {
+ this.mrSink_.id = id;
+ return this;
+ }
+
+ /**
+ * @return {boolean} Whether this sink supports queries for DIAL app
+ * availability.
+ */
+ supportsAppAvailability() {
+ return this.supportsAppAvailability_;
+ }
+
+ /**
+ * Sets whether this sink supports DIAL app availability queries.
+ * @param {boolean} availability
+ * @return {!mr.dial.Sink} This sink.
+ */
+ setSupportsAppAvailability(availability) {
+ this.supportsAppAvailability_ = availability;
+ return this;
+ }
+
+ /**
+ * Updates sink properties.
+ * Fields that can be updated: friendlyName, dialAppUrl_,
+ * deviceDescriptionUrl_, ipAddress_, port_.
+ * @param {!mr.dial.Sink} sink
+ * @return {boolean} Whether the update resulted in changes to the sink.
+ */
+ update(sink) {
+ if (this.getId() != sink.getId()) {
+ return false;
+ }
+
+ let updated = false;
+
+ if (this.mrSink_.friendlyName != sink.mrSink_.friendlyName) {
+ this.mrSink_.friendlyName = sink.mrSink_.friendlyName;
+ updated = true;
+ }
+
+ if (this.dialAppUrl_ != sink.dialAppUrl_) {
+ this.dialAppUrl_ = sink.dialAppUrl_;
+ updated = true;
+ }
+
+ if (this.deviceDescriptionUrl_ != sink.deviceDescriptionUrl_) {
+ this.deviceDescriptionUrl_ = sink.deviceDescriptionUrl_;
+ updated = true;
+ }
+
+ if (this.ipAddress_ != sink.ipAddress_) {
+ this.ipAddress_ = sink.ipAddress_;
+ updated = true;
+ }
+
+ if (this.port_ != sink.port_) {
+ this.port_ = sink.port_;
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ /**
+ * @return {?string} The IP address of the sink, if any.
+ */
+ getIpAddress() {
+ return this.ipAddress_;
+ }
+
+ /**
+ * @param {?string} ipAddress The sink IP address.
+ * @return {!mr.dial.Sink} This sink.
+ */
+ setIpAddress(ipAddress) {
+ this.ipAddress_ = ipAddress;
+ return this;
+ }
+
+ /**
+ * @return {?number} The port number of the secure channel service.
+ */
+ getPort() {
+ return this.port_;
+ }
+
+ /**
+ * @param {?number} port
+ * @return {!mr.dial.Sink} This sink.
+ */
+ setPort(port) {
+ this.port_ = port;
+ return this;
+ }
+
+ /**
+ * @return {?string} The DIAL application URL, if any.
+ */
+ getDialAppUrl() {
+ return this.dialAppUrl_;
+ }
+
+ /**
+ * @param {string} url The DIAL app URL.
+ * @return {!mr.dial.Sink} This sink.
+ */
+ setDialAppUrl(url) {
+ this.dialAppUrl_ = url;
+ return this;
+ }
+
+ /**
+ * @return {?string} The DIAL device description URL, if any.
+ */
+ getDeviceDescriptionUrl() {
+ return this.deviceDescriptionUrl_;
+ }
+
+ /**
+ * @param {string} url The DIAL device description URL.
+ * @return {!mr.dial.Sink} This sink.
+ */
+ setDeviceDescriptionUrl(url) {
+ this.deviceDescriptionUrl_ = url;
+ return this;
+ }
+
+ /**
+ * Gets the availability of an application.
+ * @param {string} appName
+ * @return {SinkAppStatus} The status of the application, or null if it was
+ * not set.
+ */
+ getAppStatus(appName) {
+ return this.appStatusMap_[appName] || SinkAppStatus.UNKNOWN;
+ }
+
+ /**
+ * Gets the time stamp of the availability of an application was set.
+ * @param {string} appName
+ * @return {?number} the number of milliseconds between midnight, January 1,
+ * 1970 and the current time, or null if availability was not set.
+ */
+ getAppStatusTimeStamp(appName) {
+ return this.appStatusTimeStamp_[appName] || null;
+ }
+
+ /**
+ * Sets the availability of an application.
+ * @param {string} appName
+ * @param {SinkAppStatus} status
+ * @return {!mr.dial.Sink} This sink.
+ */
+ setAppStatus(appName, status) {
+ this.appStatusMap_[appName] = status;
+ this.appStatusTimeStamp_[appName] = Date.now();
+ return this;
+ }
+
+ /**
+ * Clears all app status from the sink.
+ * @return {!mr.dial.Sink} This sink.
+ */
+ clearAppStatus() {
+ this.appStatusMap_ = {};
+ this.appStatusTimeStamp_ = {};
+ return this;
+ }
+
+ /**
+ * @return {string} String suitable for fine logging.
+ */
+ toDebugString() {
+ return 'name = ' + this.mrSink_.friendlyName +
+ (this.ipAddress_ ? ', ip = ' + this.ipAddress_ : '') +
+ (this.modelName_ ? ', model = ' + this.modelName_ : '') +
+ ', apps = ' + JSON.stringify(this.appStatusMap_);
+ }
+
+ /**
+ * Creates a new sink and copies the fields of the input sink to this.
+ * @param {!Object<string, *>} sink The object containing data fields.
+ * @return {!mr.dial.Sink} A newly created sink.
+ */
+ static createFrom(sink) {
+ const newSink = new DialSink(sink.mrSink_.friendlyName, '');
+ newSink.mrSink_.id = sink.mrSink_.id; // Override the sink ID.
+ newSink.ipAddress_ = sink.ipAddress_;
+ newSink.port_ = sink.port_;
+ newSink.dialAppUrl_ = sink.dialAppUrl_;
+ newSink.deviceDescriptionUrl_ = sink.deviceDescriptionUrl_;
+ newSink.modelName_ = sink.modelName_;
+ newSink.appStatusMap_ = sink.appStatusMap_;
+ newSink.appStatusTimeStamp_ = sink.appStatusTimeStamp_;
+ newSink.supportsAppAvailability_ = sink.supportsAppAvailability_;
+ return newSink;
+ }
+};
+
+
+exports = DialSink;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_discovery_service.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_discovery_service.js
new file mode 100644
index 00000000000..93d062e3519
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_discovery_service.js
@@ -0,0 +1,334 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.SinkDiscoveryService');
+goog.module.declareLegacyNamespace();
+
+const DeviceCounts = goog.require('mr.DeviceCounts');
+const DeviceCountsProvider = goog.require('mr.DeviceCountsProvider');
+const DialAnalytics = goog.require('mr.DialAnalytics');
+const DialSink = goog.require('mr.dial.Sink');
+const Logger = goog.require('mr.Logger');
+const PersistentData = goog.require('mr.PersistentData');
+const PersistentDataManager = goog.require('mr.PersistentDataManager');
+const SinkAppStatus = goog.require('mr.dial.SinkAppStatus');
+const SinkDiscoveryCallbacks = goog.require('mr.dial.SinkDiscoveryCallbacks');
+const SinkList = goog.require('mr.SinkList');
+
+
+/**
+ * Implements local discovery using DIAL.
+ * DIAL specification:
+ * http://www.dial-multiscreen.org/dial-protocol-specification
+ * @implements {PersistentData}
+ * @implements {DeviceCountsProvider}
+ */
+class SinkDiscoveryService {
+ /**
+ * @param {!SinkDiscoveryCallbacks} sinkCallBacks
+ * @final
+ */
+ constructor(sinkCallBacks) {
+ /**
+ * @private @const {!SinkDiscoveryCallbacks}
+ */
+ this.sinkCallBacks_ = sinkCallBacks;
+
+ /**
+ * @private @const {?Logger}
+ */
+ this.logger_ = Logger.getInstance('mr.dial.SinkDiscoveryService');
+
+ /**
+ * The current set of *accessible* receivers, indexed by id.
+ * @private @const {!Map<string, !DialSink>}
+ */
+ this.sinkMap_ = new Map();
+
+ /**
+ * The most recent snapshot of device counts.
+ * Updated when a DIAL onDeviceList or onError event is received.
+ * Part of PersistentData.
+ * @private {!DeviceCounts}
+ */
+ this.deviceCounts_ = {availableDeviceCount: 0, knownDeviceCount: 0};
+
+ /**
+ * The last time device counts were recorded in DialAnalytics.
+ * Persistent data.
+ * @private {number}
+ */
+ this.deviceCountMetricsRecordTime_ = 0;
+ }
+
+ /**
+ * Initializes the service. Must be called before any other methods.
+ */
+ init() {
+ PersistentDataManager.register(this);
+ }
+
+ /**
+ * Add |sinks| to sink map. Remove outdated sinks that are in sink map but not
+ * in |sinks|.
+ * @param {!Array<!mojo.Sink>} sinks list of sinks discovered by Media Router.
+ */
+ addSinks(sinks) {
+ this.logger_.info('addSinks returned ' + sinks.length + ' sinks');
+ this.logger_.fine(() => '....the list is: ' + JSON.stringify(sinks));
+
+ const oldSinkIds = new Set(this.sinkMap_.keys());
+ sinks.forEach(mojoSink => {
+ const dialSink = SinkDiscoveryService.convertSink_(mojoSink);
+ this.mayAddSink_(dialSink);
+ oldSinkIds.delete(dialSink.getId());
+ });
+
+ let removedSinks = [];
+ oldSinkIds.forEach(sinkId => {
+ const sink = this.sinkMap_.get(sinkId);
+ removedSinks.push(sink);
+ this.sinkMap_.delete(sinkId);
+ });
+
+ if (removedSinks.length > 0) {
+ this.sinkCallBacks_.onSinksRemoved(removedSinks);
+ }
+
+ // Record device count for feedback.
+ const sinkCount = this.getSinkCount();
+ this.deviceCounts_ = {
+ availableDeviceCount: sinkCount,
+ knownDeviceCount: sinkCount
+ };
+ }
+
+ /**
+ * Updates deviceCounts_ with the given counts, and reports to analytics if
+ * applicable.
+ * @param {number} availableDeviceCount
+ * @param {number} knownDeviceCount
+ * @private
+ */
+ recordDeviceCounts_(availableDeviceCount, knownDeviceCount) {
+ this.deviceCounts_ = {
+ availableDeviceCount: availableDeviceCount,
+ knownDeviceCount: knownDeviceCount
+ };
+ if (Date.now() - this.deviceCountMetricsRecordTime_ <
+ SinkDiscoveryService.DEVICE_COUNT_METRIC_THRESHOLD_MS_) {
+ return;
+ }
+ DialAnalytics.recordDeviceCounts(this.deviceCounts_);
+ this.deviceCountMetricsRecordTime_ = Date.now();
+ }
+
+ /**
+ * Adds or updates an existing sink with the given sink.
+ * @param {!DialSink} sink The new or updated sink.
+ * @private
+ */
+ mayAddSink_(sink) {
+ this.logger_.fine('mayAddSink, id = ' + sink.getId());
+ const sinkToUpdate = this.sinkMap_.get(sink.getId());
+ if (sinkToUpdate) {
+ if (sinkToUpdate.update(sink)) {
+ this.logger_.fine('Updated sink ' + sinkToUpdate.getId());
+ this.sinkCallBacks_.onSinkUpdated(sinkToUpdate);
+ }
+ } else {
+ this.logger_.fine(
+ () => `Adding new sink ${sink.getId()}: ${sink.toDebugString()}`);
+ this.sinkMap_.set(sink.getId(), sink);
+ this.sinkCallBacks_.onSinkAdded(sink);
+ }
+ }
+
+ /**
+ * Converts a mojo.Sink to a DialSink.
+ * @param {!mojo.Sink} mojoSink returned by Media Router at browser side.
+ * @return {!DialSink} DIAL sink.
+ * @private
+ */
+ static convertSink_(mojoSink) {
+
+ const uniqueId = mojoSink.sink_id;
+ const extraData = mojoSink.extra_data.dial_media_sink;
+ const isDiscoveryOnly =
+ SinkDiscoveryService.isDiscoveryOnly_(extraData.model_name);
+
+ const ip_address = extraData.ip_address.address_bytes ?
+ extraData.ip_address.address_bytes.join('.') :
+ extraData.ip_address.address.join('.');
+ return new DialSink(mojoSink.name, uniqueId)
+ .setIpAddress(ip_address)
+ .setDialAppUrl(extraData.app_url.url)
+ .setModelName(extraData.model_name)
+ .setSupportsAppAvailability(!isDiscoveryOnly);
+ }
+
+ /**
+ * Returns true if DIAL (SSDP) was only used to discover this sink, and it is
+ * not expected to support other DIAL features (app discovery, activity
+ * discovery, etc.)
+ * @param {string} modelName
+ * @return {boolean}
+ * @private
+ */
+ static isDiscoveryOnly_(modelName) {
+ return SinkDiscoveryService.DISCOVERY_ONLY_RE_.test(modelName);
+ }
+
+ /**
+ * Returns the sink with the given ID, or null if not found.
+ * @param {string} sinkId
+ * @return {?DialSink}
+ */
+ getSinkById(sinkId) {
+ return this.sinkMap_.get(sinkId) || null;
+ }
+
+ /**
+ * Returns sinks that report availability of the given app name.
+ * @param {string} appName
+ * @return {!SinkList}
+ */
+ getSinksByAppName(appName) {
+ const sinks = [];
+ this.sinkMap_.forEach(dialSink => {
+ if (dialSink.getAppStatus(appName) == SinkAppStatus.AVAILABLE)
+ sinks.push(dialSink.getMrSink());
+ });
+ return new SinkList(
+ sinks, SinkDiscoveryService.APP_ORIGIN_WHITELIST_[appName]);
+ }
+
+ /**
+ * Returns current sinks.
+ * @return {!Array<!DialSink>}
+ */
+ getSinks() {
+ return Array.from(this.sinkMap_.values());
+ }
+
+ /**
+ * @override
+ */
+ getDeviceCounts() {
+ return this.deviceCounts_;
+ }
+
+ /**
+ * @return {number}
+ */
+ getSinkCount() {
+ return this.sinkMap_.size;
+ }
+
+ /**
+ * Invoked when the app status of a sink changes.
+ * @param {string} appName
+ * @param {!DialSink} sink The sink whose status changed.
+ */
+ onAppStatusChanged(appName, sink) {
+ this.sinkCallBacks_.onSinkUpdated(sink);
+ }
+
+ /**
+ * @override
+ */
+ getStorageKey() {
+ return 'dial.DialSinkDiscoveryService';
+ }
+
+ /**
+ * @override
+ */
+ getData() {
+ return [
+ new SinkDiscoveryService.PersistentData_(
+ Array.from(this.sinkMap_), this.deviceCounts_),
+ {'deviceCountMetricsRecordTime': this.deviceCountMetricsRecordTime_}
+ ];
+ }
+
+ /**
+ * @override
+ */
+ loadSavedData() {
+ const tempData =
+ /** @type {?SinkDiscoveryService.PersistentData_} */ (
+ PersistentDataManager.getTemporaryData(this));
+ if (tempData) {
+ for (const entry of tempData.sinks) {
+ this.sinkMap_.set(entry[0], DialSink.createFrom(entry[1]));
+ }
+ this.deviceCounts_ = tempData.deviceCounts;
+ }
+
+ const permanentData = PersistentDataManager.getPersistentData(this);
+ if (permanentData) {
+ this.deviceCountMetricsRecordTime_ =
+ permanentData['deviceCountMetricsRecordTime'];
+ }
+ }
+}
+
+
+/**
+ * @private @const {!Object<string, !Array<string>>}
+ */
+SinkDiscoveryService.APP_ORIGIN_WHITELIST_ = {
+ 'YouTube': [
+ 'https://tv.youtube.com', 'https://tv-green-qa.youtube.com',
+ 'https://tv-release-qa.youtube.com', 'https://web-green-qa.youtube.com',
+ 'https://web-release-qa.youtube.com', 'https://www.youtube.com'
+ ],
+ 'Netflix': ['https://www.netflix.com'],
+ 'Pandora': ['https://www.pandora.com'],
+ 'Radio': ['https://www.pandora.com'],
+ 'Hulu': ['https://www.hulu.com'],
+ 'Vimeo': ['https://www.vimeo.com'],
+ 'Dailymotion': ['https://www.dailymotion.com'],
+ 'com.dailymotion': ['https://www.dailymotion.com'],
+};
+
+
+/**
+ * Matches DIAL model names that only support discovery.
+
+ * @private @const {!RegExp}
+ */
+SinkDiscoveryService.DISCOVERY_ONLY_RE_ =
+ new RegExp('Eureka Dongle|Chromecast Audio|Chromecast Ultra', 'i');
+
+/**
+ * How long to wait between device counts metrics are recorded. Set to 1 hour.
+ * @private @const {number}
+ */
+SinkDiscoveryService.DEVICE_COUNT_METRIC_THRESHOLD_MS_ = 60 * 60 * 1000;
+
+
+/**
+ * @private
+ */
+SinkDiscoveryService.PersistentData_ = class {
+ /**
+ * @param {!Array} sinks
+ * @param {!DeviceCounts} deviceCounts
+ */
+ constructor(sinks, deviceCounts) {
+ /**
+ * @const {!Array}
+ */
+ this.sinks = sinks;
+
+ /**
+ * @const {!DeviceCounts}
+ */
+ this.deviceCounts = deviceCounts;
+ }
+};
+
+exports = SinkDiscoveryService;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_discovery_service_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_discovery_service_test.js
new file mode 100644
index 00000000000..1aad198346c
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_discovery_service_test.js
@@ -0,0 +1,163 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.SinkDiscoveryServiceTest');
+goog.setTestOnly('mr.dial.SinkDiscoveryServiceTest');
+
+const DialAnalytics = goog.require('mr.DialAnalytics');
+const PersistentDataManager = goog.require('mr.PersistentDataManager');
+const SinkAppStatus = goog.require('mr.dial.SinkAppStatus');
+const SinkDiscoveryService = goog.require('mr.dial.SinkDiscoveryService');
+const UnitTestUtils = goog.require('mr.UnitTestUtils');
+
+describe('DIAL SinkDiscoveryService Tests', function() {
+ let service;
+ let mockClock;
+ let mockSinkCallbacks;
+
+ beforeEach(function() {
+ mockClock = UnitTestUtils.useMockClockAndPromises();
+ mockSinkCallbacks = jasmine.createSpyObj(
+ 'SinkCallbacks', ['onSinkAdded', 'onSinksRemoved', 'onSinkUpdated']);
+
+ chrome.metricsPrivate = {
+ recordTime: jasmine.createSpy('recordTime'),
+ recordMediumTime: jasmine.createSpy('recordMediumTime'),
+ recordLongTime: jasmine.createSpy('recordLongTime'),
+ recordUserAction: jasmine.createSpy('recordUserAction')
+ };
+
+ service = new SinkDiscoveryService(mockSinkCallbacks);
+ spyOn(DialAnalytics, 'recordDeviceCounts');
+ });
+
+ afterEach(function() {
+ UnitTestUtils.restoreRealClockAndPromises();
+ PersistentDataManager.clear();
+ });
+
+
+ /**
+ * Creates mojo sink instances.
+ * @param {number} numSinks The number of mojo sinks to create.
+ * @return {!Array<!mojo.Sink>} The mojo sinks.
+ */
+ function createMojoSinks(numSinks) {
+ const mojoSinks = [];
+ for (var i = 1; i <= numSinks; i++) {
+ const dialMediaSink = {
+ ip_address: {address_bytes: [127, 0, 0, i]},
+ model_name: 'Eureka Dongle',
+ app_url: {url: 'http://127.0.0.' + i + ':8008/apps'}
+ };
+
+ mojoSinks.push({
+ sink_id: 'sinkId ' + i,
+ name: 'TV ' + i,
+ extra_data: {dial_media_sink: dialMediaSink}
+ });
+ }
+ return mojoSinks;
+ }
+
+ describe('addSinks tests', function() {
+ beforeEach(function() {
+ service.init();
+ });
+
+ it('add mojo sinks to sink map', function() {
+ expect(service.getSinks().length).toBe(0);
+ const mojoSinks = createMojoSinks(1);
+ service.addSinks(mojoSinks);
+
+ // sinks were added
+ const actualSinks = service.getSinks();
+ expect(actualSinks.length).toBe(1);
+
+ const actualSink = actualSinks[0];
+ const mojoSink = mojoSinks[0];
+ const extraData = mojoSink.extra_data.dial_media_sink;
+ expect(actualSink.getFriendlyName()).toEqual(mojoSink.name);
+ expect(actualSink.getIpAddress())
+ .toEqual(extraData.ip_address.address_bytes.join('.'));
+ expect(actualSink.getDialAppUrl()).toEqual(extraData.app_url.url);
+ expect(actualSink.getModelName()).toEqual(extraData.model_name);
+ expect(actualSink.supportsAppAvailability()).toEqual(false);
+
+ // add-sink-events were fired.
+ expect(mockSinkCallbacks.onSinkAdded.calls.count()).toBe(1);
+ expect(mockSinkCallbacks.onSinksRemoved.calls.count()).toBe(0);
+ });
+
+ it('remove outdated sinks', function() {
+ expect(service.getSinks().length).toBe(0);
+ const mojoSinks = createMojoSinks(3);
+ // First round discover sink 1, 2, 3
+ service.addSinks(mojoSinks);
+
+ // Second round discover sink 1
+ const mojoSinks2 = createMojoSinks(1);
+ service.addSinks(mojoSinks2);
+ expect(mockSinkCallbacks.onSinkAdded.calls.count()).toBe(3);
+
+ // 2 devices were removed
+ expect(mockSinkCallbacks.onSinksRemoved.calls.count()).toBe(1);
+ const sinks = mockSinkCallbacks.onSinksRemoved.calls.argsFor(0)[0];
+ expect(sinks.length).toBe(2);
+ expect(sinks[0].getFriendlyName()).toEqual(mojoSinks[1].name);
+ expect(sinks[1].getFriendlyName()).toEqual(mojoSinks[2].name);
+
+ expect(mockSinkCallbacks.onSinkUpdated.calls.count()).toBe(0);
+ });
+
+ it('Gets sinks by app name', function() {
+ const mojoSinks = createMojoSinks(3);
+ service.addSinks(mojoSinks);
+ service.getSinkById(mojoSinks[0].sink_id)
+ .setAppStatus('YouTube', SinkAppStatus.AVAILABLE);
+ service.getSinkById(mojoSinks[1].sink_id)
+ .setAppStatus('Netflix', SinkAppStatus.AVAILABLE);
+ service.getSinkById(mojoSinks[1].sink_id)
+ .setAppStatus('YouTube', SinkAppStatus.AVAILABLE);
+ service.getSinkById(mojoSinks[2].sink_id)
+ .setAppStatus('Pandora', SinkAppStatus.UNAVAILABLE);
+ expect(service.getSinksByAppName('YouTube').sinks.length).toBe(2);
+ expect(service.getSinksByAppName('Netflix').sinks.length).toBe(1);
+ expect(service.getSinksByAppName('Netflix').sinks[0].id)
+ .toEqual(mojoSinks[1].sink_id);
+ expect(service.getSinksByAppName('Pandora').sinks.length).toBe(0);
+ });
+
+ });
+
+ it('Saves PersistentData without any data', function() {
+ service.init();
+ expect(service.getSinks()).toEqual([]);
+ PersistentDataManager.suspendForTest();
+ service = new SinkDiscoveryService(mockSinkCallbacks);
+ service.loadSavedData();
+ expect(service.getSinks()).toEqual([]);
+ });
+
+ it('Saves PersistentData with data', function() {
+ service.init();
+ service.addSinks(createMojoSinks(3));
+ mockClock.tick(1);
+ const sinks = service.getSinks();
+ expect(sinks.length).toBe(3);
+ const expectedDeviceCounts = {availableDeviceCount: 3, knownDeviceCount: 3};
+ expect(service.getDeviceCounts()).toEqual(expectedDeviceCounts);
+
+ PersistentDataManager.suspendForTest();
+ service = new SinkDiscoveryService(mockSinkCallbacks);
+ service.loadSavedData();
+ const restoredSinks = service.getSinks();
+ expect(restoredSinks.length).toBe(3);
+ expect(service.getDeviceCounts()).toEqual(expectedDeviceCounts);
+ for (let index = 0; index < 3; index++) {
+ expect(sinks[0].getId()).toEqual(restoredSinks[0].getId());
+ }
+ });
+
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_test.js
new file mode 100644
index 00000000000..03a467550bd
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/dial_sink_test.js
@@ -0,0 +1,127 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.SinkTest');
+goog.setTestOnly('mr.dial.SinkTest');
+
+const DialSink = goog.require('mr.dial.Sink');
+const MockClock = goog.require('mr.MockClock');
+const Sink = goog.require('mr.Sink');
+const SinkAppStatus = goog.require('mr.dial.SinkAppStatus');
+
+describe('DIAL Sink Tests', function() {
+ let mockClock;
+
+ beforeEach(function() {
+ mockClock = new MockClock(true);
+ });
+
+ afterEach(function() {
+ mockClock.uninstall();
+ });
+
+ it('Gets and sets fields', function() {
+ const sink = new DialSink('name', 'id1');
+ expect(sink.getMrSink()).toEqual(new Sink('id1', 'name'));
+
+ expect(sink.getId()).toEqual('id1');
+ sink.setId('id2');
+ expect(sink.getId()).toEqual('id2');
+
+ expect(sink.getIpAddress()).toBeNull();
+ sink.setIpAddress('192.168.111.1');
+ expect(sink.getIpAddress()).toEqual('192.168.111.1');
+
+ expect(sink.getDialAppUrl()).toBeNull();
+ sink.setDialAppUrl('http://192.168.111.1/apps');
+ expect(sink.getDialAppUrl()).toEqual('http://192.168.111.1/apps');
+
+ expect(sink.getDeviceDescriptionUrl()).toBeNull();
+ sink.setDeviceDescriptionUrl('http://192.168.111.1/desc');
+ expect(sink.getDeviceDescriptionUrl()).toEqual('http://192.168.111.1/desc');
+
+ expect(sink.getModelName()).toBeNull();
+ sink.setModelName('chromecast');
+ expect(sink.getModelName()).toEqual('chromecast');
+
+ expect(sink.getFriendlyName()).toEqual('name');
+ sink.setFriendlyName('newname');
+ expect(sink.getFriendlyName()).toEqual('newname');
+
+ expect(sink.getPort()).toEqual(null);
+ sink.setPort(8009);
+ expect(sink.getPort()).toEqual(8009);
+ });
+
+ it('Gets and sets sink app status', function() {
+ const sink = new DialSink('name', 'uniqueId');
+ expect(sink.getAppStatus('youtube')).toBe(SinkAppStatus.UNKNOWN);
+ expect(sink.getAppStatusTimeStamp('youtube')).toBe(null);
+
+ mockClock.tick(10);
+ const now1 = Date.now();
+ sink.setAppStatus('youtube', SinkAppStatus.AVAILABLE);
+ mockClock.tick(10);
+ const now2 = Date.now();
+ sink.setAppStatus('app2', SinkAppStatus.UNAVAILABLE);
+ expect(sink.getAppStatus('youtube')).toBe(SinkAppStatus.AVAILABLE);
+ expect(sink.getAppStatusTimeStamp('youtube')).toBe(now1);
+ expect(sink.getAppStatus('app2')).toBe(SinkAppStatus.UNAVAILABLE);
+ expect(sink.getAppStatusTimeStamp('app2')).toBe(now2);
+
+ sink.clearAppStatus();
+ expect(sink.getAppStatus('youtube')).toBe(SinkAppStatus.UNKNOWN);
+ expect(sink.getAppStatusTimeStamp('youtube')).toBe(null);
+ expect(sink.getAppStatus('app2')).toBe(SinkAppStatus.UNKNOWN);
+ expect(sink.getAppStatusTimeStamp('app2')).toBe(null);
+ });
+
+ it('Updates sink from another sink', function() {
+ const sink = new DialSink('name', 'uniqueId')
+ .setDialAppUrl('http://192.168.111.1/apps')
+ .setPort(8009)
+ .setDeviceDescriptionUrl('http://192.168.111.1/desc');
+
+ let updatedSink = new DialSink('name2', 'uniqueId');
+ expect(sink.update(updatedSink)).toBe(true);
+ expect(sink.getFriendlyName()).toEqual('name2');
+
+ updatedSink = new DialSink('name', 'uniqueId')
+ .setDialAppUrl('http://192.168.111.1/apps/app2');
+ expect(sink.update(updatedSink)).toBe(true);
+ expect(sink.getDialAppUrl()).toEqual('http://192.168.111.1/apps/app2');
+
+ updatedSink = new DialSink('name', 'uniqueId').setId('id2');
+ expect(sink.update(updatedSink)).toBe(false);
+ });
+
+ it('Updates ip address', function() {
+ const sink = new DialSink('name', 'uniqueId').setIpAddress('192.168.111.1');
+ const updatedSink =
+ new DialSink('name', 'uniqueId').setIpAddress('192.168.111.2');
+ expect(sink.update(updatedSink)).toBe(true);
+ expect(sink.getIpAddress()).toEqual('192.168.111.2');
+ });
+
+ it('Updates device description url', function() {
+ const sink = new DialSink('name', 'uniqueId')
+ .setDeviceDescriptionUrl('http://192.168.111.1/desc');
+ const updatedSink =
+ new DialSink('name', 'uniqueId')
+ .setDeviceDescriptionUrl('http://192.168.111.2/desc');
+ expect(sink.update(updatedSink)).toBe(true);
+ expect(sink.getDeviceDescriptionUrl()).toEqual('http://192.168.111.2/desc');
+ });
+
+ it('Creates sink from an Object', function() {
+ const sink = new DialSink('name', 'uniqueId')
+ .setDialAppUrl('http://192.168.111.1/apps')
+ .setPort(8009)
+ .setDeviceDescriptionUrl('http://192.168.111.1/desc')
+ .setAppStatus('youtube', SinkAppStatus.AVAILABLE)
+ .setIpAddress('192.168.111.1')
+ .setModelName('chromecast');
+ expect(DialSink.createFrom(sink)).toEqual(sink);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/presentation_url.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/presentation_url.js
new file mode 100644
index 00000000000..203b40e134f
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/presentation_url.js
@@ -0,0 +1,146 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.dial.PresentationUrl');
+
+const Logger = goog.require('mr.Logger');
+const base64 = goog.require('mr.base64');
+
+
+/**
+ * Represents a DIAL media source containing information specific to a DIAL
+ * launch.
+ */
+const PresentationUrl = class {
+ /**
+ * @param {string} appName The DIAL application name.
+ * @param {string=} launchParameter DIAL application launch parameter.
+ */
+ constructor(appName, launchParameter = '') {
+ /** @const {string} */
+ this.appName = appName;
+ /** @const {string} */
+ this.launchParameter = launchParameter;
+ }
+
+ /**
+ * Generates a DIAL Presentation URL using given parameters.
+ * @param {string} dialAppName Name of the DIAL app.
+ * @param {?string} dialPostData base-64 encoded string of the data for the
+ * DIAL launch.
+ * @return {string}
+ */
+ static getPresentationUrlAsString(dialAppName, dialPostData) {
+ const url = new URL('dial:' + dialAppName);
+ if (dialPostData) {
+ url.searchParams.set('postData', dialPostData);
+ }
+ return url.toString();
+ }
+
+ /**
+ * Constructs a DIAL media source from a URL. The URL can take on the new
+ * format (with dial: protocol) or the old format (with https: protocol).
+ * @param {string} urlString The media source URL.
+ * @return {?PresentationUrl} A DIAL media source if the parse was
+ * successful, null otherwise.
+ */
+ static create(urlString) {
+ let url;
+ try {
+ url = new URL(urlString);
+ } catch (err) {
+ PresentationUrl.logger_.info('Invalid URL: ' + urlString);
+ return null;
+ }
+ switch (url.protocol) {
+ case 'dial:':
+ return PresentationUrl.parseDialUrl_(url);
+ case 'https:':
+
+ return PresentationUrl.parseLegacyUrl_(url);
+ default:
+ PresentationUrl.logger_.fine('Unhandled protocol: ' + url.protocol);
+ return null;
+ }
+ }
+
+ /**
+ * Parses the given URL using the new DIAL URL format, which takes the form:
+ * dial:<App name>?postData=<base64-encoded launch parameters>
+ * @param {!URL} url
+ * @return {?PresentationUrl}
+ * @private
+ */
+ static parseDialUrl_(url) {
+ const appName = url.pathname;
+ if (!appName.match(/^\w+$/)) {
+ PresentationUrl.logger_.warning('Invalid app name: ' + appName);
+ return null;
+ }
+ let postData = url.searchParams.get('postData') || undefined;
+ if (postData) {
+ try {
+ postData = base64.decodeString(postData);
+ } catch (err) {
+ PresentationUrl.logger_.warning(
+ 'Invalid base64 encoded postData:' + postData);
+ return null;
+ }
+ }
+ return new PresentationUrl(appName, postData);
+ }
+
+ /**
+ * Parses the given URL using the legacy format specified in
+ * http://goo.gl/8qKAE7
+ * Example:
+ * http://www.youtube.com/tv#__dialAppName__=YouTube/__dialPostData__=dj0xMjM=
+ * @param {!URL} url
+ * @return {?PresentationUrl}
+ * @private
+ */
+ static parseLegacyUrl_(url) {
+ // Parse URI and get fragment.
+ const fragment = url.hash;
+ if (!fragment) return null;
+ let appName = PresentationUrl.APP_NAME_REGEX_.exec(fragment);
+ appName = appName ? appName[1] : null;
+ if (!appName) return null;
+ appName = decodeURIComponent(appName);
+
+ let postData = PresentationUrl.LAUNCH_PARAM_REGEX_.exec(fragment);
+ postData = postData ? postData[1] : undefined;
+ if (postData) {
+ try {
+ postData = base64.decodeString(postData);
+ } catch (err) {
+ PresentationUrl.logger_.warning(
+ 'Invalid base64 encoded postData:' + postData);
+ return null;
+ }
+ }
+ return new PresentationUrl(appName, postData);
+ }
+};
+
+
+/** @const @private {?Logger} */
+PresentationUrl.logger_ = Logger.getInstance('mr.dial.PresentationUrl');
+
+
+/** @const {string} */
+PresentationUrl.URN_PREFIX = 'urn:dial-multiscreen-org:dial:application:';
+
+
+/** @private @const {!RegExp} */
+PresentationUrl.APP_NAME_REGEX_ =
+ /__dialAppName__=([A-Za-z0-9-._~!$&'()*+,;=%]+)/;
+
+
+/** @private @const {!RegExp} */
+PresentationUrl.LAUNCH_PARAM_REGEX_ = /__dialPostData__=([A-Za-z0-9]+={0,2})/;
+
+
+exports = PresentationUrl;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/presentation_url_test.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/presentation_url_test.js
new file mode 100644
index 00000000000..4477e5ee404
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/presentation_url_test.js
@@ -0,0 +1,75 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('PresentationUrlTest');
+goog.setTestOnly('PresentationUrlTest');
+
+const PresentationUrl = goog.require('mr.dial.PresentationUrl');
+
+describe('Tests PresentationUrl', function() {
+ it('Does not create from empty input', function() {
+ expect(PresentationUrl.create('')).toBeNull();
+ });
+
+ it('Creates from a valid URL', function() {
+ expect(PresentationUrl.create(
+ 'https://www.youtube.com/tv#__dialAppName__=YouTube'))
+ .toEqual(new PresentationUrl('YouTube'));
+ });
+
+ it('Creates from a valid URL with launch parameters', function() {
+ expect(PresentationUrl.create(
+ 'https://www.youtube.com/tv#' +
+ '__dialAppName__=YouTube/__dialPostData__=dj0xMjM='))
+ .toEqual(new PresentationUrl('YouTube', 'v=123'));
+ expect(PresentationUrl.create(
+ 'https://www.youtube.com/tv#' +
+ '__dialAppName__=YouTube/__dialPostData__=dj1NSnlKS3d6eEZwWQ=='))
+ .toEqual(new PresentationUrl('YouTube', 'v=MJyJKwzxFpY'));
+ });
+
+ it('Does not create from an invalid URL', function() {
+ expect(PresentationUrl.create(
+ 'https://www.youtube.com/tv#___emanPpaLiad__=YouTube'))
+ .toBeNull();
+ });
+
+ it('Does not create from an invalid postData', function() {
+ expect(PresentationUrl.create(
+ 'https://www.youtube.com/tv#___emanPpaLiad__=YouTube' +
+ '/__dialPostData__=dj1=N'))
+ .toBeNull();
+ });
+
+ it('Creates from DIAL URL', () => {
+ expect(PresentationUrl.create('dial:YouTube'))
+ .toEqual(new PresentationUrl('YouTube'));
+ expect(PresentationUrl.create('dial:YouTube?foo=bar'))
+ .toEqual(new PresentationUrl('YouTube'));
+ expect(PresentationUrl.create('dial:YouTube?foo=bar&postData=dj0xMjM='))
+ .toEqual(new PresentationUrl('YouTube', 'v=123'));
+ expect(PresentationUrl.create('dial:YouTube?postData=dj0xMjM%3D'))
+ .toEqual(new PresentationUrl('YouTube', 'v=123'));
+ });
+
+ it('Does not create from invalid DIAL URL', () => {
+ expect(PresentationUrl.create('dial:')).toBeNull();
+ expect(PresentationUrl.create('dial://')).toBeNull();
+ expect(PresentationUrl.create('dial://YouTube')).toBeNull();
+ expect(
+ PresentationUrl.create('dial:YouTube?postData=notEncodedProperly111'))
+ .toBeNull();
+ });
+
+ it('Does not create from URL of unknown protocol', () => {
+ expect(PresentationUrl.create('unknown:YouTube')).toBeNull();
+ });
+
+ it('getPresentationUrl returns DIAL presentation URLs', () => {
+ expect(PresentationUrl.getPresentationUrlAsString('YouTube', null))
+ .toEqual('dial:YouTube');
+ expect(PresentationUrl.getPresentationUrlAsString('YouTube', 'dj0xMjM='))
+ .toEqual('dial:YouTube?postData=dj0xMjM%3D');
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/sink_app_status.js b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/sink_app_status.js
new file mode 100644
index 00000000000..5acd9a9a8fa
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/providers/dial/sink_app_status.js
@@ -0,0 +1,23 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview The availability of an app on a sink.
+ */
+
+goog.provide('mr.dial.SinkAppStatus');
+
+
+/**
+ * Tracks the availability of an app on a sink. Apps start out in an
+ * UNKNOWN status and are changed to AVAILABLE or UNAVAILABLE once the status is
+ * known, i.e. after we query the sink for the app.
+ *
+ * @enum {string}
+ */
+mr.dial.SinkAppStatus = {
+ AVAILABLE: 'available',
+ UNAVAILABLE: 'unavailable',
+ UNKNOWN: 'unknown'
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/analytics.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/analytics.js
new file mode 100644
index 00000000000..8761cc404d7
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/analytics.js
@@ -0,0 +1,305 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/** @fileoverview API for Analytics events. */
+
+goog.provide('mr.Analytics');
+goog.provide('mr.LongTiming');
+goog.provide('mr.MediumTiming');
+goog.provide('mr.Timing');
+
+goog.require('mr.Logger');
+
+
+/**
+ * Begins the timing period.
+ */
+mr.Timing = class {
+ /**
+ * @param {!string} name
+ */
+ constructor(name) {
+ /** @private {!string} */
+ this.name_ = name;
+
+ /** @private {number} */
+ this.startTime_ = Date.now();
+ }
+
+ /**
+ * Gets the full name with the suffix appended if provided.
+ * @param {string} name The name of the event or histogram.
+ * @param {string=} opt_suffix The optional suffix to add.
+ * @return {string} The full name with suffix added.
+ * @private
+ */
+ getFullName_(name, opt_suffix) {
+ if (opt_suffix != null) {
+ name += '_' + opt_suffix;
+ }
+ return name;
+ }
+
+ /**
+ * Sets the name for the timing object.
+ * @param {string} name The new name.
+ */
+ setName(name) {
+ this.name_ = name;
+ }
+
+ /**
+ * Ends the timing period and reports to UMA.
+ * @param {string=} opt_suffix An optional suffix.
+ */
+ end(opt_suffix) {
+ const duration = Date.now() - this.startTime_;
+ const name = this.getFullName_(this.name_, opt_suffix);
+ mr.Timing.recordDuration(name, duration);
+ }
+
+ /**
+ * Sends a short duration value (up to 10 seconds) for analytics collection.
+ * @param {string} name
+ * @param {number} duration Duration in milliseconds.
+ */
+ static recordDuration(name, duration) {
+ if (duration < 0) {
+ mr.Timing.logger_.warning('Timing analytics event with negative time');
+ duration = 0;
+ }
+
+ if (duration > mr.Timing.TEN_SECONDS_) {
+ duration = mr.Timing.TEN_SECONDS_;
+ }
+
+ try {
+ chrome.metricsPrivate.recordTime(name, duration);
+ } catch (e) {
+ mr.Timing.logger_.warning(
+ 'Failed to record time ' + duration + ' in ' + name);
+ }
+ }
+};
+
+
+/** @private const */
+mr.Timing.logger_ = mr.Logger.getInstance('mr.Timing');
+
+
+/**
+ * Ten seconds in milliseconds.
+ * @const {number}
+ * @private
+ */
+mr.Timing.TEN_SECONDS_ = 10 * 1000;
+
+
+/**
+ * Begins a medium timing period (should be measured in seconds).
+ */
+mr.MediumTiming = class extends mr.Timing {
+ /**
+ * @param {string} name The histogram name.
+ */
+ constructor(name) {
+ super(name);
+ }
+
+ /**
+ * @override
+ */
+ end(opt_suffix) {
+ const duration = Date.now() - this.startTime_;
+ const name = this.getFullName_(this.name_, opt_suffix);
+ mr.MediumTiming.recordDuration(name, duration);
+ }
+
+ /**
+ * Sends a medium duration value (up to 3 minutes) for analytics collection.
+ * @param {string} name
+ * @param {number} duration Duration in milliseconds.
+ */
+ static recordDuration(name, duration) {
+ if (duration < 0) {
+ mr.MediumTiming.logger_.warning(
+ 'Timing analytics event with negative time');
+ return;
+ }
+
+ if (duration < mr.Timing.TEN_SECONDS_) {
+ duration = mr.Timing.TEN_SECONDS_;
+ }
+
+ if (duration > mr.MediumTiming.THREE_MINUTES_) {
+ duration = mr.MediumTiming.THREE_MINUTES_;
+ }
+
+ try {
+ chrome.metricsPrivate.recordMediumTime(name, duration);
+ } catch (e) {
+ mr.MediumTiming.logger_.warning(
+ 'Failed to record time ' + duration + ' in ' + name);
+ }
+ }
+};
+
+
+/** @private @const */
+mr.MediumTiming.logger_ = mr.Logger.getInstance('mr.MediumTiming');
+
+
+/**
+ * Constant of 3 minutes (in milliseconds).
+ * @private @const {number}
+ **/
+mr.MediumTiming.THREE_MINUTES_ = 3 * 60 * 1000;
+
+
+/**
+ * Begins a long timing period (up to 1 hour).
+ */
+mr.LongTiming = class extends mr.Timing {
+ /**
+ * @param {string} name The name of the histogram.
+ */
+ constructor(name) {
+ super(name);
+ }
+
+ /**
+ * @override
+ */
+ end(opt_suffix) {
+ const duration = Date.now() - this.startTime_;
+ const name = this.getFullName_(this.name_, opt_suffix);
+ mr.LongTiming.recordDuration(name, duration);
+ }
+
+ /**
+ * Sends a long duration value (up to 1 hour) for analytics collection.
+ * @param {string} name
+ * @param {number} duration Duration in milliseconds.
+ */
+ static recordDuration(name, duration) {
+ if (duration < 0) {
+ mr.LongTiming.logger_.warning(
+ 'Timing analytics event with negative time');
+ return;
+ }
+
+ if (duration < mr.MediumTiming.THREE_MINUTES_) {
+ duration = mr.MediumTiming.THREE_MINUTES_;
+ }
+
+ if (duration > mr.LongTiming.ONE_HOUR_) {
+ duration = mr.LongTiming.ONE_HOUR_;
+ }
+
+ try {
+ chrome.metricsPrivate.recordLongTime(name, duration);
+ } catch (e) {
+ mr.LongTiming.logger_.warning(
+ 'Failed to record time ' + duration + ' in ' + name);
+ }
+ }
+};
+
+
+/** @private @const */
+mr.LongTiming.logger_ = mr.Logger.getInstance('mr.LongTiming');
+
+
+/**
+ * Constant of 1 hour (in milliseconds).
+ * @private @const {number}
+ **/
+mr.LongTiming.ONE_HOUR_ = 60 * 60 * 1000;
+
+
+/** @const {*} */
+mr.Analytics = {};
+
+
+/**
+ * @const {mr.Logger}
+ * @private
+ */
+mr.Analytics.logger_ = mr.Logger.getInstance('mr.Analytics');
+
+
+/**
+ * Sends a user action for analytics collection.
+ * @param {!string} name
+ */
+mr.Analytics.recordEvent = function(name) {
+ try {
+ chrome.metricsPrivate.recordUserAction(name);
+ } catch (e) {
+ mr.Analytics.logger_.warning('Failed to record event ' + name);
+ }
+};
+
+
+/**
+ * Send a value for analytics collection.
+ * @param {!string} name
+ * @param {!number} value
+ * @param {!Object<string,number>} values
+ */
+mr.Analytics.recordEnum = function(name, value, values) {
+ let foundKey;
+ let size = 0;
+ for (let key in values) {
+ size++;
+ if (values[key] == value) {
+ foundKey = key;
+ }
+ }
+ if (!foundKey) {
+ mr.Analytics.logger_.error(
+ 'Unknown analytics value, ' + value + ' for histogram, ' + name,
+ Error() /* for stack trace */);
+ return;
+ }
+
+ const config = {
+ 'metricName': name,
+ 'type': 'histogram-linear',
+ 'min': 1,
+ 'max': size,
+ // Add one for the underflow bucket.
+ 'buckets': size + 1
+ };
+
+ try {
+ chrome.metricsPrivate.recordValue(config, value);
+ } catch (/** Error */ e) {
+ mr.Analytics.logger_.warning(
+ 'Failed to record enum value ' + foundKey + ' (' + value + ') in ' +
+ name,
+ e);
+ }
+};
+
+
+/**
+ * Records a small count (0 to 100) for analytics collection.
+ * @param {string} name
+ * @param {number} count
+ */
+mr.Analytics.recordSmallCount = function(name, count) {
+ try {
+ if (count < 0) {
+ throw new Error(`Invalid count for ${name}: ${count}`);
+ } else if (count > 100) {
+ mr.Analytics.logger_.warning(
+ `Small count for ${name} exceeded limits: ${count}`, Error());
+ }
+ chrome.metricsPrivate.recordSmallCount(name, count);
+ } catch (/** Error */ e) {
+ mr.Analytics.logger_.warning(
+ `Failed to record small count ${name} (${count})`, e);
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/analytics_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/analytics_test.js
new file mode 100644
index 00000000000..e9f6ee2274c
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/analytics_test.js
@@ -0,0 +1,246 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly();
+goog.require('mr.Analytics');
+goog.require('mr.LongTiming');
+goog.require('mr.MediumTiming');
+goog.require('mr.MockClock');
+goog.require('mr.Timing');
+
+describe('Tests Analytics', function() {
+ let mockClock;
+
+ const TEN_SECONDS = 10 * 1000;
+ const THREE_MINUTES = 3 * 60 * 1000;
+ const ONE_HOUR = 60 * 60 * 1000;
+
+ beforeEach(function() {
+ mockClock = new mr.MockClock(true);
+ chrome.metricsPrivate = {
+ recordTime: jasmine.createSpy('recordTime'),
+ recordMediumTime: jasmine.createSpy('recordMediumTime'),
+ recordLongTime: jasmine.createSpy('recordLongTime'),
+ recordUserAction: jasmine.createSpy('recordUserAction'),
+ recordValue: jasmine.createSpy('recordValue'),
+ recordSmallCount: jasmine.createSpy('recordSmallCount'),
+ };
+ });
+
+ afterEach(function() {
+ mockClock.uninstall();
+ });
+
+ describe('Test Timing Events', function() {
+ describe('Test mr.Timing', function() {
+ it('Should record the time passing', function() {
+ const histogramName = 'Test';
+ const timeToPass = 34;
+ const timing = new mr.Timing(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordTime)
+ .toHaveBeenCalledWith(histogramName, timeToPass);
+ });
+ it('Should record the time passing with a suffix', function() {
+ const histogramName = 'Test';
+ const suffixName = 'Test';
+ const expectedFinalName = histogramName + '_' + suffixName;
+ const timeToPass = 34;
+ const timing = new mr.Timing(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end(suffixName);
+ expect(chrome.metricsPrivate.recordTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordTime)
+ .toHaveBeenCalledWith(expectedFinalName, timeToPass);
+ });
+ it('Should record the max if duration exceeds ten seconds', function() {
+ const histogramName = 'Test';
+ const timeToPass = TEN_SECONDS + 1;
+ const timing = new mr.Timing(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordTime)
+ .toHaveBeenCalledWith(histogramName, TEN_SECONDS);
+ });
+ it('Should record the minimum if duration is negative', function() {
+ const histogramName = 'Test';
+ const timeToPass = -1;
+ const timing = new mr.Timing(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordTime)
+ .toHaveBeenCalledWith(histogramName, 0);
+ });
+ });
+ describe('Test mr.MediumTiming', function() {
+ it('Should record the time passing', function() {
+ const histogramName = 'Test';
+ const timeToPass = 34 * 1000;
+ const timing = new mr.MediumTiming(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordMediumTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordMediumTime)
+ .toHaveBeenCalledWith(histogramName, timeToPass);
+ });
+ it('Should record the time passing with a suffix', function() {
+ const histogramName = 'Test';
+ const suffixName = 'Test';
+ const expectedFinalName = histogramName + '_' + suffixName;
+ const timeToPass = 34 * 1000;
+ const timing = new mr.MediumTiming(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end(suffixName);
+ expect(chrome.metricsPrivate.recordMediumTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordMediumTime)
+ .toHaveBeenCalledWith(expectedFinalName, timeToPass);
+ });
+ it('Should record ten seconds if duration is below ten seconds',
+ function() {
+ const histogramName = 'Test';
+ const timeToPass = 34;
+ const timing = new mr.MediumTiming(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordMediumTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordMediumTime)
+ .toHaveBeenCalledWith(histogramName, TEN_SECONDS);
+ });
+ it('Should record three minutes if duration exceeds three minutes',
+ function() {
+ const histogramName = 'Test';
+ const timeToPass = THREE_MINUTES + 1;
+ const timing = new mr.MediumTiming(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordMediumTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordMediumTime)
+ .toHaveBeenCalledWith(histogramName, THREE_MINUTES);
+ });
+ });
+ describe('Test mr.LongTiming', function() {
+ it('Should record the time passing', function() {
+ const histogramName = 'Test';
+ const timeToPass = 34 * 60 * 1000;
+ const timing = new mr.LongTiming(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordLongTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordLongTime)
+ .toHaveBeenCalledWith(histogramName, timeToPass);
+ });
+ it('Should record the time passing with a suffix', function() {
+ const histogramName = 'Test';
+ const suffixName = 'Test';
+ const expectedFinalName = histogramName + '_' + suffixName;
+ const timeToPass = 34 * 60 * 1000;
+ const timing = new mr.LongTiming(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end(suffixName);
+ expect(chrome.metricsPrivate.recordLongTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordLongTime)
+ .toHaveBeenCalledWith(expectedFinalName, timeToPass);
+ });
+ it('Should record three minutes if duration is below three minutes',
+ function() {
+ const histogramName = 'Test';
+ const timeToPass = 2 * 60 * 1000;
+ const timing = new mr.LongTiming(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordLongTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordLongTime)
+ .toHaveBeenCalledWith(histogramName, THREE_MINUTES);
+ });
+ it('Should record one hour if duration exceeds one hour', function() {
+ const histogramName = 'Test';
+ const timeToPass = ONE_HOUR + 1;
+ const timing = new mr.LongTiming(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordLongTime.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordLongTime)
+ .toHaveBeenCalledWith(histogramName, ONE_HOUR);
+ });
+ it('Should not record the time if it we went back in time', function() {
+ const histogramName = 'Test';
+ const timeToPass = -1;
+ const timing = new mr.LongTiming(histogramName);
+ mockClock.tick(timeToPass);
+ timing.end();
+ expect(chrome.metricsPrivate.recordLongTime.calls.count()).toBe(0);
+ });
+ });
+ });
+ describe('Test recordEvent', function() {
+ it('Should record an event', function() {
+ const eventName = 'Test';
+ mr.Analytics.recordEvent(eventName);
+ expect(chrome.metricsPrivate.recordUserAction.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordUserAction)
+ .toHaveBeenCalledWith(eventName);
+ });
+ });
+ describe('Test recordEnum', function() {
+ const testHistogram = 'Test';
+ const testValues = {TEST1: 0, TEST2: 1, TEST3: 2};
+ const testConfig = {
+ 'metricName': testHistogram,
+ 'type': 'histogram-linear',
+ 'min': 1,
+ 'max': 3,
+ 'buckets': 4
+ };
+ it('Should record an event with corrct index of 0', function() {
+ mr.Analytics.recordEnum(testHistogram, testValues.TEST1, testValues);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 0);
+ });
+ it('Should record an event with corrct index of 1', function() {
+ mr.Analytics.recordEnum(testHistogram, testValues.TEST2, testValues);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 1);
+ });
+ it('Should record an event with correct index of 2', function() {
+ mr.Analytics.recordEnum(testHistogram, testValues.TEST3, testValues);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordValue)
+ .toHaveBeenCalledWith(testConfig, 2);
+ });
+ it('Should not record an event with an unknown value', function() {
+ mr.Analytics.recordEnum(testHistogram, 3, testValues);
+ expect(chrome.metricsPrivate.recordValue.calls.count()).toBe(0);
+ });
+ });
+ describe('Test recordSmallCount', () => {
+ it('Record 0 count succeeds', () => {
+ mr.Analytics.recordSmallCount('smallCount', 0);
+ expect(chrome.metricsPrivate.recordSmallCount.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordSmallCount)
+ .toHaveBeenCalledWith('smallCount', 0);
+ });
+ it('Record negative count fails', () => {
+ mr.Analytics.recordSmallCount('smallCount', -1);
+ expect(chrome.metricsPrivate.recordSmallCount.calls.count()).toBe(0);
+ });
+ it('Record large count succeeds', () => {
+ mr.Analytics.recordSmallCount('smallCount', 200);
+ expect(chrome.metricsPrivate.recordSmallCount.calls.count()).toBe(1);
+ expect(chrome.metricsPrivate.recordSmallCount)
+ .toHaveBeenCalledWith('smallCount', 200);
+ });
+ it('Record regular count succeeds', () => {
+ mr.Analytics.recordSmallCount('smallCount', 1);
+ mr.Analytics.recordSmallCount('smallCount', 50);
+ mr.Analytics.recordSmallCount('smallCount', 100);
+ expect(chrome.metricsPrivate.recordSmallCount.calls.count()).toBe(3);
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/assertions.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/assertions.js
new file mode 100644
index 00000000000..93abbb7d8db
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/assertions.js
@@ -0,0 +1,95 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Various assert-like functions.
+ */
+goog.module('mr.Assertions');
+goog.module.declareLegacyNamespace();
+
+const Config = goog.require('mr.Config');
+
+
+/**
+ * Given an unknown value, return it if it is an Error, or return a new Error
+ * otherwise. Note that unlike other methods in the module, it does not throw
+ * exceptions.
+ *
+ * @param {*} err The purported error
+ * @param {string=} opt_message The message used to construct an Error if |err|
+ * is not an error.
+ * @return {!Error}
+ */
+exports.toError = function(err, opt_message) {
+ if (err instanceof Error) {
+ return err;
+ } else {
+ return Error(opt_message || `Expected an Error value, got ${err}`);
+ }
+};
+
+
+/**
+ * Represents an assertion failure.
+ */
+const AssertionError = class extends Error {
+ /**
+ * @param {string=} message Error message.
+ */
+ constructor(message = '') {
+ super();
+ this.name = 'AssertionError';
+ this.message = message;
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, AssertionError);
+ } else {
+ this.stack = new Error().stack;
+ }
+ }
+};
+
+
+/**
+ * Checks if the condition evaluates to true if mr.Config.isDebugChannel is
+ * true.
+ * @template T
+ * @param {T} condition The condition to check.
+ * @param {string=} message Error message if condition evaluates to false.
+ * @throws {AssertionError} When the condition evaluates to false.
+ * @return {T} The condition.
+ */
+exports.assert = function(condition, message = undefined) {
+ if (Config.isDebugChannel && !condition) {
+ throw new AssertionError(message);
+ }
+ return condition;
+};
+
+
+/**
+ * Checks that a value is a string if mr.Config.isDebugChannel is true.
+ * @param {*} value The value to check
+ * @param {string=} message The message
+ * @return {string} The value
+ * @throws {AssertionError} if the value is not a string
+ */
+exports.assertString = function(value, message = undefined) {
+ if (Config.isDebugChannel && typeof value !== 'string') {
+ throw new AssertionError();
+ }
+ return /** @type {string} */ (value);
+};
+
+
+/**
+ * Returns a Promise that rejects with 'Not implemented' as the error
+ * message.
+ * @template T
+ * @return {!Promise<T>}
+ */
+exports.rejectNotImplemented = function() {
+ return Promise.reject(new Error('Not implemented'));
+};
+
+exports.AssertionError = AssertionError;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/base64.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/base64.js
new file mode 100644
index 00000000000..5d8b3d1ed08
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/base64.js
@@ -0,0 +1,46 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.base64');
+
+/**
+ * @const {!Map<string, string>}
+ */
+const URL_SAFE_MAP = new Map().set('+', '-').set('/', '_').set('=', '.');
+
+
+/**
+ * @const {!Map<string, string>}
+ */
+const INVERSE_URL_SAFE_MAP =
+ new Map().set('-', '+').set('_', '/').set('.', '=');
+
+
+/**
+ * Decodes a base64 string using either the normal or URL-safe alphabet.
+ * @param {string} encoded
+ * @return {string}
+ */
+function decodeString(encoded) {
+ return atob(encoded.replace(/[-_.]/g, c => INVERSE_URL_SAFE_MAP.get(c)));
+}
+
+
+/**
+ * Encodes an array of byte values in base64.
+ * @param {!Array<number>} data An array of byte values.
+ * @param {boolean} urlSafe If true, uses a URL-safe base64 alphabet.
+ * @return {string} The encoded data.
+ */
+function encodeArray(data, urlSafe) {
+ const encoded = btoa(String.fromCharCode(...data));
+ return urlSafe ? encoded.replace(/[+/=]/g, c => URL_SAFE_MAP.get(c)) :
+ encoded;
+}
+
+
+exports = {
+ decodeString,
+ encodeArray,
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/base64_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/base64_test.js
new file mode 100644
index 00000000000..07240a4f82d
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/base64_test.js
@@ -0,0 +1,72 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.base64.test');
+goog.setTestOnly();
+
+const {encodeArray, decodeString} = goog.require('mr.base64');
+
+/**
+ * Converts a ASCII string to an array of bytes.
+ * @param {string} s
+ * @return {!Array<number>}
+ */
+function strBytes(s) {
+ return s.split('').map(c => c.codePointAt(0));
+}
+
+describe('mr.base64.encodeArray', () => {
+ it('encodes well-known values correctly', () => {
+ expect(encodeArray(strBytes(''))).toBe('');
+ expect(encodeArray(strBytes('f'))).toBe('Zg==');
+ expect(encodeArray(strBytes('fo'))).toBe('Zm8=');
+ expect(encodeArray(strBytes('foo'))).toBe('Zm9v');
+ expect(encodeArray(strBytes('foob'))).toBe('Zm9vYg==');
+ expect(encodeArray(strBytes('fooba'))).toBe('Zm9vYmE=');
+ expect(encodeArray(strBytes('foobar'))).toBe('Zm9vYmFy');
+ expect(
+ encodeArray(strBytes(
+ '\xe4\xb8\x80\xe4\xba\x8c\xe4\xb8\x89\xe5\x9b\x9b\xe4\xba\x94\xe5' +
+ '\x85\xad\xe4\xb8\x83\xe5\x85\xab\xe4\xb9\x9d\xe5\x8d\x81')))
+ .toBe('5LiA5LqM5LiJ5Zub5LqU5YWt5LiD5YWr5Lmd5Y2B');
+ expect(encodeArray(strBytes('>>>???>>>???=/+')))
+ .toBe('Pj4+Pz8/Pj4+Pz8/PS8r');
+ });
+
+ it('handles the urlSafe parameter correctly', () => {
+ expect(encodeArray(strBytes('f'), true)).toBe('Zg..');
+ expect(encodeArray(strBytes('fo'), true)).toBe('Zm8.');
+ expect(encodeArray(strBytes('foo'), true)).toBe('Zm9v');
+ expect(encodeArray(strBytes('foob'), true)).toBe('Zm9vYg..');
+ expect(encodeArray(strBytes('fooba'), true)).toBe('Zm9vYmE.');
+ expect(encodeArray(strBytes('foobar'), true)).toBe('Zm9vYmFy');
+ expect(encodeArray(strBytes('>>>???>>>???=/+'), true))
+ .toBe('Pj4-Pz8_Pj4-Pz8_PS8r');
+ });
+
+ it('decodes correctly with the standard alphabet', () => {
+ expect(decodeString('')).toBe('');
+ expect(decodeString('Zg==')).toBe('f');
+ expect(decodeString('Zm8=')).toBe('fo');
+ expect(decodeString('Zm9v')).toBe('foo');
+ expect(decodeString('Zm9vYg==')).toBe('foob');
+ expect(decodeString('Zm9vYmE=')).toBe('fooba');
+ expect(decodeString('Zm9vYmFy')).toBe('foobar');
+ expect(decodeString('5LiA5LqM5LiJ5Zub5LqU5YWt5LiD5YWr5Lmd5Y2B'))
+ .toBe(
+ '\xe4\xb8\x80\xe4\xba\x8c\xe4\xb8\x89\xe5\x9b\x9b\xe4\xba\x94\xe5' +
+ '\x85\xad\xe4\xb8\x83\xe5\x85\xab\xe4\xb9\x9d\xe5\x8d\x81');
+ expect(decodeString('Pj4+Pz8/Pj4+Pz8/PS8r')).toBe('>>>???>>>???=/+');
+ });
+
+ it('decodes correctly with the URL-safe alphabet', () => {
+ expect(decodeString('Zg..')).toBe('f');
+ expect(decodeString('Zm8.')).toBe('fo');
+ expect(decodeString('Zm9v')).toBe('foo');
+ expect(decodeString('Zm9vYg..')).toBe('foob');
+ expect(decodeString('Zm9vYmE.')).toBe('fooba');
+ expect(decodeString('Zm9vYmFy')).toBe('foobar');
+ expect(decodeString('Pj4-Pz8_Pj4-Pz8_PS8r')).toBe('>>>???>>>???=/+');
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/device_counts.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/device_counts.js
new file mode 100644
index 00000000000..d8f7392dc69
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/device_counts.js
@@ -0,0 +1,18 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.DeviceCounts');
+goog.module.declareLegacyNamespace();
+
+
+/**
+ * A struct to hold a snapshot of device counts for a sink discovery service.
+ * @typedef {{
+ * availableDeviceCount: number,
+ * knownDeviceCount: number
+ * }}
+ */
+let DeviceCounts;
+
+exports = DeviceCounts;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/device_counts_provider.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/device_counts_provider.js
new file mode 100644
index 00000000000..a76ecc07702
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/device_counts_provider.js
@@ -0,0 +1,23 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.DeviceCountsProvider');
+goog.module.declareLegacyNamespace();
+
+const DeviceCounts = goog.require('mr.DeviceCounts');
+
+/**
+ * Implemented by services that are capable of providing counts of devices that
+ * they manage.
+ * @record
+ */
+const DeviceCountsProvider = class {
+ /**
+ * Returns the device counts currently known to the service.
+ * @return {!DeviceCounts}
+ */
+ getDeviceCounts() {}
+};
+
+exports = DeviceCountsProvider;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/event_analytics.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/event_analytics.js
new file mode 100644
index 00000000000..f9fbde5e388
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/event_analytics.js
@@ -0,0 +1,57 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/** @fileoverview Analytics for events. */
+
+goog.provide('mr.EventAnalytics');
+goog.provide('mr.EventAnalytics.Event');
+
+goog.require('mr.Analytics');
+
+/**
+ * Possible event types that can wake the event page. Keep names in sync with
+ * extensions/browser/extension_event_histogram_value.h in Chromium and values
+ * in sync with MediaRouterWakeEventType in
+ * google3/analysis/uma/configs/chrome/histograms.xml.
+ *
+ * @enum {number}
+ */
+mr.EventAnalytics.Event = {
+ // Special value meaning the event page was woken by the Media Router and not
+ // a regular extension event.
+ MEDIA_ROUTER: 0,
+ CAST_CHANNEL_ON_ERROR: 1,
+ CAST_CHANNEL_ON_MESSAGE: 2,
+ DIAL_ON_DEVICE_LIST: 3,
+ DIAL_ON_ERROR: 4,
+ GCM_ON_MESSAGE: 5,
+ IDENTITY_ON_SIGN_IN_CHANGED: 6,
+ MDNS_ON_SERVICE_LIST: 7,
+ NETWORKING_PRIVATE_ON_NETWORKS_CHANGED: 8,
+ NETWORKING_PRIVATE_ON_NETWORK_LIST_CHANGED: 9,
+ PROCESSES_ON_UPDATED: 10,
+ RUNTIME_ON_MESSAGE: 11,
+ RUNTIME_ON_MESSAGE_EXTERNAL: 12,
+ SETTINGS_PRIVATE_ON_PREFS_CHANGED: 13,
+ TABS_ON_UPDATED: 14,
+};
+
+/**
+ * @private {mr.EventAnalytics.Event} The event that woke the event page.
+ */
+mr.EventAnalytics.firstEvent_;
+
+/**
+ * Records an event handler invocation in the event page. If it is the first
+ * event that woke the page, a histogram is recorded. Subsequent events are a
+ * no-op.
+ *
+ * @param {!mr.EventAnalytics.Event} eventType The event type.
+ */
+mr.EventAnalytics.recordEvent = function(eventType) {
+ if (mr.EventAnalytics.firstEvent_ != undefined) return;
+ mr.Analytics.recordEnum(
+ 'MediaRouter.Provider.WakeEvent', eventType, mr.EventAnalytics.Event);
+ mr.EventAnalytics.firstEvent_ = eventType;
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/event_analytics_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/event_analytics_test.js
new file mode 100644
index 00000000000..5be671d2ccb
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/event_analytics_test.js
@@ -0,0 +1,27 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly();
+goog.require('mr.Analytics');
+goog.require('mr.EventAnalytics');
+
+describe('Tests EventAnalytics', () => {
+
+ beforeEach(() => {
+ mr.EventAnalytics.firstEvent_ = undefined;
+ });
+
+ describe('Test recordEvent', () => {
+ it('should record only the first event', () => {
+ spyOn(mr.Analytics, 'recordEnum');
+ mr.EventAnalytics.recordEvent(mr.EventAnalytics.Event.DIAL_ON_ERROR);
+ mr.EventAnalytics.recordEvent(mr.EventAnalytics.Event.TABS_ON_UPDATED);
+ expect(mr.Analytics.recordEnum.calls.count()).toEqual(1);
+ expect(mr.Analytics.recordEnum)
+ .toHaveBeenCalledWith(
+ 'MediaRouter.Provider.WakeEvent',
+ mr.EventAnalytics.Event.DIAL_ON_ERROR, mr.EventAnalytics.Event);
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/event_target.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/event_target.js
new file mode 100644
index 00000000000..8fbae73ddf1
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/event_target.js
@@ -0,0 +1,69 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.EventTarget');
+goog.module.declareLegacyNamespace();
+
+
+/** @final */
+class EventTarget {
+ constructor() {
+ /** @private @const {!Array<!Subscription>} */
+ this.subscriptions_ = [];
+ }
+
+ /**
+ * @param {string} type The event type id.
+ * @param {function(this:T, ?)} handler Callback
+ * @param {T=} target
+ * @template T
+ */
+ listen(type, handler, target = undefined) {
+ this.subscriptions_.push({type, handler, target});
+ }
+
+ /**
+ * @param {string} type The event type id.
+ * @param {function(this:T, ?)} handler Callback
+ * @param {T=} target
+ * @template T
+ */
+ unlisten(type, handler, target = undefined) {
+ const index = this.subscriptions_.findIndex(
+ sub =>
+ sub.type == type && sub.handler == handler && sub.target == target);
+ if (index != -1) {
+ this.subscriptions_.splice(index, 1);
+ }
+ }
+
+ /**
+ * @param {{type: string}} event
+ */
+ dispatchEvent(event) {
+ this.subscriptions_.forEach(sub => {
+ if (sub.type == event.type) {
+ // Call handler asynchronously so exceptions don't show up at the source
+ // of the event.
+ Promise.resolve().then(() => sub.handler.call(sub.target, event));
+ }
+ });
+ }
+}
+
+
+/** @record */
+const Subscription = class {};
+
+/** @type {string} */
+Subscription.prototype.type;
+
+/** @type {function(?)} */
+Subscription.prototype.handler;
+
+/** @type {Object|undefined} */
+Subscription.prototype.target;
+
+
+exports = EventTarget;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/fixed_size_queue.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/fixed_size_queue.js
new file mode 100644
index 00000000000..7f8cb7c23a1
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/fixed_size_queue.js
@@ -0,0 +1,126 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview FIFO with a hard limit on its size.
+
+ */
+
+goog.module('mr.FixedSizeQueue');
+goog.module.declareLegacyNamespace();
+
+
+/**
+ * A fixed-sized buffer with FIFO semantics.
+ * @template T
+ */
+class FixedSizeQueue {
+ /**
+ * @param {number} maxSize The size of the buffer.
+ */
+ constructor(maxSize) {
+ if (maxSize <= 0) {
+ throw Error('invalid buffer size');
+ }
+
+ /**
+ * Items are popped from here. Elements are stored in reverse
+ * insertion order.
+ * @private @type {!Array<T>}
+ */
+ this.head_ = [];
+
+ /**
+ * Items are pushed here. Elements are stored in insertion order.
+ * @private @type {!Array<T>}
+ */
+ this.tail_ = [];
+
+ /**
+ * @private @const
+ */
+ this.maxSize_ = maxSize;
+ }
+
+ /**
+ * Adds an item to the buffer. Drops the last added item if the
+ * buffer is full.
+ * @param {T} item
+ */
+ enqueue(item) {
+ if (this.getCount() >= this.maxSize_) {
+ this.dequeue();
+ }
+ this.tail_.push(item);
+ }
+
+ /**
+ * Removes the oldest item from the buffer, which must be non-empty.
+ * @return {T} The removed item.
+ */
+ dequeue() {
+ if (this.isEmpty()) {
+ throw Error('Empty queue');
+ }
+ if (this.head_.length == 0) {
+ this.head_ = this.tail_;
+ this.head_.reverse();
+ this.tail_ = [];
+ }
+ return this.head_.pop();
+ }
+
+ /**
+ * Removes and returns all items in the buffer in insertion order.
+ * @return {!Array<T>}
+ */
+ dequeueAll() {
+ const result = this.getValues();
+ this.clear();
+ return result;
+ }
+
+ /**
+ * @return {number} The number of items in the buffer.
+ */
+ getCount() {
+ return this.head_.length + this.tail_.length;
+ }
+
+ /**
+ * @return {boolean} True if the buffer is full.
+ */
+ isFull() {
+ return this.getCount() == this.maxSize_;
+ }
+
+ /**
+ * @return {boolean} True if the buffer is empty.
+ */
+ isEmpty() {
+ return this.getCount() == 0;
+ }
+
+ /**
+ * Gets all the items in the buffer in insertion order.
+ * @return {!Array<T>}
+ */
+ getValues() {
+ const result = this.head_.slice(); // clones array
+ result.reverse();
+ result.push(...this.tail_);
+ return result;
+ }
+
+ /**
+ * Makes the buffer empty.
+ */
+ clear() {
+ this.head_ = [];
+ this.tail_ = [];
+ }
+}
+
+
+exports = FixedSizeQueue;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/fixed_size_queue_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/fixed_size_queue_test.js
new file mode 100644
index 00000000000..09f0bff6ae9
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/fixed_size_queue_test.js
@@ -0,0 +1,74 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.require('mr.FixedSizeQueue');
+
+describe('mr.FixedSizeQueue', function() {
+ let queue;
+
+ beforeEach(function() {
+ queue = new mr.FixedSizeQueue(3);
+ });
+
+ it('works', function() {
+ expect(queue.getCount()).toBe(0);
+ expect(queue.isFull()).toBe(false);
+ expect(queue.isEmpty()).toBe(true);
+ expect(queue.getValues()).toEqual([]);
+
+ queue.enqueue(1);
+ expect(queue.getCount()).toBe(1);
+ expect(queue.isFull()).toBe(false);
+ expect(queue.isEmpty()).toBe(false);
+ expect(queue.getValues()).toEqual([1]);
+
+ queue.enqueue(2);
+ queue.enqueue(3);
+ expect(queue.getCount()).toBe(3);
+ expect(queue.isFull()).toBe(true);
+ expect(queue.isEmpty()).toBe(false);
+ expect(queue.getValues()).toEqual([1, 2, 3]);
+
+ queue.enqueue(4);
+ expect(queue.getCount()).toBe(3);
+ expect(queue.isFull()).toBe(true);
+ expect(queue.isEmpty()).toBe(false);
+ expect(queue.getValues()).toEqual([2, 3, 4]);
+
+ queue.clear();
+ expect(queue.getCount()).toBe(0);
+ expect(queue.isFull()).toBe(false);
+ expect(queue.isEmpty()).toBe(true);
+ expect(queue.getValues()).toEqual([]);
+ });
+
+ describe('when full', function() {
+ beforeEach(function() {
+ queue.enqueue(1);
+ queue.enqueue(2);
+ queue.enqueue(3);
+ expect(queue.getCount()).toBe(3);
+ expect(queue.isFull()).toBe(true);
+ });
+
+ it('supports dequeue', function() {
+ expect(queue.dequeue()).toBe(1);
+ expect(queue.getCount()).toBe(2);
+ expect(queue.getValues()).toEqual([2, 3]);
+ expect(queue.dequeue()).toBe(2);
+ expect(queue.getCount()).toBe(1);
+ expect(queue.getValues()).toEqual([3]);
+ expect(queue.dequeue()).toBe(3);
+ expect(queue.getCount()).toBe(0);
+ expect(queue.getValues()).toEqual([]);
+ expect(queue.isFull()).toBe(false);
+ expect(queue.isEmpty()).toBe(true);
+ });
+
+ it('supports deqeueAll', function() {
+ expect(queue.dequeueAll()).toEqual([1, 2, 3]);
+ expect(queue.isEmpty()).toBe(true);
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/logger.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/logger.js
new file mode 100644
index 00000000000..49b214a4b90
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/logger.js
@@ -0,0 +1,261 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.provide('mr.Logger');
+
+goog.require('mr.Assertions');
+goog.require('mr.Config');
+
+
+/**
+ * An object for recording logs.
+ */
+mr.Logger = class {
+ /**
+ * @param {string} name
+ */
+ constructor(name) {
+ /**
+ * @private @const {string}
+ */
+ this.name_ = name;
+ }
+
+ /**
+ * @param {string} name
+ * @return {!mr.Logger}
+ */
+ static getInstance(name) {
+ let instance = mr.Logger.instances_.get(name);
+ if (!instance) {
+ instance = new mr.Logger(name);
+ mr.Logger.instances_.set(name, instance);
+ }
+ return instance;
+ }
+
+ /**
+ * @param {function(!mr.Logger.Record)} handler
+ */
+ static addHandler(handler) {
+ mr.Logger.handlers_.push(handler);
+ }
+
+ /**
+ * Logs a pre-built record if its level is high enough.
+ * @param {!mr.Logger.Record} record
+ */
+ static logRecord(record) {
+ if (record.level >= mr.Logger.level) {
+ mr.Logger.handlers_.forEach(handler => handler(record));
+ }
+ }
+
+ /**
+ * Logs a message at the specified log level with an optional exception.
+ *
+ * @param {mr.Logger.Level} level
+ * @param {mr.Logger.Loggable} message
+ * @param {*=} exception An exception to associate with the log message. This
+ * should normally be an Error instance for best results, but any type is
+ * acceptable.
+ */
+ log(level, message, exception = undefined) {
+ if (level < mr.Logger.level) {
+ return;
+ }
+
+ // Logging will occur at the current logging level. If the message is a
+ // lazy-evaluated one, eval now.
+ if (typeof message == 'function') {
+ message = message();
+ }
+
+ // For non-debug builds, make an effort to programmatically scrub message
+ // text that potentially contains personally-identifying information.
+ // However, note that this only covers some of the more-obvious forms of
+ // PII, and no heuristic can ever hope to provide 100% safety. Also, some of
+ // the regular expressions may sometimes match more or less than what was
+ // intended.
+ mr.Assertions.assert(
+ typeof message == 'string', 'Expected message to be a string.');
+ if (!mr.Config.isDebugChannel) {
+ message = message.replace(mr.Logger.URL_REGEXP_, '[Redacted URL]');
+ message = message.replace(
+ mr.Logger.DOMAIN_OR_EMAIL_REGEXP_, '[Redacted domain/email]');
+ message = message.replace(mr.Logger.SINK_ID_REGEXP_, (match, p1, p2) => {
+ return p1 + ':<' + p2.substr(-4) + '>';
+ });
+ }
+
+ const record = {
+ logger: this.name_,
+ level: level,
+ time: Date.now(),
+ message: message,
+ exception: exception,
+ };
+ mr.Logger.handlers_.forEach(handler => handler(record));
+ }
+
+ /**
+ * @param {mr.Logger.Loggable} message
+ * @param {*=} exception
+ */
+ error(message, exception = undefined) {
+ this.log(mr.Logger.Level.SEVERE, message, exception);
+ }
+
+ /**
+ * @param {mr.Logger.Loggable} message
+ * @param {*=} exception
+ */
+ warning(message, exception = undefined) {
+ this.log(mr.Logger.Level.WARNING, message, exception);
+ }
+
+ /**
+ * @param {mr.Logger.Loggable} message
+ * @param {*=} exception
+ */
+ info(message, exception = undefined) {
+ this.log(mr.Logger.Level.INFO, message, exception);
+ }
+
+ /**
+ * @param {mr.Logger.Loggable} message
+ * @param {*=} exception
+ */
+ fine(message, exception = undefined) {
+ this.log(mr.Logger.Level.FINE, message, exception);
+ }
+
+ /**
+ * @param {mr.Logger.Level} level
+ * @return {string}
+ */
+ static levelToString(level) {
+ return mr.Logger.LEVEL_NAMES_[level];
+ }
+
+ /**
+ * @param {string} levelName
+ * @param {mr.Logger.Level} defaultLevel
+ * @return {mr.Logger.Level}
+ */
+ static stringToLevel(levelName, defaultLevel) {
+ const index = mr.Logger.LEVEL_NAMES_.indexOf(levelName);
+ return index == -1 ? defaultLevel : /** @type {mr.Logger.Level} */ (index);
+ }
+
+ /**
+ * Converts a numeric log level (as used in the Closure library) into a log
+ * level constant.
+ * @param {number} levelValue
+ * @return {mr.Logger.Level}
+ */
+ static numberToLevel(levelValue) {
+ if (levelValue <= 600) {
+ return mr.Logger.Level.FINE;
+ } else if (levelValue <= 850) {
+ return mr.Logger.Level.INFO;
+ } else if (levelValue <= 950) {
+ return mr.Logger.Level.WARNING;
+ } else {
+ return mr.Logger.Level.SEVERE;
+ }
+ }
+};
+
+
+/**
+ * @private @const {!Array<function(mr.Logger.Record)>}
+ */
+mr.Logger.handlers_ = [];
+
+
+/**
+ * @private @const {!Map<string, !mr.Logger>}
+ */
+mr.Logger.instances_ = new Map();
+
+
+/**
+ * The available log levels.
+ * @enum {number}
+ */
+mr.Logger.Level = {
+ FINE: 0,
+ INFO: 1,
+ WARNING: 2,
+ SEVERE: 3,
+};
+
+
+/**
+ * The canonical names of log levels in ascending order of severity.
+ * @private const {!Array<string>}
+ */
+mr.Logger.LEVEL_NAMES_ = ['FINE', 'INFO', 'WARNING', 'SEVERE'];
+
+
+/**
+ * A regular expression that matches a very broad-range of text that looks like
+ * it could be a domain name or an e-mail address.
+ * @private const {!RegExp}
+ */
+mr.Logger.DOMAIN_OR_EMAIL_REGEXP_ =
+ /(([\w.+-]+@)|((www|m|mail|ftp)[.]))[\w.-]+[.][\w-]{2,4}/gi;
+
+
+/**
+ * A regular expression that matches a very broad-range of text that looks like
+ * it could be an URL.
+ * @private const {!RegExp}
+ */
+mr.Logger.URL_REGEXP_ = /(data:|https?:\/\/)\S+/gi;
+
+
+/**
+ * A regular expression that matches a very broad-range of text that looks like
+ * it could be a sink ID.
+ * @private const {!RegExp}
+ */
+mr.Logger.SINK_ID_REGEXP_ = /(dial|cast):<([a-zA-Z0-9]+)>/gi;
+
+
+/**
+ * An abstract represenation of a log message.
+ *
+ * The `time` field should be in the format returned by `Date.now()`. The
+ * `exception` field will typically be an Error instance, but code that handles
+ * log records must be prepared to handle any type.
+ *
+ * @typedef {{
+ * level: mr.Logger.Level,
+ * logger: string,
+ * time: number,
+ * message: string,
+ * exception: *,
+ * }}
+ */
+mr.Logger.Record;
+
+
+/**
+ * @typedef {string|function():string}
+ */
+mr.Logger.Loggable;
+
+
+/**
+ * @const
+ */
+mr.Logger.DEFAULT_LEVEL = mr.Logger.Level.INFO;
+
+
+/**
+ * @type {mr.Logger.Level}
+ */
+mr.Logger.level = mr.Logger.DEFAULT_LEVEL;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/logger_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/logger_test.js
new file mode 100644
index 00000000000..bb5048642ad
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/logger_test.js
@@ -0,0 +1,166 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.LoggerTest');
+goog.setTestOnly('mr.LoggerTest');
+
+const Config = goog.require('mr.Config');
+const Logger = goog.require('mr.Logger');
+
+describe('Test mr.Logger', function() {
+ let originalLevel;
+ let logger;
+
+ beforeEach(() => {
+ originalLevel = Logger.level;
+ logger = new Logger('test');
+ });
+
+ afterEach(() => {
+ Logger.level = originalLevel;
+ Logger.handlers_ = [];
+ });
+
+ it('logs string messages only at INFO and above', () => {
+ Logger.level = Logger.Level.WARNING;
+ const loggedMessages = [];
+ Logger.addHandler(record => {
+ expect(record.level).not.toBeLessThan(Logger.Level.WARNING);
+ expect(record.logger).toEqual('test');
+ expect(typeof record.time).toBe('number');
+ expect(typeof record.message).toBe('string');
+ loggedMessages.push(record.message);
+ });
+
+ logger.fine('Should not log this message.');
+ logger.info('Should not log this message either.');
+ logger.warning('Should log this warning message.');
+ logger.error('Should log this error message.');
+
+ expect(loggedMessages).toEqual([
+ 'Should log this warning message.', 'Should log this error message.'
+ ]);
+ });
+
+ it('logs lazy-evaluated messages', () => {
+ Logger.level = Logger.Level.FINE;
+ const loggedMessages = [];
+ Logger.addHandler(record => {
+ expect(record.level).not.toBeLessThan(Logger.Level.FINE);
+ expect(record.logger).toEqual('test');
+ expect(typeof record.time).toBe('number');
+ expect(typeof record.message).toBe('string');
+ loggedMessages.push(record.message);
+ });
+
+ logger.fine(() => 'Should log this fine message.');
+ logger.info(() => 'Should log this info message.');
+ logger.warning(() => 'Should log this warning message.');
+ logger.error(() => 'Should log this error message.');
+
+ expect(loggedMessages).toEqual([
+ 'Should log this fine message.', 'Should log this info message.',
+ 'Should log this warning message.', 'Should log this error message.'
+ ]);
+ });
+
+ describe('Personally-identifying info scrubbbing tests', () => {
+ let loggedMessages;
+ let isDebugChannelDefault = Config.isDebugChannel;
+
+ beforeEach(() => {
+ Config.isDebugChannel = false;
+ loggedMessages = [];
+ Logger.level = Logger.Level.FINE;
+ Logger.addHandler(record => {
+ expect(typeof record.message).toBe('string');
+ loggedMessages.push(record.message);
+ });
+ });
+
+ afterEach(() => {
+ Config.isDebugChannel = isDebugChannelDefault;
+ });
+
+ it('does not scrub non-PII from messages', () => {
+ // Things that shouldn't be scrubbed.
+ logger.info('');
+ logger.info('42');
+ logger.info(
+ 'Found sink with id: ac6982d68e687faf6ebf8cc (Chromecast Ultra)');
+ logger.info('The event occurred at 20:21:22 on 29 Mar 2017.');
+
+ expect(loggedMessages).toEqual([
+ '', '42',
+ 'Found sink with id: ac6982d68e687faf6ebf8cc (Chromecast Ultra)',
+ 'The event occurred at 20:21:22 on 29 Mar 2017.'
+ ]);
+ });
+
+ it('scrubs domains', () => {
+ // Things that look like domain names.
+ logger.info('Visiting www.google.com...');
+ logger.info('Tab favicon domain is: ftp.myfilez.net');
+ // The following example shows the RegExp currently used does not match
+ // against all possible domains perfectly.
+ logger.info(
+ 'mail.personaldata.security.biz mapped to ' +
+ 'personaldata.security.biz.');
+
+ expect(loggedMessages).toEqual([
+ 'Visiting [Redacted domain/email]...',
+ 'Tab favicon domain is: [Redacted domain/email]',
+ '[Redacted domain/email] mapped to personaldata.security.biz.'
+ ]);
+ });
+
+ it('scrubs email addresses', () => {
+ // Things that look like e-mail addresses.
+ logger.info('Reply to nobody@love-spam.net, and see what happens.');
+ logger.info(
+ 'This CL was written by somebody@developers.chromium.org, ' +
+ 'or was it somebody@hooli.com?');
+
+ expect(loggedMessages).toEqual([
+ 'Reply to [Redacted domain/email], and see what happens.',
+ 'This CL was written by [Redacted domain/email], or was it ' +
+ '[Redacted domain/email]?'
+ ]);
+ });
+
+ it('scrubs URLs', () => {
+ // Things that look like URLs.
+ logger.info(
+ 'Downloading from http://www.pictures.com/gifs/' +
+ 'kittens%20falling%20of%20furniture.png...');
+ logger.info('Page navigation detected: https://youtube.com/profile');
+ logger.info(
+ 'Relpacing content with: ' +
+ 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D');
+
+ expect(loggedMessages).toEqual([
+ 'Downloading from [Redacted URL]',
+ 'Page navigation detected: [Redacted URL]',
+ 'Relpacing content with: [Redacted URL]'
+ ]);
+ });
+
+ it('scrubs sink IDs', () => {
+ logger.info(
+ 'Sink has pending connection' +
+ ' dial:<05f5e10100641000bc6f90f1aaa0bd90>');
+ logger.info(
+ 'Adding new session: cast:<de51d94921f15f8af6dbf65592bb3610>, ' +
+ '5d85e5da-b773-4382-ba06-43c2a6dc6ba6');
+ logger.info('Connecting to (id 1) rf72niQ3FPe8VpTz_tIEGNSkfGUo.');
+ logger.info('');
+
+ expect(loggedMessages).toEqual([
+ 'Sink has pending connection dial:<bd90>',
+ 'Adding new session: cast:<3610>, 5d85e5da-b773-4382-ba06-43c2a6dc6ba6',
+ 'Connecting to (id 1) rf72niQ3FPe8VpTz_tIEGNSkfGUo.', ''
+ ]);
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/media_source_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/media_source_utils.js
new file mode 100644
index 00000000000..e0392b46cf4
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/media_source_utils.js
@@ -0,0 +1,127 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview The media source URN related utilities methods.
+ */
+
+goog.provide('mr.MediaSourceUtils');
+
+goog.require('mr.Config');
+
+
+
+
+/**
+ * @param {string} sourceUrn
+ * @return {boolean} True if it is a mirror URN.
+ */
+mr.MediaSourceUtils.isMirrorSource = function(sourceUrn) {
+ return mr.MediaSourceUtils.isTabMirrorSource(sourceUrn) ||
+ mr.MediaSourceUtils.isDesktopMirrorSource(sourceUrn);
+};
+
+
+/**
+ * @param {string} sourceUrn
+ * @return {boolean} True if it is a two UA mode presentation source.
+ * A presentation source has a sourceUrn that is a valid uri and
+ * is not a Cast custom receiver app.
+ */
+mr.MediaSourceUtils.isPresentationSource = function(sourceUrn) {
+
+ if (!sourceUrn.startsWith('http:') && !sourceUrn.startsWith('https:')) {
+ return false;
+ }
+ // Use the DOM to parse sourceUrn.
+ const link = document.createElement('a');
+ link.href = sourceUrn;
+ // Protocol must be http or https.
+ if (link.protocol != 'http:' && link.protocol != 'https:') {
+ return false;
+ }
+
+ // Must not be a custom Cast receiver app.
+ return link.hash.indexOf(mr.MediaSourceUtils.CAST_APP_ID_) == -1;
+};
+
+
+
+/** @const {string} */
+mr.MediaSourceUtils.CAST_STREAMING_APP_ID = '0F5096E8';
+
+
+/** @const {string} */
+mr.MediaSourceUtils.TAB_MIRROR_URN_PREFIX =
+ 'urn:x-org.chromium.media:source:tab:';
+
+/** @const {string} */
+mr.MediaSourceUtils.TAB_REMOTING_URN_PREFIX =
+ 'urn:x-org.chromium.media:source:tab_content_remoting:';
+
+/** @const {string} */
+mr.MediaSourceUtils.DESKTOP_MIRROR_URN =
+ 'urn:x-org.chromium.media:source:desktop';
+
+
+/** @private @const {string} */
+mr.MediaSourceUtils.CAST_APP_ID_ = '__castAppId__';
+
+
+/**
+ * @private @const {!Array<string>}
+ */
+mr.MediaSourceUtils.MIRROR_APP_ID_ORIGIN_WHITELIST_ = [
+ 'https://docs.google.com', // slides
+];
+
+
+/**
+ * @param {string} sourceUrn
+ * @return {?Array<string>} array of origins whitelisted for the sourceUrn or
+ * |null| if any origin is allowed for the sourceUrn.
+ */
+mr.MediaSourceUtils.getWhitelistedOrigins = function(sourceUrn) {
+ if (mr.Config.isDebugChannel &&
+ window.localStorage['debug.allowAllOrigins']) {
+ return null;
+ }
+ return sourceUrn.indexOf(mr.MediaSourceUtils.CAST_STREAMING_APP_ID) != -1 ?
+ mr.MediaSourceUtils.MIRROR_APP_ID_ORIGIN_WHITELIST_ :
+ null;
+};
+
+
+/**
+ * @param {string} sourceUrn
+ * @return {boolean} True if it is a tab mirror URN.
+ */
+mr.MediaSourceUtils.isTabMirrorSource = function(sourceUrn) {
+ return sourceUrn.startsWith(mr.MediaSourceUtils.TAB_MIRROR_URN_PREFIX) ||
+ sourceUrn.indexOf(mr.MediaSourceUtils.CAST_STREAMING_APP_ID) != -1;
+};
+
+
+/**
+ * @param {string} sourceUrn
+ * @return {boolean} True if it is a desktop mirror URN.
+ */
+mr.MediaSourceUtils.isDesktopMirrorSource = function(sourceUrn) {
+ return sourceUrn == mr.MediaSourceUtils.DESKTOP_MIRROR_URN;
+};
+
+
+/**
+ * Get the tab ID from |sourceUrn|. Returns null if the sourceUrn is not a tab
+ * mirror URN or if it doesn't contain a valid tab ID.
+ * @param {string} sourceUrn
+ * @return {?number}
+ */
+mr.MediaSourceUtils.getMirrorTabId = function(sourceUrn) {
+ const pos = sourceUrn.search(mr.MediaSourceUtils.TAB_MIRROR_URN_PREFIX);
+ if (pos == -1) return null;
+ const tabIdStr =
+ sourceUrn.substr(pos + mr.MediaSourceUtils.TAB_MIRROR_URN_PREFIX.length);
+ return parseInt(tabIdStr, 10) || null;
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/media_source_utils_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/media_source_utils_test.js
new file mode 100644
index 00000000000..9647921874f
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/media_source_utils_test.js
@@ -0,0 +1,66 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.require('mr.MediaSourceUtils');
+
+describe('Tests MediaSourceUtils', function() {
+ describe('Tests isTabMirrorSource', function() {
+ it('should return true for tab mirror source', function() {
+ expect(mr.MediaSourceUtils.isTabMirrorSource(
+ 'urn:x-org.chromium.media:source:tab:2'))
+ .toBe(true);
+ expect(mr.MediaSourceUtils.isTabMirrorSource(
+ 'urn:x-org.chromium.media:source:tab:666'))
+ .toBe(true);
+ });
+
+ it('should return false for non tab mirror source', function() {
+ expect(mr.MediaSourceUtils.isTabMirrorSource(
+ 'urn:x-org.chromium.media:source:desktop'))
+ .toBe(false);
+ });
+ });
+
+ describe('Tests isPresentationSource', function() {
+ it('should return true for presentation source', function() {
+ expect(mr.MediaSourceUtils.isPresentationSource('http://www.google.com'))
+ .toBe(true);
+ expect(mr.MediaSourceUtils.isPresentationSource('https://www.google.com'))
+ .toBe(true);
+ });
+
+ it('should return false for non tab mirror source', function() {
+ expect(
+ mr.MediaSourceUtils.isPresentationSource('Invalid media source urn'))
+ .toBe(false);
+ });
+
+ it('should return false for cast receiver app', function() {
+ expect(mr.MediaSourceUtils.isPresentationSource(
+ 'http://www.google.com/cast#__castAppId__=deadbeef'))
+ .toBe(false);
+ });
+ });
+
+ describe('Tests getMirrorTabId', function() {
+ it('should return null for non tab mirror source or invalid ID',
+ function() {
+ expect(mr.MediaSourceUtils.getMirrorTabId('http://www.google.com'))
+ .toBeNull();
+ expect(mr.MediaSourceUtils.getMirrorTabId(
+ 'urn:x-org.chromium.media:source:tab:'))
+ .toBeNull();
+ });
+
+ it('should return correct ID for correct tab mirror source', function() {
+ expect(mr.MediaSourceUtils.getMirrorTabId(
+ 'urn:x-org.chromium.media:source:tab:2'))
+ .toBe(2);
+ expect(mr.MediaSourceUtils.getMirrorTabId(
+ 'urn:x-org.chromium.media:source:tab:666'))
+ .toBe(666);
+ });
+ });
+
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/mock_clock.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/mock_clock.js
new file mode 100644
index 00000000000..dbb165cfb14
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/mock_clock.js
@@ -0,0 +1,391 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly('mr.MockClock');
+goog.provide('mr.MockClock');
+
+goog.require('mr.MockPromise');
+
+
+/**
+ * Class for unit testing code that uses setTimeout, clearTimeout, etc.
+ * @final
+ */
+mr.MockClock = class {
+ /**
+ * Installs the MockClock by overriding the global object's
+ * implementation of setTimeout, setInterval, clearTimeout and
+ * clearInterval.
+ */
+ constructor() {
+ if (window.setTimeout !== mr.MockClock.REAL_SETTIMEOUT_) {
+ throw Error('MockClock already installed.');
+ }
+
+ /**
+ * List of times to fire, sorted in reverse order of when they
+ * will be executed.
+ *
+ * @type {!Array<mr.MockClock.Timeout_>}
+ * @private
+ */
+ this.queue_ = [];
+
+ /**
+ * The current simulated time in milliseconds.
+ * @type {number}
+ * @private
+ */
+ this.nowMillis_ = 0;
+
+ mr.MockClock.installedHere_ = Error('MockClock was installed here.');
+
+ window.setTimeout = this.setTimeout_.bind(this);
+ window.setInterval = this.setInterval_.bind(this);
+ window.setImmediate = this.setImmediate_.bind(this);
+ window.clearTimeout = this.clearTimeout_.bind(this);
+ window.clearInterval = this.clearTimeout_.bind(this);
+ Date.now = this.getCurrentTime_.bind(this);
+ }
+
+ /**
+ * Removes the MockClock's hooks into the global object's functions
+ * and revert to their original values.
+ */
+ uninstall() {
+ if (window.setTimeout === mr.MockClock.REAL_SETTIMEOUT_) {
+ throw Error('MockClock not installed.');
+ }
+
+ mr.MockClock.installedHere_ = null;
+
+ window.setTimeout = mr.MockClock.REAL_SETTIMEOUT_;
+ window.setInterval = mr.MockClock.REAL_SETINTERVAL_;
+ window.setImmediate = mr.MockClock.REAL_SETIMMEDIATE_;
+ window.clearTimeout = mr.MockClock.REAL_CLEARTIMEOUT_;
+ window.clearInterval = mr.MockClock.REAL_CLEARINTERVAL_;
+ Date.now = mr.MockClock.REAL_DATENOW_;
+ }
+
+ /**
+ * Restores this clock to the state it was in just after it was
+ * created.
+ */
+ reset() {
+ this.queue_ = [];
+ this.nowMillis_ = 0;
+ }
+
+ /**
+ * Increments the MockClock's time by a given number of
+ * milliseconds, running any functions that are now overdue.
+ * @param {number=} millis Number of milliseconds to increment the
+ * counter. If not specified, clock ticks 1 millisecond.
+ * @return {number} Current mock time in milliseconds.
+ */
+ tick(millis = 1) {
+ const endTime = this.nowMillis_ + millis;
+ this.runFunctionsWithinRange_(endTime);
+ this.nowMillis_ = endTime;
+ return endTime;
+ }
+
+ /**
+ * Ticks the clock until there are no more actions scheduled to run.
+ */
+ flush() {
+ this.tick(Infinity);
+ }
+
+ /**
+ * Takes a promise and then ticks the mock clock. If the promise
+ * successfully resolves, returns the value produced by the
+ * promise. If the promise is rejected, it throws the rejection as
+ * an exception. If the promise is not resolved at all, throws an
+ * exception. Also ticks the general clock by the specified amount.
+ *
+ * @param {!mr.MockPromise<T>} promise A promise that should be
+ * resolved after the mockClock is ticked for the given
+ * opt_millis.
+ * @param {number=} millis Number of milliseconds to increment the
+ * counter. If not specified, clock ticks 1 millisecond.
+ * @return {T}
+ * @template T
+ */
+ tickPromise(promise, millis = 1) {
+ let value;
+ let error;
+ let resolved = false;
+ promise.then(
+ v => {
+ value = v;
+ resolved = true;
+ },
+ e => {
+ error = e;
+ resolved = true;
+ });
+ this.tick(millis);
+ if (!resolved) {
+ throw new Error(
+ 'Promise was expected to be resolved ' +
+ 'after mock clock tick.');
+ }
+ if (error) {
+ throw error;
+ }
+ return value;
+ }
+
+ /**
+ * Takes a promise and then ticks the mock clock. If the promise
+ * rejects, returns the error produced by the promise. If the
+ * promise is rejected, it throws the rejection as an exception. If
+ * the promise is not rejected at all, throws an exception. Also
+ * ticks the general clock by the specified amount.
+ *
+ * @param {!mr.MockPromise<T>} promise A promise that should be
+ * rejected after the mockClock is ticked for the given
+ * opt_millis.
+ * @param {number=} millis Number of milliseconds to increment the
+ * counter. If not specified, clock ticks 1 millisecond.
+ * @return {*} Error produced by the promise.
+ * @template T
+ */
+ tickRejectingPromise(promise, millis = 1) {
+ let error;
+ let rejected = false;
+ promise.catch(e => {
+ error = e;
+ rejected = true;
+ });
+ this.tick(millis);
+ if (!rejected) {
+ throw new Error(
+ 'Promise was expected to be rejected after mock clock tick.');
+ }
+ return error;
+ }
+
+ /**
+ * @return {number} The MockClock's current time in milliseconds.
+ * @private
+ */
+ getCurrentTime_() {
+ return this.nowMillis_;
+ }
+
+ /**
+ * Runs any function that is scheduled before a certain time.
+ * @param {number} endTime The latest time in the range, in
+ * milliseconds.
+ * @private
+ */
+ runFunctionsWithinRange_(endTime) {
+ mr.MockPromise.callPendingHandlers();
+
+ // Repeatedly pop off the last item since the queue is always
+ // sorted.
+ while (this.queue_ && this.queue_.length &&
+ this.queue_[this.queue_.length - 1].runAtMillis <= endTime) {
+ const timeout = this.queue_.pop();
+ // Only move time forwards.
+ this.nowMillis_ = Math.max(this.nowMillis_, timeout.runAtMillis);
+ if (timeout.recurring) {
+ // Reschedule before calling the function so that if the
+ // function deletes the timeout, it's in the queue to be
+ // removed.
+ this.scheduleFunction_(
+ timeout.timeoutKey, timeout.funcToCall, timeout.millis, true);
+ }
+ timeout.funcToCall.call(undefined);
+ mr.MockPromise.callPendingHandlers();
+ }
+ }
+
+ /**
+ * Schedules a function to be run at a certain time.
+ * @param {number} timeoutKey The timeout key.
+ * @param {!Function} funcToCall The function to call.
+ * @param {number} millis The number of milliseconds to call it in.
+ * @param {boolean} recurring Whether to function call should recur.
+ * @private
+ */
+ scheduleFunction_(timeoutKey, funcToCall, millis, recurring) {
+ const timeout = {
+ runAtMillis: this.nowMillis_ + millis,
+ funcToCall: funcToCall,
+ recurring: recurring,
+ timeoutKey: timeoutKey,
+ millis: millis,
+ };
+
+ // Insert a timer descriptor into a descending-order queue.
+ //
+ // Later-inserted duplicates appear at lower indices. For
+ // example, the asterisk in (5,4,*,3,2,1) would be the insertion
+ // point for 3. (The numbers here refer to timestamps.)
+ //
+ // Insertion of N items is quadratic, but unit tests are normally
+ // small, so scalability is not a primary issue.
+ //
+ // Since the queue is in reverse order (so we can pop rather than
+ // unshift), and later timers with the same time stamp should be
+ // executed later, we look for the element strictly greater than
+ // the one we are inserting.
+ let i;
+ for (i = this.queue_.length; i != 0; i--) {
+ if (this.queue_[i - 1].runAtMillis > timeout.runAtMillis) {
+ break;
+ }
+ this.queue_[i] = this.queue_[i - 1];
+ }
+ this.queue_[i] = timeout;
+ }
+
+ /**
+ * Schedules a function to be called after `millis`
+ * milliseconds. Mock implementation for setTimeout.
+ * @param {!Function} funcToCall The function to call.
+ * @param {number=} millis The number of milliseconds to call it
+ * after.
+ * @param {...*} args Arguments to pass to the function.
+ * @return {number} The number of timeouts created.
+ * @private
+ */
+ setTimeout_(funcToCall, millis = 0, ...args) {
+ if (millis > mr.MockClock.MAX_INT_) {
+ throw Error(`Bad timeout value: ${millis}`);
+ }
+ this.scheduleFunction_(
+ mr.MockClock.nextId_, funcToCall.bind(undefined, ...args), millis,
+ false);
+ return mr.MockClock.nextId_++;
+ }
+
+ /**
+ * Schedules a function to be called every `millis` milliseconds.
+ * Mock implementation for setInterval.
+ * @param {!Function} funcToCall The function to call.
+ * @param {number=} millis The number of milliseconds between calls.
+ * @param {...*} args Arguments to pass to the function.
+ * @return {number} The number of timeouts created.
+ * @private
+ */
+ setInterval_(funcToCall, millis = 0, ...args) {
+ this.scheduleFunction_(
+ mr.MockClock.nextId_, funcToCall.bind(undefined, ...args), millis,
+ true);
+ return mr.MockClock.nextId_++;
+ }
+
+ /**
+ * Schedules a function to be called immediately after the current JS
+ * execution.
+ * Mock implementation for setImmediate.
+ * @param {!Function} funcToCall The function to call.
+ * @param {...*} args Arguments to pass to the function.
+ * @return {number} The number of timeouts created.
+ * @private
+ */
+ setImmediate_(funcToCall, ...args) {
+ return this.setTimeout_(funcToCall, 0, ...args);
+ }
+
+ /**
+ * Clears a timeout.
+ * Mock implementation for clearTimeout and clearInterval.
+ * @param {number} timeoutKey The timeout key to clear.
+ * @private
+ */
+ clearTimeout_(timeoutKey) {
+ const newQueue =
+ this.queue_.filter(timeout => timeout.timeoutKey != timeoutKey);
+ if (newQueue.length == this.queue_.length_) {
+ // The real versions of clearTimeout and clearInterval silently
+ // ignore invalid keys, but we hold ourselves to a higher
+ // standard :-)
+ throw Error('Invalid timeoutKey');
+ }
+ this.queue_ = newQueue;
+ }
+};
+
+
+/**
+ * ID to use for next timeout. Timeout IDs must never be reused, even
+ * across MockClock instances.
+ * @private {number}
+ */
+mr.MockClock.nextId_ = 0;
+
+
+/**
+ * @private @const
+ */
+mr.MockClock.REAL_SETTIMEOUT_ = window.setTimeout;
+
+
+/**
+ * @private @const
+ */
+mr.MockClock.REAL_SETINTERVAL_ = window.setInterval;
+
+
+/**
+ * @private @const
+ */
+mr.MockClock.REAL_SETIMMEDIATE_ = window.setImmediate;
+
+
+/**
+ * @private @const
+ */
+mr.MockClock.REAL_CLEARTIMEOUT_ = window.clearTimeout;
+
+
+/**
+ * @private @const
+ */
+mr.MockClock.REAL_CLEARINTERVAL_ = window.clearInterval;
+
+
+/**
+ * @private @const
+ */
+mr.MockClock.REAL_DATENOW_ = Date.now;
+
+
+/**
+ * Maximum 32-bit signed integer.
+ *
+ * Timeouts over this time return immediately in many browsers, due to
+ * integer overflow. Such known browsers include Firefox, Chrome, and
+ * Safari, but not IE.
+ *
+ * @type {number}
+ * @private
+ */
+mr.MockClock.MAX_INT_ = 2147483647;
+
+
+/**
+ * @typedef {{
+ * runAtMillis: number,
+ * funcToCall: !Function,
+ * recurring: boolean,
+ * timeoutKey: number,
+ * millis: number,
+ * }}
+ * @private
+ */
+mr.MockClock.Timeout_;
+
+
+/**
+ * Exception used to record where the current MockClock was created. Helpful
+ * for diagnosing unit tests that fail to uninstall their mock clocks.
+ * @private {Error}
+ */
+mr.MockClock.installedHere_ = null;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/mock_promise.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/mock_promise.js
new file mode 100644
index 00000000000..511ddbdec0a
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/mock_promise.js
@@ -0,0 +1,602 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.provide('mr.MockPromise');
+goog.setTestOnly('mr.MockPromise');
+
+
+/**
+ * Why does this class exist?
+ *
+ * Most of our unit tests that involve promises are written in "synchronous
+ * style". In this style, everything happens in one iteration of the JS event
+ * loop. With native promises, this style isn't possible, because a call like
+ * p.then(f) won't run f until at least the next event loop iteration, even if p
+ * is already resolved.
+ *
+ * When the tests were originally written, they relied on a particular
+ * interaction between goog.testing.MockClock and goog.Promise, where calling
+ * mockClock.tick() would force any code scheduled by goog.Promise to be
+ * executed immediately. Since then, the non-test code has been changed to use
+ * native promises, and the test code has been changed to use this class and
+ * mr.MockClock, but the same principle applies.
+ *
+ * The long-term plan is to convert the tests to use asynchronous style, and get
+ * rid of this class and mr.MockClock entirely.
+ *
+ * @template TYPE
+ * @final
+ */
+mr.MockPromise = class {
+ /**
+ * @param {function(
+ * function((TYPE|mr.MockPromise<TYPE>)=),
+ * function(*=)): void} resolver
+ */
+ constructor(resolver) {
+ /**
+ * The internal state of this Promise. Either PENDING, FULFILLED, REJECTED,
+ * or BLOCKED.
+ * @private {mr.MockPromise.State_}
+ */
+ this.state_ = mr.MockPromise.State_.PENDING;
+
+ /**
+ * The settled result of the Promise. Immutable once set with either a
+ * fulfillment value or rejection reason.
+ * @private {*}
+ */
+ this.result_ = undefined;
+
+ /**
+ * The linked list of `onFulfilled` and `onRejected` callbacks
+ * added to this Promise by calls to {@code then()}.
+ * @private {!Array<!mr.MockPromise.CallbackEntry_>}
+ */
+ this.callbackEntries_ = [];
+
+ /**
+ * Whether the Promise is in the queue of Promises to execute.
+ * @private {boolean}
+ */
+ this.executing_ = false;
+
+ /**
+ * A boolean that is set if the Promise is rejected, and reset to false if
+ * an `onRejected` callback is invoked for the Promise (or one of its
+ * descendants). If the rejection is not handled before the next timestep,
+ * the rejection reason is passed to the unhandled rejection handler.
+ * @private {boolean}
+ */
+ this.hadUnhandledRejection_ = false;
+
+ try {
+ resolver.call(
+ null,
+ value => {
+ this.resolve_(mr.MockPromise.State_.FULFILLED, value);
+ },
+ reason => {
+ try {
+ // Promise was rejected. Step up one call frame to see why.
+ if (reason instanceof Error) {
+ throw reason;
+ } else {
+ throw new Error('Promise rejected.');
+ }
+ } catch (e) {
+ // Only thrown so browser dev tools can catch rejections of
+ // promises when the option to break on caught exceptions is
+ // activated.
+ }
+ this.resolve_(mr.MockPromise.State_.REJECTED, reason);
+ });
+ } catch (e) {
+ this.resolve_(mr.MockPromise.State_.REJECTED, e);
+ }
+ }
+
+ /**
+ * Replaces the native Promise class with this class.
+ */
+ static install() {
+ if (window.Promise !== mr.MockPromise.origPromise_) {
+ throw Error('Error installing mr.MockPromise');
+ }
+ if (mr.MockPromise.pendingHandlers_.length) {
+ throw Error('Expected no pending handlers.');
+ }
+ window.Promise = mr.MockPromise;
+ mr.MockPromise.ignoreUnhandledRejections = false;
+ }
+
+ /**
+ * Undoes the effect of calling install().
+ */
+ static uninstall() {
+ if (window.Promise !== mr.MockPromise) {
+ throw Error('mr.MockPromise not installed');
+ }
+ if (mr.MockPromise.pendingHandlers_.length) {
+ console.warn('Discarding pending handlers.');
+ mr.MockPromise.pendingHandlers_.length = 0;
+ // throw Error('Expected no pending handlers.');
+ }
+ window.Promise = mr.MockPromise.origPromise_;
+ }
+
+ /**
+ * @return {void}
+ */
+ static callPendingHandlers() {
+ while (mr.MockPromise.pendingHandlers_.length) {
+ const handler = mr.MockPromise.pendingHandlers_.shift();
+ handler();
+ }
+ }
+
+ /**
+ * @param {Function} onFulfilled
+ * @param {Function} onRejected
+ * @return {!mr.MockPromise.CallbackEntry_}
+ * @private
+ */
+ static getCallbackEntry_(onFulfilled, onRejected) {
+ const entry = new mr.MockPromise.CallbackEntry_();
+ entry.onFulfilled = onFulfilled;
+ entry.onRejected = onRejected;
+ return entry;
+ }
+
+ /**
+ * @param {*=} opt_value
+ * @return {!mr.MockPromise} A new Promise that is immediately resolved
+ * with the given value. If the input value is already a mr.MockPromise,
+ * it will be returned immediately without creating a new instance.
+ */
+ static resolve(opt_value) {
+ if (opt_value instanceof mr.MockPromise) {
+ // Avoid creating a new object if we already have a promise object
+ // of the correct type.
+ return opt_value;
+ }
+
+ return new mr.MockPromise((resolve, reject) => {
+ resolve(opt_value);
+ });
+ }
+
+ /**
+ * @param {*=} opt_reason
+ * @return {!mr.MockPromise} A new Promise that is immediately rejected with the
+ * given reason.
+ */
+ static reject(opt_reason) {
+ return new mr.MockPromise((resolve, reject) => {
+ reject(opt_reason);
+ });
+ }
+
+ /**
+ * @param {!Array} promises
+ * @return {!mr.MockPromise} A Promise that receives the result of the first
+ * Promise input to settle immediately after it settles.
+ */
+ static race(promises) {
+ return new mr.MockPromise((resolve, reject) => {
+ if (!promises.length) {
+ resolve(undefined);
+ }
+ for (let i = 0, promise; i < promises.length; i++) {
+ promise = promises[i];
+ mr.MockPromise.resolve(promise).then(resolve, reject);
+ }
+ });
+ }
+
+ /**
+ * @param {!Array} promises
+ * @return {!mr.MockPromise<!Array>} A Promise that receives a list of
+ * every fulfilled value once every input Promise is successfully
+ * fulfilled, or is rejected with the first rejection reason immediately
+ * after it is rejected.
+ */
+ static all(promises) {
+ return new mr.MockPromise((resolve, reject) => {
+ let toFulfill = promises.length;
+ const values = [];
+
+ if (!toFulfill) {
+ resolve(values);
+ return;
+ }
+
+ const onFulfill = (index, value) => {
+ toFulfill--;
+ values[index] = value;
+ if (toFulfill == 0) {
+ resolve(values);
+ }
+ };
+
+ const onReject = reason => {
+ reject(reason);
+ };
+
+ for (let i = 0, promise; i < promises.length; i++) {
+ promise = promises[i];
+ mr.MockPromise.resolve(promise).then(onFulfill.bind(null, i), onReject);
+ }
+ });
+ }
+
+ /**
+ * Adds callbacks that will operate on the result of the Promise, returning a
+ * new child Promise.
+ *
+ * If the Promise is fulfilled, the `onFulfilled` callback will be
+ * invoked with the fulfillment value as argument, and the child Promise will
+ * be fulfilled with the return value of the callback. If the callback throws
+ * an exception, the child Promise will be rejected with the thrown value
+ * instead.
+ *
+ * If the Promise is rejected, the `onRejected` callback will be invoked
+ * with the rejection reason as argument, and the child Promise will be
+ * resolved with the return value or rejected with the thrown value of the
+ * callback.
+ *
+ * @param {?(function(TYPE):?)=} onFulfilled A
+ * function that will be invoked with the fulfillment value if the Promise
+ * is fulfilled.
+ * @param {?(function(*): *)=} onRejected A function that will
+ * be invoked with the rejection reason if the Promise is rejected.
+ * @return {!mr.MockPromise} A new Promise that will receive the result of the
+ * callback.
+ * @override
+ */
+ then(onFulfilled = null, onRejected = null) {
+ if (onFulfilled != null && typeof onFulfilled != 'function') {
+ throw Error('onFulfilled should be a function.');
+ }
+ if (onRejected != null && typeof onRejected != 'function') {
+ throw Error('onRejected should be a function.');
+ }
+
+ return this.addChildPromise_(onFulfilled, onRejected);
+ }
+
+ /**
+ * Adds a callback that will be invoked only if the Promise is rejected. This
+ * is equivalent to {@code then(null, onRejected)}.
+ *
+ * @param {function(*): *} onRejected A function that will be
+ * invoked with the rejection reason if the Promise is rejected.
+ * @return {!mr.MockPromise} A new Promise that will receive the result of the
+ * callback.
+ */
+ catch(onRejected) {
+ return this.addChildPromise_(null, onRejected);
+ }
+
+ /**
+ * Adds a callback entry to the current Promise, and schedules callback
+ * execution if the Promise has already been settled.
+ *
+ * @param {mr.MockPromise.CallbackEntry_} callbackEntry Record containing
+ * {
+ * @private
+ */
+ addCallbackEntry_(callbackEntry) {
+ if (!this.hasEntry_() &&
+ (this.state_ == mr.MockPromise.State_.FULFILLED ||
+ this.state_ == mr.MockPromise.State_.REJECTED)) {
+ this.scheduleCallbacks_();
+ }
+ this.queueEntry_(callbackEntry);
+ }
+
+ /**
+ * Creates a child Promise and adds it to the callback entry list. The result
+ * of the child Promise is determined by the result of the `onFulfilled`
+ * or `onRejected` callbacks as specified in the Promise resolution
+ * procedure.
+ *
+ * @param {?function(TYPE):
+ * (RESULT|mr.MockPromise<RESULT>)} onFulfilled A callback that
+ * will be invoked if the Promise is fulfilled, or null.
+ * @param {?function(*): *} onRejected A callback that will be
+ * invoked if the Promise is rejected, or null.
+ * @return {!mr.MockPromise} The child Promise.
+ * @template RESULT
+ * @private
+ */
+ addChildPromise_(onFulfilled, onRejected) {
+ /** @type {mr.MockPromise.CallbackEntry_} */
+ const callbackEntry = mr.MockPromise.getCallbackEntry_(null, null, null);
+
+ callbackEntry.child = new mr.MockPromise((resolve, reject) => {
+ // Invoke onFulfilled, or resolve with the parent's value if absent.
+ callbackEntry.onFulfilled = onFulfilled ? value => {
+ try {
+ const result = onFulfilled.call(null, value);
+ resolve(result);
+ } catch (err) {
+ reject(err);
+ }
+ } : resolve;
+
+ // Invoke onRejected, or reject with the parent's reason if absent.
+ callbackEntry.onRejected = onRejected ? reason => {
+ try {
+ resolve(onRejected.call(null, reason));
+ } catch (err) {
+ reject(err);
+ }
+ } : reject;
+ });
+
+ this.addCallbackEntry_(callbackEntry);
+ return callbackEntry.child;
+ }
+
+ /**
+ * Unblocks the Promise and fulfills it with the given value.
+ *
+ * @param {TYPE} value
+ * @private
+ */
+ unblockAndFulfill_(value) {
+ if (this.state_ != mr.MockPromise.State_.BLOCKED) {
+ throw Error('Expected state to be BLOCKED.');
+ }
+ this.state_ = mr.MockPromise.State_.PENDING;
+ this.resolve_(mr.MockPromise.State_.FULFILLED, value);
+ }
+
+ /**
+ * Unblocks the Promise and rejects it with the given rejection reason.
+ *
+ * @param {*} reason
+ * @private
+ */
+ unblockAndReject_(reason) {
+ if (this.state_ != mr.MockPromise.State_.BLOCKED) {
+ throw Error('Expected state to be BLOCKED.');
+ }
+ this.state_ = mr.MockPromise.State_.PENDING;
+ this.resolve_(mr.MockPromise.State_.REJECTED, reason);
+ }
+
+ /**
+ * Attempts to resolve a Promise with a given resolution state and value. This
+ * is a no-op if the given Promise has already been resolved.
+ *
+ * If the given result is a Promise, the Promise will be settled with the same
+ * state and result as the Thenable once it is itself settled.
+ *
+ * If the given result is not a Promise, the Promise will be settled
+ * (fulfilled or rejected) with that result based on the given state.
+ *
+ * @param {mr.MockPromise.State_} state
+ * @param {*} x The result to apply to the Promise.
+ * @private
+ */
+ resolve_(state, x) {
+ if (this.state_ != mr.MockPromise.State_.PENDING) {
+ return;
+ }
+
+ if (this === x) {
+ state = mr.MockPromise.State_.REJECTED;
+ x = new TypeError('Promise cannot resolve to itself');
+ }
+
+ this.state_ = mr.MockPromise.State_.BLOCKED;
+
+ if (x instanceof mr.MockPromise) {
+ x.addCallbackEntry_(mr.MockPromise.getCallbackEntry_(
+ this.unblockAndFulfill_.bind(this),
+ this.unblockAndReject_.bind(this)));
+ return;
+ }
+
+ this.result_ = x;
+ this.state_ = state;
+ this.scheduleCallbacks_();
+
+ if (state == mr.MockPromise.State_.REJECTED) {
+ mr.MockPromise.addUnhandledRejection_(this, x);
+ }
+ }
+
+ /**
+ * Executes the pending callbacks of a settled Promise after a timeout.
+ * @private
+ */
+ scheduleCallbacks_() {
+ if (!this.executing_) {
+ this.executing_ = true;
+ mr.MockPromise.pendingHandlers_.push(this.executeCallbacks_.bind(this));
+ }
+ }
+
+ /**
+ * @return {boolean} Whether there are any pending callbacks queued.
+ * @private
+ */
+ hasEntry_() {
+ return this.callbackEntries_.size > 0;
+ }
+
+ /**
+ * @param {mr.MockPromise.CallbackEntry_} entry
+ * @private
+ */
+ queueEntry_(entry) {
+ if (entry.onFulfilled == null) {
+ throw Error('entry.onFulfilled == null');
+ }
+ this.callbackEntries_.push(entry);
+ }
+
+ /**
+ * @return {mr.MockPromise.CallbackEntry_} entry
+ * @private
+ */
+ popEntry_() {
+ const entry = this.callbackEntries_.shift() || null;
+ if (entry && entry.onFulfilled == null) {
+ throw Error('entry.onFulfulled == null');
+ }
+ return entry;
+ }
+
+ /**
+ * Executes all pending callbacks for this Promise.
+ *
+ * @private
+ */
+ executeCallbacks_() {
+ let entry = null;
+ while (entry = this.popEntry_()) {
+ this.executeCallback_(entry, this.state_, this.result_);
+ }
+ this.executing_ = false;
+ }
+
+ /**
+ * Executes a pending callback for this Promise. Invokes an
+ * `onFulfilled` or `onRejected` callback based on the settled
+ * state of the Promise.
+ *
+ * @param {!mr.MockPromise.CallbackEntry_} callbackEntry An entry containing
+ * the onFulfilled and/or onRejected callbacks for this step.
+ * @param {mr.MockPromise.State_} state The resolution status of the Promise,
+ * either FULFILLED or REJECTED.
+ * @param {*} result The settled result of the Promise.
+ * @private
+ */
+ executeCallback_(callbackEntry, state, result) {
+ // Cancel an unhandled rejection if the then call had an onRejected.
+ if (state == mr.MockPromise.State_.REJECTED && callbackEntry.onRejected) {
+ this.hadUnhandledRejection_ = false;
+ }
+
+ if (callbackEntry.child) {
+ mr.MockPromise.invokeCallback_(callbackEntry, state, result);
+ } else {
+ try {
+ mr.MockPromise.invokeCallback_(callbackEntry, state, result);
+ } catch (err) {
+ mr.MockPromise.handleRejection_(err);
+ }
+ }
+ }
+
+ /**
+ * Executes the onFulfilled or onRejected callback for a callbackEntry.
+ *
+ * @param {!mr.MockPromise.CallbackEntry_} callbackEntry
+ * @param {mr.MockPromise.State_} state
+ * @param {*} result
+ * @private
+ */
+ static invokeCallback_(callbackEntry, state, result) {
+ if (state == mr.MockPromise.State_.FULFILLED) {
+ callbackEntry.onFulfilled.call(null, result);
+ } else if (callbackEntry.onRejected) {
+ callbackEntry.onRejected.call(null, result);
+ }
+ }
+
+ /**
+ * Marks this rejected Promise as unhandled. If no `onRejected` callback
+ * is called for this Promise before the `UNHANDLED_REJECTION_DELAY`
+ * expires, the reason will be passed to the unhandled rejection handler. The
+ * handler typically rethrows the rejection reason so that it becomes visible
+ * in
+ * the developer console.
+ *
+ * @param {!mr.MockPromise} promise The rejected Promise.
+ * @param {*} reason The Promise rejection reason.
+ * @private
+ */
+ static addUnhandledRejection_(promise, reason) {
+ promise.hadUnhandledRejection_ = true;
+ mr.MockPromise.pendingHandlers_.push(() => {
+ if (promise.hadUnhandledRejection_) {
+ mr.MockPromise.handleRejection_(reason);
+ }
+ });
+ }
+
+ /**
+ * @param {*} reason
+ * @private
+ */
+ static handleRejection_(reason) {
+ if (!mr.MockPromise.ignoreUnhandledRejections) {
+ throw reason;
+ }
+ }
+};
+
+
+/**
+ * @type {boolean}
+ */
+mr.MockPromise.ignoreUnhandledRejections = false;
+
+
+
+/**
+ * @private @const
+ */
+mr.MockPromise.origPromise_ = Promise;
+
+
+/**
+ * The possible internal states for a Promise. These states are not directly
+ * observable to external callers.
+ * @enum {number}
+ * @private
+ */
+mr.MockPromise.State_ = {
+ /** The Promise is waiting for resolution. */
+ PENDING: 0,
+
+ /** The Promise is blocked waiting for the result of another Thenable. */
+ BLOCKED: 1,
+
+ /** The Promise has been resolved with a fulfillment value. */
+ FULFILLED: 2,
+
+ /** The Promise has been resolved with a rejection reason. */
+ REJECTED: 3
+};
+
+
+/**
+ * @private @const {!Array<function()>}
+ */
+mr.MockPromise.pendingHandlers_ = [];
+
+
+/**
+ * Entries in the callback chain. Each call to `then` or
+ * `catch` creates an entry containing the
+ * functions that may be invoked once the Promise is settled.
+ *
+ * @private @final
+ */
+mr.MockPromise.CallbackEntry_ = class {
+ constructor() {
+ /** @type {?mr.MockPromise} */
+ this.child = null;
+ /** @type {Function} */
+ this.onFulfilled = null;
+ /** @type {Function} */
+ this.onRejected = null;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/mock_promise_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/mock_promise_test.js
new file mode 100644
index 00000000000..dc02a027eb3
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/mock_promise_test.js
@@ -0,0 +1,850 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.provide('mr.MockPromiseTest');
+goog.setTestOnly('mr.MockPromiseTest');
+
+goog.require('mr.MockPromise');
+
+
+describe('mr.MockPromise', () => {
+ afterEach(() => {
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ // Simple shared objects used as test values.
+ const dummy = {
+ toString() {
+ return '[object dummy]';
+ }
+ };
+ const sentinel = {
+ toString() {
+ return '[object sentinel]';
+ }
+ };
+
+ /**
+ * Dummy onfulfilled or onrejected function that should not be called.
+ *
+ * @param {*} result The result passed into the callback.
+ */
+ function shouldNotCall(result) {
+ fail('This should not have been called (result: ' + String(result) + ')');
+ }
+
+ function resolveSoon(value) {
+ let resolveFunc;
+ const promise = new mr.MockPromise((resolve, reject) => {
+ resolveFunc = resolve.bind(null, value);
+ });
+ return [promise, resolveFunc];
+ }
+
+ function rejectSoon(value) {
+ let rejectFunc;
+ const promise = new mr.MockPromise((resolve, reject) => {
+ rejectFunc = reject.bind(null, value);
+ });
+ return [promise, rejectFunc];
+ }
+
+ // A trivial expectation test to suppress Jasmine warnings.
+ function noExpectations() {
+ expect(true).toBe(true);
+ }
+
+ it('testThenIsFulfilled', () => {
+ let timesCalled = 0;
+
+ const p = new mr.MockPromise((resolve, reject) => {
+ resolve(sentinel);
+ });
+ p.then(value => {
+ timesCalled++;
+ expect(value).toBe(sentinel);
+ });
+
+ expect(timesCalled).toBe(0);
+
+ mr.MockPromise.callPendingHandlers();
+ expect(timesCalled).toBe(1);
+ });
+
+ it('testThenIsRejected', () => {
+ let timesCalled = 0;
+
+ const p = mr.MockPromise.reject(sentinel);
+ p.then(shouldNotCall, value => {
+ timesCalled++;
+ expect(value).toBe(sentinel);
+ });
+
+ expect(timesCalled).toBe(0);
+
+ mr.MockPromise.callPendingHandlers();
+ expect(timesCalled).toBe(1);
+ });
+
+ it('testThenAsserts', () => {
+ const p = mr.MockPromise.resolve();
+
+ expect(() => {
+ p.then({});
+ }).toThrowError(/onFulfilled should be a function./);
+
+ expect(() => {
+ p.then(() => {}, {});
+ }).toThrowError(/onRejected should be a function./);
+ });
+
+ it('testOptionalOnFulfilled', done => {
+ mr.MockPromise.resolve(sentinel)
+ .then(null, null)
+ .then(null, shouldNotCall)
+ .then(value => {
+ expect(value).toBe(sentinel);
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testOptionalOnRejected', done => {
+ mr.MockPromise.reject(sentinel)
+ .then(null, null)
+ .then(shouldNotCall)
+ .then(null, reason => {
+ expect(reason).toBe(sentinel);
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testMultipleResolves', () => {
+ let timesCalled = 0;
+ let resolvePromise;
+
+ const p = new mr.MockPromise((resolve, reject) => {
+ resolvePromise = resolve;
+ resolve('foo');
+ resolve('bar');
+ });
+
+ p.then(value => {
+ timesCalled++;
+ expect(timesCalled).toBe(1);
+ });
+
+ mr.MockPromise.callPendingHandlers();
+
+ resolvePromise('baz');
+ expect(timesCalled).toBe(1);
+ });
+
+ it('testMultipleRejects', () => {
+ let timesCalled = 0;
+ let rejectPromise;
+
+ const p = new mr.MockPromise((resolve, reject) => {
+ rejectPromise = reject;
+ reject('foo');
+ reject('bar');
+ });
+
+ p.then(shouldNotCall, value => {
+ timesCalled++;
+ expect(timesCalled).toBe(1);
+ });
+
+ mr.MockPromise.callPendingHandlers();
+
+ rejectPromise('baz');
+ expect(timesCalled).toBe(1);
+ });
+
+ it('testResolveWithPromise', () => {
+ let resolveBlocker;
+ let hasFulfilled = false;
+ const blocker = new mr.MockPromise((resolve, reject) => {
+ resolveBlocker = resolve;
+ });
+
+ const p = mr.MockPromise.resolve(blocker);
+ p.then(value => {
+ hasFulfilled = true;
+ expect(value).toBe(sentinel);
+ }, shouldNotCall);
+
+ expect(hasFulfilled).toBe(false);
+ resolveBlocker(sentinel);
+
+ mr.MockPromise.callPendingHandlers();
+ expect(hasFulfilled).toBe(true);
+ });
+
+ it('testResolveWithRejectedPromise', () => {
+ let rejectBlocker;
+ let hasRejected = false;
+ const blocker = new mr.MockPromise((resolve, reject) => {
+ rejectBlocker = reject;
+ });
+
+ const p = mr.MockPromise.resolve(blocker);
+ const child = p.then(shouldNotCall, reason => {
+ hasRejected = true;
+ expect(reason).toBe(sentinel);
+ });
+
+ expect(hasRejected).toBe(false);
+ rejectBlocker(sentinel);
+
+ mr.MockPromise.callPendingHandlers();
+ expect(hasRejected).toBe(true);
+ });
+
+ it('testRejectWithPromise', () => {
+ let resolveBlocker;
+ let hasFulfilled = false;
+ const blocker = new mr.MockPromise((resolve, reject) => {
+ resolveBlocker = resolve;
+ });
+
+ const p = mr.MockPromise.reject(blocker);
+ const child = p.then(value => {
+ hasFulfilled = true;
+ expect(value).toBe(sentinel);
+ }, shouldNotCall);
+
+ expect(hasFulfilled).toBe(false);
+ resolveBlocker(sentinel);
+
+ mr.MockPromise.callPendingHandlers();
+ expect(hasFulfilled).toBe(true);
+ });
+
+ it('testRejectWithRejectedPromise', () => {
+ let rejectBlocker;
+ let hasRejected = false;
+ const blocker = new mr.MockPromise((resolve, reject) => {
+ rejectBlocker = reject;
+ });
+
+ const p = mr.MockPromise.reject(blocker);
+ const child = p.then(shouldNotCall, reason => {
+ hasRejected = true;
+ expect(reason).toBe(sentinel);
+ });
+
+ expect(hasRejected).toBe(false);
+ rejectBlocker(sentinel);
+
+ mr.MockPromise.callPendingHandlers();
+ expect(hasRejected).toBe(true);
+ });
+
+ it('testResolveAndReject', () => {
+ let onFulfilledCalled = false;
+ let onRejectedCalled = false;
+ const p = new mr.MockPromise((resolve, reject) => {
+ resolve();
+ reject();
+ });
+
+ p.then(
+ () => {
+ onFulfilledCalled = true;
+ },
+ () => {
+ onRejectedCalled = true;
+ });
+
+ mr.MockPromise.callPendingHandlers();
+ expect(onFulfilledCalled).toBe(true);
+ expect(onRejectedCalled).toBe(false);
+ });
+
+ it('testResolveWithSelfRejects', done => {
+ let r;
+ const p = new mr.MockPromise(resolve => {
+ r = resolve;
+ });
+ r(p);
+ p.then(shouldNotCall, e => {
+ expect(e.message).toBe('Promise cannot resolve to itself');
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testResolveWithObjectStringResolves', done => {
+ mr.MockPromise.resolve('[object Object]').then(v => {
+ expect(v).toBe('[object Object]');
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testRejectAndResolve', done => {
+ noExpectations();
+ new mr
+ .MockPromise((resolve, reject) => {
+ reject();
+ resolve();
+ })
+ .then(shouldNotCall, done);
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testThenReturnsBeforeCallbackWithFulfill', done => {
+ let thenHasReturned = false;
+ const p = mr.MockPromise.resolve();
+
+ p.then(() => {
+ expect(thenHasReturned).toBe(true);
+ done();
+ });
+ thenHasReturned = true;
+
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testThenReturnsBeforeCallbackWithReject', done => {
+ let thenHasReturned = false;
+ const p = mr.MockPromise.reject();
+
+ const child = p.then(shouldNotCall, () => {
+ expect(thenHasReturned).toBe(true);
+ done();
+ });
+ thenHasReturned = true;
+
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testResolutionOrder', () => {
+ const callbacks = [];
+ mr.MockPromise.resolve()
+ .then(
+ () => {
+ callbacks.push(1);
+ },
+ shouldNotCall)
+ .then(
+ () => {
+ callbacks.push(2);
+ },
+ shouldNotCall)
+ .then(() => {
+ callbacks.push(3);
+ }, shouldNotCall);
+
+ mr.MockPromise.callPendingHandlers();
+ expect(callbacks).toEqual([1, 2, 3]);
+ });
+
+ it('testResolutionOrderWithThrow', done => {
+ const callbacks = [];
+ const p = mr.MockPromise.resolve();
+
+ p.then(() => {
+ callbacks.push(1);
+ }, shouldNotCall);
+ const child = p.then(() => {
+ callbacks.push(2);
+ throw Error();
+ }, shouldNotCall);
+
+ child.then(shouldNotCall, () => {
+ // The parent callbacks should be evaluated before the child.
+ callbacks.push(4);
+ });
+
+ p.then(() => {
+ callbacks.push(3);
+ }, shouldNotCall);
+
+ child.then(shouldNotCall, () => {
+ callbacks.push(5);
+ expect(callbacks).toEqual([1, 2, 3, 4, 5]);
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testResolutionOrderWithNestedThen', done => {
+ const callbacks = [];
+ const promise = new mr.MockPromise((resolve, reject) => {
+ const p = mr.MockPromise.resolve();
+
+ p.then(() => {
+ callbacks.push(1);
+ p.then(() => {
+ callbacks.push(3);
+ resolve();
+ });
+ });
+ p.then(() => {
+ callbacks.push(2);
+ });
+ });
+
+ promise.then(() => {
+ expect(callbacks).toEqual([1, 2, 3]);
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testRejectionOrder', () => {
+ const callbacks = [];
+ const p = mr.MockPromise.reject();
+
+ p.then(shouldNotCall, () => {
+ callbacks.push(1);
+ });
+ p.then(shouldNotCall, () => {
+ callbacks.push(2);
+ });
+ p.then(shouldNotCall, () => {
+ callbacks.push(3);
+ });
+
+ mr.MockPromise.callPendingHandlers();
+ expect(callbacks).toEqual([1, 2, 3]);
+ });
+
+ it('testRejectionOrderWithThrow', () => {
+ const callbacks = [];
+ const p = mr.MockPromise.reject();
+
+ p.then(shouldNotCall, () => {
+ callbacks.push(1);
+ });
+ p.then(shouldNotCall, () => {
+ callbacks.push(2);
+ throw Error();
+ }).catch(() => {});
+ p.then(shouldNotCall, () => {
+ callbacks.push(3);
+ });
+
+ mr.MockPromise.callPendingHandlers();
+ expect(callbacks).toEqual([1, 2, 3]);
+ });
+
+ it('testRejectionOrderWithNestedThen', done => {
+ const callbacks = [];
+ const promise = new mr.MockPromise((resolve, reject) => {
+
+ const p = mr.MockPromise.reject();
+
+ p.then(shouldNotCall, () => {
+ callbacks.push(1);
+ p.then(shouldNotCall, () => {
+ callbacks.push(3);
+ resolve();
+ });
+ });
+ p.then(shouldNotCall, () => {
+ callbacks.push(2);
+ });
+ });
+
+ promise.then(() => {
+ expect(callbacks).toEqual([1, 2, 3]);
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testBranching', () => {
+ const p = mr.MockPromise.resolve(2);
+ let branchesResolved = 0;
+
+ const branch1 = p.then(value => {
+ expect(value).toBe(2);
+ return value / 2;
+ }).then(value => {
+ expect(value).toBe(1);
+ ++branchesResolved;
+ });
+
+ const branch2 = p.then(value => {
+ expect(value).toBe(2);
+ throw value + 1;
+ }).then(shouldNotCall, reason => {
+ expect(reason).toBe(3);
+ ++branchesResolved;
+ });
+
+ const branch3 = p.then(value => {
+ expect(value).toBe(2);
+ return value * 2;
+ }).then(value => {
+ expect(value).toBe(4);
+ ++branchesResolved;
+ });
+
+ mr.MockPromise.all([branch1, branch2, branch3]);
+ mr.MockPromise.callPendingHandlers();
+ expect(branchesResolved).toBe(3);
+ });
+
+ it('testThenReturnsPromise', () => {
+ const parent = mr.MockPromise.resolve();
+ const child = parent.then();
+
+ expect(child instanceof mr.MockPromise).toBe(true);
+ expect(child).not.toEqual(parent);
+ });
+
+ it('testBlockingPromise', () => {
+ const p = mr.MockPromise.resolve();
+ let wasFulfilled = false;
+ let wasRejected = false;
+
+ const p2 = p.then(() => new mr.MockPromise((resolve, reject) => {}));
+
+ p2.then(
+ () => {
+ wasFulfilled = true;
+ },
+ () => {
+ wasRejected = true;
+ });
+
+ mr.MockPromise.callPendingHandlers();
+ expect(wasFulfilled).toBe(false);
+ expect(wasRejected).toBe(false);
+ });
+
+ it('testBlockingPromiseFulfilled', done => {
+ let resolveBlockingPromise;
+ const blockingPromise = new mr.MockPromise((resolve, reject) => {
+ resolveBlockingPromise = resolve;
+ });
+
+ const p = mr.MockPromise.resolve(dummy);
+ const p2 = p.then(value => blockingPromise);
+
+ p2.then(value => {
+ expect(value).toBe(sentinel);
+ done();
+ });
+ resolveBlockingPromise(sentinel);
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testBlockingPromiseRejected', done => {
+ let rejectBlockingPromise;
+ const blockingPromise = new mr.MockPromise((resolve, reject) => {
+ rejectBlockingPromise = reject;
+ });
+
+ const p = mr.MockPromise.resolve(blockingPromise);
+
+ p.then(shouldNotCall, reason => {
+ expect(reason).toBe(sentinel);
+ done();
+ });
+ rejectBlockingPromise(sentinel);
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testCatch', done => {
+ let catchCalled = false;
+ mr.MockPromise.reject()
+ .catch(reason => {
+ catchCalled = true;
+ return sentinel;
+ })
+ .then(value => {
+ expect(catchCalled).toBe(true);
+ expect(value).toBe(sentinel);
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testRaceWithEmptyList', done => {
+ mr.MockPromise.race([]).then(value => {
+ expect(value).toBe(undefined);
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testRaceWithFulfill', done => {
+ const [a, resolveA] = resolveSoon('a');
+ const [b, resolveB] = resolveSoon('b');
+ const [c, resolveC] = resolveSoon('c');
+ const [d, resolveD] = resolveSoon('d');
+
+ mr.MockPromise.race([a, b, c, d])
+ .then(value => {
+ expect(value).toBe('c');
+ // Return the slowest input promise to wait for it to complete.
+ return a;
+ })
+ .then(value => {
+ expect(value).toBe('a');
+ done();
+ });
+ resolveC();
+ resolveD();
+ resolveB();
+ resolveA();
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testRaceWithNonThenable', done => {
+ const [a, resolveA] = resolveSoon('a');
+ const b = 'b';
+ const [c, resolveC] = resolveSoon('c');
+ const [d, resolveD] = resolveSoon('d');
+
+ mr.MockPromise.race([a, b, c, d])
+ .then(value => {
+ expect(value).toBe('b');
+ // Return the slowest input promise to wait for it to complete.
+ return a;
+ })
+ .then(value => {
+ expect(value).toBe('a');
+ done();
+ });
+ resolveC();
+ resolveD();
+ resolveA();
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testRaceWithFalseyNonThenable', done => {
+ const [a, resolveA] = resolveSoon('a');
+ const b = 0;
+ const [c, resolveC] = resolveSoon('c');
+ const [d, resolveD] = resolveSoon('d');
+
+ mr.MockPromise.race([a, b, c, d])
+ .then(value => {
+ expect(value).toBe(0);
+ // Return the slowest input promise to wait for it to complete.
+ return a;
+ })
+ .then(value => {
+ expect(value).toBe('a');
+ done();
+ });
+ resolveC();
+ resolveD();
+ resolveA();
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testRaceWithFulfilledBeforeNonThenable', done => {
+ const [a, resolveA] = resolveSoon('a');
+ const b = mr.MockPromise.resolve('b');
+ const c = 'c';
+ const [d, resolveD] = resolveSoon('d');
+
+ mr.MockPromise.race([a, b, c, d])
+ .then(value => {
+ expect(value).toBe('b');
+ // Return the slowest input promise to wait for it to complete.
+ return a;
+ })
+ .then(value => {
+ expect(value).toBe('a');
+ done();
+ });
+ resolveD();
+ resolveA();
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testRaceWithReject', done => {
+ const [a, rejectA] = rejectSoon('rejected-a', 40);
+ const [b, rejectB] = rejectSoon('rejected-b', 30);
+ const [c, rejectC] = rejectSoon('rejected-c', 10);
+ const [d, rejectD] = rejectSoon('rejected-d', 20);
+
+ mr.MockPromise.race([a, b, c, d])
+ .then(
+ shouldNotCall,
+ value => {
+ expect(value).toBe('rejected-c');
+ return a;
+ })
+ .then(shouldNotCall, reason => {
+ expect(reason).toBe('rejected-a');
+ done();
+ });
+ rejectC();
+ rejectD();
+ rejectB();
+ rejectA();
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testAllWithEmptyList', done => {
+ mr.MockPromise.all([]).then(value => {
+ expect(value).toEqual([]);
+ done();
+ });
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testAllWithFulfill', done => {
+ const [a, resolveA] = resolveSoon('a');
+ const [b, resolveB] = resolveSoon('b');
+ const [c, resolveC] = resolveSoon('c');
+ const [d, resolveD] = resolveSoon('d');
+ // Test a falsey value.
+ const [z, resolveZ] = resolveSoon(0);
+
+ mr.MockPromise.all([a, b, c, d, z]).then(value => {
+ expect(value).toEqual(['a', 'b', 'c', 'd', 0]);
+ done();
+ });
+ resolveC();
+ resolveD();
+ resolveB();
+ resolveZ();
+ resolveA();
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testAllWithNonThenable', done => {
+ const [a, resolveA] = resolveSoon('a');
+ const b = 'b';
+ const [c, resolveC] = resolveSoon('c');
+ const [d, resolveD] = resolveSoon('d');
+ // Test a falsey value.
+ const z = 0;
+
+ mr.MockPromise.all([a, b, c, d, z]).then(value => {
+ expect(value).toEqual(['a', 'b', 'c', 'd', 0]);
+ done();
+ });
+ resolveC();
+ resolveD();
+ resolveA();
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testAllWithReject', done => {
+ const [a, resolveA] = resolveSoon('a');
+ const [b, rejectB] = rejectSoon('rejected-b');
+ const [c, resolveC] = resolveSoon('c');
+ const [d, resolveD] = resolveSoon('d');
+
+ mr.MockPromise.all([a, b, c, d])
+ .then(
+ shouldNotCall,
+ reason => {
+ expect(reason).toBe('rejected-b');
+ return a;
+ })
+ .then(value => {
+ expect(value).toBe('a');
+ done();
+ });
+ resolveC();
+ resolveD();
+ rejectB();
+ resolveA();
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testMockClock', () => {
+ let resolveA;
+ let resolveB;
+ const calls = [];
+
+ const p = new mr.MockPromise((resolve, reject) => {
+ resolveA = resolve;
+ });
+
+ p.then(value => {
+ expect(value).toBe(sentinel);
+ calls.push('then');
+ });
+
+ const fulfilledChild = p.then(value => {
+ expect(value).toBe(sentinel);
+ return mr.MockPromise.resolve(1);
+ }).then(value => {
+ expect(value).toBe(1);
+ calls.push('fulfilledChild');
+
+ });
+
+ const rejectedChild = p.then(value => {
+ expect(value).toBe(sentinel);
+ return mr.MockPromise.reject(2);
+ }).then(shouldNotCall, reason => {
+ expect(reason).toBe(2);
+ calls.push('rejectedChild');
+ });
+
+ const unresolvedChild = p.then(value => {
+ expect(value).toBe(sentinel);
+ return new mr.MockPromise(r => {
+ resolveB = r;
+ });
+ }).then(value => {
+ expect(value).toBe(3);
+ calls.push('unresolvedChild');
+ });
+
+ resolveA(sentinel);
+ expect(calls).toEqual([]);
+
+ mr.MockPromise.callPendingHandlers();
+ expect(calls).toEqual(['then', 'fulfilledChild', 'rejectedChild']);
+
+ resolveB(3);
+ expect(calls).toEqual(['then', 'fulfilledChild', 'rejectedChild']);
+
+ mr.MockPromise.callPendingHandlers();
+ expect(calls).toEqual(
+ ['then', 'fulfilledChild', 'rejectedChild', 'unresolvedChild']);
+ });
+
+ it('testHandledRejection', () => {
+ noExpectations();
+ mr.MockPromise.reject(sentinel).then(shouldNotCall, reason => {});
+ mr.MockPromise.callPendingHandlers();
+ });
+
+ it('testUnhandledRejection1', () => {
+ mr.MockPromise.reject(sentinel);
+ expect(mr.MockPromise.callPendingHandlers).toThrow();
+ });
+
+ it('testUnhandledRejection2', () => {
+ mr.MockPromise.reject(sentinel).then(shouldNotCall);
+ expect(mr.MockPromise.callPendingHandlers).toThrow();
+ });
+
+ it('testUnhandledThrow', () => {
+ mr.MockPromise.resolve().then(() => {
+ throw sentinel;
+ });
+ expect(mr.MockPromise.callPendingHandlers).toThrow();
+ });
+
+ it('testUnhandledBlockingRejection', () => {
+ const blocker = mr.MockPromise.reject(sentinel);
+ mr.MockPromise.resolve(blocker);
+ expect(mr.MockPromise.callPendingHandlers).toThrow();
+ });
+
+ it('testHandledBlockingRejection', () => {
+ noExpectations();
+ const blocker = mr.MockPromise.reject(sentinel);
+ mr.MockPromise.resolve(blocker).then(shouldNotCall, reason => {});
+ mr.MockPromise.callPendingHandlers();
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/mojo_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/mojo_utils.js
new file mode 100644
index 00000000000..f9d98798f8a
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/mojo_utils.js
@@ -0,0 +1,63 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.MojoUtils');
+goog.module.declareLegacyNamespace();
+
+
+/**
+ * Converts a mojo.Origin object to a string.
+ * @param {!mojo.Origin} origin
+ * @return {string}
+ */
+exports.mojoOriginToString = function(origin) {
+ if (origin.unique) {
+ return '';
+ } else {
+ return `${origin.scheme}:\/\/${origin.host}
+ ${origin.port ? `:${origin.port}` : ''}/`;
+ }
+};
+
+
+/**
+ * Converts an origin string to a mojo.Origin object.
+ * @param {string} origin
+ * @return {!mojo.Origin}
+ */
+exports.stringToMojoOrigin = function(origin) {
+ const url = new URL(origin);
+ const mojoOrigin = {};
+ mojoOrigin.scheme = url.protocol.replace(':', '');
+ mojoOrigin.host = url.hostname;
+ var port = url.port ? Number.parseInt(url.port, 10) : 0;
+ switch (mojoOrigin.scheme) {
+ case 'http':
+ mojoOrigin.port = port || 80;
+ break;
+ case 'https':
+ mojoOrigin.port = port || 443;
+ break;
+ default:
+ throw new Error('Scheme must be http or https');
+ }
+ mojoOrigin.suborigin = '';
+ return new mojo.Origin(mojoOrigin);
+};
+
+/**
+ * @param {?mojo.TimeDelta} timeDelta
+ * @return {number}
+ */
+exports.timeDeltaToSeconds = function(timeDelta) {
+ return timeDelta ? timeDelta.microseconds / 1000000 : 0;
+};
+
+/**
+ * @param {number} seconds
+ * @return {!mojo.TimeDelta}
+ */
+exports.secondsToTimeDelta = function(seconds) {
+ return new mojo.TimeDelta({microseconds: Math.floor(seconds * 1000000)});
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/object_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/object_utils.js
new file mode 100644
index 00000000000..699c9f0be27
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/object_utils.js
@@ -0,0 +1,31 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Utility methods for Objects.
+ */
+
+goog.provide('mr.ObjectUtils');
+
+mr.ObjectUtils = class {
+ /**
+ * Gets the value in an Object with the given list of keys by traversing the
+ * objects with them.
+ * @param {!Object} obj
+ * @param {...string} path
+ * @return {*} The value in the object with the given path, or undefined if
+ * it does not exist due to missing either one of the intermediate keys
+ * or the final key.
+ */
+ static getPath(obj, ...path) {
+ let value = obj;
+ for (const key of path) {
+ if (value == undefined || typeof value != 'object') {
+ return undefined;
+ }
+ value = value[key];
+ }
+ return value;
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/object_utils_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/object_utils_test.js
new file mode 100644
index 00000000000..253c6142f4e
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/object_utils_test.js
@@ -0,0 +1,33 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.setTestOnly('object_utils_test');
+
+goog.require('mr.ObjectUtils');
+
+describe('mr.ObjectUtils test', () => {
+ const thudObj = {thud: 2};
+ const obj = {foo: {bar: {xyzzy: null, baz: {qux: 1, corge: thudObj}}}};
+
+ it('getPath returns undefined for nonexistent path', () => {
+ expect(mr.ObjectUtils.getPath(obj, 'foo', 'bar', 'nonexistent'))
+ .toBeUndefined();
+ expect(
+ mr.ObjectUtils.getPath(obj, 'foo', 'bar', 'baz', 'qux', 'nonexistent'))
+ .toBeUndefined();
+ expect(mr.ObjectUtils.getPath(obj, 'foo', 'bar', 'xyzzy', 'nonexistent'))
+ .toBeUndefined();
+ });
+
+ it('getPath returns value', () => {
+ expect(mr.ObjectUtils.getPath(obj, 'foo', 'bar', 'baz', 'qux')).toBe(1);
+ expect(mr.ObjectUtils.getPath(obj, 'foo', 'bar', 'baz', 'corge'))
+ .toBe(thudObj);
+ expect(mr.ObjectUtils.getPath(obj, 'foo', 'bar', 'xyzzy')).toBeNull();
+ });
+
+ it('getPath returns itself if no path provided', () => {
+ expect(mr.ObjectUtils.getPath(obj)).toBe(obj);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/platform_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/platform_utils.js
new file mode 100644
index 00000000000..26828b03848
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/platform_utils.js
@@ -0,0 +1,62 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Platform related utilities methods.
+
+ */
+
+goog.provide('mr.PlatformUtils');
+
+
+/**
+ * Returns true if the extension is running on Windows and Windows 8 or
+ * newer.
+ *
+ * @return {boolean} Whether the extension is running on Windows 8 or
+ * newer.
+ */
+mr.PlatformUtils.isWindows8OrNewer = function() {
+ // See MSDN for different windows versions:
+ // http://msdn.microsoft.com/en-us/library/ms537503(v=vs.85).aspx
+ const [, version] = navigator.userAgent.match(/Windows NT (\d+.\d+)/) || [];
+
+ // Windows 8 has version number 6.2.
+ return parseFloat(version) >= 6.2;
+};
+
+
+/**
+ * Enumeration of possible current operating systems.
+ * @enum {string}
+ */
+mr.PlatformUtils.OS = {
+ CHROMEOS: 'ChromeOS',
+ WINDOWS: 'Windows',
+ MAC: 'Mac',
+ LINUX: 'Linux',
+ OTHER: 'Other'
+};
+
+
+/**
+ * Returns the current OS.
+ * @return {!mr.PlatformUtils.OS}
+ */
+mr.PlatformUtils.getCurrentOS = function() {
+ const userAgent = navigator.userAgent;
+ if (userAgent.includes('CrOS')) {
+ return mr.PlatformUtils.OS.CHROMEOS;
+ }
+ if (userAgent.includes('Windows')) {
+ return mr.PlatformUtils.OS.WINDOWS;
+ }
+ if (userAgent.includes('Macintosh')) {
+ return mr.PlatformUtils.OS.MAC;
+ }
+ if (userAgent.includes('Linux')) {
+ return mr.PlatformUtils.OS.LINUX;
+ }
+ return mr.PlatformUtils.OS.OTHER;
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/promise_resolver.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/promise_resolver.js
new file mode 100644
index 00000000000..f7645339199
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/promise_resolver.js
@@ -0,0 +1,54 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+
+ */
+goog.module('mr.PromiseResolver');
+goog.module.declareLegacyNamespace();
+
+
+/**
+ * Wrapper around a Promise that allows the Promise to be resolved by
+ * calling a method.
+ *
+ * @template T
+ */
+exports = class {
+ constructor() {
+ /**
+ * @private {function(T)}
+ */
+ this.resolveFunc_;
+
+ /**
+ * @private {function(*)}
+ */
+ this.rejectFunc_;
+
+ /**
+ * @const {!Promise<T>}
+ */
+ this.promise = new Promise((resolve, reject) => {
+ this.resolveFunc_ = resolve;
+ this.rejectFunc_ = reject;
+ });
+ }
+
+ /**
+ * Resolves the promise.
+ * @param {T} value
+ */
+ resolve(value) {
+ this.resolveFunc_(value);
+ }
+
+ /**
+ * Rejects the promise.
+ * @param {*} reason
+ */
+ reject(reason) {
+ this.rejectFunc_(reason);
+ }
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/promise_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/promise_utils.js
new file mode 100644
index 00000000000..0dcd2e23b12
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/promise_utils.js
@@ -0,0 +1,36 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Utility functions for dealing with promises.
+ */
+goog.module('mr.PromiseUtils');
+goog.module.declareLegacyNamespace();
+
+
+/**
+ * Given a list of promises, waits for all promises to settle, and produces a
+ * list indicating whether each input promise was fulfilled (i.e. resolved) or
+ * rejected.
+ *
+ * Each object in the output contains two fields: the |fulfilled| field is true
+ * if the corresponding input promise was resolved, or false if it was rejected.
+ * When |fulfilled| is true, the |value| field contains the value with which the
+ * promise was resolved. Otherwise, the |reason| field contains the reason with
+ * which the promise was rejected.
+ *
+ * @param {!Array<!Promise<T>>} promises
+ * @return {!Promise<!Array<{
+ * fulfilled: boolean,
+ * value: (T | undefined),
+ * reason: *
+ * }>>}
+ * @template T
+ */
+exports.allSettled = function(promises) {
+ return Promise.all(promises.map(
+ promise => promise.then(
+ value => ({fulfilled: true, value: value}),
+ reason => ({fulfilled: false, reason: reason}))));
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/sha1.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/sha1.js
new file mode 100644
index 00000000000..8b537c9f02e
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/sha1.js
@@ -0,0 +1,239 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.Sha1');
+
+const BLOCK_SIZE = 512 / 8;
+
+/**
+ * SHA-1 cryptographic hash constructor.
+ * @final
+ */
+class Sha1 {
+ constructor() {
+ /**
+ * Holds the previous values of accumulated variables a-e in the compress_
+ * function.
+ * @private @const {!Array<number>}
+ */
+ this.chain_ = [];
+
+ /**
+ * A buffer holding the partially computed hash result.
+ * @private @const {!Array<number>}
+ */
+ this.buf_ = [];
+
+ /**
+ * An array of 80 bytes, each a part of the message to be hashed. Referred
+ * to as the message schedule in the docs.
+ * @private @const {!Array<number>}
+ */
+ this.W_ = [];
+
+ /**
+ * Contains data needed to pad messages less than 64 bytes.
+ * @private @const {!Array<number>}
+ */
+ this.pad_ = [];
+
+ this.pad_[0] = 128;
+ for (let i = 1; i < BLOCK_SIZE; ++i) {
+ this.pad_[i] = 0;
+ }
+
+ /**
+ * @private {number}
+ */
+ this.inbuf_ = 0;
+
+ /**
+ * @private {number}
+ */
+ this.total_ = 0;
+
+ this.reset();
+ }
+
+ /**
+ * Resets the internal accumulator.
+ */
+ reset() {
+ this.chain_[0] = 0x67452301;
+ this.chain_[1] = 0xefcdab89;
+ this.chain_[2] = 0x98badcfe;
+ this.chain_[3] = 0x10325476;
+ this.chain_[4] = 0xc3d2e1f0;
+
+ this.inbuf_ = 0;
+ this.total_ = 0;
+ }
+
+ /**
+ * Internal compress helper function.
+ * @param {!Array<number>|string} buf Block to compress.
+ * @param {number=} offset Offset of the block in the buffer.
+ * @private
+ */
+ compress_(buf, offset = 0) {
+ let W = this.W_;
+
+ // get 16 big endian words
+ if (typeof buf === 'string') {
+ for (let i = 0; i < 16; i++) {
+ W[i] = (buf.charCodeAt(offset) << 24) |
+ (buf.charCodeAt(offset + 1) << 16) |
+ (buf.charCodeAt(offset + 2) << 8) | (buf.charCodeAt(offset + 3));
+ offset += 4;
+ }
+ } else {
+ for (let i = 0; i < 16; i++) {
+ W[i] = (buf[offset] << 24) | (buf[offset + 1] << 16) |
+ (buf[offset + 2] << 8) | (buf[offset + 3]);
+ offset += 4;
+ }
+ }
+
+ // expand to 80 words
+ for (let i = 16; i < 80; i++) {
+ let t = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16];
+ W[i] = ((t << 1) | (t >>> 31)) & 0xffffffff;
+ }
+
+ let a = this.chain_[0];
+ let b = this.chain_[1];
+ let c = this.chain_[2];
+ let d = this.chain_[3];
+ let e = this.chain_[4];
+ let f, k;
+
+ for (let i = 0; i < 80; i++) {
+ if (i < 40) {
+ if (i < 20) {
+ f = d ^ (b & (c ^ d));
+ k = 0x5a827999;
+ } else {
+ f = b ^ c ^ d;
+ k = 0x6ed9eba1;
+ }
+ } else {
+ if (i < 60) {
+ f = (b & c) | (d & (b | c));
+ k = 0x8f1bbcdc;
+ } else {
+ f = b ^ c ^ d;
+ k = 0xca62c1d6;
+ }
+ }
+
+ let t = (((a << 5) | (a >>> 27)) + f + e + k + W[i]) & 0xffffffff;
+ e = d;
+ d = c;
+ c = ((b << 30) | (b >>> 2)) & 0xffffffff;
+ b = a;
+ a = t;
+ }
+
+ this.chain_[0] = (this.chain_[0] + a) & 0xffffffff;
+ this.chain_[1] = (this.chain_[1] + b) & 0xffffffff;
+ this.chain_[2] = (this.chain_[2] + c) & 0xffffffff;
+ this.chain_[3] = (this.chain_[3] + d) & 0xffffffff;
+ this.chain_[4] = (this.chain_[4] + e) & 0xffffffff;
+ }
+
+ /**
+ * Adds a string (must only contain 8-bit, i.e., Latin1 characters)
+ * to the internal accumulator.
+ *
+ * @param {string|!Array<number>} bytes Data used for the update.
+ * @param {number=} length
+ */
+ update(bytes, length = bytes.length) {
+ let lengthMinusBlock = length - BLOCK_SIZE;
+ let n = 0;
+ // Using local instead of member variables gives ~5% speedup on Firefox 16.
+ let buf = this.buf_;
+ let inbuf = this.inbuf_;
+
+ // The outer while loop should execute at most twice.
+ while (n < length) {
+ // When we have no data in the block to top up, we can directly process
+ // the input buffer (assuming it contains sufficient data). This gives
+ // ~25% speedup on Chrome 23 and ~15% speedup on Firefox 16, but requires
+ // that the data is provided in large chunks (or in multiples of 64
+ // bytes).
+ if (inbuf == 0) {
+ while (n <= lengthMinusBlock) {
+ this.compress_(bytes, n);
+ n += BLOCK_SIZE;
+ }
+ }
+
+ if (typeof bytes === 'string') {
+ while (n < length) {
+ buf[inbuf] = bytes.charCodeAt(n);
+ ++inbuf;
+ ++n;
+ if (inbuf == BLOCK_SIZE) {
+ this.compress_(buf);
+ inbuf = 0;
+ // Jump to the outer loop so we use the full-block optimization.
+ break;
+ }
+ }
+ } else {
+ while (n < length) {
+ buf[inbuf] = bytes[n];
+ ++inbuf;
+ ++n;
+ if (inbuf == BLOCK_SIZE) {
+ this.compress_(buf);
+ inbuf = 0;
+ // Jump to the outer loop so we use the full-block optimization.
+ break;
+ }
+ }
+ }
+ }
+
+ this.inbuf_ = inbuf;
+ this.total_ += length;
+ }
+
+ /**
+ * @return {!Array<number>} The finalized hash computed
+ * from the internal accumulator.
+ */
+ digest() {
+ let digest = [];
+ let totalBits = this.total_ * 8;
+
+ // Add pad 0x80 0x00*.
+ if (this.inbuf_ < 56) {
+ this.update(this.pad_, 56 - this.inbuf_);
+ } else {
+ this.update(this.pad_, BLOCK_SIZE - (this.inbuf_ - 56));
+ }
+
+ // Add # bits.
+ for (let i = BLOCK_SIZE - 1; i >= 56; i--) {
+ this.buf_[i] = totalBits & 255;
+ totalBits /= 256; // Don't use bit-shifting here!
+ }
+
+ this.compress_(this.buf_);
+
+ let n = 0;
+ for (let i = 0; i < 5; i++) {
+ for (let j = 24; j >= 0; j -= 8) {
+ digest[n] = (this.chain_[i] >> j) & 255;
+ ++n;
+ }
+ }
+
+ return digest;
+ }
+}
+
+exports = Sha1;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/sha1_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/sha1_test.js
new file mode 100644
index 00000000000..1385d776a0d
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/sha1_test.js
@@ -0,0 +1,60 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.Sha1.test');
+goog.setTestOnly();
+
+const Sha1 = goog.require('mr.Sha1');
+
+/**
+ * Converts an array of byte values to a hexadecimal string.
+ * @param {!Array<number>} bytes
+ * @return {string}
+ */
+function toHex(bytes) {
+ return bytes.map(byte => byte.toString(16).padStart(2, '0')).join('');
+}
+
+describe('mr.Sha1', () => {
+ // Test vectors from:
+ // csrc.nist.gov/publications/fips/fips180-2/fips180-2withchangenotice.pdf
+
+ let sha1;
+
+ beforeEach(() => {
+ sha1 = new Sha1();
+ });
+
+ it('hashes an empty stream correctly', () => {
+ expect(toHex(sha1.digest()))
+ .toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709');
+ });
+
+ it('hashes a one-block message correctly', () => {
+ sha1.update('abc');
+ expect(toHex(sha1.digest()))
+ .toBe('a9993e364706816aba3e25717850c26c9cd0d89d');
+ });
+
+ it('hashes a multi-block message correctly', () => {
+ sha1.update('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq');
+ expect(toHex(sha1.digest()))
+ .toBe('84983e441c3bd26ebaae4aa1f95129e5e54670f1');
+ });
+
+ it('hashes a long message correctly', () => {
+ const thousandAs = 'a'.repeat(1000);
+ for (let i = 0; i < 1000; ++i) {
+ sha1.update(thousandAs);
+ }
+ expect(toHex(sha1.digest()))
+ .toBe('34aa973cd4c4daa4f61eeb2bdbad27316534016f');
+ });
+
+ it('hashes a standard message correctly', () => {
+ sha1.update('The quick brown fox jumps over the lazy dog');
+ expect(toHex(sha1.digest()))
+ .toBe('2fd4e1c67a2d28fced849ee1bb76e7391b93eb12');
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/string_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/string_utils.js
new file mode 100644
index 00000000000..c21aee6dafc
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/string_utils.js
@@ -0,0 +1,24 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.StringUtils');
+
+class StringUtils {
+ /**
+ * Returns a string with at least 64-bits of randomness.
+ *
+ * Doesn't trust Javascript's random function entirely. Uses a combination of
+ * random and current timestamp, and then encodes the string in base-36 to
+ * make it shorter.
+ *
+ * @return {string} A random string, e.g. sn1s7vb4gcic.
+ */
+ static getRandomString() {
+ var x = 2147483648;
+ return Math.floor(Math.random() * x).toString(36) +
+ Math.abs(Math.floor(Math.random() * x) ^ Date.now()).toString(36);
+ }
+}
+
+exports = StringUtils;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/tab_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/tab_utils.js
new file mode 100644
index 00000000000..730322e3d67
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/tab_utils.js
@@ -0,0 +1,28 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Tab-related utilities methods.
+
+ */
+
+goog.provide('mr.TabUtils');
+
+
+/**
+ * @param {number} tabId
+ * @return {!Promise<!Tab>} A promise fulfilled with tab, or rejected if
+ * tab does not exist.
+ */
+mr.TabUtils.getTab = function(tabId) {
+ return new Promise((resolve, reject) => {
+ chrome.tabs.get(tabId, resolve);
+ })
+ .then(tab => {
+ if (!tab) {
+ throw Error('No such tab ' + tabId);
+ }
+ return tab;
+ });
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/throttle.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/throttle.js
new file mode 100644
index 00000000000..ecc2967e9d5
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/throttle.js
@@ -0,0 +1,93 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.Throttle');
+goog.module.declareLegacyNamespace();
+
+
+/**
+ * Throttle will perform an action that is passed in no more than once
+ * per interval (specified in milliseconds). If it gets multiple signals
+ * to perform the action while it is waiting, it will only perform the action
+ * once at the end of the interval.
+ * @final
+ * @template T
+ */
+class Throttle {
+ /**
+ * @param {function(this: T, ...?)} listener Function to callback when the
+ * action is triggered.
+ * @param {number} interval Interval over which to throttle. The listener can
+ * only be called once per interval.
+ * @param {T=} handler Object in whose scope to call the listener.
+ */
+ constructor(listener, interval, handler = undefined) {
+ /** @private @const */
+ this.listener_ = handler != null ? listener.bind(handler) : listener;
+
+ /** @private @const */
+ this.interval_ = interval;
+
+ /** @private @const */
+ this.callback_ = this.onTimer_.bind(this);
+
+ /**
+ * The last arguments passed into `fire`.
+ * @private {!Array<?>}
+ */
+ this.args_ = [];
+
+ /**
+ * Indicates that the action is pending and needs to be fired.
+ * @private {boolean}
+ */
+ this.shouldFire_ = false;
+
+ /**
+ * Timer for scheduling the next callback
+ * @private {?number}
+ */
+ this.timerId_ = null;
+ }
+
+ /**
+ * Notifies the throttle that the action has happened. It will throttle the
+ * call so that the callback is not called too often according to the interval
+ * parameter passed to the constructor, passing the arguments from the last
+ * call of this function into the throttled function.
+ * @param {...?} args Arguments to pass on to the throttled function.
+ */
+ fire(...args) {
+ this.args_ = [...args];
+ if (this.timerId_ == null) {
+ this.doAction_();
+ } else {
+ this.shouldFire_ = true;
+ }
+ }
+
+ /**
+ * Calls the callback
+ * @private
+ */
+ doAction_() {
+ this.timerId_ = setTimeout(this.callback_, this.interval_);
+ this.listener_(...this.args_);
+ }
+
+ /**
+ * Handler for the timer to fire the throttle
+ * @private
+ */
+ onTimer_() {
+ this.timerId_ = null;
+
+ if (this.shouldFire_) {
+ this.shouldFire_ = false;
+ this.doAction_();
+ }
+ }
+}
+
+exports = Throttle;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/throttle_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/throttle_test.js
new file mode 100644
index 00000000000..b048a452b5e
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/throttle_test.js
@@ -0,0 +1,100 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.Throttle.test');
+goog.setTestOnly();
+
+const Throttle = goog.require('mr.Throttle');
+const UnitTestUtils = goog.require('mr.UnitTestUtils');
+
+
+describe('mr.Timer', () => {
+ let clock;
+
+ beforeEach(() => {
+ clock = UnitTestUtils.useMockClockAndPromises();
+ });
+
+ afterEach(() => {
+ UnitTestUtils.restoreRealClockAndPromises();
+ });
+
+ it('works', () => {
+ let callBackCount = 0;
+ const callBackFunction = () => {
+ callBackCount++;
+ };
+
+ const throttle = new Throttle(callBackFunction, 100);
+ expect(callBackCount).toBe(0);
+ throttle.fire();
+ expect(callBackCount).toBe(1);
+ throttle.fire();
+ expect(callBackCount).toBe(1);
+ throttle.fire();
+ throttle.fire();
+ expect(callBackCount).toBe(1);
+ clock.tick(101);
+ expect(callBackCount).toBe(2);
+ clock.tick(101);
+ expect(callBackCount).toBe(2);
+ });
+
+ it('binds scope correctly', () => {
+ const interval = 500;
+ const x = {'y': 0};
+ new Throttle(function() {
+ ++this['y'];
+ }, interval, x).fire();
+ expect(x['y']).toBe(1);
+ });
+
+
+ it('binds arguments correctly', () => {
+ const interval = 500;
+ let calls = 0;
+ const throttle = new Throttle((a, b, c) => {
+ ++calls;
+ expect(a).toBe(3);
+ expect(b).toBe('string');
+ expect(c).toBe(false);
+ }, interval);
+
+ throttle.fire(3, 'string', false);
+ expect(calls).toBe(1);
+
+ // fire should always pass the last arguments passed to it into the
+ // decorated function, even if called multiple times.
+ throttle.fire();
+ clock.tick(interval / 2);
+ throttle.fire(8, null, true);
+ throttle.fire(3, 'string', false);
+ clock.tick(interval);
+ expect(calls).toBe(2);
+ });
+
+
+ it('binds arguments and scope correctly', () => {
+ const interval = 500;
+ const x = {'calls': 0};
+ const throttle = new Throttle(function(a, b, c) {
+ ++this['calls'];
+ expect(a).toBe(3);
+ expect(b).toBe('string');
+ expect(c).toBe(false);
+ }, interval, x);
+
+ throttle.fire(3, 'string', false);
+ expect(x['calls']).toBe(1);
+
+ // fire should always pass the last arguments passed to it into the
+ // decorated function, even if called multiple times.
+ throttle.fire();
+ clock.tick(interval / 2);
+ throttle.fire(8, null, true);
+ throttle.fire(3, 'string', false);
+ clock.tick(interval);
+ expect(x['calls']).toBe(2);
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/unit_test_utils.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/unit_test_utils.js
new file mode 100644
index 00000000000..e2d8461b2ab
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/unit_test_utils.js
@@ -0,0 +1,307 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+
+ * @fileoverview Test utilities for unit tests.
+ */
+goog.provide('mr.UnitTestUtils');
+goog.setTestOnly('mr.UnitTestUtils');
+
+goog.require('mr.Assertions');
+goog.require('mr.MockClock');
+goog.require('mr.MockPromise');
+
+
+/**
+ * Creates a mock implementation of the RouteControllerCallbacks interface.
+ * @return {!Object}
+ */
+mr.UnitTestUtils.createRouteControllerCallbacksSpyObj = function() {
+ return jasmine.createSpyObj('RouteControllerCallbacks', [
+ 'onRouteControllerInvalidated', 'sendMediaControlRequest',
+ 'sendVolumeRequest'
+ ]);
+};
+
+
+/**
+ * Creates a Mojo InterfaceRequest-like object with Jasmine spy.
+ * @return {!Object}
+ */
+mr.UnitTestUtils.createMojoRequestSpyObj = function() {
+ return jasmine.createSpyObj('Request', ['close']);
+};
+
+
+/**
+ * Creates a Mojo Binding-like object with Jasmine spy.
+ * @return {!Object}
+ */
+mr.UnitTestUtils.createMojoBindingSpyObj = function() {
+ return jasmine.createSpyObj(
+ 'Binding', ['bind', 'close', 'setConnectionErrorHandler']);
+};
+
+
+/**
+ * Creates a Mojo MediaStatusObserver-like object with Jasmine spies.
+ * @return {!Object}
+ */
+mr.UnitTestUtils.createMojoMediaStatusObserverSpyObj = function() {
+ return {
+ ptr: jasmine.createSpyObj(
+ 'PtrController', ['reset', 'setConnectionErrorHandler']),
+ onMediaStatusUpdated: jasmine.createSpy('onMediaStatusUpdated')
+ };
+};
+
+
+/**
+ * Creates a Jasmine spy object with the same methods of the given constructor
+ * type.
+ * @param {Function} constructor The object's constructor to be mocked. E.g.
+ * MyClass.prototype.
+ * @param {string} mockName The mock's name (will show up in failed test
+ * results).
+ * @return {Object}
+ */
+mr.UnitTestUtils.createMock = function(constructor, mockName) {
+ const methodNames = new Set();
+ while (constructor) {
+ for (let name of Object.getOwnPropertyNames(constructor)) {
+ methodNames.add(name);
+ }
+ constructor = Object.getPrototypeOf(constructor);
+ }
+ return jasmine.createSpyObj(mockName, [...methodNames]);
+};
+
+/**
+ * Replaces parts of the window.mojo API with Jasmine spys used by unit tests.
+ */
+mr.UnitTestUtils.mockMojoApi = function() {
+ window.mojo = {
+ Binding: {},
+ HangoutsMediaRouteController: {},
+ HangoutsMediaStatusExtraData: {},
+ MediaStatus: {
+ PlayState: {PLAYING: 0, PAUSED: 1, BUFFERING: 2},
+ },
+ MediaController: {},
+ RouteControllerType: {kNone: 0, kGeneric: 1, kHangouts: 2},
+ TimeDelta: {},
+ Origin: {}
+ };
+ // We return a copy of the object used to construct the MediaStatus.
+ // This works because the fields in the object maps directly to the fields
+ // in MediaStatus.
+ spyOn(window.mojo, 'HangoutsMediaStatusExtraData')
+ .and.callFake(obj => Object.assign({}, obj));
+ spyOn(window.mojo, 'MediaStatus').and.callFake(obj => Object.assign({}, obj));
+ spyOn(window.mojo, 'TimeDelta').and.callFake(obj => Object.assign({}, obj));
+ spyOn(window.mojo, 'Origin').and.callFake(obj => Object.assign({}, obj));
+};
+
+/**
+ * Replaces parts of the chrome API with Jasmine spy objects. After calling this
+ * function, call restoreChromeApi() during test teardown to restore the
+ * original API.
+ */
+mr.UnitTestUtils.mockChromeApi = function() {
+ mr.UnitTestUtils.originalChromeApi_ = chrome;
+ chrome = {
+ cast: {
+ channel: {
+ onError: jasmine.createSpy('chrome.cast.channel.onError spy'),
+ onMessage: jasmine.createSpy('chrome.cast.channel.onMessage spy')
+ },
+ SessionStatus: {
+ CONNECTED: 'connected',
+ DISCONNECTED: 'disconnected',
+ STOPPED: 'stopped'
+ },
+ VolumeControlType:
+ {ATTENUATION: 'attenuation', FIXED: 'fixed', MASTER: 'master'}
+ },
+ dial: {
+ onDeviceList: jasmine.createSpy('chrome.dial.onDeviceList spy'),
+ onError: jasmine.createSpy('chrome.dial.onError spy')
+ },
+ identity: {
+ onSignInChanged: {
+ addListener:
+ jasmine.createSpy('chrome.identity.onSignInChanged.addListener spy')
+ }
+ },
+ runtime: {
+ onMessage: {
+ addListener:
+ jasmine.createSpy('chrome.runtime.onMessage.addListener spy')
+ },
+ onMessageExternal: {
+ addListener: jasmine.createSpy(
+ 'chrome.runtime.onMessageExternal.addListener spy')
+ },
+ onStartup: {
+ addListener:
+ jasmine.createSpy('chrome.runtime.onStartup.addListener spy')
+ },
+ onSuspend: {
+ addListener:
+ jasmine.createSpy('chrome.runtime.onSuspend.addListener spy')
+ },
+ getManifest: () => {
+ return {version: '1.0'};
+ },
+ sendMessage: jasmine.createSpy('chrome.runtime.sendMessage spy'),
+ },
+ mdns: {onSerivceList: jasmine.createSpy('chrome.mdns.onServiceList spy')},
+ metricsPrivate: {
+ recordMediumTime:
+ jasmine.createSpy('chrome.metricsPrivate.recordMediumTime spy'),
+ recordUserAction:
+ jasmine.createSpy('chrome.metricsPrivate.recordUserAction spy'),
+ },
+ networkingPrivate: {
+ onNetworksChanged:
+ jasmine.createSpy('chrome.networkingPrivate.onNetworksChanged spy')
+ },
+ gcm: {
+ onMessage: {
+ addListener: jasmine.createSpy('chrome.gcm.onMessage.addListener spy')
+ }
+ },
+ };
+};
+
+/**
+ * Restores the original chrome API after it's been mocked by a mockChromeApi()
+ * call.
+ */
+mr.UnitTestUtils.restoreChromeApi = function() {
+ chrome = mr.UnitTestUtils.originalChromeApi_;
+};
+
+/**
+ * Stores a reference to the original chrome API while it is mocked.
+ * @private {?Object}
+ */
+mr.UnitTestUtils.originalChromeApi_ = null;
+
+
+
+/**
+ * @private {mr.MockClock}
+ */
+mr.UnitTestUtils.mockClock_ = null;
+
+
+/**
+ * Installs a mock clock and replaces the native Promise type with goog.Promise.
+ *
+
+ *
+ * @return {!mr.MockClock}
+ */
+mr.UnitTestUtils.useMockClockAndPromises = function() {
+ mr.Assertions.assert(mr.UnitTestUtils.mockClock_ == null);
+ mr.MockPromise.install();
+ const mockClock = new mr.MockClock(true);
+ mr.UnitTestUtils.mockClock_ = mockClock;
+ return mockClock;
+};
+
+
+/**
+ * Undoes the effect of calling useMockClockAndPromises().
+ */
+mr.UnitTestUtils.restoreRealClockAndPromises = function() {
+ mr.UnitTestUtils.mockClock_.uninstall();
+ mr.UnitTestUtils.mockClock_ = null;
+ mr.MockPromise.uninstall();
+};
+
+
+/**
+ * Asserts that mock clock and mock promises are in use.
+ * @private
+ */
+mr.UnitTestUtils.assertUsingMockPromises_ = function() {
+ mr.Assertions.assert(Promise === mr.MockPromise);
+};
+
+
+/**
+ * Waits for the provided promise to resolve.
+ * @param {!Promise<T>} promise The promise to resolve.
+ * @template T
+ */
+mr.UnitTestUtils.awaitPromiseResolved = function(promise) {
+ mr.UnitTestUtils.assertUsingMockPromises_();
+ let resolved = false;
+ promise.then(result => {
+ resolved = true;
+ });
+ mr.MockPromise.callPendingHandlers();
+ expect(resolved).toBe(true);
+};
+
+
+/**
+ * Expects the provided promise to resolve to a value that makes the given
+ * predicate true.
+ * @param {!Promise<T>} promise The promise to resolve.
+ * @param {function(T):boolean} isCorrect A function called with the resolved
+ * value of the promise; expected to return true.
+ * @template T
+ */
+mr.UnitTestUtils.expectPromiseResult = function(promise, isCorrect) {
+ mr.UnitTestUtils.assertUsingMockPromises_();
+ let resolved = false;
+ let actual;
+ promise.then(result => {
+ resolved = true;
+ actual = result;
+ });
+ mr.MockPromise.callPendingHandlers();
+ expect(resolved).toBe(true);
+ expect(isCorrect(actual)).toBe(true);
+};
+
+
+/**
+ * Expects the provided promise to resolve to the given result.
+ * @param {!Promise} promise
+ * @param {*} expectedResult
+ */
+mr.UnitTestUtils.expectPromiseResultToEqual = function(
+ promise, expectedResult) {
+ mr.UnitTestUtils.assertUsingMockPromises_();
+ let resolved = false;
+ let actual;
+ promise.then(result => {
+ resolved = true;
+ actual = result;
+ });
+ mr.MockPromise.callPendingHandlers();
+ expect(resolved).toBe(true);
+ expect(actual).toEqual(expectedResult);
+};
+
+
+/**
+ * Expects the provided promise to resolve to the given result.
+ * @param {!Promise} promise
+ * @param {Error} expectedError
+ */
+mr.UnitTestUtils.expectPromiseRejection = function(promise, expectedError) {
+ mr.UnitTestUtils.assertUsingMockPromises_();
+ let actual;
+ promise.catch(error => {
+ actual = error;
+ });
+ mr.MockPromise.callPendingHandlers();
+ expect(actual).toEqual(expectedError);
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/xhr_manager.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/xhr_manager.js
new file mode 100644
index 00000000000..41017f63493
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/xhr_manager.js
@@ -0,0 +1,191 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.XhrManager');
+goog.module.declareLegacyNamespace();
+
+const Assertions = goog.require('mr.Assertions');
+const PromiseResolver = goog.require('mr.PromiseResolver');
+
+/**
+ * Wraps XmlHttpRequest API with additional functionalities such as queueing,
+ * timeout, and retries.
+ */
+class XhrManager {
+ /**
+ * @param {number} maxRequests The maximum number of concurrent XHR
+ * requests.
+ * @param {number} defaultTimeoutMillis The default timeout for each XHR
+ * request.
+ * @param {number} defaultNumAttempts The default number of attempts for each
+ * operation.
+ */
+ constructor(maxRequests, defaultTimeoutMillis, defaultNumAttempts) {
+ /** @private @const {number} */
+ this.maxRequests_ = maxRequests;
+
+ /** @private @const {number} */
+ this.defaultTimeoutMillis_ = defaultTimeoutMillis;
+
+ /** @private @const {number} */
+ this.defaultNumAttempts_ = defaultNumAttempts;
+
+ /**
+ * Number of pending requests. Does not exceed this.maxRequests_.
+ * @private {number}
+ */
+ this.numPendingRequests_ = 0;
+
+ /**
+ * Holds requests that have not yet been executed.
+ * @private {!Array<!QueueEntry_>}
+ */
+ this.queuedRequests_ = [];
+ }
+
+ /**
+ * Adds a request with the given parameters.
+ * @param {string} url
+ * @param {string} method
+ * @param {string=} body
+ * @param {{timeoutMillis: (number|undefined),
+ * numAttempts: (number|undefined),
+ * headers: (!Array<!Array<string>>|undefined),
+ * responseType: (string|undefined)}=} overrides "headers" is an Array
+ * of pairs of strings. "responseType" is a valid
+ * XMLHttpRequest.responseType enum value.
+ * @return {!Promise<!XMLHttpRequest>} Resolves with the response. Rejects if
+ * the request timed out on all attempts.
+ */
+ send(url, method, body = undefined, {
+ timeoutMillis = this.defaultTimeoutMillis_,
+ numAttempts = this.defaultNumAttempts_,
+ headers = null,
+ responseType = '',
+ } = {}) {
+ const entry = {
+ resolver: new PromiseResolver(),
+ url: url,
+ method: method,
+ headers: headers,
+ responseType: responseType,
+ body: body,
+ timeoutMillis: timeoutMillis,
+ numAttemptsLeft: numAttempts
+ };
+
+ if (this.numPendingRequests_ < this.maxRequests_) {
+ this.startRequest_(entry);
+ } else {
+ this.queuedRequests_.push(entry);
+ }
+
+ return entry.resolver.promise;
+ }
+
+ /**
+ * Starts a request from the request queue if there is room for an additional
+ * pending request.
+ * @private
+ */
+ startNextRequestFromQueue_() {
+ if (this.queuedRequests_.length > 0 &&
+ this.numPendingRequests_ < this.maxRequests_) {
+ const request = this.queuedRequests_.shift();
+ this.startRequest_(request);
+ }
+ }
+
+ /**
+ * Attempts the given request once. If successful, resolves the
+ * PromiseResolver with the result. Otherwise, requeues the request for retry,
+ * or rejects the PromiseResolver if it ran out of attempts. Also processes a
+ * queued request, if any, at the end.
+ * @param {!QueueEntry_} request
+ * @private
+ */
+ startRequest_(request) {
+ this.numPendingRequests_++;
+ Assertions.assert(
+ request.numAttemptsLeft > 0, 'request.numAttemptsLeft > 0');
+ request.numAttemptsLeft--;
+
+ const cleanUpAndStartNextRequest = () => {
+ this.numPendingRequests_--;
+ this.startNextRequestFromQueue_();
+ };
+
+ this.sendOneAttempt_(request).then(
+ response => {
+ request.resolver.resolve(response);
+ cleanUpAndStartNextRequest();
+ },
+ e => {
+ if (request.numAttemptsLeft == 0) {
+ request.resolver.reject(e);
+ } else {
+ // Try it again later by re-adding the request to back of queue.
+
+ this.queuedRequests_.push(request);
+ }
+ cleanUpAndStartNextRequest();
+ });
+ }
+
+ /**
+ * Executes a XMLHttpRequest and returns a Promise that resolves with the
+ * response, or rejects if the request times out.
+ * @param {!QueueEntry_} request
+ * @return {!Promise<!XMLHttpRequest>} Resolves with the response. Rejects if
+ * the request timed out.
+ * @private
+ */
+ sendOneAttempt_(request) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState == XMLHttpRequest.DONE) {
+ resolve(xhr);
+ }
+ };
+
+ xhr.timeout = request.timeoutMillis;
+ xhr.ontimeout = () => {
+ reject(new Error('Timed out'));
+ };
+
+ xhr.open(request.method, request.url, true);
+ if (request.headers == null) {
+ xhr.setRequestHeader(
+ 'Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
+ } else {
+ request.headers.forEach(
+ header => xhr.setRequestHeader(header[0], header[1]));
+ }
+ xhr.responseType = request.responseType;
+ xhr.send(request.body);
+ });
+ }
+}
+
+/** @private @record */
+const QueueEntry_ = class {};
+/** @type {!PromiseResolver<!XMLHttpRequest>} */
+QueueEntry_.prototype.resolver;
+/** @type {string} */
+QueueEntry_.prototype.url;
+/** @type {string} */
+QueueEntry_.prototype.method;
+/** @type {?Array<!Array<string>>} */
+QueueEntry_.prototype.headers;
+/** @type {string} */
+QueueEntry_.prototype.responseType;
+/** @type {string|undefined} */
+QueueEntry_.prototype.body;
+/** @type {number} */
+QueueEntry_.prototype.timeoutMillis;
+/** @type {number} */
+QueueEntry_.prototype.numAttemptsLeft;
+
+exports = XhrManager;
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/utils/xhr_manager_test.js b/chromium/chrome/browser/resources/media_router/extension/src/utils/xhr_manager_test.js
new file mode 100644
index 00000000000..44857e2a23d
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/utils/xhr_manager_test.js
@@ -0,0 +1,185 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.module('mr.XhrManagerTest');
+goog.setTestOnly('mr.XhrManagerTest');
+
+const XhrManager = goog.require('mr.XhrManager');
+
+describe('XhrManager Tests', function() {
+ const MAX_REQUESTS = 5;
+ const DEFAULT_TIMEOUT = 10;
+ const DEFAULT_ATTEMPTS = 3;
+ const SEND_URL = 'https://www.google.com';
+ const BODY = 'body';
+
+ let mockXhr;
+ let xhrManager;
+
+ beforeEach(() => {
+ mockXhr = jasmine.createSpyObj('Xhr', ['open', 'send', 'setRequestHeader']);
+ xhrManager =
+ new XhrManager(MAX_REQUESTS, DEFAULT_TIMEOUT, DEFAULT_ATTEMPTS);
+ });
+
+ it('Resolves on first try with defaults', done => {
+ spyOn(window, 'XMLHttpRequest').and.returnValue(mockXhr);
+ mockXhr.open.and.callFake((method, url, async) => {
+ expect(mockXhr.onreadystatechange).toBeDefined();
+ expect(mockXhr.timeout).toEqual(DEFAULT_TIMEOUT);
+ expect(mockXhr.ontimeout).toBeDefined();
+ expect(method).toEqual('POST');
+ expect(url).toEqual(SEND_URL);
+ expect(async).toBe(true);
+ });
+ const headersThatWereSet = [];
+ mockXhr.setRequestHeader.and.callFake((key, value) => {
+ headersThatWereSet.push([key, value]);
+ });
+ mockXhr.send.and.callFake(body => {
+ expect(mockXhr.open).toHaveBeenCalled();
+ expect(mockXhr.setRequestHeader).toHaveBeenCalled();
+ expect(headersThatWereSet).toEqual([
+ ['Content-Type', 'application/x-www-form-urlencoded;charset=utf-8']
+ ]);
+ expect(mockXhr.responseType).toBe('');
+ expect(body).toEqual(BODY);
+
+ setTimeout(() => {
+ mockXhr.readyState = XMLHttpRequest.DONE;
+ mockXhr.onreadystatechange();
+ }, 0);
+ });
+
+ xhrManager.send(SEND_URL, 'POST', BODY).then(response => {
+ expect(mockXhr.send).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('Resolves on first try with overrides', done => {
+ const overrides = {
+ timeoutMillis: 1234,
+ headers: [['Hello', 'World'], ['Eat', 'Cheese']],
+ responseType: 'arraybuffer'
+ };
+
+ spyOn(window, 'XMLHttpRequest').and.returnValue(mockXhr);
+ mockXhr.open.and.callFake((method, url, async) => {
+ expect(mockXhr.onreadystatechange).toBeDefined();
+ expect(mockXhr.timeout).toEqual(overrides.timeoutMillis);
+ expect(mockXhr.ontimeout).toBeDefined();
+ expect(method).toEqual('GET');
+ expect(url).toEqual(SEND_URL);
+ expect(async).toBe(true);
+ });
+ const headersThatWereSet = [];
+ mockXhr.setRequestHeader.and.callFake((key, value) => {
+ headersThatWereSet.push([key, value]);
+ });
+ mockXhr.send.and.callFake(body => {
+ expect(mockXhr.open).toHaveBeenCalled();
+ expect(mockXhr.setRequestHeader).toHaveBeenCalled();
+ expect(headersThatWereSet).toEqual(overrides.headers);
+ expect(mockXhr.responseType).toBe(overrides.responseType);
+ expect(body).toEqual(BODY);
+
+ setTimeout(() => {
+ mockXhr.readyState = XMLHttpRequest.DONE;
+ mockXhr.onreadystatechange();
+ }, 0);
+ });
+
+ xhrManager.send(SEND_URL, 'GET', BODY, overrides).then(response => {
+ expect(mockXhr.send).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('Resolves on retry', done => {
+ spyOn(window, 'XMLHttpRequest').and.returnValue(mockXhr);
+ let numAttempts = 0;
+ mockXhr.send.and.callFake(() => {
+ expect(mockXhr.timeout).toBe(DEFAULT_TIMEOUT);
+ ++numAttempts;
+ if (numAttempts <= 1) {
+ setTimeout(() => mockXhr.ontimeout(), DEFAULT_TIMEOUT);
+ } else {
+ setTimeout(() => {
+ mockXhr.readyState = XMLHttpRequest.DONE;
+ mockXhr.onreadystatechange();
+ }, 0);
+ }
+ });
+
+ xhrManager.send(SEND_URL, 'GET').then(() => {
+ expect(numAttempts).toBe(2);
+ done();
+ });
+ });
+
+ it('Queues requests', done => {
+ let xhrs = [];
+ spyOn(window, 'XMLHttpRequest').and.callFake(() => {
+ const mockXhr =
+ jasmine.createSpyObj('Xhr', ['open', 'send', 'setRequestHeader']);
+ mockXhr.send.and.callFake(() => {
+ expect(xhrs.length).toBeLessThan(MAX_REQUESTS);
+ xhrs.push(mockXhr);
+ });
+ return mockXhr;
+ });
+
+ let promises1 = [];
+ let promises2 = [];
+ for (let i = 0; i < MAX_REQUESTS; i++) {
+ promises1.push(xhrManager.send(SEND_URL, 'POST', BODY));
+ }
+ for (let i = 0; i < MAX_REQUESTS; i++) {
+ promises2.push(xhrManager.send(SEND_URL, 'POST', BODY));
+ }
+
+ // Resolve the first 5 requests.
+ expect(xhrs.length).toBe(MAX_REQUESTS);
+ while (xhrs.length > 0) {
+ let xhr = xhrs.shift();
+ setTimeout(() => {
+ xhr.readyState = XMLHttpRequest.DONE;
+ xhr.onreadystatechange();
+ }, 0);
+ }
+
+ Promise.all(promises1).then(() => {
+ // Resolve the next 5 requests.
+ expect(xhrs.length).toBe(MAX_REQUESTS);
+ while (xhrs.length > 0) {
+ let xhr = xhrs.shift();
+ setTimeout(() => {
+ xhr.readyState = XMLHttpRequest.DONE;
+ xhr.onreadystatechange();
+ }, 0);
+ }
+ Promise.all(promises2).then(done);
+ });
+ });
+
+ it('Rejects if all retries failed', done => {
+ spyOn(window, 'XMLHttpRequest').and.returnValue(mockXhr);
+ let numAttempts = 0;
+ mockXhr.send.and.callFake((path, init) => {
+ ++numAttempts;
+ setTimeout(() => mockXhr.ontimeout(), DEFAULT_TIMEOUT);
+ });
+
+ xhrManager.send(SEND_URL, 'GET')
+ .then(
+ _ => {
+ fail('send() unexpectedly succeeded.');
+ },
+ () => {
+ expect(numAttempts).toBe(DEFAULT_ATTEMPTS);
+ done();
+ });
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/webrtc/messages.js b/chromium/chrome/browser/resources/media_router/extension/src/webrtc/messages.js
new file mode 100644
index 00000000000..6f8e6d43b3c
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/webrtc/messages.js
@@ -0,0 +1,200 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.provide('mr.webrtc.AuthReadyMessageData');
+goog.provide('mr.webrtc.ChannelType');
+goog.provide('mr.webrtc.Message');
+goog.provide('mr.webrtc.MessageType');
+goog.provide('mr.webrtc.OfferMessageData');
+
+goog.require('mr.mirror.Settings');
+
+goog.scope(function() {
+
+
+/**
+ * The channel types supported by the cloud MRP.
+ * @enum {string}
+ */
+mr.webrtc.ChannelType = {
+ WEAVE: 'weave',
+ SLARTI: 'slarti',
+ MESI: 'mesi'
+};
+
+
+/**
+ * The types of messages used by the cloud MRP.
+ * @enum {string}
+ */
+mr.webrtc.MessageType = {
+ // TURN messages.
+ GET_TURN_CREDENTIALS: 'GET_TURN_CREDENTIALS', // Request for creds.
+ TURN_CREDENTIALS: 'TURN_CREDENTIALS', // Creds received.
+
+ // Signaling messages.
+ OFFER: 'OFFER', // Peer connection offer.
+ ANSWER: 'ANSWER', // Peer connection answer.
+ KNOCK_ANSWER: 'KNOCK_ANSWER', // Knocking peer connection answer.
+ STOP: 'STOP', // Stop the session.
+
+ // Event messages.
+ SESSION_START_SUCCESS: 'SESSION_START_SUCCESS', // Start success event.
+ SESSION_FAILURE: 'SESSION_FAILURE', // Start failure event.
+ SESSION_END: 'SESSION_END', // Session ended event.
+
+ WEB_RTC_STATS: '__webrtc_stats__', // WebRTC stats message.
+
+ // Hangout session control messages.
+ REFRESH_AUTH: 'REFRESH_AUTH', // Request to refresh the auth token.
+ // Message data for AUTH_READY messages will be an instance of
+ // mr.webwrtc.AuthReadyMessageData.
+ AUTH_READY: 'AUTH_READY', // Response that auth token was updated.
+
+ // Route details control messages.
+ MUTE: 'MUTE', // Request to mute audio.
+ LOCAL_PRESENT: 'LOCAL_PRESENT', // Request to disable conf mode.
+ ROUTE_STATUS_REQUEST: 'STATUS_REQUEST', // Request for route status update.
+ ROUTE_STATUS_RESPONSE:
+ 'STATUS_RESPONSE', // Response to route details status.
+
+ // Hangout issues.
+ HANGOUT_INVALID: 'HANGOUT_INVALID', // Hangout name could not be resolved.
+ HANGOUT_INACTIVE: 'HANGOUT_INACTIVE', // Not enough participants in Hangout.
+
+ // Application messages sent through Presentation API.
+ PRESENTATION_CONNECTION_MESSAGE: 'PRESENTATION_CONNECTION_MESSAGE',
+};
+
+
+/**
+ * Cloud message object used for internal communication.
+ */
+mr.webrtc.Message = class {
+ /**
+ * @param {mr.webrtc.MessageType} type
+ * @param {!Object|undefined=} opt_data
+ */
+ constructor(type, opt_data) {
+ /**
+ * @type {mr.webrtc.MessageType}
+ * @export
+ */
+ this.type = type;
+ /**
+ * @type {!Object|undefined}
+ * @export
+ */
+ this.data = opt_data;
+ }
+
+ /**
+ * Returns the message for the provided JSON string.
+ * @param {string} messageStr
+ * @return {!mr.webrtc.Message}
+ */
+ static fromString(messageStr) {
+ const messageJson = JSON.parse(messageStr);
+ if (!messageJson['type']) {
+ throw Error('Invalid message');
+ }
+ return new Message(
+ /** @type {mr.webrtc.MessageType} */ (messageJson['type']),
+ /** @type {!Object|undefined} */ (messageJson['data']));
+ }
+
+ /**
+ * Constructs an AUTH_READY message.
+ * @param {!mr.webrtc.AuthReadyMessageData} data
+ * @return {!mr.webrtc.Message}
+ */
+ static authReady(data) {
+ return new Message(mr.webrtc.MessageType.AUTH_READY, data);
+ }
+
+ /**
+ * Workaround for broken handling of ES6 classes in Jasmine.
+ * @return {string}
+ */
+ jasmineToString() {
+ return '[mr.webrtc.Message instance]';
+ }
+};
+
+const Message = mr.webrtc.Message;
+
+
+/**
+ * The data for an offer message. Setting the presentation url in the WebRTC
+ * offer message indicates to the receiver that the session is a presentation
+ * session.
+ */
+mr.webrtc.OfferMessageData = class {
+ /**
+ * @param {RTCSessionDescription} description
+ * @param {mr.mirror.Settings=} opt_settings
+ * @param {MediaConstraints=} opt_mediaConstraints
+ * @param {string=} opt_presentationUrl
+ * @param {string=} opt_presentationId
+ */
+ constructor(
+ description, opt_settings, opt_mediaConstraints, opt_presentationUrl,
+ opt_presentationId) {
+ /**
+ * @type {RTCSessionDescription}
+ * @export
+ */
+ this.description = description;
+
+ /**
+ * @type {?mr.mirror.Settings}
+ * @export
+ */
+ this.settings = opt_settings || null;
+
+ /**
+ * @type {?MediaConstraints}
+ * @export
+ */
+ this.mediaConstraints = opt_mediaConstraints || null;
+
+ /**
+ * @type {?string}
+ * @export
+ */
+ this.presentationUrl = opt_presentationUrl || null;
+
+ /**
+ * @type {?string}
+ * @export
+ */
+ this.presentationId = opt_presentationId || null;
+ }
+};
+
+
+/**
+ * Data associated with an AUTH_READY message.
+ *
+ * Fields:
+ *
+ * - isMeeting: True for Thor meetings, false for Hangouts.
+ *
+ * - hangoutId: The public ID of the Hangout/meeting.
+ *
+ * - resolvedId: The resolved (internal) ID of the Hangout/meeting. May be ''.
+ *
+ * - conferenceMode: If defined, used to override the setting of the
+ * isConferenceMode_ field of HangoutSession.
+ *
+ * @typedef {{
+ * isMeeting: boolean,
+ * hangoutId: string,
+ * resolvedId: string,
+ * conferenceMode: (boolean|undefined)
+ * }}
+ */
+mr.webrtc.AuthReadyMessageData;
+
+}); // goog.scope
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection.js b/chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection.js
new file mode 100644
index 00000000000..4517bc2c85c
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection.js
@@ -0,0 +1,570 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+goog.provide('mr.webrtc.PeerConnection');
+goog.provide('mr.webrtc.TurnCredential');
+
+goog.require('mr.Assertions');
+goog.require('mr.Logger');
+goog.require('mr.PromiseResolver');
+goog.require('mr.webrtc.PeerConnectionAnalytics');
+
+goog.scope(function() {
+
+
+/**
+ * Type of credentials returned by the TURN credential service. Can be used
+ * as-is in the 'iceConnection' property of the configuration passed to the
+ * webkitRTCPeerConnection() constructor.
+ *
+ * @typedef {{
+ * username: string,
+ * credential: string,
+ * url: string
+ * }}
+ */
+mr.webrtc.TurnCredential;
+
+
+/**
+ * Creates a new PeerConnection.
+ */
+mr.webrtc.PeerConnection = class {
+ /**
+ * @param {string} routeId The route ID for the route to open this
+ * PeerConnection. Used as the data channel ID.
+ * @param {!Array<!mr.webrtc.TurnCredential>} turnCreds
+ * TURN server credentials.
+ */
+ constructor(routeId, turnCreds) {
+ mr.Assertions.assert(
+ webkitRTCPeerConnection !== undefined,
+ 'webkitRTCPeerConnection is not available. Do you need to set flags?');
+
+ /**
+ * Logger instance.
+ * @private {mr.Logger}
+ */
+ this.logger_ = mr.Logger.getInstance('cv2.PeerConnection');
+
+ /**
+ * The media constraints to assign to the PeerConnection.
+ * @type {!MediaConstraints | undefined}
+ * @private
+ */
+ this.mediaConstraints_ = PeerConnection.MEDIA_CONSTRAINTS;
+
+ /**
+ * WebRTC PeerConnection wrapped by this class.
+ * @private {webkitRTCPeerConnection}
+ */
+ this.peerConnection_ = this.createPeerConnection_(turnCreds);
+
+ /**
+ * WebRTC data channel (if it should be enabled).
+ * @type {!RTCDataChannel}
+ * @private
+ */
+ this.dataChannel_ = this.createDataChannel_(routeId);
+
+
+
+ /**
+ * Whether we will try an ICE restart when we are in a disconnected state.
+ * @private {boolean}
+ */
+ this.enableIceRestart_ = true;
+
+
+
+ /**
+ * Resolves when the Web RTC session description is received.
+ * @private {!mr.PromiseResolver<RTCSessionDescription>}
+ */
+ this.sessionDescriptionResolver_ = new mr.PromiseResolver();
+
+ /**
+ * True if sessionDescriptionResolver_ has been resolved.
+ * @private {boolean}
+ */
+ this.sessionDescriptionResolved_ = false;
+
+ /**
+ * The number of ICE candidates that have been received so far.
+ * @private {number}
+ */
+ this.numIceCandidatesReceived_ = 0;
+
+ /**
+ * ID of the timer used to abort ICE candidate gathering.
+ * @private {?number}
+ */
+ this.iceCandidateGatheringTimerId_ = null;
+
+ /**
+ * The time when the offer is created.
+ * @private {number}
+ */
+ this.offerCreationTime_ = 0;
+
+ /**
+ * The time when the most recent ICE candidate was received.
+ * @private {number}
+ */
+ this.lastIceCandidateTime_ = 0;
+
+ /**
+ * True if the PeerConnection has been started.
+ * @private {boolean}
+ */
+ this.started_ = false;
+
+ /**
+ * Callback invoked when a PeerConnection has connection issues.
+ * @private {function()}
+ */
+ this.onConnectionStale_ = () => {};
+
+ /**
+ * Callback invoked when the offer description is ready.
+ * @private {function(RTCSessionDescription)}
+ */
+ this.onOfferDescription_ = () => {};
+
+ /**
+ * Callback invoked when the connection is successfully made.
+ * @private {function(mr.webrtc.PeerConnection.Event)}
+ */
+ this.onConnectionSuccess_ = () => {};
+
+ /**
+ * Callback invoked when the connection fails.
+ * @private {function(mr.webrtc.PeerConnection.Event)}
+ */
+ this.onConnectionFailure_ = () => {};
+
+ /**
+ * Callback invoked when the connection is closed.
+ * @type {function(mr.webrtc.PeerConnection.Event)}
+ * @private
+ */
+ this.onConnectionClosed_ = () => {};
+ }
+
+ /**
+ * @param {function()} staleFn The callback.
+ */
+ setOnConnectionStale(staleFn) {
+ this.onConnectionStale_ = staleFn;
+ }
+
+ /**
+ * @param {function(mr.webrtc.PeerConnection.Event)} successFn
+ */
+ setOnConnectionSuccess(successFn) {
+ this.onConnectionSuccess_ = successFn;
+ }
+
+ /**
+ * @param {function(mr.webrtc.PeerConnection.Event)} failureFn
+ */
+ setOnConnectionFailure(failureFn) {
+ this.onConnectionFailure_ = failureFn;
+ }
+
+ /**
+ * @param {function(mr.webrtc.PeerConnection.Event)} closedFn
+ */
+ setOnConnectionClosed(closedFn) {
+ this.onConnectionClosed_ = closedFn;
+ }
+
+ /**
+ * @param {function(RTCSessionDescription)} onOfferFn
+ */
+ setOnOfferDescriptionReady(onOfferFn) {
+ this.onOfferDescription_ = onOfferFn;
+ }
+
+ /**
+ * @param {function(string)} onMessageFn
+ */
+ setOnDataChannelMessage(onMessageFn) {
+ this.dataChannel_.onmessage = function(event) {
+ onMessageFn(event.data);
+ };
+ }
+
+ /**
+ * @param {boolean} shouldEnable Whether we should enable ICE restart.
+ */
+ enableIceRestart(shouldEnable) {
+ this.enableIceRestart_ = shouldEnable;
+ }
+
+ /**
+ * @return {boolean} true if the PeerConnection has been started.
+ */
+ isStarted() {
+ return this.started_;
+ }
+
+ /**
+ * Returns the configuration data for the WebRTC PeerConnection.
+ * @return {!RTCConfiguration} The configuration.
+ * @param {!Array<!mr.webrtc.TurnCredential>} turnCreds
+ * TURN server credentials.
+ * @private
+ * @suppress {invalidCasts} invalid cast - must be a subtype or supertype
+ * from: {}
+ * to : (!RTCConfiguration)
+ */
+ getPeerConnectionConfig_(turnCreds) {
+ const server = {};
+ server['url'] = 'stun:stun.l.google.com:19302';
+ const config = {};
+ config['iceServers'] = [server].concat(turnCreds);
+ return /** @type {!RTCConfiguration} */ (config);
+ }
+
+ /**
+ * Creates the WebRTC PeerConnection object.
+ * @return {webkitRTCPeerConnection} The new PC.
+ * @param {!Array<!mr.webrtc.TurnCredential>} turnCreds
+ * TURN server credentials.
+ * @private
+ */
+ createPeerConnection_(turnCreds) {
+ const config = this.getPeerConnectionConfig_(turnCreds);
+ const peerConnection = new webkitRTCPeerConnection(config);
+ peerConnection.onicecandidate = this.onIceCandidate_.bind(this);
+ peerConnection.onicegatheringstatechange =
+ this.onIceGatheringStateChange_.bind(this);
+ peerConnection.oniceconnectionstatechange =
+ this.onIceConnectionStateChange_.bind(this);
+ this.logger_.info(
+ () => 'Created webkitRTCPeerConnnection with config: ' +
+ JSON.stringify(config));
+ return peerConnection;
+ }
+
+ /**
+ * Creates a data channel. Must be called in the initiator before the SDP is
+ * created.
+ * @param {string} channelId
+ * @return {!RTCDataChannel}
+ * @private
+ */
+ createDataChannel_(channelId) {
+ const dataChannel =
+ this.peerConnection_.createDataChannel(channelId, {'reliable': false});
+ return dataChannel;
+ }
+
+ /**
+ * Sends the provided message via the data channel.
+ * @param {!Object|string} message The message to send.
+ */
+ sendDataChannelMessage(message) {
+ if (typeof message == 'string') {
+ this.dataChannel_.send(message);
+ } else {
+ this.dataChannel_.send(JSON.stringify(message));
+ }
+ }
+
+ /**
+ * Starts the PeerConnection with any added streams.
+ */
+ start() {
+ if (!this.started_) {
+ this.started_ = true;
+ // Caller initiates offer to peer.
+ this.createOffer_();
+ }
+ }
+
+ /**
+ * Stops this PeerConnection.
+ */
+ stop() {
+ this.logger_.info('Stopping peer connection...');
+ if (this.started_) {
+ this.started_ = false;
+ if (this.peerConnection_.signalingState != 'closed') {
+ this.peerConnection_.close();
+ }
+ }
+ this.peerConnection_ = null;
+ }
+
+ /**
+ * Adds a stream to the PeerConnection.
+ * @param {!MediaStream} stream The media stream to add.
+ */
+ addStream(stream) {
+ this.peerConnection_.addStream(stream);
+ }
+
+ /**
+ * Removes a stream from the PeerConnection.
+ * @param {!MediaStream} stream The media stream to remove.
+ */
+ removeStream(stream) {
+ if (this.started_) {
+ this.peerConnection_.removeStream(stream);
+ }
+ }
+
+ /**
+ * Initiates a call to the peer.
+ * @private
+ */
+ createOffer_() {
+ this.logger_.info('Sending offer to peer.');
+ this.offerCreationTime_ = Date.now();
+
+ this.peerConnection_.createOffer(
+ this.setLocalDescription_.bind(this), error => {
+ this.logger_.warning('Error creating offer.', error);
+ }, this.mediaConstraints_);
+ this.getSessionDescription().then(sessionDescription => {
+ this.onOfferDescription_(sessionDescription);
+ });
+ }
+
+ /**
+ * Sets the local description for the session.
+ * @param {!RTCSessionDescription} sessionDescription The offer.
+ * @private
+ */
+ setLocalDescription_(sessionDescription) {
+
+ this.logger_.info(
+ () =>
+ 'Setting local description: ' + JSON.stringify(sessionDescription));
+ this.peerConnection_.setLocalDescription(
+ sessionDescription,
+ () => {
+ this.logger_.info('Local description set successfully');
+ },
+ error => {
+ this.logger_.warning('Error setting local description.', error);
+ });
+ // Cloud connections only send messages when ICE gathering is complete.
+ // This is done in createOffer_ for cloud connections instead.
+ }
+
+ /**
+ * There's currently a WebRTC "bug" which means we can't JSON.stringify an
+ * RTCSessionDescription. See http://b/19817649. So for now, we'll just put it
+ * in our own object.
+
+ * @param {RTCSessionDescription} description
+ * @return {RTCSessionDescription}
+ * @private
+ */
+ formDescriptionMessage_(description) {
+ return /** @type {RTCSessionDescription} */ (
+ {'type': description.type, 'sdp': description.sdp});
+ }
+
+ /**
+ * Returns the session offer description (if this is the sender) or answer
+ * description (if this is the receiver).
+ * Note that this description contains all of the ICE candidates as well.
+ * @return {!Promise<RTCSessionDescription>}
+ */
+ getSessionDescription() {
+ return this.sessionDescriptionResolver_.promise;
+ }
+
+ /**
+ * Sets the remote description on the peer connection.
+ * @param {!RTCSessionDescription} sessionDescription
+ */
+ setRemoteDescription(sessionDescription) {
+ this.logger_.fine(() => '<===: ' + JSON.stringify(sessionDescription));
+ const description = new RTCSessionDescription(sessionDescription);
+ this.logger_.info(
+ () => 'Setting remote description: ' + JSON.stringify(description));
+ // We received an answer! Just set the description.
+ this.peerConnection_.setRemoteDescription(
+ description,
+ () => {
+ this.logger_.info('Remote description set successfully.');
+ },
+ error => {
+ this.logger_.warning('Error setting remote description.', error);
+ });
+ }
+
+ /**
+ * Resolves the session description once all ice candidates have been
+ * received.
+ * @param {RTCPeerConnectionIceEvent} event The ICE candidate event.
+ * @private
+ */
+ onIceCandidate_(event) {
+ if (event.candidate) {
+ this.numIceCandidatesReceived_++;
+ this.lastIceCandidateTime_ = Date.now();
+ if (this.numIceCandidatesReceived_ == 1) {
+ // This is the first ICE candidate. Set a timer to abort gathering
+ // candidates. This is needed because sometimes the end of ICE
+ // candidate
+ // gathering is not detected right away even though all candidates have
+ // been gathered.
+ mr.Assertions.assert(this.iceCandidateGatheringTimerId_ == null);
+ this.iceCandidateGatheringTimerId_ = setTimeout(() => {
+ this.logger_.info('ICE candidate gathering timed out.');
+ this.iceCandidateGatheringTimerId_ = null;
+ this.resolveSessionDescription_();
+ }, PeerConnection.ICE_CANDIDATE_GATHERING_TIMEOUT_MS_);
+ } else if (this.sessionDescriptionResolved_) {
+ // This branch runs when additional ICE candidates are reported after
+ // the timeout above fires.
+ this.logger_.warning(
+ 'Received ICE candidate after resolving session description.');
+ }
+ } else {
+ this.logger_.info('End of ICE candidates.');
+ mr.webrtc.PeerConnectionAnalytics
+ .recordIceCandidateGatheringReportedDuration(
+ Date.now() - this.offerCreationTime_);
+
+ // This is a no-op if the timout above has already fired.
+ this.resolveSessionDescription_();
+
+ // Record the true duration of candidate gathering based on the time the
+ // last candidate was reported. Don't record anything if no candidates
+ // were
+ // found, because the computed duration will be invalid.
+ if (this.numIceCandidatesReceived_ > 0) {
+ mr.webrtc.PeerConnectionAnalytics
+ .recordIceCandidateGatheringRealDuration(
+ this.lastIceCandidateTime_ - this.offerCreationTime_);
+ }
+ }
+ }
+
+ /**
+ * Called when ICE gathering state changes. Tracks when the candidates are
+ * complete.
+ * @private
+ */
+ onIceGatheringStateChange_() {
+ // This method never appears to be called.
+ const state = this.peerConnection_.iceGatheringState;
+ if (state == 'completed') {
+ this.resolveSessionDescription_();
+ }
+ }
+
+ /**
+ * Resolves the session description promise. Should be called after all ICE
+ * candidates have been received.
+ * @private
+ */
+ resolveSessionDescription_() {
+ clearTimeout(this.iceCandidateGatheringTimerId_);
+ this.iceCandidateGatheringTimerId_ = null;
+ if (!this.sessionDescriptionResolved_) {
+ this.logger_.info(
+ 'Resolving sesion description after gathering ' +
+ this.numIceCandidatesReceived_ + ' ICE candidates.');
+ this.sessionDescriptionResolver_.resolve(
+ this.formDescriptionMessage_(this.peerConnection_.localDescription));
+ this.sessionDescriptionResolved_ = true;
+ }
+ }
+
+ /**
+ * Handles ICE connection state changes. Tries to restart PeerConnection when
+ * we
+ * are in a disconnected state by creating a new offer with the IceRestart
+ * constraint.
+ * @param {Event} event
+ * @private
+ */
+ onIceConnectionStateChange_(event) {
+ if (!this.peerConnection_) return;
+
+ const state = this.peerConnection_.iceConnectionState;
+ this.logger_.info('New ICE connection state: ' + state + '.');
+ if (state == 'connected') {
+ this.onConnectionSuccess_(PeerConnection.Event.ICE_CONNECTED);
+ } else if (state == 'completed') {
+ this.onConnectionSuccess_(PeerConnection.Event.ICE_COMPLETED);
+ } else if (state == 'failed') {
+ this.logger_.warning(
+ () => 'Ice connection failed: ' + JSON.stringify(event));
+ this.onConnectionFailure_(PeerConnection.Event.ICE_FAILED);
+ } else if (state == 'closed') {
+ this.onConnectionClosed_(PeerConnection.Event.ICE_CLOSED);
+ } else if (state == 'disconnected') {
+ this.logger_.warning('Ice connection state is bad.');
+ if (this.enableIceRestart_ && this.isStarted()) {
+ this.logger_.info('Restarting ICE.');
+ this.peerConnection_.createOffer(
+ this.setLocalDescription_.bind(this), error => {
+ this.logger_.warning('Error creating new offer.', error);
+ }, PeerConnection.ICE_RESTART_MEDIA_CONSTRAINTS);
+ } else {
+ this.onConnectionStale_();
+ }
+ }
+ }
+};
+
+const PeerConnection = mr.webrtc.PeerConnection;
+
+
+/**
+ * Default media constraints for tab capture.
+ * @const {!MediaConstraints}
+ */
+PeerConnection.MEDIA_CONSTRAINTS = {
+ 'mandatory': {'OfferToReceiveAudio': true, 'OfferToReceiveVideo': true}
+};
+
+
+/**
+ * Default media constraints during ICE restarts.
+ * @const {!MediaConstraints}
+ */
+PeerConnection.ICE_RESTART_MEDIA_CONSTRAINTS = {
+ 'mandatory': {
+ 'IceRestart': true,
+ 'OfferToReceiveAudio': true,
+ 'OfferToReceiveVideo': true
+ }
+};
+
+
+/**
+ * PeerConnection event types.
+ * @enum {string}
+ */
+PeerConnection.Event = {
+ // events that have a corresponding on on<Event> callback.
+ ADD_STREAM: 'addstream',
+ REMOVE_STREAM: 'removestream',
+ ICE_CANDIDATE: 'icecandidate',
+ // events that do not have a corresponding on on<Event> callback.
+ ICE_CONNECTED: 'iceconnected',
+ ICE_COMPLETED: 'icecompleted',
+ ICE_FAILED: 'icefailed',
+ ICE_CLOSED: 'iceclosed'
+};
+
+
+/**
+ * The maximum time to wait, in ms, for all ICE candidates to be gathered.
+ * Timing starts when the first ICE candidate is seen.
+ * @private @const
+ */
+PeerConnection.ICE_CANDIDATE_GATHERING_TIMEOUT_MS_ = 5 * 1000;
+
+}); // goog.scope
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection_analytics.js b/chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection_analytics.js
new file mode 100644
index 00000000000..69b5e3fabd8
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection_analytics.js
@@ -0,0 +1,58 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Defines UMA analytics specific to peer connection.
+ */
+
+goog.provide('mr.webrtc.PeerConnectionAnalytics');
+
+goog.require('mr.Timing');
+
+
+/** @const {*} */
+mr.webrtc.PeerConnectionAnalytics = {};
+
+
+/**
+ * Histogram name for time taken to gather ICE candidates, from the start of
+ * candidate gethering to the time the last candidate is reported.
+ * @private @const {string}
+ */
+mr.webrtc.PeerConnectionAnalytics.ICE_CANDIDATE_GATHERING_REAL_DURATION_ =
+ 'MediaRouter.WebRtc.IceCandidateGathering.Duration.Real';
+
+
+/**
+ * Histogram name for time taken to gather ICE candidates, from the start of
+ * candidate gethering to the time the the end of collection is reported.
+ * @private @const {string}
+ */
+mr.webrtc.PeerConnectionAnalytics.ICE_CANDIDATE_GATHERING_REPORTED_DURATION_ =
+ 'MediaRouter.WebRtc.IceCandidateGathering.Duration.Reported';
+
+
+/**
+ * Records the real duration of ICE candidate gathering.
+ * @param {number} value
+ */
+mr.webrtc.PeerConnectionAnalytics.recordIceCandidateGatheringRealDuration =
+ function(value) {
+ mr.Timing.recordDuration(
+ mr.webrtc.PeerConnectionAnalytics.ICE_CANDIDATE_GATHERING_REAL_DURATION_,
+ value);
+};
+
+
+/**
+ * Records the reported duration of ICE candidate gathering.
+ * @param {number} value
+ */
+mr.webrtc.PeerConnectionAnalytics.recordIceCandidateGatheringReportedDuration =
+ function(value) {
+ mr.Timing.recordDuration(
+ mr.webrtc.PeerConnectionAnalytics
+ .ICE_CANDIDATE_GATHERING_REPORTED_DURATION_,
+ value);
+};
diff --git a/chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection_test.js b/chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection_test.js
new file mode 100644
index 00000000000..bd87b43971d
--- /dev/null
+++ b/chromium/chrome/browser/resources/media_router/extension/src/webrtc/peer_connection_test.js
@@ -0,0 +1,215 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Tests for peer_connection.
+ */
+goog.setTestOnly('peer_connection_test');
+
+goog.require('mr.webrtc.PeerConnection');
+
+describe('mr.webrtc.PeerConnection', function() {
+ let peerConnection;
+ let mockWebkitPeerConnection, mockDataChannel;
+ let mockOnConnectionSuccess, mockOnConnectionStale;
+ let mockOnConnectionClosed, mockOnConnectionFailure;
+ let mockOnDescriptionFn, mockOnDataChannelMessage;
+ let mockClock;
+
+ const DATA_CHANNEL_NAME = 'TEST_DATA_CHANNEL';
+
+ beforeEach(function() {
+ mockOnDescriptionFn = jasmine.createSpy('mockOnDescriptionFn');
+ mockOnConnectionSuccess = jasmine.createSpy('mockOnConnectionSuccess');
+ mockOnConnectionClosed = jasmine.createSpy('mockOnConnectionClosed');
+ mockOnConnectionFailure = jasmine.createSpy('mockOnConnectionFailure');
+ mockOnConnectionStale = jasmine.createSpy('mockOnConnectionStale');
+ mockOnDataChannelMessage = jasmine.createSpy('mockOnDataChannelMessage');
+ // There seems to no longer be a prototype exposed for
+ // webkitRTCPeerConnection as of cr43. Likely duplicate of b/19817649 (and
+ // related). So create a dumb Jasmine mock object with all methods we care
+ // about.
+ mockWebkitPeerConnection = jasmine.createSpyObj('peerConnection', [
+ 'close', 'createOffer', 'createAnswer', 'createDataChannel', 'addStream',
+ 'setLocalDescription', 'setRemoteDescription', 'addIceCandidate',
+ 'removeStream', 'oniceconnected', 'onicecompleted', 'onicefailed',
+ 'onicecandidate', 'oniceconnectionstatechange'
+ ]);
+ mockDataChannel =
+ jasmine.createSpyObj('dataChannel', ['onmessage', 'send']);
+ mockWebkitPeerConnection.createDataChannel.and.returnValue(mockDataChannel);
+ webkitRTCPeerConnection = function(config) {
+ return mockWebkitPeerConnection;
+ };
+ peerConnection = new mr.webrtc.PeerConnection(DATA_CHANNEL_NAME);
+ peerConnection.setOnConnectionSuccess(mockOnConnectionSuccess);
+ peerConnection.setOnConnectionClosed(mockOnConnectionClosed);
+ peerConnection.setOnConnectionFailure(mockOnConnectionFailure);
+ peerConnection.setOnConnectionStale(mockOnConnectionStale);
+ peerConnection.setOnOfferDescriptionReady(mockOnDescriptionFn);
+ });
+
+ it('constructor creates webkit peer connection with data channel, ' +
+ 'does not start',
+ function() {
+ expect(peerConnection.isStarted()).toEqual(false);
+ expect(peerConnection.peerConnection_).toEqual(mockWebkitPeerConnection);
+ expect(peerConnection.dataChannel_).toEqual(mockDataChannel);
+ expect(mockWebkitPeerConnection.createDataChannel)
+ .toHaveBeenCalledWith(DATA_CHANNEL_NAME, {'reliable': false});
+ });
+
+ it('data channel onmessage calls callback', function() {
+ peerConnection.setOnDataChannelMessage(mockOnDataChannelMessage);
+ const event = {'data': 'DATA!!!'};
+ mockDataChannel.onmessage(event);
+ expect(mockOnDataChannelMessage).toHaveBeenCalledWith(event.data);
+ });
+
+ it('sendDataChannelMessage sends message via data channel', function() {
+ // String message.
+ const stringMessage = 'String message!';
+ peerConnection.sendDataChannelMessage(stringMessage);
+ expect(mockDataChannel.send).toHaveBeenCalledWith(stringMessage);
+
+ // Object message.
+ const objMessage = {'obj': 'message'};
+ peerConnection.sendDataChannelMessage(objMessage);
+ expect(mockDataChannel.send)
+ .toHaveBeenCalledWith(JSON.stringify(objMessage));
+ });
+
+ it('start creates offer, sets local description and calls on description ' +
+ 'callback message when ready',
+ function(done) {
+ peerConnection.start();
+
+ expect(peerConnection.isStarted()).toEqual(true);
+ expect(mockWebkitPeerConnection.createOffer).toHaveBeenCalled();
+ const createOfferArgs =
+ mockWebkitPeerConnection.createOffer.calls.mostRecent().args;
+ expect(createOfferArgs[2])
+ .toEqual(mr.webrtc.PeerConnection.MEDIA_CONSTRAINTS);
+
+ // Now call the local description callback (first arg)
+ const description = {'sdp': 'SDP!', 'type': 'TYPE!'};
+ createOfferArgs[0](description);
+ const localDescriptionArgs =
+ mockWebkitPeerConnection.setLocalDescription.calls.mostRecent().args;
+ expect(localDescriptionArgs[0]).toEqual(description);
+
+ // Now trigger the message sending by triggering ICE complete.
+ const webkitLocalDescription = {
+ 'sdp': 'Local SDP with ICE candidates!',
+ 'type': 'Local Type!'
+ };
+ mockWebkitPeerConnection.localDescription = webkitLocalDescription;
+ peerConnection.onIceCandidate_({
+ 'candidate': null // empty candidate signifies that it's done.
+ });
+ mockOnDescriptionFn.and.callFake(arg => {
+ expect(arg).toEqual(webkitLocalDescription);
+ done();
+ });
+ });
+
+ it('stop closes the webkit peer connection', function() {
+ peerConnection.started_ = true;
+ peerConnection.stop();
+
+ expect(peerConnection.isStarted()).toEqual(false);
+ expect(mockWebkitPeerConnection.close).toHaveBeenCalled();
+ });
+
+ it('addStream adds the stream to the webkit peer connection', function() {
+ const stream = {'fake': 'media stream'};
+ peerConnection.addStream(stream);
+
+ expect(mockWebkitPeerConnection.addStream).toHaveBeenCalledWith(stream);
+ });
+
+ it('removeStream removes the stream from the webkit peer connection',
+ function() {
+ const stream = {'fake': 'media stream'};
+ peerConnection.started_ = true;
+ peerConnection.removeStream(stream);
+
+ expect(mockWebkitPeerConnection.removeStream)
+ .toHaveBeenCalledWith(stream);
+ });
+
+ it('setRemoteDescription sets remote description', function() {
+ const description = {'sdp': 'SDP!', 'type': 'TYPE!'};
+ const mockRtcSessionDescription = {'sdp': 'RTC SDP!', 'type': 'RTC TYPE!'};
+ spyOn(window, 'RTCSessionDescription')
+ .and.returnValue(mockRtcSessionDescription);
+
+ peerConnection.setRemoteDescription(description);
+
+ const args =
+ mockWebkitPeerConnection.setRemoteDescription.calls.mostRecent().args;
+ expect(args[0]).toEqual(mockRtcSessionDescription);
+ });
+
+ it('onIceGatheringStateChange_ resolves the session description', done => {
+ const description = {'sdp': 'SDP!', 'type': 'TYPE!'};
+ mockWebkitPeerConnection.iceGatheringState = 'completed';
+ mockWebkitPeerConnection.localDescription = description;
+ peerConnection.onIceGatheringStateChange_();
+
+ peerConnection.sessionDescriptionResolver_.promise.then(value => {
+ expect(value).toEqual(description);
+ done();
+ });
+ });
+
+ it('onIceConnectionStateChange_ calls success callback when connected',
+ function() {
+ mockWebkitPeerConnection.iceConnectionState = 'connected';
+ peerConnection.onIceConnectionStateChange_({});
+ expect(mockOnConnectionSuccess)
+ .toHaveBeenCalledWith(mr.webrtc.PeerConnection.Event.ICE_CONNECTED);
+
+ mockOnConnectionSuccess.calls.reset();
+ mockWebkitPeerConnection.iceConnectionState = 'completed';
+ peerConnection.onIceConnectionStateChange_({});
+ expect(mockOnConnectionSuccess)
+ .toHaveBeenCalledWith(mr.webrtc.PeerConnection.Event.ICE_COMPLETED);
+ });
+
+ it('onConnectionStateChange_ calls closed callback when connection closes',
+ function() {
+ mockWebkitPeerConnection.iceConnectionState = 'closed';
+ peerConnection.onIceConnectionStateChange_({});
+ expect(mockOnConnectionClosed)
+ .toHaveBeenCalledWith(mr.webrtc.PeerConnection.Event.ICE_CLOSED);
+ });
+
+ it('onConnectionStateChange_ calls failure callback when connection fails',
+ function() {
+ mockWebkitPeerConnection.iceConnectionState = 'failed';
+ peerConnection.onIceConnectionStateChange_({});
+ expect(mockOnConnectionFailure)
+ .toHaveBeenCalledWith(mr.webrtc.PeerConnection.Event.ICE_FAILED);
+ });
+
+ it('onIceConnectionStateChange_ tries to re-connect when disconnected',
+ function() {
+ peerConnection.started_ = true;
+ peerConnection.enableIceRestart_ = true;
+
+ mockWebkitPeerConnection.iceConnectionState = 'disconnected';
+ peerConnection.onIceConnectionStateChange_({});
+
+ const args =
+ mockWebkitPeerConnection.createOffer.calls.mostRecent().args;
+ expect(args[2]).toEqual(
+ mr.webrtc.PeerConnection.ICE_RESTART_MEDIA_CONSTRAINTS);
+
+ // Instead, try when enableIceRestart is false.
+ peerConnection.enableIceRestart_ = false;
+ peerConnection.onIceConnectionStateChange_({});
+ expect(mockOnConnectionStale).toHaveBeenCalled();
+ });
+});
diff --git a/chromium/chrome/browser/resources/media_router/media_router_ui_interface.js b/chromium/chrome/browser/resources/media_router/media_router_ui_interface.js
index 8cd1ca3406d..2c1bce1a3ad 100644
--- a/chromium/chrome/browser/resources/media_router/media_router_ui_interface.js
+++ b/chromium/chrome/browser/resources/media_router/media_router_ui_interface.js
@@ -16,6 +16,9 @@ cr.define('media_router.ui', function() {
// The route-controls element. Is null if the route details view isn't open.
var routeControls = null;
+ // The initial height for |container|.
+ var initialMaxHeight = 0;
+
/**
* Handles response of previous create route attempt.
*
@@ -64,6 +67,11 @@ cr.define('media_router.ui', function() {
function setElements(mediaRouterContainer, mediaRouterHeader) {
container = mediaRouterContainer;
header = mediaRouterHeader;
+
+ if (initialMaxHeight) {
+ container.updateMaxDialogHeight(initialMaxHeight);
+ initialMaxHeight = 0;
+ }
}
/**
@@ -183,7 +191,12 @@ cr.define('media_router.ui', function() {
* @param {number} height
*/
function updateMaxHeight(height) {
- container.updateMaxDialogHeight(height);
+ if (container) {
+ container.updateMaxDialogHeight(height);
+ } else {
+ // Update the max height once |container| gets set.
+ initialMaxHeight = height;
+ }
}
/**
diff --git a/chromium/chrome/browser/resources/net_internals/http_cache_view.html b/chromium/chrome/browser/resources/net_internals/http_cache_view.html
index e2c45132725..9af570e3229 100644
--- a/chromium/chrome/browser/resources/net_internals/http_cache_view.html
+++ b/chromium/chrome/browser/resources/net_internals/http_cache_view.html
@@ -1,8 +1,4 @@
<div id=http-cache-view-tab-content class=content-box>
- <div class="hide-when-not-capturing">
- <a href="chrome://view-http-cache" target=_blank>Explore cache entries</a>
- </div>
-
<h4>Statistics</h4>
<div id=http-cache-view-cache-stats>Nothing loaded yet.</div>
</div>
diff --git a/chromium/chrome/browser/resources/ntp4/new_tab.js b/chromium/chrome/browser/resources/ntp4/new_tab.js
index c6564c4f062..98066bfe562 100644
--- a/chromium/chrome/browser/resources/ntp4/new_tab.js
+++ b/chromium/chrome/browser/resources/ntp4/new_tab.js
@@ -284,7 +284,8 @@ cr.define('ntp', function() {
var headerContainer = $('login-status-header-container');
headerContainer.classList.toggle('login-status-icon', !!iconURL);
- headerContainer.style.backgroundImage = iconURL ? url(iconURL) : 'none';
+ headerContainer.style.backgroundImage =
+ iconURL ? getUrlForCss(iconURL) : 'none';
}
if (shouldShowLoginBubble) {
diff --git a/chromium/chrome/browser/resources/offline_pages/offline_internals.html b/chromium/chrome/browser/resources/offline_pages/offline_internals.html
index 013cfd09390..f7ab768f209 100644
--- a/chromium/chrome/browser/resources/offline_pages/offline_internals.html
+++ b/chromium/chrome/browser/resources/offline_pages/offline_internals.html
@@ -58,24 +58,22 @@
<table class="stored-pages-table">
<thead>
<tr>
- <th>&nbsp;</th>
+ <th>#</th>
+ <th></th>
<th>URL</th>
<th>Namespace</th>
<th>Size (Kb)</th>
- <th>Expired</th>
- <th>Request Origin</th>
</tr>
</thead>
<tbody id="stored-pages"> </tbody>
</table>
<template id="stored-pages-table-row">
<tr>
+ <td></td>
<td><input type="checkbox" name="stored"></td>
<td><a></a></td>
<td></td>
<td></td>
- <td></td>
- <td></td>
</tr>
</template>
<div id="page-actions-info" class="dump"></div>
diff --git a/chromium/chrome/browser/resources/offline_pages/offline_internals.js b/chromium/chrome/browser/resources/offline_pages/offline_internals.js
index d9034ccda18..f184c8c005b 100644
--- a/chromium/chrome/browser/resources/offline_pages/offline_internals.js
+++ b/chromium/chrome/browser/resources/offline_pages/offline_internals.js
@@ -35,18 +35,27 @@ cr.define('offlineInternals', function() {
var template = $('stored-pages-table-row');
var td = template.content.querySelectorAll('td');
- for (let page of pages) {
- var checkbox = td[0].querySelector('input');
+ for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
+ var page = pages[pageIndex];
+ td[0].textContent = pageIndex;
+ var checkbox = td[1].querySelector('input');
checkbox.setAttribute('value', page.id);
- var link = td[1].querySelector('a');
+ var link = td[2].querySelector('a');
link.setAttribute('href', page.onlineUrl);
- link.textContent = page.onlineUrl;
+ var maxUrlCharsPerLine = 50;
+ if (page.onlineUrl.length > maxUrlCharsPerLine) {
+ link.textContent = '';
+ for (let i = 0; i < page.onlineUrl.length; i += maxUrlCharsPerLine) {
+ link.textContent += page.onlineUrl.slice(i, i + maxUrlCharsPerLine);
+ link.textContent += '\r\n';
+ }
+ } else {
+ link.textContent = page.onlineUrl;
+ }
- td[2].textContent = page.namespace;
- td[3].textContent = Math.round(page.size / 1024);
- td[4].textContent = page.isExpired;
- td[5].textContent = page.requestOrigin;
+ td[3].textContent = page.namespace;
+ td[4].textContent = Math.round(page.size / 1024);
var row = document.importNode(template.content, true);
storedPagesTable.appendChild(row);
diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-bookmark/viewer-bookmark.html b/chromium/chrome/browser/resources/pdf/elements/viewer-bookmark/viewer-bookmark.html
index 7320212941e..174b498e017 100644
--- a/chromium/chrome/browser/resources/pdf/elements/viewer-bookmark/viewer-bookmark.html
+++ b/chromium/chrome/browser/resources/pdf/elements/viewer-bookmark/viewer-bookmark.html
@@ -8,8 +8,8 @@
<template>
<style>
#item {
- @apply(--layout-center);
- @apply(--layout-horizontal);
+ @apply --layout-center;
+ @apply --layout-horizontal;
cursor: pointer;
height: 30px;
position: relative;
diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector/viewer-page-selector.html b/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector/viewer-page-selector.html
index 8ada7527043..a2c53b9ee6d 100644
--- a/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector/viewer-page-selector.html
+++ b/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector/viewer-page-selector.html
@@ -15,10 +15,9 @@
}
#pageselector {
- --container: { visibility: hidden; };
+ --container: { display: none; };
--paper-input-container-underline: var(--container);
--paper-input-container-underline-focus: var(--container);
- display: inline-block;
padding: 0;
width: 1ch;
}
@@ -26,6 +25,7 @@
#input {
-webkit-margin-start: -3px;
color: #fff;
+ height: 100%;
line-height: 18px;
padding: 3px;
text-align: end;
@@ -43,10 +43,19 @@
}
#pagelength-spacer {
- display: inline-block;
+ -webkit-margin-start: -2px;
+ padding-bottom: 1px;
text-align: start;
}
+ #pageselector,
+ #slash,
+ #pagelength-spacer {
+ display: inline-block;
+ margin-bottom: 2px;
+ vertical-align: middle;
+ }
+
#input,
#slash,
#pagelength {
@@ -54,7 +63,7 @@
}
</style>
<paper-input-container id="pageselector" no-label-float>
- <input id="input" is="iron-input" value="{{pageNo}}"
+ <input id="input" is="iron-input" value="{{pageNo}}" slot="input"
prevent-invalid-input allowed-pattern="\d" on-mouseup="select"
on-change="pageNoCommitted" aria-label$="{{strings.labelPageNumber}}">
</paper-input-container>
diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar/viewer-pdf-toolbar.html b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar/viewer-pdf-toolbar.html
index 1d5c1ad9bb5..af50fd9f2ab 100644
--- a/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar/viewer-pdf-toolbar.html
+++ b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar/viewer-pdf-toolbar.html
@@ -28,7 +28,7 @@
}
#title {
- @apply(--layout-flex-5);
+ @apply --layout-flex-5;
font-size: 0.87rem;
font-weight: 500;
overflow: hidden;
@@ -37,7 +37,7 @@
}
#pageselector-container {
- @apply(--layout-flex-1);
+ @apply --layout-flex-1;
text-align: center;
/* The container resizes according to the width of the toolbar. On small
* screens with large numbers of pages, overflow page numbers without
@@ -46,7 +46,7 @@
}
#buttons {
- @apply(--layout-flex-5);
+ @apply --layout-flex-5;
text-align: end;
user-select: none;
}
@@ -68,7 +68,7 @@
}
#toolbar {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
background-color: rgb(50, 54, 57);
color: rgb(241, 241, 241);
display: flex;
diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-toolbar-dropdown/viewer-toolbar-dropdown.html b/chromium/chrome/browser/resources/pdf/elements/viewer-toolbar-dropdown/viewer-toolbar-dropdown.html
index 2e3e6182753..95d75885720 100644
--- a/chromium/chrome/browser/resources/pdf/elements/viewer-toolbar-dropdown/viewer-toolbar-dropdown.html
+++ b/chromium/chrome/browser/resources/pdf/elements/viewer-toolbar-dropdown/viewer-toolbar-dropdown.html
@@ -26,7 +26,7 @@
}
#dropdown {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
background-color: rgb(256, 256, 256);
border-radius: 4px;
color: var(--primary-text-color);
diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar/viewer-zoom-button.html b/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar/viewer-zoom-button.html
index 83d3d8df7f0..28ed7680a35 100644
--- a/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar/viewer-zoom-button.html
+++ b/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar/viewer-zoom-button.html
@@ -21,7 +21,7 @@
}
paper-fab {
- @apply(--shadow-elevation-4dp);
+ @apply --shadow-elevation-4dp;
--paper-fab-keyboard-focus-background: var(--viewer-icon-ink-color);
--paper-fab-mini: {
height: 36px;
diff --git a/chromium/chrome/browser/resources/pdf/open_pdf_params_parser.js b/chromium/chrome/browser/resources/pdf/open_pdf_params_parser.js
index e573656c3fd..5903e403366 100644
--- a/chromium/chrome/browser/resources/pdf/open_pdf_params_parser.js
+++ b/chromium/chrome/browser/resources/pdf/open_pdf_params_parser.js
@@ -2,25 +2,28 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-var OpenPDFParamsParser;
-
(function() {
'use strict';
/**
- * Creates a new OpenPDFParamsParser. This parses the open pdf parameters
- * passed in the url to set initial viewport settings for opening the pdf.
- * @param {!Function} getNamedDestinationsFunction The function called to fetch
- * the page number for a named destination.
- * @constructor
+ * Parses the open pdf parameters passed in the url to set initial viewport
+ * settings for opening the pdf.
*/
-OpenPDFParamsParser = function(getNamedDestinationsFunction) {
- this.outstandingRequests_ = [];
- this.getNamedDestinationsFunction_ = getNamedDestinationsFunction;
-};
+window.OpenPDFParamsParser = class {
+ /**
+ * Constructor.
+ * @param {function(Object)} postMessageCallback
+ * Function called to fetch information for a named destination.
+ */
+ constructor(postMessageCallback) {
+ /** @private {!Array<!Object>} */
+ this.outstandingRequests_ = [];
+
+ /** @private {!function(Object)} */
+ this.postMessageCallback_ = postMessageCallback;
+ }
-OpenPDFParamsParser.prototype = {
/**
* @private
* Parse zoom parameter of open PDF parameters. The PDF should be opened at
@@ -28,14 +31,14 @@ OpenPDFParamsParser.prototype = {
* @param {string} paramValue zoom value.
* @return {Object} Map with zoom parameters (zoom and position).
*/
- parseZoomParam_: function(paramValue) {
- var paramValueSplit = paramValue.split(',');
+ parseZoomParam_(paramValue) {
+ const paramValueSplit = paramValue.split(',');
if (paramValueSplit.length != 1 && paramValueSplit.length != 3)
return {};
// User scale of 100 means zoom value of 100% i.e. zoom factor of 1.0.
- var zoomFactor = parseFloat(paramValueSplit[0]) / 100;
- if (isNaN(zoomFactor))
+ const zoomFactor = parseFloat(paramValueSplit[0]) / 100;
+ if (Number.isNaN(zoomFactor))
return {};
// Handle #zoom=scale.
@@ -44,12 +47,12 @@ OpenPDFParamsParser.prototype = {
}
// Handle #zoom=scale,left,top.
- var position = {
+ const position = {
x: parseFloat(paramValueSplit[1]),
y: parseFloat(paramValueSplit[2])
};
return {'position': position, 'zoom': zoomFactor};
- },
+ }
/**
* @private
@@ -58,14 +61,14 @@ OpenPDFParamsParser.prototype = {
* @param {string} paramValue view value.
* @return {Object} Map with view parameters (view and viewPosition).
*/
- parseViewParam_: function(paramValue) {
- var viewModeComponents = paramValue.toLowerCase().split(',');
+ parseViewParam_(paramValue) {
+ const viewModeComponents = paramValue.toLowerCase().split(',');
if (viewModeComponents.length < 1)
return {};
- var params = {};
- var viewMode = viewModeComponents[0];
- var acceptsPositionParam;
+ const params = {};
+ const viewMode = viewModeComponents[0];
+ let acceptsPositionParam;
if (viewMode === 'fit') {
params['view'] = FittingType.FIT_TO_PAGE;
acceptsPositionParam = false;
@@ -80,12 +83,12 @@ OpenPDFParamsParser.prototype = {
if (!acceptsPositionParam || viewModeComponents.length < 2)
return params;
- var position = parseFloat(viewModeComponents[1]);
- if (!isNaN(position))
+ const position = parseFloat(viewModeComponents[1]);
+ if (!Number.isNaN(position))
params['viewPosition'] = position;
return params;
- },
+ }
/**
* Parse the parameters encoded in the fragment of a URL into a dictionary.
@@ -93,14 +96,14 @@ OpenPDFParamsParser.prototype = {
* @param {string} url to parse
* @return {Object} Key-value pairs of URL parameters
*/
- parseUrlParams_: function(url) {
- var params = {};
+ parseUrlParams_(url) {
+ const params = {};
- var paramIndex = url.search('#');
+ const paramIndex = url.search('#');
if (paramIndex == -1)
return params;
- var paramTokens = url.substring(paramIndex + 1).split('&');
+ const paramTokens = url.substring(paramIndex + 1).split('&');
if ((paramTokens.length == 1) && (paramTokens[0].search('=') == -1)) {
// Handle the case of http://foo.com/bar#NAMEDDEST. This is not
// explicitly mentioned except by example in the Adobe
@@ -109,15 +112,15 @@ OpenPDFParamsParser.prototype = {
return params;
}
- for (var i = 0; i < paramTokens.length; ++i) {
- var keyValueSplit = paramTokens[i].split('=');
+ for (let paramToken of paramTokens) {
+ const keyValueSplit = paramToken.split('=');
if (keyValueSplit.length != 2)
continue;
params[keyValueSplit[0]] = keyValueSplit[1];
}
return params;
- },
+ }
/**
* Parse PDF url parameters used for controlling the state of UI. These need
@@ -126,15 +129,15 @@ OpenPDFParamsParser.prototype = {
* @param {string} url that needs to be parsed.
* @return {Object} parsed url parameters.
*/
- getUiUrlParams: function(url) {
- var params = this.parseUrlParams_(url);
- var uiParams = {toolbar: true};
+ getUiUrlParams(url) {
+ const params = this.parseUrlParams_(url);
+ const uiParams = {toolbar: true};
if ('toolbar' in params && params['toolbar'] == 0)
uiParams.toolbar = false;
return uiParams;
- },
+ }
/**
* Parse PDF url parameters. These parameters are mentioned in the url
@@ -144,16 +147,16 @@ OpenPDFParamsParser.prototype = {
* @param {string} url that needs to be parsed.
* @param {Function} callback function to be called with viewport info.
*/
- getViewportFromUrlParams: function(url, callback) {
- var params = {};
+ getViewportFromUrlParams(url, callback) {
+ const params = {};
params['url'] = url;
- var urlParams = this.parseUrlParams_(url);
+ const urlParams = this.parseUrlParams_(url);
if ('page' in urlParams) {
// |pageNumber| is 1-based, but goToPage() take a zero-based page number.
- var pageNumber = parseInt(urlParams['page'], 10);
- if (!isNaN(pageNumber) && pageNumber > 0)
+ const pageNumber = parseInt(urlParams['page'], 10);
+ if (!Number.isNaN(pageNumber) && pageNumber > 0)
params['page'] = pageNumber - 1;
}
@@ -165,11 +168,14 @@ OpenPDFParamsParser.prototype = {
if (params.page === undefined && 'nameddest' in urlParams) {
this.outstandingRequests_.push({callback: callback, params: params});
- this.getNamedDestinationsFunction_(urlParams['nameddest']);
+ this.postMessageCallback_({
+ type: 'getNamedDestination',
+ namedDestination: urlParams['nameddest']
+ });
} else {
callback(params);
}
- },
+ }
/**
* This is called when a named destination is received and the page number
@@ -177,12 +183,12 @@ OpenPDFParamsParser.prototype = {
* @param {number} pageNumber The page corresponding to the named destination
* requested.
*/
- onNamedDestinationReceived: function(pageNumber) {
- var outstandingRequest = this.outstandingRequests_.shift();
+ onNamedDestinationReceived(pageNumber) {
+ const outstandingRequest = this.outstandingRequests_.shift();
if (pageNumber != -1)
outstandingRequest.params.page = pageNumber;
outstandingRequest.callback(outstandingRequest.params);
- },
+ }
};
}());
diff --git a/chromium/chrome/browser/resources/pdf/pdf.js b/chromium/chrome/browser/resources/pdf/pdf.js
index 4a8cb93199e..029f5b231af 100644
--- a/chromium/chrome/browser/resources/pdf/pdf.js
+++ b/chromium/chrome/browser/resources/pdf/pdf.js
@@ -106,15 +106,20 @@ function PDFViewer(browserApi) {
this.isUserInitiatedEvent_ = true;
/**
- * @type {PDFMetrics}
+ * @type {!PDFMetrics}
*/
this.metrics =
(chrome.metricsPrivate ? new PDFMetricsImpl() : new PDFMetricsDummy());
this.metrics.onDocumentOpened();
+ /**
+ * @private {!PDFCoordsTransformer}
+ */
+ this.coordsTransformer_ =
+ new PDFCoordsTransformer(this.postMessage_.bind(this));
+
// Parse open pdf parameters.
- this.paramsParser_ =
- new OpenPDFParamsParser(this.getNamedDestination_.bind(this));
+ this.paramsParser_ = new OpenPDFParamsParser(this.postMessage_.bind(this));
var toolbarEnabled =
this.paramsParser_.getUiUrlParams(this.originalUrl_).toolbar &&
!this.isPrintPreview_;
@@ -224,9 +229,6 @@ function PDFViewer(browserApi) {
this.toolbar_.docTitle = getFilenameFromURL(this.originalUrl_);
}
- this.coordsTransformer_ =
- new PDFCoordsTransformer(this.plugin_.postMessage.bind(this.plugin_));
-
document.body.addEventListener('change-page', e => {
this.viewport_.goToPage(e.detail.page);
if (e.detail.origin == 'bookmark')
@@ -391,7 +393,7 @@ PDFViewer.prototype = {
return;
case 65: // 'a' key.
if (e.ctrlKey || e.metaKey) {
- this.plugin_.postMessage({type: 'selectAll'});
+ this.postMessage_({type: 'selectAll'});
// Since we do selection ourselves.
e.preventDefault();
}
@@ -451,7 +453,7 @@ PDFViewer.prototype = {
*/
rotateClockwise_: function() {
this.metrics.onRotation();
- this.plugin_.postMessage({type: 'rotateClockwise'});
+ this.postMessage_({type: 'rotateClockwise'});
},
/**
@@ -460,7 +462,7 @@ PDFViewer.prototype = {
*/
rotateCounterClockwise_: function() {
this.metrics.onRotation();
- this.plugin_.postMessage({type: 'rotateCounterclockwise'});
+ this.postMessage_({type: 'rotateCounterclockwise'});
},
/**
@@ -488,7 +490,7 @@ PDFViewer.prototype = {
* Notify the plugin to print.
*/
print_: function() {
- this.plugin_.postMessage({type: 'print'});
+ this.postMessage_({type: 'print'});
},
/**
@@ -496,17 +498,7 @@ PDFViewer.prototype = {
* Notify the plugin to save.
*/
save_: function() {
- this.plugin_.postMessage({type: 'save'});
- },
-
- /**
- * Fetches the page number corresponding to the given named destination from
- * the plugin.
- * @param {string} name The namedDestination to fetch page number from plugin.
- */
- getNamedDestination_: function(name) {
- this.plugin_.postMessage(
- {type: 'getNamedDestination', namedDestination: name});
+ this.postMessage_({type: 'save'});
},
/**
@@ -630,12 +622,22 @@ PDFViewer.prototype = {
* @param {Object} event a password-submitted event.
*/
onPasswordSubmitted_: function(event) {
- this.plugin_.postMessage(
+ this.postMessage_(
{type: 'getPasswordComplete', password: event.detail.password});
},
/**
* @private
+ * Post a message to the PPAPI plugin. Some messages will cause an async reply
+ * to be received through handlePluginMessage_().
+ * @param {Object} message Message to post.
+ */
+ postMessage_: function(message) {
+ this.plugin_.postMessage(message);
+ },
+
+ /**
+ * @private
* An event handler for handling message events received from the plugin.
* @param {MessageObject} message a message event.
*/
@@ -747,13 +749,13 @@ PDFViewer.prototype = {
* reacting to scroll events while zoom is taking place to avoid flickering.
*/
beforeZoom_: function() {
- this.plugin_.postMessage({type: 'stopScrolling'});
+ this.postMessage_({type: 'stopScrolling'});
if (this.viewport_.pinchPhase == Viewport.PinchPhase.PINCH_START) {
var position = this.viewport_.position;
var zoom = this.viewport_.zoom;
var pinchPhase = this.viewport_.pinchPhase;
- this.plugin_.postMessage({
+ this.postMessage_({
type: 'viewport',
userInitiated: true,
zoom: zoom,
@@ -776,7 +778,7 @@ PDFViewer.prototype = {
var pinchCenter = this.viewport_.pinchCenter || {x: 0, y: 0};
var pinchPhase = this.viewport_.pinchPhase;
- this.plugin_.postMessage({
+ this.postMessage_({
type: 'viewport',
userInitiated: this.isUserInitiatedEvent_,
zoom: zoom,
@@ -935,7 +937,7 @@ PDFViewer.prototype = {
case 'getSelectedText':
case 'print':
case 'selectAll':
- this.plugin_.postMessage(message.data);
+ this.postMessage_(message.data);
break;
}
},
@@ -952,7 +954,7 @@ PDFViewer.prototype = {
switch (message.data.type.toString()) {
case 'loadPreviewPage':
- this.plugin_.postMessage(message.data);
+ this.postMessage_(message.data);
return true;
case 'resetPrintPreviewMode':
this.loadState_ = LoadState.LOADING;
@@ -977,7 +979,7 @@ PDFViewer.prototype = {
this.pageIndicator_.pageLabels = message.data.pageNumbers;
- this.plugin_.postMessage({
+ this.postMessage_({
type: 'resetPrintPreviewMode',
url: message.data.url,
grayscale: message.data.grayscale,
diff --git a/chromium/chrome/browser/resources/plugin_metadata/plugins_linux.json b/chromium/chrome/browser/resources/plugin_metadata/plugins_linux.json
index 9d4b50b6b41..d00ba641dd8 100644
--- a/chromium/chrome/browser/resources/plugin_metadata/plugins_linux.json
+++ b/chromium/chrome/browser/resources/plugin_metadata/plugins_linux.json
@@ -1,5 +1,5 @@
{
- "x-version": 28,
+ "x-version": 29,
"google-talk": {
"mime_types": [
],
@@ -80,9 +80,9 @@
],
"versions": [
{
- "version": "28.0.0.161",
+ "version": "29.0.0.140",
"status": "up_to_date",
- "reference": "https://helpx.adobe.com/security/products/flash-player/apsb18-03.html"
+ "reference": "https://helpx.adobe.com/security/products/flash-player/apsb18-08.html"
}
],
"lang": "en-US",
diff --git a/chromium/chrome/browser/resources/plugin_metadata/plugins_mac.json b/chromium/chrome/browser/resources/plugin_metadata/plugins_mac.json
index 5a7f063708f..a4feba50406 100644
--- a/chromium/chrome/browser/resources/plugin_metadata/plugins_mac.json
+++ b/chromium/chrome/browser/resources/plugin_metadata/plugins_mac.json
@@ -1,5 +1,5 @@
{
- "x-version": 34,
+ "x-version": 35,
"google-talk": {
"mime_types": [
],
@@ -115,9 +115,9 @@
],
"versions": [
{
- "version": "28.0.0.161",
+ "version": "29.0.0.140",
"status": "requires_authorization",
- "reference": "https://helpx.adobe.com/security/products/flash-player/apsb18-03.html"
+ "reference": "https://helpx.adobe.com/security/products/flash-player/apsb18-08.html"
}
],
"lang": "en-US",
diff --git a/chromium/chrome/browser/resources/plugin_metadata/plugins_win.json b/chromium/chrome/browser/resources/plugin_metadata/plugins_win.json
index 4d356b256f0..45880e4aefa 100644
--- a/chromium/chrome/browser/resources/plugin_metadata/plugins_win.json
+++ b/chromium/chrome/browser/resources/plugin_metadata/plugins_win.json
@@ -1,5 +1,5 @@
{
- "x-version": 43,
+ "x-version": 44,
"google-talk": {
"mime_types": [
],
@@ -137,9 +137,9 @@
],
"versions": [
{
- "version": "28.0.0.161",
+ "version": "29.0.0.140",
"status": "requires_authorization",
- "reference": "https://helpx.adobe.com/security/products/flash-player/apsb18-03.html"
+ "reference": "https://helpx.adobe.com/security/products/flash-player/apsb18-08.html"
}
],
"lang": "en-US",
diff --git a/chromium/chrome/browser/resources/policy.css b/chromium/chrome/browser/resources/policy.css
index d9b74acf519..45df8882782 100644
--- a/chromium/chrome/browser/resources/policy.css
+++ b/chromium/chrome/browser/resources/policy.css
@@ -36,11 +36,6 @@ html[dir='rtl'] div.left-aligned-button {
float: right;
}
-div.chrome-for-work {
- -webkit-padding-start: 25px;
- display: inline-block;
-}
-
section.status-box-section {
clear: both;
} \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/policy.html b/chromium/chrome/browser/resources/policy.html
index e03b6758dd7..93a74fdf41d 100644
--- a/chromium/chrome/browser/resources/policy.html
+++ b/chromium/chrome/browser/resources/policy.html
@@ -36,10 +36,6 @@
<div class="left-aligned-button">
<button id="export-policies">$i18n{exportPoliciesJSON}</button>
</div>
- <div class="chrome-for-work">
- <a href="http://g.co/chromeent/learn" target="_blank">
- <span>$i18n{chromeForWork}</span></a>
- </div>
<div id="show-unset-container" class="show-unset-checkbox">
<label>
<input id="show-unset" type="checkbox">
diff --git a/chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.html b/chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.html
index ad8c3a7cae5..61edce46557 100644
--- a/chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.html
+++ b/chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.html
@@ -1,50 +1,4 @@
<div id='rpp_enabled'>
- <tabbox id="rpp_data">
- <tabs>
- <tab>URL Table Cache</tab>
- <tab>Host Table Cache</tab>
- <tab>Origin Table Cache</tab>
- </tabs>
- <tabpanels>
- <tabpanel>
- <table>
- <thead>
- <tr>
- <th>Main Frame Url</th>
- <th>Resource Url</th>
- <th>Resource Type</th>
- <th>Num Hits</th>
- <th>Num Misses</th>
- <th>Consec Misses</th>
- <th>Average Position</th>
- <th>Score</th>
- <th>Before FCP</th>
- </tr>
- </thead>
- <tbody id="rpp_url_body">
- </tbody>
- </table>
- </tabpanel>
- <tabpanel>
- <table>
- <thead>
- <tr>
- <th>Host</th>
- <th>Resource Url</th>
- <th>Resource Type</th>
- <th>Num Hits</th>
- <th>Num Misses</th>
- <th>Consec Misses</th>
- <th>Average Position</th>
- <th>Score</th>
- <th>Before FCP</th>
- </tr>
- </thead>
- <tbody id="rpp_host_body">
- </tbody>
- </table>
- </tabpanel>
- <tabpanel>
<table>
<thead>
<tr>
@@ -62,9 +16,6 @@
<tbody id="rpp_origin_body">
</tbody>
</table>
- </tabpanel>
- </tabpanels>
- </tabbox>
</div>
<div id='rpp_disabled'>
Resource prefetch prediction is disabled.
diff --git a/chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.js b/chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.js
index a00b7d58a20..66af67b4186 100644
--- a/chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.js
+++ b/chromium/chrome/browser/resources/predictors/resource_prefetch_predictor.js
@@ -45,62 +45,13 @@ function updateResourcePrefetchPredictorDbView(database) {
$('rpp_enabled').style.display = 'block';
$('rpp_disabled').style.display = 'none';
- var hasUrlData = database.url_db && database.url_db.length > 0;
- var hasHostData = database.host_db && database.host_db.length > 0;
var hasOriginData = database.origin_db && database.origin_db.length > 0;
- if (hasUrlData)
- renderCacheData($('rpp_url_body'), database.url_db);
- if (hasHostData)
- renderCacheData($('rpp_host_body'), database.host_db);
if (hasOriginData)
renderOriginData($('rpp_origin_body'), database.origin_db);
}
/**
- * Renders cache data for URL or host based data.
- * @param {HTMLElement} body element of table to render into.
- * @param {Object} database to render.
- */
-function renderCacheData(body, database) {
- body.textContent = '';
- for (let main of database) {
- for (var j = 0; j < main.resources.length; ++j) {
- var resource = main.resources[j];
- var row = document.createElement('tr');
-
- if (j == 0) {
- var t = document.createElement('td');
- t.rowSpan = main.resources.length;
- t.textContent = truncateString(main.main_frame_url);
- row.appendChild(t);
- }
-
- row.className =
- resource.is_prefetchable ? 'action-prerender' : 'action-none';
-
- row.appendChild(document.createElement('td')).textContent =
- truncateString(resource.resource_url);
- row.appendChild(document.createElement('td')).textContent =
- resource.resource_type;
- row.appendChild(document.createElement('td')).textContent =
- resource.number_of_hits;
- row.appendChild(document.createElement('td')).textContent =
- resource.number_of_misses;
- row.appendChild(document.createElement('td')).textContent =
- resource.consecutive_misses;
- row.appendChild(document.createElement('td')).textContent =
- resource.position;
- row.appendChild(document.createElement('td')).textContent =
- resource.score;
- row.appendChild(document.createElement('td')).textContent =
- resource.before_first_contentful_paint;
- body.appendChild(row);
- }
- }
-}
-
-/**
* Renders the content of the predictor origin table.
* @param {HTMLElement} body element of table to render into.
* @param {Object} database to render.
diff --git a/chromium/chrome/browser/resources/print_preview/OWNERS b/chromium/chrome/browser/resources/print_preview/OWNERS
index ef10fbb92d5..e542f20827d 100644
--- a/chromium/chrome/browser/resources/print_preview/OWNERS
+++ b/chromium/chrome/browser/resources/print_preview/OWNERS
@@ -1,5 +1,3 @@
-dpapad@chromium.org
-gene@chromium.org
-vitalybuka@chromium.org
+file://printing/OWNERS
# COMPONENT: UI>Browser>PrintPreview
diff --git a/chromium/chrome/browser/resources/print_preview/cloud_print_interface.html b/chromium/chrome/browser/resources/print_preview/cloud_print_interface.html
new file mode 100644
index 00000000000..9997dacb339
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/cloud_print_interface.html
@@ -0,0 +1,9 @@
+<link rel="import" href="chrome://resources/html/cr.html">
+<link rel="import" href="native_layer.html">
+<link rel="import" href="data/cloud_parsers.html">
+<link rel="import" href="data/destination.html">
+<link rel="import" href="data/document_info.html">
+<link rel="import" href="data/invitation.html">
+<link rel="import" href="data/user_info.html">
+
+<script src="cloud_print_interface.js"></script>
diff --git a/chromium/chrome/browser/resources/print_preview/data/cloud_parsers.html b/chromium/chrome/browser/resources/print_preview/data/cloud_parsers.html
new file mode 100644
index 00000000000..12f99ebed56
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/data/cloud_parsers.html
@@ -0,0 +1,5 @@
+<link rel="import" href="chrome://resources/html/cr.html">
+<link rel="import" href="destination.html">
+<link rel="import" href="invitation.html">
+
+<script src="cloud_parsers.js"></script>
diff --git a/chromium/chrome/browser/resources/print_preview/data/destination.js b/chromium/chrome/browser/resources/print_preview/data/destination.js
index 46f97789af1..a3b1e5627d0 100644
--- a/chromium/chrome/browser/resources/print_preview/data/destination.js
+++ b/chromium/chrome/browser/resources/print_preview/data/destination.js
@@ -71,10 +71,40 @@ print_preview.DestinationCertificateStatus = {
};
/**
+ * @typedef {{
+ * display_name: (string),
+ * type: (string | undefined),
+ * value: (number | string | boolean),
+ * is_default: (boolean | undefined),
+ * }}
+ */
+print_preview.VendorCapabilitySelectOption;
+
+/**
+ * Specifies a custom vendor capability.
+ * @typedef {{
+ * id: (string),
+ * display_name: (string),
+ * localized_display_name: (string | undefined),
+ * type: (string),
+ * select_cap: ({
+ * option: (Array<!print_preview.VendorCapabilitySelectOption>|undefined),
+ * }|undefined),
+ * typed_value_cap: ({
+ * default: (number | string | boolean | undefined),
+ * }|undefined),
+ * range_cap: ({
+ * default: (number),
+ * }),
+ * }}
+ */
+print_preview.VendorCapability;
+
+/**
* Capabilities of a print destination represented in a CDD.
*
* @typedef {{
- * vendor_capability: !Array<{Object}>,
+ * vendor_capability: !Array<!print_preview.VendorCapability>,
* collate: ({default: (boolean|undefined)}|undefined),
* color: ({
* option: !Array<{
@@ -537,10 +567,18 @@ cr.define('print_preview', function() {
}
/**
+ * @return {boolean} Whether the destination is offline or has an invalid
+ * certificate.
+ */
+ get isOfflineOrInvalid() {
+ return this.isOffline || this.shouldShowInvalidCertificateError;
+ }
+
+ /**
* @return {string} Human readable status for a destination that is offline
* or has a bad certificate. */
get connectionStatusText() {
- if (!this.isOffline && !this.shouldShowInvalidCertificateError)
+ if (!this.isOfflineOrInvalid)
return '';
const offlineDurationMs = Date.now() - this.lastAccessTime_;
let statusMessageId;
@@ -781,7 +819,6 @@ cr.define('print_preview', function() {
LOCAL_2X: 'images/2x/printer.png',
MOBILE: 'images/mobile.png',
MOBILE_SHARED: 'images/mobile_shared.png',
- THIRD_PARTY: 'images/third_party.png',
PDF: 'images/pdf.png',
DOCS: 'images/google_doc.png',
ENTERPRISE: 'images/business.svg'
diff --git a/chromium/chrome/browser/resources/print_preview/data/destination_store.html b/chromium/chrome/browser/resources/print_preview/data/destination_store.html
index ff0c3f8f95c..67f7dbc63f8 100644
--- a/chromium/chrome/browser/resources/print_preview/data/destination_store.html
+++ b/chromium/chrome/browser/resources/print_preview/data/destination_store.html
@@ -1,7 +1,4 @@
<link rel="import" href="chrome://resources/html/cr.html">
-<link rel="import" href="chrome://resources/html/event_tracker.html">
-<link rel="import" href="chrome://resources/html/webui_listener_tracker.html">
-<link rel="import" href="chrome://resources/html/cr/event_target.html">
<link rel="import" href="../metrics.html">
<link rel="import" href="../native_layer.html">
<link rel="import" href="destination.html">
diff --git a/chromium/chrome/browser/resources/print_preview/data/invitation.html b/chromium/chrome/browser/resources/print_preview/data/invitation.html
new file mode 100644
index 00000000000..0a72be7c1ea
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/data/invitation.html
@@ -0,0 +1,4 @@
+<link rel="import" href="chrome://resources/html/cr.html">
+<link rel="import" href="destination.html">
+
+<script src="invitation.js"></script>
diff --git a/chromium/chrome/browser/resources/print_preview/images/2x/printer.png b/chromium/chrome/browser/resources/print_preview/images/2x/printer.png
index b704e02f841..6bd2a925be3 100644
--- a/chromium/chrome/browser/resources/print_preview/images/2x/printer.png
+++ b/chromium/chrome/browser/resources/print_preview/images/2x/printer.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/print_preview/images/2x/printer_shared.png b/chromium/chrome/browser/resources/print_preview/images/2x/printer_shared.png
index bbddfd04d2c..f27e672f7b9 100644
--- a/chromium/chrome/browser/resources/print_preview/images/2x/printer_shared.png
+++ b/chromium/chrome/browser/resources/print_preview/images/2x/printer_shared.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/print_preview/images/third_party.png b/chromium/chrome/browser/resources/print_preview/images/third_party.png
deleted file mode 100644
index d15552d390c..00000000000
--- a/chromium/chrome/browser/resources/print_preview/images/third_party.png
+++ /dev/null
Binary files differ
diff --git a/chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.html b/chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.html
index 554ba1337a4..5ecb91c14e2 100644
--- a/chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.html
@@ -1,7 +1,11 @@
<link rel="import" href="chrome://resources/html/polymer.html">
+<link rel="import" href="chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.html">
+<link rel="import" href="../data/destination.html">
+<link rel="import" href="advanced_settings_dialog.html">
<link rel="import" href="button_css.html">
<link rel="import" href="print_preview_shared_css.html">
+<link rel="import" href="settings_behavior.html">
<link rel="import" href="settings_section.html">
<dom-module id="print-preview-advanced-options-settings">
@@ -15,9 +19,16 @@
<print-preview-settings-section>
<span slot="title">$i18n{advancedOptionsLabel}</span>
<div slot="controls">
- <button>$i18n{showAdvancedOptions}</button>
+ <button disabled$="[[disabled]]" on-click="onButtonClick_">
+ $i18n{showAdvancedOptions}
+ </button>
</div>
</print-preview-settings-section>
+ <template is="cr-lazy-render" id="advancedDialog">
+ <print-preview-advanced-dialog
+ settings="{{settings}}" destination="[[destination]]">
+ </print-preview-advanced-dialog>
+ </template>
</template>
<script src="advanced_options_settings.js"></script>
</dom-module>
diff --git a/chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.js b/chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.js
index 6df05eb6896..b47ab004d75 100644
--- a/chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/advanced_options_settings.js
@@ -4,4 +4,23 @@
Polymer({
is: 'print-preview-advanced-options-settings',
+
+ behaviors: [SettingsBehavior],
+
+ properties: {
+ disabled: Boolean,
+
+ /** @type {!print_preview.Destination} */
+ destination: Object,
+ },
+
+ /** @private */
+ onButtonClick_: function() {
+ const dialog = this.$.advancedDialog.get();
+ // This async() call is a workaround to prevent a DCHECK - see
+ // https://crbug.com/804047.
+ this.async(() => {
+ dialog.show();
+ }, 1);
+ },
});
diff --git a/chromium/chrome/browser/resources/print_preview/new/advanced_settings_dialog.html b/chromium/chrome/browser/resources/print_preview/new/advanced_settings_dialog.html
new file mode 100644
index 00000000000..91e920b5383
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/advanced_settings_dialog.html
@@ -0,0 +1,47 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/cr_elements/cr_dialog/cr_dialog.html">
+<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
+<link rel="import" href="chrome://resources/html/i18n_behavior.html">
+<link rel="import" href="../data/destination.html">
+<link rel="import" href="advanced_settings_item.html">
+<link rel="import" href="settings_behavior.html">
+<link rel="import" href="button_css.html">
+<link rel="import" href="print_preview_shared_css.html">
+<link rel="import" href="search_dialog_css.html">
+
+<dom-module id="print-preview-advanced-dialog">
+ <style include="print-preview-shared search-dialog button cr-hidden-style">
+ </style>
+ <template>
+ <dialog is="cr-dialog" id="dialog" on-close="onCloseOrCancel_">
+ <div slot="title">
+ <div>[[i18n('advancedSettingsDialogTitle', destination.displayName)]]
+ </div>
+ <print-preview-search-box id="searchBox"
+ label="$i18n{advancedSettingsSearchBoxPlaceholder}"
+ search-query="{{searchQuery_}}">
+ </print-preview-search-box>
+ </div>
+ <div slot="body">
+ <template is="dom-repeat"
+ items="[[destination.capabilities.printer.vendor_capability]]">
+ <print-preview-advanced-settings-item capability="[[item]]"
+ settings="[[settings]]">
+ </print-preview-advanced-settings-item>
+ </template>
+ <div class="no-settings-match-hint"
+ hidden$="[[!shouldShowHint_(hasMatching_)]]">
+ $i18n{noAdvancedSettingsMatchSearchHint}
+ </div>
+ </div>
+ <div slot="button-container">
+ <button on-click="onCancelButtonClick_">$i18n{cancel}</button>
+ <button on-click="onApplyButtonClick_">
+ $i18n{advancedSettingsDialogConfirm}
+ </button>
+ </div>
+ </dialog>
+ </template>
+ <script src="advanced_settings_dialog.js"></script>
+</dom-module>
diff --git a/chromium/chrome/browser/resources/print_preview/new/advanced_settings_dialog.js b/chromium/chrome/browser/resources/print_preview/new/advanced_settings_dialog.js
new file mode 100644
index 00000000000..989de5b0285
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/advanced_settings_dialog.js
@@ -0,0 +1,82 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+Polymer({
+ is: 'print-preview-advanced-dialog',
+
+ behaviors: [SettingsBehavior, I18nBehavior],
+
+ properties: {
+ /** @type {!print_preview.Destination} */
+ destination: Object,
+
+ /** @private {?RegExp} */
+ searchQuery_: {
+ type: Object,
+ value: null,
+ },
+
+ /** @private {boolean} */
+ hasMatching_: {
+ type: Boolean,
+ notify: true,
+ computed: 'computeHasMatching_(searchQuery_)',
+ },
+ },
+
+ /**
+ * @return {boolean} Whether there is a setting matching the query.
+ * @private
+ */
+ computeHasMatching_: function() {
+ const listItems = this.shadowRoot.querySelectorAll(
+ 'print-preview-advanced-settings-item');
+ let hasMatch = false;
+ listItems.forEach(item => {
+ const matches = item.hasMatch(this.searchQuery_);
+ item.hidden = !matches;
+ hasMatch = hasMatch || matches;
+ item.updateHighlighting(this.searchQuery_);
+ });
+ return hasMatch;
+ },
+
+ /**
+ * @return {boolean} Whether the no matching settings hint should be shown.
+ * @private
+ */
+ shouldShowHint_: function() {
+ return !!this.searchQuery_ && !this.hasMatching_;
+ },
+
+ /** @private */
+ onCloseOrCancel_: function() {
+ if (this.searchQuery_)
+ this.$.searchBox.setValue('');
+ },
+
+ /** @private */
+ onCancelButtonClick_: function() {
+ this.$.dialog.cancel();
+ },
+
+ /** @private */
+ onApplyButtonClick_: function() {
+ const settingsValues = {};
+ this.shadowRoot.querySelectorAll('print-preview-advanced-settings-item')
+ .forEach(item => {
+ settingsValues[item.capability.id] = item.getCurrentValue();
+ });
+ this.setSetting('vendorItems', settingsValues);
+ this.$.dialog.close();
+ },
+
+ show: function() {
+ this.$.dialog.showModal();
+ },
+
+ close: function() {
+ this.$.dialog.close();
+ },
+});
diff --git a/chromium/chrome/browser/resources/print_preview/new/advanced_settings_item.html b/chromium/chrome/browser/resources/print_preview/new/advanced_settings_item.html
new file mode 100644
index 00000000000..af531c738fa
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/advanced_settings_item.html
@@ -0,0 +1,68 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/cr_elements/search_highlight_style_css.html">
+<link rel="import" href="../print_preview_utils.html">
+<link rel="import" href="../data/destination.html">
+<link rel="import" href="highlight_utils.html">
+<link rel="import" href="print_preview_shared_css.html">
+<link rel="import" href="select_css.html">
+<link rel="import" href="settings_behavior.html">
+
+<dom-module id="print-preview-advanced-settings-item">
+ <style include="print-preview-shared select search-highlight-style">
+ :host {
+ display: flex;
+ position: relative;
+ }
+
+ :host > * {
+ overflow: hidden;
+ padding-bottom: 15px;
+ padding-top: 10px;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ white-space: nowrap;
+ }
+
+ :host .label {
+ -webkit-padding-end: 20px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ width: 250px;
+ }
+
+ :host .value {
+ width: 175px;
+ }
+
+ :host input {
+ height: 28px;
+ line-height: 24px;
+ width: 175px;
+ }
+ </style>
+ <template>
+ <label class="label searchable">[[getDisplayName_(capability)]]</label>
+ <div class="value">
+ <template is="dom-if" if="[[isCapabilityTypeSelect_(capability)]]"
+ restamp>
+ <div>
+ <select on-change="onUserInput_">
+ <template is="dom-repeat" items="[[capability.select_cap.option]]">
+ <option class="searchable" text="[[getDisplayName_(item)]]"
+ value="[[item.value]]"
+ selected="[[isOptionSelected_(item, currentValue_)]]">
+ </option>
+ </template>
+ </select>
+ </div>
+ </template>
+ <span hidden$="[[isCapabilityTypeSelect_(capability)]]">
+ <input type="text" on-input="onUserInput_"
+ placeholder="[[getCapabilityPlaceholder_(capability)]]">
+ </span>
+ </div>
+ </template>
+ <script src="advanced_settings_item.js"></script>
+</dom-module>
diff --git a/chromium/chrome/browser/resources/print_preview/new/advanced_settings_item.js b/chromium/chrome/browser/resources/print_preview/new/advanced_settings_item.js
new file mode 100644
index 00000000000..3de026d756b
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/advanced_settings_item.js
@@ -0,0 +1,154 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+Polymer({
+ is: 'print-preview-advanced-settings-item',
+
+ behaviors: [SettingsBehavior],
+
+ properties: {
+ /** @type {!print_preview.VendorCapability} */
+ capability: Object,
+
+ /** @private {(number | string | boolean)} */
+ currentValue_: {
+ type: Object,
+ value: null,
+ },
+ },
+
+ observers: [
+ 'updateFromSettings_(capability, settings.vendorItems.value)',
+ ],
+
+ /** @private {boolean} */
+ highlighted_: false,
+
+ /** @private */
+ updateFromSettings_: function() {
+ const settings = this.getSetting('vendorItems').value;
+
+ // The settings may not have a property with the id if they were populated
+ // from sticky settings from a different destination or if the
+ // destination's capabilities changed since the sticky settings were
+ // generated.
+ if (!settings.hasOwnProperty(this.capability.id))
+ return;
+
+ const value = settings[this.capability.id];
+ if (this.isCapabilityTypeSelect_()) {
+ // Ignore a value that can't be selected.
+ if (this.hasOptionWithValue_(value))
+ this.currentValue_ = value;
+ } else {
+ this.currentValue_ = value;
+ this.$$('input[type="text"]').value = this.currentValue_;
+ }
+ },
+
+ /**
+ * @param {!print_preview.VendorCapability |
+ * !print_preview.VendorCapabilitySelectOption} item
+ * @return {string} The display name for the setting.
+ * @private
+ */
+ getDisplayName_: function(item) {
+ let displayName = item.display_name;
+ if (!displayName && item.display_name_localized)
+ displayName = getStringForCurrentLocale(item.display_name_localized);
+ return displayName || '';
+ },
+
+ /**
+ * @return {boolean} Whether the capability represented by this item is
+ * of type select.
+ * @private
+ */
+ isCapabilityTypeSelect_: function() {
+ return this.capability.type == 'SELECT';
+ },
+
+ /**
+ * @param {!print_preview.VendorCapabilitySelectOption} option The option
+ * for a select capability.
+ * @return {boolean} Whether the option is selected.
+ * @private
+ */
+ isOptionSelected_: function(option) {
+ return (this.currentValue_ !== null &&
+ option.value === this.currentValue_) ||
+ (this.currentValue_ == null && !!option.is_default);
+ },
+
+ /**
+ * @return {string} The placeholder value for the capability's text input.
+ * @private
+ */
+ getCapabilityPlaceholder_: function() {
+ if (this.capability.type == 'TYPED_VALUE' &&
+ this.capability.typed_value_cap &&
+ this.capability.typed_value_cap.default != undefined) {
+ return this.capability.typed_value_cap.default.toString() || '';
+ }
+ if (this.capability.type == 'RANGE' && this.capability.range_cap &&
+ this.capability.range_cap.default != undefined)
+ return this.capability.range_cap.default.toString() || '';
+ return '';
+ },
+
+ /**
+ * @return {boolean}
+ * @private
+ */
+ hasOptionWithValue_: function(value) {
+ return !!this.capability.select_cap &&
+ !!this.capability.select_cap.option &&
+ this.capability.select_cap.option.some(option => option.value == value);
+ },
+
+ /**
+ * @param {?RegExp} query The current search query.
+ * @return {boolean} Whether the item has a match for the query.
+ */
+ hasMatch: function(query) {
+ if (!query || this.getDisplayName_(this.capability).match(query))
+ return true;
+
+ if (!this.isCapabilityTypeSelect_())
+ return false;
+
+ for (let option of
+ /** @type {!Array<!print_preview.VendorCapabilitySelectOption>} */ (
+ this.capability.select_cap.option)) {
+ if (this.getDisplayName_(option).match(query))
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * @param {!Event} e Event containing the new value.
+ * @private
+ */
+ onUserInput_: function(e) {
+ this.currentValue_ = e.target.value;
+ },
+
+ /**
+ * @return {(number | string | boolean)} The current value of the setting.
+ */
+ getCurrentValue: function() {
+ return this.currentValue_;
+ },
+
+ /**
+ * @param {?RegExp} query The current search query.
+ * @return {boolean} Whether the current query is a match for this item.
+ */
+ updateHighlighting: function(query) {
+ this.highlighted_ =
+ print_preview.updateHighlights(this, query, this.highlighted_);
+ return this.highlighted_ || !query;
+ },
+});
diff --git a/chromium/chrome/browser/resources/print_preview/new/app.html b/chromium/chrome/browser/resources/print_preview/new/app.html
index fd29781eb45..dbfe5590f85 100644
--- a/chromium/chrome/browser/resources/print_preview/new/app.html
+++ b/chromium/chrome/browser/resources/print_preview/new/app.html
@@ -2,6 +2,7 @@
<link rel="import" href="chrome://resources/html/event_tracker.html">
<link rel="import" href="chrome://resources/html/webui_listener_tracker.html">
+<link rel="import" href="../cloud_print_interface.html">
<link rel="import" href="../native_layer.html">
<link rel="import" href="../data/destination.html">
<link rel="import" href="../data/destination_store.html">
@@ -9,6 +10,7 @@
<link rel="import" href="../data/measurement_system.html">
<link rel="import" href="../data/user_info.html">
<link rel="import" href="settings_behavior.html">
+<link rel="import" href="state.html">
<link rel="import" href="model.html">
<link rel="import" href="header.html">
<link rel="import" href="preview_area.html">
@@ -48,64 +50,82 @@
overflow: auto;
}
- #preview-area {
+ #preview-area-container {
-webkit-border-start: 1px solid #dcdcdc;
align-items: center;
background-color: #e6e6e6;
flex: 1;
}
</style>
+ <print-preview-state id="state" state="{{state}}"></print-preview-state>
<print-preview-model id="model" settings="{{settings}}"
destination="{{destination_}}" document-info="{{documentInfo_}}"
recent-destinations="{{recentDestinations_}}"
on-save-sticky-settings="onSaveStickySettings_">
</print-preview-model>
- <div id="sidebar">
- <print-preview-header destination="[[destination_]]" state="{{state_}}"
- settings="[[settings]]"></print-preview-header>
+ <div id="sidebar" on-setting-valid-changed="onSettingValidChanged_">
+ <print-preview-header destination="[[destination_]]" state="[[state]]"
+ error-message="[[errorMessage_]]" settings="[[settings]]"
+ on-print-requested="onPrintRequested_"
+ on-cancel-requested="onCancelRequested_">
+ </print-preview-header>
<div id="settings-sections">
- <print-preview-destination-settings destination="[[destination_]]">
+ <print-preview-destination-settings id="destinationSettings"
+ destination="[[destination_]]"
+ destination-store="[[destinationStore_]]"
+ disabled="[[controlsDisabled_]]" state="[[state]]"
+ recent-destinations="[[recentDestinations_]]"
+ user-info="{{userInfo_}}">
</print-preview-destination-settings>
<print-preview-pages-settings settings="{{settings}}"
- document-info="[[documentInfo_]]"
+ document-info="[[documentInfo_]]" disabled="[[controlsDisabled_]]"
hidden$="[[!settings.pages.available]]">
</print-preview-pages-settings>
<print-preview-copies-settings settings="{{settings}}"
+ disabled="[[controlsDisabled_]]"
hidden$="[[!settings.copies.available]]">
</print-preview-copies-settings>
<print-preview-layout-settings settings="{{settings}}"
+ disabled="[[controlsDisabled_]]"
hidden$="[[!settings.layout.available]]">
</print-preview-layout-settings>
<print-preview-color-settings settings="{{settings}}"
+ disabled="[[controlsDisabled_]]"
hidden$="[[!settings.color.available]]">
</print-preview-color-settings>
<print-preview-media-size-settings settings="{{settings}}"
capability="[[destination_.capabilities.printer.media_size]]"
+ disabled="[[controlsDisabled_]]"
hidden$="[[!settings.mediaSize.available]]">
</print-preview-media-size-settings>
<print-preview-margins-settings settings="{{settings}}"
+ disabled="[[controlsDisabled_]]"
hidden$="[[!settings.margins.available]]">
</print-preview-margins-settings>
<print-preview-dpi-settings settings="{{settings}}"
capability="[[destination_.capabilities.printer.dpi]]"
+ disabled="[[controlsDisabled_]]"
hidden$="[[!settings.dpi.available]]">
</print-preview-dpi-settings>
<print-preview-scaling-settings settings="{{settings}}"
- document-info="[[documentInfo_]]"
+ document-info="[[documentInfo_]]" disabled="[[controlsDisabled_]]"
hidden$="[[!settings.scaling.available]]">
</print-preview-scaling-settings>
<print-preview-other-options-settings settings="{{settings}}"
+ disabled="[[controlsDisabled_]]"
hidden$="[[!settings.otherOptions.available]]">
</print-preview-other-options-settings>
<print-preview-advanced-options-settings settings="{{settings}}"
+ destination="[[destination_]]" disabled="[[controlsDisabled_]]"
hidden$="[[!settings.vendorItems.available]]">
</print-preview-advanced-options-settings>
</div>
</div>
- <div id="preview-area">
- <print-preview-preview-area settings="{{settings}}"
+ <div id="preview-area-container">
+ <print-preview-preview-area id="previewArea" settings="{{settings}}"
destination="[[destination_]]" document-info="{{documentInfo_}}"
- state="{{state_}}">
+ state="[[state]]" on-preview-failed="onPreviewFailed_"
+ on-preview-loaded="onPreviewLoaded_">
</print-preview-preview-area>
</div>
</template>
diff --git a/chromium/chrome/browser/resources/print_preview/new/app.js b/chromium/chrome/browser/resources/print_preview/new/app.js
index c45886a2ec6..0e4dcebab1d 100644
--- a/chromium/chrome/browser/resources/print_preview/new/app.js
+++ b/chromium/chrome/browser/resources/print_preview/new/app.js
@@ -17,14 +17,21 @@ Polymer({
notify: true,
},
- /** @private {print_preview.DocumentInfo} */
- documentInfo_: {
+ /** @private {print_preview.Destination} */
+ destination_: {
type: Object,
notify: true,
},
- /** @private {print_preview.Destination} */
- destination_: {
+ /** @private {?print_preview.DestinationStore} */
+ destinationStore_: {
+ type: Object,
+ notify: true,
+ value: null,
+ },
+
+ /** @private {print_preview.DocumentInfo} */
+ documentInfo_: {
type: Object,
notify: true,
},
@@ -35,40 +42,55 @@ Polymer({
notify: true,
},
- /** @private {!print_preview_new.State} */
- state_: {
+ /** @type {!print_preview_new.State} */
+ state: {
+ type: Number,
+ observer: 'onStateChanged_',
+ },
+
+ /** @private {?print_preview.UserInfo} */
+ userInfo_: {
type: Object,
notify: true,
- value: {
- previewLoading: false,
- previewFailed: false,
- cloudPrintError: '',
- privetExtensionError: '',
- invalidSettings: false,
- initialized: false,
- cancelled: false,
- },
+ value: null,
},
- },
- /** @private {?print_preview.NativeLayer} */
- nativeLayer_: null,
+ /** @private {string} */
+ errorMessage_: {
+ type: String,
+ notify: true,
+ value: '',
+ },
- /** @private {?print_preview.UserInfo} */
- userInfo_: null,
+ /** @private {boolean} */
+ controlsDisabled_: {
+ type: Boolean,
+ notify: true,
+ computed: 'computeControlsDisabled_(state)',
+ }
+ },
/** @private {?WebUIListenerTracker} */
listenerTracker_: null,
- /** @private {?print_preview.DestinationStore} */
- destinationStore_: null,
+ /** @type {!print_preview.MeasurementSystem} */
+ measurementSystem_: new print_preview.MeasurementSystem(
+ ',', '.', print_preview.MeasurementSystemUnitType.IMPERIAL),
+
+ /** @private {?print_preview.NativeLayer} */
+ nativeLayer_: null,
+
+ /** @private {?cloudprint.CloudPrintInterface} */
+ cloudPrintInterface_: null,
/** @private {!EventTracker} */
tracker_: new EventTracker(),
- /** @type {!print_preview.MeasurementSystem} */
- measurementSystem_: new print_preview.MeasurementSystem(
- ',', '.', print_preview.MeasurementSystemUnitType.IMPERIAL),
+ /** @private {boolean} */
+ cancelled_: false,
+
+ /** @private {boolean} */
+ isInAppKioskMode_: false,
/** @override */
attached: function() {
@@ -76,6 +98,8 @@ Polymer({
this.documentInfo_ = new print_preview.DocumentInfo();
this.userInfo_ = new print_preview.UserInfo();
this.listenerTracker_ = new WebUIListenerTracker();
+ this.listenerTracker_.add(
+ 'use-cloud-print', this.onCloudPrintEnable_.bind(this));
this.destinationStore_ = new print_preview.DestinationStore(
this.userInfo_, this.listenerTracker_);
this.tracker_.add(
@@ -98,6 +122,14 @@ Polymer({
},
/**
+ * @return {boolean} Whether the controls should be disabled.
+ * @private
+ */
+ computeControlsDisabled_: function() {
+ return this.state != print_preview_new.State.READY;
+ },
+
+ /**
* @param {!print_preview.NativeInitialSettings} settings
* @private
*/
@@ -109,7 +141,7 @@ Polymer({
this.notifyPath('documentInfo_.hasSelection');
this.notifyPath('documentInfo_.title');
this.notifyPath('documentInfo_.pageCount');
- this.$.model.updateFromStickySettings(settings.serializedAppStateStr);
+ this.$.model.setStickySettings(settings.serializedAppStateStr);
this.measurementSystem_.setSystem(
settings.thousandsDelimeter, settings.decimalDelimeter,
settings.unitType);
@@ -120,9 +152,42 @@ Polymer({
this.recentDestinations_);
},
+ /**
+ * Called when Google Cloud Print integration is enabled by the
+ * PrintPreviewHandler.
+ * Fetches the user's cloud printers.
+ * @param {string} cloudPrintUrl The URL to use for cloud print servers.
+ * @param {boolean} appKioskMode Whether to print automatically for kiosk
+ * mode.
+ * @private
+ */
+ onCloudPrintEnable_: function(cloudPrintUrl, appKioskMode) {
+ assert(!this.cloudPrintInterface_);
+ this.cloudPrintInterface_ = new cloudprint.CloudPrintInterface(
+ cloudPrintUrl, assert(this.nativeLayer_), assert(this.userInfo_),
+ appKioskMode);
+ this.tracker_.add(
+ assert(this.cloudPrintInterface_),
+ cloudprint.CloudPrintInterfaceEventType.SUBMIT_DONE,
+ this.close_.bind(this));
+ [cloudprint.CloudPrintInterfaceEventType.SEARCH_FAILED,
+ cloudprint.CloudPrintInterfaceEventType.SUBMIT_FAILED,
+ cloudprint.CloudPrintInterfaceEventType.PRINTER_FAILED,
+ ].forEach(eventType => {
+ this.tracker_.add(
+ assert(this.cloudPrintInterface_), eventType,
+ this.onCloudPrintError_.bind(this));
+ });
+
+ this.destinationStore_.setCloudPrintInterface(this.cloudPrintInterface_);
+ if (this.$.destinationSettings.isDialogOpen())
+ this.destinationStore_.startLoadCloudDestinations();
+ },
+
/** @private */
onDestinationSelect_: function() {
this.destination_ = this.destinationStore_.selectedDestination;
+ this.$.state.transitTo(print_preview_new.State.NOT_READY);
},
/** @private */
@@ -130,16 +195,10 @@ Polymer({
this.set(
'destination_.capabilities',
this.destinationStore_.selectedDestination.capabilities);
- if (!this.state_.initialized)
- this.set('state_.initialized', true);
- },
-
- /** @private */
- onPreviewCancelled_: function() {
- if (!this.state_.cancelled)
- return;
- this.detached();
- this.nativeLayer_.dialogClose(true);
+ if (this.state != print_preview_new.State.READY)
+ this.$.state.transitTo(print_preview_new.State.READY);
+ if (!this.$.model.initialized())
+ this.$.model.applyStickySettings();
},
/**
@@ -149,4 +208,128 @@ Polymer({
onSaveStickySettings_: function(e) {
this.nativeLayer_.saveAppState(/** @type {string} */ (e.detail));
},
+
+ /** @private */
+ onStateChanged_: function() {
+ if (this.state == print_preview_new.State.CLOSING) {
+ this.remove();
+ this.nativeLayer_.dialogClose(this.cancelled_);
+ } else if (this.state == print_preview_new.State.HIDDEN) {
+ this.nativeLayer_.hidePreview();
+ } else if (this.state == print_preview_new.State.PRINTING) {
+ const destination = assert(this.destinationStore_.selectedDestination);
+ const whenPrintDone =
+ this.nativeLayer_.print(this.$.model.createPrintTicket(destination));
+ if (destination.isLocal) {
+ const onError = destination.id ==
+ print_preview.Destination.GooglePromotedId.SAVE_AS_PDF ?
+ this.onFileSelectionCancel_.bind(this) :
+ this.onPrintFailed_.bind(this);
+ whenPrintDone.then(this.close_.bind(this), onError);
+ } else {
+ // Cloud print resolves when print data is returned to submit to cloud
+ // print, or if print ticket cannot be read, no PDF data is found, or
+ // PDF is oversized.
+ whenPrintDone.then(
+ this.onPrintToCloud_.bind(this), this.onPrintFailed_.bind(this));
+ }
+ }
+ },
+
+ /** @private */
+ onPreviewLoaded_: function() {
+ if (this.state == print_preview_new.State.HIDDEN)
+ this.$.state.transitTo(print_preview_new.State.PRINTING);
+ },
+
+ /** @private */
+ onPrintRequested_: function() {
+ this.$.state.transitTo(
+ this.$.previewArea.previewLoaded() ? print_preview_new.State.PRINTING :
+ print_preview_new.State.HIDDEN);
+ },
+
+ /** @private */
+ onCancelRequested_: function() {
+ this.cancelled_ = true;
+ this.$.state.transitTo(print_preview_new.State.CLOSING);
+ },
+
+ /**
+ * @param {!CustomEvent} e The event containing the new validity.
+ * @private
+ */
+ onSettingValidChanged_: function(e) {
+ this.$.state.transitTo(
+ /** @type {boolean} */ (e.detail) ?
+ print_preview_new.State.READY :
+ print_preview_new.State.INVALID_TICKET);
+ },
+
+ /** @private */
+ onFileSelectionCancel_: function() {
+ this.$.state.transitTo(print_preview_new.State.READY);
+ },
+
+ /**
+ * Called when the native layer has retrieved the data to print to Google
+ * Cloud Print.
+ * @param {string} data The body to send in the HTTP request.
+ * @private
+ */
+ onPrintToCloud_: function(data) {
+ assert(
+ this.cloudPrintInterface_ != null, 'Google Cloud Print is not enabled');
+ const destination = assert(this.destinationStore_.selectedDestination);
+ this.cloudPrintInterface_.submit(
+ destination, this.$.model.createCloudJobTicket(destination),
+ assert(this.documentInfo_), data);
+ },
+
+ /**
+ * Called when printing to a privet, cloud, or extension printer fails.
+ * @param {*} httpError The HTTP error code, or -1 or a string describing
+ * the error, if not an HTTP error.
+ * @private
+ */
+ onPrintFailed_: function(httpError) {
+ console.error('Printing failed with error code ' + httpError);
+ this.errorMessage_ = httpError.toString();
+ this.$.state.transitTo(print_preview_new.State.FATAL_ERROR);
+ },
+
+ /** @private */
+ onPreviewFailed_: function() {
+ this.$.state.transitTo(print_preview_new.State.FATAL_ERROR);
+ },
+
+ /**
+ * Called when there was an error communicating with Google Cloud print.
+ * Displays an error message in the print header.
+ * @param {!Event} event Contains the error message.
+ * @private
+ */
+ onCloudPrintError_: function(event) {
+ if (event.status == 0) {
+ return; // Ignore, the system does not have internet connectivity.
+ }
+ if (event.status == 403) {
+ if (!this.isInAppKioskMode_) {
+ this.$.destinationSettings.showCloudPrintPromo();
+ }
+ } else {
+ this.set('state_.cloudPrintError', event.message);
+ }
+ if (event.status == 200) {
+ console.error(
+ `Google Cloud Print Error: (${event.errorCode}) ${event.message}`);
+ } else {
+ console.error(`Google Cloud Print Error: HTTP status ${event.status}`);
+ }
+ },
+
+ /** @private */
+ close_: function() {
+ this.$.state.transitTo(print_preview_new.State.CLOSING);
+ },
});
diff --git a/chromium/chrome/browser/resources/print_preview/new/button_css.html b/chromium/chrome/browser/resources/print_preview/new/button_css.html
index d33555445a4..77481fa255a 100644
--- a/chromium/chrome/browser/resources/print_preview/new/button_css.html
+++ b/chromium/chrome/browser/resources/print_preview/new/button_css.html
@@ -13,13 +13,13 @@
<if expr="not is_ios">
button:enabled:hover {
background-image: linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
- @apply(--print-preview-hover);
+ @apply --print-preview-hover;
}
</if>
button:enabled:active {
background-image: linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
- @apply(--print-preview-active);
+ @apply --print-preview-active;
}
</style>
</template>
diff --git a/chromium/chrome/browser/resources/print_preview/new/checkbox_radio_css.html b/chromium/chrome/browser/resources/print_preview/new/checkbox_radio_css.html
index f2564079cbd..159ad3c3dbb 100644
--- a/chromium/chrome/browser/resources/print_preview/new/checkbox_radio_css.html
+++ b/chromium/chrome/browser/resources/print_preview/new/checkbox_radio_css.html
@@ -48,14 +48,14 @@
[type='radio']) {
background-image:
linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
- @apply(--print-preview-hover);
+ @apply --print-preview-hover;
}
</if>
input:enabled:active:-webkit-any([type='checkbox'],
[type='radio']) {
background-image: linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
- @apply(--print-preview-active);
+ @apply --print-preview-active;
}
input:disabled:-webkit-any([type='checkbox'],
diff --git a/chromium/chrome/browser/resources/print_preview/new/color_settings.html b/chromium/chrome/browser/resources/print_preview/new/color_settings.html
index b95459a0f05..88adf84c90c 100644
--- a/chromium/chrome/browser/resources/print_preview/new/color_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/color_settings.html
@@ -11,7 +11,8 @@
<print-preview-settings-section>
<span id="color-label" slot="title">$i18n{optionColor}</span>
<div slot="controls">
- <select aria-labelledby="color-label" on-change="onChange_">
+ <select aria-labelledby="color-label" on-change="onChange_"
+ disabled$="[[disabled]]">
<option value="bw" selected>$i18n{optionBw}</option>
<option value="color">$i18n{optionColor}</option>
</select>
diff --git a/chromium/chrome/browser/resources/print_preview/new/color_settings.js b/chromium/chrome/browser/resources/print_preview/new/color_settings.js
index 075141f2a01..4c8631aa5ec 100644
--- a/chromium/chrome/browser/resources/print_preview/new/color_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/color_settings.js
@@ -7,6 +7,10 @@ Polymer({
behaviors: [SettingsBehavior],
+ properties: {
+ disabled: Boolean,
+ },
+
observers: ['onColorSettingChange_(settings.color.value)'],
/**
diff --git a/chromium/chrome/browser/resources/print_preview/new/compiled_resources2.gyp b/chromium/chrome/browser/resources/print_preview/new/compiled_resources2.gyp
index 0441a3c1e62..e75ada55312 100644
--- a/chromium/chrome/browser/resources/print_preview/new/compiled_resources2.gyp
+++ b/chromium/chrome/browser/resources/print_preview/new/compiled_resources2.gyp
@@ -21,6 +21,7 @@
'preview_area',
'model',
'state',
+ '../compiled_resources2.gyp:cloud_print_interface',
'../compiled_resources2.gyp:native_layer',
'../data/compiled_resources2.gyp:destination',
'../data/compiled_resources2.gyp:destination_store',
@@ -39,6 +40,7 @@
'model',
'settings_behavior',
'state',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
@@ -47,6 +49,11 @@
'target_name': 'destination_settings',
'dependencies': [
'../data/compiled_resources2.gyp:destination',
+ '../data/compiled_resources2.gyp:destination_store',
+ '../data/compiled_resources2.gyp:user_info',
+ 'destination_dialog',
+ 'state',
+ '<(DEPTH)/ui/webui/resources/cr_elements/cr_lazy_render/compiled_resources2.gyp:cr_lazy_render',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
@@ -55,7 +62,6 @@
'dependencies': [
'settings_behavior',
'../data/compiled_resources2.gyp:document_info',
- '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
@@ -126,6 +132,9 @@
{
'target_name': 'advanced_options_settings',
'dependencies': [
+ '../data/compiled_resources2.gyp:destination',
+ 'advanced_settings_dialog',
+ 'settings_behavior',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
@@ -156,6 +165,7 @@
'target_name': 'preview_area',
'dependencies': [
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:i18n_behavior',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:web_ui_listener_behavior',
'../../pdf/compiled_resources2.gyp:pdf_scripting_api',
'../compiled_resources2.gyp:native_layer',
@@ -166,12 +176,80 @@
'../data/compiled_resources2.gyp:size',
'../data/compiled_resources2.gyp:margins',
'../data/compiled_resources2.gyp:printable_area',
+ 'model',
'settings_behavior',
'state',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
{
+ 'target_name': 'destination_dialog',
+ 'dependencies': [
+ '<(DEPTH)/ui/webui/resources/cr_elements/cr_dialog/compiled_resources2.gyp:cr_dialog',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data',
+ '../data/compiled_resources2.gyp:destination',
+ '../data/compiled_resources2.gyp:destination_store',
+ '../data/compiled_resources2.gyp:user_info',
+ 'destination_list',
+ 'print_preview_search_box',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
+ 'target_name': 'destination_list',
+ 'dependencies': [
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:i18n_behavior',
+ '../compiled_resources2.gyp:native_layer',
+ '../data/compiled_resources2.gyp:destination',
+ 'destination_list_item',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
+ 'target_name': 'destination_list_item',
+ 'dependencies': [
+ 'highlight_utils',
+ '../data/compiled_resources2.gyp:destination',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
+ 'target_name': 'advanced_settings_dialog',
+ 'dependencies': [
+ '<(DEPTH)/ui/webui/resources/cr_elements/cr_dialog/compiled_resources2.gyp:cr_dialog',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:i18n_behavior',
+ '../data/compiled_resources2.gyp:destination',
+ 'advanced_settings_item',
+ 'print_preview_search_box',
+ 'settings_behavior',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
+ 'target_name': 'advanced_settings_item',
+ 'dependencies': [
+ 'highlight_utils',
+ '../compiled_resources2.gyp:print_preview_utils',
+ '../data/compiled_resources2.gyp:destination',
+ 'settings_behavior',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
+ 'target_name': 'print_preview_search_box',
+ 'dependencies': [
+ '<(DEPTH)/ui/webui/resources/cr_elements/cr_search_field/compiled_resources2.gyp:cr_search_field_behavior',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
+ 'target_name': 'highlight_utils',
+ 'dependencies': [
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:search_highlight_utils',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
'target_name': 'model',
'dependencies': [
'settings_behavior',
diff --git a/chromium/chrome/browser/resources/print_preview/new/copies_settings.html b/chromium/chrome/browser/resources/print_preview/new/copies_settings.html
index f1168f6f5d8..f64b68121a0 100644
--- a/chromium/chrome/browser/resources/print_preview/new/copies_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/copies_settings.html
@@ -11,12 +11,13 @@
</style>
<print-preview-number-settings-section max-value="999" min-value=1
default-value="1" input-label="$i18n{copiesLabel}"
- input-string="{{inputString_}}" input-valid="{{inputValid_}}"
- hint-message="$i18n{copiesInstruction}">
+ disabled="[[disabled]]" current-value="{{currentValue_}}"
+ input-valid="{{inputValid_}}" hint-message="$i18n{copiesInstruction}">
<div slot="opt-inside-content" class="checkbox" aria-live="polite"
- hidden$="[[collateHidden_(inputString_, inputValid_)]]">
+ hidden$="[[collateHidden_(currentValue_, inputValid_)]]">
<label>
<input id="collate" type="checkbox" on-change="onCollateChange_"
+ disabled$="[[getDisabled(state)]]"
aria-labelledby="copies-collate-label">
<span id="copies-collate-label">$i18n{optionCollate}</span>
</label>
diff --git a/chromium/chrome/browser/resources/print_preview/new/copies_settings.js b/chromium/chrome/browser/resources/print_preview/new/copies_settings.js
index 826c5de2217..1115ac4c99f 100644
--- a/chromium/chrome/browser/resources/print_preview/new/copies_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/copies_settings.js
@@ -9,17 +9,19 @@ Polymer({
properties: {
/** @private {string} */
- inputString_: String,
+ currentValue_: String,
/** @private {boolean} */
inputValid_: Boolean,
+
+ disabled: Boolean,
},
/** @private {boolean} */
isInitialized_: false,
observers: [
- 'onInputChanged_(inputString_, inputValid_)',
+ 'onInputChanged_(currentValue_, inputValid_)',
'onInitialized_(settings.copies.value, settings.collate.value)'
],
@@ -32,7 +34,7 @@ Polymer({
return;
this.isInitialized_ = true;
const copies = this.getSetting('copies');
- this.inputString_ = /** @type {string} */ (copies.value.toString());
+ this.currentValue_ = /** @type {string} */ (copies.value.toString());
const collate = this.getSetting('collate');
this.$.collate.checked = /** @type {boolean} */ (collate.value);
},
@@ -44,7 +46,7 @@ Polymer({
*/
onInputChanged_: function() {
this.setSetting(
- 'copies', this.inputValid_ ? parseInt(this.inputString_, 10) : 1);
+ 'copies', this.inputValid_ ? parseInt(this.currentValue_, 10) : 1);
this.setSettingValid('copies', this.inputValid_);
},
@@ -53,7 +55,7 @@ Polymer({
* @private
*/
collateHidden_: function() {
- return !this.inputValid_ || parseInt(this.inputString_, 10) == 1;
+ return !this.inputValid_ || parseInt(this.currentValue_, 10) == 1;
},
/** @private */
diff --git a/chromium/chrome/browser/resources/print_preview/new/destination_dialog.html b/chromium/chrome/browser/resources/print_preview/new/destination_dialog.html
new file mode 100644
index 00000000000..540227ae57d
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/destination_dialog.html
@@ -0,0 +1,132 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/cr_elements/cr_dialog/cr_dialog.html">
+<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
+<link rel="import" href="chrome://resources/html/action_link_css.html">
+<link rel="import" href="chrome://resources/html/i18n_behavior.html">
+<link rel="import" href="chrome://resources/html/load_time_data.html">
+<link rel="import" href="../data/destination.html">
+<link rel="import" href="../data/destination_store.html">
+<link rel="import" href="button_css.html">
+<link rel="import" href="destination_list.html">
+<link rel="import" href="print_preview_search_box.html">
+<link rel="import" href="print_preview_shared_css.html">
+<link rel="import" href="search_dialog_css.html">
+<link rel="import" href="select_css.html">
+
+<dom-module id="print-preview-destination-dialog">
+ <template>
+ <style include="print-preview-shared button action-link select cr-hidden-style search-dialog">
+ :host #dialog {
+ width: 640px;
+ }
+
+ :host .user-info {
+ font-size: calc(13/15 * 1em);
+ margin-top: 14px;
+ }
+
+ :host .user-info .account-select-label {
+ -webkit-padding-end: 18px;
+ }
+
+ :host .user-info .account-select {
+ width: auto
+ }
+
+ :host #dialog .cloudprint-promo {
+ align-items: center;
+ background-color: #f5f5f5;
+ border-color: #e7e7e7;
+ border-top-style: solid;
+ border-width: 1px;
+ color: #888;
+ display: flex;
+ padding: 14px 17px;
+ }
+
+ :host .cloudprint-promo .promo-text {
+ flex: 1;
+ }
+
+ :host .cloudprint-promo .icon {
+ -webkit-margin-end: 12px;
+ display: block;
+ height: 24px;
+ width: 24px;
+ }
+
+ :host .cloudprint-promo .close-button {
+ -webkit-margin-start: 12px;
+ background-image: -webkit-image-set(
+ url(chrome://theme/IDR_CLOSE_DIALOG) 1x,
+ url(chrome://theme/IDR_CLOSE_DIALOG@2x) 2x);
+ background-repeat: no-repeat;
+ background-size: 14px;
+ height: 14px;
+ width: 14px;
+ }
+
+ :host .cloudprint-promo .close-button:hover {
+ background-image: -webkit-image-set(
+ url(chrome://theme/IDR_CLOSE_DIALOG_H) 1x,
+ url(chrome://theme/IDR_CLOSE_DIALOG_H@2x) 2x);
+ }
+
+ :host .cloudprint-promo .close-button:active {
+ background-image: -webkit-image-set(
+ url(chrome://theme/IDR_CLOSE_DIALOG_P) 1x,
+ url(chrome://theme/IDR_CLOSE_DIALOG_P@2x) 2x);
+ }
+ </style>
+ <dialog is="cr-dialog" id="dialog" on-close="onCloseOrCancel_">
+ <div slot="title">
+ <div>$i18n{destinationSearchTitle}</div>
+ <div class="user-info" hidden$="[[!userInfo.loggedIn]]">
+ <label class="account-select-label" id="accountSelectLabel">
+ $i18n{accountSelectTitle}
+ </label>
+ <select class="account-select" aria-labelledby="accountSelectLabel"
+ on-change="onUserChange_">
+ <template is="dom-repeat" items="[[userInfo.users]]">
+ <option selected="[[isSelected_(item, userInfo.activeUser)]]"
+ value="[[item]]">
+ [[item]]
+ </option>
+ </template>
+ <option value="">$i18n{addAccountTitle}</option>
+ </select>
+ </div>
+ <print-preview-search-box id="searchBox"
+ label="$i18n{searchBoxPlaceholder}" search-query="{{searchQuery_}}">
+ </print-preview-search-box>
+ </div>
+ <div slot="body" scrollable>
+ <print-preview-destination-list
+ destinations="[[recentDestinationList_]]"
+ search-query="[[searchQuery_]]"
+ title="$i18n{recentDestinationsTitle}"
+ on-destination-selected="onDestinationSelected_">
+ </print-preview-destination-list>
+ <print-preview-destination-list destinations="[[destinations_]]"
+ has-action-link loading-destinations="[[loadingDestinations_]]"
+ search-query="[[searchQuery_]]"
+ title="$i18n{printDestinationsTitle}"
+ on-destination-selected="onDestinationSelected_">
+ </print-preview-destination-list>
+ </div>
+ <div slot="button-container">
+ <button class="cancel-button" on-click="onCancelButtonClick_">
+ $i18n{cancel}
+ </button>
+ </div>
+ <div class="cloudprint-promo" slot="footer"
+ hidden$="[[!showCloudPrintPromo]]">
+ <img src="../images/cloud.png" class="icon" alt="">
+ <div class="promo-text"></div>
+ <div class="close-button"></div>
+ </div>
+ </dialog>
+ </template>
+ <script src="destination_dialog.js"></script>
+</dom-module>
diff --git a/chromium/chrome/browser/resources/print_preview/new/destination_dialog.js b/chromium/chrome/browser/resources/print_preview/new/destination_dialog.js
new file mode 100644
index 00000000000..7e2162941c6
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/destination_dialog.js
@@ -0,0 +1,205 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+Polymer({
+ is: 'print-preview-destination-dialog',
+
+ behaviors: [I18nBehavior],
+
+ properties: {
+ /** @type {?print_preview.DestinationStore} */
+ destinationStore: {
+ type: Object,
+ observer: 'onDestinationStoreSet_',
+ },
+
+ /** @type {!print_preview.UserInfo} */
+ userInfo: {
+ type: Object,
+ notify: true,
+ },
+
+ /** @type {boolean} */
+ showCloudPrintPromo: {
+ type: Boolean,
+ notify: true,
+ },
+
+ /** @private {!Array<!print_preview.Destination>} */
+ destinations_: {
+ type: Array,
+ notify: true,
+ value: [],
+ },
+
+ /** @private {boolean} */
+ loadingDestinations_: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @type {!Array<!print_preview.RecentDestination>} */
+ recentDestinations: Array,
+
+ /** @private {!Array<!print_preview.Destination>} */
+ recentDestinationList_: {
+ type: Array,
+ notify: true,
+ computed: 'computeRecentDestinationList_(' +
+ 'destinationStore, recentDestinations, recentDestinations.*, ' +
+ 'userInfo, destinations_.*)',
+ },
+
+ /** @private {?RegExp} */
+ searchQuery_: {
+ type: Object,
+ value: null,
+ },
+ },
+
+ /** @private {!EventTracker} */
+ tracker_: new EventTracker(),
+
+ /** @override */
+ ready: function() {
+ this.$$('.promo-text').innerHTML =
+ this.i18nAdvanced('cloudPrintPromotion', {
+ substitutions: ['<a is="action-link" class="sign-in">', '</a>'],
+ attrs: {
+ 'is': (node, v) => v == 'action-link',
+ 'class': (node, v) => v == 'sign-in',
+ },
+ });
+ },
+
+ /** @override */
+ attached: function() {
+ this.tracker_.add(
+ assert(this.$$('.sign-in')), 'click', this.onSignInClick_.bind(this));
+ this.tracker_.add(
+ assert(this.$$('.cloudprint-promo > .close-button')), 'click',
+ this.onCloudPrintPromoDismissed_.bind(this));
+ },
+
+ /** @private */
+ onDestinationStoreSet_: function() {
+ assert(this.destinations_.length == 0);
+ const destinationStore = assert(this.destinationStore);
+ this.tracker_.add(
+ destinationStore,
+ print_preview.DestinationStore.EventType.DESTINATIONS_INSERTED,
+ this.updateDestinations_.bind(this));
+ this.tracker_.add(
+ destinationStore,
+ print_preview.DestinationStore.EventType.DESTINATION_SEARCH_DONE,
+ this.updateDestinations_.bind(this));
+ },
+
+ /** @private */
+ updateDestinations_: function() {
+ this.notifyPath('userInfo.users');
+ this.notifyPath('userInfo.activeUser');
+ this.notifyPath('userInfo.loggedIn');
+ if (this.userInfo.loggedIn)
+ this.showCloudPrintPromo = false;
+
+ this.destinations_ = this.userInfo ?
+ this.destinationStore.destinations(this.userInfo.activeUser) :
+ [];
+ this.loadingDestinations_ =
+ this.destinationStore.isPrintDestinationSearchInProgress;
+ },
+
+ /**
+ * @return {!Array<!print_preview.Destination>}
+ * @private
+ */
+ computeRecentDestinationList_: function() {
+ let recentDestinations = [];
+ const filterAccount = this.userInfo.activeUser;
+ this.recentDestinations.forEach((recentDestination) => {
+ const destination = this.destinationStore.getDestination(
+ recentDestination.origin, recentDestination.id,
+ recentDestination.account || '');
+ if (destination &&
+ (!destination.account || destination.account == filterAccount)) {
+ recentDestinations.push(destination);
+ }
+ });
+ return recentDestinations;
+ },
+
+ /** @private */
+ onCloseOrCancel_: function() {
+ if (this.searchQuery_)
+ this.$.searchBox.setValue('');
+ },
+
+ /** @private */
+ onCancelButtonClick_: function() {
+ this.$.dialog.cancel();
+ },
+
+ /**
+ * @param {!CustomEvent} e Event containing the selected destination.
+ * @private
+ */
+ onDestinationSelected_: function(e) {
+ this.destinationStore.selectDestination(
+ /** @type {!print_preview.Destination} */ (e.detail));
+ this.$.dialog.close();
+ },
+
+ show: function() {
+ this.loadingDestinations_ =
+ this.destinationStore.isPrintDestinationSearchInProgress;
+ this.$.dialog.showModal();
+ },
+
+ /** @return {boolean} Whether the dialog is open. */
+ isOpen: function() {
+ return this.$.dialog.hasAttribute('open');
+ },
+
+ /** @private */
+ isSelected_: function(account) {
+ return account == this.userInfo.activeUser;
+ },
+
+ /** @private */
+ onSignInClick_: function() {
+ print_preview.NativeLayer.getInstance().signIn(false).then(() => {
+ this.destinationStore.onDestinationsReload();
+ });
+ },
+
+ /** @private */
+ onCloudPrintPromoDismissed_: function() {
+ this.showCloudPrintPromo = false;
+ },
+
+ /** @private */
+ onUserChange_: function() {
+ const select = this.$$('select');
+ const account = select.value;
+ if (account) {
+ this.showCloudPrintPromo = false;
+ this.userInfo.activeUser = account;
+ this.notifyPath('userInfo.activeUser');
+ this.notifyPath('userInfo.loggedIn');
+ this.destinationStore.reloadUserCookieBasedDestinations();
+ } else {
+ print_preview.NativeLayer.getInstance().signIn(true).then(
+ this.destinationStore.onDestinationsReload.bind(
+ this.destinationStore));
+ const options = select.querySelectorAll('option');
+ for (let i = 0; i < options.length; i++) {
+ if (options[i].value == this.userInfo.activeUser) {
+ select.selectedIndex = i;
+ break;
+ }
+ }
+ }
+ },
+});
diff --git a/chromium/chrome/browser/resources/print_preview/new/destination_list.html b/chromium/chrome/browser/resources/print_preview/new/destination_list.html
new file mode 100644
index 00000000000..3a008a5df4d
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/destination_list.html
@@ -0,0 +1,102 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
+<link rel="import" href="chrome://resources/html/action_link_css.html">
+<link rel="import" href="chrome://resources/html/i18n_behavior.html">
+<link rel="import" href="../native_layer.html">
+<link rel="import" href="../data/destination.html">
+<link rel="import" href="destination_list_item.html">
+<link rel="import" href="print_preview_shared_css.html">
+<link rel="import" href="throbber_css.html">
+
+<dom-module id="print-preview-destination-list">
+ <template>
+ <style include="print-preview-shared action-link cr-hidden-style throbber">
+ :host {
+ padding: 0 14px 18px;
+ user-select: none;
+ }
+
+ :host > header {
+ -webkit-padding-end: 19px;
+ -webkit-padding-start: 0;
+ background-color: transparent;
+ border-bottom: 1px solid #d2d2d2;
+ padding-bottom: 8px;
+ }
+
+ :host :-webkit-any(.title, .action-link, .total) {
+ -webkit-padding-end: 8px;
+ -webkit-padding-start: 4px;
+ display: inline;
+ vertical-align: middle;
+ }
+
+ :host .throbber-container {
+ -webkit-padding-end: 16px;
+ -webkit-padding-start: 8px;
+ display: inline-block;
+ position: relative;
+ vertical-align: middle;
+ }
+
+ :host .throbber {
+ vertical-align: middle;
+ }
+
+ :host .no-destinations-message {
+ -webkit-padding-start: 18px;
+ color: #999;
+ padding-bottom: 8px;
+ padding-top: 8px;
+ }
+
+ :host .list-item {
+ -webkit-padding-end: 2px;
+ -webkit-padding-start: 18px;
+ cursor: default;
+ display: flex;
+ padding-bottom: 3px;
+ padding-top: 3px;
+ }
+
+ :not(.moving).list-item {
+ transition: background-color 150ms;
+ }
+
+ .list-item:hover,
+ .list-item:focus {
+ background-color: rgb(228, 236, 247);
+ }
+
+ .list-item:focus {
+ outline: none;
+ }
+ </style>
+ <header>
+ <h4 class="title">[[title]]</h4>
+ <span class="total" hidden$="[[!showDestinationsTotal_]]">
+ [[i18n('destinationCount', matchingDestinationsCount_)]]
+ </span>
+ <a is="action-link" class="action-link" hidden$="[[!hasActionLink]]"
+ on-click="onActionLinkClick_">
+ $i18n{manage}
+ </a>
+ <div class="throbber-container" hidden$="[[!loadingDestinations]]">
+ <div class="throbber"></div>
+ </div>
+ </header>
+ <template is="dom-repeat" items="[[destinations]]" notify-dom-change
+ on-dom-change="updateIfNeeded_">
+ <print-preview-destination-list-item class="list-item"
+ search-query="[[searchQuery]]" destination="[[item]]"
+ on-click="onDestinationSelected_">
+ </print-preview-destination-list-item>
+ </template>
+ <div class="no-destinations-message" hidden$="[[hasDestinations_]]">
+ $i18n{noDestinationsMessage}
+ </div>
+ </template>
+ <script src="destination_list.js"></script>
+</dom-module>
+
diff --git a/chromium/chrome/browser/resources/print_preview/new/destination_list.js b/chromium/chrome/browser/resources/print_preview/new/destination_list.js
new file mode 100644
index 00000000000..b83f9309149
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/destination_list.js
@@ -0,0 +1,138 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+(function() {
+'use strict';
+
+Polymer({
+ is: 'print-preview-destination-list',
+
+ behaviors: [I18nBehavior],
+
+ properties: {
+ /** @type {Array<!print_preview.Destination>} */
+ destinations: {
+ type: Array,
+ observer: 'destinationsChanged_',
+ },
+
+ /** @type {boolean} */
+ hasActionLink: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @type {boolean} */
+ loadingDestinations: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @type {?RegExp} */
+ searchQuery: {
+ type: Object,
+ observer: 'update_',
+ },
+
+ /** @type {boolean} */
+ title: String,
+
+ /** @private {number} */
+ matchingDestinationsCount_: {
+ type: Number,
+ value: 0,
+ },
+
+ /** @private {boolean} */
+ hasDestinations_: {
+ type: Boolean,
+ computed: 'computeHasDestinations_(matchingDestinationsCount_)',
+ },
+
+ /** @private {boolean} */
+ showDestinationsTotal_: {
+ type: Boolean,
+ computed: 'computeShowDestinationsTotal_(matchingDestinationsCount_)',
+ },
+ },
+
+ /** @private {boolean} */
+ newDestinations_: false,
+
+ /**
+ * @param {!Array<!print_preview.Destination>} current
+ * @param {?Array<!print_preview.Destination>} previous
+ * @private
+ */
+ destinationsChanged_: function(current, previous) {
+ if (previous == undefined) {
+ this.matchingDestinationsCount_ = this.destinations.length;
+ } else {
+ this.newDestinations_ = true;
+ }
+ },
+
+ /** @private */
+ updateIfNeeded_: function() {
+ if (!this.newDestinations_)
+ return;
+ this.newDestinations_ = false;
+ this.update_();
+ },
+
+ /** @private */
+ update_: function() {
+ if (!this.destinations)
+ return;
+
+ const listItems =
+ this.shadowRoot.querySelectorAll('print-preview-destination-list-item');
+
+ let matchCount = 0;
+ listItems.forEach(item => {
+ item.hidden =
+ !!this.searchQuery && !item.destination.matches(this.searchQuery);
+ if (!item.hidden) {
+ matchCount++;
+ item.update();
+ }
+ });
+
+ this.matchingDestinationsCount_ =
+ !this.searchQuery ? listItems.length : matchCount;
+ },
+
+ /**
+ * @return {boolean}
+ * @private
+ */
+ computeHasDestinations_: function() {
+ return !this.destinations || this.matchingDestinationsCount_ > 0;
+ },
+
+ /**
+ * @return {boolean}
+ * @private
+ */
+ computeShowDestinationsTotal_: function() {
+ return this.matchingDestinationsCount_ > 4;
+ },
+
+ /** @private */
+ onActionLinkClick_: function() {
+ print_preview.NativeLayer.getInstance().managePrinters();
+ },
+
+ /**
+ * @param {!Event} e Event containing the destination that was selected.
+ * @private
+ */
+ onDestinationSelected_: function(e) {
+ this.fire(
+ 'destination-selected',
+ /** @type {PrintPreviewDestinationListItemElement} */
+ (e.target).destination);
+ },
+});
+})();
diff --git a/chromium/chrome/browser/resources/print_preview/new/destination_list_item.html b/chromium/chrome/browser/resources/print_preview/new/destination_list_item.html
new file mode 100644
index 00000000000..ad4503b6c68
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/destination_list_item.html
@@ -0,0 +1,136 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
+<link rel="import" href="../native_layer.html">
+<link rel="import" href="../data/destination.html">
+<link rel="import" href="highlight_utils.html">
+<link rel="import" href="print_preview_shared_css.html">
+
+<dom-module id="print-preview-destination-list-item">
+ <template>
+ <style include="print-preview-shared action-link cr-hidden-style">
+ :host .icon {
+ -webkit-margin-end: 8px;
+ display: inline-block;
+ flex: 0 0 auto;
+ height: 24px;
+ transition: opacity 150ms;
+ vertical-align: middle;
+ width: 24px;
+ }
+
+ :host .name,
+ :host .search-hint {
+ flex: 0 1 auto;
+ line-height: 24px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ white-space: nowrap;
+ }
+
+ :host .search-hint {
+ -webkit-margin-start: 1em;
+ color: #999;
+ font-size: 75%;
+ }
+
+ :host .connection-status,
+ :host .learn-more-link {
+ -webkit-margin-start: 1em;
+ flex: 0 0 auto;
+ font-size: 75%;
+ line-height: 24px;
+ vertical-align: middle;
+ }
+
+ :host .learn-more-link {
+ color: rgb(51, 103, 214);
+ }
+
+ :host .register-promo {
+ -webkit-margin-start: 1em;
+ flex: 0 0 auto;
+ }
+
+ :host .extension-controlled-indicator {
+ display: flex;
+ flex: 1;
+ justify-content: flex-end;
+ min-width: 150px;
+ }
+
+ :host .extension-name {
+ -webkit-margin-start: 1em;
+ color: #777;
+ line-height: 24px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ :host .extension-icon {
+ background-position: center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ flex: 0 0 auto;
+ height: 24px;
+ margin: 0 3px;
+ width: 24px;
+ }
+
+ :host .configuring-in-progress-text,
+ :host .configuring-failed-text {
+ -webkit-margin-start: 1em;
+ flex: 0 1 auto;
+ line-height: 24px;
+ vertical-align: middle;
+ }
+
+ :host .configuring-failed-text {
+ color: red;
+ font-style: italic;
+ }
+
+ :host([stale_]) :-webkit-any(.icon, .name, .connection-status) {
+ opacity: 0.4;
+ }
+ </style>
+ <img class="icon" src="[[destination.iconUrl]]"
+ srcset="[[destination.srcSet]]">
+ <span class="name searchable">[[destination.displayName]]</span>
+ <span class="search-hint searchable">[[searchHint_]]</span>
+ <span class="connection-status"
+ hidden$="[[!destination.isOfflineOrInvalid]]">
+ [[destination.connectionStatusText]]
+ </span>
+ <a is="action-link" class="learn-more-link"
+ hidden$="[[!destination.shouldShowInvalidCertificateError]]">
+ $i18n{learnMore}
+ </a>
+ <span class="register-promo" hidden$="[[!destination.isUnregistered]]">
+ <button class="register-promo-button">
+ $i18n{registerPromoButtonText}
+ </button>
+ </span>
+ <span class="extension-controlled-indicator"
+ hidden$="[[!destination.isExtension]]">
+ <span class="extension-name searchable">
+ [[destination.extensionName]]
+ </span>
+ <span class="extension-icon" role="button" tabindex="0"></span>
+ </span>
+<if expr="chromeos">
+ <span class="configuring-in-progress-text" hidden>
+ $i18n{configuringInProgressText}
+ <span class="configuring-text-jumping-dots">
+ <span>.</span><span>.</span><span>.</span>
+ </span>
+ </span>
+ <span class="configuring-failed-text" hidden>
+ $i18n{configuringFailedText}
+ </span>
+</if>
+ </template>
+ <script src="destination_list_item.js"></script>
+</dom-module>
diff --git a/chromium/chrome/browser/resources/print_preview/new/destination_list_item.js b/chromium/chrome/browser/resources/print_preview/new/destination_list_item.js
new file mode 100644
index 00000000000..7e790c0644e
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/destination_list_item.js
@@ -0,0 +1,58 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+Polymer({
+ is: 'print-preview-destination-list-item',
+
+ properties: {
+ /** @type {!print_preview.Destination} */
+ destination: Object,
+
+ /** @type {?RegExp} */
+ searchQuery: Object,
+
+ /** @private */
+ stale_: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+
+ /** @private {string} */
+ searchHint_: String,
+ },
+
+ observers: [
+ 'onDestinationPropertiesChange_(' +
+ 'destination.displayName, destination.isOfflineOrInvalid)',
+ ],
+
+ /** @private {boolean} */
+ highlighted_: false,
+
+ /** @private */
+ onDestinationPropertiesChange_: function() {
+ this.title = this.destination.displayName;
+ this.stale_ = this.destination.isOfflineOrInvalid;
+ },
+
+ update: function() {
+ this.updateSearchHint_();
+ this.updateHighlighting_();
+ },
+
+ /** @private */
+ updateSearchHint_: function() {
+ this.searchHint_ = !this.searchQuery ?
+ '' :
+ this.destination.extraPropertiesToMatch
+ .filter(p => p.match(this.searchQuery))
+ .join(' ');
+ },
+
+ /** @private */
+ updateHighlighting_: function() {
+ this.highlighted_ = print_preview.updateHighlights(
+ this, this.searchQuery, this.highlighted_);
+ },
+});
diff --git a/chromium/chrome/browser/resources/print_preview/new/destination_settings.html b/chromium/chrome/browser/resources/print_preview/new/destination_settings.html
index 69f1f01c23f..b95e9e8be34 100644
--- a/chromium/chrome/browser/resources/print_preview/new/destination_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/destination_settings.html
@@ -1,8 +1,13 @@
<link rel="import" href="chrome://resources/html/polymer.html">
+<link rel="import" href="chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.html">
<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
-<link rel="import" href="button_css.html">
+<link rel="import" href="chrome://resources/html/event_tracker.html">
<link rel="import" href="../data/destination.html">
+<link rel="import" href="../data/destination_store.html">
+<link rel="import" href="../data/user_info.html">
+<link rel="import" href="button_css.html">
+<link rel="import" href="destination_dialog.html">
<link rel="import" href="print_preview_shared_css.html">
<link rel="import" href="throbber_css.html">
<link rel="import" href="settings_section.html">
@@ -69,15 +74,28 @@
<img class="destination-icon"
src="[[destination.iconUrl]]" alt="">
<div class="destination-info-wrapper">
- <div class="destination-name">[[destination.id]]</div>
+ <div class="destination-name">[[destination.displayName]]</div>
<div class="destination-location">[[destination.hint]]</div>
<div class="destination-connection-status">
[[destination.connectionStatusText]]</div>
</div>
</div>
- <button>$i18n{changeDestination}</button>
+ <button
+ disabled$="[[shouldDisableButton_(destinationStore, disabled,
+ state)]]"
+ on-click="onChangeButtonClick_">
+ $i18n{changeDestination}
+ </button>
</div>
</print-preview-settings-section>
+ <template is="cr-lazy-render" id="destinationDialog">
+ <print-preview-destination-dialog
+ destination-store="[[destinationStore]]"
+ recent-destinations="[[recentDestinations]]"
+ user-info="{{userInfo}}"
+ show-cloud-print-promo="{{showCloudPrintPromo_}}">
+ </print-preview-destination-dialog>
+ </template>
</template>
<script src="destination_settings.js"></script>
</dom-module>
diff --git a/chromium/chrome/browser/resources/print_preview/new/destination_settings.js b/chromium/chrome/browser/resources/print_preview/new/destination_settings.js
index 6fde6cc0895..e61f33c637c 100644
--- a/chromium/chrome/browser/resources/print_preview/new/destination_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/destination_settings.js
@@ -9,19 +9,72 @@ Polymer({
/** @type {!print_preview.Destination} */
destination: Object,
+ /** @type {?print_preview.DestinationStore} */
+ destinationStore: Object,
+
+ /** @type {!Array<!print_preview.RecentDestination>} */
+ recentDestinations: Array,
+
+ /** @type {!print_preview.UserInfo} */
+ userInfo: {
+ type: Object,
+ notify: true,
+ },
+
+ disabled: Boolean,
+
+ /** @type {!print_preview_new.State} */
+ state: Number,
+
+ /** @private {boolean} */
+ showCloudPrintPromo_: {
+ type: Boolean,
+ value: false,
+ },
+
/** @private {boolean} */
- loadingDestination_: Boolean,
+ loadingDestination_: {
+ type: Boolean,
+ value: true,
+ },
},
- /** @override */
- ready: function() {
- this.loadingDestination_ = true;
- // Simulate transition from spinner to destination.
- setTimeout(this.doneLoading_.bind(this), 5000);
+ observers: ['onDestinationSet_(destination, destination.id)'],
+
+ /**
+ * @return {boolean} Whether the destination change button should be disabled.
+ * @private
+ */
+ shouldDisableButton_: function() {
+ return !this.destinationStore ||
+ (this.disabled &&
+ this.state != print_preview_new.State.INVALID_PRINTER);
},
/** @private */
- doneLoading_: function() {
- this.loadingDestination_ = false;
+ onDestinationSet_: function() {
+ if (this.destination && this.destination.id)
+ this.loadingDestination_ = false;
+ },
+
+ /** @private */
+ onChangeButtonClick_: function() {
+ this.destinationStore.startLoadAllDestinations();
+ const dialog = this.$.destinationDialog.get();
+ // This async() call is a workaround to prevent a DCHECK - see
+ // https://crbug.com/804047.
+ this.async(() => {
+ dialog.show();
+ }, 1);
+ },
+
+ showCloudPrintPromo: function() {
+ this.showCloudPrintPromo_ = true;
+ },
+
+ /** @return {boolean} Whether the destinations dialog is open. */
+ isDialogOpen: function() {
+ const destinationDialog = this.$$('print-preview-destination-dialog');
+ return destinationDialog && destinationDialog.isOpen();
},
});
diff --git a/chromium/chrome/browser/resources/print_preview/new/dpi_settings.html b/chromium/chrome/browser/resources/print_preview/new/dpi_settings.html
index 2e448060168..1714dc87c91 100644
--- a/chromium/chrome/browser/resources/print_preview/new/dpi_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/dpi_settings.html
@@ -15,7 +15,7 @@
<div slot="controls">
<print-preview-settings-select aria-labelled-by="dpi-label"
capability="[[capabilityWithLabels_]]" setting-name="dpi"
- settings="{{settings}}">
+ settings="{{settings}}" disabled="[[disabled]]">
</print-preview-settings-select>
</div>
</print-preview-settings-section>
diff --git a/chromium/chrome/browser/resources/print_preview/new/dpi_settings.js b/chromium/chrome/browser/resources/print_preview/new/dpi_settings.js
index c5d1c8d9b11..31e95f6d35b 100644
--- a/chromium/chrome/browser/resources/print_preview/new/dpi_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/dpi_settings.js
@@ -30,6 +30,8 @@ Polymer({
/** @type {{ option: Array<!print_preview_new.SelectOption> }} */
capability: Object,
+ disabled: Boolean,
+
/** @private {{ option: Array<!print_preview_new.SelectOption> }} */
capabilityWithLabels_: {
type: Object,
diff --git a/chromium/chrome/browser/resources/print_preview/new/header.html b/chromium/chrome/browser/resources/print_preview/new/header.html
index e16008da496..70468e7670d 100644
--- a/chromium/chrome/browser/resources/print_preview/new/header.html
+++ b/chromium/chrome/browser/resources/print_preview/new/header.html
@@ -1,9 +1,11 @@
<link rel="import" href="chrome://resources/html/polymer.html">
+<link rel="import" href="chrome://resources/html/cr.html">
<link rel="import" href="button_css.html">
<link rel="import" href="../data/destination.html">
<link rel="import" href="settings_behavior.html">
<link rel="import" href="print_preview_shared_css.html">
+<link rel="import" href="state.html">
<link rel="import" href="strings.html">
<dom-module id="print-preview-header">
@@ -67,16 +69,13 @@
}
</style>
<h1 class="title">$i18n{title}</h1>
- <span class="summary"
- aria-label="[[getSummaryLabel_(currentErrorOrState_, labelInfo_)]]"
- inner-h-t-m-l="[[getSummary_(currentErrorOrState_, labelInfo_)]]">
+ <span class="summary" aria-label$="[[summaryLabel_]]"
+ inner-h-t-m-l="[[summary_]]">
</span>
<div id="button-strip">
- <button class="cancel" on-tap="onCancelButtonTap_">
- $i18n{cancel}
- </button>
- <button class="print default" on-tap="onPrintButtonTap_"
- disabled$="[[printButtonDisabled_(currentErrorOrState_)]]">
+ <button class="cancel" on-click="onCancelClick_">$i18n{cancel}</button>
+ <button class="print default" on-click="onPrintClick_"
+ disabled$="[[!printButtonEnabled_]]">
[[getPrintButton_(destination.id)]]
</button>
</div>
diff --git a/chromium/chrome/browser/resources/print_preview/new/header.js b/chromium/chrome/browser/resources/print_preview/new/header.js
index 00ebc16df7f..b1bf7920e69 100644
--- a/chromium/chrome/browser/resources/print_preview/new/header.js
+++ b/chromium/chrome/browser/resources/print_preview/new/header.js
@@ -2,6 +2,16 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+cr.exportPath('print_preview_new.Header');
+
+/**
+ * @typedef {{numPages: number,
+ * numSheets: number,
+ * pagesLabel: string,
+ * summaryLabel: string}}
+ */
+print_preview_new.Header.LabelInfo;
+
Polymer({
is: 'print-preview-header',
@@ -12,51 +22,43 @@ Polymer({
destination: Object,
/** @type {!print_preview_new.State} */
- state: {
- type: Object,
- notify: true,
- },
+ state: Number,
/** @private {boolean} */
- printInProgress_: {
+ printButtonEnabled_: {
type: Boolean,
- notify: true,
value: false,
},
- /**
- * @private {?string} Null value indicates that there is no error or
- * state to display in the summary.
- */
- currentErrorOrState_: {
+ /** @private {?string} */
+ summary_: {
type: String,
- computed: 'computeErrorOrStateString_(state.*, ' +
- 'settings.copies.valid, settings.scaling.valid, ' +
- 'settings.pages.valid, printInProgress_)'
+ notify: true,
+ value: null,
},
- /**
- * @private {{numPages: number,
- * numSheets: number,
- * pagesLabel: string,
- * summaryLabel: string}}
- */
- labelInfo_: {
- type: Object,
- computed: 'getLabelInfo_(currentErrorOrState_, destination.id, ' +
- 'settings.copies.value, settings.pages.value, ' +
- 'settings.duplex.value)'
+ /** @private {?string} */
+ summaryLabel_: {
+ type: String,
+ notify: true,
+ value: null,
},
+
+ errorMessage: String,
},
+ observers:
+ ['update_(settings.copies.value, settings.duplex.value, ' +
+ 'settings.pages.value, state)'],
+
/** @private */
- onPrintButtonTap_: function() {
- this.printInProgress_ = true;
+ onPrintClick_: function() {
+ this.fire('print-requested');
},
/** @private */
- onCancelButtonTap_: function() {
- this.set('state.cancelled', true);
+ onCancelClick_: function() {
+ this.fire('cancel-requested');
},
/**
@@ -81,34 +83,10 @@ Polymer({
},
/**
- * @return {?string}
+ * @return {!print_preview_new.Header.LabelInfo}
* @private
*/
- computeErrorOrStateString_: function() {
- if (this.state.cloudPrintError != '')
- return this.state.cloudPrintError;
- if (this.state.privetExtensionError != '')
- return this.state.privetExtensionError;
- if (this.state.invalidSettings || this.state.previewFailed ||
- this.state.previewLoading || !this.getSetting('copies').valid ||
- !this.getSetting('scaling').valid || !this.getSetting('pages').valid) {
- return '';
- }
- if (this.printInProgress_) {
- return loadTimeData.getString(
- this.isPdfOrDrive_() ? 'saving' : 'printing');
- }
- return null;
- },
-
- /**
- * @return {{numPages: number,
- * numSheets: number,
- * pagesLabel: string,
- * summaryLabel: string}}
- * @private
- */
- getLabelInfo_: function() {
+ computeLabelInfo_: function() {
const saveToPdfOrDrive = this.isPdfOrDrive_();
let numPages = this.getSetting('pages').value.length;
let numSheets = numPages;
@@ -139,23 +117,41 @@ Polymer({
};
},
- /**
- * @return {boolean}
- * @private
- */
- printButtonDisabled_: function() {
- return this.currentErrorOrState_ != null;
+ /** @private */
+ update_: function() {
+ switch (this.state) {
+ case (print_preview_new.State.PRINTING):
+ this.printButtonEnabled_ = false;
+ this.summary_ = loadTimeData.getString(
+ this.isPdfOrDrive_() ? 'saving' : 'printing');
+ this.summaryLabel_ = this.summary_;
+ break;
+ case (print_preview_new.State.READY):
+ this.printButtonEnabled_ = true;
+ const labelInfo = this.computeLabelInfo_();
+ this.summary_ = this.getSummary_(labelInfo);
+ this.summaryLabel_ = this.getSummaryLabel_(labelInfo);
+ break;
+ case (print_preview_new.State.FATAL_ERROR):
+ this.printButtonEnabled_ = false;
+ this.summary_ = this.errorMessage;
+ this.summaryLabel_ = this.errorMessage;
+ break;
+ default:
+ this.summary_ = null;
+ this.summaryLabel_ = null;
+ this.printButtonEnabled_ = false;
+ break;
+ }
},
/**
+ * @param {!print_preview_new.Header.LabelInfo} labelInfo
* @return {string}
* @private
*/
- getSummary_: function() {
- let html = this.currentErrorOrState_;
- if (html != null)
- return html;
- const labelInfo = this.labelInfo_;
+ getSummary_: function(labelInfo) {
+ let html = null;
if (labelInfo.numPages != labelInfo.numSheets) {
html = loadTimeData.getStringF(
'printPreviewSummaryFormatLong',
@@ -175,13 +171,11 @@ Polymer({
},
/**
+ * @param {!print_preview_new.Header.LabelInfo} labelInfo
* @return {string}
* @private
*/
- getSummaryLabel_: function() {
- if (this.currentErrorOrState_ != null)
- return this.currentErrorOrState_;
- const labelInfo = this.labelInfo_;
+ getSummaryLabel_: function(labelInfo) {
if (labelInfo.numPages != labelInfo.numSheets) {
return loadTimeData.getStringF(
'printPreviewSummaryFormatLong', labelInfo.numSheets.toLocaleString(),
diff --git a/chromium/chrome/browser/resources/print_preview/new/highlight_utils.html b/chromium/chrome/browser/resources/print_preview/new/highlight_utils.html
new file mode 100644
index 00000000000..ad6c80409c2
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/highlight_utils.html
@@ -0,0 +1,3 @@
+<link rel="import" href="chrome://resources/html/search_highlight_utils.html">
+
+<script src="highlight_utils.js"></script>
diff --git a/chromium/chrome/browser/resources/print_preview/new/highlight_utils.js b/chromium/chrome/browser/resources/print_preview/new/highlight_utils.js
new file mode 100644
index 00000000000..0a8df2f4780
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/highlight_utils.js
@@ -0,0 +1,60 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('print_preview', function() {
+ 'use strict';
+
+ /**
+ * @param {!HTMLElement} element The element to update. Element should have a
+ * shadow root.
+ * @param {?RegExp} query The current search query
+ * @param {boolean} wasHighlighted Whether the element was previously
+ * highlighted.
+ * @return {boolean} Whether the element is highlighted after the update.
+ */
+ function updateHighlights(element, query, wasHighlighted) {
+ if (wasHighlighted) {
+ cr.search_highlight_utils.findAndRemoveHighlights(element);
+ cr.search_highlight_utils.findAndRemoveBubbles(element);
+ }
+
+ if (!query)
+ return false;
+
+ let isHighlighted = false;
+ element.shadowRoot.querySelectorAll('.searchable').forEach(childElement => {
+ childElement.childNodes.forEach(node => {
+ if (node.nodeType != Node.TEXT_NODE)
+ return;
+
+ const textContent = node.nodeValue.trim();
+ if (textContent.length == 0)
+ return;
+
+ if (query.test(textContent)) {
+ isHighlighted = true;
+ // Don't highlight <select> nodes, yellow rectangles can't be
+ // displayed within an <option>.
+ if (node.parentNode.nodeName != 'OPTION') {
+ cr.search_highlight_utils.highlight(node, textContent.split(query));
+ } else {
+ const selectNode = node.parentNode.parentNode;
+ // The bubble should be parented by the select node's parent.
+ // Note: The bubble's ::after element, a yellow arrow, will not
+ // appear correctly in print preview without SPv175 enabled. See
+ // https://crbug.com/817058.
+ cr.search_highlight_utils.highlightControlWithBubble(
+ /** @type {!HTMLElement} */ (assert(selectNode.parentNode)),
+ textContent.match(query)[0]);
+ }
+ }
+ });
+ });
+ return isHighlighted;
+ }
+
+ return {
+ updateHighlights: updateHighlights,
+ };
+});
diff --git a/chromium/chrome/browser/resources/print_preview/new/layout_settings.html b/chromium/chrome/browser/resources/print_preview/new/layout_settings.html
index afbc18b2d85..0b0e7ae3098 100644
--- a/chromium/chrome/browser/resources/print_preview/new/layout_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/layout_settings.html
@@ -11,7 +11,8 @@
<print-preview-settings-section>
<span id="layout-label" slot="title">$i18n{layoutLabel}</span>
<div slot="controls">
- <select aria-labelledby="layout-label" on-change="onChange_">
+ <select aria-labelledby="layout-label" on-change="onChange_"
+ disabled$="[[disabled]]">
<option value="portrait" selected>$i18n{optionPortrait}</option>
<option value="landscape">$i18n{optionLandscape}</option>
</select>
diff --git a/chromium/chrome/browser/resources/print_preview/new/layout_settings.js b/chromium/chrome/browser/resources/print_preview/new/layout_settings.js
index 77d431925c4..b84fbe2bd00 100644
--- a/chromium/chrome/browser/resources/print_preview/new/layout_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/layout_settings.js
@@ -7,6 +7,10 @@ Polymer({
behaviors: [SettingsBehavior],
+ properties: {
+ disabled: Boolean,
+ },
+
observers: ['onLayoutSettingChange_(settings.layout.value)'],
/**
diff --git a/chromium/chrome/browser/resources/print_preview/new/margins_settings.html b/chromium/chrome/browser/resources/print_preview/new/margins_settings.html
index dbc9ac60d9c..7bf74d1b5a3 100644
--- a/chromium/chrome/browser/resources/print_preview/new/margins_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/margins_settings.html
@@ -12,7 +12,8 @@
<print-preview-settings-section>
<span id="margins-label" slot="title">$i18n{marginsLabel}</span>
<div slot="controls">
- <select aria-labelledby="margins-label" on-change="onChange_">
+ <select aria-labelledby="margins-label" on-change="onChange_"
+ disabled$="[[disabled]]">
<!-- The order of these options must match the natural order of their
values, which come from
print_preview.ticket_items.MarginsTypeValue. -->
diff --git a/chromium/chrome/browser/resources/print_preview/new/margins_settings.js b/chromium/chrome/browser/resources/print_preview/new/margins_settings.js
index 65ec5a10e3e..c641b40c73b 100644
--- a/chromium/chrome/browser/resources/print_preview/new/margins_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/margins_settings.js
@@ -7,6 +7,10 @@ Polymer({
behaviors: [SettingsBehavior],
+ properties: {
+ disabled: Boolean,
+ },
+
observers: ['onMarginsSettingChange_(settings.margins.value)'],
/**
diff --git a/chromium/chrome/browser/resources/print_preview/new/media_size_settings.html b/chromium/chrome/browser/resources/print_preview/new/media_size_settings.html
index 540f8f0972c..03223dfdce2 100644
--- a/chromium/chrome/browser/resources/print_preview/new/media_size_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/media_size_settings.html
@@ -14,7 +14,7 @@
<div slot="controls">
<print-preview-settings-select aria-labelledby="media-size-label"
capability="[[capability]]" setting-name="mediaSize"
- settings="{{settings}}">
+ settings="{{settings}}" disabled="[[disabled]]">
</print-preview-settings-select>
</div>
</print-preview-settings-section>
diff --git a/chromium/chrome/browser/resources/print_preview/new/media_size_settings.js b/chromium/chrome/browser/resources/print_preview/new/media_size_settings.js
index 55dc7ebcb8b..1ff1bfb0715 100644
--- a/chromium/chrome/browser/resources/print_preview/new/media_size_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/media_size_settings.js
@@ -9,6 +9,8 @@ Polymer({
properties: {
capability: Object,
+
+ disabled: Boolean,
},
observers:
diff --git a/chromium/chrome/browser/resources/print_preview/new/model.html b/chromium/chrome/browser/resources/print_preview/new/model.html
index 30924113655..3e3ea6386f2 100644
--- a/chromium/chrome/browser/resources/print_preview/new/model.html
+++ b/chromium/chrome/browser/resources/print_preview/new/model.html
@@ -1,6 +1,7 @@
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/html/cr.html">
+<link rel="import" href="settings_behavior.html">
<link rel="import" href="../data/destination.html">
<link rel="import" href="../data/document_info.html">
<link rel="import" href="../data/margins.html">
diff --git a/chromium/chrome/browser/resources/print_preview/new/model.js b/chromium/chrome/browser/resources/print_preview/new/model.js
index bf4fe074786..0b90df49409 100644
--- a/chromium/chrome/browser/resources/print_preview/new/model.js
+++ b/chromium/chrome/browser/resources/print_preview/new/model.js
@@ -33,6 +33,16 @@ cr.exportPath('print_preview_new');
*/
print_preview_new.SerializedSettings;
+/**
+ * Constant values matching printing::DuplexMode enum.
+ * @enum {number}
+ */
+print_preview_new.DuplexMode = {
+ SIMPLEX: 0,
+ LONG_EDGE: 1,
+ UNKNOWN_DUPLEX_MODE: -1
+};
+
(function() {
'use strict';
@@ -42,16 +52,28 @@ const NUM_DESTINATIONS = 3;
/**
* Sticky setting names. Alphabetical except for fitToPage, which must be set
* after scaling in updateFromStickySettings().
- * @type {Array<string>}
+ * @type {!Array<string>}
*/
const STICKY_SETTING_NAMES = [
- 'collate', 'color', 'cssBackground', 'dpi', 'duplex', 'headerFooter',
- 'layout', 'margins', 'mediaSize', 'scaling', 'fitToPage'
+ 'collate',
+ 'color',
+ 'cssBackground',
+ 'dpi',
+ 'duplex',
+ 'headerFooter',
+ 'layout',
+ 'margins',
+ 'mediaSize',
+ 'scaling',
+ 'fitToPage',
+ 'vendorItems',
];
Polymer({
is: 'print-preview-model',
+ behaviors: [SettingsBehavior],
+
properties: {
/**
* Object containing current settings of Print Preview, for use by Polymer
@@ -175,7 +197,7 @@ Polymer({
unavailableValue: {},
valid: true,
available: true,
- key: '',
+ key: 'vendorOptions',
},
// This does not represent a real setting value, and is used only to
// expose the availability of the other options settings section.
@@ -228,12 +250,19 @@ Polymer({
'settings.mediaSize.value, settings.margins.value, ' +
'settings.dpi.value, settings.fitToPage.value, ' +
'settings.scaling.value, settings.duplex.value, ' +
- 'settings.headerFooter.value, settings.cssBackground.value)',
+ 'settings.headerFooter.value, settings.cssBackground.value, ' +
+ 'settings.vendorItems.value)',
],
/** @private {boolean} */
initialized_: false,
+ /** @private {?print_preview_new.SerializedSettings} */
+ stickySettings_: null,
+
+ /** @private {?print_preview.Cdd} */
+ lastDestinationCapabilities_: null,
+
/**
* Updates the availability of the settings sections and values of dpi and
* media size settings.
@@ -244,6 +273,14 @@ Polymer({
this.destination.capabilities.printer :
null;
this.updateSettingsAvailability_(caps);
+
+ if (!caps)
+ return;
+
+ if (this.destination.capabilities == this.lastDestinationCapabilities_)
+ return;
+
+ this.lastDestinationCapabilities_ = this.destination.capabilities;
this.updateSettingsValues_(caps);
},
@@ -290,6 +327,8 @@ Polymer({
this.settings.selectionOnly.available ||
this.settings.headerFooter.available ||
this.settings.rasterize.available);
+ this.set(
+ 'settings.vendorItems.available', !!caps && !!caps.vendor_capability);
},
/**
@@ -318,25 +357,38 @@ Polymer({
*/
updateSettingsValues_: function(caps) {
if (this.settings.mediaSize.available) {
- for (const option of caps.media_size.option) {
- if (option.is_default) {
- this.set('settings.mediaSize.value', option);
- break;
- }
- }
+ const defaultOption = caps.media_size.option.find(o => !!o.is_default);
+ this.set('settings.mediaSize.value', defaultOption);
}
-
if (this.settings.dpi.available) {
- for (const option of caps.dpi.option) {
- if (option.is_default) {
- this.set('settings.dpi.value', option);
- break;
- }
- }
+ const defaultOption = caps.dpi.option.find(o => !!o.is_default);
+ this.set('settings.dpi.value', defaultOption);
} else if (
caps && caps.dpi && caps.dpi.option && caps.dpi.option.length > 0) {
this.set('settings.dpi.value', caps.dpi.option[0]);
}
+
+ if (this.settings.vendorItems.available) {
+ const vendorSettings = {};
+ for (const item of caps.vendor_capability) {
+ let defaultValue = null;
+ if (item.type == 'SELECT' && !!item.select_cap &&
+ !!item.select_cap.option) {
+ const defaultOption =
+ item.select_cap.option.find(o => !!o.is_default);
+ defaultValue = !!defaultOption ? defaultOption.value : null;
+ } else if (item.type == 'RANGE') {
+ if (!!item.range_cap)
+ defaultValue = item.range_cap.default || null;
+ } else if (item.type == 'TYPED_VALUE') {
+ if (!!item.typed_value_cap)
+ defaultValue = item.typed_value_cap.default || null;
+ }
+ if (defaultValue != null)
+ vendorSettings[item.id] = defaultValue;
+ }
+ this.set('settings.vendorItems.value', vendorSettings);
+ }
},
/** @private */
@@ -371,18 +423,20 @@ Polymer({
this.recentDestinations.splice(indexFound, 1);
// Add the most recent destination
- this.recentDestinations.splice(0, 0, newDestination);
- this.notifyPath('recentDestinations');
+ this.splice('recentDestinations', 0, 0, newDestination);
// Persist sticky settings.
this.stickySettingsChanged_();
},
/**
+ * Caches the sticky settings and sets up the recent destinations. Sticky
+ * settings will be applied when destinaton capabilities have been retrieved.
* @param {?string} savedSettingsStr The sticky settings from native layer
*/
- updateFromStickySettings: function(savedSettingsStr) {
- this.initialized_ = true;
+ setStickySettings: function(savedSettingsStr) {
+ assert(!this.stickySettings_ && this.recentDestinations.length == 0);
+
if (!savedSettingsStr)
return;
@@ -403,16 +457,26 @@ Polymer({
}
this.recentDestinations = recentDestinations;
- // Reset initialized, or stickySettingsChanged_ will get called for
- // every setting that gets set below.
- this.initialized_ = false;
- STICKY_SETTING_NAMES.forEach(settingName => {
- const setting = this.get(settingName, this.settings);
- const value = savedSettings[setting.key];
- if (value != undefined)
- this.set(`settings.${settingName}.value`, value);
- });
+ this.stickySettings_ = savedSettings;
+ },
+
+ applyStickySettings: function() {
+ if (this.stickySettings_) {
+ STICKY_SETTING_NAMES.forEach(settingName => {
+ const setting = this.get(settingName, this.settings);
+ const value = this.stickySettings_[setting.key];
+ if (value != undefined)
+ this.set(`settings.${settingName}.value`, value);
+ });
+ }
this.initialized_ = true;
+ this.stickySettings_ = null;
+ this.stickySettingsChanged_();
+ },
+
+ /** @return {boolean} Whether the model has been initialized. */
+ initialized: function() {
+ return this.initialized_;
},
/** @private */
@@ -431,5 +495,168 @@ Polymer({
});
this.fire('save-sticky-settings', JSON.stringify(serialization));
},
+
+ /**
+ * Creates a string that represents a print ticket.
+ * @param {!print_preview.Destination} destination Destination to print to.
+ * @return {string} Serialized print ticket.
+ */
+ createPrintTicket: function(destination) {
+ const dpi = /** @type {{horizontal_dpi: (number | undefined),
+ vertical_dpi: (number | undefined),
+ vendor_id: (number | undefined)}} */ (
+ this.getSettingValue('dpi'));
+
+ const ticket = {
+ mediaSize: this.getSettingValue('mediaSize'),
+ pageCount: this.getSettingValue('pages').length,
+ landscape: this.getSettingValue('layout'),
+ color: destination.getNativeColorModel(
+ /** @type {boolean} */ (this.getSettingValue('color'))),
+ headerFooterEnabled: false, // only used in print preview
+ marginsType: this.getSettingValue('margins'),
+ duplex: this.getSettingValue('duplex') ?
+ print_preview_new.DuplexMode.LONG_EDGE :
+ print_preview_new.DuplexMode.SIMPLEX,
+ copies: this.getSettingValue('copies'),
+ collate: this.getSettingValue('collate'),
+ shouldPrintBackgrounds: this.getSettingValue('cssBackground'),
+ shouldPrintSelectionOnly: false, // only used in print preview
+ previewModifiable: this.documentInfo.isModifiable,
+ printToPDF: destination.id ==
+ print_preview.Destination.GooglePromotedId.SAVE_AS_PDF,
+ printWithCloudPrint: !destination.isLocal,
+ printWithPrivet: destination.isPrivet,
+ printWithExtension: destination.isExtension,
+ rasterizePDF: this.getSettingValue('rasterize'),
+ scaleFactor: parseInt(this.getSettingValue('scaling'), 10),
+ dpiHorizontal: (dpi && 'horizontal_dpi' in dpi) ? dpi.horizontal_dpi : 0,
+ dpiVertical: (dpi && 'vertical_dpi' in dpi) ? dpi.vertical_dpi : 0,
+ deviceName: destination.id,
+ fitToPageEnabled: this.getSettingValue('fitToPage'),
+ pageWidth: this.documentInfo.pageSize.width,
+ pageHeight: this.documentInfo.pageSize.height,
+ showSystemDialog: false,
+ };
+
+ // Set 'cloudPrintID' only if the destination is not local.
+ if (!destination.isLocal)
+ ticket.cloudPrintID = destination.id;
+
+ if (this.getSettingValue('margins') ==
+ print_preview.ticket_items.MarginsTypeValue.CUSTOM) {
+ // TODO (rbpotter): Replace this with real values when custom margins are
+ // implemented.
+ ticket.marginsCustom = {
+ marginTop: 70,
+ marginRight: 70,
+ marginBottom: 70,
+ marginLeft: 70,
+ };
+ }
+
+ if (destination.isPrivet || destination.isExtension) {
+ // TODO (rbpotter): Get local and PDF printers to use the same ticket and
+ // send only this ticket instead of nesting it in a larger ticket.
+ ticket.ticket = this.createCloudJobTicket(destination);
+ ticket.capabilities = JSON.stringify(destination.capabilities);
+ }
+ return JSON.stringify(ticket);
+ },
+
+ /**
+ * Creates an object that represents a Google Cloud Print print ticket.
+ * @param {!print_preview.Destination} destination Destination to print to.
+ * @return {string} Google Cloud Print print ticket.
+ */
+ createCloudJobTicket: function(destination) {
+ assert(
+ !destination.isLocal || destination.isPrivet || destination.isExtension,
+ 'Trying to create a Google Cloud Print print ticket for a local ' +
+ ' non-privet and non-extension destination');
+ assert(
+ destination.capabilities,
+ 'Trying to create a Google Cloud Print print ticket for a ' +
+ 'destination with no print capabilities');
+
+ // Create CJT (Cloud Job Ticket)
+ const cjt = {version: '1.0', print: {}};
+ if (this.settings.collate.available)
+ cjt.print.collate = {collate: this.settings.collate.value};
+ if (this.settings.color.available) {
+ const selectedOption = destination.getSelectedColorOption(
+ /** @type {boolean} */ (this.settings.color.value));
+ if (!selectedOption) {
+ console.error('Could not find correct color option');
+ } else {
+ cjt.print.color = {type: selectedOption.type};
+ if (selectedOption.hasOwnProperty('vendor_id')) {
+ cjt.print.color.vendor_id = selectedOption.vendor_id;
+ }
+ }
+ } else {
+ // Always try setting the color in the print ticket, otherwise a
+ // reasonable reader of the ticket will have to do more work, or process
+ // the ticket sub-optimally, in order to safely handle the lack of a
+ // color ticket item.
+ const defaultOption = destination.defaultColorOption;
+ if (defaultOption) {
+ cjt.print.color = {type: defaultOption.type};
+ if (defaultOption.hasOwnProperty('vendor_id')) {
+ cjt.print.color.vendor_id = defaultOption.vendor_id;
+ }
+ }
+ }
+ if (this.settings.copies.available)
+ cjt.print.copies = {copies: this.settings.copies.value};
+ if (this.settings.duplex.available) {
+ cjt.print.duplex = {
+ type: this.settings.duplex.value ? 'LONG_EDGE' : 'NO_DUPLEX'
+ };
+ }
+ if (this.settings.mediaSize.available) {
+ const mediaValue = this.settings.mediaSize.value;
+ cjt.print.media_size = {
+ width_microns: mediaValue.width_microns,
+ height_microns: mediaValue.height_microns,
+ is_continuous_feed: mediaValue.is_continuous_feed,
+ vendor_id: mediaValue.vendor_id
+ };
+ }
+ if (!this.settings.layout.available) {
+ // In this case "orientation" option is hidden from user, so user can't
+ // adjust it for page content, see Landscape.isCapabilityAvailable().
+ // We can improve results if we set AUTO here.
+ const capability = destination.capabilities.printer ?
+ destination.capabilities.printer.page_orientation :
+ null;
+ if (capability && capability.option &&
+ capability.option.some(option => option.type == 'AUTO')) {
+ cjt.print.page_orientation = {type: 'AUTO'};
+ }
+ } else {
+ cjt.print.page_orientation = {
+ type: this.settings.layout ? 'LANDSCAPE' : 'PORTRAIT'
+ };
+ }
+ if (this.settings.dpi.available) {
+ const dpiValue = this.settings.dpi.value;
+ cjt.print.dpi = {
+ horizontal_dpi: dpiValue.horizontal_dpi,
+ vertical_dpi: dpiValue.vertical_dpi,
+ vendor_id: dpiValue.vendor_id
+ };
+ }
+ if (this.settings.vendorItems.available) {
+ const items = this.settings.vendorItems.value;
+ cjt.print.vendor_ticket_item = [];
+ for (const itemId in items) {
+ if (items.hasOwnProperty(itemId)) {
+ cjt.print.vendor_ticket_item.push({id: itemId, value: items[itemId]});
+ }
+ }
+ }
+ return JSON.stringify(cjt);
+ },
});
})();
diff --git a/chromium/chrome/browser/resources/print_preview/new/number_settings_section.html b/chromium/chrome/browser/resources/print_preview/new/number_settings_section.html
index dfea6be0f89..c56de1f3557 100644
--- a/chromium/chrome/browser/resources/print_preview/new/number_settings_section.html
+++ b/chromium/chrome/browser/resources/print_preview/new/number_settings_section.html
@@ -32,13 +32,16 @@
<div slot="controls">
<slot name="opt-outside-content"></slot>
<span class="input-wrapper">
- <input class="user-value" type="number" value="{{inputString::input}}"
- max="[[maxValue]]" min="[[minValue]]" on-blur="onBlur_"
- on-keydown="onKeydown_" aria-labelled-by="section-title">
+ <input class="user-value" type="number"
+ value="{{inputString_::input}}"
+ max="[[maxValue]]" min="[[minValue]]"
+ disabled$="[[getDisabled_(inputValid, disabled)]]"
+ on-blur="onBlur_" on-keydown="onKeydown_"
+ aria-labelled-by="section-title">
<slot name="opt-inside-content"></slot>
</span>
<span class="hint" aria-live="polite"
- hidden$="[[hintHidden_(inputString, inputValid)]]">
+ hidden$="[[hintHidden_(inputString_, inputValid)]]">
[[hintMessage]]
</span>
</div>
diff --git a/chromium/chrome/browser/resources/print_preview/new/number_settings_section.js b/chromium/chrome/browser/resources/print_preview/new/number_settings_section.js
index 4084b852b71..ed173077085 100644
--- a/chromium/chrome/browser/resources/print_preview/new/number_settings_section.js
+++ b/chromium/chrome/browser/resources/print_preview/new/number_settings_section.js
@@ -6,33 +6,46 @@ Polymer({
is: 'print-preview-number-settings-section',
properties: {
- /** @type {string} */
- inputString: {
+ /** @private {string} */
+ inputString_: {
type: String,
notify: true,
+ observer: 'onInputChanged_',
},
/** @type {boolean} */
inputValid: {
type: Boolean,
notify: true,
- computed: 'computeValid_(inputString)',
+ value: true,
},
/** @type {string} */
+ currentValue: {
+ type: String,
+ notify: true,
+ observer: 'onCurrentValueChanged_',
+ },
+
defaultValue: String,
- /** @type {number} */
maxValue: Number,
- /** @type {number} */
minValue: Number,
- /** @type {string} */
inputLabel: String,
- /** @type {string} */
hintMessage: String,
+
+ disabled: Boolean,
+ },
+
+ /**
+ * @return {boolean} Whether the input should be disabled.
+ * @private
+ */
+ getDisabled_: function() {
+ return this.disabled && this.inputValid;
},
/**
@@ -45,19 +58,31 @@ Polymer({
/** @private */
onBlur_: function() {
- if (this.inputString == '')
- this.set('inputString', this.defaultValue);
+ if (this.inputString_ == '')
+ this.set('inputString_', this.defaultValue);
+ },
+
+ /** @private */
+ onInputChanged_: function() {
+ this.inputValid = this.computeValid_();
+ if (this.inputValid)
+ this.currentValue = this.inputString_;
+ },
+
+ /** @private */
+ onCurrentValueChanged_: function() {
+ this.inputString_ = this.currentValue;
},
/**
- * @return {boolean} Whether input value represented by inputString is
+ * @return {boolean} Whether input value represented by inputString_ is
* valid.
* @private
*/
computeValid_: function() {
- // Make sure value updates first, in case inputString was updated by JS.
- this.$$('.user-value').value = this.inputString;
- return this.$$('.user-value').validity.valid && this.inputString != '';
+ // Make sure value updates first, in case inputString_ was updated by JS.
+ this.$$('.user-value').value = this.inputString_;
+ return this.$$('.user-value').validity.valid && this.inputString_ != '';
},
/**
@@ -65,6 +90,6 @@ Polymer({
* @private
*/
hintHidden_: function() {
- return this.inputValid || this.inputString == '';
+ return this.inputValid || this.inputString_ == '';
},
});
diff --git a/chromium/chrome/browser/resources/print_preview/new/other_options_settings.html b/chromium/chrome/browser/resources/print_preview/new/other_options_settings.html
index b816a3274e9..afd187b856d 100644
--- a/chromium/chrome/browser/resources/print_preview/new/other_options_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/other_options_settings.html
@@ -19,12 +19,14 @@
<label aria-live="polite"
hidden$="[[!settings.headerFooter.available]]">
<input type="checkbox" id="header-footer"
+ disabled$="[[disabled]]"
on-change="onHeaderFooterChange_"
checked$="[[settings.headerFooter.value]]">
<span>$i18n{optionHeaderFooter}</span>
</label>
<label aria-live="polite" hidden$="[[!settings.duplex.available]]">
<input type="checkbox" id="duplex" on-change="onDuplexChange_"
+ disabled$="[[disabled]]"
checked$="[[settings.duplex.value]]">
<span>$i18n{optionTwoSided}</span>
</label>
@@ -32,18 +34,20 @@
hidden$="[[!settings.cssBackground.available]]">
<input type="checkbox" id="css-background"
on-change="onCssBackgroundChange_"
+ disabled$="[[disabled]]"
checked$="[[settings.cssBackground.value]]">
<span>$i18n{optionBackgroundColorsAndImages}</span>
</label>
<label aria-live="polite" hidden$="[[!settings.rasterize.available]]">
<input type="checkbox" id="rasterize"
- on-change="onRasterizeChange_"
+ disabled$="[[disabled]]" on-change="onRasterizeChange_"
checked$="[[settings.rasterize.value]]">
<span>$i18n{optionRasterize}</span>
</label>
<label aria-live="polite"
hidden$="[[!settings.selectionOnly.available]]">
<input type="checkbox" id="selection-only"
+ disabled$="[[disabled]]"
on-change="onSelectionOnlyChange_"
checked$="[[settings.selectionOnly.value]]">
<span>$i18n{optionSelectionOnly}</span>
diff --git a/chromium/chrome/browser/resources/print_preview/new/other_options_settings.js b/chromium/chrome/browser/resources/print_preview/new/other_options_settings.js
index 07e21f5de60..07d9f82eb58 100644
--- a/chromium/chrome/browser/resources/print_preview/new/other_options_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/other_options_settings.js
@@ -7,6 +7,10 @@ Polymer({
behaviors: [SettingsBehavior],
+ properties: {
+ disabled: Boolean,
+ },
+
observers: [
'onHeaderFooterSettingChange_(settings.headerFooter.value)',
'onDuplexSettingChange_(settings.duplex.value)',
diff --git a/chromium/chrome/browser/resources/print_preview/new/pages_settings.html b/chromium/chrome/browser/resources/print_preview/new/pages_settings.html
index 72fc32d2160..2777b3b0973 100644
--- a/chromium/chrome/browser/resources/print_preview/new/pages_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/pages_settings.html
@@ -1,6 +1,5 @@
<link rel="import" href="chrome://resources/html/polymer.html">
-<link rel="import" href="chrome://resources/html/cr.html">
<link rel="import" href="checkbox_radio_css.html">
<link rel="import" href="../data/document_info.html">
<link rel="import" href="input_css.html">
@@ -30,16 +29,19 @@
<div slot="controls">
<div class="radio">
<label><input type="radio" name="pages" id="all-radio-button"
- checked="{{allSelected_::change}}">
+ checked="{{allSelected_::change}}"
+ disabled$="[[getDisabled_(disabled, settings.pages.valid)]]">
<span>$i18n{optionAllPages}</span>
</label>
<label class="custom-input-wrapper"
for="page-settings-custom-input" tabindex=-1>
<input type="radio" name="pages" id="custom-radio-button"
+ disabled$="[[getDisabled_(disabled, settings.pages.valid)]]"
on-click="onCustomRadioClick_">
<input class="user-value" type="text"
value="{{inputString_::input}}" id="page-settings-custom-input"
checked="{{customSelected_::change}}"
+ disabled$="[[getDisabled_(dsiabled, settings.pages.valid)]]"
pattern="([0-9]*(-)?[0-9]*(,)( )?)*([0-9]*(-)?[0-9]*(,)?( )?)?"
on-focus="onCustomInputFocus_" on-blur="onCustomInputBlur_"
placeholder="$i18n{examplePageRangeText}"
diff --git a/chromium/chrome/browser/resources/print_preview/new/pages_settings.js b/chromium/chrome/browser/resources/print_preview/new/pages_settings.js
index e230e2c3500..4b6e5276fc1 100644
--- a/chromium/chrome/browser/resources/print_preview/new/pages_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/pages_settings.js
@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-cr.exportPath('print_preview_new');
+(function() {
+'use strict';
/** @enum {number} */
const PagesInputErrorState = {
@@ -44,6 +45,14 @@ Polymer({
value: false,
},
+ disabled: Boolean,
+
+ /** @private {number} */
+ errorState_: {
+ type: Number,
+ value: PagesInputErrorState.NO_ERROR,
+ },
+
/** @private {!Array<number>} */
pagesToPrint_: {
type: Array,
@@ -56,13 +65,6 @@ Polymer({
type: Array,
computed: 'computeRangesToPrint_(pagesToPrint_, allPagesArray_)',
},
-
- /** @private {!PagesInputErrorState} */
- errorState_: {
- type: Number,
- computed: 'computeErrorState_(documentInfo.pageCount, pagesToPrint_)',
- },
-
},
observers: [
@@ -71,6 +73,14 @@ Polymer({
],
/**
+ * @return {boolean} Whether the controls should be disabled.
+ * @private
+ */
+ getDisabled_: function() {
+ return this.getSetting('pages').valid && this.disabled;
+ },
+
+ /**
* @return {!Array<number>}
* @private
*/
@@ -88,10 +98,14 @@ Polymer({
* @private
*/
computePagesToPrint_: function() {
- if (this.allSelected_ || this.inputString_.trim() == '')
+ if (this.allSelected_ || this.inputString_.trim() == '') {
+ this.errorState_ = PagesInputErrorState.NO_ERROR;
return this.allPagesArray_;
- if (!this.$$('.user-value').validity.valid)
- return [];
+ }
+ if (!this.$$('.user-value').validity.valid) {
+ this.errorState_ = PagesInputErrorState.INVALID_SYNTAX;
+ return this.pagesToPrint_;
+ }
const pages = [];
const added = {};
@@ -103,11 +117,15 @@ Polymer({
continue;
const limits = range.split('-');
let min = parseInt(limits[0], 10);
- if (min < 1)
- return [];
+ if (min < 1) {
+ this.errorState_ = PagesInputErrorState.OUT_OF_BOUNDS;
+ return this.pagesToPrint_;
+ }
if (limits.length == 1) {
- if (min > maxPage)
- return [-1];
+ if (min > maxPage) {
+ this.errorState_ = PagesInputErrorState.OUT_OF_BOUNDS;
+ return this.pagesToPrint_;
+ }
if (!added.hasOwnProperty(min)) {
pages.push(min);
added[min] = true;
@@ -120,10 +138,14 @@ Polymer({
min = 1;
if (isNaN(max))
max = maxPage;
- if (min > max)
- return [];
- if (max > maxPage)
- return [-1];
+ if (min > max) {
+ this.errorState_ = PagesInputErrorState.INVALID_SYNTAX;
+ return this.pagesToPrint_;
+ }
+ if (max > maxPage) {
+ this.errorState_ = PagesInputErrorState.OUT_OF_BOUNDS;
+ return this.pagesToPrint_;
+ }
for (let i = min; i <= max; i++) {
if (!added.hasOwnProperty(i)) {
pages.push(i);
@@ -162,17 +184,17 @@ Polymer({
},
/**
- * @return {!PagesInputErrorState}
- * @private
+ * @return {boolean} Whether pages setting and pagesToPrint_ match.
*/
- computeErrorState_: function() {
- if (this.documentInfo.pageCount == 0) // page count not yet initialized
- return PagesInputErrorState.NO_ERROR;
- if (this.pagesToPrint_.length == 0)
- return PagesInputErrorState.INVALID_SYNTAX;
- if (this.pagesToPrint_[0] == -1)
- return PagesInputErrorState.OUT_OF_BOUNDS;
- return PagesInputErrorState.NO_ERROR;
+ settingMatches_: function() {
+ const setting = this.getSetting('pages').value;
+ if (setting.length != this.pagesToPrint_.length)
+ return false;
+ for (let index = 0; index < this.pagesToPrint_.length; index++) {
+ if (this.pagesToPrint_[index] != setting[index])
+ return false;
+ }
+ return true;
},
/**
@@ -188,8 +210,10 @@ Polymer({
}
this.$$('.user-value').classList.remove('invalid');
this.setSettingValid('pages', true);
- this.setSetting('pages', this.pagesToPrint_);
- this.setSetting('ranges', this.rangesToPrint_);
+ if (!this.settingMatches_()) {
+ this.setSetting('pages', this.pagesToPrint_);
+ this.setSetting('ranges', this.rangesToPrint_);
+ }
},
/** @private */
@@ -249,3 +273,4 @@ Polymer({
return this.errorState_ == PagesInputErrorState.NO_ERROR;
}
});
+})();
diff --git a/chromium/chrome/browser/resources/print_preview/new/preview_area.html b/chromium/chrome/browser/resources/print_preview/new/preview_area.html
index 716d3b5e6f3..a7a9d5cd05b 100644
--- a/chromium/chrome/browser/resources/print_preview/new/preview_area.html
+++ b/chromium/chrome/browser/resources/print_preview/new/preview_area.html
@@ -1,6 +1,7 @@
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/html/cr.html">
+<link rel="import" href="chrome://resources/html/i18n_behavior.html">
<link rel="import" href="chrome://resources/html/web_ui_listener_behavior.html">
<link rel="import" href="../native_layer.html">
<link rel="import" href="../print_preview_utils.html">
@@ -10,6 +11,8 @@
<link rel="import" href="../data/margins.html">
<link rel="import" href="../data/printable_area.html">
<link rel="import" href="../data/size.html">
+<link rel="import" href="model.html">
+<link rel="import" href="state.html">
<link rel="import" href="settings_behavior.html">
<dom-module id="print-preview-preview-area">
@@ -108,37 +111,20 @@
}
</style>
- <div class="preview-area-overlay-layer">
+ <div class$="preview-area-overlay-layer [[getInvisible_(previewState_)]]"
+ aria-hidden$="[[previewLoaded(previewState_)]]">
<div class="preview-area-messages">
-
- <div class="preview-area-loading-message preview-area-message"
- hidden$="[[!state.previewLoading]]">
- <span>$i18n{loading}</span>
- <span class="preview-area-loading-message-jumping-dots jumping-dots"
- ><span>.</span><span>.</span><span>.</span></span>
- </div>
-
- <div class="preview-area-custom-message preview-area-message" hidden>
- <div class="preview-area-custom-message-text"></div>
- <div class="preview-area-custom-action-area">
- <button class="preview-area-open-system-dialog-button">
- $i18n{launchNativeDialog}
- </button>
- <div
- class="preview-area-open-system-dialog-button-throbber throbber"
- hidden></div>
+ <div class="preview-area-message">
+ <div>
+ <span>[[currentMessage_(previewState_)]]</span>
+ <span class$="preview-area-loading-message-jumping-dots
+ [[getJumpingDots_(previewState_)]]"
+ hidden$="[[!isPreviewLoading_(previewState_)]]">
+ <span>.</span><span>.</span><span>.</span>
+ </span>
</div>
- </div>
-
- <div class="preview-area-preview-failed-message preview-area-message"
- hidden$="[[!state.previewFailed]]">
- $i18n{previewFailed}
- </div>
-
- <div class="preview-area-print-failed preview-area-message"
- hidden$="[[!state.invalidSettings]]">
- <div>$i18n{invalidPrinterSettings}</div>
- <div class="preview-area-print-failed-action-area">
+ <div class="preview-area-action-area"
+ hidden$="[[!displaySystemDialogButton_(previewState_)]]">
<button class="preview-area-open-system-dialog-button">
$i18n{launchNativeDialog}
</button>
@@ -147,7 +133,6 @@
hidden></div>
</div>
</div>
-
</div>
</div>
<div class="preview-area-plugin-wrapper">
diff --git a/chromium/chrome/browser/resources/print_preview/new/preview_area.js b/chromium/chrome/browser/resources/print_preview/new/preview_area.js
index 6a1b53b0d1a..9c456b76ccf 100644
--- a/chromium/chrome/browser/resources/print_preview/new/preview_area.js
+++ b/chromium/chrome/browser/resources/print_preview/new/preview_area.js
@@ -34,20 +34,23 @@ cr.exportPath('print_preview_new');
*/
print_preview_new.PDFPlugin;
-/**
- * Constant values matching printing::DuplexMode enum.
- * @enum {number}
- */
-print_preview_new.DuplexMode = {
- SIMPLEX: 0,
- LONG_EDGE: 1,
- UNKNOWN_DUPLEX_MODE: -1
+(function() {
+'use strict';
+
+/** @enum {string} */
+const PreviewAreaState_ = {
+ LOADING: 'loading',
+ DISPLAY_PREVIEW: 'display-preview',
+ OPEN_IN_PREVIEW: 'open-in-preview',
+ INVALID_SETTINGS: 'invalid-settings',
+ PREVIEW_FAILED: 'preview-failed',
};
Polymer({
is: 'print-preview-preview-area',
- behaviors: [WebUIListenerBehavior, SettingsBehavior],
+ behaviors: [WebUIListenerBehavior, SettingsBehavior, I18nBehavior],
+
properties: {
/** @type {print_preview.DocumentInfo} */
documentInfo: Object,
@@ -55,10 +58,17 @@ Polymer({
/** @type {print_preview.Destination} */
destination: Object,
- /** @type {print_preview_new.State} */
+ /** @type {!print_preview_new.State} */
state: {
- type: Object,
+ type: Number,
+ observer: 'onStateChanged_',
+ },
+
+ /** @private {string} */
+ previewState_: {
+ type: String,
notify: true,
+ value: PreviewAreaState_.LOADING,
},
},
@@ -68,9 +78,7 @@ Polymer({
'settings.layout.value, settings.margins.value, ' +
'settings.mediaSize.value, settings.ranges.value,' +
'settings.selectionOnly.value, settings.scaling.value, ' +
- 'destination.id, destination.capabilities, state.initialized)',
- 'onPreviewStateChanged_(state.previewLoading, state.invalidSettings, ' +
- 'state.previewFailed)',
+ 'settings.rasterize.value, destination.id, destination.capabilities)',
],
/** @private {print_preview.NativeLayer} */
@@ -79,13 +87,16 @@ Polymer({
/** @private {number} */
inFlightRequestId_: -1,
+ /** @private {boolean} */
+ requestPreviewWhenReady_: false,
+
/** @private {HTMLEmbedElement|print_preview_new.PDFPlugin} */
plugin_: null,
- /** @private {boolean} */
+ /** @private {boolean} Whether the plugin is loaded */
pluginLoaded_: false,
- /** @private {boolean} */
+ /** @private {boolean} Whether the document is ready */
documentReady_: false,
/** @override */
@@ -102,59 +113,119 @@ Polymer({
this.$$('.preview-area-compatibility-object-out-of-process');
const isOOPCompatible = oopCompatObj.postMessage;
oopCompatObj.parentElement.removeChild(oopCompatObj);
- if (!isOOPCompatible)
- this.set('state.previewFailed', true);
- else
- this.set('state.previewLoading', true);
+ if (!isOOPCompatible) {
+ this.previewState_ = PreviewAreaState_.PREVIEW_FAILED;
+ this.fire('preview-failed');
+ }
+ },
+
+ /** @return {boolean} Whether the preview is loaded. */
+ previewLoaded: function() {
+ return this.previewState_ == PreviewAreaState_.DISPLAY_PREVIEW;
},
/** @private */
onSettingsChanged_: function() {
- if (!this.state.initialized || !this.getSetting('scaling').valid ||
- !this.getSetting('pages').valid || !this.getSetting('copies').valid ||
- !this.destination || !this.destination.capabilities) {
+ if (this.state == print_preview_new.State.READY) {
+ this.startPreview_();
return;
}
+ this.requestPreviewWhenReady_ = true;
+ },
+
+ /**
+ * @return {string} 'invisible' if overlay is invisible, '' otherwise.
+ * @private
+ */
+ getInvisible_: function() {
+ return this.previewLoaded() ? 'invisible' : '';
+ },
+
+ /**
+ * @return {boolean} Whether the preview is currently loading.
+ * @private
+ */
+ isPreviewLoading_: function() {
+ return this.previewState_ == PreviewAreaState_.LOADING;
+ },
+
+ /**
+ * @return {string} 'jumping-dots' to enable animation, '' otherwise.
+ * @private
+ */
+ getJumpingDots_: function() {
+ return this.isPreviewLoading_() ? 'jumping-dots' : '';
+ },
+
+ /**
+ * @return {boolean} Whether the system dialog button should be shown.
+ * @private
+ */
+ displaySystemDialogButton_: function() {
+ return this.previewState_ == PreviewAreaState_.INVALID_SETTINGS ||
+ this.previewState_ == PreviewAreaState_.OPEN_IN_PREVIEW;
+ },
+
+ /**
+ * @return {string} The current preview area message to display.
+ * @private
+ */
+ currentMessage_: function() {
+ if (this.previewState_ == PreviewAreaState_.LOADING)
+ return this.i18n('loading');
+ if (this.previewState_ == PreviewAreaState_.OPEN_IN_PREVIEW)
+ return this.i18n('openPdfInPreview');
+ if (this.previewState_ == PreviewAreaState_.INVALID_SETTINGS)
+ return this.i18n('invalidSettings');
+ if (this.previewState_ == PreviewAreaState_.PREVIEW_FAILED)
+ return this.i18n('previewFailed');
+ return '';
+ },
+
+ /** @private */
+ startPreview_: function() {
+ this.previewState_ = PreviewAreaState_.LOADING;
this.documentReady_ = false;
- this.set('state.previewLoading', true);
this.getPreview_().then(
previewUid => {
if (!this.documentInfo.isModifiable)
this.onPreviewStart_(previewUid, -1);
this.documentReady_ = true;
- if (this.pluginLoaded_)
- this.set('state.previewLoading', false);
+ if (this.pluginLoaded_) {
+ this.previewState_ = PreviewAreaState_.DISPLAY_PREVIEW;
+ this.fire('preview-loaded');
+ }
},
type => {
if (/** @type{string} */ (type) == 'SETTINGS_INVALID')
- this.set('state.invalidSettings', true);
- else if (/** @type{string} */ (type) != 'CANCELLED')
- this.set('state.previewFailed', true);
- this.set('state.previewLoading', false);
+ this.previewState_ = PreviewAreaState_.INVALID_SETTINGS;
+ else if (/** @type{string} */ (type) != 'CANCELLED') {
+ this.previewState_ = PreviewAreaState_.PREVIEW_FAILED;
+ this.fire('preview-failed');
+ }
});
},
- /**
- * Set the visibility of the message overlay.
- * @param {boolean} visible Whether to make the overlay visible or not
- * @private
- */
- setOverlayVisible_: function(visible) {
- const overlayEl = this.$$('.preview-area-overlay-layer');
- overlayEl.classList.toggle('invisible', !visible);
- overlayEl.setAttribute('aria-hidden', !visible);
- },
-
/** @private */
- onPreviewStateChanged_: function() {
- // update the appearance here.
- const visible = this.state.previewLoading || this.state.previewFailed ||
- this.state.invalidSettings;
- this.setOverlayVisible_(visible);
-
- // Disable jumping animation to conserve cycles.
- const jumpingDotsEl = this.$$('.preview-area-loading-message-jumping-dots');
- jumpingDotsEl.classList.toggle('jumping-dots', this.state.previewLoading);
+ onStateChanged_: function() {
+ switch (this.state) {
+ case (print_preview_new.State.NOT_READY):
+ // Resetting the destination clears the invalid settings error.
+ this.previewState_ = PreviewAreaState_.LOADING;
+ break;
+ case (print_preview_new.State.READY):
+ // Request a new preview.
+ if (this.requestPreviewWhenReady_) {
+ this.startPreview_();
+ this.requestPreviewWhenReady_ = false;
+ }
+ break;
+ case (print_preview_new.State.INVALID_PRINTER):
+ this.previewState_ = PreviewAreaState_.INVALID_SETTINGS;
+ break;
+ default:
+ break;
+ }
},
/**
@@ -240,8 +311,10 @@ Polymer({
*/
onPluginLoad_: function() {
this.pluginLoaded_ = true;
- if (this.documentReady_)
- this.set('state.previewLoading', false);
+ if (this.documentReady_) {
+ this.previewState_ = PreviewAreaState_.DISPLAY_PREVIEW;
+ this.fire('preview-loaded');
+ }
},
/**
@@ -345,7 +418,7 @@ Polymer({
printWithCloudPrint: !this.destination.isLocal,
printWithPrivet: this.destination.isPrivet,
printWithExtension: this.destination.isExtension,
- rasterizePDF: false,
+ rasterizePDF: this.getSettingValue('rasterize'),
};
// Set 'cloudPrintID' only if the this.destination is not local.
@@ -372,3 +445,4 @@ Polymer({
return this.nativeLayer_.getPreview(JSON.stringify(ticket), pageCount);
},
});
+})();
diff --git a/chromium/chrome/browser/resources/print_preview/new/print_preview_search_box.html b/chromium/chrome/browser/resources/print_preview/new/print_preview_search_box.html
new file mode 100644
index 00000000000..50e421bb32b
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/print_preview_search_box.html
@@ -0,0 +1,41 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/cr_elements/cr_search_field/cr_search_field_behavior.html">
+<link rel="import" href="print_preview_shared_css.html">
+
+<dom-module id="print-preview-search-box">
+ <template>
+ <style include="print-preview-shared">
+ :host {
+ display: flex;
+ position: relative;
+ user-select: none;
+ }
+
+ .search-box-icon {
+ display: inline-block;
+ height: 1em;
+ left: 8px;
+ position: absolute;
+ right: 8px;
+ top: 6px;
+ user-select: none;
+ width: 1em;
+ }
+
+ .search-box-input {
+ text-indent: 2em;
+ width: 100%;
+ }
+
+ .search-box-input::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+ }
+ </style>
+ <img src="../images/search.png" class="search-box-icon" alt="">
+ <input type="search" id="searchInput" class="search-box-input"
+ on-search="onSearchTermSearch" on-input="onSearchTermInput"
+ incremental aria-label$="[[label]]" placeholder="[[label]]">
+ </template>
+ <script src="print_preview_search_box.js"></script>
+</dom-module>
diff --git a/chromium/chrome/browser/resources/print_preview/new/print_preview_search_box.js b/chromium/chrome/browser/resources/print_preview/new/print_preview_search_box.js
new file mode 100644
index 00000000000..c8890137541
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/print_preview_search_box.js
@@ -0,0 +1,42 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+(function() {
+'use strict';
+
+/** @type {!RegExp} */
+const SANITIZE_REGEX = /[-[\]{}()*+?.,\\^$|#\s]/g;
+
+Polymer({
+ is: 'print-preview-search-box',
+
+ behaviors: [CrSearchFieldBehavior],
+
+ properties: {
+ /** @type {?RegExp} */
+ searchQuery: {
+ type: Object,
+ notify: true,
+ },
+ },
+
+ listeners: {
+ 'search-changed': 'onSearchChanged_',
+ },
+
+ /** @return {!HTMLInputElement} */
+ getSearchInput: function() {
+ return this.$.searchInput;
+ },
+
+ /**
+ * @param {!CustomEvent} e Event containing the new search.
+ */
+ onSearchChanged_: function(e) {
+ const safeQuery = e.detail.trim().replace(SANITIZE_REGEX, '\\$&');
+ this.searchQuery =
+ safeQuery.length > 0 ? new RegExp(`(${safeQuery})`, 'i') : null;
+ },
+});
+})();
diff --git a/chromium/chrome/browser/resources/print_preview/new/scaling_settings.html b/chromium/chrome/browser/resources/print_preview/new/scaling_settings.html
index fbb0f21462f..6fc479afebf 100644
--- a/chromium/chrome/browser/resources/print_preview/new/scaling_settings.html
+++ b/chromium/chrome/browser/resources/print_preview/new/scaling_settings.html
@@ -12,12 +12,14 @@
</style>
<print-preview-number-settings-section max-value=200 min-value=10
default-value="100" input-label="$i18n{scalingLabel}"
- input-string="{{inputString_}}" input-valid="{{inputValid_}}"
- hint-message="$i18n{scalingInstruction}" class="multirow-controls">
+ disabled="[[disabled]]" current-value="{{currentValue_}}"
+ input-valid="{{inputValid_}}" hint-message="$i18n{scalingInstruction}"
+ class="multirow-controls">
<div slot="opt-outside-content" class="checkbox"
hidden$="[[!settings.fitToPage.available]]">
<label aria-live="polite">
<input type="checkbox" id="fit-to-page-checkbox"
+ disabled$="[[getDisabled_(disabled, inputValid_)]]"
on-change="onFitToPageChange_">
<span>$i18n{optionFitToPage}</span>
</label>
diff --git a/chromium/chrome/browser/resources/print_preview/new/scaling_settings.js b/chromium/chrome/browser/resources/print_preview/new/scaling_settings.js
index 8117771923e..4588514c6a8 100644
--- a/chromium/chrome/browser/resources/print_preview/new/scaling_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/new/scaling_settings.js
@@ -12,10 +12,12 @@ Polymer({
documentInfo: Object,
/** @private {string} */
- inputString_: String,
+ currentValue_: String,
/** @private {boolean} */
inputValid_: Boolean,
+
+ disabled: Boolean,
},
/** @private {string} */
@@ -27,7 +29,7 @@ Polymer({
observers: [
'onFitToPageSettingChange_(settings.fitToPage.value, ' +
'settings.fitToPage.available, documentInfo.fitToPageScaling)',
- 'onInputChanged_(inputString_, inputValid_)',
+ 'onInputChanged_(currentValue_, inputValid_)',
'onScalingSettingChanged_(settings.scaling.value)',
],
@@ -39,13 +41,13 @@ Polymer({
this.$$('#fit-to-page-checkbox').checked = fitToPage.value;
if (!fitToPage.value) {
// Fit to page is no longer checked. Update the display.
- this.inputString_ = this.lastValidScaling_;
+ this.currentValue_ = this.lastValidScaling_;
} else if (fitToPage.value) {
// Set flag to number of expected calls to onInputChanged_. If scaling
- // is valid, 1 call will occur due to the change to |inputString_|. If
+ // is valid, 1 call will occur due to the change to |currentValue_|. If
// not, 2 calls will occur, since |inputValid_| will also change.
this.fitToPageFlag_ = this.inputValid_ ? 1 : 2;
- this.inputString_ = this.documentInfo.fitToPageScaling;
+ this.currentValue_ = this.documentInfo.fitToPageScaling;
}
},
@@ -57,7 +59,7 @@ Polymer({
// Update last valid scaling and ensure input string matches.
this.lastValidScaling_ =
/** @type {string} */ (this.getSetting('scaling').value);
- this.inputString_ = this.lastValidScaling_;
+ this.currentValue_ = this.lastValidScaling_;
},
/**
@@ -70,8 +72,9 @@ Polymer({
if (fitToPage && this.fitToPageFlag_ == 0) {
// User modified scaling while fit to page was checked. Uncheck fit to
// page.
- if (this.inputValid_)
- this.setSetting('scaling', this.inputString_);
+ const wasValid = this.getSetting('scaling').valid;
+ if (this.inputValid_ && wasValid)
+ this.setSetting('scaling', this.currentValue_);
else
this.setSettingValid('scaling', false);
this.$$('#fit-to-page-checkbox').checked = false;
@@ -83,9 +86,10 @@ Polymer({
} else {
// User modified scaling while fit to page was not checked or
// scaling setting was set.
+ const wasValid = this.getSetting('scaling').valid;
this.setSettingValid('scaling', this.inputValid_);
- if (this.inputValid_)
- this.setSetting('scaling', this.inputString_);
+ if (this.inputValid_ && wasValid)
+ this.setSetting('scaling', this.currentValue_);
}
},
@@ -93,4 +97,12 @@ Polymer({
onFitToPageChange_: function() {
this.setSetting('fitToPage', this.$$('#fit-to-page-checkbox').checked);
},
+
+ /**
+ * @return {boolean} Whether the input should be disabled.
+ * @private
+ */
+ getDisabled_: function() {
+ return this.disabled && this.inputValid_;
+ },
});
diff --git a/chromium/chrome/browser/resources/print_preview/new/search_dialog_css.html b/chromium/chrome/browser/resources/print_preview/new/search_dialog_css.html
new file mode 100644
index 00000000000..6ec25973375
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/search_dialog_css.html
@@ -0,0 +1,48 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/cr_elements/cr_dialog/cr_dialog.html">
+<link rel="import" href="button_css.html">
+<link rel="import" href="print_preview_search_box.html">
+<link rel="import" href="print_preview_shared_css.html">
+
+<dom-module id="search-dialog">
+ <template>
+ <style include="print-preview-shared button">
+ #dialog::backdrop {
+ background-color: rgba(255, 255, 255, 0.75);
+ }
+
+ #dialog {
+ --cr-dialog-close-image: {
+ background-image: url(chrome://theme/IDR_CLOSE_DIALOG);
+ }
+ --cr-dialog-close-image-active: {
+ background-image: url(chrome://theme/IDR_CLOSE_DIALOG_P);
+ }
+ --cr-dialog-close-image-hover: {
+ background-image: url(chrome://theme/IDR_CLOSE_DIALOG_H);
+ }
+ --cr-icon-ripple-size: 0;
+ --cr-icon-size: 14px;
+ --cr-dialog-body: {
+ box-sizing: border-box;
+ padding-top: 0;
+ }
+ --cr-dialog-wrapper: {
+ max-height: calc(100vh - 40px);
+ }
+ box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2),
+ 0 2px 6px rgba(0, 0, 0, 0.15);
+ }
+
+ #searchBox {
+ font-size: calc(13/15 * 1em);
+ margin-top: 14px;
+ }
+
+ #body {
+ height: 100vh;
+ }
+ </style>
+ </template>
+</dom-module>
diff --git a/chromium/chrome/browser/resources/print_preview/new/select_css.html b/chromium/chrome/browser/resources/print_preview/new/select_css.html
index 5a316158da1..04838c08b04 100644
--- a/chromium/chrome/browser/resources/print_preview/new/select_css.html
+++ b/chromium/chrome/browser/resources/print_preview/new/select_css.html
@@ -21,14 +21,14 @@
select:enabled:hover {
background-image: url(chrome://resources/images/select.png),
linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
- @apply(--print-preview-hover);
+ @apply --print-preview-hover;
}
</if>
select:enabled:active {
background-image: url(chrome://resources/images/select.png),
linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
- @apply(--print-preview-active);
+ @apply --print-preview-active;
}
select:disabled {
diff --git a/chromium/chrome/browser/resources/print_preview/new/settings_behavior.js b/chromium/chrome/browser/resources/print_preview/new/settings_behavior.js
index aa4c95dc575..b28e5fcd8ae 100644
--- a/chromium/chrome/browser/resources/print_preview/new/settings_behavior.js
+++ b/chromium/chrome/browser/resources/print_preview/new/settings_behavior.js
@@ -91,6 +91,8 @@ const SettingsBehavior = {
// is no way for the user to change the value in this case.
if (!valid)
assert(setting.available, 'Setting is not available: ' + settingName);
+ if (valid != setting.valid)
+ this.fire('setting-valid-changed', valid);
this.set(`settings.${settingName}.valid`, valid);
}
};
diff --git a/chromium/chrome/browser/resources/print_preview/new/settings_select.html b/chromium/chrome/browser/resources/print_preview/new/settings_select.html
index 0525c00e2b5..9b867bec3b0 100644
--- a/chromium/chrome/browser/resources/print_preview/new/settings_select.html
+++ b/chromium/chrome/browser/resources/print_preview/new/settings_select.html
@@ -9,9 +9,10 @@
<template>
<style include="print-preview-shared select">
</style>
- <select on-change="onChange_">
+ <select on-change="onChange_" disabled$="[[disabled]]">
<template is="dom-repeat" items="[[capability.option]]">
- <option selected="[[item.is_default]]" value="[[getValue_(item)]]">
+ <option selected="[[isSelected_(item, selectedValue_)]]"
+ value="[[getValue_(item)]]">
[[getDisplayName_(item)]]
</option>
</template>
diff --git a/chromium/chrome/browser/resources/print_preview/new/settings_select.js b/chromium/chrome/browser/resources/print_preview/new/settings_select.js
index aba0680710a..4877620f573 100644
--- a/chromium/chrome/browser/resources/print_preview/new/settings_select.js
+++ b/chromium/chrome/browser/resources/print_preview/new/settings_select.js
@@ -23,13 +23,31 @@ Polymer({
/** @type {{ option: Array<!print_preview_new.SelectOption> }} */
capability: Object,
- /** @type {string} */
settingName: String,
+
+ disabled: Boolean,
+
+ /** @private {string} */
+ selectedValue_: {
+ type: String,
+ notify: true,
+ value: '',
+ },
+ },
+
+ /**
+ * @param {!print_preview_new.SelectOption} option Option to check.
+ * @return {boolean} Whether the option is selected.
+ * @private
+ */
+ isSelected_: function(option) {
+ return this.getValue_(option) == this.selectedValue_ ||
+ (!!option.is_default && this.selectedValue_ == '');
},
/** @param {string} value The value to select. */
selectValue: function(value) {
- this.$$('select').value = value;
+ this.selectedValue_ = value;
},
/**
diff --git a/chromium/chrome/browser/resources/print_preview/new/state.html b/chromium/chrome/browser/resources/print_preview/new/state.html
new file mode 100644
index 00000000000..12da96dc34d
--- /dev/null
+++ b/chromium/chrome/browser/resources/print_preview/new/state.html
@@ -0,0 +1,5 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/html/cr.html">
+
+<script src="state.js"></script>
diff --git a/chromium/chrome/browser/resources/print_preview/new/state.js b/chromium/chrome/browser/resources/print_preview/new/state.js
index 99cb889343e..26d1a636918 100644
--- a/chromium/chrome/browser/resources/print_preview/new/state.js
+++ b/chromium/chrome/browser/resources/print_preview/new/state.js
@@ -1,18 +1,68 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
+// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.exportPath('print_preview_new');
-/**
- * @typedef {{
- * previewLoading: boolean,
- * previewFailed: boolean,
- * cloudPrintError: string,
- * privetExtensionError: string,
- * invalidSettings: boolean,
- * initialized: boolean,
- * cancelled: boolean,
- * }}
- */
-print_preview_new.State;
+/** @enum {number} */
+print_preview_new.State = {
+ NOT_READY: 0,
+ READY: 1,
+ HIDDEN: 2,
+ PRINTING: 3,
+ INVALID_TICKET: 4,
+ INVALID_PRINTER: 5,
+ FATAL_ERROR: 6,
+ CLOSING: 7,
+};
+
+Polymer({
+ is: 'print-preview-state',
+
+ properties: {
+ /** @type {print_preview_new.State} */
+ state: {
+ type: Number,
+ notify: true,
+ value: print_preview_new.State.NOT_READY,
+ },
+ },
+
+ /** @param {print_preview_new.State} newState The state to transition to. */
+ transitTo: function(newState) {
+ switch (newState) {
+ case (print_preview_new.State.NOT_READY):
+ assert(
+ this.state == print_preview_new.State.NOT_READY ||
+ this.state == print_preview_new.State.READY ||
+ this.state == print_preview_new.State.INVALID_PRINTER);
+ break;
+ case (print_preview_new.State.READY):
+ assert(
+ this.state == print_preview_new.State.INVALID_TICKET ||
+ this.state == print_preview_new.State.NOT_READY ||
+ this.state == print_preview_new.State.PRINTING);
+ break;
+ case (print_preview_new.State.HIDDEN):
+ assert(this.state == print_preview_new.State.READY);
+ break;
+ case (print_preview_new.State.PRINTING):
+ assert(
+ this.state == print_preview_new.State.READY ||
+ this.state == print_preview_new.State.HIDDEN);
+ break;
+ case (print_preview_new.State.INVALID_TICKET):
+ assert(this.state == print_preview_new.State.READY);
+ break;
+ case (print_preview_new.State.INVALID_PRINTER):
+ assert(
+ this.state == print_preview_new.State.NOT_READY ||
+ this.state == print_preview_new.State.READY);
+ break;
+ case (print_preview_new.State.CLOSING):
+ assert(this.state != print_preview_new.State.HIDDEN);
+ break;
+ }
+ this.state = newState;
+ },
+});
diff --git a/chromium/chrome/browser/resources/print_preview/preview_generator.js b/chromium/chrome/browser/resources/print_preview/preview_generator.js
index 4cfb3d90ede..832529cca5f 100644
--- a/chromium/chrome/browser/resources/print_preview/preview_generator.js
+++ b/chromium/chrome/browser/resources/print_preview/preview_generator.js
@@ -101,6 +101,12 @@ cr.define('print_preview', function() {
this.scalingValue_ = 100;
/**
+ * Whether the PDF should be rasterized for printing.
+ * @private {boolean}
+ */
+ this.rasterize_ = false;
+
+ /**
* Page ranges setting used used to generate the last preview.
* @private {Array<{from: number, to: number}>}
*/
@@ -176,6 +182,7 @@ cr.define('print_preview', function() {
this.printTicketStore_.headerFooter.getValue();
this.colorValue_ = this.printTicketStore_.color.getValue();
this.isFitToPageEnabled_ = this.printTicketStore_.fitToPage.getValue();
+ this.rasterize_ = this.printTicketStore_.rasterize.getValue();
this.scalingValue_ = this.printTicketStore_.scaling.getValueAsNumber();
this.pageRanges_ = this.printTicketStore_.pageRange.getPageRanges();
this.marginsType_ = this.printTicketStore_.marginsType.getValue();
@@ -240,7 +247,7 @@ cr.define('print_preview', function() {
printWithCloudPrint: !destination.isLocal,
printWithPrivet: destination.isPrivet,
printWithExtension: destination.isExtension,
- rasterizePDF: false,
+ rasterizePDF: printTicketStore.rasterize.getValue(),
shouldPrintBackgrounds: printTicketStore.cssBackground.getValue(),
shouldPrintSelectionOnly: printTicketStore.selectionOnly.getValue()
};
@@ -321,6 +328,7 @@ cr.define('print_preview', function() {
!ticketStore.color.isValueEqual(this.colorValue_) ||
!ticketStore.scaling.isValueEqual(this.scalingValue_) ||
!ticketStore.fitToPage.isValueEqual(this.isFitToPageEnabled_) ||
+ !ticketStore.rasterize.isValueEqual(this.rasterize_) ||
(!ticketStore.marginsType.isValueEqual(this.marginsType_) &&
!ticketStore.marginsType.isValueEqual(
print_preview.ticket_items.MarginsTypeValue.CUSTOM)) ||
diff --git a/chromium/chrome/browser/resources/print_preview/previewarea/preview_area.css b/chromium/chrome/browser/resources/print_preview/previewarea/preview_area.css
index a6ce9ed4fae..fd272fa1824 100644
--- a/chromium/chrome/browser/resources/print_preview/previewarea/preview_area.css
+++ b/chromium/chrome/browser/resources/print_preview/previewarea/preview_area.css
@@ -80,7 +80,6 @@
}
#preview-area .learn-more-link {
- -webkit-margin-start: 0.5em;
color: rgb(51, 103, 214);
}
diff --git a/chromium/chrome/browser/resources/print_preview/previewarea/preview_area.js b/chromium/chrome/browser/resources/print_preview/previewarea/preview_area.js
index d1cb9dedd2d..89ceb0d1654 100644
--- a/chromium/chrome/browser/resources/print_preview/previewarea/preview_area.js
+++ b/chromium/chrome/browser/resources/print_preview/previewarea/preview_area.js
@@ -81,7 +81,8 @@ cr.define('print_preview', function() {
this.printTicketStore_ = printTicketStore;
/**
- * Used to contruct the preview generator.
+ * Used to construct the preview generator and to open the GCP learn more
+ * help link.
* @type {!print_preview.NativeLayer}
* @private
*/
@@ -149,6 +150,13 @@ cr.define('print_preview', function() {
this.isDocumentReady_ = false;
/**
+ * Whether the current destination is valid.
+ * @type {boolean}
+ * @private
+ */
+ this.isDestinationValid_ = true;
+
+ /**
* Timeout object used to display a loading message if the preview is taking
* a long time to generate.
* @type {?number}
@@ -213,7 +221,8 @@ cr.define('print_preview', function() {
'preview-area-open-system-dialog-button-throbber',
OVERLAY: 'preview-area-overlay-layer',
MARGIN_CONTROL: 'margin-control',
- PREVIEW_AREA: 'preview-area-plugin-wrapper'
+ PREVIEW_AREA: 'preview-area-plugin-wrapper',
+ GCP_ERROR_LEARN_MORE_LINK: 'learn-more-link'
};
/**
@@ -321,6 +330,16 @@ cr.define('print_preview', function() {
this.showMessage_(print_preview.PreviewAreaMessageId_.CUSTOM, message);
},
+ /** @param {boolean} valid Whether the current destination is valid. */
+ setDestinationValid: function(valid) {
+ this.isDestinationValid_ = valid;
+ // If destination is valid and preview is ready, hide the error message.
+ if (valid && this.isPluginReloaded_ && this.isDocumentReady_) {
+ this.setOverlayVisible_(false);
+ this.dispatchPreviewGenerationDoneIfReady_();
+ }
+ },
+
/** @override */
enterDocument: function() {
print_preview.Component.prototype.enterDocument.call(this);
@@ -328,6 +347,9 @@ cr.define('print_preview', function() {
this.tracker.add(
assert(this.openSystemDialogButton_), 'click',
this.onOpenSystemDialogButtonClick_.bind(this));
+ this.tracker.add(
+ assert(this.gcpErrorLearnMoreLink_), 'click',
+ this.onGcpErrorLearnMoreClick_.bind(this));
const TicketStoreEvent = print_preview.PrintTicketStore.EventType;
[TicketStoreEvent.INITIALIZE, TicketStoreEvent.CAPABILITIES_CHANGE,
@@ -377,6 +399,7 @@ cr.define('print_preview', function() {
print_preview.Component.prototype.exitDocument.call(this);
this.overlayEl_ = null;
this.openSystemDialogButton_ = null;
+ this.gcpErrorLearnMoreLink_ = null;
},
/** @override */
@@ -386,6 +409,8 @@ cr.define('print_preview', function() {
PreviewArea.Classes_.OVERLAY)[0];
this.openSystemDialogButton_ = this.getElement().getElementsByClassName(
PreviewArea.Classes_.OPEN_SYSTEM_DIALOG_BUTTON)[0];
+ this.gcpErrorLearnMoreLink_ = this.getElement().getElementsByClassName(
+ PreviewArea.Classes_.GCP_ERROR_LEARN_MORE_LINK)[0];
},
/**
@@ -526,11 +551,22 @@ cr.define('print_preview', function() {
},
/**
+ * Called when the learn more link for a cloud destination with an invalid
+ * certificate is clicked. Calls nativeLayer to open a new tab with the help
+ * page.
+ * @private
+ */
+ onGcpErrorLearnMoreClick_: function() {
+ this.nativeLayer_.forceOpenNewTab(
+ loadTimeData.getString('gcpCertificateErrorLearnMoreURL'));
+ },
+
+ /**
* Called when the print ticket changes. Updates the preview.
* @private
*/
onTicketChange_: function() {
- if (!this.previewGenerator_)
+ if (!this.previewGenerator_ || !this.isDestinationValid_)
return;
const previewRequest = this.previewGenerator_.requestPreview();
if (previewRequest.id <= -1) {
diff --git a/chromium/chrome/browser/resources/print_preview/print_preview.js b/chromium/chrome/browser/resources/print_preview/print_preview.js
index 918f2ad7db4..ce0d488008a 100644
--- a/chromium/chrome/browser/resources/print_preview/print_preview.js
+++ b/chromium/chrome/browser/resources/print_preview/print_preview.js
@@ -1026,6 +1026,7 @@ cr.define('print_preview', function() {
this.uiState_ = PrintPreviewUiState_.ERROR;
this.isPreviewGenerationInProgress_ = false;
this.printHeader_.isPrintButtonEnabled = false;
+ this.previewArea_.setDestinationValid(false);
},
/**
@@ -1311,8 +1312,10 @@ cr.define('print_preview', function() {
}
// Reset if we had a bad settings fetch since the user selected a new
// printer.
- if (this.uiState_ == PrintPreviewUiState_.ERROR)
+ if (this.uiState_ == PrintPreviewUiState_.ERROR) {
this.uiState_ = PrintPreviewUiState_.READY;
+ this.previewArea_.setDestinationValid(true);
+ }
if (this.destinationStore_.selectedDestination &&
this.isInKioskAutoPrintMode_) {
this.onPrintButtonClick_();
diff --git a/chromium/chrome/browser/resources/print_preview/print_preview_new.html b/chromium/chrome/browser/resources/print_preview/print_preview_new.html
index 303438ada23..b7a82538e9b 100644
--- a/chromium/chrome/browser/resources/print_preview/print_preview_new.html
+++ b/chromium/chrome/browser/resources/print_preview/print_preview_new.html
@@ -1,11 +1,14 @@
<!doctype html>
<html dir="$i18n{textdirection}" lang="$i18n{language}">
<head>
-<meta charset="utf-8">
+ <meta charset="utf-8">
<title></title>
-</head>
-<body>
<style>
+ html {
+ /* Remove 300ms delay for 'click' event, when using touch interface. */
+ touch-action: manipulation;
+ }
+
html,
body {
height: 100%;
@@ -14,6 +17,8 @@
width: 100%;
}
</style>
+</head>
+<body>
<print-preview-app></print-preview-app>
<link rel="stylesheet" href="chrome://resources/css/text_defaults.css">
<link rel="import" href="new/app.html">
diff --git a/chromium/chrome/browser/resources/print_preview/print_preview_resources.grd b/chromium/chrome/browser/resources/print_preview/print_preview_resources.grd
index 943dce160ce..089ce3ff322 100644
--- a/chromium/chrome/browser/resources/print_preview/print_preview_resources.grd
+++ b/chromium/chrome/browser/resources/print_preview/print_preview_resources.grd
@@ -29,6 +29,12 @@
<structure name="IDR_PRINT_PREVIEW_NEW_MODEL_JS"
file="new/model.js"
type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_CLOUD_PRINT_INTERFACE_HTML"
+ file="cloud_print_interface.html"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_CLOUD_PRINT_INTERFACE_JS"
+ file="cloud_print_interface.js"
+ type="chrome_html" />
<structure name="IDR_PRINT_PREVIEW_NATIVE_LAYER_HTML"
file="native_layer.html"
type="chrome_html" />
@@ -53,12 +59,24 @@
<structure name="IDR_PRINT_PREVIEW_DATA_DESTINATION_STORE_JS"
file="data/destination_store.js"
type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_DATA_CLOUD_PARSERS_HTML"
+ file="data/cloud_parsers.html"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_DATA_CLOUD_PARSERS_JS"
+ file="data/cloud_parsers.js"
+ type="chrome_html" />
<structure name="IDR_PRINT_PREVIEW_DATA_LOCAL_PARSERS_HTML"
file="data/local_parsers.html"
type="chrome_html" />
<structure name="IDR_PRINT_PREVIEW_DATA_LOCAL_PARSERS_JS"
file="data/local_parsers.js"
type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_DATA_INVITATION_HTML"
+ file="data/invitation.html"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_DATA_INVITATION_JS"
+ file="data/invitation.js"
+ type="chrome_html" />
<structure name="IDR_PRINT_PREVIEW_DATA_MARGINS_HTML"
file="data/margins.html"
type="chrome_html" />
@@ -134,6 +152,12 @@
<structure name="IDR_PRINT_PREVIEW_NEW_SETTINGS_BEHAVIOR_JS"
file="new/settings_behavior.js"
type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_STATE_HTML"
+ file="new/state.html"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_STATE_JS"
+ file="new/state.js"
+ type="chrome_html" />
<structure name="IDR_PRINT_PREVIEW_NEW_SETTINGS_SECTION_HTML"
file="new/settings_section.html"
type="chrome_html" />
@@ -212,12 +236,57 @@
<structure name="IDR_PRINT_PREVIEW_NEW_ADVANCED_OPTIONS_SETTINGS_JS"
file="new/advanced_options_settings.js"
type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_ADVANCED_SETTINGS_DIALOG_HTML"
+ file="new/advanced_settings_dialog.html"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_ADVANCED_SETTINGS_DIALOG_JS"
+ file="new/advanced_settings_dialog.js"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_ADVANCED_SETTINGS_ITEM_HTML"
+ file="new/advanced_settings_item.html"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_ADVANCED_SETTINGS_ITEM_JS"
+ file="new/advanced_settings_item.js"
+ type="chrome_html" />
<structure name="IDR_PRINT_PREVIEW_NEW_NUMBER_SETTINGS_SECTION_HTML"
file="new/number_settings_section.html"
type="chrome_html" />
<structure name="IDR_PRINT_PREVIEW_NEW_NUMBER_SETTINGS_SECTION_JS"
file="new/number_settings_section.js"
type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_DESTINATION_DIALOG_HTML"
+ file="new/destination_dialog.html"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_DESTINATION_DIALOG_JS"
+ file="new/destination_dialog.js"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_DESTINATION_LIST_HTML"
+ file="new/destination_list.html"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_DESTINATION_LIST_JS"
+ file="new/destination_list.js"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_DESTINATION_LIST_ITEM_HTML"
+ file="new/destination_list_item.html"
+ type="chrome_html"
+ preprocess="true" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_DESTINATION_LIST_ITEM_JS"
+ file="new/destination_list_item.js"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_PRINT_PREVIEW_SEARCH_BOX_HTML"
+ file="new/print_preview_search_box.html"
+ type="chrome_html"
+ flattenhtml="true"
+ allowexternalscript="true" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_PRINT_PREVIEW_SEARCH_BOX_JS"
+ file="new/print_preview_search_box.js"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_HIGHLIGHT_UTILS_HTML"
+ file="new/highlight_utils.html"
+ type="chrome_html" />
+ <structure name="IDR_PRINT_PREVIEW_NEW_HIGHLIGHT_UTILS_JS"
+ file="new/highlight_utils.js"
+ type="chrome_html" />
<structure name="IDR_PRINT_PREVIEW_NEW_PRINT_PREVIEW_SHARED_CSS_HTML"
file="new/print_preview_shared_css.html"
type="chrome_html"
@@ -240,6 +309,9 @@
<structure name="IDR_PRINT_PREVIEW_NEW_THROBBER_CSS_HTML"
file="new/throbber_css.html"
type="chrome_html"/>
+ <structure name="IDR_PRINT_PREVIEW_NEW_SEARCH_DIALOG_CSS_HTML"
+ file="new/search_dialog_css.html"
+ type="chrome_html"/>
<structure name="IDR_PRINT_PREVIEW_NEW_STRINGS_HTML"
file="new/strings.html"
type="chrome_html" />
diff --git a/chromium/chrome/browser/resources/print_preview/print_preview_utils.js b/chromium/chrome/browser/resources/print_preview/print_preview_utils.js
index cd931980ba4..0220810fe82 100644
--- a/chromium/chrome/browser/resources/print_preview/print_preview_utils.js
+++ b/chromium/chrome/browser/resources/print_preview/print_preview_utils.js
@@ -114,7 +114,7 @@ function pageRangeTextToPageRanges(pageRangeText, opt_totalPageCount) {
opt_totalPageCount ? opt_totalPageCount : MAX_PAGE_NUMBER;
const regex = /^\s*([0-9]*)\s*-\s*([0-9]*)\s*$/;
- const parts = pageRangeText.split(/,/);
+ const parts = pageRangeText.split(/,|\u3001/);
const pageRanges = [];
for (let i = 0; i < parts.length; ++i) {
diff --git a/chromium/chrome/browser/resources/print_preview/print_preview_utils_unittest.gtestjs b/chromium/chrome/browser/resources/print_preview/print_preview_utils_unittest.gtestjs
index ff15d6fa61e..17fa25be71e 100644
--- a/chromium/chrome/browser/resources/print_preview/print_preview_utils_unittest.gtestjs
+++ b/chromium/chrome/browser/resources/print_preview/print_preview_utils_unittest.gtestjs
@@ -86,6 +86,12 @@ TEST_F('PrintPreviewUtilsUnitTest', 'PageRanges', function() {
pageRangeTextToPageRanges("-", null));
assertRangesEqual([[1, 1000000000]],
pageRangeTextToPageRanges("-", 0));
+
+ // https://crbug.com/806165
+ assertRangesEqual([1, 2, 3, 1, 56],
+ pageRangeTextToPageRanges("1\u30012\u30013\u30011\u300156", 100));
+ assertRangesEqual([1, 2, 3, 1, 56],
+ pageRangeTextToPageRanges("1,2,3\u30011\u300156", 100));
});
TEST_F('PrintPreviewUtilsUnitTest', 'InvalidPageRanges', function() {
@@ -101,6 +107,11 @@ TEST_F('PrintPreviewUtilsUnitTest', 'InvalidPageRanges', function() {
pageRangeTextToPageRanges("1,2,56-40", 100));
assertEquals(PageRangeStatus.LIMIT_ERROR,
pageRangeTextToPageRanges("101-110", 100));
+
+ assertEquals(PageRangeStatus.SYNTAX_ERROR,
+ pageRangeTextToPageRanges("1\u30012\u30010\u300156", 100));
+ assertEquals(PageRangeStatus.SYNTAX_ERROR,
+ pageRangeTextToPageRanges("-1,1,2\u3001\u300156", 100));
});
TEST_F('PrintPreviewUtilsUnitTest', 'PageRangeTextToPageList', function() {
diff --git a/chromium/chrome/browser/resources/print_preview/search/destination_list_item.js b/chromium/chrome/browser/resources/print_preview/search/destination_list_item.js
index c6ef0c7b846..e31d26c99c1 100644
--- a/chromium/chrome/browser/resources/print_preview/search/destination_list_item.js
+++ b/chromium/chrome/browser/resources/print_preview/search/destination_list_item.js
@@ -72,6 +72,9 @@ cr.define('print_preview', function() {
this.tracker.add(
this.getChildElement('.register-promo-button'), 'click',
this.onRegisterPromoClicked_.bind(this));
+ this.tracker.add(
+ this.getChildElement('.learn-more-link'), 'click',
+ this.onGcpErrorLearnMoreClick_.bind(this));
},
/** @return {!print_preview.Destination} */
@@ -199,15 +202,10 @@ cr.define('print_preview', function() {
// Initialize the element which renders the destination's connection
// status.
this.getElement().classList.toggle(
- 'stale',
- this.destination_.isOffline ||
- this.destination_.shouldShowInvalidCertificateError);
+ 'stale', this.destination_.isOfflineOrInvalid);
const connectionStatusEl = this.getChildElement('.connection-status');
connectionStatusEl.textContent = this.destination_.connectionStatusText;
- setIsVisible(
- connectionStatusEl,
- this.destination_.isOffline ||
- this.destination_.shouldShowInvalidCertificateError);
+ setIsVisible(connectionStatusEl, this.destination_.isOfflineOrInvalid);
setIsVisible(
this.getChildElement('.learn-more-link'),
this.destination_.shouldShowInvalidCertificateError);
@@ -324,6 +322,16 @@ cr.define('print_preview', function() {
},
/**
+ * Called when the learn more link for an unsupported cloud destination is
+ * clicked. Opens the help page via native layer.
+ * @private
+ */
+ onGcpErrorLearnMoreClick_: function() {
+ print_preview.NativeLayer.getInstance().forceOpenNewTab(
+ loadTimeData.getString('gcpCertificateErrorLearnMoreURL'));
+ },
+
+ /**
* Handles click and 'Enter' key down events for the extension icon element.
* It opens extensions page with the extension associated with the
* destination highlighted.
diff --git a/chromium/chrome/browser/resources/print_preview/search/destination_search.css b/chromium/chrome/browser/resources/print_preview/search/destination_search.css
index fd7fa54103b..8eddc19d078 100644
--- a/chromium/chrome/browser/resources/print_preview/search/destination_search.css
+++ b/chromium/chrome/browser/resources/print_preview/search/destination_search.css
@@ -89,6 +89,10 @@
-webkit-margin-start: 10px;
}
+#destination-search .invitation-cloud-print-information {
+ padding-top: 12px;
+}
+
#destination-search #invitation-process-throbber {
display: block;
}
diff --git a/chromium/chrome/browser/resources/print_preview/search/destination_search.html b/chromium/chrome/browser/resources/print_preview/search/destination_search.html
index 7d3c4c325e0..554855e835c 100644
--- a/chromium/chrome/browser/resources/print_preview/search/destination_search.html
+++ b/chromium/chrome/browser/resources/print_preview/search/destination_search.html
@@ -23,6 +23,9 @@
<button class="invitation-reject-button">$i18n{reject}</button>
<div id="invitation-process-throbber" class="throbber" hidden></div>
</div>
+ <div class="invitation-cloud-print-information">
+ $i18nRaw{registerPrinterInformationMessage}
+ </div>
</div>
<div class="cloudprint-promo gray-bottom-bar" hidden>
<img src="../images/cloud.png" class="icon" alt="">
diff --git a/chromium/chrome/browser/resources/print_preview/search/destination_search.js b/chromium/chrome/browser/resources/print_preview/search/destination_search.js
index 2262082c5eb..6235d6f3c12 100644
--- a/chromium/chrome/browser/resources/print_preview/search/destination_search.js
+++ b/chromium/chrome/browser/resources/print_preview/search/destination_search.js
@@ -791,7 +791,7 @@ cr.define('print_preview', function() {
*/
onWindowResize_: function() {
this.reflowLists_();
- }
+ },
};
// Export
diff --git a/chromium/chrome/browser/resources/print_preview/settings/advanced_settings/advanced_settings_item.js b/chromium/chrome/browser/resources/print_preview/settings/advanced_settings/advanced_settings_item.js
index b5ce64999e5..9c54b548ce9 100644
--- a/chromium/chrome/browser/resources/print_preview/settings/advanced_settings/advanced_settings_item.js
+++ b/chromium/chrome/browser/resources/print_preview/settings/advanced_settings/advanced_settings_item.js
@@ -1,24 +1,6 @@
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-/**
- * Specifies a custom vendor capability.
- * @typedef {{
- * id: (string),
- * display_name: (string),
- * localized_display_name: (string | undefined),
- * type: (string),
- * select_cap: ({
- * option: (Array<{
- * display_name: (string),
- * type: (string | undefined),
- * value: (number | string | boolean),
- * is_default: (boolean | undefined)
- * }>|undefined)
- * }|undefined)
- * }}
- */
-print_preview.VendorCapability;
cr.define('print_preview', function() {
'use strict';
diff --git a/chromium/chrome/browser/resources/print_preview/settings/destination_settings.js b/chromium/chrome/browser/resources/print_preview/settings/destination_settings.js
index f8061808678..04b0362e2c4 100644
--- a/chromium/chrome/browser/resources/print_preview/settings/destination_settings.js
+++ b/chromium/chrome/browser/resources/print_preview/settings/destination_settings.js
@@ -139,15 +139,23 @@ cr.define('print_preview', function() {
locationEl.textContent = hint;
locationEl.title = hint;
- const connectionStatusText = destination.connectionStatusText;
+ const showDestinationInvalid =
+ (destination.hasInvalidCertificate &&
+ !loadTimeData.getBoolean('isEnterpriseManaged'));
+ let connectionStatusText = '';
+ if (showDestinationInvalid) {
+ connectionStatusText =
+ loadTimeData.getString('noLongerSupportedFragment');
+ } else {
+ connectionStatusText = destination.connectionStatusText;
+ }
const connectionStatusEl =
this.getChildElement('.destination-settings-connection-status');
connectionStatusEl.textContent = connectionStatusText;
connectionStatusEl.title = connectionStatusText;
- const hasConnectionError = destination.isOffline ||
- (destination.hasInvalidCertificate &&
- !loadTimeData.getBoolean('isEnterpriseManaged'));
+ const hasConnectionError =
+ destination.isOffline || showDestinationInvalid;
destinationSettingsBoxEl.classList.toggle(
print_preview.DestinationSettingsClasses_.STALE,
hasConnectionError);
diff --git a/chromium/chrome/browser/resources/safe_browsing/download_file_types.asciipb b/chromium/chrome/browser/resources/safe_browsing/download_file_types.asciipb
index 04a91c17835..8634478bccc 100644
--- a/chromium/chrome/browser/resources/safe_browsing/download_file_types.asciipb
+++ b/chromium/chrome/browser/resources/safe_browsing/download_file_types.asciipb
@@ -8,7 +8,7 @@
##
## Top level settings
##
-version_id: 15
+version_id: 16
sampled_ping_probability: 0.01
max_archived_binaries_to_report: 10
default_file_type {
@@ -797,7 +797,7 @@ file_types {
file_types {
# Opened by uTorrent and Transmission (can be a renamed .torrent)
# Added crbug.com/767502
- extension: "btbtskin"
+ extension: "btskin"
uma_value: 299
ping_setting: FULL_PING
}
diff --git a/chromium/chrome/browser/resources/settings/PRESUBMIT.py b/chromium/chrome/browser/resources/settings/PRESUBMIT.py
new file mode 100644
index 00000000000..dcc145b7f91
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/PRESUBMIT.py
@@ -0,0 +1,24 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+
+def _CheckChangeOnUploadOrCommit(input_api, output_api):
+ import sys
+ old_sys_path, cwd = sys.path[:], input_api.PresubmitLocalPath()
+ src_root = input_api.os_path.join(cwd, '..', '..', '..', '..')
+ try:
+ sys.path += [input_api.os_path.join(src_root, 'tools', 'web_dev_style')]
+ import web_dev_style.presubmit_support
+ finally:
+ sys.path = old_sys_path
+ return web_dev_style.presubmit_support.DisallowIncludes(input_api, output_api,
+ '<include> does not work in settings; use HTML imports instead')
+
+
+def CheckChangeOnUpload(input_api, output_api):
+ return _CheckChangeOnUploadOrCommit(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+ return _CheckChangeOnUploadOrCommit(input_api, output_api)
diff --git a/chromium/chrome/browser/resources/settings/a11y_page/a11y_page.html b/chromium/chrome/browser/resources/settings/a11y_page/a11y_page.html
index 8e9979679bd..3714da1774e 100644
--- a/chromium/chrome/browser/resources/settings/a11y_page/a11y_page.html
+++ b/chromium/chrome/browser/resources/settings/a11y_page/a11y_page.html
@@ -24,7 +24,7 @@
pref="{{prefs.settings.a11y.enable_menu}}">
</settings-toggle-button>
<div id="subpage-trigger" class="settings-box two-line"
- on-tap="onManageAccessibilityFeaturesTap_" actionable>
+ on-click="onManageAccessibilityFeaturesTap_" actionable>
<div class="start">
$i18n{manageAccessibilityFeatures}
<div class="secondary" id="themesSecondary">
diff --git a/chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.html b/chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.html
index 08720eb78c2..59420dcd63f 100644
--- a/chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.html
+++ b/chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.html
@@ -16,11 +16,7 @@
-webkit-padding-start: var(--settings-box-row-padding);
}
- .list-item settings-dropdown-menu {
- -webkit-margin-start: 16px;
- }
-
- .sub-item > .start {
+ .sub-item {
-webkit-margin-start: var(--settings-indent-width);
}
@@ -50,7 +46,7 @@
</settings-toggle-button>
<iron-collapse opened="[[prefs.settings.accessibility.value]]">
<div class="settings-box"
- on-tap="onChromeVoxSettingsTap_" actionable>
+ on-click="onChromeVoxSettingsTap_" actionable>
<div class="start">$i18n{chromeVoxOptionsLabel}</div>
<button class="icon-external" is="paper-icon-button-light"
aria-label="$i18n{chromeVoxOptionsLabel}"></button>
@@ -63,7 +59,7 @@
</settings-toggle-button>
<iron-collapse opened="[[prefs.settings.a11y.select_to_speak.value]]">
<div class="settings-box"
- on-tap="onSelectToSpeakSettingsTap_" actionable>
+ on-click="onSelectToSpeakSettingsTap_" actionable>
<div class="start">$i18n{selectToSpeakOptionsLabel}</div>
<button class="icon-external" is="paper-icon-button-light"
aria-label="$i18n{selectToSpeakOptionsLabel}"></button>
@@ -77,9 +73,33 @@
</settings-toggle-button>
<settings-toggle-button class="continuation"
pref="{{prefs.settings.a11y.screen_magnifier}}"
- label="$i18n{screenMagnifierLabel}">
+ label="$i18n{screenMagnifierLabel}"
+ disabled="[[prefs.ash.docked_magnifier.enabled.value]]">
</settings-toggle-button>
- <div class="settings-box two-line" on-tap="onDisplayTap_" actionable>
+ <div class="settings-box continuation">
+ <div class="start sub-item">$i18n{screenMagnifierZoomLabel}</div>
+ <settings-dropdown-menu label="$i18n{screenMagnifierZoomLabel}"
+ pref="{{prefs.settings.a11y.screen_magnifier_scale}}"
+ menu-options="[[screenMagnifierZoomOptions_]]"
+ disabled="[[!prefs.settings.a11y.screen_magnifier.value]]">
+ </settings-dropdown-menu>
+ </div>
+ <template is="dom-if" if="[[dockedMagnifierFeatureEnabled_]]" restamp>
+ <settings-toggle-button class="continuation"
+ pref="{{prefs.ash.docked_magnifier.enabled}}"
+ label="$i18n{dockedMagnifierLabel}"
+ disabled="[[prefs.settings.a11y.screen_magnifier.value]]">
+ </settings-toggle-button>
+ <div class="settings-box continuation">
+ <div class="start sub-item">$i18n{dockedMagnifierZoomLabel}</div>
+ <settings-dropdown-menu label="$i18n{dockedMagnifierZoomLabel}"
+ pref="{{prefs.ash.docked_magnifier.scale}}"
+ menu-options="[[screenMagnifierZoomOptions_]]"
+ disabled="[[!prefs.ash.docked_magnifier.enabled.value]]">
+ </settings-dropdown-menu>
+ </div>
+ </template>
+ <div class="settings-box two-line" on-click="onDisplayTap_" actionable>
<div class="start">
$i18n{displaySettingsTitle}
<div class="secondary">$i18n{displaySettingsDescription}</div>
@@ -88,7 +108,7 @@
aria-label="$i18n{displaySettingsTitle}"
aria-describedby="displaySettingsSecondary"></button>
</div>
- <div class="settings-box two-line" on-tap="onAppearanceTap_" actionable>
+ <div class="settings-box two-line" on-click="onAppearanceTap_" actionable>
<div class="start">
$i18n{appearanceSettingsTitle}
<div class="secondary" id="appearanceSettingsSecondary">
@@ -123,13 +143,13 @@
label="$i18n{switchAccessLabel}">
<button is="paper-icon-button-light"
class="icon-settings" slot="more-actions"
- on-tap="onSwitchAccessSettingsTap_"
+ on-click="onSwitchAccessSettingsTap_"
hidden="[[!prefs.settings.a11y.switch_access.value]]"
aria-label="$i18n{selectToSpeakOptionsLabel}">
</button>
</settings-toggle-button>
</template>
- <div class="settings-box two-line" on-tap="onKeyboardTap_" actionable>
+ <div class="settings-box two-line" on-click="onKeyboardTap_" actionable>
<div class="start">
$i18n{keyboardSettingsTitle}
<div class="secondary" id="keyboardSettingsSecondary">
@@ -146,15 +166,13 @@
pref="{{prefs.settings.a11y.autoclick}}"
label="$i18n{clickOnStopLabel}">
</settings-toggle-button>
- <div class="settings-box block first">
- <div class="list-item sub-item">
- <div class="start">$i18n{delayBeforeClickLabel}</div>
- <settings-dropdown-menu label="$i18n{delayBeforeClickLabel}"
- pref="{{prefs.settings.a11y.autoclick_delay_ms}}"
- menu-options="[[autoClickDelayOptions_]]"
- disabled="[[!prefs.settings.a11y.autoclick.value]]">
- </settings-dropdown-menu>
- </div>
+ <div class="settings-box continuation">
+ <div class="start sub-item">$i18n{delayBeforeClickLabel}</div>
+ <settings-dropdown-menu label="$i18n{delayBeforeClickLabel}"
+ pref="{{prefs.settings.a11y.autoclick_delay_ms}}"
+ menu-options="[[autoClickDelayOptions_]]"
+ disabled="[[!prefs.settings.a11y.autoclick.value]]">
+ </settings-dropdown-menu>
</div>
<settings-toggle-button class="continuation"
pref="{{prefs.settings.touchpad.enable_tap_dragging}}"
@@ -164,23 +182,21 @@
pref="{{prefs.settings.a11y.large_cursor_enabled}}"
label="$i18n{largeMouseCursorLabel}">
</settings-toggle-button>
- <div class="settings-box block continuation"
+ <div class="settings-box continuation"
hidden$="[[!prefs.settings.a11y.large_cursor_enabled.value]]">
- <div class="list-item sub-item">
- <div class="start">$i18n{largeMouseCursorSizeLabel}</div>
- <settings-slider
- pref="{{prefs.settings.a11y.large_cursor_dip_size}}"
- min="25" max="64"
- label-min="$i18n{largeMouseCursorSizeDefaultLabel}"
- label-max="$i18n{largeMouseCursorSizeLargeLabel}">
- </settings-slider>
- </div>
+ <div class="start sub-item">$i18n{largeMouseCursorSizeLabel}</div>
+ <settings-slider
+ pref="{{prefs.settings.a11y.large_cursor_dip_size}}"
+ min="25" max="64"
+ label-min="$i18n{largeMouseCursorSizeDefaultLabel}"
+ label-max="$i18n{largeMouseCursorSizeLargeLabel}">
+ </settings-slider>
</div>
<settings-toggle-button class="continuation"
pref="{{prefs.settings.a11y.cursor_highlight}}"
label="$i18n{cursorHighlightLabel}">
</settings-toggle-button>
- <div class="settings-box two-line" on-tap="onMouseTap_" actionable>
+ <div class="settings-box two-line" on-click="onMouseTap_" actionable>
<div class="start">
$i18n{mouseSettingsTitle}
<div class="secondary" id="mouseSettingsSecondary">
diff --git a/chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.js b/chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.js
index 79e39c1dd0e..090a8a7f74a 100644
--- a/chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.js
+++ b/chromium/chrome/browser/resources/settings/a11y_page/manage_a11y_page.js
@@ -19,6 +19,28 @@ Polymer({
notify: true,
},
+ screenMagnifierZoomOptions_: {
+ readOnly: true,
+ type: Array,
+ value: function() {
+ // These values correspond to the i18n values in settings_strings.grdp.
+ // If these values get changed then those strings need to be changed as
+ // well.
+ return [
+ {value: 2, name: loadTimeData.getString('screenMagnifierZoom2x')},
+ {value: 4, name: loadTimeData.getString('screenMagnifierZoom4x')},
+ {value: 6, name: loadTimeData.getString('screenMagnifierZoom6x')},
+ {value: 8, name: loadTimeData.getString('screenMagnifierZoom8x')},
+ {value: 10, name: loadTimeData.getString('screenMagnifierZoom10x')},
+ {value: 12, name: loadTimeData.getString('screenMagnifierZoom12x')},
+ {value: 14, name: loadTimeData.getString('screenMagnifierZoom14x')},
+ {value: 16, name: loadTimeData.getString('screenMagnifierZoom16x')},
+ {value: 18, name: loadTimeData.getString('screenMagnifierZoom18x')},
+ {value: 20, name: loadTimeData.getString('screenMagnifierZoom20x')},
+ ];
+ },
+ },
+
autoClickDelayOptions_: {
readOnly: true,
type: Array,
@@ -56,6 +78,17 @@ Polymer({
},
},
+ /**
+ * Whether the docked magnifier flag is enabled.
+ * @private {boolean}
+ */
+ dockedMagnifierFeatureEnabled_: {
+ type: Boolean,
+ value: function() {
+ return loadTimeData.getBoolean('dockedMagnifierFeatureEnabled');
+ },
+ },
+
/** @private */
isGuest_: {
type: Boolean,
diff --git a/chromium/chrome/browser/resources/settings/about_page/about_page.html b/chromium/chrome/browser/resources/settings/about_page/about_page.html
index 4f2f097bbe7..9053116b11d 100644
--- a/chromium/chrome/browser/resources/settings/about_page/about_page.html
+++ b/chromium/chrome/browser/resources/settings/about_page/about_page.html
@@ -78,7 +78,7 @@
<if expr="_google_chrome and is_macosx">
#promoteUpdater[disabled] {
- @apply(--cr-secondary-text);
+ @apply --cr-secondary-text;
}
</if>
</style>
@@ -88,7 +88,7 @@
focus-config="[[focusConfig_]]">
<neon-animatable route-path="default">
<div class="settings-box two-line">
- <img id="product-logo" on-tap="onProductLogoTap_"
+ <img id="product-logo" on-click="onProductLogoTap_"
srcset="chrome://theme/current-channel-logo@1x 1x,
chrome://theme/current-channel-logo@2x 2x"
alt="$i18n{aboutProductLogoAlt}">
@@ -153,18 +153,18 @@
<div class="separator" hidden="[[!showButtonContainer_]]"></div>
<span id="buttonContainer" hidden="[[!showButtonContainer_]]">
<paper-button id="relaunch" class="secondary-button"
- hidden="[[!showRelaunch_]]" on-tap="onRelaunchTap_">
+ hidden="[[!showRelaunch_]]" on-click="onRelaunchTap_">
$i18n{aboutRelaunch}
</paper-button>
<if expr="chromeos">
<paper-button id="relaunchAndPowerwash" class="secondary-button"
hidden="[[!showRelaunchAndPowerwash_]]"
- on-tap="onRelaunchAndPowerwashTap_">
+ on-click="onRelaunchAndPowerwashTap_">
$i18n{aboutRelaunchAndPowerwash}
</paper-button>
<paper-button id="checkForUpdates" class="secondary-button"
hidden="[[!showCheckUpdates_]]"
- on-tap="onCheckUpdatesTap_">
+ on-click="onCheckUpdatesTap_">
$i18n{aboutCheckForUpdates}
</paper-button>
</if>
@@ -173,13 +173,13 @@
<if expr="chromeos">
<div id="aboutTPMFirmwareUpdate" class="settings-box two-line"
hidden$="[[!showTPMFirmwareUpdateLineItem_]]"
- on-tap="onTPMFirmwareUpdateTap_" actionable>
+ on-click="onTPMFirmwareUpdateTap_" actionable>
<div class="start">
<div>$i18n{aboutTPMFirmwareUpdateTitle}</div>
<div class="secondary">
$i18n{aboutTPMFirmwareUpdateDescription}
<a href="$i18n{aboutTPMFirmwareUpdateLearnMoreURL}"
- target="_blank" on-tap="onLearnMoreTap_">
+ target="_blank" on-click="onLearnMoreTap_">
$i18n{learnMore}
</a>
</div>
@@ -194,12 +194,12 @@
<div id="promoteUpdater" class="settings-box"
disabled$="[[promoteUpdaterStatus_.disabled]]"
actionable$="[[promoteUpdaterStatus_.actionable]]"
- on-tap="onPromoteUpdaterTap_">
+ on-click="onPromoteUpdaterTap_">
<div class="start">
[[promoteUpdaterStatus_.text]]
<a href="https://support.google.com/chrome/answer/95414"
target="_blank" id="updaterLearnMore"
- on-tap="onLearnMoreTap_">
+ on-click="onLearnMoreTap_">
$i18n{learnMore}
</a>
</div>
@@ -211,21 +211,22 @@
</div>
</template>
</if>
- <div id="help" class="settings-box" on-tap="onHelpTap_" actionable>
+ <div id="help" class="settings-box" on-click="onHelpTap_"
+ actionable>
<div class="start">$i18n{aboutGetHelpUsingChrome}</div>
<button class="icon-external" is="paper-icon-button-light"
aria-labelledby="help"></button>
</div>
<if expr="_google_chrome">
<div id="reportIssue" class="settings-box" actionable
- on-tap="onReportIssueTap_">
+ on-click="onReportIssueTap_">
<div class="start">$i18n{aboutReportAnIssue}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-labelledby="reportIssue"></button>
</div>
</if>
<if expr="chromeos">
- <div class="settings-box" on-tap="onDetailedBuildInfoTap_"
+ <div class="settings-box" on-click="onDetailedBuildInfoTap_"
actionable>
<div class="start">$i18n{aboutDetailedBuildInfo}</div>
<button id="detailed-build-info-trigger" class="subpage-arrow"
diff --git a/chromium/chrome/browser/resources/settings/about_page/channel_switcher_dialog.html b/chromium/chrome/browser/resources/settings/about_page/channel_switcher_dialog.html
index 9f58ef3661b..70cbc55efe9 100644
--- a/chromium/chrome/browser/resources/settings/about_page/channel_switcher_dialog.html
+++ b/chromium/chrome/browser/resources/settings/about_page/channel_switcher_dialog.html
@@ -53,15 +53,15 @@
</iron-selector>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelTap_"
+ <paper-button class="cancel-button" on-click="onCancelTap_"
id="cancel">$i18n{cancel}</paper-button>
<paper-button id="changeChannel" class="action-button"
- on-tap="onChangeChannelTap_"
+ on-click="onChangeChannelTap_"
hidden="[[!shouldShowButtons_.changeChannel]]">
$i18n{aboutChangeChannel}
</paper-button>
<paper-button id="changeChannelAndPowerwash" class="action-button"
- on-tap="onChangeChannelAndPowerwashTap_"
+ on-click="onChangeChannelAndPowerwashTap_"
hidden="[[!shouldShowButtons_.changeChannelAndPowerwash]]">
$i18n{aboutChangeChannelAndPowerwash}
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/about_page/detailed_build_info.html b/chromium/chrome/browser/resources/settings/about_page/detailed_build_info.html
index a1ae3142cd9..08ab773cc7c 100644
--- a/chromium/chrome/browser/resources/settings/about_page/detailed_build_info.html
+++ b/chromium/chrome/browser/resources/settings/about_page/detailed_build_info.html
@@ -39,7 +39,7 @@
<div class="secondary">[[currentlyOnChannelText_]]</div>
</div>
<div class="separator"></div>
- <paper-button on-tap="onChangeChannelTap_"
+ <paper-button on-click="onChangeChannelTap_"
disabled="[[!canChangeChannel_]]">
$i18n{aboutChangeChannel}
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/about_page/update_warning_dialog.html b/chromium/chrome/browser/resources/settings/about_page/update_warning_dialog.html
index 3a336109f1f..e6dd4ab2540 100644
--- a/chromium/chrome/browser/resources/settings/about_page/update_warning_dialog.html
+++ b/chromium/chrome/browser/resources/settings/about_page/update_warning_dialog.html
@@ -16,9 +16,9 @@
</div>
<div slot="button-container">
<paper-button id="cancel" class="cancel-button"
- on-tap="onCancelTap_">$i18n{cancel}</paper-button>
+ on-click="onCancelTap_">$i18n{cancel}</paper-button>
<paper-button id="continue" class="action-button"
- on-tap="onContinueTap_">
+ on-click="onContinueTap_">
$i18n{aboutUpdateWarningContinue}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/android_apps_page/android_apps_page.html b/chromium/chrome/browser/resources/settings/android_apps_page/android_apps_page.html
index a59514198c0..db780b0d718 100644
--- a/chromium/chrome/browser/resources/settings/android_apps_page/android_apps_page.html
+++ b/chromium/chrome/browser/resources/settings/android_apps_page/android_apps_page.html
@@ -23,7 +23,7 @@
<template is="dom-if" if="[[havePlayStoreApp]]" restamp>
<div id="android-apps" class="settings-box two-line first"
actionable$="[[androidAppsInfo.playStoreEnabled]]"
- on-tap="onSubpageTap_">
+ on-click="onSubpageTap_">
<div class="start">
$i18n{androidAppsPageLabel}
<div class="secondary" id="secondaryText"
@@ -43,7 +43,7 @@
<div class="separator"></div>
<paper-button id="enable" class="secondary-button"
disabled="[[isEnforced_(prefs.arc.enabled)]]"
- on-tap="onEnableTap_"
+ on-click="onEnableTap_"
aria-label="$i18n{androidAppsPageTitle}"
aria-describedby="secondaryText">
$i18n{androidAppsEnable}
diff --git a/chromium/chrome/browser/resources/settings/android_apps_page/android_apps_subpage.html b/chromium/chrome/browser/resources/settings/android_apps_page/android_apps_subpage.html
index 58cc44902e0..771918fe76a 100644
--- a/chromium/chrome/browser/resources/settings/android_apps_page/android_apps_subpage.html
+++ b/chromium/chrome/browser/resources/settings/android_apps_page/android_apps_subpage.html
@@ -21,7 +21,7 @@
</template>
<template is="dom-if" if="[[allowRemove_(prefs.arc.enabled.*)]]">
- <div id="remove" class="settings-box" actionable on-tap="onRemoveTap_">
+ <div id="remove" class="settings-box" actionable on-click="onRemoveTap_">
<div class="start">$i18n{androidAppsRemove}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{androidAppsRemove}">
@@ -37,11 +37,11 @@
<div slot="body" inner-h-t-m-l="[[dialogBody_]]"></div>
<div slot="button-container">
<paper-button class="cancel-button"
- on-tap="onConfirmDisableDialogCancel_">
+ on-click="onConfirmDisableDialogCancel_">
$i18n{cancel}
</paper-button>
<paper-button class="action-button"
- on-tap="onConfirmDisableDialogConfirm_">
+ on-click="onConfirmDisableDialogConfirm_">
$i18n{androidAppsDisableDialogRemove}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/android_apps_page/android_settings_element.html b/chromium/chrome/browser/resources/settings/android_apps_page/android_settings_element.html
index 17b6e861109..eb65859100f 100644
--- a/chromium/chrome/browser/resources/settings/android_apps_page/android_settings_element.html
+++ b/chromium/chrome/browser/resources/settings/android_apps_page/android_settings_element.html
@@ -12,7 +12,7 @@
<style include="settings-shared"></style>
<div id="manageApps" class="settings-box first"
on-keydown="onManageAndroidAppsKeydown_"
- on-tap="onManageAndroidAppsTap_" actionable>
+ on-click="onManageAndroidAppsTap_" actionable>
<div class="start">
<div>$i18n{androidAppsManageApps}</div>
</div>
diff --git a/chromium/chrome/browser/resources/settings/appearance_page/appearance_fonts_page.html b/chromium/chrome/browser/resources/settings/appearance_page/appearance_fonts_page.html
index 3b014cc73a3..d82f8743fac 100644
--- a/chromium/chrome/browser/resources/settings/appearance_page/appearance_fonts_page.html
+++ b/chromium/chrome/browser/resources/settings/appearance_page/appearance_fonts_page.html
@@ -121,7 +121,7 @@
</div>
<template is="dom-if" if="[[!isGuest_]]">
<div class="settings-box two-line" id="advancedButton"
- on-tap="openAdvancedExtension_" actionable>
+ on-click="openAdvancedExtension_" actionable>
<div class="start">
$i18n{advancedFontSettings}
<div class="secondary" id="advancedButtonSublabel">
diff --git a/chromium/chrome/browser/resources/settings/appearance_page/appearance_page.html b/chromium/chrome/browser/resources/settings/appearance_page/appearance_page.html
index 7ac5845998d..6667159ff4e 100644
--- a/chromium/chrome/browser/resources/settings/appearance_page/appearance_page.html
+++ b/chromium/chrome/browser/resources/settings/appearance_page/appearance_page.html
@@ -60,7 +60,7 @@
<button icon-class="icon-external" id="wallpaperButton"
is="cr-link-row"
hidden="[[!pageVisibility.setWallpaper]]"
- on-tap="openWallpaperManager_"
+ on-click="openWallpaperManager_"
label="$i18n{setWallpaper}" sub-label="$i18n{openWallpaperApp}"
disabled="[[isWallpaperPolicyControlled_]]">
<template is="dom-if" if="[[isWallpaperPolicyControlled_]]">
@@ -76,11 +76,11 @@
<button class="first" icon-class="icon-external" is="cr-link-row"
hidden="[[!pageVisibility.setTheme]]"
label="$i18n{themes}" sub-label="[[themeSublabel_]]"
- on-tap="openThemeUrl_"></button>
+ on-click="openThemeUrl_"></button>
<if expr="not is_linux or chromeos">
<template is="dom-if" if="[[prefs.extensions.theme.id.value]]">
<div class="separator"></div>
- <paper-button id="useDefault" on-tap="onUseDefaultTap_"
+ <paper-button id="useDefault" on-click="onUseDefaultTap_"
class="secondary-button">
$i18n{resetToDefaultTheme}
</paper-button>
@@ -94,14 +94,14 @@
<div class="separator"></div>
<template is="dom-if" if="[[showUseClassic_(
prefs.extensions.theme.id.value, useSystemTheme_)]]" restamp>
- <paper-button id="useDefault" on-tap="onUseDefaultTap_"
+ <paper-button id="useDefault" on-click="onUseDefaultTap_"
class="secondary-button">
$i18n{useClassicTheme}
</paper-button>
</template>
<template is="dom-if" if="[[showUseSystem_(
prefs.extensions.theme.id.value, useSystemTheme_)]]" restamp>
- <paper-button id="useSystem" on-tap="onUseSystemTap_"
+ <paper-button id="useSystem" on-click="onUseSystemTap_"
class="secondary-button">
$i18n{useSystemTheme}
</paper-button>
@@ -168,7 +168,7 @@
</div>
<button class="hr" is="cr-link-row"
icon-class="subpage-arrow" id="customize-fonts-subpage-trigger"
- label="$i18n{customizeFonts}" on-tap="onCustomizeFontsTap_">
+ label="$i18n{customizeFonts}" on-click="onCustomizeFontsTap_">
</button>
<div class="settings-box" hidden="[[!pageVisibility.pageZoom]]">
<div id="pageZoom" class="start">$i18n{pageZoom}</div>
diff --git a/chromium/chrome/browser/resources/settings/basic_page/basic_page.html b/chromium/chrome/browser/resources/settings/basic_page/basic_page.html
index 94029ec4769..80ab0b65c36 100644
--- a/chromium/chrome/browser/resources/settings/basic_page/basic_page.html
+++ b/chromium/chrome/browser/resources/settings/basic_page/basic_page.html
@@ -46,7 +46,7 @@
--paper-button: {
text-transform: none;
}
- @apply(--settings-actionable);
+ @apply --settings-actionable;
align-items: center;
display: flex;
margin-bottom: 3px;
@@ -56,7 +56,7 @@
}
#secondaryUserBanner {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
align-items: center;
background-color: white;
border-radius: 2px;
diff --git a/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_device_list_item.html b/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_device_list_item.html
index b7134f661f3..c3ddc427f33 100644
--- a/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_device_list_item.html
+++ b/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_device_list_item.html
@@ -33,14 +33,15 @@
<span hidden$="[[!device.connecting]]">$i18n{bluetoothConnecting}</span>
<div hidden$="[[!device.paired]]">
<button is="paper-icon-button-light" class="icon-more-vert"
- on-tap="onMenuButtonTap_" tabindex$="[[tabindex]]"
+ on-click="onMenuButtonTap_" tabindex$="[[tabindex]]"
title="$i18n{moreActions}" on-keydown="ignoreEnterKey_">
</button>
<dialog id="dotsMenu" is="cr-action-menu">
- <button class="dropdown-item" on-tap="onConnectActionTap_">
+ <button slot="item" class="dropdown-item"
+ on-click="onConnectActionTap_">
[[getConnectActionText_(device.connected)]]
</button>
- <button class="dropdown-item" on-tap="onRemoveTap_">
+ <button slot="item" class="dropdown-item" on-click="onRemoveTap_">
$i18n{bluetoothRemove}
</button>
</dialog>
diff --git a/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_page.html b/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_page.html
index fb27688ce72..1e386deda42 100644
--- a/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_page.html
+++ b/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_page.html
@@ -21,7 +21,7 @@
focus-config="[[focusConfig_]]">
<neon-animatable route-path="default">
<div id="bluetoothDevices"
- class="settings-box two-line" actionable on-tap="onTap_">
+ class="settings-box two-line" actionable on-click="onTap_">
<iron-icon icon="[[getIcon_(bluetoothToggleState_)]]"></iron-icon>
<div class="middle">
$i18n{bluetoothPageTitle}
@@ -36,7 +36,7 @@
</cr-policy-pref-indicator>
<template is="dom-if" if="[[bluetoothToggleState_]]">
<button class="subpage-arrow" is="paper-icon-button-light"
- on-tap="onSubpageArrowTap_"
+ on-click="onSubpageArrowTap_"
aria-label="$i18n{bluetoothPageTitle}"
aria-describedby="bluetoothSecondary">
</button>
@@ -46,7 +46,7 @@
checked="{{bluetoothToggleState_}}"
disabled$=
"[[!isToggleEnabled_(adapterState_, stateChangeInProgress_)]]"
- on-tap="stopTap_"
+ on-click="stopTap_"
aria-label="$i18n{bluetoothToggleA11yLabel}">
</paper-toggle-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_subpage.html b/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_subpage.html
index 6ca9d1b16fa..018a505ac33 100644
--- a/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_subpage.html
+++ b/chromium/chrome/browser/resources/settings/bluetooth_page/bluetooth_subpage.html
@@ -15,7 +15,7 @@
<template>
<style include="settings-shared iron-flex">
.container {
- @apply(--settings-list-frame-padding);
+ @apply --settings-list-frame-padding;
display: flex;
flex-direction: column;
min-height: 10px;
@@ -27,7 +27,7 @@
}
paper-spinner-lite {
- @apply(--cr-icon-height-width);
+ @apply --cr-icon-height-width;
}
#onOff {
@@ -39,7 +39,7 @@
}
</style>
- <div class="settings-box first" actionable on-tap="onEnableTap_">
+ <div class="settings-box first" actionable on-click="onEnableTap_">
<div id="onOff" class="start" on$="[[bluetoothToggleState]]">
[[getOnOffString_(bluetoothToggleState,
'$i18nPolymer{deviceOn}', '$i18nPolymer{deviceOff}')]]
@@ -48,7 +48,7 @@
checked="{{bluetoothToggleState}}"
disabled$="[[!isToggleEnabled_(adapterState, stateChangeInProgress)]]"
aria-label="$i18n{bluetoothToggleA11yLabel}"
- on-tap="stopTap_">
+ on-click="stopTap_">
</paper-toggle-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/change_password_page/change_password_page.html b/chromium/chrome/browser/resources/settings/change_password_page/change_password_page.html
index deb22750820..ceaae386226 100644
--- a/chromium/chrome/browser/resources/settings/change_password_page/change_password_page.html
+++ b/chromium/chrome/browser/resources/settings/change_password_page/change_password_page.html
@@ -46,7 +46,7 @@
</div>
<div class="separator"></div>
<paper-button class="primary-button" id="changePassword"
- on-tap="changePassword_">
+ on-click="changePassword_">
$i18n{changePasswordPageButton}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.html b/chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.html
index 245dbdd0765..d0cb57647aa 100644
--- a/chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.html
+++ b/chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.html
@@ -84,11 +84,11 @@
</iron-icon>
</div>
<div class="start">
- <div>[[title_]]</div>
+ <div role="status">[[title_]]</div>
<div hidden="[[!showExplanation_]]">
<span class="secondary">[[explanation_]]</span>
<a id="learn-more" href="$i18n{chromeCleanupLearnMoreUrl}"
- on-tap="learnMore_" target="_blank"
+ on-click="learnMore_" target="_blank"
hidden="[[!showLearnMore_]]">
$i18n{learnMore}
</a>
@@ -97,7 +97,7 @@
<template is="dom-if" if="[[showActionButton_]]">
<div class="separator"></div>
<paper-button id="action-button" class="primary-button"
- on-tap="proceed_">
+ on-click="proceed_">
[[actionButtonLabel_]]
</paper-button>
</template>
@@ -112,7 +112,7 @@
on-settings-boolean-control-change="changeLogsPermission_">
</settings-toggle-button>
<div id="show-items-button" class="settings-box" actionable
- on-tap="toggleExpandButton_" hidden="[[!showItemsToRemove_]]">
+ on-click="toggleExpandButton_" hidden="[[!showItemsToRemove_]]">
<div class="start">[[showItemsLinkLabel_]]</div>
<cr-expand-button expanded="{{itemsToRemoveSectionExpanded_}}"
alt="[[showItemsLinkLabel_]]">
diff --git a/chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.js b/chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.js
index ec8cc5aa7cc..7a31f0eef1d 100644
--- a/chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.js
+++ b/chromium/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.js
@@ -767,14 +767,14 @@ Polymer({
},
DISMISS_CLEANUP_SUCCESS: {
- label: this.i18n('chromeCleanupDoneButtonLabel'),
+ label: this.i18n('done'),
doAction: this.dismiss_.bind(
this,
settings.ChromeCleanupDismissSource.CLEANUP_SUCCESS_DONE_BUTTON),
},
DISMISS_CLEANUP_FAILURE: {
- label: this.i18n('chromeCleanupDoneButtonLabel'),
+ label: this.i18n('done'),
doAction: this.dismiss_.bind(
this,
settings.ChromeCleanupDismissSource.CLEANUP_FAILURE_DONE_BUTTON),
diff --git a/chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.html b/chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.html
index 6f8b7eb1f77..21e3bb1cedc 100644
--- a/chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.html
+++ b/chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.html
@@ -20,18 +20,34 @@
color: var(--google-blue-500);
cursor: pointer;
}
+
+ #remaining-list {
+ margin-top: -13px;
+ }
</style>
<div id="title" class="secondary" hidden="[[!titleVisible]]">
[[title]]
</div>
- <ul id="list" class="secondary">
- <template is="dom-repeat" items="[[visibleItems_]]">
+ <ul class="secondary">
+ <template is="dom-repeat" items="[[initialItems_]]">
<li class="visible-item">[[item]]</li>
</template>
- <li id="more-items-link" hidden="[[expanded_]]" on-tap="expandList_">
+ <li id="more-items-link" hidden="[[expanded_]]" on-click="expandList_">
[[moreItemsLinkText_]]
</li>
</ul>
+ <!-- Remaining items are kept in a separate <ul> element so that screen
+ readers don't get confused when the list is expanded. If new items are
+ simply added to the first <ul> element, the first new item (which will
+ replace the "N more" link), will be skipped by the reader. As a
+ consequence, visual impaired users will only have a chance to inspect
+ that item if they move up on the list, which can't be considered an
+ expected action. -->
+ <ul id="remaining-list" hidden="[[!expanded_]]" class="secondary">
+ <template is="dom-repeat" items="[[remainingItems_]]">
+ <li class$="[[remainingItemsClass_(expanded_)]]">[[item]]</li>
+ </template>
+ </ul>
</template>
<script src="items_to_remove_list.js"></script>
</dom-module>
diff --git a/chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.js b/chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.js
index e628437c50e..8b9c803fb3b 100644
--- a/chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.js
+++ b/chromium/chrome/browser/resources/settings/chrome_cleanup_page/items_to_remove_list.js
@@ -71,11 +71,21 @@ Polymer({
},
/**
- * The list of items to actually present on the card. If |expanded_|, then
- * it's the same as |itemsToShow|.
+ * The items to be shown to the user the first time this component is
+ * rendered. If |initiallyExpanded| is true, then it includes all items
+ * from |itemsToShow|. Otherwise, it contains the first
+ * |CHROME_CLEANUP_DEFAULT_ITEMS_TO_SHOW| items.
* @private {?Array<string>}
*/
- visibleItems_: Array,
+ initialItems_: Array,
+
+ /**
+ * The remaining items to be presented that are not included in
+ * |initialItems_|. Items in this list are only shown to the user if
+ * |expanded_| is true.
+ * @private {?Array<string>}
+ */
+ remainingItems_: Array,
/**
* The text for the "show more" link available if not all files are visible
@@ -93,7 +103,6 @@ Polymer({
/** @private */
expandList_: function() {
this.expanded_ = true;
- this.visibleItems_ = this.itemsToShow;
this.moreItemsLinkText_ = '';
},
@@ -118,25 +127,34 @@ Polymer({
updateVisibleState_: function(itemsToShow, initiallyExpanded) {
// Start expanded if there are less than
// |settings.CHROME_CLEANUP_DEFAULT_ITEMS_TO_SHOW| items to show.
- this.expanded_ = this.initiallyExpanded ||
- this.itemsToShow.length <=
- settings.CHROME_CLEANUP_DEFAULT_ITEMS_TO_SHOW;
+ this.expanded_ = initiallyExpanded ||
+ itemsToShow.length <= settings.CHROME_CLEANUP_DEFAULT_ITEMS_TO_SHOW;
if (this.expanded_) {
- this.visibleItems_ = this.itemsToShow;
+ this.initialItems_ = itemsToShow;
+ this.remainingItems_ = [];
this.moreItemsLinkText_ = '';
return;
}
- this.visibleItems_ = this.itemsToShow.slice(
- 0, settings.CHROME_CLEANUP_DEFAULT_ITEMS_TO_SHOW - 1);
+ this.initialItems_ =
+ itemsToShow.slice(0, settings.CHROME_CLEANUP_DEFAULT_ITEMS_TO_SHOW - 1);
+ this.remainingItems_ =
+ itemsToShow.slice(settings.CHROME_CLEANUP_DEFAULT_ITEMS_TO_SHOW - 1);
const browserProxy = settings.ChromeCleanupProxyImpl.getInstance();
- browserProxy
- .getMoreItemsPluralString(
- this.itemsToShow.length - this.visibleItems_.length)
+ browserProxy.getMoreItemsPluralString(this.remainingItems_.length)
.then(linkText => {
this.moreItemsLinkText_ = linkText;
});
},
+
+ /**
+ * Returns the class for the <li> elements that correspond to the items hidden
+ * in the default view.
+ * @param {boolean} expanded
+ */
+ remainingItemsClass_: function(expanded) {
+ return expanded ? 'visible-item' : 'hidden-item';
+ },
});
diff --git a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.html b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.html
index ec3fad23b77..dbbc94a3556 100644
--- a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.html
+++ b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.html
@@ -2,8 +2,10 @@
<link rel="import" href="chrome://resources/cr_elements/cr_dialog/cr_dialog.html">
<link rel="import" href="chrome://resources/html/web_ui_listener_behavior.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/iron-pages/iron-pages.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-spinner/paper-spinner-lite.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/paper-tabs/paper-tabs.html">
<link rel="import" href="../i18n_setup.html">
<link rel="import" href="clear_browsing_data_browser_proxy.html">
<link rel="import" href="history_deletion_dialog.html">
@@ -12,12 +14,29 @@
<link rel="import" href="../controls/settings_dropdown_menu.html">
<link rel="import" href="../icons.html">
<link rel="import" href="../settings_shared_css.html">
+<link rel="import" href="../settings_vars_css.html">
-<!-- This file is forked as clear_browsing_data_dialog_tabs.html until the new
- CBD UI is launched. -->
<dom-module id="settings-clear-browsing-data-dialog">
<template>
<style include="settings-shared">
+ :host {
+ /* Fixed height to allow multiple tabs with different height.
+ * The last entry in the advanced tab should show half an entry.
+ * crbug.com/652027 */
+ --body-container-height: 322px;
+ }
+
+ #clearBrowsingDataDialog {
+ --cr-dialog-top-container-min-height: 42px;
+ --cr-dialog-title: {
+ padding-bottom: 8px;
+ };
+ --cr-dialog-body-container: {
+ border-top: 1px solid var(--paper-grey-300);
+ height: var(--body-container-height);
+ };
+ }
+
#clearBrowsingDataDialog:not(.fully-rendered) {
visibility: hidden;
}
@@ -26,6 +45,16 @@
color: var(--paper-grey-600);
}
+ #clearBrowsingDataDialog [slot=body] {
+ padding-top: 8px;
+ }
+
+ #importantSitesDialog {
+ --cr-dialog-body-container: {
+ height: var(--body-container-height);
+ };
+ }
+
.row {
align-items: center;
display: flex;
@@ -43,56 +72,36 @@
--settings-row-two-line-min-height: 48px;
--settings-checkbox-label: {
line-height: 1.25rem;
- };
- }
-
- #generalFooter {
- margin: 0;
- min-height: 18px;
- }
-
- #generalFooter iron-icon {
- height: 18px;
- padding: 1px;
- width: 18px;
- }
-
- #googleFooter {
- margin: 0 0 0.8em 0;
- min-height: 16px;
- }
-
- #googleFooter iron-icon {
- height: 16px;
- padding: 2px;
- width: 16px;
- }
-
- [slot=footer] iron-icon {
- margin: auto;
+ }
}
- .clear-browsing-data-footer {
- -webkit-padding-start: 4px;
- align-items: flex-start;
- display: flex;
- line-height: 1.538em; /* 20px/13px */
+ #basic-tab settings-checkbox + settings-checkbox {
+ --settings-checkbox-margin-top: 12px;
}
- .clear-browsing-data-footer .footer-text {
- -webkit-margin-start: 16px;
+ paper-tabs {
+ --paper-tabs-selection-bar-color: var(--google-blue-500);
+ --paper-tabs: {
+ font-size: 100%;
+ height: 40px;
+ }
}
- .clear-browsing-data-footer iron-icon {
- flex-shrink: 0;
+ paper-tab {
+ --paper-tab-content: {
+ color: var(--google-blue-700);
+ };
+ --paper-tab-content-unselected: {
+ opacity: 1;
+ color: var(--paper-grey-600);
+ };
}
- .clear-browsing-data-footer a {
- text-decoration: none;
+ .time-range-row {
+ margin-bottom: 12px;
}
- #clearFrom {
- -webkit-margin-start: 0.5em;
+ .time-range-select {
/* Adjust for md-select-underline and 1px additional bottom padding
* to keep md-select's text (without the underline) aligned with
* neighboring text that does not have an underline. */
@@ -103,115 +112,153 @@
font-size: calc(13 / 15 * 100%);
padding-top: 8px;
}
-
- /* Cap the height on smaller screens to avoid unfavorable clipping.
- * Replace the bottom margin with padding to avoid the gap between
- * the scrollbar and the bottom separator. */
- @media all and (max-height: 724px) {
- #clearBrowsingDataDialog {
- /* crbug.com/652027: Show four and a *half* items in the list. */
- --cr-dialog-body-container: {
- max-height: 280px;
- };
- }
- }
</style>
<dialog is="cr-dialog" id="clearBrowsingDataDialog"
on-close="onClearBrowsingDataDialogClose_"
- close-text="$i18n{close}" ignore-popstate>
- <div slot="title">$i18n{clearBrowsingData}</div>
+ close-text="$i18n{close}" ignore-popstate has-tabs>
+ <div slot="title">
+ <div>$i18n{clearBrowsingData}</div>
+ </div>
+ <div slot="header">
+ <paper-tabs noink on-selected-changed="recordTabChange_"
+ selected="{{prefs.browser.last_clear_browsing_data_tab.value}}">
+ <paper-tab>$i18n{basicPageTitle}</paper-tab>
+ <paper-tab>$i18n{advancedPageTitle}</paper-tab>
+ </paper-tabs>
+ </div>
<div slot="body">
- <div class="row">
- $i18n{clearFollowingItemsFrom}
- <settings-dropdown-menu id="clearFrom"
- label="$i18n{clearFollowingItemsFrom}"
- pref="{{prefs.browser.clear_data.time_period}}"
- menu-options="[[clearFromOptions_]]">
- </settings-dropdown-menu>
- </div>
- <!-- Note: whether these checkboxes are checked are ignored if deleting
- history is disabled (i.e. supervised users, policy), so it's OK to
- have a hidden checkbox that's also checked (as the C++ accounts for
- whether a user is allowed to delete history independently). -->
- <settings-checkbox id="browsingCheckbox" class="browsing-data-checkbox"
- pref="{{prefs.browser.clear_data.browsing_history}}"
- label="$i18n{clearBrowsingHistory}"
- sub-label="[[counters_.browsing_history]]"
- disabled="[[clearingInProgress_]]"
- hidden="[[isSupervised_]]">
- </settings-checkbox>
- <settings-checkbox id="downloadCheckbox" class="browsing-data-checkbox"
- pref="{{prefs.browser.clear_data.download_history}}"
- label="$i18n{clearDownloadHistory}"
- sub-label="[[counters_.download_history]]"
- disabled="[[clearingInProgress_]]"
- hidden="[[isSupervised_]]">
- </settings-checkbox>
- <settings-checkbox id="cacheCheckbox" class="browsing-data-checkbox"
- pref="{{prefs.browser.clear_data.cache}}"
- label="$i18n{clearCache}"
- sub-label="[[counters_.cache]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox id="cookiesCheckbox" class="browsing-data-checkbox"
- pref="{{prefs.browser.clear_data.cookies}}"
- label="$i18n{clearCookies}"
- sub-label="$i18n{clearCookiesCounter}"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox class="browsing-data-checkbox"
- pref="{{prefs.browser.clear_data.passwords}}"
- label="$i18n{clearPasswords}"
- sub-label="[[counters_.passwords]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox class="browsing-data-checkbox"
- pref="{{prefs.browser.clear_data.form_data}}"
- label="$i18n{clearFormData}"
- sub-label="[[counters_.form_data]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox class="browsing-data-checkbox"
- pref="{{prefs.browser.clear_data.hosted_apps_data}}"
- label="$i18n{clearHostedAppData}"
- sub-label="[[counters_.hosted_apps_data]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox class="browsing-data-checkbox"
- pref="{{prefs.browser.clear_data.media_licenses}}"
- label="$i18n{clearMediaLicenses}"
- sub-label="[[counters_.media_licenses]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
+ <iron-pages id="tabs"
+ selected="[[prefs.browser.last_clear_browsing_data_tab.value]]">
+ <div id="basic-tab">
+ <div class="row time-range-row">
+ <span class="time-range-label">
+ $i18n{clearTimeRange}
+ </span>
+ <settings-dropdown-menu id="clearFromBasic"
+ class="time-range-select"
+ label="$i18n{clearTimeRange}"
+ pref="{{prefs.browser.clear_data.time_period_basic}}"
+ menu-options="[[clearFromOptions_]]">
+ </settings-dropdown-menu>
+ </div>
+ <!-- Note: whether these checkboxes are checked are ignored if
+ deleting history is disabled (i.e. supervised users, policy),
+ so it's OK to have a hidden checkbox that's also checked (as
+ the C++ accounts for whether a user is allowed to delete
+ history independently). -->
+ <settings-checkbox id="browsingCheckboxBasic"
+ pref="{{prefs.browser.clear_data.browsing_history_basic}}"
+ label="$i18n{clearBrowsingHistory}"
+ sub-label-html="[[browsingCheckboxLabel_(
+ isSignedIn_, isSyncingHistory_,
+ '$i18nPolymer{clearBrowsingHistorySummary}',
+ '$i18nPolymer{clearBrowsingHistorySummarySignedIn}',
+ '$i18nPolymer{clearBrowsingHistorySummarySynced}')]]"
+ disabled="[[clearingInProgress_]]"
+ hidden="[[isSupervised_]]">
+ </settings-checkbox>
+ <settings-checkbox id="cookiesCheckboxBasic"
+ class="cookies-checkbox"
+ pref="{{prefs.browser.clear_data.cookies_basic}}"
+ label="$i18n{clearCookies}"
+ sub-label="$i18n{clearCookiesSummary}"
+ disabled="[[clearingInProgress_]]">
+ </settings-checkbox>
+ <settings-checkbox id="cacheCheckboxBasic"
+ class="cache-checkbox"
+ pref="{{prefs.browser.clear_data.cache_basic}}"
+ label="$i18n{clearCache}"
+ sub-label="[[counters_.cache_basic]]"
+ disabled="[[clearingInProgress_]]">
+ </settings-checkbox>
+ </div>
+ <div id="advanced-tab">
+ <div class="row time-range-row">
+ <span class="time-range-label">
+ $i18n{clearTimeRange}
+ </span>
+ <settings-dropdown-menu id="clearFrom"
+ class="time-range-select"
+ label="$i18n{clearTimeRange}"
+ pref="{{prefs.browser.clear_data.time_period}}"
+ menu-options="[[clearFromOptions_]]">
+ </settings-dropdown-menu>
+ </div>
+ <settings-checkbox id="browsingCheckbox"
+ pref="{{prefs.browser.clear_data.browsing_history}}"
+ label="$i18n{clearBrowsingHistory}"
+ sub-label="[[counters_.browsing_history]]"
+ disabled="[[clearingInProgress_]]"
+ hidden="[[isSupervised_]]">
+ </settings-checkbox>
+ <settings-checkbox id="downloadCheckbox"
+ pref="{{prefs.browser.clear_data.download_history}}"
+ label="$i18n{clearDownloadHistory}"
+ sub-label="[[counters_.download_history]]"
+ disabled="[[clearingInProgress_]]"
+ hidden="[[isSupervised_]]">
+ </settings-checkbox>
+ <settings-checkbox id="cookiesCheckbox"
+ class="cookies-checkbox"
+ pref="{{prefs.browser.clear_data.cookies}}"
+ label="$i18n{clearCookies}"
+ sub-label="[[counters_.cookies]]"
+ disabled="[[clearingInProgress_]]">
+ </settings-checkbox>
+ <settings-checkbox id="cacheCheckbox"
+ class="cache-checkbox"
+ pref="{{prefs.browser.clear_data.cache}}"
+ label="$i18n{clearCache}"
+ sub-label="[[counters_.cache]]"
+ disabled="[[clearingInProgress_]]">
+ </settings-checkbox>
+ <settings-checkbox
+ pref="{{prefs.browser.clear_data.passwords}}"
+ label="$i18n{clearPasswords}"
+ sub-label="[[counters_.passwords]]"
+ disabled="[[clearingInProgress_]]">
+ </settings-checkbox>
+ <settings-checkbox
+ pref="{{prefs.browser.clear_data.form_data}}"
+ label="$i18n{clearFormData}"
+ sub-label="[[counters_.form_data]]"
+ disabled="[[clearingInProgress_]]">
+ </settings-checkbox>
+ <settings-checkbox
+ pref="{{prefs.browser.clear_data.site_settings}}"
+ label="[[siteSettingsLabel_(
+ '$i18nPolymer{siteSettings}',
+ '$i18nPolymer{contentSettings}')]]"
+ sub-label="[[counters_.site_settings]]"
+ disabled="[[clearingInProgress_]]">
+ </settings-checkbox>
+ <settings-checkbox
+ pref="{{prefs.browser.clear_data.hosted_apps_data}}"
+ label="$i18n{clearHostedAppData}"
+ sub-label="[[counters_.hosted_apps_data]]"
+ disabled="[[clearingInProgress_]]">
+ </settings-checkbox>
+ <settings-checkbox
+ pref="{{prefs.browser.clear_data.media_licenses}}"
+ label="$i18n{clearMediaLicenses}"
+ sub-label="[[counters_.media_licenses]]"
+ disabled="[[clearingInProgress_]]">
+ </settings-checkbox>
+ </div>
+ </iron-pages>
</div>
<div slot="button-container">
<paper-spinner-lite active="[[clearingInProgress_]]">
</paper-spinner-lite>
<paper-button class="cancel-button" disabled="[[clearingInProgress_]]"
- on-tap="onCancelTap_">$i18n{cancel}</paper-button>
+ on-click="onCancelTap_">$i18n{cancel}</paper-button>
<paper-button id="clearBrowsingDataConfirm"
class="action-button" disabled="[[clearingInProgress_]]"
- on-tap="onClearBrowsingDataTap_">
- $i18n{clearBrowsingData}
+ on-click="onClearBrowsingDataTap_">
+ $i18n{clearData}
</paper-button>
</div>
- <div slot="footer">
- <div id="googleFooter" class="clear-browsing-data-footer">
- <iron-icon icon="settings:googleg"></iron-icon>
- <div class="footer-text">$i18nRaw{otherFormsOfBrowsingHistory}</div>
- </div>
- <div id="generalFooter" class="clear-browsing-data-footer">
- <iron-icon icon="settings:info"></iron-icon>
- <div class="footer-text">
- <span id="syncedDataSentence">$i18n{clearsSyncedData}</span>
- <span>$i18n{warnAboutNonClearedData}</span>
- <a id="clear-browser-data-old-learn-more-link"
- href="$i18n{clearBrowsingDataLearnMoreUrl}"
- target="_blank">$i18n{learnMore}</a>
- </div>
- </div>
- </div>
</dialog>
<template is="dom-if" if="[[showImportantSitesDialog_]]">
@@ -220,13 +267,12 @@
<div slot="title">
$i18n{clearBrowsingData}
<div class="secondary">
- <template is="dom-if"
- if="[[!prefs.browser.clear_data.cache.value]]">
+ <span hidden$="[[showImportantSitesCacheSubtitle_]]">
$i18n{importantSitesSubtitleCookies}
- </template>
- <template is="dom-if" if="[[prefs.browser.clear_data.cache.value]]">
+ </span>
+ <span hidden$="[[!showImportantSitesCacheSubtitle_]]">
$i18n{importantSitesSubtitleCookiesAndCache}
- </template>
+ </span>
</div>
</div>
<div slot="body">
@@ -243,10 +289,10 @@
<paper-spinner-lite active="[[clearingInProgress_]]">
</paper-spinner-lite>
<paper-button class="cancel-button" disabled="[[clearingInProgress_]]"
- on-tap="onImportantSitesCancelTap_">$i18n{cancel}</paper-button>
+ on-click="onImportantSitesCancelTap_">$i18n{cancel}</paper-button>
<paper-button id="importantSitesConfirm"
class="action-button" disabled="[[clearingInProgress_]]"
- on-tap="onImportantSitesConfirmTap_">
+ on-click="onImportantSitesConfirmTap_">
$i18n{importantSitesConfirm}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.js b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.js
index 60ec1ccbf33..97beb3c7906 100644
--- a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.js
+++ b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog.js
@@ -3,16 +3,13 @@
// found in the LICENSE file.
/**
- * @fileoverview 'settings-clear-browsing-data-dialog' allows the user to delete
- * browsing data that has been cached by Chromium.
- *
- * This file is forked as clear_browsing_data_dialog_tabs.js until the new
- * CBD UI is launched.
+ * @fileoverview 'settings-clear-browsing-data-dialog' allows the user to
+ * delete browsing data that has been cached by Chromium.
*/
Polymer({
is: 'settings-clear-browsing-data-dialog',
- behaviors: [WebUIListenerBehavior],
+ behaviors: [WebUIListenerBehavior, settings.RouteObserverBehavior],
properties: {
/**
@@ -45,11 +42,11 @@ Polymer({
readOnly: true,
type: Array,
value: [
- {value: 0, name: loadTimeData.getString('clearDataHour')},
- {value: 1, name: loadTimeData.getString('clearDataDay')},
- {value: 2, name: loadTimeData.getString('clearDataWeek')},
- {value: 3, name: loadTimeData.getString('clearData4Weeks')},
- {value: 4, name: loadTimeData.getString('clearDataEverything')},
+ {value: 0, name: loadTimeData.getString('clearPeriodHour')},
+ {value: 1, name: loadTimeData.getString('clearPeriod24Hours')},
+ {value: 2, name: loadTimeData.getString('clearPeriod7Days')},
+ {value: 3, name: loadTimeData.getString('clearPeriod4Weeks')},
+ {value: 4, name: loadTimeData.getString('clearPeriodEverything')},
],
},
@@ -73,6 +70,18 @@ Polymer({
value: false,
},
+ /** @private */
+ isSignedIn_: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @private */
+ isSyncingHistory_: {
+ type: Boolean,
+ value: false,
+ },
+
/** @private {!Array<ImportantSite>} */
importantSites_: {
type: Array,
@@ -90,7 +99,25 @@ Polymer({
},
/** @private */
- showImportantSitesDialog_: {type: Boolean, value: false},
+ showImportantSitesDialog_: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @private */
+ showImportantSitesCacheSubtitle_: {
+ type: Boolean,
+ value: false,
+ },
+
+ /**
+ * Time in ms, when the dialog was opened.
+ * @private
+ */
+ dialogOpenedTime_: {
+ type: Number,
+ value: 0,
+ }
},
/** @private {settings.ClearBrowsingDataBrowserProxy} */
@@ -98,7 +125,6 @@ Polymer({
/** @override */
ready: function() {
- this.$.clearFrom.menuOptions = this.clearFromOptions_;
this.addWebUIListener(
'update-sync-state', this.updateSyncState_.bind(this));
this.addWebUIListener(
@@ -109,6 +135,7 @@ Polymer({
attached: function() {
this.browserProxy_ =
settings.ClearBrowsingDataBrowserProxyImpl.getInstance();
+ this.dialogOpenedTime_ = Date.now();
this.browserProxy_.initialize().then(() => {
this.$.clearBrowsingDataDialog.showModal();
});
@@ -121,21 +148,67 @@ Polymer({
},
/**
- * Updates the footer to show only those sentences that are relevant to this
- * user.
+ * Record visits to the CBD dialog.
+ *
+ * settings.RouteObserverBehavior
+ * @param {!settings.Route} currentRoute
+ * @protected
+ */
+ currentRouteChanged: function(currentRoute) {
+ if (currentRoute == settings.routes.CLEAR_BROWSER_DATA) {
+ chrome.metricsPrivate.recordUserAction('ClearBrowsingData_DialogCreated');
+ this.dialogOpenedTime_ = Date.now();
+ }
+ },
+
+ /**
+ * Updates the history description to show the relevant information
+ * depending on sync and signin state.
+ *
* @param {boolean} signedIn Whether the user is signed in.
- * @param {boolean} syncing Whether the user is syncing data.
- * @param {boolean} otherFormsOfBrowsingHistory Whether the user has other
- * forms of browsing history in their account.
+ * @param {boolean} syncing Whether the user is syncing history.
* @private
*/
- updateSyncState_: function(signedIn, syncing, otherFormsOfBrowsingHistory) {
- this.$.googleFooter.hidden = !otherFormsOfBrowsingHistory;
- this.$.syncedDataSentence.hidden = !syncing;
+ updateSyncState_: function(signedIn, syncing) {
+ this.isSignedIn_ = signedIn;
+ this.isSyncingHistory_ = syncing;
this.$.clearBrowsingDataDialog.classList.add('fully-rendered');
},
/**
+ * Choose a summary checkbox label.
+ * @param {boolean} isSignedIn
+ * @param {boolean} isSyncingHistory
+ * @param {string} historySummary
+ * @param {string} historySummarySigned
+ * @param {string} historySummarySynced
+ * @return {string}
+ * @private
+ */
+ browsingCheckboxLabel_: function(
+ isSignedIn, isSyncingHistory, historySummary, historySummarySigned,
+ historySummarySynced) {
+ if (isSyncingHistory) {
+ return historySummarySynced;
+ } else if (isSignedIn) {
+ return historySummarySigned;
+ }
+ return historySummary;
+ },
+
+ /**
+ * Choose a content/site settings label.
+ * @param {string} siteSettings
+ * @param {string} contentSettings
+ * @return {string}
+ * @private
+ */
+ siteSettingsLabel_: function(siteSettings, contentSettings) {
+ return loadTimeData.getBoolean('enableSiteSettings') ? siteSettings :
+ contentSettings;
+ },
+
+ /**
* Updates the text of a browsing data counter corresponding to the given
* preference.
* @param {string} prefName Browsing data type deletion preference.
@@ -156,8 +229,10 @@ Polymer({
shouldShowImportantSites_: function() {
if (!this.importantSitesFlagEnabled_)
return false;
- if (!this.$.cookiesCheckbox.checked)
+ const tab = this.$.tabs.selectedItem;
+ if (!tab.querySelector('.cookies-checkbox').checked) {
return false;
+ }
const haveImportantSites = this.importantSites_.length > 0;
chrome.send(
@@ -172,12 +247,13 @@ Polymer({
*/
onClearBrowsingDataTap_: function() {
if (this.shouldShowImportantSites_()) {
+ const tab = this.$.tabs.selectedItem;
this.showImportantSitesDialog_ = true;
+ this.showImportantSitesCacheSubtitle_ =
+ tab.querySelector('.cache-checkbox').checked;
this.$.clearBrowsingDataDialog.close();
// Show important sites dialog after dom-if is applied.
- this.async(function() {
- this.$$('#importantSitesDialog').showModal();
- });
+ this.async(() => this.$$('#importantSitesDialog').showModal());
} else {
this.clearBrowsingData_();
}
@@ -200,21 +276,31 @@ Polymer({
*/
clearBrowsingData_: function() {
this.clearingInProgress_ = true;
+ const tab = this.$.tabs.selectedItem;
- const checkboxes = this.root.querySelectorAll('.browsing-data-checkbox');
+ const checkboxes = tab.querySelectorAll('settings-checkbox');
const dataTypes = [];
checkboxes.forEach((checkbox) => {
if (checkbox.checked)
dataTypes.push(checkbox.pref.key);
});
- const timePeriod = this.$.clearFrom.pref.value;
+ const timePeriod = tab.querySelector('.time-range-select').pref.value;
+
+ if (tab.id == 'basic-tab') {
+ chrome.metricsPrivate.recordUserAction('ClearBrowsingData_BasicTab');
+ } else {
+ chrome.metricsPrivate.recordUserAction('ClearBrowsingData_AdvancedTab');
+ }
this.browserProxy_
.clearBrowsingData(dataTypes, timePeriod, this.importantSites_)
.then(shouldShowNotice => {
this.clearingInProgress_ = false;
this.showHistoryDeletionDialog_ = shouldShowNotice;
+ chrome.metricsPrivate.recordMediumTime(
+ 'History.ClearBrowsingData.TimeSpentInDialog',
+ Date.now() - this.dialogOpenedTime_);
if (!shouldShowNotice)
this.closeDialogs_();
});
@@ -257,4 +343,20 @@ Polymer({
this.showHistoryDeletionDialog_ = false;
this.closeDialogs_();
},
+
+ /**
+ * Records an action when the user changes between the basic and advanced tab.
+ * @param {!Event} event
+ * @private
+ */
+ recordTabChange_: function(event) {
+ if (event.detail.value == 0) {
+ chrome.metricsPrivate.recordUserAction(
+ 'ClearBrowsingData_SwitchTo_BasicTab');
+ } else {
+ chrome.metricsPrivate.recordUserAction(
+ 'ClearBrowsingData_SwitchTo_AdvancedTab');
+ }
+ },
+
});
diff --git a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.html b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.html
deleted file mode 100644
index b9118df038f..00000000000
--- a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.html
+++ /dev/null
@@ -1,311 +0,0 @@
-<link rel="import" href="chrome://resources/html/polymer.html">
-
-<link rel="import" href="chrome://resources/cr_elements/cr_dialog/cr_dialog.html">
-<link rel="import" href="chrome://resources/html/web_ui_listener_behavior.html">
-<link rel="import" href="chrome://resources/polymer/v1_0/iron-pages/iron-pages.html">
-<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
-<link rel="import" href="chrome://resources/polymer/v1_0/paper-spinner/paper-spinner-lite.html">
-<link rel="import" href="chrome://resources/polymer/v1_0/paper-tabs/paper-tabs.html">
-<link rel="import" href="../i18n_setup.html">
-<link rel="import" href="clear_browsing_data_browser_proxy.html">
-<link rel="import" href="history_deletion_dialog.html">
-<link rel="import" href="../controls/important_site_checkbox.html">
-<link rel="import" href="../controls/settings_checkbox.html">
-<link rel="import" href="../controls/settings_dropdown_menu.html">
-<link rel="import" href="../icons.html">
-<link rel="import" href="../settings_shared_css.html">
-<link rel="import" href="../settings_vars_css.html">
-
-<!-- This file is a fork of clear_browsing_data_dialog.html until the new CBD
- UI is launched. -->
-<dom-module id="settings-clear-browsing-data-dialog-tabs">
- <template>
- <style include="settings-shared">
- :host {
- /* Fixed height to allow multiple tabs with different height.
- * The last entry in the advanced tab should show half an entry.
- * crbug.com/652027 */
- --body-container-height: 322px;
- }
-
- #clearBrowsingDataDialog {
- --cr-dialog-top-container-min-height: 42px;
- --cr-dialog-title: {
- padding-bottom: 8px;
- };
- --cr-dialog-body-container: {
- border-top: 1px solid var(--paper-grey-300);
- height: var(--body-container-height);
- };
- }
-
- #clearBrowsingDataDialog:not(.fully-rendered) {
- visibility: hidden;
- }
-
- #clearBrowsingDataDialog [slot=footer] {
- color: var(--paper-grey-600);
- }
-
- #clearBrowsingDataDialog [slot=body] {
- padding-top: 8px;
- }
-
- #importantSitesDialog {
- --cr-dialog-body-container: {
- height: var(--body-container-height);
- };
- }
-
- .row {
- align-items: center;
- display: flex;
- min-height: 40px;
- }
-
- paper-spinner-lite {
- -webkit-margin-end: 16px;
- margin-bottom: auto;
- margin-top: auto;
- }
-
- settings-checkbox,
- important-site-checkbox {
- --settings-row-two-line-min-height: 48px;
- --settings-checkbox-label: {
- line-height: 1.25rem;
- }
- }
-
- #basic-tab settings-checkbox + settings-checkbox {
- --settings-checkbox-margin-top: 12px;
- }
-
- paper-tabs {
- --paper-tabs-selection-bar-color: var(--google-blue-500);
- --paper-tabs: {
- font-size: 100%;
- height: 40px;
- }
- }
-
- paper-tab {
- --paper-tab-content: {
- color: var(--google-blue-700);
- };
- --paper-tab-content-unselected: {
- opacity: 1;
- color: var(--paper-grey-600);
- };
- }
-
- .time-range-row {
- margin-bottom: 12px;
- }
-
- .time-range-select {
- /* Adjust for md-select-underline and 1px additional bottom padding
- * to keep md-select's text (without the underline) aligned with
- * neighboring text that does not have an underline. */
- margin-top: 3px;
- }
-
- [slot=title] .secondary {
- font-size: calc(13 / 15 * 100%);
- padding-top: 8px;
- }
- </style>
-
- <dialog is="cr-dialog" id="clearBrowsingDataDialog"
- on-close="onClearBrowsingDataDialogClose_"
- close-text="$i18n{close}" ignore-popstate has-tabs>
- <div slot="title">
- <div>$i18n{clearBrowsingData}</div>
- </div>
- <div slot="header">
- <paper-tabs noink on-selected-changed="recordTabChange_"
- selected="{{prefs.browser.last_clear_browsing_data_tab.value}}">
- <paper-tab>$i18n{basicPageTitle}</paper-tab>
- <paper-tab>$i18n{advancedPageTitle}</paper-tab>
- </paper-tabs>
- </div>
- <div slot="body">
- <iron-pages id="tabs"
- selected="[[prefs.browser.last_clear_browsing_data_tab.value]]">
- <div id="basic-tab">
- <div class="row time-range-row">
- <span class="time-range-label">
- $i18n{clearTimeRange}
- </span>
- <settings-dropdown-menu id="clearFromBasic"
- class="time-range-select"
- label="$i18n{clearTimeRange}"
- pref="{{prefs.browser.clear_data.time_period_basic}}"
- menu-options="[[clearFromOptions_]]">
- </settings-dropdown-menu>
- </div>
- <!-- Note: whether these checkboxes are checked are ignored if
- deleting history is disabled (i.e. supervised users, policy),
- so it's OK to have a hidden checkbox that's also checked (as
- the C++ accounts for whether a user is allowed to delete
- history independently). -->
- <settings-checkbox id="browsingCheckboxBasic"
- pref="{{prefs.browser.clear_data.browsing_history_basic}}"
- label="$i18n{clearBrowsingHistory}"
- sub-label-html="[[browsingCheckboxLabel_(
- isSignedIn_, isSyncingHistory_,
- '$i18nPolymer{clearBrowsingHistorySummary}',
- '$i18nPolymer{clearBrowsingHistorySummarySignedIn}',
- '$i18nPolymer{clearBrowsingHistorySummarySynced}')]]"
- disabled="[[clearingInProgress_]]"
- hidden="[[isSupervised_]]">
- </settings-checkbox>
- <settings-checkbox id="cookiesCheckboxBasic"
- class="cookies-checkbox"
- pref="{{prefs.browser.clear_data.cookies_basic}}"
- label="$i18n{clearCookies}"
- sub-label="$i18n{clearCookiesSummary}"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox id="cacheCheckboxBasic"
- class="cache-checkbox"
- pref="{{prefs.browser.clear_data.cache_basic}}"
- label="$i18n{clearCache}"
- sub-label="[[counters_.cache_basic]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- </div>
- <div id="advanced-tab">
- <div class="row time-range-row">
- <span class="time-range-label">
- $i18n{clearTimeRange}
- </span>
- <settings-dropdown-menu id="clearFrom"
- class="time-range-select"
- label="$i18n{clearTimeRange}"
- pref="{{prefs.browser.clear_data.time_period}}"
- menu-options="[[clearFromOptions_]]">
- </settings-dropdown-menu>
- </div>
- <settings-checkbox id="browsingCheckbox"
- pref="{{prefs.browser.clear_data.browsing_history}}"
- label="$i18n{clearBrowsingHistory}"
- sub-label="[[counters_.browsing_history]]"
- disabled="[[clearingInProgress_]]"
- hidden="[[isSupervised_]]">
- </settings-checkbox>
- <settings-checkbox id="downloadCheckbox"
- pref="{{prefs.browser.clear_data.download_history}}"
- label="$i18n{clearDownloadHistory}"
- sub-label="[[counters_.download_history]]"
- disabled="[[clearingInProgress_]]"
- hidden="[[isSupervised_]]">
- </settings-checkbox>
- <settings-checkbox id="cookiesCheckbox"
- class="cookies-checkbox"
- pref="{{prefs.browser.clear_data.cookies}}"
- label="$i18n{clearCookies}"
- sub-label="[[counters_.cookies]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox id="cacheCheckbox"
- class="cache-checkbox"
- pref="{{prefs.browser.clear_data.cache}}"
- label="$i18n{clearCache}"
- sub-label="[[counters_.cache]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox
- pref="{{prefs.browser.clear_data.passwords}}"
- label="$i18n{clearPasswords}"
- sub-label="[[counters_.passwords]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox
- pref="{{prefs.browser.clear_data.form_data}}"
- label="$i18n{clearFormData}"
- sub-label="[[counters_.form_data]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox
- pref="{{prefs.browser.clear_data.site_settings}}"
- label="[[siteSettingsLabel_(
- '$i18nPolymer{siteSettings}',
- '$i18nPolymer{contentSettings}')]]"
- sub-label="[[counters_.site_settings]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox
- pref="{{prefs.browser.clear_data.hosted_apps_data}}"
- label="$i18n{clearHostedAppData}"
- sub-label="[[counters_.hosted_apps_data]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- <settings-checkbox
- pref="{{prefs.browser.clear_data.media_licenses}}"
- label="$i18n{clearMediaLicenses}"
- sub-label="[[counters_.media_licenses]]"
- disabled="[[clearingInProgress_]]">
- </settings-checkbox>
- </div>
- </iron-pages>
- </div>
- <div slot="button-container">
- <paper-spinner-lite active="[[clearingInProgress_]]">
- </paper-spinner-lite>
- <paper-button class="cancel-button" disabled="[[clearingInProgress_]]"
- on-tap="onCancelTap_">$i18n{cancel}</paper-button>
- <paper-button id="clearBrowsingDataConfirm"
- class="action-button" disabled="[[clearingInProgress_]]"
- on-tap="onClearBrowsingDataTap_">
- $i18n{clearData}
- </paper-button>
- </div>
- </dialog>
-
- <template is="dom-if" if="[[showImportantSitesDialog_]]">
- <dialog is="cr-dialog" id="importantSitesDialog" close-text="$i18n{close}"
- show-scroll-borders ignore-popstate>
- <div slot="title">
- $i18n{clearBrowsingData}
- <div class="secondary">
- <span hidden$="[[showImportantSitesCacheSubtitle_]]">
- $i18n{importantSitesSubtitleCookies}
- </span>
- <span hidden$="[[!showImportantSitesCacheSubtitle_]]">
- $i18n{importantSitesSubtitleCookiesAndCache}
- </span>
- </div>
- </div>
- <div slot="body">
- <template is="dom-repeat" items="[[importantSites_]]">
- <div class="row">
- <important-site-checkbox
- site="[[item]]"
- disabled="[[clearingInProgress_]]">
- </important-site-checkbox>
- </div>
- </template>
- </div>
- <div slot="button-container">
- <paper-spinner-lite active="[[clearingInProgress_]]">
- </paper-spinner-lite>
- <paper-button class="cancel-button" disabled="[[clearingInProgress_]]"
- on-tap="onImportantSitesCancelTap_">$i18n{cancel}</paper-button>
- <paper-button id="importantSitesConfirm"
- class="action-button" disabled="[[clearingInProgress_]]"
- on-tap="onImportantSitesConfirmTap_">
- $i18n{importantSitesConfirm}
- </paper-button>
- </div>
- </dialog>
- </template>
-
- <template is="dom-if" if="[[showHistoryDeletionDialog_]]" restamp>
- <settings-history-deletion-dialog id="notice"
- on-close="onHistoryDeletionDialogClose_">
- </settings-history-deletion-dialog>
- </template>
- </template>
- <script src="clear_browsing_data_dialog_tabs.js"></script>
-</dom-module>
diff --git a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.js b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.js
deleted file mode 100644
index 27f055c5e4b..00000000000
--- a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.js
+++ /dev/null
@@ -1,367 +0,0 @@
-// Copyright 2015 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-/**
- * @fileoverview 'settings-clear-browsing-data-dialog-tabs' allows the user to
- * delete browsing data that has been cached by Chromium.
- *
- * This file is a fork of clear_browsing_data_dialog.js until the new CBD UI is
- * launched.
- */
-Polymer({
- is: 'settings-clear-browsing-data-dialog-tabs',
-
- behaviors: [WebUIListenerBehavior, settings.RouteObserverBehavior],
-
- properties: {
- /**
- * Preferences state.
- */
- prefs: {
- type: Object,
- notify: true,
- },
-
- /**
- * Results of browsing data counters, keyed by the suffix of
- * the corresponding data type deletion preference, as reported
- * by the C++ side.
- * @private {!Object<string>}
- */
- counters_: {
- type: Object,
- // Will be filled as results are reported.
- value: function() {
- return {};
- }
- },
-
- /**
- * List of options for the dropdown menu.
- * @private {!DropdownMenuOptionList}
- */
- clearFromOptions_: {
- readOnly: true,
- type: Array,
- value: [
- {value: 0, name: loadTimeData.getString('clearPeriodHour')},
- {value: 1, name: loadTimeData.getString('clearPeriod24Hours')},
- {value: 2, name: loadTimeData.getString('clearPeriod7Days')},
- {value: 3, name: loadTimeData.getString('clearPeriod4Weeks')},
- {value: 4, name: loadTimeData.getString('clearPeriodEverything')},
- ],
- },
-
- /** @private */
- clearingInProgress_: {
- type: Boolean,
- value: false,
- },
-
- /** @private */
- isSupervised_: {
- type: Boolean,
- value: function() {
- return loadTimeData.getBoolean('isSupervised');
- },
- },
-
- /** @private */
- showHistoryDeletionDialog_: {
- type: Boolean,
- value: false,
- },
-
- /** @private */
- isSignedIn_: {
- type: Boolean,
- value: false,
- },
-
- /** @private */
- isSyncingHistory_: {
- type: Boolean,
- value: false,
- },
-
- /** @private {!Array<ImportantSite>} */
- importantSites_: {
- type: Array,
- value: function() {
- return [];
- }
- },
-
- /** @private */
- importantSitesFlagEnabled_: {
- type: Boolean,
- value: function() {
- return loadTimeData.getBoolean('importantSitesInCbd');
- },
- },
-
- /** @private */
- showImportantSitesDialog_: {
- type: Boolean,
- value: false,
- },
-
- /** @private */
- showImportantSitesCacheSubtitle_: {
- type: Boolean,
- value: false,
- },
-
- /**
- * Time in ms, when the dialog was opened.
- * @private
- */
- dialogOpenedTime_: {
- type: Number,
- value: 0,
- }
- },
-
- /** @private {settings.ClearBrowsingDataBrowserProxy} */
- browserProxy_: null,
-
- /** @override */
- ready: function() {
- this.addWebUIListener(
- 'update-sync-state', this.updateSyncState_.bind(this));
- this.addWebUIListener(
- 'update-counter-text', this.updateCounterText_.bind(this));
- },
-
- /** @override */
- attached: function() {
- this.browserProxy_ =
- settings.ClearBrowsingDataBrowserProxyImpl.getInstance();
- this.dialogOpenedTime_ = Date.now();
- this.browserProxy_.initialize().then(() => {
- this.$.clearBrowsingDataDialog.showModal();
- });
-
- if (this.importantSitesFlagEnabled_) {
- this.browserProxy_.getImportantSites().then(sites => {
- this.importantSites_ = sites;
- });
- }
- },
-
- /**
- * Record visits to the CBD dialog.
- *
- * settings.RouteObserverBehavior
- * @param {!settings.Route} currentRoute
- * @protected
- */
- currentRouteChanged: function(currentRoute) {
- if (currentRoute == settings.routes.CLEAR_BROWSER_DATA) {
- chrome.metricsPrivate.recordUserAction('ClearBrowsingData_DialogCreated');
- this.dialogOpenedTime_ = Date.now();
- }
- },
-
- /**
- * Updates the history description to show the relevant information
- * depending on sync and signin state.
- *
- * @param {boolean} signedIn Whether the user is signed in.
- * @param {boolean} syncing Whether the user is syncing history.
- * @param {boolean} otherFormsOfBrowsingHistory Whether the user has other
- * forms of browsing history in their account.
- * @private
- */
- updateSyncState_: function(signedIn, syncing, otherFormsOfBrowsingHistory) {
- this.isSignedIn_ = signedIn;
- this.isSyncingHistory_ = syncing;
- this.$.clearBrowsingDataDialog.classList.add('fully-rendered');
- },
-
- /**
- * Choose a summary checkbox label.
- * @param {boolean} isSignedIn
- * @param {boolean} isSyncingHistory
- * @param {string} historySummary
- * @param {string} historySummarySigned
- * @param {string} historySummarySynced
- * @return {string}
- * @private
- */
- browsingCheckboxLabel_: function(
- isSignedIn, isSyncingHistory, historySummary, historySummarySigned,
- historySummarySynced) {
- if (isSyncingHistory) {
- return historySummarySynced;
- } else if (isSignedIn) {
- return historySummarySigned;
- }
- return historySummary;
- },
-
- /**
- * Choose a content/site settings label.
- * @param {string} siteSettings
- * @param {string} contentSettings
- * @return {string}
- * @private
- */
- siteSettingsLabel_: function(siteSettings, contentSettings) {
- return loadTimeData.getBoolean('enableSiteSettings') ? siteSettings :
- contentSettings;
- },
-
- /**
- * Updates the text of a browsing data counter corresponding to the given
- * preference.
- * @param {string} prefName Browsing data type deletion preference.
- * @param {string} text The text with which to update the counter
- * @private
- */
- updateCounterText_: function(prefName, text) {
- // Data type deletion preferences are named "browser.clear_data.<datatype>".
- // Strip the common prefix, i.e. use only "<datatype>".
- const matches = prefName.match(/^browser\.clear_data\.(\w+)$/);
- this.set('counters_.' + assert(matches[1]), text);
- },
-
- /**
- * @return {boolean} Whether the ImportantSites dialog should be shown.
- * @private
- */
- shouldShowImportantSites_: function() {
- if (!this.importantSitesFlagEnabled_)
- return false;
- const tab = this.$.tabs.selectedItem;
- if (!tab.querySelector('.cookies-checkbox').checked) {
- return false;
- }
-
- const haveImportantSites = this.importantSites_.length > 0;
- chrome.send(
- 'metricsHandler:recordBooleanHistogram',
- ['History.ClearBrowsingData.ImportantDialogShown', haveImportantSites]);
- return haveImportantSites;
- },
-
- /**
- * Handles the tap on the Clear Data button.
- * @private
- */
- onClearBrowsingDataTap_: function() {
- if (this.shouldShowImportantSites_()) {
- const tab = this.$.tabs.selectedItem;
- this.showImportantSitesDialog_ = true;
- this.showImportantSitesCacheSubtitle_ =
- tab.querySelector('.cache-checkbox').checked;
- this.$.clearBrowsingDataDialog.close();
- // Show important sites dialog after dom-if is applied.
- this.async(() => this.$$('#importantSitesDialog').showModal());
- } else {
- this.clearBrowsingData_();
- }
- },
-
- /**
- * Handles closing of the clear browsing data dialog. Stops the close
- * event from propagating if another dialog is shown to prevent the
- * privacy-page from closing this dialog.
- * @private
- */
- onClearBrowsingDataDialogClose_: function(event) {
- if (this.showImportantSitesDialog_)
- event.stopPropagation();
- },
-
- /**
- * Clears browsing data and maybe shows a history notice.
- * @private
- */
- clearBrowsingData_: function() {
- this.clearingInProgress_ = true;
- const tab = this.$.tabs.selectedItem;
-
- checkboxes = tab.querySelectorAll('settings-checkbox');
- const dataTypes = [];
- checkboxes.forEach((checkbox) => {
- if (checkbox.checked)
- dataTypes.push(checkbox.pref.key);
- });
-
- const timePeriod = tab.querySelector('.time-range-select').pref.value;
-
- if (tab.id == 'basic-tab') {
- chrome.metricsPrivate.recordUserAction('ClearBrowsingData_BasicTab');
- } else {
- chrome.metricsPrivate.recordUserAction('ClearBrowsingData_AdvancedTab');
- }
-
- this.browserProxy_
- .clearBrowsingData(dataTypes, timePeriod, this.importantSites_)
- .then(shouldShowNotice => {
- this.clearingInProgress_ = false;
- this.showHistoryDeletionDialog_ = shouldShowNotice;
- chrome.metricsPrivate.recordMediumTime(
- 'History.ClearBrowsingData.TimeSpentInDialog',
- Date.now() - this.dialogOpenedTime_);
- if (!shouldShowNotice)
- this.closeDialogs_();
- });
- },
-
- /**
- * Closes the clear browsing data or important site dialog if they are open.
- * @private
- */
- closeDialogs_: function() {
- if (this.$.clearBrowsingDataDialog.open)
- this.$.clearBrowsingDataDialog.close();
- if (this.showImportantSitesDialog_)
- this.$$('#importantSitesDialog').close();
- },
-
- /** @private */
- onCancelTap_: function() {
- this.$.clearBrowsingDataDialog.cancel();
- },
-
- /**
- * Handles the tap confirm button in important sites.
- * @private
- */
- onImportantSitesConfirmTap_: function() {
- this.clearBrowsingData_();
- },
-
- /** @private */
- onImportantSitesCancelTap_: function() {
- /** @type {!CrDialogElement} */ (this.$$('#importantSitesDialog')).cancel();
- },
-
- /**
- * Handles the closing of the notice about other forms of browsing history.
- * @private
- */
- onHistoryDeletionDialogClose_: function() {
- this.showHistoryDeletionDialog_ = false;
- this.closeDialogs_();
- },
-
- /**
- * Records an action when the user changes between the basic and advanced tab.
- * @param {!Event} event
- * @private
- */
- recordTabChange_: function(event) {
- if (event.detail.value == 0) {
- chrome.metricsPrivate.recordUserAction(
- 'ClearBrowsingData_SwitchTo_BasicTab');
- } else {
- chrome.metricsPrivate.recordUserAction(
- 'ClearBrowsingData_SwitchTo_AdvancedTab');
- }
- },
-
-});
diff --git a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/compiled_resources2.gyp b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/compiled_resources2.gyp
index 518e48ef804..8660449e98a 100644
--- a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/compiled_resources2.gyp
+++ b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/compiled_resources2.gyp
@@ -14,6 +14,7 @@
{
'target_name': 'clear_browsing_data_dialog',
'dependencies': [
+ '<(DEPTH)/third_party/polymer/v1_0/components-chromium/iron-pages/compiled_resources2.gyp:iron-pages-extracted',
'<(DEPTH)/third_party/polymer/v1_0/components-chromium/iron-resizable-behavior/compiled_resources2.gyp:iron-resizable-behavior-extracted',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data',
diff --git a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/history_deletion_dialog.html b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/history_deletion_dialog.html
index a17572fb569..52429899184 100644
--- a/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/history_deletion_dialog.html
+++ b/chromium/chrome/browser/resources/settings/clear_browsing_data_dialog/history_deletion_dialog.html
@@ -11,7 +11,7 @@
<div slot="title">$i18n{historyDeletionDialogTitle}</div>
<div slot="body">$i18nRaw{historyDeletionDialogBody}</div>
<div slot="button-container">
- <paper-button class="action-button" on-tap="onOkTap_">
+ <paper-button class="action-button" on-click="onOkTap_">
$i18n{historyDeletionDialogOK}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/compiled_resources2.gyp b/chromium/chrome/browser/resources/settings/compiled_resources2.gyp
index a0f14ce056a..f56f7ebe8c4 100644
--- a/chromium/chrome/browser/resources/settings/compiled_resources2.gyp
+++ b/chromium/chrome/browser/resources/settings/compiled_resources2.gyp
@@ -57,6 +57,7 @@
'target_name': 'search_settings',
'dependencies': [
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:search_highlight_utils',
],
'includes': ['../../../../third_party/closure_compiler/compile_js2.gypi'],
},
@@ -79,6 +80,7 @@
'default_browser_page/compiled_resources2.gyp:*',
'device_page/compiled_resources2.gyp:*',
'downloads_page/compiled_resources2.gyp:*',
+ 'incompatible_applications_page/compiled_resources2.gyp:*',
'internet_page/compiled_resources2.gyp:*',
'languages_page/compiled_resources2.gyp:*',
'on_startup_page/compiled_resources2.gyp:*',
diff --git a/chromium/chrome/browser/resources/settings/controls/controlled_button.html b/chromium/chrome/browser/resources/settings/controls/controlled_button.html
index e0134790c7d..a5cf488854f 100644
--- a/chromium/chrome/browser/resources/settings/controls/controlled_button.html
+++ b/chromium/chrome/browser/resources/settings/controls/controlled_button.html
@@ -49,7 +49,7 @@
<paper-button disabled="[[enforced_]]">[[label]]</paper-button>
<template is="dom-if" if="[[hasPrefPolicyIndicator(pref.*)]]" restamp>
- <cr-policy-pref-indicator pref="[[pref]]" on-tap="onIndicatorTap_"
+ <cr-policy-pref-indicator pref="[[pref]]" on-click="onIndicatorTap_"
icon-aria-label="[[label]]">
</cr-policy-pref-indicator>
</template>
diff --git a/chromium/chrome/browser/resources/settings/controls/controlled_button.js b/chromium/chrome/browser/resources/settings/controls/controlled_button.js
index 307475678a9..49d46860349 100644
--- a/chromium/chrome/browser/resources/settings/controls/controlled_button.js
+++ b/chromium/chrome/browser/resources/settings/controls/controlled_button.js
@@ -32,7 +32,7 @@ Polymer({
* @private
*/
onIndicatorTap_: function(e) {
- // Disallow <controlled-button on-tap="..."> when controlled.
+ // Disallow <controlled-button on-click="..."> when controlled.
e.preventDefault();
e.stopPropagation();
},
diff --git a/chromium/chrome/browser/resources/settings/controls/controlled_radio_button.html b/chromium/chrome/browser/resources/settings/controls/controlled_radio_button.html
index 10b1bca7fe2..d6fcdf26b06 100644
--- a/chromium/chrome/browser/resources/settings/controls/controlled_radio_button.html
+++ b/chromium/chrome/browser/resources/settings/controls/controlled_radio_button.html
@@ -12,7 +12,7 @@
:host {
--ink-to-circle: calc((var(--paper-radio-button-ink-size) -
var(--paper-radio-button-size)) / 2);
- @apply(--settings-actionable);
+ @apply --settings-actionable;
align-items: center;
display: flex;
outline: none;
@@ -24,7 +24,7 @@
}
#label {
- color: var(--paper-radio-button-label-color, --primary-text-color);
+ color: var(--paper-radio-button-label-color, var(--primary-text-color));
}
.circle,
@@ -53,11 +53,12 @@
.circle {
border: 2px solid var(--paper-radio-button-unchecked-color,
- --primary-text-color);
+ var(--primary-text-color));
}
:host([checked]) .circle {
- border-color: var(--paper-radio-button-checked-color, --primary-color);
+ border-color: var(--paper-radio-button-checked-color,
+ var(--primary-color));
}
.disc {
@@ -69,23 +70,23 @@
:host([checked]) .disc {
background-color: var(--paper-radio-button-checked-color,
- --primary-color);
+ var(--primary-color));
transform: scale(0.5);
}
paper-ripple {
color: var(--paper-radio-button-unchecked-ink-color,
- --primary-text-color);
+ var(--primary-text-color));
opacity: .6;
}
:host([checked]) paper-ripple {
color: var(--paper-radio-button-checked-ink-color,
- --primary-text-color);
+ var(--primary-text-color));
}
:host(:not([controlled_])) {
- @apply(--settings-actionable);
+ @apply --settings-actionable;
}
:host([controlled_]) {
@@ -100,12 +101,12 @@
:host([controlled_]) .circle {
border-color: var(--paper-radio-button-unchecked-color,
- --primary-text-color);
+ var(--primary-text-color));
}
:host([controlled_][checked]) .disc {
background-color: var(--paper-radio-button-unchecked-color,
- --primary-text-color);
+ var(--primary-text-color));
}
:host([controlled_]) #labelWrapper {
@@ -132,7 +133,7 @@
</div>
<template is="dom-if" if="[[showIndicator_(controlled_, name, pref.*)]]">
- <cr-policy-pref-indicator pref="[[pref]]" on-tap="onIndicatorTap_"
+ <cr-policy-pref-indicator pref="[[pref]]" on-click="onIndicatorTap_"
icon-aria-label="[[label]]">
</cr-policy-pref-indicator>
</template>
diff --git a/chromium/chrome/browser/resources/settings/controls/controlled_radio_button.js b/chromium/chrome/browser/resources/settings/controls/controlled_radio_button.js
index ca55d6b2bb1..53bd0505a3c 100644
--- a/chromium/chrome/browser/resources/settings/controls/controlled_radio_button.js
+++ b/chromium/chrome/browser/resources/settings/controls/controlled_radio_button.js
@@ -48,7 +48,6 @@ Polymer({
'blur': 'updatePressed_',
'down': 'updatePressed_',
'focus': 'updatePressed_',
- 'tap': 'onTap_',
'up': 'updatePressed_',
},
@@ -88,17 +87,11 @@ Polymer({
* @private
*/
onIndicatorTap_: function(e) {
- // Disallow <controlled-radio-button on-tap="..."> when controlled.
+ // Disallow <controlled-radio-button on-click="..."> when controlled.
e.preventDefault();
e.stopPropagation();
},
- /** @private */
- onTap_: function() {
- if (!this.controlled_)
- this.checked = true;
- },
-
/**
* @param {!Event} e
* @private
diff --git a/chromium/chrome/browser/resources/settings/controls/extension_controlled_indicator.html b/chromium/chrome/browser/resources/settings/controls/extension_controlled_indicator.html
index ff53538c98f..50e3313c33c 100644
--- a/chromium/chrome/browser/resources/settings/controls/extension_controlled_indicator.html
+++ b/chromium/chrome/browser/resources/settings/controls/extension_controlled_indicator.html
@@ -17,7 +17,7 @@
}
img {
- @apply(--cr-icon-height-width);
+ @apply --cr-icon-height-width;
-webkit-margin-end: 16px;
}
@@ -32,7 +32,7 @@
<img role="presentation" src="chrome://extension-icon/[[extensionId]]/40/1">
<span inner-h-t-m-l="[[getLabel_(extensionId, extensionName)]]"></span>
<template is="dom-if" if="[[extensionCanBeDisabled]]" restamp>
- <paper-button class="secondary-button" on-tap="onDisableTap_">
+ <paper-button class="secondary-button" on-click="onDisableTap_">
$i18n{disable}
</paper-button>
</template>
diff --git a/chromium/chrome/browser/resources/settings/controls/important_site_checkbox.html b/chromium/chrome/browser/resources/settings/controls/important_site_checkbox.html
index 0a0dd1b1937..e70da80b8f5 100644
--- a/chromium/chrome/browser/resources/settings/controls/important_site_checkbox.html
+++ b/chromium/chrome/browser/resources/settings/controls/important_site_checkbox.html
@@ -20,7 +20,7 @@
}
paper-checkbox:not([checked]) .secondary {
- @apply(--settings-secondary-unchecked);
+ @apply --settings-secondary-unchecked;
}
.middot {
@@ -28,7 +28,7 @@
}
.label {
- @apply(--settings-checkbox-label);
+ @apply --settings-checkbox-label;
}
</style>
<div id="outerRow">
diff --git a/chromium/chrome/browser/resources/settings/controls/settings_checkbox.html b/chromium/chrome/browser/resources/settings/controls/settings_checkbox.html
index b46ee716595..0563536cb09 100644
--- a/chromium/chrome/browser/resources/settings/controls/settings_checkbox.html
+++ b/chromium/chrome/browser/resources/settings/controls/settings_checkbox.html
@@ -30,7 +30,7 @@
}
paper-checkbox:not([checked]) .secondary {
- @apply(--settings-secondary-unchecked);
+ @apply --settings-secondary-unchecked;
}
cr-policy-pref-indicator {
@@ -38,7 +38,7 @@
}
.label {
- @apply(--settings-checkbox-label);
+ @apply --settings-checkbox-label;
}
</style>
<div id="outerRow" noSubLabel$="[[!hasSubLabel_(subLabel, subLabelHtml)]]">
diff --git a/chromium/chrome/browser/resources/settings/controls/settings_checkbox.js b/chromium/chrome/browser/resources/settings/controls/settings_checkbox.js
index 3c0e8ded868..c4482b15c39 100644
--- a/chromium/chrome/browser/resources/settings/controls/settings_checkbox.js
+++ b/chromium/chrome/browser/resources/settings/controls/settings_checkbox.js
@@ -33,7 +33,7 @@ Polymer({
subLabelHtmlChanged_: function() {
const links = this.root.querySelectorAll('.secondary.label a');
links.forEach((link) => {
- link.addEventListener('tap', this.stopPropagation);
+ link.addEventListener('click', this.stopPropagation);
});
},
diff --git a/chromium/chrome/browser/resources/settings/controls/settings_toggle_button.html b/chromium/chrome/browser/resources/settings/controls/settings_toggle_button.html
index 7cdb86fdff3..2277abc6f7e 100644
--- a/chromium/chrome/browser/resources/settings/controls/settings_toggle_button.html
+++ b/chromium/chrome/browser/resources/settings/controls/settings_toggle_button.html
@@ -1,5 +1,6 @@
<link rel="import" href="chrome://resources/html/polymer.html">
+<link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html">
<link rel="import" href="chrome://resources/cr_elements/cr_toggle/cr_toggle.html">
<link rel="import" href="chrome://resources/cr_elements/policy/cr_policy_pref_indicator.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-flex-layout/iron-flex-layout-classes.html">
@@ -10,7 +11,7 @@
<template>
<style include="settings-shared iron-flex">
:host {
- @apply(--cr-section);
+ @apply --cr-section;
}
:host(.first),
@@ -29,7 +30,7 @@
}
:host([elide-label]) .label {
- @apply(--settings-text-elide);
+ @apply --cr-text-elide;
}
#outerRow {
diff --git a/chromium/chrome/browser/resources/settings/date_time_page/date_time_page.html b/chromium/chrome/browser/resources/settings/date_time_page/date_time_page.html
index c064fabcbef..20b846cb2e7 100644
--- a/chromium/chrome/browser/resources/settings/date_time_page/date_time_page.html
+++ b/chromium/chrome/browser/resources/settings/date_time_page/date_time_page.html
@@ -45,7 +45,7 @@
if="[[prefs.cros.flags.fine_grained_time_zone_detection_enabled.value]]"
restamp>
<div id="timeZoneSettingsTrigger" class="settings-box first"
- on-tap="onTimeZoneSettings_" actionable>
+ on-click="onTimeZoneSettings_" actionable>
<div id="timeZoneButton" class="two-line">
$i18n{timeZoneButton}
<div class="secondary">
@@ -79,7 +79,7 @@
label="$i18n{use24HourClock}">
</settings-toggle-button>
<div class="settings-box" id="setDateTime" actionable
- on-tap="onSetDateTimeTap_" hidden$="[[!canSetDateTime_]]">
+ on-click="onSetDateTimeTap_" hidden$="[[!canSetDateTime_]]">
<div class="start">$i18n{setDateTime}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{setDateTime}"></button>
diff --git a/chromium/chrome/browser/resources/settings/default_browser_page/default_browser_page.html b/chromium/chrome/browser/resources/settings/default_browser_page/default_browser_page.html
index ee383626bcc..c24783b0bd2 100644
--- a/chromium/chrome/browser/resources/settings/default_browser_page/default_browser_page.html
+++ b/chromium/chrome/browser/resources/settings/default_browser_page/default_browser_page.html
@@ -17,7 +17,7 @@
</div>
<div class="separator"></div>
<paper-button class="secondary-button"
- on-tap="onSetDefaultBrowserTap_">
+ on-click="onSetDefaultBrowserTap_">
$i18n{defaultBrowserMakeDefaultButton}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/device_page/device_page.html b/chromium/chrome/browser/resources/settings/device_page/device_page.html
index 56cc4fcfd76..cf90e9489fb 100644
--- a/chromium/chrome/browser/resources/settings/device_page/device_page.html
+++ b/chromium/chrome/browser/resources/settings/device_page/device_page.html
@@ -24,7 +24,7 @@
focus-config="[[focusConfig_]]">
<neon-animatable id="main" route-path="default">
<div id="pointersRow" class="settings-box first"
- on-tap="onPointersTap_" actionable>
+ on-click="onPointersTap_" actionable>
<div class="start">
[[getPointersTitle_(hasMouse_, hasTouchpad_)]]
</div>
@@ -32,34 +32,34 @@
aria-label$="[[getPointersTitle_(hasMouse_,
hasTouchpad_)]]"></button>
</div>
- <div id="keyboardRow" class="settings-box" on-tap="onKeyboardTap_"
+ <div id="keyboardRow" class="settings-box" on-click="onKeyboardTap_"
actionable>
<div class="start">$i18n{keyboardTitle}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{keyboardTitle}"></button>
</div>
<template is="dom-if" if="[[hasStylus_]]">
- <div id="stylusRow" class="settings-box" on-tap="onStylusTap_"
+ <div id="stylusRow" class="settings-box" on-click="onStylusTap_"
actionable>
<div class="start">$i18n{stylusTitle}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{stylusTitle}"></button>
</div>
</template>
- <div id="displayRow" class="settings-box" on-tap="onDisplayTap_"
+ <div id="displayRow" class="settings-box" on-click="onDisplayTap_"
actionable>
<div class="start">$i18n{displayTitle}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{displayTitle}"></button>
</div>
- <div id="storageRow" class="settings-box" on-tap="onStorageTap_"
+ <div id="storageRow" class="settings-box" on-click="onStorageTap_"
actionable>
<div class="start">$i18n{storageTitle}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{storageTitle}"></button>
</div>
<template is="dom-if" if="[[enablePowerSettings_]]">
- <div id="powerRow" class="settings-box" on-tap="onPowerTap_"
+ <div id="powerRow" class="settings-box" on-click="onPowerTap_"
actionable>
<div class="start">$i18n{powerTitle}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
diff --git a/chromium/chrome/browser/resources/settings/device_page/device_page_browser_proxy.js b/chromium/chrome/browser/resources/settings/device_page/device_page_browser_proxy.js
index 0344e145e0b..ae2fb6d008c 100644
--- a/chromium/chrome/browser/resources/settings/device_page/device_page_browser_proxy.js
+++ b/chromium/chrome/browser/resources/settings/device_page/device_page_browser_proxy.js
@@ -185,7 +185,7 @@ cr.define('settings', function() {
/** override */
handleLinkEvent(e) {
- // Prevent the link from activating its parent element when tapped or
+ // Prevent the link from activating its parent element when clicked or
// when Enter is pressed.
if (e.type != 'keydown' || e.keyCode == 13)
e.stopPropagation();
diff --git a/chromium/chrome/browser/resources/settings/device_page/display.html b/chromium/chrome/browser/resources/settings/device_page/display.html
index 81cf27e5056..0eec56f0f97 100644
--- a/chromium/chrome/browser/resources/settings/device_page/display.html
+++ b/chromium/chrome/browser/resources/settings/device_page/display.html
@@ -82,7 +82,7 @@
restamp>
<div class="secondary self-start">
<paper-checkbox checked="[[isMirrored_(displays)]]"
- on-tap="onMirroredTap_"
+ on-click="onMirroredTap_"
aria-label="[[getDisplayMirrorText_(displays)]]">
<div class="text-area">[[getDisplayMirrorText_(displays)]]</div>
</paper-checkbox>
@@ -188,7 +188,7 @@
<button is="cr-link-row" icon-class="subpage-arrow" class="indented hr"
id="overscan" label="$i18n{displayOverscanPageTitle}"
- sub-label="$i18n{displayOverscanPageText}" on-tap="onOverscanTap_"
+ sub-label="$i18n{displayOverscanPageText}" on-click="onOverscanTap_"
hidden$="[[!showOverscanSetting_(selectedDisplay)]]">
</button>
@@ -198,7 +198,7 @@
</settings-display-overscan-dialog>
<div class="settings-box indented two-line"
- on-tap="onTouchCalibrationTap_"
+ on-click="onTouchCalibrationTap_"
hidden$="[[!showTouchCalibrationSetting_(selectedDisplay)]]"
actionable>
<div class="start">
diff --git a/chromium/chrome/browser/resources/settings/device_page/display.js b/chromium/chrome/browser/resources/settings/device_page/display.js
index c21864b443b..2fcb479af3b 100644
--- a/chromium/chrome/browser/resources/settings/device_page/display.js
+++ b/chromium/chrome/browser/resources/settings/device_page/display.js
@@ -699,25 +699,18 @@ Polymer({
// Blur the control so that when the transition animation completes and the
// UI is focused, the control does not receive focus. crbug.com/785070
event.target.blur();
- let id = '';
- /** @type {!chrome.system.display.DisplayProperties} */
- const properties = {};
- if (this.isMirrored_(this.displays)) {
- id = this.primaryDisplayId;
- properties.mirroringSourceId = '';
- } else {
- // Set the mirroringSourceId of the secondary (first non-primary) display.
- for (let i = 0; i < this.displays.length; ++i) {
- const display = this.displays[i];
- if (display.id != this.primaryDisplayId) {
- id = display.id;
- break;
- }
- }
- properties.mirroringSourceId = this.primaryDisplayId;
- }
- settings.display.systemDisplayApi.setDisplayProperties(
- id, properties, this.setPropertiesCallback_.bind(this));
+
+ /** @type {!chrome.system.display.MirrorModeInfo} */
+ let mirrorModeInfo = {
+ mode: this.isMirrored_(this.displays) ?
+ chrome.system.display.MirrorMode.OFF :
+ chrome.system.display.MirrorMode.NORMAL
+ };
+ settings.display.systemDisplayApi.setMirrorMode(mirrorModeInfo, () => {
+ let error = chrome.runtime.lastError;
+ if (error)
+ console.error('setMirrorMode Error: ' + error.message);
+ });
},
/** @private */
diff --git a/chromium/chrome/browser/resources/settings/device_page/display_layout.html b/chromium/chrome/browser/resources/settings/device_page/display_layout.html
index 289ebf9c8f2..4b138cb2b17 100644
--- a/chromium/chrome/browser/resources/settings/device_page/display_layout.html
+++ b/chromium/chrome/browser/resources/settings/device_page/display_layout.html
@@ -54,7 +54,7 @@
}
.display.elevate {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
}
</style>
<div id="displayArea" on-iron-resize="calculateVisualScale_">
@@ -67,15 +67,11 @@
</template>
<template is="dom-repeat" items="[[displays]]">
<div id="_[[item.id]]" class="display elevate"
- draggable="[[dragEnabled]]" on-tap="onSelectDisplayTap_"
+ draggable="[[dragEnabled]]" on-click="onSelectDisplayTap_"
style$="[[getDivStyle_(item.id, item.bounds, visualScale)]]"
selected$="[[isSelected_(item, selectedDisplay)]]">
- <div hidden$="[[mirroring]]">
- [[item.name]]
- </div>
- <div hidden$="[[!mirroring]]">
- $i18n{displayMirrorDisplayName}
- </div>
+ [[getDisplayName_(mirroring, item.name,
+ '$i18nPolymer{displayMirrorDisplayName}')]]
</div>
</template>
</div>
diff --git a/chromium/chrome/browser/resources/settings/device_page/display_layout.js b/chromium/chrome/browser/resources/settings/device_page/display_layout.js
index 4b9f4192931..6cd70ca9581 100644
--- a/chromium/chrome/browser/resources/settings/device_page/display_layout.js
+++ b/chromium/chrome/browser/resources/settings/device_page/display_layout.js
@@ -191,6 +191,17 @@ Polymer({
},
/**
+ * @param {boolean} mirroring
+ * @param {string} displayName
+ * @param {string} mirroringName
+ * @return {string}
+ * @private
+ */
+ getDisplayName_: function(mirroring, displayName, mirroringName) {
+ return mirroring ? mirroringName : displayName;
+ },
+
+ /**
* @param {!chrome.system.display.DisplayUnitInfo} display
* @param {!chrome.system.display.DisplayUnitInfo} selectedDisplay
* @return {boolean}
diff --git a/chromium/chrome/browser/resources/settings/device_page/display_overscan_dialog.html b/chromium/chrome/browser/resources/settings/device_page/display_overscan_dialog.html
index 08e263c7738..5c90de1c152 100644
--- a/chromium/chrome/browser/resources/settings/device_page/display_overscan_dialog.html
+++ b/chromium/chrome/browser/resources/settings/device_page/display_overscan_dialog.html
@@ -74,10 +74,10 @@
</div>
</div>
<div slot="button-container">
- <paper-button id="reset" class="cancel-button" on-tap="onResetTap_">
+ <paper-button id="reset" class="cancel-button" on-click="onResetTap_">
$i18n{displayOverscanReset}
</paper-button>
- <paper-button class="action-button" on-tap="onSaveTap_">
+ <paper-button class="action-button" on-click="onSaveTap_">
$i18n{ok}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/device_page/drive_cache_dialog.html b/chromium/chrome/browser/resources/settings/device_page/drive_cache_dialog.html
index 696cbad5190..d111fcf73a3 100644
--- a/chromium/chrome/browser/resources/settings/device_page/drive_cache_dialog.html
+++ b/chromium/chrome/browser/resources/settings/device_page/drive_cache_dialog.html
@@ -17,11 +17,11 @@
</div>
<div slot="button-container">
<paper-button id="cancelButton" class="cancel-button"
- on-tap="onCancelTap_">
+ on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
<paper-button id="deleteButton" class="action-button"
- on-tap="onDeleteTap_">
+ on-click="onDeleteTap_">
$i18n{storageDeleteAllButtonTitle}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/device_page/keyboard.html b/chromium/chrome/browser/resources/settings/device_page/keyboard.html
index cabf3ca8e43..1ec10018922 100644
--- a/chromium/chrome/browser/resources/settings/device_page/keyboard.html
+++ b/chromium/chrome/browser/resources/settings/device_page/keyboard.html
@@ -104,12 +104,12 @@
</div>
</iron-collapse>
<div id="keyboardOverlay" class="settings-box"
- on-tap="onShowKeyboardShortcutsOverlayTap_" actionable>
+ on-click="onShowKeyboardShortcutsOverlayTap_" actionable>
<div class="start">$i18n{showKeyboardShortcutsOverlay}</div>
<button class="icon-external" is="paper-icon-button-light"
aria-label="$i18n{showKeyboardShortcutsOverlay}"></button>
</div>
- <div class="settings-box" on-tap="onShowLanguageInputTap_" actionable>
+ <div class="settings-box" on-click="onShowLanguageInputTap_" actionable>
<div class="start">$i18n{keyboardShowLanguageAndInput}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{keyboardShowLanguageAndInput}"></button>
diff --git a/chromium/chrome/browser/resources/settings/device_page/pointers.html b/chromium/chrome/browser/resources/settings/device_page/pointers.html
index d93db637cf5..0fb133e2933 100644
--- a/chromium/chrome/browser/resources/settings/device_page/pointers.html
+++ b/chromium/chrome/browser/resources/settings/device_page/pointers.html
@@ -85,7 +85,7 @@
<paper-radio-button name="true">
$i18n{naturalScrollLabel}
<a href="$i18n{naturalScrollLearnMoreLink}" target="_blank"
- on-tap="onLearnMoreLinkActivated_"
+ on-click="onLearnMoreLinkActivated_"
on-keydown="onLearnMoreLinkActivated_">
$i18n{naturalScrollLearnMore}
</a>
diff --git a/chromium/chrome/browser/resources/settings/device_page/storage.html b/chromium/chrome/browser/resources/settings/device_page/storage.html
index a10225b2f15..fc8cc1df590 100644
--- a/chromium/chrome/browser/resources/settings/device_page/storage.html
+++ b/chromium/chrome/browser/resources/settings/device_page/storage.html
@@ -195,7 +195,7 @@
</div>
</div>
</div>
- <div class="settings-box two-line" on-tap="onDownloadsTap_" actionable>
+ <div class="settings-box two-line" on-click="onDownloadsTap_" actionable>
<div class="start">
$i18n{storageItemDownloads}
<div id="downloadsSize" class="secondary">
@@ -207,7 +207,7 @@
aria-describedby="downloadsSize"></button>
</div>
<template is="dom-if" if="[[driveEnabled_]]">
- <div class="settings-box two-line" on-tap="onDriveCacheTap_"
+ <div class="settings-box two-line" on-click="onDriveCacheTap_"
actionable$="[[hasDriveCache_]]" >
<div class="start">
$i18n{storageItemDriveCache}
@@ -221,7 +221,7 @@
</button>
</div>
</template>
- <div class="settings-box two-line" on-tap="onBrowsingDataTap_" actionable>
+ <div class="settings-box two-line" on-click="onBrowsingDataTap_" actionable>
<div class="start">
$i18n{storageItemBrowsingData}
<div id="browsingDataSize" class="secondary">
@@ -233,7 +233,7 @@
aria-describedby="browsingDataSize"></button>
</div>
<template is="dom-if" if="[[androidEnabled_]]">
- <div class="settings-box two-line" on-tap="onAndroidTap_" actionable>
+ <div class="settings-box two-line" on-click="onAndroidTap_" actionable>
<div class="start">
$i18n{storageItemAndroid}
<div id="androidSize" class="secondary">
@@ -246,7 +246,7 @@
</div>
</template>
<template is="dom-if" if="[[!isGuest_]]">
- <div class="settings-box two-line" on-tap="onOtherUsersTap_" actionable>
+ <div class="settings-box two-line" on-click="onOtherUsersTap_" actionable>
<div class="start">
$i18n{storageItemOtherUsers}
<div id="otherUsersSize" class="secondary">
diff --git a/chromium/chrome/browser/resources/settings/device_page/stylus.html b/chromium/chrome/browser/resources/settings/device_page/stylus.html
index 9aa157eceab..93252074b04 100644
--- a/chromium/chrome/browser/resources/settings/device_page/stylus.html
+++ b/chromium/chrome/browser/resources/settings/device_page/stylus.html
@@ -19,7 +19,7 @@
paper-spinner-lite {
margin-left: 12px;
- @apply(--cr-icon-height-width);
+ @apply --cr-icon-height-width;
}
cr-policy-indicator {
@@ -77,7 +77,7 @@
<!-- TODO(scottchen): Make a proper a[href].settings-box with
icon-external (see: https://crbug.com/684005)-->
- <div class="settings-box two-line" on-tap="onFindAppsTap_" actionable
+ <div class="settings-box two-line" on-click="onFindAppsTap_" actionable
hidden$="[[!prefs.arc.enabled.value]]">
<div class="start">
$i18n{stylusFindMoreAppsPrimary}
@@ -97,7 +97,7 @@
<div class="settings-box first">
<div id="lock-screen-toggle-label" class="start"
actionable$="[[!disallowedOnLockScreenByPolicy_(selectedApp_)]]"
- on-tap="toggleLockScreenSupport_">
+ on-click="toggleLockScreenSupport_">
$i18n{stylusNoteTakingAppEnabledOnLockScreen}
</div>
<template is="dom-if"
diff --git a/chromium/chrome/browser/resources/settings/downloads_page/downloads_page.html b/chromium/chrome/browser/resources/settings/downloads_page/downloads_page.html
index 2f213e62e6a..ea4a199cb0a 100644
--- a/chromium/chrome/browser/resources/settings/downloads_page/downloads_page.html
+++ b/chromium/chrome/browser/resources/settings/downloads_page/downloads_page.html
@@ -29,7 +29,7 @@
<div class="separator"></div>
<controlled-button class="secondary-button" id="changeDownloadsPath"
label="$i18n{changeDownloadLocation}"
- on-tap="selectDownloadLocation_"
+ on-click="selectDownloadLocation_"
pref="[[prefs.download.default_directory]]"
end-justified>
</controlled-button>
@@ -52,7 +52,7 @@
</div>
<div class="separator"></div>
<paper-button id="resetAutoOpenFileTypes" class="secondary-button"
- on-tap="onClearAutoOpenFileTypesTap_">
+ on-click="onClearAutoOpenFileTypesTap_">
$i18n{clear}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/google_assistant_page/google_assistant_page.html b/chromium/chrome/browser/resources/settings/google_assistant_page/google_assistant_page.html
index b96f029582e..46081160c54 100644
--- a/chromium/chrome/browser/resources/settings/google_assistant_page/google_assistant_page.html
+++ b/chromium/chrome/browser/resources/settings/google_assistant_page/google_assistant_page.html
@@ -30,7 +30,7 @@
on-change="onGoogleAssistantContextEnableChange_">
</settings-toggle-button>
<div id="googleAssistantSettings" class="settings-box"
- on-tap="onGoogleAssistantSettingsTapped_" actionable>
+ on-click="onGoogleAssistantSettingsTapped_" actionable>
<div class="start">
$i18n{googleAssistantSettings}
</div>
diff --git a/chromium/chrome/browser/resources/settings/icons.html b/chromium/chrome/browser/resources/settings/icons.html
index c15dd7ba4a7..bf9a0008176 100644
--- a/chromium/chrome/browser/resources/settings/icons.html
+++ b/chromium/chrome/browser/resources/settings/icons.html
@@ -10,9 +10,7 @@ List icons here rather than importing large sets of (e.g. Polymer) icons.
<defs>
<!-- Ads icon in the Content Settings -->
<g id="ads">
- <path d="M19,3H5C3.89,3,3,3.9,3,5v14c0,1.1,0.89,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3z M11,15H9.5v-1.5h-2V15H6v-4.5V9h1.5h2H10h1V15z M18,14c0,0.55-0.45,1-1,1h-4V9h4c0.55,0,1,0.45,1,1V14z"></path>
- <rect x="7.5" y="10.5" width="2" height="1.5"></rect>
- <rect x="14.5" y="10.5" width="2" height="3"></rect>
+ <path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm0 14H5V8h14v10z"></path>
</g>
<!-- Cookie SVG obtained from rolfe@ -->
@@ -104,6 +102,7 @@ List icons here rather than importing large sets of (e.g. Polymer) icons.
<g id="refresh"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"></path></g>
<g id="restore"><path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"></path></g>
<g id="rotate-right"><path d="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z"></path></g>
+ <g id="sensors"><path d="M10 8.5c-0.8 0-1.5 0.7-1.5 1.5s0.7 1.5 1.5 1.5s1.5-0.7 1.5-1.5S10.8 8.5 10 8.5z M7.6 5.8 C6.2 6.7 5.2 8.2 5.2 10c0 1.8 1 3.4 2.4 4.2l0.8-1.4c-1-0.6-1.6-1.6-1.6-2.8c0-1.2 0.6-2.2 1.6-2.8L7.6 5.8z M14.8 10 c0-1.8-1-3.4-2.4-4.2l-0.8 1.4c0.9 0.6 1.6 1.6 1.6 2.8c0 1.2-0.6 2.2-1.6 2.8l0.8 1.4C13.8 13.4 14.8 11.8 14.8 10z M6 3 c-2.4 1.4-4 4-4 7c0 3 1.6 5.6 4 7l0.8-1.4c-1.9-1.1-3.2-3.2-3.2-5.6c0-2.4 1.3-4.5 3.2-5.6L6 3z M13.2 4.4 c1.9 1.1 3.2 3.2 3.2 5.6c0 2.4-1.3 4.5-3.2 5.6L14 17c2.4-1.4 4-4 4-7c0-3-1.6-5.6-4-7L13.2 4.4z"></path></g>
<g id="security"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"></path></g>
<if expr="chromeos">
<g id="alert-device-out-of-range" fill="none" fill-rule="evenodd"><path d="M-1-1h20v20H-1z"></path><path fill="#C53929" fill-rule="nonzero" d="M8.167 11.5h1.666v1.667H8.167V11.5zm0-6.667h1.666v5H8.167v-5zM8.992.667C4.392.667.667 4.4.667 9s3.725 8.333 8.325 8.333c4.608 0 8.341-3.733 8.341-8.333S13.6.667 8.992.667zm.008 15A6.665 6.665 0 0 1 2.333 9 6.665 6.665 0 0 1 9 2.333 6.665 6.665 0 0 1 15.667 9 6.665 6.665 0 0 1 9 15.667z"></path></g>
diff --git a/chromium/chrome/browser/resources/settings/images/sync_banner.svg b/chromium/chrome/browser/resources/settings/images/sync_banner.svg
new file mode 100644
index 00000000000..d2ecb8603a7
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/images/sync_banner.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="155px" height="131px" viewBox="0 0 155 131" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <polygon fill="#3874D5" points="0 114.5 154.75 114.5 154.75 0 0 0"></polygon>
+ <g transform="translate(61.000000, 53.250000)">
+ <path d="M59.25,77 L19,77 C17.35,77 16,75.65 16,74 L16,3 C16,1.35 17.35,0 19,0 L59.25,0 C60.9,0 62.25,1.35 62.25,3 L62.25,74 C62.25,75.65 60.9,77 59.25,77" fill="#4285F4"></path>
+ <path d="M31.4473,61.5527 C31.4473,70.0837 24.5313,76.9997 16.0003,76.9997 C7.4683,76.9997 0.5523,70.0837 0.5523,61.5527 C0.5523,53.0207 7.4683,46.1047 16.0003,46.1047 C24.5313,46.1047 31.4473,53.0207 31.4473,61.5527" fill="#FABB05"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/settings/incompatible_applications_page/compiled_resources2.gyp b/chromium/chrome/browser/resources/settings/incompatible_applications_page/compiled_resources2.gyp
new file mode 100644
index 00000000000..c73f9ebe50a
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/incompatible_applications_page/compiled_resources2.gyp
@@ -0,0 +1,33 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+{
+ 'targets': [
+ {
+ 'target_name': 'incompatible_applications_browser_proxy',
+ 'dependencies': [
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
+ 'target_name': 'incompatible_applications_page',
+ 'dependencies': [
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:assert',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:i18n_behavior',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:web_ui_listener_behavior',
+ 'incompatible_applications_browser_proxy',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ {
+ 'target_name': 'incompatible_application_item',
+ 'dependencies': [
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:assert',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:i18n_behavior',
+ 'incompatible_applications_browser_proxy',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
+ ],
+}
diff --git a/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_application_item.html b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_application_item.html
new file mode 100644
index 00000000000..1f8027ae747
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_application_item.html
@@ -0,0 +1,25 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/html/assert.html">
+<link rel="import" href="chrome://resources/html/i18n_behavior.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
+<link rel="import" href="../settings_shared_css.html">
+<link rel="import" href="incompatible_applications_browser_proxy.html">
+
+<dom-module id="incompatible-application-item">
+ <template>
+ <style include="settings-shared">
+ :host {
+ display: block;
+ }
+ </style>
+ <div class="list-item">
+ <div class="start">[[applicationName]]</div>
+ <div class="separator"></div>
+ <paper-button class="primary-button" on-click="onActionTap_">
+ [[getActionName_(actionType)]]
+ </paper-button>
+ </div>
+ </template>
+ <script src="incompatible_application_item.js"></script>
+</dom-module>
diff --git a/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_application_item.js b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_application_item.js
new file mode 100644
index 00000000000..72dd7f1941e
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_application_item.js
@@ -0,0 +1,103 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview
+ * 'incompatible-application-item' represents one item in a "list-box" of
+ * incompatible applications, as defined in
+ * chrome/browser/conflicts/problematic_programs_updater_win.h.
+ * This element contains a button that can be used to remove or update the
+ * incompatible application, depending on the value of the action-type property.
+ *
+ * Example usage:
+ *
+ * <div class="list-box">
+ * <incompatible-application-item
+ * application-name="Google Chrome"
+ * action-type="1"
+ * action-url="https://www.google.com/chrome/more-info">
+ * </incompatible-application-item>
+ * </div>
+ *
+ * or
+ *
+ * <div class="list-box">
+ * <template is="dom-repeat" items="[[applications]]" as="application">
+ * <incompatible-application-item
+ * application-name="[[application.name]]"
+ * action-type="[[application.actionType]]"
+ * action-url="[[application.actionUrl]]">
+ * </incompatible-application-item>
+ * </template>
+ * </div>
+ */
+
+Polymer({
+ is: 'incompatible-application-item',
+
+ behaviors: [I18nBehavior],
+
+ properties: {
+ /**
+ * The name of the application to be displayed. Also used for the UNINSTALL
+ * action, where the name is passed to the startProgramUninstallation()
+ * call.
+ */
+ applicationName: String,
+
+ /**
+ * The type of the action to be taken on this incompatible application. Must
+ * be one of BlacklistMessageType in
+ * chrome/browser/conflicts/proto/module_list.proto.
+ * @type {!settings.ActionTypes}
+ */
+ actionType: Number,
+
+ /**
+ * For the actions MORE_INFO and UPGRADE, this is the URL that must be
+ * opened when the action button is tapped.
+ */
+ actionUrl: String,
+ },
+
+ /** @private {settings.IncompatibleApplicationsBrowserProxy} */
+ browserProxy_: null,
+
+ /** @override */
+ created: function() {
+ this.browserProxy_ =
+ settings.IncompatibleApplicationsBrowserProxyImpl.getInstance();
+ },
+
+ /**
+ * Executes the action for this incompatible application, depending on
+ * actionType.
+ * @private
+ */
+ onActionTap_: function() {
+ if (this.actionType === settings.ActionTypes.UNINSTALL) {
+ this.browserProxy_.startProgramUninstallation(this.applicationName);
+ } else if (
+ this.actionType === settings.ActionTypes.MORE_INFO ||
+ this.actionType === settings.ActionTypes.UPGRADE) {
+ this.browserProxy_.openURL(this.actionUrl);
+ } else {
+ assertNotReached();
+ }
+ },
+
+ /**
+ * @return {string} The label that should be applied to the action button.
+ * @private
+ */
+ getActionName_: function(actionType) {
+ if (actionType === settings.ActionTypes.UNINSTALL)
+ return this.i18n('incompatibleApplicationsRemoveButton');
+ if (actionType === settings.ActionTypes.MORE_INFO)
+ return this.i18n('learnMore');
+ if (actionType === settings.ActionTypes.UPGRADE)
+ return this.i18n('incompatibleApplicationsUpdateButton');
+ assertNotReached();
+ },
+});
diff --git a/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_browser_proxy.html b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_browser_proxy.html
new file mode 100644
index 00000000000..8d55a232daa
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_browser_proxy.html
@@ -0,0 +1,2 @@
+<link rel="import" href="chrome://resources/html/cr.html">
+<script src="incompatible_applications_browser_proxy.js"></script>
diff --git a/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_browser_proxy.js b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_browser_proxy.js
new file mode 100644
index 00000000000..7017e0a469d
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_browser_proxy.js
@@ -0,0 +1,122 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview A helper object used from the Incompatible Applications section
+ * to interact with the browser.
+ */
+
+cr.exportPath('settings');
+
+/**
+ * All possible actions to take on an incompatible application.
+ *
+ * Must be kept in sync with BlacklistMessageType in
+ * chrome/browser/conflicts/proto/module_list.proto
+ * @readonly
+ * @enum {number}
+ */
+settings.ActionTypes = {
+ UNINSTALL: 0,
+ MORE_INFO: 1,
+ UPGRADE: 2,
+};
+
+/**
+ * @typedef {{
+ * name: string,
+ * actionType: {settings.ActionTypes},
+ * actionUrl: string,
+ * }}
+ */
+settings.IncompatibleApplication;
+
+cr.define('settings', function() {
+ /** @interface */
+ class IncompatibleApplicationsBrowserProxy {
+ /**
+ * Get the list of incompatible applications.
+ * @return {!Promise<!Array<!settings.IncompatibleApplication>>}
+ */
+ requestIncompatibleApplicationsList() {}
+
+ /**
+ * Launches the Apps & Features page that allows uninstalling 'programName'.
+ * @param {string} programName
+ */
+ startProgramUninstallation(programName) {}
+
+ /**
+ * Opens the specified URL in a new tab.
+ * @param {!string} url
+ */
+ openURL(url) {}
+
+ /**
+ * Requests the plural string for the subtitle of the Incompatible
+ * Applications subpage.
+ * @param {number} numApplications
+ * @return {!Promise<string>}
+ */
+ getSubtitlePluralString(numApplications) {}
+
+ /**
+ * Requests the plural string for the subtitle of the Incompatible
+ * Applications subpage, when the user does not have administrator rights.
+ * @param {number} numApplications
+ * @return {!Promise<string>}
+ */
+ getSubtitleNoAdminRightsPluralString(numApplications) {}
+
+ /**
+ * Requests the plural string for the title of the list of Incompatible
+ * Applications.
+ * @param {number} numApplications
+ * @return {!Promise<string>}
+ */
+ getListTitlePluralString(numApplications) {}
+ }
+
+ /** @implements {settings.IncompatibleApplicationsBrowserProxy} */
+ class IncompatibleApplicationsBrowserProxyImpl {
+ /** @override */
+ requestIncompatibleApplicationsList() {
+ return cr.sendWithPromise('requestIncompatibleApplicationsList');
+ }
+
+ /** @override */
+ startProgramUninstallation(programName) {
+ chrome.send('startProgramUninstallation', [programName]);
+ }
+
+ /** @override */
+ openURL(url) {
+ window.open(url);
+ }
+
+ /** @override */
+ getSubtitlePluralString(numApplications) {
+ return cr.sendWithPromise('getSubtitlePluralString', numApplications);
+ }
+
+ /** @override */
+ getSubtitleNoAdminRightsPluralString(numApplications) {
+ return cr.sendWithPromise(
+ 'getSubtitleNoAdminRightsPluralString', numApplications);
+ }
+
+ /** @override */
+ getListTitlePluralString(numApplications) {
+ return cr.sendWithPromise('getListTitlePluralString', numApplications);
+ }
+ }
+
+ cr.addSingletonGetter(IncompatibleApplicationsBrowserProxyImpl);
+
+ return {
+ IncompatibleApplicationsBrowserProxy: IncompatibleApplicationsBrowserProxy,
+ IncompatibleApplicationsBrowserProxyImpl:
+ IncompatibleApplicationsBrowserProxyImpl,
+ };
+});
diff --git a/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_page.html b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_page.html
new file mode 100644
index 00000000000..9fcccb14ef3
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_page.html
@@ -0,0 +1,59 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/html/assert.html">
+<link rel="import" href="chrome://resources/html/i18n_behavior.html">
+<link rel="import" href="chrome://resources/html/web_ui_listener_behavior.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html">
+<link rel="import" href="../settings_shared_css.html">
+<link rel="import" href="incompatible_application_item.html">
+<link rel="import" href="incompatible_applications_browser_proxy.html">
+
+<dom-module id="settings-incompatible-applications-page">
+ <template>
+ <style include="settings-shared">
+ #is-done-section > iron-icon {
+ --iron-icon-fill-color: var(--google-blue-500);
+ }
+ </style>
+
+ <div hidden$="[[!isDone_]]" id="is-done-section" class="settings-box first">
+ <iron-icon icon="settings:check-circle"></iron-icon>
+ <div class="middle no-min-width">
+ $i18n{incompatibleApplicationsDone}
+ </div>
+ </div>
+
+ <template is="dom-if" if="[[!isDone_]]">
+ <div class="settings-box first two-line">
+ <iron-icon icon="settings:security"></iron-icon>
+ <div class="middle no-min-width">
+ <div hidden$="[[!hasAdminRights_]]">
+ [[subtitleText_]] $i18nRaw{incompatibleApplicationsSubpageLearnHow}
+ </div>
+ <div hidden$="[[hasAdminRights_]]">
+ [[subtitleNoAdminRightsText_]]
+ </div>
+ </div>
+ </div>
+ <div class="settings-box continuation">
+ <div class="secondary">[[listTitleText_]]</div>
+ </div>
+ <div id="incompatible-applications-list" class="list-frame vertical-list">
+ <template is="dom-repeat" items="[[applications_]]" as="application">
+ <incompatible-application-item
+ hidden$="[[!hasAdminRights_]]"
+ class="incompatible-application"
+ application-name="[[application.name]]"
+ action-type="[[application.type]]"
+ action-url="[[application.url]]">
+ </incompatible-application-item>
+ <div hidden$="[[hasAdminRights_]]"
+ class="list-item incompatible-application">
+ [[application.name]]
+ </div>
+ </template>
+ </div>
+ </template>
+ </template>
+ <script src="incompatible_applications_page.js"></script>
+</dom-module>
diff --git a/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_page.js b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_page.js
new file mode 100644
index 00000000000..27426d236a8
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/incompatible_applications_page/incompatible_applications_page.js
@@ -0,0 +1,138 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview
+ * 'settings-incompatible-applications-page' is the settings subpage containing
+ * the list of incompatible applications.
+ *
+ * Example:
+ *
+ * <iron-animated-pages>
+ * <settings-incompatible-applications-page">
+ * </settings-incompatible-applications-page>
+ * ... other pages ...
+ * </iron-animated-pages>
+ */
+
+Polymer({
+ is: 'settings-incompatible-applications-page',
+
+ behaviors: [I18nBehavior, WebUIListenerBehavior],
+
+ properties: {
+ /**
+ * Indicates if the current user has administrator rights.
+ * @private
+ */
+ hasAdminRights_: {
+ type: Boolean,
+ value: function() {
+ return loadTimeData.getBoolean('hasAdminRights');
+ },
+ },
+
+ /**
+ * The list of all the incompatible applications.
+ * @private {Array<settings.IncompatibleApplication>}
+ */
+ applications_: Array,
+
+ /**
+ * Determines if the user has finished with this page.
+ * @private
+ */
+ isDone_: {
+ type: Boolean,
+ computed: 'computeIsDone_(applications_.*)',
+ },
+
+ /**
+ * The text for the subtitle of the subpage.
+ * @private
+ */
+ subtitleText_: {
+ type: String,
+ value: '',
+ },
+
+ /**
+ * The text for the subtitle of the subpage, when the user does not have
+ * administrator rights.
+ * @private
+ */
+ subtitleNoAdminRightsText_: {
+ type: String,
+ value: '',
+ },
+
+ /**
+ * The text for the title of the list of incompatible applications.
+ * @private
+ */
+ listTitleText_: {
+ type: String,
+ value: '',
+ },
+ },
+
+ /** @override */
+ ready: function() {
+ this.addWebUIListener(
+ 'incompatible-application-removed',
+ this.onIncompatibleApplicationRemoved_.bind(this));
+
+ settings.IncompatibleApplicationsBrowserProxyImpl.getInstance()
+ .requestIncompatibleApplicationsList()
+ .then(list => {
+ this.applications_ = list;
+ this.updatePluralStrings_();
+ });
+ },
+
+ /**
+ * @return {boolean}
+ * @private
+ */
+ computeIsDone_: function() {
+ return this.applications_.length === 0;
+ },
+
+ /**
+ * Removes a single incompatible application from the |applications_| list.
+ * @private
+ */
+ onIncompatibleApplicationRemoved_: function(applicationName) {
+ // Find the index of the element.
+ let index = this.applications_.findIndex(function(application) {
+ return application.name == applicationName;
+ });
+
+ assert(index !== -1);
+
+ this.splice('applications_', index, 1);
+ },
+
+ /**
+ * Updates the texts of the Incompatible Applications subpage that depends on
+ * the length of |applications_|.
+ * @private
+ */
+ updatePluralStrings_: function() {
+ const browserProxy =
+ settings.IncompatibleApplicationsBrowserProxyImpl.getInstance();
+ const numApplications = this.applications_.length;
+ Promise
+ .all([
+ browserProxy.getSubtitlePluralString(numApplications),
+ browserProxy.getSubtitleNoAdminRightsPluralString(numApplications),
+ browserProxy.getListTitlePluralString(numApplications),
+ ])
+ .then(strings => {
+ this.subtitleText_ = strings[0];
+ this.subtitleNoAdminRightsText_ = strings[1];
+ this.listTitleText_ = strings[2];
+ });
+ },
+});
diff --git a/chromium/chrome/browser/resources/settings/internet_page/internet_config.html b/chromium/chrome/browser/resources/settings/internet_page/internet_config.html
index 4fa0a79d5e8..fce0c7e3d29 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/internet_config.html
+++ b/chromium/chrome/browser/resources/settings/internet_page/internet_config.html
@@ -32,7 +32,7 @@
share-allow-enable="[[shareAllowEnable_]]"
share-default="[[shareDefault_]]"
error="{{error_}}"
- on-close="close">
+ on-close="onClose_">
</network-config>
</div>
@@ -40,17 +40,17 @@
<template is="dom-if" if="[[error_]]" restamp>
<div class="flex error">[[getError_(error_)]]</div>
</template>
- <paper-button class="cancel-button" on-tap="onCancelTap_">
+ <paper-button class="cancel-button" on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
- <template is="dom-if" if="[[isConfigured_(networkProperties_, guid)]]">
- <paper-button class="action-button" on-tap="onSaveOrConnectTap_"
+ <template is="dom-if" if="[[!showConnect]]">
+ <paper-button class="action-button" on-click="onSaveTap_"
disabled="[[!enableSave_]]">
$i18n{save}
</paper-button>
</template>
- <template is="dom-if" if="[[!isConfigured_(networkProperties_, guid)]]">
- <paper-button class="action-button" on-tap="onSaveOrConnectTap_"
+ <template is="dom-if" if="[[showConnect]]">
+ <paper-button class="action-button" on-click="onConnectTap_"
disabled="[[!enableConnect_]]">
$i18n{networkButtonConnect}
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/internet_page/internet_config.js b/chromium/chrome/browser/resources/settings/internet_page/internet_config.js
index b6bda81171f..ba7c90de4ab 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/internet_config.js
+++ b/chromium/chrome/browser/resources/settings/internet_page/internet_config.js
@@ -56,6 +56,12 @@ Polymer({
*/
name: String,
+ /**
+ * Set to true to show the 'connect' button instead of 'save'.
+ * @private
+ */
+ showConnect: Boolean,
+
/** @private */
enableConnect_: Boolean,
@@ -103,6 +109,16 @@ Polymer({
},
/**
+ * @param {!Event} event
+ * @private
+ */
+ onClose_: function(event) {
+ this.close();
+ this.fire('networks-changed');
+ event.stopPropagation();
+ },
+
+ /**
* @return {string}
* @private
*/
@@ -124,22 +140,18 @@ Polymer({
return this.i18n('networkErrorUnknown');
},
- /**
- * @return {boolean}
- * @private
- */
- isConfigured_: function() {
- const source = this.networkProperties_.Source;
- return !!this.guid && !!source && source != CrOnc.Source.NONE;
- },
-
/** @private */
onCancelTap_: function() {
this.close();
},
/** @private */
- onSaveOrConnectTap_: function() {
- this.$.networkConfig.saveOrConnect();
+ onSaveTap_: function() {
+ this.$.networkConfig.save();
+ },
+
+ /** @private */
+ onConnectTap_: function() {
+ this.$.networkConfig.connect();
},
});
diff --git a/chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.html b/chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.html
index e3afc520289..cbee01f9b6e 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.html
+++ b/chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.html
@@ -81,30 +81,30 @@
</template>
</div>
<template is="dom-if" if="[[!isSecondaryUser_]]">
- <paper-button on-tap="onForgetTap_"
+ <paper-button on-click="onForgetTap_"
hidden$="[[!showForget_(networkProperties)]]">
$i18n{networkButtonForget}
</paper-button>
- <paper-button on-tap="onViewAccountTap_"
+ <paper-button on-click="onViewAccountTap_"
hidden$="[[!showViewAccount_(networkProperties)]]">
$i18n{networkButtonViewAccount}
</paper-button>
- <paper-button on-tap="onActivateTap_"
+ <paper-button on-click="onActivateTap_"
hidden$="[[!showActivate_(networkProperties)]]">
$i18n{networkButtonActivate}
</paper-button>
- <paper-button on-tap="onConfigureTap_"
+ <paper-button on-click="onConfigureTap_"
hidden$="[[!showConfigure_(networkProperties, globalPolicy)]]">
$i18n{networkButtonConfigure}
</paper-button>
</template>
- <paper-button class="primary-button" on-tap="onConnectTap_"
+ <paper-button class="primary-button" on-click="onConnectTap_"
hidden$="[[!showConnect_(networkProperties, globalPolicy)]]"
disabled="[[!enableConnect_(networkProperties, defaultNetwork,
globalPolicy, networkPropertiesReceived_, outOfRange_)]]">
$i18n{networkButtonConnect}
</paper-button>
- <paper-button class="primary-button" on-tap="onDisconnectTap_"
+ <paper-button class="primary-button" on-click="onDisconnectTap_"
hidden$="[[!showDisconnect_(networkProperties)]]">
$i18n{networkButtonDisconnect}
</paper-button>
@@ -203,7 +203,7 @@
<template is="dom-if" if="[[showAdvanced_(networkProperties)]]">
<!-- Advanced toggle. -->
- <div class="settings-box" actionable on-tap="toggleAdvancedExpanded_">
+ <div class="settings-box" actionable on-click="toggleAdvancedExpanded_">
<div class="flex">$i18n{networkSectionAdvanced}</div>
<cr-expand-button expanded="{{advancedExpanded_}}"
alt="$i18n{networkSectionAdvancedA11yLabel}">
@@ -232,7 +232,7 @@
<template is="dom-if" if="[[hasNetworkSection_(networkProperties)]]">
<!-- Network toggle -->
- <div class="settings-box" actionable on-tap="toggleNetworkExpanded_">
+ <div class="settings-box" actionable on-click="toggleNetworkExpanded_">
<div class="start">$i18n{networkSectionNetwork}</div>
<cr-expand-button expanded="{{networkExpanded_}}"
alt="$i18n{networkSectionNetworkExpandA11yLabel}">
@@ -274,7 +274,7 @@
<template is="dom-if" if="[[hasProxySection_(networkProperties)]]">
<!-- Proxy toggle -->
- <div class="settings-box" actionable on-tap="toggleProxyExpanded_">
+ <div class="settings-box" actionable on-click="toggleProxyExpanded_">
<div class="start">$i18n{networkSectionProxy}</div>
<cr-expand-button expanded="{{proxyExpanded_}}"
alt="$i18n{networkSectionProxyExpandA11yLabel}">
diff --git a/chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.js b/chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.js
index b8c3480ddcd..c21b36319f5 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.js
+++ b/chromium/chrome/browser/resources/settings/internet_page/internet_detail_page.js
@@ -630,7 +630,6 @@ Polymer({
this.showTetherDialog_();
return;
}
-
this.fire('network-connect', {networkProperties: this.networkProperties});
},
diff --git a/chromium/chrome/browser/resources/settings/internet_page/internet_known_networks_page.html b/chromium/chrome/browser/resources/settings/internet_page/internet_known_networks_page.html
index 6d2f1357614..4466b23e664 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/internet_known_networks_page.html
+++ b/chromium/chrome/browser/resources/settings/internet_page/internet_known_networks_page.html
@@ -36,12 +36,12 @@
</cr-policy-indicator>
</template>
<button class="subpage-arrow" is="paper-icon-button-light"
- actionable on-tap="fireShowDetails_" tabindex$="[[tabindex]]"
+ actionable on-click="fireShowDetails_" tabindex$="[[tabindex]]"
aria-label$="[[item.Name]]">
</button>
<div class="separator"></div>
<button is="paper-icon-button-light" class="icon-more-vert"
- preferred tabindex$="[[tabindex]]" on-tap="onMenuButtonTap_"
+ preferred tabindex$="[[tabindex]]" on-click="onMenuButtonTap_"
title="$i18n{moreActions}">
</button>
</div>
@@ -63,12 +63,12 @@
</cr-policy-indicator>
</template>
<button class="subpage-arrow" is="paper-icon-button-light"
- actionable on-tap="fireShowDetails_" tabindex$="[[tabindex]]"
+ actionable on-click="fireShowDetails_" tabindex$="[[tabindex]]"
aria-label$="[[item.Name]]">
</button>
<div class="separator"></div>
<button is="paper-icon-button-light" class="icon-more-vert"
- tabindex$="[[tabindex]]" on-tap="onMenuButtonTap_"
+ tabindex$="[[tabindex]]" on-click="onMenuButtonTap_"
title="$i18n{moreActions}">
</button>
</div>
@@ -76,16 +76,16 @@
</div>
<dialog id="dotsMenu" is="cr-action-menu">
- <button class="dropdown-item" hidden="[[!showAddPreferred_]]"
- on-tap="onAddPreferredTap_">
+ <button slot="item" class="dropdown-item" hidden="[[!showAddPreferred_]]"
+ on-click="onAddPreferredTap_">
$i18n{knownNetworksMenuAddPreferred}
</button>
- <button class="dropdown-item" hidden="[[!showRemovePreferred_]]"
- on-tap="onRemovePreferredTap_">
+ <button slot="item" class="dropdown-item"
+ hidden="[[!showRemovePreferred_]]" on-click="onRemovePreferredTap_">
$i18n{knownNetworksMenuRemovePreferred}
</button>
- <button class="dropdown-item" disabled="[[!enableForget_]]"
- on-tap="onForgetTap_">
+ <button slot="item" class="dropdown-item" disabled="[[!enableForget_]]"
+ on-click="onForgetTap_">
$i18n{knownNetworksMenuForget}
</button>
</dialog>
diff --git a/chromium/chrome/browser/resources/settings/internet_page/internet_page.html b/chromium/chrome/browser/resources/settings/internet_page/internet_page.html
index cd6b2afbadb..0c7cf9c5c72 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/internet_page.html
+++ b/chromium/chrome/browser/resources/settings/internet_page/internet_page.html
@@ -38,7 +38,7 @@
</network-summary>
<template is="dom-if" if="[[allowAddConnection_(globalPolicy_)]]">
<div actionable class="settings-box two-line"
- on-tap="onExpandAddConnectionsTap_">
+ on-click="onExpandAddConnectionsTap_">
<div class="start layout horizontal center">
<div>$i18n{internetAddConnection}</div>
</div>
@@ -50,7 +50,7 @@
<div class="list-frame vertical-list">
<template is="dom-if"
if="[[deviceIsEnabled_(deviceStates.WiFi)]]">
- <div actionable class="list-item" on-tap="onAddWiFiTap_">
+ <div actionable class="list-item" on-click="onAddWiFiTap_">
<div class="start">$i18n{internetAddWiFi}</div>
<button class$="[[getAddNetworkClass_('WiFi')]]"
is="paper-icon-button-light"
@@ -58,7 +58,7 @@
</button>
</div>
</template>
- <div actionable class="list-item" on-tap="onAddVPNTap_">
+ <div actionable class="list-item" on-click="onAddVPNTap_">
<div class="start">$i18n{internetAddVPN}</div>
<button class$="[[getAddNetworkClass_('VPN')]]"
is="paper-icon-button-light"
@@ -67,7 +67,7 @@
</div>
<template is="dom-repeat" items="[[thirdPartyVpnProviders_]]">
<div actionable class="list-item"
- on-tap="onAddThirdPartyVpnTap_" provider="[[item]]">
+ on-click="onAddThirdPartyVpnTap_" provider="[[item]]">
<div class="start">[[getAddThirdPartyVpnLabel_(item)]]</div>
<button class="icon-external" is="paper-icon-button-light"
aria-label$="[[getAddThirdPartyVpnLabel_(item)]]">
@@ -76,7 +76,7 @@
</template>
<template is="dom-if" if="[[arcVpnProviders_.length]]">
<div actionable class="list-item" id="addArcVpn"
- on-tap="onAddArcVpnTap_">
+ on-click="onAddArcVpnTap_">
<div class="start">$i18n{internetAddArcVPN}</div>
<button class="icon-external" is="paper-icon-button-light"
aria-label$="$i18n{internetAddArcVPN}">
diff --git a/chromium/chrome/browser/resources/settings/internet_page/internet_page.js b/chromium/chrome/browser/resources/settings/internet_page/internet_page.js
index 3b8126607c7..ed1eda816ef 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/internet_page.js
+++ b/chromium/chrome/browser/resources/settings/internet_page/internet_page.js
@@ -272,27 +272,27 @@ Polymer({
*/
onShowConfig_: function(event) {
const properties = event.detail;
+ let configAndConnect = !properties.GUID; // New configuration
this.showConfig_(
- properties.Type, properties.GUID, CrOnc.getNetworkName(properties));
+ configAndConnect, properties.Type, properties.GUID,
+ CrOnc.getNetworkName(properties));
},
/**
+ * @param {boolean} configAndConnect
* @param {string} type
* @param {string=} guid
* @param {string=} name
* @private
*/
- showConfig_: function(type, guid, name) {
- if (!loadTimeData.getBoolean('networkSettingsConfig')) {
- chrome.send('configureNetwork', [guid]);
- return;
- }
+ showConfig_: function(configAndConnect, type, guid, name) {
const configDialog =
/** @type {!InternetConfigElement} */ (this.$.configDialog);
configDialog.type =
/** @type {chrome.networkingPrivate.NetworkType} */ (type);
configDialog.guid = guid || '';
configDialog.name = name || '';
+ configDialog.showConnect = configAndConnect;
configDialog.open();
},
@@ -369,7 +369,7 @@ Polymer({
},
/**
- * Event triggered when the 'Add connections' div is tapped.
+ * Event triggered when the 'Add connections' div is clicked.
* @param {!Event} event
* @private
*/
@@ -382,7 +382,7 @@ Polymer({
/** @private */
onAddWiFiTap_: function() {
if (loadTimeData.getBoolean('networkSettingsConfig'))
- this.showConfig_(CrOnc.Type.WI_FI);
+ this.showConfig_(true /* configAndConnect */, CrOnc.Type.WI_FI);
else
chrome.send('addNetwork', [CrOnc.Type.WI_FI]);
},
@@ -390,7 +390,7 @@ Polymer({
/** @private */
onAddVPNTap_: function() {
if (loadTimeData.getBoolean('networkSettingsConfig'))
- this.showConfig_(CrOnc.Type.VPN);
+ this.showConfig_(true /* configAndConnect */, CrOnc.Type.VPN);
else
chrome.send('addNetwork', [CrOnc.Type.VPN]);
},
@@ -604,7 +604,8 @@ Polymer({
}
if (properties.Connectable === false || properties.ErrorState) {
- this.showConfig_(properties.Type, properties.GUID, name);
+ this.showConfig_(
+ true /* configAndConnect */, properties.Type, properties.GUID, name);
return;
}
@@ -618,7 +619,9 @@ Polymer({
console.error(
'networkingPrivate.startConnect error: ' + message +
' For: ' + properties.GUID);
- this.showConfig_(properties.Type, properties.GUID, name);
+ this.showConfig_(
+ true /* configAndConnect */, properties.Type, properties.GUID,
+ name);
}
});
},
diff --git a/chromium/chrome/browser/resources/settings/internet_page/internet_subpage.html b/chromium/chrome/browser/resources/settings/internet_page/internet_subpage.html
index 3119ac4ec1c..ad9e3d48712 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/internet_subpage.html
+++ b/chromium/chrome/browser/resources/settings/internet_page/internet_subpage.html
@@ -65,12 +65,12 @@
}
#gmscore-notifications-device-string {
- @apply(--cr-secondary-text);
+ @apply --cr-secondary-text;
margin-top: 5px;
}
#gmscore-notifications-instructions {
- @apply(--cr-secondary-text);
+ @apply --cr-secondary-text;
-webkit-padding-start: 15px;
margin: 0;
}
@@ -86,19 +86,19 @@
<button is="paper-icon-button-light" id="addButton"
hidden$="[[!showAddButton_(deviceState, globalPolicy)]]"
aria-label="$i18n{internetAddWiFi}" class="icon-add-wifi"
- on-tap="onAddButtonTap_" tabindex$="[[tabindex]]">
+ on-click="onAddButtonTap_" tabindex$="[[tabindex]]">
</button>
<paper-toggle-button id="deviceEnabledButton"
aria-label$="[[getToggleA11yString_(deviceState)]]"
checked="[[deviceIsEnabled_(deviceState)]]"
disabled="[[!enableToggleIsEnabled_(deviceState)]]"
- on-tap="onDeviceEnabledTap_">
+ on-click="onDeviceEnabledTap_">
</paper-toggle-button>
</div>
</template>
<template is="dom-if" if="[[knownNetworksIsVisible_(deviceState)]]">
- <div actionable class="settings-box" on-tap="onKnownNetworksTap_">
+ <div actionable class="settings-box" on-click="onKnownNetworksTap_">
<div class="start">$i18n{knownNetworksButton}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{knownNetworksButton}">
@@ -114,7 +114,7 @@
<div class="flex">$i18n{networkVpnBuiltin}</div>
<button is="paper-icon-button-light" class="icon-add-circle"
aria-label="$i18n{internetAddVPN}"
- on-tap="onAddButtonTap_" tabindex$="[[tabindex]]">
+ on-click="onAddButtonTap_" tabindex$="[[tabindex]]">
</button>
</div>
</template>
@@ -143,6 +143,7 @@
<li>$i18n{gmscoreNotificationsFirstStep}</li>
<li>$i18n{gmscoreNotificationsSecondStep}</li>
<li>$i18n{gmscoreNotificationsThirdStep}</li>
+ <li>$i18n{gmscoreNotificationsFourthStep}</li>
</ol>
</div>
</template>
@@ -165,7 +166,7 @@
<div class="flex">[[item.ProviderName]]</div>
<button is="paper-icon-button-light" class="icon-add-circle"
aria-label$="[[getAddThirdPartyVpnA11yString_(item)]]"
- on-tap="onAddThirdPartyVpnTap_" tabindex$="[[tabindex]]">
+ on-click="onAddThirdPartyVpnTap_" tabindex$="[[tabindex]]">
</button>
</div>
<cr-network-list show-buttons
@@ -185,7 +186,7 @@
<div class="flex">[[item.ProviderName]]</div>
<button is="paper-icon-button-light" class="icon-add-circle"
aria-label$="[[getAddArcVpnAllyString_(item)]]"
- on-tap="onAddArcVpnTap_" tabindex$="[[tabindex]]">
+ on-click="onAddArcVpnTap_" tabindex$="[[tabindex]]">
</button>
</div>
<cr-network-list show-buttons
@@ -204,7 +205,7 @@
<template is="dom-if"
if="[[tetherToggleIsVisible_(deviceState, tetherDeviceState)]]">
<div class="settings-box two-line" actionable
- on-tap="onTetherEnabledTap_">
+ on-click="onTetherEnabledTap_">
<div class="start">
$i18n{internetToggleTetherLabel}
<div id="tetherSecondary" class="secondary">
diff --git a/chromium/chrome/browser/resources/settings/internet_page/internet_subpage.js b/chromium/chrome/browser/resources/settings/internet_page/internet_subpage.js
index 2d1c08ee297..c45566055aa 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/internet_subpage.js
+++ b/chromium/chrome/browser/resources/settings/internet_page/internet_subpage.js
@@ -442,7 +442,7 @@ Polymer({
},
/**
- * Event triggered when the known networks button is tapped.
+ * Event triggered when the known networks button is clicked.
* @private
*/
onKnownNetworksTap_: function() {
diff --git a/chromium/chrome/browser/resources/settings/internet_page/network_proxy_section.html b/chromium/chrome/browser/resources/settings/internet_page/network_proxy_section.html
index f5d88e89c41..7aa2e646067 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/network_proxy_section.html
+++ b/chromium/chrome/browser/resources/settings/internet_page/network_proxy_section.html
@@ -90,11 +90,11 @@
</div>
<div slot="button-container">
<paper-button class="cancel-button"
- on-tap="onAllowSharedDialogCancel_">
+ on-click="onAllowSharedDialogCancel_">
$i18n{cancel}
</paper-button>
<paper-button class="action-button"
- on-tap="onAllowSharedDialogConfirm_">
+ on-click="onAllowSharedDialogConfirm_">
$i18n{confirm}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/internet_page/network_summary_item.html b/chromium/chrome/browser/resources/settings/internet_page/network_summary_item.html
index 812bf51fff9..2df92961938 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/network_summary_item.html
+++ b/chromium/chrome/browser/resources/settings/internet_page/network_summary_item.html
@@ -34,7 +34,7 @@
font-weight: 400;
}
</style>
- <div actionable class="settings-box two-line" on-tap="onShowDetailsTap_">
+ <div actionable class="settings-box two-line" on-click="onShowDetailsTap_">
<div id="details" no-flex$="[[showSimInfo_(deviceState)]]">
<cr-network-icon network-state="[[activeNetworkState]]"
device-state="[[deviceState]]">
@@ -48,7 +48,7 @@
</div>
<template is="dom-if" if="[[showSimInfo_(deviceState)]]" restamp>
- <network-siminfo editable on-tap="doNothing_"
+ <network-siminfo editable on-click="doNothing_"
network-properties="[[getCellularState_(deviceState)]]"
networking-private="[[networkingPrivate]]">
</network-siminfo>
@@ -57,7 +57,7 @@
<template is="dom-if" if="[[showPolicyIndicator_(activeNetworkState)]]">
<cr-policy-indicator indicator-type="[[getIndicatorTypeForSource(
activeNetworkState.Source)]]"
- on-tap="doNothing_">
+ on-click="doNothing_">
</cr-policy-indicator>
</template>
@@ -75,7 +75,7 @@
aria-label$="[[getToggleA11yString_(deviceState)]]"
checked="[[deviceIsEnabled_(deviceState)]]"
disabled="[[!enableToggleIsEnabled_(deviceState)]]"
- on-tap="onDeviceEnabledTap_">
+ on-click="onDeviceEnabledTap_">
</paper-toggle-button>
</template>
</div>
diff --git a/chromium/chrome/browser/resources/settings/internet_page/network_summary_item.js b/chromium/chrome/browser/resources/settings/internet_page/network_summary_item.js
index 65c882d595d..e7a54cb245a 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/network_summary_item.js
+++ b/chromium/chrome/browser/resources/settings/internet_page/network_summary_item.js
@@ -68,10 +68,9 @@ Polymer({
* @private
*/
getNetworkStateText_: function(activeNetworkState, deviceState) {
- const state = activeNetworkState.ConnectionState;
- const name = CrOnc.getNetworkName(activeNetworkState);
- if (state)
- return this.getConnectionStateText_(state, name);
+ const stateText = this.getConnectionStateText_(activeNetworkState);
+ if (stateText)
+ return stateText;
// No network state, use device state.
if (deviceState) {
// Type specific scanning or initialization states.
@@ -98,12 +97,15 @@ Polymer({
},
/**
- * @param {CrOnc.ConnectionState} state
- * @param {string} name
+ * @param {!CrOnc.NetworkStateProperties} networkState
* @return {string}
* @private
*/
- getConnectionStateText_: function(state, name) {
+ getConnectionStateText_: function(networkState) {
+ const state = networkState.ConnectionState;
+ if (!state)
+ return '';
+ const name = CrOnc.getNetworkName(networkState);
switch (state) {
case CrOnc.ConnectionState.CONNECTED:
return name;
@@ -112,6 +114,10 @@ Polymer({
return CrOncStrings.networkListItemConnectingTo.replace('$1', name);
return CrOncStrings.networkListItemConnecting;
case CrOnc.ConnectionState.NOT_CONNECTED:
+ if (networkState.Type == CrOnc.Type.CELLULAR && networkState.Cellular &&
+ networkState.Cellular.Scanning) {
+ return this.i18n('internetMobileSearching');
+ }
return CrOncStrings.networkListItemNotConnected;
}
assertNotReached();
diff --git a/chromium/chrome/browser/resources/settings/internet_page/tether_connection_dialog.html b/chromium/chrome/browser/resources/settings/internet_page/tether_connection_dialog.html
index 51e02e88fb0..c54c5c1186a 100644
--- a/chromium/chrome/browser/resources/settings/internet_page/tether_connection_dialog.html
+++ b/chromium/chrome/browser/resources/settings/internet_page/tether_connection_dialog.html
@@ -108,11 +108,11 @@
</ul>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onNotNowTap_">
+ <paper-button class="cancel-button" on-click="onNotNowTap_">
$i18n{tetherConnectionNotNowButton}
</paper-button>
<paper-button id="connectButton" class="action-button"
- on-tap="onConnectTap_" disabled="[[outOfRange]]">
+ on-click="onConnectTap_" disabled="[[outOfRange]]">
$i18n{tetherConnectionConnectButton}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/languages_page/add_languages_dialog.html b/chromium/chrome/browser/resources/settings/languages_page/add_languages_dialog.html
index 6a6a1512872..a3f6edd6f54 100644
--- a/chromium/chrome/browser/resources/settings/languages_page/add_languages_dialog.html
+++ b/chromium/chrome/browser/resources/settings/languages_page/add_languages_dialog.html
@@ -59,10 +59,10 @@
</iron-list>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelButtonTap_">
+ <paper-button class="cancel-button" on-click="onCancelButtonTap_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onActionButtonTap_"
+ <paper-button class="action-button" on-click="onActionButtonTap_"
disabled="[[disableActionButton_]]">
$i18n{add}
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/languages_page/edit_dictionary_page.html b/chromium/chrome/browser/resources/settings/languages_page/edit_dictionary_page.html
index d5d0912c66d..ce5c803d292 100644
--- a/chromium/chrome/browser/resources/settings/languages_page/edit_dictionary_page.html
+++ b/chromium/chrome/browser/resources/settings/languages_page/edit_dictionary_page.html
@@ -42,7 +42,7 @@
'$i18nPolymer{addDictionaryWordDuplicateError}',
'$i18nPolymer{addDictionaryWordLengthError}')]]"></paper-input>
</div>
- <paper-button class="secondary-button" on-tap="onAddWordTap_"
+ <paper-button class="secondary-button" on-click="onAddWordTap_"
disabled="[[disableAddButton_(newWordValue_)]]" id="addWord">
$i18n{addDictionaryWordButton}
</paper-button>
@@ -58,7 +58,7 @@
<div class="list-item">
<div class="word text-elide">[[item]]</div>
<button is="paper-icon-button-light" class="icon-clear"
- on-tap="onRemoveWordTap_" tabindex$="[[tabIndex]]">
+ on-click="onRemoveWordTap_" tabindex$="[[tabIndex]]">
</button>
</div>
</template>
diff --git a/chromium/chrome/browser/resources/settings/languages_page/languages_page.html b/chromium/chrome/browser/resources/settings/languages_page/languages_page.html
index 608022db7bb..7b70201c2cb 100644
--- a/chromium/chrome/browser/resources/settings/languages_page/languages_page.html
+++ b/chromium/chrome/browser/resources/settings/languages_page/languages_page.html
@@ -84,7 +84,7 @@
focus-config="[[focusConfig_]]">
<neon-animatable route-path="default">
<div class$="settings-box first [[getLanguageListTwoLine_()]]"
- actionable on-tap="toggleExpandButton_">
+ actionable on-click="toggleExpandButton_">
<div class="start">
<div>$i18n{languagesListTitle}</div>
<if expr="chromeos or is_win">
@@ -127,20 +127,20 @@
<if expr="chromeos or is_win">
<template is="dom-if" if="[[isRestartRequired_(
item.language.code, languages.prospectiveUILanguage)]]">
- <paper-button on-tap="onRestartTap_">
+ <paper-button on-click="onRestartTap_">
$i18n{restart}
</paper-button>
</template>
</if>
<button is="paper-icon-button-light" title="$i18n{moreActions}"
- id="more-[[item.language.code]]" on-tap="onDotsTap_"
+ id="more-[[item.language.code]]" on-click="onDotsTap_"
class="icon-more-vert">
</button>
</div>
</template>
<div class="list-item">
<a is="action-link" class="list-button" id="addLanguages"
- on-tap="onAddLanguagesTap_">
+ on-click="onAddLanguagesTap_">
$i18n{addLanguages}
</a>
</div>
@@ -153,7 +153,7 @@
<if expr="chromeos">
<div id="manageInputMethodsSubpageTrigger"
class="settings-box two-line" actionable
- on-tap="toggleExpandButton_">
+ on-click="toggleExpandButton_">
<div class="start">
<div>$i18n{inputMethodsListTitle}</div>
<div class="secondary">
@@ -171,7 +171,7 @@
items="[[languages.inputMethods.enabled]]">
<div class$="list-item [[getInputMethodItemClass_(
item.id, languages.inputMethods.currentId)]]"
- on-tap="onInputMethodTap_" on-keypress="onInputMethodTap_"
+ on-click="onInputMethodTap_" on-keypress="onInputMethodTap_"
actionable tabindex="0">
<div class="start">
<div>[[item.displayName]]</div>
@@ -182,12 +182,13 @@
</div>
</div>
<button class="icon-external" is="paper-icon-button-light"
- on-tap="onInputMethodOptionsTap_"
+ on-click="onInputMethodOptionsTap_"
hidden="[[!item.hasOptionsPage]]">
</button>
</div>
</template>
- <div class="list-item" on-tap="onManageInputMethodsTap_" actionable>
+ <div class="list-item" on-click="onManageInputMethodsTap_"
+ actionable>
<div class="start" id="manageInputMethods">
$i18n{manageInputMethods}
</div>
@@ -207,7 +208,7 @@
class$="settings-box [[getSpellCheckListTwoLine_(
spellCheckSecondaryText_)]]"
actionable$="[[!spellCheckDisabled_]]"
- on-tap="toggleExpandButton_">
+ on-click="toggleExpandButton_">
<div class="start">
<div>$i18n{spellCheckListTitle}</div>
<div class="secondary">[[spellCheckSecondaryText_]]</div>
@@ -230,7 +231,7 @@
<template is="dom-repeat" items="[[spellCheckLanguages_]]">
<div class="list-item">
<template is="dom-if" if="[[!item.isManaged]]">
- <div class="start" on-tap="onSpellCheckChange_"
+ <div class="start" on-click="onSpellCheckChange_"
actionable$="[[item.language.supportsSpellcheck]]">
[[item.language.displayName]]
</div>
@@ -250,7 +251,7 @@
</template>
</div>
</template>
- <div class="list-item" on-tap="onEditDictionaryTap_" actionable>
+ <div class="list-item" on-click="onEditDictionaryTap_" actionable>
<div class="start" id="customSpelling">
$i18n{manageSpellCheck}
</div>
@@ -265,7 +266,8 @@
<dialog is="cr-action-menu"
class$="[[getMenuClass_(prefs.translate.enabled.value)]]">
<if expr="chromeos or is_win">
- <paper-checkbox id="uiLanguageItem" class="dropdown-item"
+ <paper-checkbox id="uiLanguageItem" slot="item"
+ class="dropdown-item"
checked="[[isProspectiveUILanguage_(
detailLanguage_.language.code,
languages.prospectiveUILanguage)]]"
@@ -275,7 +277,8 @@
$i18n{displayInThisLanguage}
</paper-checkbox>
</if>
- <paper-checkbox id="offerTranslations" class="dropdown-item"
+ <paper-checkbox id="offerTranslations" slot="item"
+ class="dropdown-item"
checked="[[detailLanguage_.translateEnabled]]"
on-change="onTranslateCheckboxChange_"
hidden="[[!prefs.translate.enabled.value]]"
@@ -283,26 +286,26 @@
detailLanguage_.language, languages.translateTarget)]]">
$i18n{offerToTranslateInThisLanguage}
</paper-checkbox>
- <hr>
- <button class="dropdown-item" role="menuitem"
- on-tap="onMoveToTopTap_"
+ <hr slot="item">
+ <button slot="item" class="dropdown-item" role="menuitem"
+ on-click="onMoveToTopTap_"
hidden="[[isNthLanguage_(
0, detailLanguage_, languages.enabled.*)]]">
$i18n{moveToTop}
</button>
- <button class="dropdown-item" role="menuitem"
- on-tap="onMoveUpTap_"
+ <button slot="item" class="dropdown-item" role="menuitem"
+ on-click="onMoveUpTap_"
hidden="[[!showMoveUp_(detailLanguage_, languages.enabled.*)]]">
$i18n{moveUp}
</button>
- <button class="dropdown-item" role="menuitem"
- on-tap="onMoveDownTap_"
+ <button slot="item" class="dropdown-item" role="menuitem"
+ on-click="onMoveDownTap_"
hidden="[[!showMoveDown_(
detailLanguage_, languages.enabled.*)]]">
$i18n{moveDown}
</button>
- <button class="dropdown-item" role="menuitem"
- on-tap="onRemoveLanguageTap_"
+ <button slot="item" class="dropdown-item" role="menuitem"
+ on-click="onRemoveLanguageTap_"
hidden="[[!detailLanguage_.removable]]">
$i18n{removeLanguage}
</button>
diff --git a/chromium/chrome/browser/resources/settings/languages_page/languages_page.js b/chromium/chrome/browser/resources/settings/languages_page/languages_page.js
index 43787f93abb..d9df072708b 100644
--- a/chromium/chrome/browser/resources/settings/languages_page/languages_page.js
+++ b/chromium/chrome/browser/resources/settings/languages_page/languages_page.js
@@ -116,11 +116,13 @@ Polymer({
},
},
+ // <if expr="not is_macosx">
observers: [
'updateSpellcheckLanguages_(languages.enabled.*, ' +
'languages.forcedSpellCheckLanguages.*)',
'updateSpellcheckEnabled_(prefs.browser.enable_spellchecking.*)',
],
+ // </if>
/**
* Stamps and opens the Add Languages dialog, registering a listener to
@@ -628,7 +630,7 @@ Polymer({
/**
* Closes the shared action menu after a short delay, so when a checkbox is
- * tapped it can be seen to change state before disappearing.
+ * clicked it can be seen to change state before disappearing.
* @private
*/
closeMenuSoon_: function() {
diff --git a/chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.html b/chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.html
index 40bfadfde9d..f4bdec1ade6 100644
--- a/chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.html
+++ b/chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.html
@@ -23,13 +23,11 @@
label="$i18n{onStartupOpenNewTab}"
no-extension-indicator>
</controlled-radio-button>
- <template is="dom-if" if="[[showIndicator_(
- ntpExtension_, prefs.session.restore_on_startup.value)]]">
+ <template is="dom-if" if="[[ntpExtension_]]">
<extension-controlled-indicator
extension-id="[[ntpExtension_.id]]"
extension-name="[[ntpExtension_.name]]"
- extension-can-be-disabled="[[ntpExtension_.canBeDisabled]]"
- on-extension-disable="getNtpExtension_">
+ extension-can-be-disabled="[[ntpExtension_.canBeDisabled]]">
</extension-controlled-indicator>
</template>
<controlled-radio-button name="[[prefValues_.CONTINUE]]"
diff --git a/chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.js b/chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.js
index 26484951dcf..aff7303d079 100644
--- a/chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.js
+++ b/chromium/chrome/browser/resources/settings/on_startup_page/on_startup_page.js
@@ -37,29 +37,13 @@ Polymer({
/** @override */
attached: function() {
- this.getNtpExtension_();
- this.addWebUIListener('update-ntp-extension', ntpExtension => {
+ const updateNtpExtension = ntpExtension => {
// Note that |ntpExtension| is empty if there is no NTP extension.
this.ntpExtension_ = ntpExtension;
- });
- },
-
- /** @private */
- getNtpExtension_: function() {
+ };
settings.OnStartupBrowserProxyImpl.getInstance().getNtpExtension().then(
- function(ntpExtension) {
- this.ntpExtension_ = ntpExtension;
- }.bind(this));
- },
-
- /**
- * @param {?NtpExtension} ntpExtension
- * @param {number} restoreOnStartup Value of prefs.session.restore_on_startup.
- * @return {boolean}
- * @private
- */
- showIndicator_: function(ntpExtension, restoreOnStartup) {
- return !!ntpExtension && restoreOnStartup == this.prefValues_.OPEN_NEW_TAB;
+ updateNtpExtension);
+ this.addWebUIListener('update-ntp-extension', updateNtpExtension);
},
/**
diff --git a/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_dialog.html b/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_dialog.html
index d2139335785..313bdf7aba0 100644
--- a/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_dialog.html
+++ b/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_dialog.html
@@ -21,10 +21,10 @@
</paper-input>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelTap_"
+ <paper-button class="cancel-button" on-click="onCancelTap_"
id="cancel">$i18n{cancel}</paper-button>
<paper-button id="actionButton" class="action-button"
- on-tap="onActionButtonTap_">[[actionButtonText_]]</paper-button>
+ on-click="onActionButtonTap_">[[actionButtonText_]]</paper-button>
</div>
</dialog>
</template>
diff --git a/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.html b/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.html
index cf1a7a9cf81..99699c9a021 100644
--- a/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.html
+++ b/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.html
@@ -25,16 +25,17 @@
<div class="text-elide secondary">[[model.url]]</div>
</div>
<template is="dom-if" if="[[editable]]">
- <button is="paper-icon-button-light" id="dots" on-tap="onDotsTap_"
+ <button is="paper-icon-button-light" id="dots" on-click="onDotsTap_"
title="$i18n{moreActions}" focus-row-control focus-type="menu"
class="icon-more-vert">
</button>
<template is="cr-lazy-render" id="menu">
<dialog is="cr-action-menu">
- <button class="dropdown-item" on-tap="onEditTap_">
+ <button slot="item" class="dropdown-item" on-click="onEditTap_">
$i18n{edit}
</button>
- <button class="dropdown-item" id="remove" on-tap="onRemoveTap_">
+ <button slot="item" class="dropdown-item" id="remove"
+ on-click="onRemoveTap_">
$i18n{onStartupRemove}
</button>
</dialog>
diff --git a/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.js b/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.js
index 8c4460dc865..dd6ac50a045 100644
--- a/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.js
+++ b/chromium/chrome/browser/resources/settings/on_startup_page/startup_url_entry.js
@@ -12,7 +12,7 @@ cr.exportPath('settings');
/**
* The name of the event fired from this element when the "Edit" option is
- * tapped.
+ * clicked.
* @type {string}
*/
settings.EDIT_STARTUP_URL_EVENT = 'edit-startup-url';
diff --git a/chromium/chrome/browser/resources/settings/on_startup_page/startup_urls_page.html b/chromium/chrome/browser/resources/settings/on_startup_page/startup_urls_page.html
index 02b87b2093a..93cd7cd0c94 100644
--- a/chromium/chrome/browser/resources/settings/on_startup_page/startup_urls_page.html
+++ b/chromium/chrome/browser/resources/settings/on_startup_page/startup_urls_page.html
@@ -18,7 +18,7 @@
<template>
<style include="settings-shared action-link iron-flex">
.list-frame {
- @apply(--settings-list-frame-padding);
+ @apply --settings-list-frame-padding;
}
.list-frame > div {
@@ -26,7 +26,7 @@
}
#outer {
- @apply(--settings-list-frame-padding);
+ @apply --settings-list-frame-padding;
max-height: 355px; /** Enough height to show six entries. */
}
@@ -53,13 +53,13 @@
<template is="dom-if" if="[[shouldAllowUrlsEdit_(
prefs.session.startup_urls.enforcement)]]" restamp>
<div class="list-item" id="addPage">
- <a is="action-link" class="list-button" on-tap="onAddPageTap_">
+ <a is="action-link" class="list-button" on-click="onAddPageTap_">
$i18n{onStartupAddNewPage}
</a>
</div>
<div class="list-item" id="useCurrentPages">
<a is="action-link" class="list-button"
- on-tap="onUseCurrentPagesTap_">
+ on-click="onUseCurrentPagesTap_">
$i18n{onStartupUseCurrent}
</a>
</div>
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/address_edit_dialog.html b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/address_edit_dialog.html
index 37c189b5231..8d6515df3a8 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/address_edit_dialog.html
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/address_edit_dialog.html
@@ -51,7 +51,7 @@
transform: scale(0.75);
transform-origin: left;
width: 133%;
- @apply(--paper-input-container-label-floating);
+ @apply --paper-input-container-label-floating;
}
:host-context([dir=rtl]) #select-label {
@@ -144,11 +144,11 @@
</div>
<div slot="button-container">
<paper-button id="cancelButton" class="cancel-button"
- on-tap="onCancelTap_">
+ on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
<paper-button id="saveButton" class="action-button"
- disabled="[[!canSave_]]" on-tap="onSaveButtonTap_">
+ disabled="[[!canSave_]]" on-click="onSaveButtonTap_">
$i18n{save}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/autofill_section.html b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/autofill_section.html
index 4da90c016dc..e5193b1a7d1 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/autofill_section.html
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/autofill_section.html
@@ -65,9 +65,9 @@
}
</style>
<settings-toggle-button id="autofillToggle"
- class="first primary-toggle"
+ class="first"
aria-label="$i18n{autofill}" no-extension-indicator
- label="[[getOnOffLabel_(prefs.autofill.enabled.value)]]"
+ label="$i18n{autofillFormsLabel}"
pref="{{prefs.autofill.enabled}}">
</settings-toggle-button>
<template is="dom-if" if="[[prefs.autofill.enabled.extensionId]]">
@@ -85,7 +85,7 @@
<h2 class="start">$i18n{addresses}</h2>
<paper-button id="addAddress"
class="secondary-button header-aligned-button"
- on-tap="onAddAddressTap_">
+ on-click="onAddAddressTap_">
$i18n{add}
</paper-button>
</div>
@@ -108,13 +108,13 @@
</div>
<template is="dom-if" if="[[item.metadata.isLocal]]">
<button is="paper-icon-button-light" id="addressMenu"
- class="icon-more-vert" on-tap="onAddressMenuTap_"
+ class="icon-more-vert" on-click="onAddressMenuTap_"
title="$i18n{moreActions}">
</button>
</template>
<template is="dom-if" if="[[!item.metadata.isLocal]]">
<button is="paper-icon-button-light" class="icon-external"
- on-tap="onRemoteEditAddressTap_" actionable></button>
+ on-click="onRemoteEditAddressTap_" actionable></button>
</template>
</div>
</template>
@@ -125,10 +125,10 @@
</div>
</div>
<dialog is="cr-action-menu" id="addressSharedMenu">
- <button id="menuEditAddress" class="dropdown-item"
- on-tap="onMenuEditAddressTap_">$i18n{edit}</button>
- <button id="menuRemoveAddress" class="dropdown-item"
- on-tap="onMenuRemoveAddressTap_">$i18n{removeAddress}</button>
+ <button id="menuEditAddress" slot="item" class="dropdown-item"
+ on-click="onMenuEditAddressTap_">$i18n{edit}</button>
+ <button id="menuRemoveAddress" slot="item" class="dropdown-item"
+ on-click="onMenuRemoveAddressTap_">$i18n{removeAddress}</button>
</dialog>
<template is="dom-if" if="[[showAddressDialog_]]" restamp>
<settings-address-edit-dialog address="[[activeAddress]]"
@@ -139,7 +139,7 @@
<h2 class="start">$i18n{creditCards}</h2>
<paper-button id="addCreditCard"
class="secondary-button header-aligned-button"
- on-tap="onAddCreditCardTap_"
+ on-click="onAddCreditCardTap_"
hidden$="[[isDisabled_(prefs.autofill.credit_card_enabled)]]">
$i18n{add}
</paper-button>
@@ -173,13 +173,13 @@
<template is="dom-if" if="[[showDots_(item.metadata)]]">
<button is="paper-icon-button-light" id="creditCardMenu"
class="icon-more-vert" title="$i18n{moreActions}"
- on-tap="onCreditCardMenuTap_">
+ on-click="onCreditCardMenuTap_">
</button>
</template>
<template is="dom-if" if="[[!showDots_(item.metadata)]]">
<button is="paper-icon-button-light" id="remoteCreditCardLink"
class="icon-external"
- on-tap="onRemoteEditCreditCardTap_" actionable></button>
+ on-click="onRemoteEditCreditCardTap_" actionable></button>
</template>
</div>
</div>
@@ -198,13 +198,13 @@
</div>
</div>
<dialog is="cr-action-menu" id="creditCardSharedMenu">
- <button id="menuEditCreditCard" class="dropdown-item"
- on-tap="onMenuEditCreditCardTap_">$i18n{edit}</button>
- <button id="menuRemoveCreditCard" class="dropdown-item"
+ <button id="menuEditCreditCard" slot="item" class="dropdown-item"
+ on-click="onMenuEditCreditCardTap_">$i18n{edit}</button>
+ <button id="menuRemoveCreditCard" slot="item" class="dropdown-item"
hidden$="[[!activeCreditCard.metadata.isLocal]]"
- on-tap="onMenuRemoveCreditCardTap_">$i18n{removeCreditCard}</button>
- <button id="menuClearCreditCard" class="dropdown-item"
- on-tap="onMenuClearCreditCardTap_"
+ on-click="onMenuRemoveCreditCardTap_">$i18n{removeCreditCard}</button>
+ <button id="menuClearCreditCard" slot="item" class="dropdown-item"
+ on-click="onMenuClearCreditCardTap_"
hidden$="[[!activeCreditCard.metadata.isCached]]">
$i18n{clearCreditCard}
</button>
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/credit_card_edit_dialog.html b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/credit_card_edit_dialog.html
index 041413545b9..6465c600227 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/credit_card_edit_dialog.html
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/credit_card_edit_dialog.html
@@ -89,9 +89,9 @@
</div>
<div slot="button-container">
<paper-button id="cancelButton" class="cancel-button"
- on-tap="onCancelButtonTap_">$i18n{cancel}</paper-button>
+ on-click="onCancelButtonTap_">$i18n{cancel}</paper-button>
<paper-button id="saveButton" class="action-button"
- on-tap="onSaveButtonTap_" disabled>$i18n{save}</paper-button>
+ on-click="onSaveButtonTap_" disabled>$i18n{save}</paper-button>
</div>
</dialog>
</template>
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_edit_dialog.html b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_edit_dialog.html
index 0a0814697a6..ae402835da2 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_edit_dialog.html
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_edit_dialog.html
@@ -22,7 +22,7 @@
};
--paper-input-container-label-focus: {
- color: var(--paper-input-container-color, --secondary-text-color);
+ color: var(--secondary-text-color);
};
}
@@ -61,15 +61,15 @@
<button is="paper-icon-button-light" id="showPasswordButton"
class$="[[getIconClass_(item.password)]]"
hidden$="[[item.entry.federationText]]"
- on-tap="onShowPasswordButtonTap_"
+ on-click="onShowPasswordButtonTap_"
title="[[showPasswordTitle_(item.password,
'$i18nPolymer{hidePassword}','$i18nPolymer{showPassword}')]]">
</button>
</div>
</div>
<div slot="button-container">
- <paper-button class="action-button" on-tap="onActionButtonTap_">
- $i18n{passwordsDone}
+ <paper-button class="action-button" on-click="onActionButtonTap_">
+ $i18n{done}
</paper-button>
</div>
</dialog>
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_list_item.html b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_list_item.html
index 85dda982896..59f3d8a1e3c 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_list_item.html
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/password_list_item.html
@@ -48,12 +48,12 @@
<template is="dom-if" if="[[!item.entry.federationText]]">
<input id="password" aria-label=$i18n{editPasswordPasswordLabel}
type="[[getPasswordInputType_(item.password)]]"
- on-tap="onReadonlyInputTap_" class="password-field" readonly
+ on-click="onReadonlyInputTap_" class="password-field" readonly
disabled$="[[!item.password]]"
value="[[getPassword_(item.password)]]">
<button is="paper-icon-button-light" id="showPasswordButton"
class$="[[getIconClass_(item.password)]]"
- on-tap="onShowPasswordButtonTap_"
+ on-click="onShowPasswordButtonTap_"
title="[[showPasswordTitle_(item.password,
'$i18nPolymer{hidePassword}','$i18nPolymer{showPassword}')]]"
focus-row-control focus-type="showPassword">
@@ -66,7 +66,7 @@
</template>
</div>
<button is="paper-icon-button-light" id="passwordMenu"
- class="icon-more-vert" on-tap="onPasswordMenuTap_"
+ class="icon-more-vert" on-click="onPasswordMenuTap_"
title="$i18n{moreActions}" focus-row-control
focus-type="passwordMenu">
</button>
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_and_forms_page.html b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_and_forms_page.html
index 55dd3fd8ed6..a994bde577f 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_and_forms_page.html
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_and_forms_page.html
@@ -25,10 +25,10 @@
<neon-animatable route-path="default">
<button is="cr-link-row" icon-class="subpage-arrow"
id="autofillManagerButton" label="$i18n{autofill}"
- sub-label="$i18n{autofillDetail}" on-tap="onAutofillTap_">
+ sub-label="$i18n{autofillDetail}" on-click="onAutofillTap_">
</button>
<div class="settings-box two-line">
- <div class="start two-line" on-tap="onPasswordsTap_" actionable
+ <div class="start two-line" on-click="onPasswordsTap_" actionable
id="passwordManagerButton">
<div class="flex">
$i18n{passwords}
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.html b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.html
index 9353ae7e5fd..2cfe136b936 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.html
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.html
@@ -8,8 +8,15 @@
<dom-module id="passwords-export-dialog">
<template>
<style include="settings-shared iron-flex">
+ paper-progress {
+ width: 100%;
+ --paper-progress-active-color: var(--google-blue-500);
+ }
+ .action-button {
+ -webkit-margin-start: 8px;
+ }
</style>
- <dialog is="cr-dialog" id="dialog" close-text="$i18n{close}">
+ <dialog is="cr-dialog" id="dialog_start" close-text="$i18n{close}">
<div slot="title">$i18n{exportPasswordsTitle}</div>
<div slot="body">
<div class="layout horizontal center">
@@ -18,15 +25,51 @@
</div>
<div slot="button-container">
<paper-button class="secondary-button header-aligned-button"
- on-tap="onCancelButtonTap_">
+ on-click="onCancelButtonTap_" id="cancelButton">
$i18n{cancel}
</paper-button>
<paper-button class="action-button header-aligned-button"
- on-tap="onExportTap_" id="exportPasswordsButton">
+ on-click="onExportTap_" id="exportPasswordsButton">
$i18n{exportPasswords}
</paper-button>
</div>
</dialog>
+
+ <dialog is="cr-dialog" id="dialog_progress" no-cancel="true">
+ <div slot="title">$i18n{exportingPasswordsTitle}</div>
+ <div slot="body">
+ <paper-progress indeterminate class="blue"></paper-progress>
+ </div>
+ <div slot="button-container">
+ <paper-button id="cancel_progress_button"
+ class="secondary-button header-aligned-button"
+ on-click="onCancelProgressButtonTap_">
+ $i18n{cancel}
+ </paper-button>
+ </div>
+ </dialog>
+
+ <dialog is="cr-dialog" id="dialog_error" close-text="$i18n{close}">
+ <div slot="title">[[exportErrorMessage]]</div>
+ <div slot="body">
+ $i18n{exportPasswordsFailTips}
+ <ul>
+ <li>$i18n{exportPasswordsFailTipsEnoughSpace}</li>
+ <li>$i18n{exportPasswordsFailTipsAnotherFolder}</li>
+ </ul>
+ </div>
+ <div slot="button-container">
+ <paper-button class="secondary-button header-aligned-button"
+ on-click="onCancelButtonTap_" id="cancelErrorButton">
+ $i18n{cancel}
+ </paper-button>
+ <paper-button class="action-button header-aligned-button"
+ on-click="onExportTap_" id="tryAgainButton">
+ $i18n{exportPasswordsTryAgain}
+ </paper-button>
+ </div>
+ </dialog>
+
</template>
<script src="passwords_export_dialog.js"></script>
-</dom-module> \ No newline at end of file
+</dom-module>
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.js b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.js
index da06563ff1c..737b3f504e4 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.js
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_export_dialog.js
@@ -10,9 +10,43 @@
(function() {
'use strict';
+/**
+ * The states of the export passwords dialog.
+ * @enum {string}
+ */
+const States = {
+ START: 'START',
+ IN_PROGRESS: 'IN_PROGRESS',
+ ERROR: 'ERROR',
+};
+
+const ProgressStatus = chrome.passwordsPrivate.ExportProgressStatus;
+
+/**
+ * The amount of time (ms) between the start of the export and the moment we
+ * start showing the progress bar.
+ * @type {number}
+ */
+const progressBarDelayMs = 100;
+
+/**
+ * The minimum amount of time (ms) that the progress bar will be visible.
+ * @type {number}
+ */
+const progressBarBlockMs = 1000;
+
Polymer({
is: 'passwords-export-dialog',
+ behaviors: [I18nBehavior],
+
+ properties: {
+ /** The error that occurred while exporting. */
+ exportErrorMessage: String,
+ },
+
+ listeners: {'cancel': 'close'},
+
/**
* The interface for callbacks to the browser.
* Defined in passwords_section.js
@@ -21,16 +55,114 @@ Polymer({
*/
passwordManager_: null,
+ /** @private {function(!PasswordManager.PasswordExportProgress):void} */
+ onPasswordsFileExportProgressListener_: null,
+
+ /**
+ * The task that will display the progress bar, if the export doesn't finish
+ * quickly. This is null, unless the task is currently scheduled.
+ * @private {?number}
+ */
+ progressTaskToken_: null,
+
+ /**
+ * The task that will display the completion of the export, if any. We display
+ * the progress bar for at least |progressBarBlockMs|, therefore, if export
+ * finishes earlier, we cache the result in |delayedProgress_| and this task
+ * will consume it. This is null, unless the task is currently scheduled.
+ * @private {?number}
+ */
+ delayedCompletionToken_: null,
+
+ /**
+ * We display the progress bar for at least |progressBarBlockMs|. If progress
+ * is achieved earlier, we store the update here and consume it later.
+ * @private {?PasswordManager.PasswordExportProgress}
+ */
+ delayedProgress_: null,
+
/** @override */
attached: function() {
- this.$.dialog.showModal();
-
this.passwordManager_ = PasswordManagerImpl.getInstance();
+
+ this.switchToDialog_(States.START);
+
+ this.onPasswordsFileExportProgressListener_ =
+ this.onPasswordsFileExportProgress_.bind(this);
+
+ // If export started on a different tab and is still in progress, display a
+ // busy UI.
+ this.passwordManager_.requestExportProgressStatus(status => {
+ if (status == ProgressStatus.IN_PROGRESS)
+ this.switchToDialog_(States.IN_PROGRESS);
+ });
+
+ this.passwordManager_.addPasswordsFileExportProgressListener(
+ this.onPasswordsFileExportProgressListener_);
+ },
+
+ /**
+ * Handles an export progress event by changing the visible dialog or caching
+ * the event for later consumption.
+ * @param {!PasswordManager.PasswordExportProgress} progress
+ * @private
+ */
+ onPasswordsFileExportProgress_(progress) {
+ // If Chrome has already started displaying the progress bar
+ // (|progressTaskToken_ is null) and hasn't completed its minimum display
+ // time (|delayedCompletionToken_| is not null) progress should be cached
+ // for consumption when the blocking time ends.
+ const progressBlocked =
+ !this.progressTaskToken_ && !!this.delayedCompletionToken_;
+ if (!progressBlocked) {
+ clearTimeout(this.progressTaskToken_);
+ this.progressTaskToken_ = null;
+ this.processProgress_(progress);
+ } else {
+ this.delayedProgress_ = progress;
+ }
+ },
+
+ /**
+ * Displays the progress bar and suspends further UI updates for
+ * |progressBarBlockMs|.
+ * @private
+ */
+ progressTask_() {
+ this.progressTaskToken_ = null;
+ this.switchToDialog_(States.IN_PROGRESS);
+
+ this.delayedCompletionToken_ =
+ setTimeout(this.delayedCompletionTask_.bind(this), progressBarBlockMs);
+ },
+
+ /**
+ * Unblocks progress after showing the progress bar for |progressBarBlock|ms
+ * and processes any progress that was delayed.
+ * @private
+ */
+ delayedCompletionTask_() {
+ this.delayedCompletionToken_ = null;
+ if (this.delayedProgress_) {
+ this.processProgress_(this.delayedProgress_);
+ this.delayedProgress_ = null;
+ }
},
/** Closes the dialog. */
close: function() {
- this.$.dialog.close();
+ clearTimeout(this.progressTaskToken_);
+ clearTimeout(this.delayedCompletionToken_);
+ this.progressTaskToken_ = null;
+ this.delayedCompletionToken_ = null;
+ this.passwordManager_.removePasswordsFileExportProgressListener(
+ this.onPasswordsFileExportProgressListener_);
+ if (this.$.dialog_start.open)
+ this.$.dialog_start.close();
+ if (this.$.dialog_progress.open)
+ this.$.dialog_progress.close();
+ if (this.$.dialog_error.open)
+ this.$.dialog_error.close();
},
/**
@@ -38,7 +170,56 @@ Polymer({
* @private
*/
onExportTap_: function() {
- this.passwordManager_.exportPasswords();
+ this.passwordManager_.exportPasswords(() => {
+ if (chrome.runtime.lastError &&
+ chrome.runtime.lastError.message == 'in-progress') {
+ // Exporting was started by a different call to exportPasswords() and is
+ // is still in progress. This UI needs to be updated to the current
+ // status.
+ this.switchToDialog_(States.IN_PROGRESS);
+ }
+ });
+ },
+
+ /**
+ * Prepares and displays the appropriate view (with delay, if necessary).
+ * @param {!PasswordManager.PasswordExportProgress} progress
+ * @private
+ */
+ processProgress_(progress) {
+ if (progress.status == ProgressStatus.IN_PROGRESS) {
+ this.progressTaskToken_ =
+ setTimeout(this.progressTask_.bind(this), progressBarDelayMs);
+ return;
+ }
+ if (progress.status == ProgressStatus.SUCCEEDED) {
+ this.close();
+ return;
+ }
+ if (progress.status == ProgressStatus.FAILED_WRITE_FAILED) {
+ this.exportErrorMessage =
+ this.i18n('exportPasswordsFailTitle', progress.folderName);
+ this.switchToDialog_(States.ERROR);
+ return;
+ }
+ },
+
+ /**
+ * Opens the specified dialog and hides the others.
+ * @param {!States} state the dialog to open.
+ * @private
+ */
+ switchToDialog_(state) {
+ this.$.dialog_start.open = false;
+ this.$.dialog_error.open = false;
+ this.$.dialog_progress.open = false;
+
+ if (state == States.START)
+ this.$.dialog_start.showModal();
+ if (state == States.ERROR)
+ this.$.dialog_error.showModal();
+ if (state == States.IN_PROGRESS)
+ this.$.dialog_progress.showModal();
},
/**
@@ -48,5 +229,15 @@ Polymer({
onCancelButtonTap_: function() {
this.close();
},
+
+ /**
+ * Handler for tapping the 'cancel' button on the progress dialog. It should
+ * cancel the export and dismiss the dialog.
+ * @private
+ */
+ onCancelProgressButtonTap_: function() {
+ this.passwordManager_.cancelExportPasswords();
+ this.close();
+ },
});
})(); \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.html b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.html
index 4b47e458f25..956780bb648 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.html
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.html
@@ -52,12 +52,16 @@
#undoToast {
z-index: 1;
- }
+ }
+
+ #exportImportMenuButton {
+ -webkit-margin-end: 0;
+ }
</style>
<settings-toggle-button id="passwordToggle"
- class="first primary-toggle"
+ class="first"
aria-label="$i18n{passwords}" no-extension-indicator
- label="[[getOnOffLabel_(prefs.credentials_enable_service.value)]]"
+ label="$i18n{passwordsSavePasswordsLabel}"
pref="{{prefs.credentials_enable_service}}">
</settings-toggle-button>
<template is="dom-if"
@@ -89,7 +93,7 @@
if="[[showImportOrExportPasswords_(
showExportPasswords_, showImportPasswords_)]]">
<button is="paper-icon-button-light" id="exportImportMenuButton"
- class="icon-more-vert" on-tap="onImportExportMenuTap_"
+ class="icon-more-vert" on-click="onImportExportMenuTap_"
title="$i18n{moreActions}" focus-type="exportImportMenuButton">
</button>
</template>
@@ -122,20 +126,20 @@
</div>
</div>
<dialog is="cr-action-menu" id="menu">
- <button id="menuEditPassword" class="dropdown-item"
- on-tap="onMenuEditPasswordTap_">$i18n{passwordViewDetails}</button>
- <button id="menuRemovePassword" class="dropdown-item"
- on-tap="onMenuRemovePasswordTap_">$i18n{removePassword}</button>
+ <button id="menuEditPassword" slot="item" class="dropdown-item"
+ on-click="onMenuEditPasswordTap_">$i18n{passwordViewDetails}</button>
+ <button id="menuRemovePassword" slot="item" class="dropdown-item"
+ on-click="onMenuRemovePasswordTap_">$i18n{removePassword}</button>
</dialog>
<dialog is="cr-action-menu" id="exportImportMenu">
- <template is="dom-if" if="[[showImportPasswords_]]">
- <button id="menuImportPassword" class="dropdown-item"
- on-tap="onImportTap_">$i18n{import}</button>
- </template>
- <template is="dom-if" if="[[showExportPasswords_]]">
- <button id="menuExportPassword" class="dropdown-item"
- on-tap="onExportTap_">$i18n{export}</button>
- </template>
+ <button id="menuImportPassword" slot="item" class="dropdown-item"
+ on-click="onImportTap_" hidden="[[!showImportPasswords_]]">
+ $i18n{import}
+ </button>
+ <button id="menuExportPassword" slot="item" class="dropdown-item"
+ on-click="onExportTap_" hidden="[[!showExportPasswords_]]">
+ $i18n{exportMenuItem}
+ </button>
</dialog>
<template is="dom-if" if="[[showPasswordsExportDialog_]]" restamp>
<passwords-export-dialog on-close="onPasswordsExportDialogClosed_">
@@ -148,7 +152,7 @@
</template>
<cr-toast id="undoToast" duration="[[toastDuration_]]">
<div id="undoLabel">$i18n{passwordDeleted}</div>
- <paper-button id="undoButton" on-tap="onUndoButtonTap_">
+ <paper-button id="undoButton" on-click="onUndoButtonTap_">
$i18n{undoRemovePassword}
</paper-button>
</cr-toast>
@@ -165,7 +169,7 @@
</a>
</div>
<button is="paper-icon-button-light" id="removeExceptionButton"
- class="icon-clear" on-tap="onRemoveExceptionButtonTap_"
+ class="icon-clear" on-click="onRemoveExceptionButtonTap_"
tabindex$="[[tabIndex]]"
title="$i18n{deletePasswordException}">
</button>
diff --git a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.js b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.js
index d4f0f70ac06..33cb93e78a8 100644
--- a/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.js
+++ b/chromium/chrome/browser/resources/settings/passwords_and_forms_page/passwords_section.js
@@ -10,6 +10,8 @@
/**
* Interface for all callbacks to the password API.
+ * TODO(crbug.com/802352) Move the PasswordManager proxy to a separate
+ * location.
* @interface
*/
class PasswordManager {
@@ -84,8 +86,32 @@ class PasswordManager {
/**
* Triggers the dialogue for exporting passwords.
+ * @param {function():void} callback
*/
- exportPasswords() {}
+ exportPasswords(callback) {}
+
+ /**
+ * Cancels the ongoing export of passwords.
+ */
+ cancelExportPasswords(callback) {}
+
+ /**
+ * Queries the status of any ongoing export.
+ * @param {function(!PasswordManager.ExportProgressStatus):void} callback
+ */
+ requestExportProgressStatus(callback) {}
+
+ /**
+ * Add an observer to the export progress.
+ * @param {function(!PasswordManager.PasswordExportProgress):void} listener
+ */
+ addPasswordsFileExportProgressListener(listener) {}
+
+ /**
+ * Remove an observer from the export progress.
+ * @param {function(!PasswordManager.PasswordExportProgress):void} listener
+ */
+ removePasswordsFileExportProgressListener(listener) {}
}
/** @typedef {chrome.passwordsPrivate.PasswordUiEntry} */
@@ -103,6 +129,12 @@ PasswordManager.PlaintextPasswordEvent;
/** @typedef {{ entry: !PasswordManager.PasswordUiEntry, password: string }} */
PasswordManager.UiEntryWithPassword;
+/** @typedef {chrome.passwordsPrivate.PasswordExportProgress} */
+PasswordManager.PasswordExportProgress;
+
+/** @typedef {chrome.passwordsPrivate.ExportProgressStatus} */
+PasswordManager.ExportProgressStatus;
+
/**
* Implementation that accesses the private API.
* @implements {PasswordManager}
@@ -176,8 +208,29 @@ class PasswordManagerImpl {
}
/** @override */
- exportPasswords() {
- chrome.passwordsPrivate.exportPasswords();
+ exportPasswords(callback) {
+ chrome.passwordsPrivate.exportPasswords(callback);
+ }
+
+ /** @override */
+ cancelExportPasswords() {
+ chrome.passwordsPrivate.cancelExportPasswords();
+ }
+
+ /** @override */
+ requestExportProgressStatus(callback) {
+ chrome.passwordsPrivate.requestExportProgressStatus(callback);
+ }
+
+ /** @override */
+ addPasswordsFileExportProgressListener(listener) {
+ chrome.passwordsPrivate.onPasswordsFileExportProgress.addListener(listener);
+ }
+
+ /** @override */
+ removePasswordsFileExportProgressListener(listener) {
+ chrome.passwordsPrivate.onPasswordsFileExportProgress.removeListener(
+ listener);
}
}
@@ -251,10 +304,7 @@ Polymer({
/** @private */
showExportPasswords_: {
type: Boolean,
- value: function() {
- return loadTimeData.valueExists('showExportPasswords') &&
- loadTimeData.getBoolean('showExportPasswords');
- }
+ computed: 'showExportPasswordsAndReady_(savedPasswords)'
},
/** @private */
@@ -489,6 +539,7 @@ Polymer({
*/
onImportTap_: function() {
this.passwordManager_.importPasswords();
+ this.$.exportImportMenu.close();
},
/**
@@ -503,6 +554,8 @@ Polymer({
/** @private */
onPasswordsExportDialogClosed_: function() {
this.showPasswordsExportDialog_ = false;
+ cr.ui.focusWithoutInk(assert(this.activeDialogAnchor_));
+ this.activeDialogAnchor_ = null;
},
/**
@@ -538,6 +591,16 @@ Polymer({
/**
* @private
+ * @param {!Array<!PasswordManager.PasswordUiEntry>} savedPasswords
+ */
+ showExportPasswordsAndReady_: function(savedPasswords) {
+ return loadTimeData.valueExists('showExportPasswords') &&
+ loadTimeData.getBoolean('showExportPasswords') &&
+ savedPasswords.length > 0;
+ },
+
+ /**
+ * @private
* @param {boolean} showExportPasswords
* @param {boolean} showImportPasswords
* @return {boolean}
diff --git a/chromium/chrome/browser/resources/settings/people_page/change_picture.html b/chromium/chrome/browser/resources/settings/people_page/change_picture.html
index 87ec0b1917d..f14495a8af7 100644
--- a/chromium/chrome/browser/resources/settings/people_page/change_picture.html
+++ b/chromium/chrome/browser/resources/settings/people_page/change_picture.html
@@ -106,7 +106,7 @@
choose-file-label="$i18n{chooseFile}"
old-image-label="$i18n{oldPhoto}"
profile-image-label="$i18n{profilePhoto}"
- take-photo-label="$i18n{takePhoto}">
+ take-photo-label="$i18n{takePhoto}"
capture-video-label="$i18n{captureVideo}">
</cr-picture-list>
</div>
diff --git a/chromium/chrome/browser/resources/settings/people_page/change_picture.js b/chromium/chrome/browser/resources/settings/people_page/change_picture.js
index 48eaa44ed61..eeca32878b8 100644
--- a/chromium/chrome/browser/resources/settings/people_page/change_picture.js
+++ b/chromium/chrome/browser/resources/settings/people_page/change_picture.js
@@ -81,6 +81,9 @@ Polymer({
/** @private {?CrPictureListElement} */
pictureList_: null,
+ /** @private {boolean} */
+ oldImagePending_: false,
+
/** @override */
ready: function() {
this.browserProxy_ = settings.ChangePictureBrowserProxyImpl.getInstance();
@@ -144,6 +147,7 @@ Polymer({
* @private
*/
receiveOldImage_: function(imageInfo) {
+ this.oldImagePending_ = false;
this.pictureList_.setOldImageUrl(imageInfo.url, imageInfo.index);
},
@@ -216,6 +220,7 @@ Polymer({
* @private
*/
onPhotoTaken_: function(event) {
+ this.oldImagePending_ = true;
this.browserProxy_.photoTaken(event.detail.photoDataUrl);
this.pictureList_.setOldImageUrl(event.detail.photoDataUrl);
this.pictureList_.setFocus();
@@ -235,6 +240,9 @@ Polymer({
/** @private */
onDiscardImage_: function() {
+ // Prevent image from being discarded if old image is pending.
+ if (this.oldImagePending_)
+ return;
this.pictureList_.setOldImageUrl(CrPicture.kDefaultImageUrl);
// Revert to profile image as we don't know what last used default image is.
this.browserProxy_.selectProfileImage();
diff --git a/chromium/chrome/browser/resources/settings/people_page/change_picture_browser_proxy.js b/chromium/chrome/browser/resources/settings/people_page/change_picture_browser_proxy.js
index 7295e66f6a0..3cd333e51bd 100644
--- a/chromium/chrome/browser/resources/settings/people_page/change_picture_browser_proxy.js
+++ b/chromium/chrome/browser/resources/settings/people_page/change_picture_browser_proxy.js
@@ -51,8 +51,9 @@ cr.define('settings', function() {
selectProfileImage() {}
/**
- * Provides the taken photo as a data URL to the C++. No response is
- * expected.
+ * Provides the taken photo as a data URL to the C++ and sets the user
+ * image to the 'old' image. As a response, the C++ sends the
+ * 'old-image-changed' WebUIListener event.
* @param {string} photoDataUrl
*/
photoTaken(photoDataUrl) {}
diff --git a/chromium/chrome/browser/resources/settings/people_page/compiled_resources2.gyp b/chromium/chrome/browser/resources/settings/people_page/compiled_resources2.gyp
index 3159cd12ca1..5c15adeb340 100644
--- a/chromium/chrome/browser/resources/settings/people_page/compiled_resources2.gyp
+++ b/chromium/chrome/browser/resources/settings/people_page/compiled_resources2.gyp
@@ -255,5 +255,19 @@
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
+ {
+ 'target_name': 'sync_account_control',
+ 'dependencies': [
+ '../compiled_resources2.gyp:route',
+ '../prefs/compiled_resources2.gyp:prefs_behavior',
+ '<(DEPTH)/ui/webui/resources/cr_elements/cr_action_menu/compiled_resources2.gyp:cr_action_menu',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:icon',
+ '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:web_ui_listener_behavior',
+ 'profile_info_browser_proxy',
+ 'sync_browser_proxy',
+ ],
+ 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
+ },
],
}
diff --git a/chromium/chrome/browser/resources/settings/people_page/easy_unlock_turn_off_dialog.html b/chromium/chrome/browser/resources/settings/people_page/easy_unlock_turn_off_dialog.html
index 211d825ee32..dd65510f60f 100644
--- a/chromium/chrome/browser/resources/settings/people_page/easy_unlock_turn_off_dialog.html
+++ b/chromium/chrome/browser/resources/settings/people_page/easy_unlock_turn_off_dialog.html
@@ -21,11 +21,12 @@
hidden="[[isButtonBarHidden_(status_)]]">
<paper-spinner-lite active="[[isSpinnerActive_(status_)]]">
</paper-spinner-lite>
- <paper-button class="cancel-button" on-tap="onCancelTap_"
+ <paper-button class="cancel-button" on-click="onCancelTap_"
hidden="[[isCancelButtonHidden_(status_)]]">
$i18n{cancel}
</paper-button>
- <paper-button id="turnOff" class="action-button" on-tap="onTurnOffTap_"
+ <paper-button id="turnOff" class="action-button"
+ on-click="onTurnOffTap_"
disabled="[[!isTurnOffButtonEnabled_(status_)]]">
[[getTurnOffButtonText_(status_)]]
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/people_page/fingerprint_list.html b/chromium/chrome/browser/resources/settings/people_page/fingerprint_list.html
index 6c62e3e44af..c98c91b9c8a 100644
--- a/chromium/chrome/browser/resources/settings/people_page/fingerprint_list.html
+++ b/chromium/chrome/browser/resources/settings/people_page/fingerprint_list.html
@@ -29,7 +29,7 @@
}
.body {
- @apply(--settings-list-frame-padding);
+ @apply --settings-list-frame-padding;
}
.list-item {
@@ -55,14 +55,14 @@
on-change="onFingerprintLabelChanged_">
</paper-input>
<button is="paper-icon-button-light" class="icon-delete-gray"
- on-tap="onFingerprintDeleteTapped_">
+ on-click="onFingerprintDeleteTapped_">
</button>
</div>
</template>
</iron-list>
<div class="continuation">
<paper-button id="addFingerprint" class="add-link action-button"
- on-tap="openAddFingerprintDialog_">
+ on-click="openAddFingerprintDialog_">
$i18n{lockScreenAddFingerprint}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/people_page/import_data_dialog.html b/chromium/chrome/browser/resources/settings/people_page/import_data_dialog.html
index 985a4aec02c..cb07d334450 100644
--- a/chromium/chrome/browser/resources/settings/people_page/import_data_dialog.html
+++ b/chromium/chrome/browser/resources/settings/people_page/import_data_dialog.html
@@ -104,7 +104,7 @@
importStatusEnum_.SUCCEEDED, importStatus_)]]"
disabled="[[hasImportStatus_(
importStatusEnum_.IN_PROGRESS, importStatus_)]]"
- on-tap="closeDialog_">
+ on-click="closeDialog_">
$i18n{cancel}
</paper-button>
<paper-button id="import" class="action-button"
@@ -112,14 +112,14 @@
importStatusEnum_.SUCCEEDED, importStatus_)]]"
disabled="[[shouldDisableImport_(
importStatus_, noImportDataTypeSelected_)]]"
- on-tap="onActionButtonTap_">
+ on-click="onActionButtonTap_">
[[getActionButtonText_(selected_)]]
</paper-button>
<paper-button id="done" class="action-button"
hidden$="[[!hasImportStatus_(
importStatusEnum_.SUCCEEDED, importStatus_)]]"
- on-tap="closeDialog_">$i18n{done}</paper-button>
+ on-click="closeDialog_">$i18n{done}</paper-button>
</div>
</dialog>
</template>
diff --git a/chromium/chrome/browser/resources/settings/people_page/lock_screen.html b/chromium/chrome/browser/resources/settings/people_page/lock_screen.html
index cdaa2505cae..d12222f1f36 100644
--- a/chromium/chrome/browser/resources/settings/people_page/lock_screen.html
+++ b/chromium/chrome/browser/resources/settings/people_page/lock_screen.html
@@ -59,7 +59,7 @@
}
#easyUnlockSettingsCollapsible {
- @apply(--settings-list-frame-padding);
+ @apply --settings-list-frame-padding;
}
.no-padding {
@@ -111,7 +111,7 @@
<div id="pinPasswordSecondaryActionDiv"
class="secondary-action">
<paper-button id="setupPinButton" class="secondary-button"
- on-tap="onConfigurePin_">
+ on-click="onConfigurePin_">
[[getSetupPinText_(hasPin)]]
</paper-button>
</div>
@@ -140,7 +140,7 @@
<div class="separator"></div>
<div class="secondary-action">
<paper-button class="secondary-button"
- on-tap="onEditFingerprints_"
+ on-click="onEditFingerprints_"
aria-label="$i18n{lockScreenEditFingerprints}"
aria-descibedby="lockScreenEditFingerprintsSecondary">
$i18n{lockScreenSetupFingerprintButton}
@@ -167,13 +167,13 @@
<div class="separator"></div>
<template is="dom-if" if="[[!easyUnlockEnabled_]]">
<paper-button id="easyUnlockSetup" class="secondary-button"
- on-tap="onEasyUnlockSetupTap_">
+ on-click="onEasyUnlockSetupTap_">
$i18n{easyUnlockSetupButton}
</paper-button>
</template>
<template is="dom-if" if="[[easyUnlockEnabled_]]">
<paper-button id="easyUnlockTurnOff" class="secondary-button"
- on-tap="onEasyUnlockTurnOffTap_">
+ on-click="onEasyUnlockTurnOffTap_">
$i18n{easyUnlockTurnOffButton}
</paper-button>
</template>
diff --git a/chromium/chrome/browser/resources/settings/people_page/password_prompt_dialog.html b/chromium/chrome/browser/resources/settings/people_page/password_prompt_dialog.html
index 9a7dac7a58c..68056695bfe 100644
--- a/chromium/chrome/browser/resources/settings/people_page/password_prompt_dialog.html
+++ b/chromium/chrome/browser/resources/settings/people_page/password_prompt_dialog.html
@@ -34,11 +34,11 @@
</paper-input>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelTap_">
+ <paper-button class="cancel-button" on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="submitPassword_"
+ <paper-button class="action-button" on-click="submitPassword_"
disabled$="[[!enableConfirm_(password_, passwordInvalid_)]]">
$i18n{confirm}
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/people_page/people_page.html b/chromium/chrome/browser/resources/settings/people_page/people_page.html
index 4ab0dfcd81f..d7b6822e5ec 100644
--- a/chromium/chrome/browser/resources/settings/people_page/people_page.html
+++ b/chromium/chrome/browser/resources/settings/people_page/people_page.html
@@ -33,6 +33,7 @@
<link rel="import" href="users_page.html">
</if>
<if expr="not chromeos">
+<link rel="import" href="sync_account_control.html">
<link rel="import" href="import_data_dialog.html">
<link rel="import" href="manage_profile.html">
</if>
@@ -46,9 +47,7 @@
}
#profile-icon {
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
+ background: center / cover no-repeat;
border-radius: 20px;
flex-shrink: 0;
height: 40px;
@@ -102,66 +101,80 @@
<settings-animated-pages id="pages" section="people"
focus-config="[[focusConfig_]]">
<neon-animatable route-path="default">
- <div id="picture-subpage-trigger" class="settings-box first two-line">
- <template is="dom-if" if="[[syncStatus]]">
- <div id="profile-icon" on-tap="onPictureTap_" actionable
- style="background-image: [[getIconImageSet_(profileIconUrl_)]]">
- </div>
<if expr="not chromeos">
- <div class="middle two-line no-min-width"
- on-tap="onProfileNameTap_" actionable>
-</if>
-<if expr="chromeos">
- <div class="middle two-line no-min-width" on-tap="onPictureTap_"
- actionable>
+ <template is="dom-if" if="[[shouldShowSyncAccountControl_(diceEnabled_,
+ syncStatus.syncSystemEnabled, syncStatus.signinAllowed)]]">
+ <settings-sync-account-control
+ promo-label="$i18n{peopleSignInPrompt}"
+ promo-secondary-label="$i18n{peopleSignInPromptSecondary}">
+ </settings-sync-account-control>
+ </template>
+ <template is="dom-if" if="[[!diceEnabled_]]">
</if>
- <div class="flex text-elide">
- <span id="profile-name">[[profileName_]]</span>
- <div class="secondary" hidden="[[!syncStatus.signedIn]]">
- [[syncStatus.signedInUsername]]
- </div>
+ <div id="picture-subpage-trigger" class="settings-box first two-line">
+ <template is="dom-if" if="[[syncStatus]]">
+ <div id="profile-icon" on-click="onProfileTap_" actionable
+ style="background-image: [[getIconImageSet_(
+ profileIconUrl_)]]">
</div>
+ <div class="middle two-line no-min-width" on-click="onProfileTap_"
+ actionable>
+ <div class="flex text-elide">
+ <span id="profile-name">[[profileName_]]</span>
+ <div class="secondary" hidden="[[!syncStatus.signedIn]]">
+ [[syncStatus.signedInUsername]]
+ </div>
+ </div>
<if expr="not chromeos">
- <button class="subpage-arrow" is="paper-icon-button-light"
- aria-label="$i18n{editPerson}"
- aria-describedby="profile-name"></button>
+ <button class="subpage-arrow" is="paper-icon-button-light"
+ aria-label="$i18n{editPerson}"
+ aria-describedby="profile-name"></button>
</if>
<if expr="chromeos">
- <button class="subpage-arrow" is="paper-icon-button-light"
- aria-label="$i18n{changePictureTitle}"
- aria-describedby="profile-name"></button>
+ <button class="subpage-arrow" is="paper-icon-button-light"
+ aria-label="$i18n{changePictureTitle}"
+ aria-describedby="profile-name"></button>
</if>
- </div>
+ </div>
<if expr="not chromeos">
- <template is="dom-if" if="[[showSignin_(syncStatus)]]">
- <div class="separator"></div>
- <paper-button class="primary-button" on-tap="onSigninTap_"
- disabled="[[syncStatus.setupInProgress]]">
- $i18n{syncSignin}
- </paper-button>
- </template>
- <template is="dom-if" if="[[syncStatus.signedIn]]">
- <div class="separator"></div>
- <paper-button id="disconnectButton" class="secondary-button"
- on-tap="onDisconnectTap_"
- disabled="[[syncStatus.setupInProgress]]">
- $i18n{syncDisconnect}
- </paper-button>
+ <template is="dom-if" if="[[showSignin_(syncStatus)]]">
+ <div class="separator"></div>
+ <paper-button class="primary-button" on-click="onSigninTap_"
+ disabled="[[syncStatus.setupInProgress]]">
+ $i18n{syncSignin}
+ </paper-button>
+ </template>
+ <template is="dom-if" if="[[syncStatus.signedIn]]">
+ <div class="separator"></div>
+ <paper-button id="disconnectButton" class="secondary-button"
+ on-click="onDisconnectTap_"
+ disabled="[[syncStatus.setupInProgress]]">
+ $i18n{syncDisconnect}
+ </paper-button>
+ </template>
+</if>
</template>
+ </div>
+<if expr="not chromeos">
+ </template> <!-- if="[[!diceEnabled_]]" -->
</if>
- </template>
- </div>
<template is="dom-if" if="[[!syncStatus.signedIn]]">
- <div class="settings-box two-line"
- hidden="[[!syncStatus.signinAllowed]]">
- <div class="start">
- $i18n{syncOverview}
- <a target="_blank" href="$i18n{syncLearnMoreUrl}">
- $i18n{learnMore}
- </a>
+<if expr="not chromeos">
+ <template is="dom-if" if="[[!diceEnabled_]]">
+</if>
+ <div class="settings-box two-line" id="sync-overview"
+ hidden="[[!syncStatus.signinAllowed]]">
+ <div class="start">
+ $i18n{syncOverview}
+ <a target="_blank" href="$i18n{syncLearnMoreUrl}">
+ $i18n{learnMore}
+ </a>
+ </div>
</div>
- </div>
+<if expr="not chromeos">
+ </template> <!-- if="[[!diceEnabled_]]" -->
+</if>
<div class="settings-box" hidden="[[syncStatus.signinAllowed]]">
$i18n{syncDisabledByAdministrator}
</div>
@@ -182,7 +195,7 @@
<template is="dom-if"
if="[[isAdvancedSyncSettingsVisible_(syncStatus)]]">
- <div class="settings-box two-line" on-tap="onSyncTap_"
+ <div class="settings-box two-line" on-click="onSyncTap_"
id="sync-status" actionable$="[[isSyncStatusActionable_(
syncStatus)]]">
<div class="icon-container">
@@ -202,9 +215,20 @@
</div>
</template>
+<if expr="not chromeos">
+ <template is="dom-if" if="[[diceEnabled_]]">
+ <div class="settings-box" id="edit-profile" on-click="onProfileTap_"
+ actionable>
+ <div class="start">$i18n{profileNameAndPicture}</div>
+ <button class="subpage-arrow" is="paper-icon-button-light"
+ aria-label="$i18n{editPerson}"></button>
+ </div>
+ </template>
+</if>
+
<if expr="chromeos">
<div id="lock-screen-subpage-trigger" class="settings-box two-line"
- actionable on-tap="onConfigureLockTap_">
+ actionable on-click="onConfigureLockTap_">
<div class="start">
$i18n{lockScreenTitle}
<div class="secondary" id="lockScreenSecondary">
@@ -219,14 +243,14 @@
</if>
<div id="manage-other-people-subpage-trigger"
- class="settings-box" on-tap="onManageOtherPeople_" actionable>
+ class="settings-box" on-click="onManageOtherPeople_" actionable>
<div class="start">$i18n{manageOtherPeople}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{manageOtherPeople}"></button>
</div>
<if expr="not chromeos">
- <div class="settings-box" on-tap="onImportDataTap_" actionable>
+ <div class="settings-box" on-click="onImportDataTap_" actionable>
<div class="start">$i18n{importTitle}</div>
<button id="importDataDialogTrigger" class="subpage-arrow"
is="paper-icon-button-light" aria-label="$i18n{importTitle}">
@@ -234,16 +258,6 @@
</div>
</if>
- <template is="dom-if" if="[[profileManagesSupervisedUsers_]]">
- <a id="manageSupervisedUsersContainer"
- class="settings-box inherit-color no-outline" tabindex="-1"
- target="_blank" href="$i18n{supervisedUsersUrl}">
- <div class="start">$i18n{manageSupervisedUsers}</div>
- <button class="icon-external" is="paper-icon-button-light"
- actionable aria-label="$i18n{manageSupervisedUsers}">
- </button>
- </a>
- </template>
</neon-animatable>
<template is="dom-if" route-path="/syncSetup"
no-search$="[[!isAdvancedSyncSettingsVisible_(syncStatus)]]">
@@ -274,10 +288,7 @@
<settings-subpage
associated-control="[[$$('#manage-other-people-subpage-trigger')]]"
page-title="$i18n{manageOtherPeople}">
- <settings-users-page
- prefs="{{prefs}}"
- profile-manages-supervised-users=
- "[[profileManagesSupervisedUsers_]]">
+ <settings-users-page prefs="{{prefs}}">
</settings-users-page>
</settings-subpage>
</template>
@@ -313,16 +324,16 @@
</div>
</div>
<div slot="button-container">
- <paper-button on-tap="onDisconnectCancel_" class="cancel-button">
+ <paper-button on-click="onDisconnectCancel_" class="cancel-button">
$i18n{cancel}
</paper-button>
<paper-button id="disconnectConfirm" class="action-button"
- hidden="[[syncStatus.domain]]" on-tap="onDisconnectConfirm_">
+ hidden="[[syncStatus.domain]]" on-click="onDisconnectConfirm_">
$i18n{syncDisconnect}
</paper-button>
<paper-button id="disconnectManagedProfileConfirm"
class="action-button" hidden="[[!syncStatus.domain]]"
- on-tap="onDisconnectConfirm_">
+ on-click="onDisconnectConfirm_">
$i18n{syncDisconnectConfirm}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/people_page/people_page.js b/chromium/chrome/browser/resources/settings/people_page/people_page.js
index 60555a4e8a4..d1be1d2c850 100644
--- a/chromium/chrome/browser/resources/settings/people_page/people_page.js
+++ b/chromium/chrome/browser/resources/settings/people_page/people_page.js
@@ -25,6 +25,22 @@ Polymer({
notify: true,
},
+ // <if expr="not chromeos">
+ /**
+ * This flag is used to conditionally show a set of new sign-in UIs to the
+ * profiles that have been migrated to be consistent with the web sign-ins.
+ * TODO(scottchen): In the future when all profiles are completely migrated,
+ * this should be removed, and UIs hidden behind it should become default.
+ * @private
+ */
+ diceEnabled_: {
+ type: Boolean,
+ value: function() {
+ return loadTimeData.getBoolean('diceEnabled');
+ },
+ },
+ // </if>
+
/**
* The current sync status, supplied by SyncBrowserProxy.
* @type {?settings.SyncStatus}
@@ -33,33 +49,33 @@ Polymer({
/**
* The currently selected profile icon URL. May be a data URL.
+ * @private
*/
profileIconUrl_: String,
/**
* The current profile name.
+ * @private
*/
profileName_: String,
/**
- * True if the current profile manages supervised users.
- */
- profileManagesSupervisedUsers_: Boolean,
-
- /**
* The profile deletion warning. The message indicates the number of
* profile stats that will be deleted if a non-zero count for the profile
* stats is returned from the browser.
+ * @private
*/
deleteProfileWarning_: String,
/**
* True if the profile deletion warning is visible.
+ * @private
*/
deleteProfileWarningVisible_: Boolean,
/**
* True if the checkbox to delete the profile has been checked.
+ * @private
*/
deleteProfile_: Boolean,
@@ -134,12 +150,6 @@ Polymer({
this.addWebUIListener(
'profile-info-changed', this.handleProfileInfo_.bind(this));
- profileInfoProxy.getProfileManagesSupervisedUsers().then(
- this.handleProfileManagesSupervisedUsers_.bind(this));
- this.addWebUIListener(
- 'profile-manages-supervised-users-changed',
- this.handleProfileManagesSupervisedUsers_.bind(this));
-
this.addWebUIListener(
'profile-stats-count-ready', this.handleProfileStatsCount_.bind(this));
@@ -209,15 +219,6 @@ Polymer({
},
/**
- * Handler for when the profile starts or stops managing supervised users.
- * @private
- * @param {boolean} managesSupervisedUsers
- */
- handleProfileManagesSupervisedUsers_: function(managesSupervisedUsers) {
- this.profileManagesSupervisedUsers_ = managesSupervisedUsers;
- },
-
- /**
* Handler for when the profile stats count is pushed from the browser.
* @param {number} count
* @private
@@ -251,7 +252,7 @@ Polymer({
},
/** @private */
- onPictureTap_: function() {
+ onProfileTap_: function() {
// <if expr="chromeos">
settings.navigateTo(settings.routes.CHANGE_PICTURE);
// </if>
@@ -260,13 +261,6 @@ Polymer({
// </if>
},
- // <if expr="not chromeos">
- /** @private */
- onProfileNameTap_: function() {
- settings.navigateTo(settings.routes.MANAGE_PROFILE);
- },
- // </if>
-
/** @private */
onSigninTap_: function() {
this.syncBrowserProxy_.startSignIn();
@@ -275,7 +269,16 @@ Polymer({
/** @private */
onDisconnectClosed_: function() {
this.showDisconnectDialog_ = false;
+ // <if expr="not chromeos">
+ if (!this.diceEnabled_) {
+ // If DICE-enabled, this button won't exist here.
+ cr.ui.focusWithoutInk(assert(this.$$('#disconnectButton')));
+ }
+ // </if>
+
+ // <if expr="chromeos">
cr.ui.focusWithoutInk(assert(this.$$('#disconnectButton')));
+ // </if>
if (settings.getCurrentRoute() == settings.routes.SIGN_OUT)
settings.navigateToPreviousRoute();
@@ -391,6 +394,15 @@ Polymer({
settings.navigateToPreviousRoute();
cr.ui.focusWithoutInk(assert(this.$.importDataDialogTrigger));
},
+
+ /**
+ * @return {boolean}
+ * @private
+ */
+ shouldShowSyncAccountControl_: function() {
+ return !!this.diceEnabled_ && !!this.syncStatus.syncSystemEnabled &&
+ !!this.syncStatus.signinAllowed;
+ },
// </if>
/**
diff --git a/chromium/chrome/browser/resources/settings/people_page/pin_keyboard.html b/chromium/chrome/browser/resources/settings/people_page/pin_keyboard.html
deleted file mode 100644
index 65056df0516..00000000000
--- a/chromium/chrome/browser/resources/settings/people_page/pin_keyboard.html
+++ /dev/null
@@ -1 +0,0 @@
-<include src="../../chromeos/quick_unlock/pin_keyboard.html">
diff --git a/chromium/chrome/browser/resources/settings/people_page/pin_keyboard.js b/chromium/chrome/browser/resources/settings/people_page/pin_keyboard.js
deleted file mode 100644
index 4cb8d021731..00000000000
--- a/chromium/chrome/browser/resources/settings/people_page/pin_keyboard.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// <include src="../../chromeos/quick_unlock/pin_keyboard.js">
diff --git a/chromium/chrome/browser/resources/settings/people_page/profile_info_browser_proxy.js b/chromium/chrome/browser/resources/settings/people_page/profile_info_browser_proxy.js
index 31f4824fd3b..65808a60de6 100644
--- a/chromium/chrome/browser/resources/settings/people_page/profile_info_browser_proxy.js
+++ b/chromium/chrome/browser/resources/settings/people_page/profile_info_browser_proxy.js
@@ -32,12 +32,6 @@ cr.define('settings', function() {
* 'profile-stats-count-ready' WebUI listener event.
*/
getProfileStatsCount() {}
-
- /**
- * Returns a Promise that's true if the profile manages supervised users.
- * @return {!Promise<boolean>}
- */
- getProfileManagesSupervisedUsers() {}
}
/**
@@ -53,11 +47,6 @@ cr.define('settings', function() {
getProfileStatsCount() {
chrome.send('getProfileStatsCount');
}
-
- /** @override */
- getProfileManagesSupervisedUsers() {
- return cr.sendWithPromise('getProfileManagesSupervisedUsers');
- }
}
cr.addSingletonGetter(ProfileInfoBrowserProxyImpl);
diff --git a/chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.html b/chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.html
index c39f06d3012..7e005111b04 100644
--- a/chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.html
+++ b/chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.html
@@ -72,13 +72,13 @@
</div>
</div>
<div slot="button-container">
- <paper-button id="addAnotherButton" on-tap="onAddAnotherFingerprint_"
+ <paper-button id="addAnotherButton" on-click="onAddAnotherFingerprint_"
hidden$="[[hideAddAnother_(step_)]]">
$i18n{configureFingerprintAddAnotherButton}
</paper-button>
<paper-button id="closeButton"
- class$="[[getCloseButtonClass_(step_)]]" on-tap="onClose_">
+ class$="[[getCloseButtonClass_(step_)]]" on-click="onClose_">
[[getCloseButtonText_(step_)]]
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.js b/chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.js
index 7922b7808ec..4b9ca548d58 100644
--- a/chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.js
+++ b/chromium/chrome/browser/resources/settings/people_page/setup_fingerprint_dialog.js
@@ -259,7 +259,7 @@ Polymer({
*/
getCloseButtonText_: function(step) {
if (step == settings.FingerprintSetupStep.READY)
- return this.i18n('configureFingerprintDoneButton');
+ return this.i18n('done');
return this.i18n('cancel');
},
diff --git a/chromium/chrome/browser/resources/settings/people_page/setup_pin_dialog.html b/chromium/chrome/browser/resources/settings/people_page/setup_pin_dialog.html
index d7a74e2a102..7cffa7ec8cb 100644
--- a/chromium/chrome/browser/resources/settings/people_page/setup_pin_dialog.html
+++ b/chromium/chrome/browser/resources/settings/people_page/setup_pin_dialog.html
@@ -1,3 +1,4 @@
+<link rel="import" href="chrome://resources/cr_components/chromeos/quick_unlock/pin_keyboard.html">
<link rel="import" href="chrome://resources/cr_elements/icons.html">
<link rel="import" href="chrome://resources/html/polymer.html">
@@ -6,7 +7,6 @@
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="../i18n_setup.html">
<link rel="import" href="lock_screen_constants.html">
-<link rel="import" href="pin_keyboard.html">
<link rel="import" href="../settings_shared_css.html">
<dom-module id="settings-setup-pin-dialog">
@@ -28,6 +28,13 @@
--iron-icon-fill-color: var(--paper-grey-700);
}
+ pin-keyboard {
+ --pin-keyboard-digit-button: {
+ font-size: 18px;
+ padding: 15px 21px;
+ };
+ }
+
#pinKeyboardDiv {
justify-content: center;
}
@@ -62,11 +69,11 @@
</div>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelTap_">
+ <paper-button class="cancel-button" on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onPinSubmit_"
+ <paper-button class="action-button" on-click="onPinSubmit_"
disabled$="[[!enableSubmit_]]">
<span>[[getContinueMessage_(isConfirmStep_)]]</span>
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/people_page/sync_account_control.html b/chromium/chrome/browser/resources/settings/people_page/sync_account_control.html
new file mode 100644
index 00000000000..e59e5f06258
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/people_page/sync_account_control.html
@@ -0,0 +1,200 @@
+<link rel="import" href="chrome://resources/html/polymer.html">
+
+<link rel="import" href="chrome://resources/cr_elements/icons.html">
+<link rel="import" href="chrome://resources/cr_elements/cr_action_menu/cr_action_menu.html">
+<link rel="import" href="chrome://resources/html/web_ui_listener_behavior.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/iron-icons/notification-icons.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
+<link rel="import" href="profile_info_browser_proxy.html">
+<link rel="import" href="sync_browser_proxy.html">
+<link rel="import" href="../i18n_setup.html">
+<link rel="import" href="../route.html">
+<link rel="import" href="../settings_shared_css.html">
+
+<dom-module id="settings-sync-account-control">
+ <template>
+ <style include="settings-shared">
+ :host {
+ --sync-icon-size: 16px;
+ --sync-icon-border-size: 2px;
+ --shown-avatar-size: 40px;
+ }
+
+ setting-box.middle {
+ /* Per spec, middle text is indented 20px in this section. */
+ -webkit-margin-start: 20px;
+ }
+
+ .account-icon {
+ border-radius: 20px;
+ flex-shrink: 0;
+ height: var(--shown-avatar-size);
+ width: var(--shown-avatar-size);
+ }
+
+ .account-icon.small {
+ height: 20px;
+ width: 20px;
+ }
+
+ #menu .dropdown-item {
+ padding: 12px;
+ }
+
+ #menu .dropdown-item span {
+ -webkit-margin-start: 8px;
+ }
+
+ .flex {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
+
+ #avatar-container {
+ position: relative;
+ }
+
+ #sync-icon-container {
+ align-items: center;
+ background: var(--google-blue-500);
+ border: var(--sync-icon-border-size) solid white;
+ border-radius: 50%;
+ display: flex;
+ height: var(--sync-icon-size);
+ position: absolute;
+ right: -6px;
+ top: calc(var(--shown-avatar-size) - var(--sync-icon-size) -
+ var(--sync-icon-border-size));
+ width: var(--sync-icon-size);
+ }
+
+ :host-context([dir='rtl']) #sync-icon-container {
+ left: -6px;
+ right: initial;
+ }
+
+ #sync-icon-container[syncing] {
+ background: green;
+ }
+
+ #sync-icon-container iron-icon {
+ fill: white;
+ height: 12px;
+ margin: auto;
+ width: 12px;
+ }
+
+ #sign-in {
+ min-width: 100px;
+ }
+
+ #banner {
+ background-color: var(--google-blue-500);
+ display: none;
+ }
+
+ #banner img {
+ -webkit-margin-start: 380px;
+ height: 100px;
+ margin-bottom: -12px;
+ margin-top: 32px;
+ }
+
+ :host([showing-promo]) #banner {
+ display: flex;
+ }
+
+ :host([showing-promo]) #promo-headers {
+ line-height: 1.625rem;
+ padding-bottom: 10px;
+ padding-top: 10px;
+ }
+
+ :host([showing-promo]) #promo-headers #promo-title {
+ font-size: 1.1rem;
+ }
+
+ :host([showing-promo]) #promo-headers .secondary {
+ font-size: 0.9rem;
+ }
+
+ :host([showing-promo]) #promo-headers .separator {
+ display: none;
+ }
+ </style>
+ <div class="settings-box" id="banner">
+ <img src="../images/sync_banner.svg" alt="">
+ </div>
+ <div class="settings-box first two-line" id="promo-headers"
+ hidden="[[syncStatus.signedIn]]">
+ <div class="start">
+ <div id="promo-title">[[promoLabel]]</div>
+ <div class="secondary">
+ [[promoSecondaryLabel]]
+ </div>
+ </div>
+ <div class="separator" hidden="[[shouldShowAvatarRow_]]"></div>
+ <paper-button class="action-button" on-click="onSigninTap_"
+ disabled="[[syncStatus.setupInProgress]]" id="sign-in"
+ hidden="[[shouldShowAvatarRow_]]">
+ $i18n{peopleSignIn}
+ </paper-button>
+ </div>
+ <template is="dom-if" if="[[shouldShowAvatarRow_]]">
+ <div class="settings-box first two-line" id="avatar-row">
+ <div id="avatar-container">
+ <img class="account-icon" alt=""
+ src="[[getAccountImageSrc_(shownAccount_.avatarImage)]]">
+ <div id="sync-icon-container" syncing$="[[syncStatus.signedIn]]">
+ <iron-icon icon="notification:sync"></iron-icon>
+ </div>
+ </div>
+ <div class="middle two-line no-min-width">
+ <div class="flex text-elide" id="user-info">
+ <span>
+ [[getNameDisplay_('$i18nPolymer{syncedToName}',
+ shownAccount_.fullName, syncStatus.signedIn)]]
+ </span>
+ <div class="secondary">[[shownAccount_.email]]</div>
+ </div>
+ </div>
+ <button is="paper-icon-button-light" id="dropdown-arrow"
+ on-click="onMenuButtonTap_" title="$i18n{useAnotherAccount}"
+ class="icon-arrow-dropdown" hidden="[[syncStatus.signedIn]]">
+ </button>
+ <div class="separator" hidden="[[syncStatus.signedIn]]"></div>
+ <paper-button class="action-button" on-click="onSyncButtonTap_"
+ hidden="[[syncStatus.signedIn]]"
+ disabled="[[syncStatus.setupInProgress]]">
+ [[getSubstituteLabel_(
+ '$i18nPolymer{syncAsName}', shownAccount_.givenName)]]
+ </paper-button>
+ <paper-button class="secondary-button" on-click="onTurnOffButtonTap_"
+ hidden="[[!syncStatus.signedIn]]"
+ disabled="[[syncStatus.setupInProgress]]">
+ $i18n{turnOffSync}
+ </paper-button>
+ </div>
+ <template is="dom-if" if="[[!syncStatus.signedIn]]" restamp>
+ <dialog is="cr-action-menu" id="menu" auto-reposition>
+ <template is="dom-repeat" items="[[storedAccounts_]]">
+ <button class="dropdown-item" on-click="onAccountTap_" slot="item">
+ <img class="account-icon small" alt=""
+ src="[[getAccountImageSrc_(item.avatarImage)]]">
+ <span>[[item.email]]</span>
+ </button>
+ </template>
+ <button class="dropdown-item" on-click="onSigninTap_" slot="item"
+ disabled="[[syncStatus.setupInProgress]]" id="sign-in-item">
+ <img class="account-icon small" alt=""
+ src="chrome://theme/IDR_PROFILE_AVATAR_PLACEHOLDER_LARGE">
+ <span>$i18n{useAnotherAccount}</span>
+ </button>
+ </dialog>
+ </template>
+ </template>
+ </template>
+ <script src="sync_account_control.js"></script>
+</dom-module>
diff --git a/chromium/chrome/browser/resources/settings/people_page/sync_account_control.js b/chromium/chrome/browser/resources/settings/people_page/sync_account_control.js
new file mode 100644
index 00000000000..8b079fe22ac
--- /dev/null
+++ b/chromium/chrome/browser/resources/settings/people_page/sync_account_control.js
@@ -0,0 +1,222 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+/**
+ * @fileoverview
+ * 'settings-sync-account-section' is the settings page containing sign-in
+ * settings.
+ */
+cr.exportPath('settings');
+
+/** @const {number} */
+settings.MAX_SIGNIN_PROMO_IMPRESSION = 10;
+
+Polymer({
+ is: 'settings-sync-account-control',
+ behaviors: [WebUIListenerBehavior],
+ properties: {
+ /**
+ * The current sync status, supplied by SyncBrowserProxy.
+ * @type {!settings.SyncStatus}
+ */
+ syncStatus: Object,
+
+ /**
+ * Proxy variable for syncStatus.signedIn to shield observer from being
+ * triggered multiple times whenever syncStatus changes.
+ * @private {boolean}
+ */
+ signedIn_: {
+ type: Boolean,
+ computed: 'computeSignedIn_(syncStatus.signedIn)',
+ observer: 'onSignedInChanged_',
+ },
+
+ /** @private {!Array<!settings.StoredAccount>} */
+ storedAccounts_: Object,
+
+ /** @private {?settings.StoredAccount} */
+ shownAccount_: Object,
+
+ showingPromo: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+
+ promoLabel: String,
+
+ promoSecondaryLabel: String,
+
+ /** @private {boolean} */
+ shouldShowAvatarRow_: {
+ type: Boolean,
+ value: false,
+ computed: 'computeShouldShowAvatarRow_(storedAccounts_, syncStatus,' +
+ 'storedAccounts_.length, syncStatus.signedIn)',
+ observer: 'onShouldShowAvatarRowChange_',
+ }
+ },
+
+ observers: [
+ 'onShownAccountShouldChange_(storedAccounts_, syncStatus)',
+ ],
+
+ /** @private {?settings.SyncBrowserProxy} */
+ syncBrowserProxy_: null,
+
+ /** @override */
+ attached: function() {
+ this.syncBrowserProxy_ = settings.SyncBrowserProxyImpl.getInstance();
+ this.syncBrowserProxy_.getSyncStatus().then(
+ this.handleSyncStatus_.bind(this));
+ this.syncBrowserProxy_.getStoredAccounts().then(
+ this.handleStoredAccounts_.bind(this));
+ this.addWebUIListener(
+ 'sync-status-changed', this.handleSyncStatus_.bind(this));
+ this.addWebUIListener(
+ 'stored-accounts-updated', this.handleStoredAccounts_.bind(this));
+ },
+
+ /**
+ * @return {boolean}
+ * @private
+ */
+ computeSignedIn_: function() {
+ return !!this.syncStatus.signedIn;
+ },
+
+ /** @private */
+ onSignedInChanged_: function() {
+ if (!this.showingPromo && !this.syncStatus.signedIn &&
+ this.syncBrowserProxy_.getPromoImpressionCount() <
+ settings.MAX_SIGNIN_PROMO_IMPRESSION) {
+ this.showingPromo = true;
+ this.syncBrowserProxy_.incrementPromoImpressionCount();
+ } else {
+ // Turn off the promo if the user is signed in.
+ this.showingPromo = false;
+ }
+ },
+
+ /**
+ * @param {string} label
+ * @param {string} name
+ * @return {string}
+ * @private
+ */
+ getSubstituteLabel_: function(label, name) {
+ return loadTimeData.substituteString(label, name);
+ },
+
+ /**
+ * @param {string} label
+ * @param {string} name
+ * @return {string}
+ * @private
+ */
+ getNameDisplay_: function(label, name) {
+ return this.syncStatus.signedIn ?
+ loadTimeData.substituteString(label, name) :
+ name;
+ },
+
+ /**
+ * @param {?string} image
+ * @return {string}
+ * @private
+ */
+ getAccountImageSrc_: function(image) {
+ // image can be undefined if the account has not set an avatar photo.
+ return image || 'chrome://theme/IDR_PROFILE_AVATAR_PLACEHOLDER_LARGE';
+ },
+
+ /**
+ * @param {!Array<!settings.StoredAccount>} accounts
+ * @private
+ */
+ handleStoredAccounts_: function(accounts) {
+ this.storedAccounts_ = accounts;
+ },
+
+ /**
+ * Handler for when the sync state is pushed from the browser.
+ * @param {!settings.SyncStatus} syncStatus
+ * @private
+ */
+ handleSyncStatus_: function(syncStatus) {
+ this.syncStatus = syncStatus;
+ },
+
+ /**
+ * @return {boolean}
+ * @private
+ */
+ computeShouldShowAvatarRow_: function() {
+ return this.syncStatus.signedIn || this.storedAccounts_.length > 0;
+ },
+
+ /** @private */
+ onSigninTap_: function() {
+ this.syncBrowserProxy_.startSignIn();
+
+ // Need to close here since one menu item also triggers this function.
+ if (this.$$('#menu')) {
+ /** @type {!CrActionMenuElement} */ (this.$$('#menu')).close();
+ }
+ },
+
+ /** @private */
+ onSyncButtonTap_: function() {
+ assert(this.shownAccount_);
+ this.syncBrowserProxy_.startSyncingWithEmail(this.shownAccount_.email);
+ },
+
+ /** @private */
+ onTurnOffButtonTap_: function() {
+ /* This will route to people_page's disconnect dialog. */
+ settings.navigateTo(settings.routes.SIGN_OUT);
+ },
+
+ /** @private */
+ onMenuButtonTap_: function() {
+ const actionMenu =
+ /** @type {!CrActionMenuElement} */ (this.$$('#menu'));
+ actionMenu.showAt(assert(this.$$('#dropdown-arrow')));
+ },
+
+ /** @private */
+ onShouldShowAvatarRowChange_: function() {
+ // Close dropdown when avatar-row hides, so if it appears again, the menu
+ // won't be open by default.
+ const actionMenu = this.$$('#menu');
+ if (!this.shouldShowAvatarRow_ && actionMenu && actionMenu.open)
+ actionMenu.close();
+ },
+
+ /**
+ * @param {!{model:
+ * !{item: !settings.StoredAccount},
+ * }} e
+ * @private
+ */
+ onAccountTap_: function(e) {
+ this.shownAccount_ = e.model.item;
+ /** @type {!CrActionMenuElement} */ (this.$$('#menu')).close();
+ },
+
+ /** @private */
+ onShownAccountShouldChange_: function() {
+ if (this.syncStatus.signedIn) {
+ for (let i = 0; i < this.storedAccounts_.length; i++) {
+ if (this.storedAccounts_[i].email == this.syncStatus.signedInUsername) {
+ this.shownAccount_ = this.storedAccounts_[i];
+ return;
+ }
+ }
+ } else {
+ this.shownAccount_ =
+ this.storedAccounts_ ? this.storedAccounts_[0] : null;
+ }
+ }
+}); \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/settings/people_page/sync_browser_proxy.js b/chromium/chrome/browser/resources/settings/people_page/sync_browser_proxy.js
index 05f345f3edf..ecb2a43ed3d 100644
--- a/chromium/chrome/browser/resources/settings/people_page/sync_browser_proxy.js
+++ b/chromium/chrome/browser/resources/settings/people_page/sync_browser_proxy.js
@@ -10,12 +10,20 @@
cr.exportPath('settings');
/**
+ * @typedef {{fullName: (string|undefined),
+ * givenName: (string|undefined),
+ * email: string,
+ * avatarImage: (string|undefined)}}
+ * @see chrome/browser/ui/webui/settings/people_handler.cc
+ */
+settings.StoredAccount;
+
+/**
* @typedef {{childUser: (boolean|undefined),
* domain: (string|undefined),
* hasError: (boolean|undefined),
* hasUnrecoverableError: (boolean|undefined),
* managed: (boolean|undefined),
- * setupCompleted: (boolean|undefined),
* setupInProgress: (boolean|undefined),
* signedIn: (boolean|undefined),
* signedInUsername: (string|undefined),
@@ -104,6 +112,12 @@ settings.PageStatus = {
};
cr.define('settings', function() {
+ /**
+ * Key to be used with localStorage.
+ * @type {string}
+ */
+ const PROMO_IMPRESSION_COUNT_KEY = 'signin-promo-count';
+
/** @interface */
class SyncBrowserProxy {
// <if expr="not chromeos">
@@ -124,6 +138,16 @@ cr.define('settings', function() {
*/
manageOtherPeople() {}
+ /**
+ * @return {number} the number of times the sync account promo was shown.
+ */
+ getPromoImpressionCount() {}
+
+ /**
+ * Increment the number of times the sync account promo was shown.
+ */
+ incrementPromoImpressionCount() {}
+
// </if>
// <if expr="chromeos">
@@ -141,6 +165,12 @@ cr.define('settings', function() {
getSyncStatus() {}
/**
+ * Gets a list of stored accounts.
+ * @return {!Promise<!Array<!settings.StoredAccount>>}
+ */
+ getStoredAccounts() {}
+
+ /**
* Function to invoke when the sync page has been navigated to. This
* registers the UI as the "active" sync UI so that if the user tries to
* open another sync UI, this one will be shown instead.
@@ -168,6 +198,12 @@ cr.define('settings', function() {
setSyncEncryption(syncPrefs) {}
/**
+ * Start syncing with an account, specified by its email.
+ * @param {string} email
+ */
+ startSyncingWithEmail(email) {}
+
+ /**
* Opens the Google Activity Controls url in a new tab.
*/
openActivityControlsUrl() {}
@@ -193,13 +229,26 @@ cr.define('settings', function() {
chrome.send('SyncSetupManageOtherPeople');
}
+ /** @override */
+ getPromoImpressionCount() {
+ return parseInt(
+ window.localStorage.getItem(PROMO_IMPRESSION_COUNT_KEY), 10) ||
+ 0;
+ }
+
+ /** @override */
+ incrementPromoImpressionCount() {
+ window.localStorage.setItem(
+ PROMO_IMPRESSION_COUNT_KEY,
+ (this.getPromoImpressionCount() + 1).toString());
+ }
+
// </if>
// <if expr="chromeos">
/** @override */
attemptUserExit() {
return chrome.send('AttemptUserExit');
}
-
// </if>
/** @override */
@@ -208,6 +257,11 @@ cr.define('settings', function() {
}
/** @override */
+ getStoredAccounts() {
+ return cr.sendWithPromise('SyncSetupGetStoredAccounts');
+ }
+
+ /** @override */
didNavigateToSyncPage() {
chrome.send('SyncSetupShowSetupUI');
}
@@ -230,6 +284,11 @@ cr.define('settings', function() {
}
/** @override */
+ startSyncingWithEmail(email) {
+ chrome.send('SyncSetupStartSyncingWithEmail', [email]);
+ }
+
+ /** @override */
openActivityControlsUrl() {
chrome.metricsPrivate.recordUserAction(
'Signin_AccountSettings_GoogleActivityControlsClicked');
diff --git a/chromium/chrome/browser/resources/settings/people_page/sync_page.html b/chromium/chrome/browser/resources/settings/people_page/sync_page.html
index bea84fd3f43..5f9b6a925e1 100644
--- a/chromium/chrome/browser/resources/settings/people_page/sync_page.html
+++ b/chromium/chrome/browser/resources/settings/people_page/sync_page.html
@@ -88,7 +88,7 @@
on-keypress="onSubmitExistingPassphraseTap_">
</paper-input>
<paper-button id="submitExistingPassphrase"
- on-tap="onSubmitExistingPassphraseTap_" class="action-button"
+ on-click="onSubmitExistingPassphraseTap_" class="action-button"
disabled="[[!existingPassphrase_]]">
$i18n{submitPassphraseButton}
</paper-button>
@@ -251,7 +251,7 @@
<a class="settings-box two-line inherit-color no-outline" tabindex="-1"
target="_blank" href="$i18n{activityControlsUrl}"
- on-tap="onActivityControlsTap_">
+ on-click="onActivityControlsTap_">
<div class="start">
$i18n{personalizeGoogleServicesTitle}
<div class="secondary" id="activityControlsSecondary">
@@ -294,7 +294,7 @@
<span>[[syncPrefs.fullEncryptionBody]]</span>
</template>
<template is="dom-if" if="[[!syncPrefs.fullEncryptionBody]]">
- <span on-tap="onLearnMoreTap_">
+ <span on-click="onLearnMoreTap_">
$i18nRaw{encryptWithSyncPassphraseLabel}
</span>
</template>
@@ -323,7 +323,7 @@
error-message="$i18n{mismatchedPassphraseError}">
</paper-input>
<paper-button id="saveNewPassphrase"
- on-tap="onSaveNewPassphraseTap_" class="action-button"
+ on-click="onSaveNewPassphraseTap_" class="action-button"
disabled="[[!isSaveNewPassphraseEnabled_(passphrase_,
confirmation_)]]">
$i18n{save}
diff --git a/chromium/chrome/browser/resources/settings/people_page/user_list.html b/chromium/chrome/browser/resources/settings/people_page/user_list.html
index 0873990c000..4ca878eef14 100644
--- a/chromium/chrome/browser/resources/settings/people_page/user_list.html
+++ b/chromium/chrome/browser/resources/settings/people_page/user_list.html
@@ -46,7 +46,7 @@
</template>
</div>
<button is="paper-icon-button-light" class="icon-clear"
- on-tap="removeUser_"
+ on-click="removeUser_"
hidden="[[shouldHideCloseButton_(disabled, item.isOwner)]]">
</button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/people_page/users_add_user_dialog.html b/chromium/chrome/browser/resources/settings/people_page/users_add_user_dialog.html
index 99850568bff..fe8cd9c784b 100644
--- a/chromium/chrome/browser/resources/settings/people_page/users_add_user_dialog.html
+++ b/chromium/chrome/browser/resources/settings/people_page/users_add_user_dialog.html
@@ -25,10 +25,10 @@
</paper-input>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelTap_">
+ <paper-button class="cancel-button" on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
- <paper-button on-tap="addUser_" class="action-button"
+ <paper-button on-click="addUser_" class="action-button"
disabled$="[[!isValid_]]">
$i18n{add}
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/people_page/users_page.html b/chromium/chrome/browser/resources/settings/people_page/users_page.html
index ac720c8628e..bd58da4cdd0 100644
--- a/chromium/chrome/browser/resources/settings/people_page/users_page.html
+++ b/chromium/chrome/browser/resources/settings/people_page/users_page.html
@@ -39,13 +39,6 @@
label="$i18n{guestBrowsingLabel}"
disabled="[[isEditingDisabled_(isOwner_, isWhitelistManaged_)]]">
</settings-toggle-button>
- <template is="dom-if" if="[[profileManagesSupervisedUsers]]">
- <settings-toggle-button class="continuation"
- pref="{{prefs.cros.accounts.supervisedUsersEnabled}}"
- label="$i18n{supervisedUsersLabel}"
- disabled="[[isEditingDisabled_(isOwner_, isWhitelistManaged_)]]">
- </settings-toggle-button>
- </template>
<settings-toggle-button class="continuation"
pref="{{prefs.cros.accounts.showUserNamesOnSignIn}}"
label="$i18n{showOnSigninLabel}"
@@ -66,7 +59,7 @@
<div id="add-user-button" class="list-item"
hidden="[[isEditingUsersDisabled_(isOwner_, isWhitelistManaged_,
prefs.cros.accounts.allowGuest.value)]]">
- <a is="action-link" class="list-button" on-tap="openAddUserDialog_">
+ <a is="action-link" class="list-button" on-click="openAddUserDialog_">
$i18n{addUsers}
</a>
</div>
diff --git a/chromium/chrome/browser/resources/settings/people_page/users_page.js b/chromium/chrome/browser/resources/settings/people_page/users_page.js
index 762fb6763f8..2d14833a0dc 100644
--- a/chromium/chrome/browser/resources/settings/people_page/users_page.js
+++ b/chromium/chrome/browser/resources/settings/people_page/users_page.js
@@ -19,15 +19,6 @@ Polymer({
notify: true,
},
- /**
- * True if the current profile manages supervised users.
- * Set in people-page.
- */
- profileManagesSupervisedUsers: {
- type: Boolean,
- value: false,
- },
-
/** @private */
isOwner_: {
type: Boolean,
diff --git a/chromium/chrome/browser/resources/settings/prefs/pref_util.js b/chromium/chrome/browser/resources/settings/prefs/pref_util.js
index f9ce20040b3..fd5a45a3ce8 100644
--- a/chromium/chrome/browser/resources/settings/prefs/pref_util.js
+++ b/chromium/chrome/browser/resources/settings/prefs/pref_util.js
@@ -18,7 +18,7 @@ cr.define('Settings.PrefUtil', function() {
case chrome.settingsPrivate.PrefType.BOOLEAN:
return value == 'true';
case chrome.settingsPrivate.PrefType.NUMBER:
- const n = parseInt(value, 10);
+ const n = parseFloat(value);
if (isNaN(n)) {
console.error(
'Argument to stringToPrefValue for number pref ' +
diff --git a/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.html b/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.html
index b9cdf9e4628..d8436bfe128 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.html
+++ b/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.html
@@ -51,18 +51,18 @@
<div slot="dialog-buttons">
<div> <!-- Left group -->
<paper-button id="manuallyAddPrinterButton" class="secondary-button"
- on-tap="switchToManualAddDialog_">
+ on-click="switchToManualAddDialog_">
$i18n{manuallyAddPrinterButtonText}
</paper-button>
</div>
<div> <!-- Right group -->
<paper-button class="cancel-button secondary-button"
- on-tap="onCancelTap_">
+ on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
<paper-button class="action-button" id="addPrinterButton"
disabled="[[!canAddPrinter_(selectedPrinter)]]"
- on-tap="switchToConfiguringDialog_">
+ on-click="switchToConfiguringDialog_">
$i18n{addPrinterButtonText}
</paper-button>
</div>
@@ -122,8 +122,7 @@
<div class="label">$i18n{printerAddress}</div>
<div class="secondary">
<paper-input no-label-float id="printerAddressInput"
- value="{{newPrinter.printerAddress}}"
- on-input="onAddressChanged_">
+ value="{{newPrinter.printerAddress}}">
</paper-input>
</div>
</div>
@@ -159,32 +158,21 @@
</div>
</div>
</div>
- <div class="search-printer-box" id="searchInProgress" hidden>
- <paper-spinner-lite active></paper-spinner-lite>
- <span class="spinner-comment">$i18n{searchingPrinter}</span>
- </div>
- <div class="search-printer-box printer-not-found"
- id="searchNotFound" hidden>
- <span>$i18n{printerNotFound}</span>
- </div>
- <div class="search-printer-box printer-found" id="searchFound" hidden>
- <span>$i18n{printerFound}</span>
- </div>
</div>
<div slot="dialog-buttons">
<div> <!-- Left group -->
<paper-button class="secondary-button"
- on-tap="switchToDiscoveryDialog_">
+ on-click="switchToDiscoveryDialog_">
$i18n{discoverPrintersButtonText}
</paper-button>
</div>
<div> <!-- Right group -->
<paper-button class="cancel-button secondary-button"
- on-tap="onCancelTap_">
+ on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
<paper-button id="addPrinterButton" class="action-button"
- on-tap="addPressed_"
+ on-click="addPressed_"
disabled="[[!canAddPrinter_(newPrinter.printerName,
newPrinter.printerAddress)]]">
$i18n{addPrinterButtonText}
@@ -233,9 +221,11 @@
<div class="label">$i18n{selectDriver}</div>
<div class="secondary">
<paper-input class="browse-file-input" no-label-float readonly
- value="[[getBaseName_(activePrinter.printerPPDPath)]]">
- <paper-button class="browse-button" suffix
- on-tap="onBrowseFile_">
+ value="[[getBaseName_(activePrinter.printerPPDPath)]]"
+ error-message="$i18n{selectDriverErrorMessage}"
+ invalid="[[invalidPPD]]">
+ <paper-button class="browse-button" slot="suffix"
+ on-click="onBrowseFile_">
$i18n{selectDriverButtonText}
</paper-button>
</paper-input>
@@ -248,14 +238,14 @@
</div>
<div slot="dialog-buttons">
<paper-button class="cancel-button secondary-button"
- on-tap="onCancelTap_">
+ on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
<paper-button class="action-button" id="addPrinterButton"
disabled="[[!canAddPrinter_(activePrinter.ppdManufacturer,
activePrinter.ppdModel,
activePrinter.printerPPDPath)]]"
- on-tap="switchToConfiguringDialog_">
+ on-click="switchToConfiguringDialog_">
$i18n{addPrinterButtonText}
</paper-button>
</div>
@@ -279,7 +269,7 @@
</div>
<div slot="dialog-buttons">
<paper-button class="cancel-button secondary-button"
- on-tap="onCancelConfiguringTap_">
+ on-click="onCancelConfiguringTap_">
$i18n{cancel}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.js b/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.js
index bd6a143ae4d..52ff88a5f5c 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.js
+++ b/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog.js
@@ -184,13 +184,6 @@ Polymer({
this.fire('open-configuring-printer-dialog');
},
- /** @private */
- onAddressChanged_: function() {
- // TODO(xdai): Check if the printer address exists and then show the
- // corresponding message after the API is ready.
- // The format of address is: ip-address-or-hostname:port-number.
- },
-
/**
* @param {!Event} event
* @private
diff --git a/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog_util.html b/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog_util.html
index 797f6fe0f76..3928af6bfcd 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog_util.html
+++ b/chromium/chrome/browser/resources/settings/printing_page/cups_add_printer_dialog_util.html
@@ -23,8 +23,8 @@
selected="{{selectedPrinter}}">
</array-selector>
<template is="dom-repeat" items="[[printers]]">
- <button class="list-item" on-tap="onSelect_">
- [[item.printerName]] [[item.printerModel]]
+ <button class="list-item" on-click="onSelect_">
+ [[item.printerName]]
</button>
</template>
</div>
@@ -39,7 +39,11 @@
width: 270px;
}
- iron-dropdown .dropdown-content {
+ iron-dropdown {
+ height: 270px;
+ }
+
+ iron-dropdown [slot='dropdown-content'] {
background-color: white;
box-shadow: 0 2px 6px var(--paper-grey-500);
min-width: 128px;
@@ -56,22 +60,23 @@
background-size: 24px;
}
</style>
- <paper-input-container no-label-float on-tap="onTap_">
+ <paper-input-container no-label-float on-click="onTap_">
<input is="iron-input" type="search" bind-value="{{selectedItem}}"
- on-search="onInputValueChanged_" on-change="onChange_" incremental>
- <button is="paper-icon-button-light" class="icon-search" suffix
- id="searchIcon" hidden>
+ on-search="onInputValueChanged_" on-change="onChange_" incremental
+ slot="input">
+ <button is="paper-icon-button-light" class="icon-search"
+ id="searchIcon" hidden slot="suffix">
</button>
- <button is="paper-icon-button-light" class="icon-arrow-dropdown" suffix
- id="dropdownIcon">
+ <button is="paper-icon-button-light" class="icon-arrow-dropdown"
+ id="dropdownIcon" slot="suffix">
</button>
</paper-input-container>
<iron-dropdown horizontal-align="left" vertical-align="top"
vertical-offset="35">
- <div class="dropdown-content">
+ <div slot="dropdown-content">
<template is="dom-repeat" items="[[items]]"
filter="[[filterItems_(searchTerm_)]]">
- <button class="list-item" on-tap="onSelect_">[[item]]</button>
+ <button class="list-item" on-click="onSelect_">[[item]]</button>
</template>
</div>
</iron-dropdown>
diff --git a/chromium/chrome/browser/resources/settings/printing_page/cups_edit_printer_dialog.html b/chromium/chrome/browser/resources/settings/printing_page/cups_edit_printer_dialog.html
index 63ebb00216d..8f0c517f723 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/cups_edit_printer_dialog.html
+++ b/chromium/chrome/browser/resources/settings/printing_page/cups_edit_printer_dialog.html
@@ -100,8 +100,8 @@
<div class="secondary">
<paper-input class="browse-file-input" no-label-float readonly
value="[[getBaseName_(activePrinter.printerPPDPath)]]">
- <paper-button class="browse-button" suffix
- on-tap="onBrowseFile_">
+ <paper-button class="browse-button" slot="suffix"
+ on-click="onBrowseFile_">
$i18n{selectDriverButtonText}
</paper-button>
</paper-input>
@@ -111,10 +111,10 @@
</div>
<div slot="dialog-buttons">
<paper-button class="cancel-button secondary-button"
- on-tap="onCancelTap_">
+ on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onSaveTap_">
+ <paper-button class="action-button" on-click="onSaveTap_">
$i18n{editPrinterButtonText}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/printing_page/cups_printer_shared_css.html b/chromium/chrome/browser/resources/settings/printing_page/cups_printer_shared_css.html
index 029074eb08d..026ed96e938 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/cups_printer_shared_css.html
+++ b/chromium/chrome/browser/resources/settings/printing_page/cups_printer_shared_css.html
@@ -89,7 +89,7 @@
padding: 0 24px;
text-align: start;
width: 100%;
- @apply(--settings-actionable);
+ @apply --settings-actionable;
}
.list-item:focus {
diff --git a/chromium/chrome/browser/resources/settings/printing_page/cups_printers.html b/chromium/chrome/browser/resources/settings/printing_page/cups_printers.html
index 3df538af0ea..8fe645217ad 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/cups_printers.html
+++ b/chromium/chrome/browser/resources/settings/printing_page/cups_printers.html
@@ -46,6 +46,14 @@
width: 350px;
}
+ #noSearchResultsMessage {
+ color: var(--md-loading-message-color);
+ font-size: 16px;
+ font-weight: 500;
+ margin-top: 80px;
+ text-align: center;
+ }
+
#addPrinterErrorMessage {
display: flex;
justify-content: space-around;
@@ -68,7 +76,7 @@
</div>
</div>
<paper-button class="primary-button" id="addPrinter"
- on-tap="onAddPrinterTap_" disabled="[[!canAddPrinter_]]">
+ on-click="onAddPrinterTap_" disabled="[[!canAddPrinter_]]">
$i18n{addCupsPrinter}
</paper-button>
</div>
@@ -88,6 +96,11 @@
search-term="[[searchTerm]]">
</settings-cups-printers-list>
+ <div id="noSearchResultsMessage"
+ hidden="[[!showNoSearchResultsMessage_(searchTerm)]]">
+ $i18n{noSearchResults}
+ </div>
+
<div id="message">
<div class="center" id="addPrinterDoneMessage" hidden>
$i18n{printerAddedSuccessfulMessage}
diff --git a/chromium/chrome/browser/resources/settings/printing_page/cups_printers.js b/chromium/chrome/browser/resources/settings/printing_page/cups_printers.js
index f5c6e55c690..055f4a807c5 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/cups_printers.js
+++ b/chromium/chrome/browser/resources/settings/printing_page/cups_printers.js
@@ -186,4 +186,18 @@ Polymer({
});
},
+ /**
+ * @param {string} searchTerm
+ * @return {boolean} If the 'no-search-results-found' string should be shown.
+ * @private
+ */
+ showNoSearchResultsMessage_: function(searchTerm) {
+ if (!searchTerm || !this.printers.length)
+ return false;
+ searchTerm = searchTerm.toLowerCase();
+ return !this.printers.some(printer => {
+ return printer.printerName.toLowerCase().includes(searchTerm);
+ });
+ },
+
});
diff --git a/chromium/chrome/browser/resources/settings/printing_page/cups_printers_list.html b/chromium/chrome/browser/resources/settings/printing_page/cups_printers_list.html
index 8c4a1d51ac0..353742c55e2 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/cups_printers_list.html
+++ b/chromium/chrome/browser/resources/settings/printing_page/cups_printers_list.html
@@ -15,10 +15,10 @@
</style>
<dialog is="cr-action-menu">
- <button class="dropdown-item" on-tap="onEditTap_">
+ <button slot="item" class="dropdown-item" on-click="onEditTap_">
$i18n{editPrinter}
</button>
- <button class="dropdown-item" on-tap="onRemoveTap_">
+ <button slot="item" class="dropdown-item" on-click="onRemoveTap_">
$i18n{removePrinter}
</button>
</dialog>
@@ -29,7 +29,7 @@
<div class="printer-name text-elide">[[item.printerName]]</div>
<!--TODO(xdai): Add icon for enterprise CUPS printer. -->
<button is="paper-icon-button-light" class="icon-more-vert"
- on-tap="onOpenActionMenuTap_" title="$i18n{moreActions}">
+ on-click="onOpenActionMenuTap_" title="$i18n{moreActions}">
</button>
</div>
</template>
diff --git a/chromium/chrome/browser/resources/settings/printing_page/cups_set_manufacturer_model_behavior.js b/chromium/chrome/browser/resources/settings/printing_page/cups_set_manufacturer_model_behavior.js
index 84508310b53..43f805bf3f8 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/cups_set_manufacturer_model_behavior.js
+++ b/chromium/chrome/browser/resources/settings/printing_page/cups_set_manufacturer_model_behavior.js
@@ -16,15 +16,16 @@ const SetManufacturerModelBehavior = {
notify: true,
},
- /** @type {?Array<string>} */
- manufacturerList: {
- type: Array,
+ invalidPPD: {
+ type: Boolean,
+ value: false,
},
/** @type {?Array<string>} */
- modelList: {
- type: Array,
- },
+ manufacturerList: Array,
+
+ /** @type {?Array<string>} */
+ modelList: Array,
},
observers: [
@@ -86,11 +87,12 @@ const SetManufacturerModelBehavior = {
},
/**
- * @param {string} path
+ * @param {string} path The full path to the selected PPD file
* @private
*/
printerPPDPathChanged_: function(path) {
this.set('activePrinter.printerPPDPath', path);
+ this.invalidPPD = !path;
},
/**
diff --git a/chromium/chrome/browser/resources/settings/printing_page/printing_page.html b/chromium/chrome/browser/resources/settings/printing_page/printing_page.html
index 90e18fbb2e5..6513b2f9f1a 100644
--- a/chromium/chrome/browser/resources/settings/printing_page/printing_page.html
+++ b/chromium/chrome/browser/resources/settings/printing_page/printing_page.html
@@ -22,7 +22,7 @@
<neon-animatable route-path="default">
<if expr="chromeos">
<div id="cupsPrinters" class="settings-box first"
- on-tap="onTapCupsPrinters_" actionable>
+ on-click="onTapCupsPrinters_" actionable>
<div class="start">$i18n{cupsPrintersTitle}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{cupsPrintersTitle}"></button>
@@ -30,14 +30,14 @@
</if>
<if expr="not chromeos">
<div class="settings-box first"
- on-tap="onTapLocalPrinters_" actionable>
+ on-click="onTapLocalPrinters_" actionable>
<div class="start">$i18n{localPrintersTitle}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{localPrintersTitle}"></button>
</div>
</if>
<div id="cloudPrinters" class="settings-box"
- on-tap="onTapCloudPrinters_" actionable>
+ on-click="onTapCloudPrinters_" actionable>
<div class="start">$i18n{cloudPrintersTitle}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{cloudPrintersTitle}"></button>
diff --git a/chromium/chrome/browser/resources/settings/privacy_page/privacy_page.html b/chromium/chrome/browser/resources/settings/privacy_page/privacy_page.html
index 1ba4c86e60b..d0bd4cfddfd 100644
--- a/chromium/chrome/browser/resources/settings/privacy_page/privacy_page.html
+++ b/chromium/chrome/browser/resources/settings/privacy_page/privacy_page.html
@@ -9,7 +9,6 @@
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-icon-button/paper-icon-button-light.html">
<link rel="import" href="../clear_browsing_data_dialog/clear_browsing_data_dialog.html">
-<link rel="import" href="../clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.html">
<link rel="import" href="../controls/settings_toggle_button.html">
<link rel="import" href="../lifetime_browser_proxy.html">
<link rel="import" href="../route.html">
@@ -40,16 +39,9 @@
<style include="settings-shared">
</style>
<template is="dom-if" if="[[showClearBrowsingDataDialog_]]" restamp>
- <template is="dom-if" if="[[!tabsInCbd_]]" restamp>
- <settings-clear-browsing-data-dialog prefs="{{prefs}}"
- on-close="onDialogClosed_">
- </settings-clear-browsing-data-dialog>
- </template>
- <template is="dom-if" if="[[tabsInCbd_]]" restamp>
- <settings-clear-browsing-data-dialog-tabs prefs="{{prefs}}"
- on-close="onDialogClosed_">
- </settings-clear-browsing-data-dialog-tabs>
- </template>
+ <settings-clear-browsing-data-dialog prefs="{{prefs}}"
+ on-close="onDialogClosed_">
+ </settings-clear-browsing-data-dialog>
</template>
<template id="doNotTrackDialogIf" is="dom-if"
if="[[showDoNotTrackDialog_]]" notify-dom-change>
@@ -60,11 +52,11 @@
<div slot="body">$i18nRaw{doNotTrackDialogMessage}</div>
<div slot="button-container">
<paper-button class="cancel-button"
- on-tap="onDoNotTrackDialogCancel_">
+ on-click="onDoNotTrackDialogCancel_">
$i18n{cancel}
</paper-button>
<paper-button class="action-button"
- on-tap="onDoNotTrackDialogConfirm_">
+ on-click="onDoNotTrackDialogConfirm_">
$i18n{confirm}
</paper-button>
</div>
@@ -141,7 +133,7 @@
</if>
<if expr="use_nss_certs or is_win or is_macosx">
<div id="manageCertificates" class="settings-box two-line"
- actionable on-tap="onManageCertificatesTap_">
+ actionable on-click="onManageCertificatesTap_">
<div class="start">
$i18n{manageCertificates}
<div class="secondary" id="manageCertificatesSecondary">
@@ -162,7 +154,7 @@
</if>
<div id="site-settings-subpage-trigger"
class="settings-box two-line" actionable
- on-tap="onSiteSettingsTap_">
+ on-click="onSiteSettingsTap_">
<div class="start">
[[siteSettingsPageTitle_()]]
<div class="secondary" id="siteSettingsSecondary">
@@ -174,7 +166,7 @@
aria-describedby="siteSettingsSecondary"></button>
</div>
<div class="settings-box two-line" id="clearBrowsingData"
- on-tap="onClearBrowsingDataTap_" actionable>
+ on-click="onClearBrowsingDataTap_" actionable>
<div class="start">
$i18n{clearBrowsingData}
<div class="secondary" id="clearBrowsingDataSecondary">
@@ -265,7 +257,7 @@
label="$i18n{thirdPartyCookie}"
sub-label="$i18n{thirdPartyCookieSublabel}">
</settings-toggle-button>
- <div class="settings-box" actionable on-tap="onSiteDataTap_">
+ <div class="settings-box" actionable on-click="onSiteDataTap_">
<div class="start" id="cookiesLink">
$i18n{siteSettingsCookieLink}
</div>
@@ -375,6 +367,21 @@
</category-setting-exceptions>
</settings-subpage>
</template>
+ <template is="dom-if" if="[[enableSensorsContentSetting_]]" no-search>
+ <template is="dom-if" route-path="/content/sensors" no-search>
+ <settings-subpage page-title="$i18n{siteSettingsSensors}">
+ <category-default-setting
+ toggle-off-label="$i18n{siteSettingsSensorsBlock}"
+ toggle-on-label="$i18n{siteSettingsSensorsAllow}"
+ category="{{ContentSettingsTypes.SENSORS}}">
+ </category-default-setting>
+ <category-setting-exceptions
+ category="{{ContentSettingsTypes.SENSORS}}" read-only-list
+ block-header="$i18n{siteSettingsBlock}">
+ </category-setting-exceptions>
+ </settings-subpage>
+ </template>
+ </template>
<template is="dom-if" route-path="/content/notifications" no-search>
<settings-subpage page-title="$i18n{siteSettingsCategoryNotifications}">
<category-default-setting
@@ -479,7 +486,7 @@
<template is="dom-if" route-path="/cookies/detail" no-search>
<settings-subpage page-title="[[pageTitle]]">
<paper-button slot="subpage-title-extra" class="secondary-button"
- on-tap="onRemoveAllCookiesFromSite_">
+ on-click="onRemoveAllCookiesFromSite_">
$i18n{siteSettingsCookieRemoveAll}
</paper-button>
<site-data-details-subpage page-title="{{pageTitle}}">
diff --git a/chromium/chrome/browser/resources/settings/privacy_page/privacy_page.js b/chromium/chrome/browser/resources/settings/privacy_page/privacy_page.js
index e55ac46515f..abb969d0d3b 100644
--- a/chromium/chrome/browser/resources/settings/privacy_page/privacy_page.js
+++ b/chromium/chrome/browser/resources/settings/privacy_page/privacy_page.js
@@ -80,14 +80,6 @@ Polymer({
showClearBrowsingDataDialog_: Boolean,
/** @private */
- tabsInCbd_: {
- type: Boolean,
- value: function() {
- return loadTimeData.getBoolean('tabsInCbd');
- }
- },
-
- /** @private */
showDoNotTrackDialog_: {
type: Boolean,
value: false,
@@ -128,6 +120,15 @@ Polymer({
}
},
+ /** @private */
+ enableSensorsContentSetting_: {
+ type: Boolean,
+ readOnly: true,
+ value: function() {
+ return loadTimeData.getBoolean('enableSensorsContentSetting');
+ }
+ },
+
/** @private {!Map<string, string>} */
focusConfig_: {
type: Object,
@@ -282,7 +283,7 @@ Polymer({
/** @private */
onDialogClosed_: function() {
- settings.navigateToPreviousRoute();
+ settings.navigateTo(settings.routes.CLEAR_BROWSER_DATA.parent);
cr.ui.focusWithoutInk(assert(this.$.clearBrowsingDataTrigger));
},
@@ -337,16 +338,21 @@ Polymer({
// </if>
/**
- * @param {boolean} enabled Whether reporting is enabled or not.
+ * @param {!SberPrefState} sberPrefState SBER enabled and managed state.
* @private
*/
- setSafeBrowsingExtendedReporting_: function(enabled) {
+ setSafeBrowsingExtendedReporting_: function(sberPrefState) {
// Ignore the next change because it will happen when we set the pref.
- this.safeBrowsingExtendedReportingPref_ = {
+ const pref = {
key: '',
type: chrome.settingsPrivate.PrefType.BOOLEAN,
- value: enabled,
+ value: sberPrefState.enabled,
};
+ if (sberPrefState.managed) {
+ pref.enforcement = chrome.settingsPrivate.Enforcement.ENFORCED;
+ pref.controlledBy = chrome.settingsPrivate.ControlledBy.USER_POLICY;
+ }
+ this.safeBrowsingExtendedReportingPref_ = pref;
},
/**
diff --git a/chromium/chrome/browser/resources/settings/privacy_page/privacy_page_browser_proxy.js b/chromium/chrome/browser/resources/settings/privacy_page/privacy_page_browser_proxy.js
index e5037cfef21..63790dfadb2 100644
--- a/chromium/chrome/browser/resources/settings/privacy_page/privacy_page_browser_proxy.js
+++ b/chromium/chrome/browser/resources/settings/privacy_page/privacy_page_browser_proxy.js
@@ -7,6 +7,9 @@
/** @typedef {{enabled: boolean, managed: boolean}} */
let MetricsReporting;
+/** @typedef {{enabled: boolean, managed: boolean}} */
+let SberPrefState;
+
cr.define('settings', function() {
/** @interface */
class PrivacyPageBrowserProxy {
@@ -25,7 +28,7 @@ cr.define('settings', function() {
// </if>
- /** @return {!Promise<boolean>} */
+ /** @return {!Promise<!SberPrefState>} */
getSafeBrowsingExtendedReporting() {}
/** @param {boolean} enabled */
diff --git a/chromium/chrome/browser/resources/settings/reset_page/powerwash_dialog.html b/chromium/chrome/browser/resources/settings/reset_page/powerwash_dialog.html
index 1459a9afe0e..98a82fd5991 100644
--- a/chromium/chrome/browser/resources/settings/reset_page/powerwash_dialog.html
+++ b/chromium/chrome/browser/resources/settings/reset_page/powerwash_dialog.html
@@ -22,10 +22,10 @@
</span>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelTap_"
+ <paper-button class="cancel-button" on-click="onCancelTap_"
id="cancel">$i18n{cancel}</paper-button>
<paper-button class="action-button" id="powerwash"
- on-tap="onRestartTap_">$i18n{powerwashDialogButton}</paper-button>
+ on-click="onRestartTap_">$i18n{powerwashDialogButton}</paper-button>
</div>
</dialog>
</template>
diff --git a/chromium/chrome/browser/resources/settings/reset_page/reset_page.html b/chromium/chrome/browser/resources/settings/reset_page/reset_page.html
index 82b9c6cf022..d45c9976996 100644
--- a/chromium/chrome/browser/resources/settings/reset_page/reset_page.html
+++ b/chromium/chrome/browser/resources/settings/reset_page/reset_page.html
@@ -17,6 +17,7 @@
<if expr="_google_chrome and is_win">
<link rel="import" href="../chrome_cleanup_page/chrome_cleanup_page.html">
+<link rel="import" href="../incompatible_applications_page/incompatible_applications_page.html">
</if>
<dom-module id="settings-reset-page">
@@ -25,7 +26,7 @@
<settings-animated-pages id="reset-pages" section="reset">
<neon-animatable route-path="default">
<div class="settings-box first two-line" id="resetProfile"
- on-tap="onShowResetProfileDialog_" actionable>
+ on-click="onShowResetProfileDialog_" actionable>
<div class="start">
$i18n{resetTrigger}
<div class="secondary" id="resetProfileSecondary">
@@ -44,7 +45,7 @@
</template>
<if expr="chromeos">
<div class="settings-box two-line" id="powerwash" actionable
- on-tap="onShowPowerwashDialog_" hidden="[[!allowPowerwash_]]">
+ on-click="onShowPowerwashDialog_" hidden="[[!allowPowerwash_]]">
<div class="start">
$i18n{powerwashTitle}
<div class="secondary" id="powerwashSecondary">
@@ -62,20 +63,28 @@
</if>
<if expr="_google_chrome and is_win">
<template is="dom-if" if="[[userInitiatedCleanupsEnabled_]]" restamp>
- <div class="settings-box two-line" id="chromeCleanupSubpageTrigger"
- on-tap="onChromeCleanupTap_" actionable>
- <div class="start">
- $i18n{resetCleanupComputerTrigger}
- <div class="secondary" id="chromeCleanupSecondary">
- $i18n{resetCleanupComputerTriggerDescription}
- </div>
- </div>
+ <div class="settings-box" id="chromeCleanupSubpageTrigger"
+ on-click="onChromeCleanupTap_" actionable>
+ <div class="start">$i18n{resetCleanupComputerTrigger}</div>
<button id="chromeCleanupArrow" is="paper-icon-button-light"
class="subpage-arrow"
aria-label="$i18n{resetCleanupComputerTrigger}"
aria-describedby="chromeCleanupSecondary"></button>
</div>
</template>
+ <template is="dom-if" if="[[showIncompatibleApplications_]]" restamp>
+ <div class="settings-box"
+ id="incompatibleApplicationsSubpageTrigger"
+ on-click="onIncompatibleApplicationsTap_" actionable>
+ <div class="start">
+ $i18n{incompatibleApplicationsResetCardTitle}
+ </div>
+ <button is="paper-icon-button-light"
+ class="subpage-arrow"
+ aria-label="$i18n{incompatibleApplicationsResetCardTitle}"
+ aria-describedby="incompatibleApplicationsSecondary"></button>
+ </div>
+ </template>
</if>
</neon-animatable>
<if expr="_google_chrome and is_win">
@@ -89,6 +98,16 @@
</settings-subpage>
</template>
</template>
+ <template is="dom-if" if="[[showIncompatibleApplications_]]">
+ <template is="dom-if" route-path="/incompatibleApplications">
+ <settings-subpage id="incompatibleApplicationsSubpage"
+ associated-control="[[$$('#incompatibleApplicationsSubpageTrigger')]]"
+ page-title="$i18n{incompatibleApplicationsResetCardTitle}">
+ <settings-incompatible-applications-page>
+ </settings-incompatible-applications-page>
+ </settings-subpage>
+ </template>
+ </template>
</if>
</settings-animated-pages>
</template>
diff --git a/chromium/chrome/browser/resources/settings/reset_page/reset_page.js b/chromium/chrome/browser/resources/settings/reset_page/reset_page.js
index fb5c2229223..10082a0b8f8 100644
--- a/chromium/chrome/browser/resources/settings/reset_page/reset_page.js
+++ b/chromium/chrome/browser/resources/settings/reset_page/reset_page.js
@@ -40,6 +40,14 @@ Polymer({
return loadTimeData.getBoolean('userInitiatedCleanupsEnabled');
},
},
+
+ /** @private */
+ showIncompatibleApplications_: {
+ type: Boolean,
+ value: function() {
+ return loadTimeData.getBoolean('showIncompatibleApplications');
+ },
+ },
// </if>
},
@@ -87,9 +95,15 @@ Polymer({
// </if>
// <if expr="_google_chrome and is_win">
+ /** @private */
onChromeCleanupTap_: function() {
settings.navigateTo(settings.routes.CHROME_CLEANUP);
},
+
+ /** @private */
+ onIncompatibleApplicationsTap_: function() {
+ settings.navigateTo(settings.routes.INCOMPATIBLE_APPLICATIONS);
+ },
// </if>
});
diff --git a/chromium/chrome/browser/resources/settings/reset_page/reset_profile_banner.html b/chromium/chrome/browser/resources/settings/reset_page/reset_profile_banner.html
index 3f49fa4eb03..aed186e6fab 100644
--- a/chromium/chrome/browser/resources/settings/reset_page/reset_profile_banner.html
+++ b/chromium/chrome/browser/resources/settings/reset_page/reset_profile_banner.html
@@ -19,10 +19,10 @@
</span>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onOkTap_" id="ok">
+ <paper-button class="cancel-button" on-click="onOkTap_" id="ok">
$i18n{ok}
</paper-button>
- <paper-button class="action-button" on-tap="onResetTap_" id="reset">
+ <paper-button class="action-button" on-click="onResetTap_" id="reset">
$i18n{resetProfileBannerButton}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.html b/chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.html
index ef12529acb3..be6154e2f30 100644
--- a/chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.html
+++ b/chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.html
@@ -35,11 +35,11 @@
<div slot="button-container">
<paper-spinner-lite id="resetSpinner" active="[[clearingInProgress_]]">
</paper-spinner-lite>
- <paper-button class="cancel-button" on-tap="onCancelTap_"
+ <paper-button class="cancel-button" on-click="onCancelTap_"
id="cancel" disabled="[[clearingInProgress_]]">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onResetTap_"
+ <paper-button class="action-button" on-click="onResetTap_"
id="reset" disabled="[[clearingInProgress_]]">
$i18n{resetPageCommit}
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.js b/chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.js
index 683e2692f9e..22066b37961 100644
--- a/chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.js
+++ b/chromium/chrome/browser/resources/settings/reset_page/reset_profile_dialog.js
@@ -77,7 +77,7 @@ Polymer({
});
this.$$('paper-checkbox a')
- .addEventListener('tap', this.onShowReportedSettingsTap_.bind(this));
+ .addEventListener('click', this.onShowReportedSettingsTap_.bind(this));
// Prevent toggling of the checkbox when hitting the "Enter" key on the
// link.
this.$$('paper-checkbox a').addEventListener('keydown', function(e) {
diff --git a/chromium/chrome/browser/resources/settings/route.js b/chromium/chrome/browser/resources/settings/route.js
index de01fd22a9c..68183214b91 100644
--- a/chromium/chrome/browser/resources/settings/route.js
+++ b/chromium/chrome/browser/resources/settings/route.js
@@ -36,6 +36,7 @@
* FONTS: (undefined|!settings.Route),
* GOOGLE_ASSISTANT: (undefined|!settings.Route),
* IMPORT_DATA: (undefined|!settings.Route),
+ * INCOMPATIBLE_APPLICATIONS: (undefined|!settings.Route),
* INPUT_METHODS: (undefined|!settings.Route),
* INTERNET: (undefined|!settings.Route),
* INTERNET_NETWORKS: (undefined|!settings.Route),
@@ -73,6 +74,7 @@
* SITE_SETTINGS_HANDLERS: (undefined|!settings.Route),
* SITE_SETTINGS_IMAGES: (undefined|!settings.Route),
* SITE_SETTINGS_JAVASCRIPT: (undefined|!settings.Route),
+ * SITE_SETTINGS_SENSORS: (undefined|!settings.Route),
* SITE_SETTINGS_SOUND: (undefined|!settings.Route),
* SITE_SETTINGS_LOCATION: (undefined|!settings.Route),
* SITE_SETTINGS_MICROPHONE: (undefined|!settings.Route),
@@ -317,6 +319,7 @@ cr.define('settings', function() {
r.SITE_SETTINGS_IMAGES = r.SITE_SETTINGS.createChild('images');
r.SITE_SETTINGS_JAVASCRIPT = r.SITE_SETTINGS.createChild('javascript');
r.SITE_SETTINGS_SOUND = r.SITE_SETTINGS.createChild('sound');
+ r.SITE_SETTINGS_SENSORS = r.SITE_SETTINGS.createChild('sensors');
r.SITE_SETTINGS_LOCATION = r.SITE_SETTINGS.createChild('location');
r.SITE_SETTINGS_MICROPHONE = r.SITE_SETTINGS.createChild('microphone');
r.SITE_SETTINGS_NOTIFICATIONS =
@@ -388,6 +391,10 @@ cr.define('settings', function() {
if (loadTimeData.getBoolean('userInitiatedCleanupsEnabled')) {
r.CHROME_CLEANUP = r.RESET.createChild('/cleanup');
}
+ if (loadTimeData.getBoolean('showIncompatibleApplications')) {
+ r.INCOMPATIBLE_APPLICATIONS =
+ r.RESET.createChild('/incompatibleApplications');
+ }
// </if>
}
}
diff --git a/chromium/chrome/browser/resources/settings/search_engines_page/omnibox_extension_entry.html b/chromium/chrome/browser/resources/settings/search_engines_page/omnibox_extension_entry.html
index aa5677369c5..89475dacb36 100644
--- a/chromium/chrome/browser/resources/settings/search_engines_page/omnibox_extension_entry.html
+++ b/chromium/chrome/browser/resources/settings/search_engines_page/omnibox_extension_entry.html
@@ -35,14 +35,16 @@
</div>
<div class="keyword-column">[[engine.keyword]]</div>
<button is="paper-icon-button-light" class="icon-more-vert"
- on-tap="onDotsTap_" title="$i18n{moreActions}" focus-row-control
+ on-click="onDotsTap_" title="$i18n{moreActions}" focus-row-control
focus-type="menu">
</button>
<dialog is="cr-action-menu">
- <button class="dropdown-item" on-tap="onManageTap_" id="manage">
+ <button slot="item" class="dropdown-item" on-click="onManageTap_"
+ id="manage">
$i18n{searchEnginesManageExtension}
</button>
- <button class="dropdown-item" on-tap="onDisableTap_" id="disable">
+ <button slot="item" class="dropdown-item" on-click="onDisableTap_"
+ id="disable">
$i18n{disable}
</button>
</dialog>
diff --git a/chromium/chrome/browser/resources/settings/search_engines_page/search_engine_dialog.html b/chromium/chrome/browser/resources/settings/search_engines_page/search_engine_dialog.html
index a2788c608ec..a8e0591c515 100644
--- a/chromium/chrome/browser/resources/settings/search_engines_page/search_engine_dialog.html
+++ b/chromium/chrome/browser/resources/settings/search_engines_page/search_engine_dialog.html
@@ -44,10 +44,10 @@
</paper-input>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="cancel_" id="cancel">
+ <paper-button class="cancel-button" on-click="cancel_" id="cancel">
$i18n{cancel}</paper-button>
<paper-button id="actionButton" class="action-button"
- on-tap="onActionButtonTap_">
+ on-click="onActionButtonTap_">
[[actionButtonText_]]
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/search_engines_page/search_engine_entry.html b/chromium/chrome/browser/resources/settings/search_engines_page/search_engine_entry.html
index f4b23582e1c..5f0f410817a 100644
--- a/chromium/chrome/browser/resources/settings/search_engines_page/search_engine_entry.html
+++ b/chromium/chrome/browser/resources/settings/search_engines_page/search_engine_entry.html
@@ -54,19 +54,19 @@
<div id="keyword-column"><div>[[engine.keyword]]</div></div>
<div id="url-column" class="text-elide">[[engine.url]]</div>
<button is="paper-icon-button-light" class="icon-more-vert"
- on-tap="onDotsTap_" title="$i18n{moreActions}" focus-row-control
+ on-click="onDotsTap_" title="$i18n{moreActions}" focus-row-control
focus-type="cr-menu-button">
</button>
<dialog is="cr-action-menu">
- <button class="dropdown-item" on-tap="onMakeDefaultTap_"
+ <button slot="item" class="dropdown-item" on-click="onMakeDefaultTap_"
hidden$="[[!engine.canBeDefault]]" id="makeDefault">
$i18n{searchEnginesMakeDefault}
</button>
- <button class="dropdown-item" on-tap="onEditTap_"
+ <button slot="item" class="dropdown-item" on-click="onEditTap_"
hidden$="[[!engine.canBeEdited]]" id="edit">
$i18n{edit}
</button>
- <button class="dropdown-item" on-tap="onDeleteTap_"
+ <button slot="item" class="dropdown-item" on-click="onDeleteTap_"
hidden$="[[!engine.canBeRemoved]]" id="delete">
$i18n{searchEnginesRemoveFromList}
</button>
diff --git a/chromium/chrome/browser/resources/settings/search_engines_page/search_engines_list.html b/chromium/chrome/browser/resources/settings/search_engines_page/search_engines_list.html
index 507bf4875ad..1e6af9d9737 100644
--- a/chromium/chrome/browser/resources/settings/search_engines_page/search_engines_list.html
+++ b/chromium/chrome/browser/resources/settings/search_engines_page/search_engines_list.html
@@ -23,7 +23,7 @@
}
#outer {
- @apply(--settings-list-frame-padding);
+ @apply --settings-list-frame-padding;
}
settings-search-engine-entry {
diff --git a/chromium/chrome/browser/resources/settings/search_engines_page/search_engines_page.html b/chromium/chrome/browser/resources/settings/search_engines_page/search_engines_page.html
index eb06d3cd345..44638d4ee43 100644
--- a/chromium/chrome/browser/resources/settings/search_engines_page/search_engines_page.html
+++ b/chromium/chrome/browser/resources/settings/search_engines_page/search_engines_page.html
@@ -19,7 +19,7 @@
.extension-engines,
#noOtherEngines,
.no-search-results {
- @apply(--settings-list-frame-padding);
+ @apply --settings-list-frame-padding;
}
settings-omnibox-extension-entry {
@@ -43,7 +43,7 @@
<div class="settings-box first">
<h2 class="start">$i18n{searchEnginesOther}</h2>
<paper-button class="secondary-button header-aligned-button"
- on-tap="onAddSearchEngineTap_" id="addSearchEngine">
+ on-click="onAddSearchEngineTap_" id="addSearchEngine">
$i18n{add}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/search_page/search_page.html b/chromium/chrome/browser/resources/settings/search_page/search_page.html
index 1e9f88f1e7c..d32e97c489c 100644
--- a/chromium/chrome/browser/resources/settings/search_page/search_page.html
+++ b/chromium/chrome/browser/resources/settings/search_page/search_page.html
@@ -84,7 +84,7 @@
<!-- Manage search engines -->
<div id="engines-subpage-trigger" class="settings-box"
- on-tap="onManageSearchEnginesTap_" actionable>
+ on-click="onManageSearchEnginesTap_" actionable>
<div class="start">
$i18n{searchEnginesManage}
</div>
@@ -96,7 +96,7 @@
<!-- Google Assistant -->
<template is="dom-if" if="[[voiceInteractionFeatureEnabled_]]">
<div id="assistant-subpage-trigger" class="settings-box two-line"
- on-tap="onGoogleAssistantTap_" actionable>
+ on-click="onGoogleAssistantTap_" actionable>
<div class="start">
$i18n{searchGoogleAssistant}
<div class="secondary">
@@ -111,7 +111,7 @@
<template is="dom-if" if="[[!assistantOn_]]">
<div class="separator"></div>
<paper-button id="enable" class="secondary-button"
- on-tap="onAssistantTurnOnTap_"
+ on-click="onAssistantTurnOnTap_"
aria-label="$i18n{searchPageTitle}"
aria-describedby="secondaryText">
$i18n{assistantTurnOn}
diff --git a/chromium/chrome/browser/resources/settings/search_settings.js b/chromium/chrome/browser/resources/settings/search_settings.js
index 684aed657e9..71971fa8246 100644
--- a/chromium/chrome/browser/resources/settings/search_settings.js
+++ b/chromium/chrome/browser/resources/settings/search_settings.js
@@ -17,18 +17,6 @@ cr.exportPath('settings');
settings.SearchResult;
cr.define('settings', function() {
- /** @type {string} */
- const WRAPPER_CSS_CLASS = 'search-highlight-wrapper';
-
- /** @type {string} */
- const ORIGINAL_CONTENT_CSS_CLASS = 'search-highlight-original-content';
-
- /** @type {string} */
- const HIT_CSS_CLASS = 'search-highlight-hit';
-
- /** @type {string} */
- const SEARCH_BUBBLE_CSS_CLASS = 'search-bubble';
-
/**
* A CSS attribute indicating that a node should be ignored during searching.
* @type {string}
@@ -65,59 +53,8 @@ cr.define('settings', function() {
* @private
*/
function findAndRemoveHighlights_(node) {
- const wrappers = node.querySelectorAll('* /deep/ .' + WRAPPER_CSS_CLASS);
-
- for (let i = 0; i < wrappers.length; i++) {
- const wrapper = wrappers[i];
- const originalNode =
- wrapper.querySelector('.' + ORIGINAL_CONTENT_CSS_CLASS);
- wrapper.parentElement.replaceChild(originalNode.firstChild, wrapper);
- }
-
- const searchBubbles =
- node.querySelectorAll('* /deep/ .' + SEARCH_BUBBLE_CSS_CLASS);
- for (let j = 0; j < searchBubbles.length; j++)
- searchBubbles[j].remove();
- }
-
- /**
- * Applies the highlight UI (yellow rectangle) around all matches in |node|.
- * @param {!Node} node The text node to be highlighted. |node| ends up
- * being removed from the DOM tree.
- * @param {!Array<string>} tokens The string tokens after splitting on the
- * relevant regExp. Even indices hold text that doesn't need highlighting,
- * odd indices hold the text to be highlighted. For example:
- * const r = new RegExp('(foo)', 'i');
- * 'barfoobar foo bar'.split(r) => ['bar', 'foo', 'bar ', 'foo', ' bar']
- * @private
- */
- function highlight_(node, tokens) {
- const wrapper = document.createElement('span');
- wrapper.classList.add(WRAPPER_CSS_CLASS);
- // Use existing node as placeholder to determine where to insert the
- // replacement content.
- node.parentNode.replaceChild(wrapper, node);
-
- // Keep the existing node around for when the highlights are removed. The
- // existing text node might be involved in data-binding and therefore should
- // not be discarded.
- const span = document.createElement('span');
- span.classList.add(ORIGINAL_CONTENT_CSS_CLASS);
- span.style.display = 'none';
- span.appendChild(node);
- wrapper.appendChild(span);
-
- for (let i = 0; i < tokens.length; ++i) {
- if (i % 2 == 0) {
- wrapper.appendChild(document.createTextNode(tokens[i]));
- } else {
- const hitSpan = document.createElement('span');
- hitSpan.classList.add(HIT_CSS_CLASS);
- hitSpan.style.backgroundColor = '#ffeb3b'; // --var(--paper-yellow-500)
- hitSpan.textContent = tokens[i];
- wrapper.appendChild(hitSpan);
- }
- }
+ cr.search_highlight_utils.findAndRemoveHighlights(node);
+ cr.search_highlight_utils.findAndRemoveBubbles(node);
}
/**
@@ -163,8 +100,10 @@ cr.define('settings', function() {
// displayed within an <option>.
// TODO(dpapad): highlight <select> controls with a search bubble
// instead.
- if (node.parentNode.nodeName != 'OPTION')
- highlight_(node, textContent.split(request.regExp));
+ if (node.parentNode.nodeName != 'OPTION') {
+ cr.search_highlight_utils.highlight(
+ node, textContent.split(request.regExp));
+ }
}
// Returning early since TEXT_NODE nodes never have children.
return;
@@ -189,44 +128,6 @@ cr.define('settings', function() {
}
/**
- * Highlights the HTML control that triggers a subpage, by displaying a search
- * bubble.
- * @param {!HTMLElement} element The element to be highlighted.
- * @param {string} rawQuery The search query.
- * @private
- */
- function highlightAssociatedControl_(element, rawQuery) {
- let searchBubble = element.querySelector('.' + SEARCH_BUBBLE_CSS_CLASS);
- // If the associated control has already been highlighted due to another
- // match on the same subpage, there is no need to do anything.
- if (searchBubble)
- return;
-
- searchBubble = document.createElement('div');
- searchBubble.classList.add(SEARCH_BUBBLE_CSS_CLASS);
- const innards = document.createElement('div');
- innards.classList.add('search-bubble-innards', 'text-elide');
- innards.textContent = rawQuery;
- searchBubble.appendChild(innards);
- element.appendChild(searchBubble);
-
- // Dynamically position the bubble at the edge the associated control
- // element.
- const updatePosition = function() {
- searchBubble.style.top = element.offsetTop +
- (innards.classList.contains('above') ? -searchBubble.offsetHeight :
- element.offsetHeight) +
- 'px';
- };
- updatePosition();
-
- searchBubble.addEventListener('mouseover', function() {
- innards.classList.toggle('above');
- updatePosition();
- });
- }
-
- /**
* Finds and makes visible the <settings-section> parent of |node|.
* @param {!Node} node
* @param {string} rawQuery
@@ -253,8 +154,10 @@ cr.define('settings', function() {
// Need to add the search bubble after the parent SETTINGS-SECTION has
// become visible, otherwise |offsetWidth| returns zero.
- if (associatedControl)
- highlightAssociatedControl_(associatedControl, rawQuery);
+ if (associatedControl) {
+ cr.search_highlight_utils.highlightControlWithBubble(
+ associatedControl, rawQuery);
+ }
}
/** @abstract */
diff --git a/chromium/chrome/browser/resources/settings/settings.html b/chromium/chrome/browser/resources/settings/settings.html
index 8732184b614..2f8a74473c8 100644
--- a/chromium/chrome/browser/resources/settings/settings.html
+++ b/chromium/chrome/browser/resources/settings/settings.html
@@ -10,6 +10,8 @@
html {
background-color: #f1f1f1;
overflow: hidden;
+ /* Remove 300ms delay for 'click' event, when using touch interface. */
+ touch-action: manipulation;
}
.loading {
@@ -20,6 +22,7 @@
</head>
<body>
<settings-ui></settings-ui>
+ <link rel="stylesheet" href="chrome://resources/css/md_colors.css">
<link rel="stylesheet" href="chrome://resources/css/text_defaults_md.css">
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="settings_ui/settings_ui.html">
diff --git a/chromium/chrome/browser/resources/settings/settings_main/settings_main.html b/chromium/chrome/browser/resources/settings/settings_main/settings_main.html
index fcbd63786ef..31e3040c21e 100644
--- a/chromium/chrome/browser/resources/settings/settings_main/settings_main.html
+++ b/chromium/chrome/browser/resources/settings/settings_main/settings_main.html
@@ -2,6 +2,7 @@
<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/icons.html">
+<link rel="import" href="chrome://resources/html/search_highlight_utils.html">
<link rel="import" href="chrome://resources/html/promise_resolver.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-a11y-announcer/iron-a11y-announcer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html">
diff --git a/chromium/chrome/browser/resources/settings/settings_page/settings_animated_pages.html b/chromium/chrome/browser/resources/settings/settings_page/settings_animated_pages.html
index 5b3e82131ca..655b3faa8a0 100644
--- a/chromium/chrome/browser/resources/settings/settings_page/settings_animated_pages.html
+++ b/chromium/chrome/browser/resources/settings/settings_page/settings_animated_pages.html
@@ -11,6 +11,7 @@
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/neon-animatable.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/neon-animated-pages.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/neon-animation-runner-behavior.html">
+<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/web-animations.html">
<link rel="import" href="../animation/fade_animations.html">
<link rel="import" href="../route.html">
diff --git a/chromium/chrome/browser/resources/settings/settings_page/settings_section.html b/chromium/chrome/browser/resources/settings/settings_page/settings_section.html
index 644879a6d93..3daf5135314 100644
--- a/chromium/chrome/browser/resources/settings/settings_page/settings_section.html
+++ b/chromium/chrome/browser/resources/settings/settings_page/settings_section.html
@@ -20,13 +20,13 @@
}
#header .title {
- @apply(--cr-section-text);
+ @apply --cr-section-text;
margin-bottom: 0;
margin-top: var(--settings-page-vertical-margin);
}
#card {
- @apply(--shadow-elevation-2dp);
+ @apply --shadow-elevation-2dp;
background-color: white;
border-radius: 2px;
flex: 1;
@@ -39,7 +39,7 @@
:host(.expanding) #card,
:host(.collapsing) #card,
:host(.expanded) #card {
- @apply(--shadow-elevation-4dp);
+ @apply --shadow-elevation-4dp;
overflow: hidden;
/* A stacking context constrains sliding sub-pages to the card. */
z-index: 0;
diff --git a/chromium/chrome/browser/resources/settings/settings_page/settings_subpage.html b/chromium/chrome/browser/resources/settings/settings_page/settings_subpage.html
index 12b26d259d6..1021acdcbe8 100644
--- a/chromium/chrome/browser/resources/settings/settings_page/settings_subpage.html
+++ b/chromium/chrome/browser/resources/settings/settings_page/settings_subpage.html
@@ -26,7 +26,7 @@
}
#learnMore {
- @apply(--cr-paper-icon-button-margin);
+ @apply --cr-paper-icon-button-margin;
align-items: center;
display: flex;
height: var(--cr-icon-ripple-size);
@@ -42,12 +42,12 @@
}
paper-spinner-lite {
- @apply(--cr-icon-height-width);
+ @apply --cr-icon-height-width;
}
h1 {
flex: 1; /* Push other items to the end. */
- @apply(--cr-title-text);
+ @apply --cr-title-text;
}
settings-subpage-search {
@@ -56,7 +56,7 @@
}
</style>
<div class="settings-box first" id="headerLine">
- <button is="paper-icon-button-light" on-tap="onTapBack_"
+ <button is="paper-icon-button-light" on-click="onTapBack_"
aria-label="$i18n{back}" class="icon-arrow-back">
</button>
<h1>[[pageTitle]]</h1>
diff --git a/chromium/chrome/browser/resources/settings/settings_page/settings_subpage_search.html b/chromium/chrome/browser/resources/settings/settings_page/settings_subpage_search.html
index 4f4245f7fec..da6272b5834 100644
--- a/chromium/chrome/browser/resources/settings/settings_page/settings_subpage_search.html
+++ b/chromium/chrome/browser/resources/settings/settings_page/settings_subpage_search.html
@@ -71,10 +71,10 @@
<paper-input-container no-label-float>
<input id="searchInput" type="search" on-search="onSearchTermSearch"
on-input="onSearchTermInput" aria-label$="[[label]]" incremental
- autofocus$="[[autofocus]]" placeholder="[[label]]">
- <button suffix is="paper-icon-button-light" id="clearSearch"
- class="icon-cancel" on-tap="onTapClear_" title="[[clearLabel]]"
- hidden$="[[!hasSearchText]]">
+ autofocus$="[[autofocus]]" placeholder="[[label]]" slot="input">
+ <button is="paper-icon-button-light" id="clearSearch"
+ class="icon-cancel" on-click="onTapClear_" title="[[clearLabel]]"
+ hidden$="[[!hasSearchText]]" slot="suffix">
</button>
</paper-input-container>
</template>
diff --git a/chromium/chrome/browser/resources/settings/settings_resources.grd b/chromium/chrome/browser/resources/settings/settings_resources.grd
index 34e3036a3cc..e4122b701b2 100644
--- a/chromium/chrome/browser/resources/settings/settings_resources.grd
+++ b/chromium/chrome/browser/resources/settings/settings_resources.grd
@@ -325,6 +325,26 @@
file="chrome_cleanup_page/items_to_remove_list.js"
type="chrome_html"/>
</if>
+ <if expr="is_win and _google_chrome">
+ <structure name="IDR_SETTINGS_INCOMPATIBLE_APPLICATIONS_PAGE_HTML"
+ file="incompatible_applications_page/incompatible_applications_page.html"
+ type="chrome_html" />
+ <structure name="IDR_SETTINGS_INCOMPATIBLE_APPLICATIONS_PAGE_JS"
+ file="incompatible_applications_page/incompatible_applications_page.js"
+ type="chrome_html" />
+ <structure name="IDR_SETTINGS_INCOMPATIBLE_APPLICATIONS_BROWSER_PROXY_HTML"
+ file="incompatible_applications_page/incompatible_applications_browser_proxy.html"
+ type="chrome_html" />
+ <structure name="IDR_SETTINGS_INCOMPATIBLE_APPLICATIONS_BROWSER_PROXY_JS"
+ file="incompatible_applications_page/incompatible_applications_browser_proxy.js"
+ type="chrome_html" />
+ <structure name="IDR_SETTINGS_INCOMPATIBLE_APPLICATIONS_INCOMPATIBLE_APPLICATION_ITEM_HTML"
+ file="incompatible_applications_page/incompatible_application_item.html"
+ type="chrome_html" />
+ <structure name="IDR_SETTINGS_INCOMPATIBLE_APPLICATIONS_INCOMPATIBLE_APPLICATION_ITEM_JS"
+ file="incompatible_applications_page/incompatible_application_item.js"
+ type="chrome_html" />
+ </if>
<structure name="IDR_SETTINGS_CLEAR_BROWSING_DATA_BROWSER_PROXY_HTML"
file="clear_browsing_data_dialog/clear_browsing_data_browser_proxy.html"
type="chrome_html" />
@@ -337,12 +357,6 @@
<structure name="IDR_SETTINGS_CLEAR_BROWSING_DATA_DIALOG_JS"
file="clear_browsing_data_dialog/clear_browsing_data_dialog.js"
type="chrome_html" />
- <structure name="IDR_SETTINGS_CLEAR_BROWSING_DATA_DIALOG_TABS_HTML"
- file="clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.html"
- type="chrome_html" />
- <structure name="IDR_SETTINGS_CLEAR_BROWSING_DATA_DIALOG_TABS_JS"
- file="clear_browsing_data_dialog/clear_browsing_data_dialog_tabs.js"
- type="chrome_html" />
<structure name="IDR_SETTINGS_HISTORY_DELETION_DIALOG_HTML"
file="clear_browsing_data_dialog/history_deletion_dialog.html"
type="chrome_html" />
@@ -707,6 +721,14 @@
preprocess="true"
allowexternalscript="true" />
<if expr="not chromeos">
+ <structure name="IDR_SETTINGS_PEOPLE_PAGE_SYNC_ACCOUNT_CONTROL_HTML"
+ file="people_page/sync_account_control.html"
+ type="chrome_html"
+ flattenhtml="true"
+ allowexternalscript="true" />
+ <structure name="IDR_SETTINGS_PEOPLE_PAGE_SYNC_ACCOUNT_CONTROL_JS"
+ file="people_page/sync_account_control.js"
+ type="chrome_html" />
<structure name="IDR_SETTINGS_PEOPLE_PAGE_IMPORT_DATA_DIALOG_HTML"
file="people_page/import_data_dialog.html"
type="chrome_html" />
@@ -1258,11 +1280,6 @@
type="chrome_html"
preprocess="true"
allowexternalscript="true" />
- <structure name="IDR_SETTINGS_PEOPLE_PIN_KEYBOARD_HTML"
- file="people_page/pin_keyboard.html"
- type="chrome_html"
- preprocess="true"
- allowexternalscript="true"/>
<structure name="IDR_SETTINGS_PEOPLE_LOCK_SCREEN_JS"
file="people_page/lock_screen.js"
type="chrome_html" />
@@ -1319,10 +1336,6 @@
<structure name="IDR_SETTINGS_PEOPLE_FINGERPRINT_BROWSER_PROXY_HTML"
file="people_page/fingerprint_browser_proxy.html"
type="chrome_html" />
- <structure name="IDR_SETTINGS_KEYBOARD_PIN_JS"
- file="people_page/pin_keyboard.js"
- type="chrome_html"
- preprocess="true" />
<structure name="IDR_SETTINGS_USERS_PAGE_ADD_USER_DIALOG_JS"
file="people_page/users_add_user_dialog.js"
type="chrome_html" />
diff --git a/chromium/chrome/browser/resources/settings/settings_shared_css.html b/chromium/chrome/browser/resources/settings/settings_shared_css.html
index 36f43725cd7..2c89c7a5a36 100644
--- a/chromium/chrome/browser/resources/settings/settings_shared_css.html
+++ b/chromium/chrome/browser/resources/settings/settings_shared_css.html
@@ -2,6 +2,7 @@
<link rel="import" href="chrome://resources/cr_elements/paper_checkbox_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/paper_input_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/paper_toggle_style_css.html">
+<link rel="import" href="chrome://resources/cr_elements/search_highlight_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html">
<link rel="import" href="chrome://resources/cr_elements/shared_style_css.html">
<link rel="import" href="settings_icons_css.html">
@@ -11,7 +12,7 @@
<!-- Common styles for Material Design settings. -->
<dom-module id="settings-shared">
<template>
- <style include="settings-icons paper-button-style paper-checkbox-style paper-input-style paper-toggle-style cr-shared-style">
+ <style include="settings-icons paper-button-style paper-checkbox-style paper-input-style paper-toggle-style cr-shared-style search-highlight-style">
/* Prevent action-links from being selected to avoid accidental
* selection when trying to click it. */
a[is=action-link] {
@@ -89,8 +90,9 @@
-webkit-margin-start: 16px;
}
- /* Adjust the margin between the separator and the first button. */
- .separator + paper-button {
+ /* Adjust the margin between the separator and the first button. Exclude
+ * .action-button since it has a background thus is visually different. */
+ .separator + paper-button:not(.action-button) {
-webkit-margin-start: calc(var(--cr-button-edge-spacing) * -1);
}
@@ -105,7 +107,7 @@
}
paper-toggle-button {
- @apply(--settings-actionable);
+ @apply --settings-actionable;
height: var(--settings-row-min-height);
user-select: none; /* Prevents text selection while dragging. */
width: 36px;
@@ -154,7 +156,7 @@
/* See also: .no-min-width below. */
.text-elide {
- @apply(--settings-text-elide);
+ @apply --cr-text-elide;
}
/* By default, flexbox children have min-width calculated to be the width
@@ -174,7 +176,7 @@
* outside of a settings-box. A list-frame is likely to follow a
* settings box. */
.list-frame {
- @apply(--settings-list-frame-padding);
+ @apply --settings-list-frame-padding;
align-items: center;
display: block;
}
@@ -230,7 +232,7 @@
/* A settings-box is a horizontal row of text or controls within a
* setting section (page or subpage). */
.settings-box {
- @apply(--cr-section);
+ @apply --cr-section;
}
.settings-box.two-line {
@@ -273,7 +275,7 @@
/* The lower line of text in a two-line row. */
.secondary {
- @apply(--cr-secondary-text);
+ @apply --cr-secondary-text;
}
/* The |:empty| CSS selector only works when there is no whitespace.
@@ -358,44 +360,6 @@
width: 16px;
}
- .search-bubble {
- /* RGB value matches var(--paper-yellow-500). */
- --search-bubble-color: rgba(255, 235, 59, 0.9);
- position: absolute;
- z-index: 1;
- }
-
- .search-bubble-innards {
- align-items: center;
- background-color: var(--search-bubble-color);
- border-radius: 2px;
- max-width: 100px;
- min-width: 64px;
- padding: 4px 10px;
- text-align: center;
- }
-
- /* Provides the arrow which points at the anchor element. */
- .search-bubble-innards::after {
- background-color: var(--search-bubble-color);
- content: '';
- height: 10px;
- left: calc(50% - 5px);
- position: absolute;
- top: -5px;
- transform: rotate(-45deg);
- width: 10px;
- z-index: -1;
- }
-
- /* Turns the arrow direction downwards, when the bubble is placed above
- * the anchor element */
- .search-bubble-innards.above::after {
- bottom: -5px;
- top: auto;
- transform: rotate(-135deg);
- }
-
.column-header {
color: var(--paper-grey-600);
font-weight: 500;
diff --git a/chromium/chrome/browser/resources/settings/settings_ui/settings_ui.html b/chromium/chrome/browser/resources/settings/settings_ui/settings_ui.html
index 7aca114c1af..9031db844ab 100644
--- a/chromium/chrome/browser/resources/settings/settings_ui/settings_ui.html
+++ b/chromium/chrome/browser/resources/settings/settings_ui/settings_ui.html
@@ -24,7 +24,7 @@
<template>
<style include="settings-shared">
:host {
- @apply(--layout-fit);
+ @apply --layout-fit;
color: var(--primary-text-color);
display: flex;
flex-direction: column;
@@ -38,7 +38,7 @@
}
cr-toolbar {
- @apply(--layout-center);
+ @apply --layout-center;
--iron-icon-fill-color: white;
background-color: var(--google-blue-700);
color: white;
diff --git a/chromium/chrome/browser/resources/settings/settings_ui/settings_ui.js b/chromium/chrome/browser/resources/settings/settings_ui/settings_ui.js
index 097280993a4..4b8c62678ec 100644
--- a/chromium/chrome/browser/resources/settings/settings_ui/settings_ui.js
+++ b/chromium/chrome/browser/resources/settings/settings_ui/settings_ui.js
@@ -118,6 +118,8 @@ Polymer({
loadTimeData.getString('networkListItemConnectingTo'),
networkListItemInitializing:
loadTimeData.getString('networkListItemInitializing'),
+ networkListItemScanning:
+ loadTimeData.getString('networkListItemScanning'),
networkListItemNotConnected:
loadTimeData.getString('networkListItemNotConnected'),
networkListItemNoNetwork:
diff --git a/chromium/chrome/browser/resources/settings/settings_vars_css.html b/chromium/chrome/browser/resources/settings/settings_vars_css.html
index 595e689d955..5e9062ead38 100644
--- a/chromium/chrome/browser/resources/settings/settings_vars_css.html
+++ b/chromium/chrome/browser/resources/settings/settings_vars_css.html
@@ -37,12 +37,6 @@
--settings-row-three-line-min-height:
var(--cr-section-three-line-min-height);
- --settings-text-elide: {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- };
-
--settings-separator-height: var(--cr-separator-height);
--settings-separator-line: var(--cr-separator-line);
diff --git a/chromium/chrome/browser/resources/settings/site_settings/add_site_dialog.html b/chromium/chrome/browser/resources/settings/site_settings/add_site_dialog.html
index 94157bee5f4..3aad9d10c78 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/add_site_dialog.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/add_site_dialog.html
@@ -35,10 +35,10 @@
</paper-checkbox>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelTap_">
+ <paper-button class="cancel-button" on-click="onCancelTap_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" id="add" on-tap="onSubmit_"
+ <paper-button class="action-button" id="add" on-click="onSubmit_"
disabled>
$i18n{add}
</paper-button>
diff --git a/chromium/chrome/browser/resources/settings/site_settings/all_sites.html b/chromium/chrome/browser/resources/settings/site_settings/all_sites.html
index 47fa36b5184..2891cc77aa8 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/all_sites.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/all_sites.html
@@ -18,7 +18,7 @@
<div class="list-frame menu-content vertical-list" id="listContainer">
<template is="dom-repeat" items="[[sites]]">
<div class="list-item">
- <div class="layout horizontal center flex" on-tap="onOriginTap_"
+ <div class="layout horizontal center flex" on-click="onOriginTap_"
actionable>
<div class="favicon-image"
style$="[[computeSiteIcon(item.origin)]]">
diff --git a/chromium/chrome/browser/resources/settings/site_settings/category_default_setting.js b/chromium/chrome/browser/resources/settings/site_settings/category_default_setting.js
index 220b76dad6f..9933ccaaabe 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/category_default_setting.js
+++ b/chromium/chrome/browser/resources/settings/site_settings/category_default_setting.js
@@ -97,6 +97,7 @@ Polymer({
case settings.ContentSettingsTypes.IMAGES:
case settings.ContentSettingsTypes.JAVASCRIPT:
case settings.ContentSettingsTypes.SOUND:
+ case settings.ContentSettingsTypes.SENSORS:
case settings.ContentSettingsTypes.POPUPS:
case settings.ContentSettingsTypes.PROTOCOL_HANDLERS:
diff --git a/chromium/chrome/browser/resources/settings/site_settings/constants.js b/chromium/chrome/browser/resources/settings/site_settings/constants.js
index c6a690d271f..f878c560619 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/constants.js
+++ b/chromium/chrome/browser/resources/settings/site_settings/constants.js
@@ -30,9 +30,10 @@ settings.ContentSettingsTypes = {
MIDI_DEVICES: 'midi-sysex',
USB_DEVICES: 'usb-chooser-data',
ZOOM_LEVELS: 'zoom-levels',
- PROTECTED_CONTENT: 'protectedContent',
+ PROTECTED_CONTENT: 'protected-content',
ADS: 'ads',
CLIPBOARD: 'clipboard',
+ SENSORS: 'sensors',
};
/**
diff --git a/chromium/chrome/browser/resources/settings/site_settings/edit_exception_dialog.html b/chromium/chrome/browser/resources/settings/site_settings/edit_exception_dialog.html
index f6ab56fa94a..25da836f905 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/edit_exception_dialog.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/edit_exception_dialog.html
@@ -19,10 +19,10 @@
</paper-input>
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCancelTap_"
+ <paper-button class="cancel-button" on-click="onCancelTap_"
id="cancel">$i18n{cancel}</paper-button>
<paper-button id="actionButton" class="action-button"
- on-tap="onActionButtonTap_" disabled="[[invalid_]]">
+ on-click="onActionButtonTap_" disabled="[[invalid_]]">
$i18n{edit}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.html b/chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.html
index a37356e6dda..b734faea7df 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.html
@@ -44,34 +44,55 @@
<div class="middle" >
<div class="protocol-host">[[item.host]]</div>
<div class="secondary protocol-default"
- hidden$="[[!isDefault_(index, protocol.default_handler)]]">
+ hidden$="[[!item.is_default]]">
$i18n{handlerIsDefault}
</div>
</div>
- <button is="paper-icon-button-light" on-tap="showMenu_"
+ <button is="paper-icon-button-light" on-click="showMenu_"
class="icon-more-vert" title="$i18n{moreActions}">
</button>
</div>
-
</template>
</div>
</template>
<dialog is="cr-action-menu">
- <button class="dropdown-item" on-tap="onDefaultTap_" id="defaultButton"
- hidden$="[[isModelDefault_(actionMenuModel_)]]">
+ <button slot="item" class="dropdown-item" on-click="onDefaultClick_"
+ id="defaultButton" hidden$="[[actionMenuModel_.is_default]]">
$i18n{handlerSetDefault}
</button>
- <button class="dropdown-item" on-tap="onRemoveTap_" id="removeButton">
+ <button slot="item" class="dropdown-item" on-click="onRemoveClick_"
+ id="removeButton">
$i18n{handlerRemove}
</button>
</dialog>
+ <template is="dom-if" if="[[ignoredProtocols.length]]">
+ <div class="column-header">$i18n{siteSettingsBlocked}</div>
+ <div class="list-frame menu-content vertical-list">
+ <template is="dom-repeat" items="[[ignoredProtocols]]">
+ <div class="list-item">
+ <div class="favicon-image" style$="[[computeSiteIcon(item.host)]]">
+ </div>
+ <div class="middle" >
+ <div class="protocol-host">[[item.host]]</div>
+ <div class="secondary protocol-protocol">[[item.protocol]]</div>
+ </div>
+
+ <button is="paper-icon-button-light" on-click="onRemoveIgnored_"
+ class="icon-clear" title="$i18n{moreActions}"
+ id="removeIgnoredButton">
+ </button>
+ </div>
+ </template>
+ </div>
+ </template>
+
<if expr="chromeos">
<template is="dom-if" if="[[settingsAppAvailable_]]">
<div class="settings-box first"
- on-tap="onManageAndroidAppsTap_" actionable>
+ on-click="onManageAndroidAppsClick_" actionable>
<div class="start">
<div>$i18n{androidAppsManageAppLinks}</div>
</div>
diff --git a/chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.js b/chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.js
index 6ad35cec77e..17667e90278 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.js
+++ b/chromium/chrome/browser/resources/settings/site_settings/protocol_handlers.js
@@ -19,16 +19,14 @@ const MenuActions = {
/**
* @typedef {{host: string,
+ * is_default: boolean,
* protocol: string,
* spec: string}}
*/
let HandlerEntry;
/**
- * @typedef {{default_handler: number,
- * handlers: !Array<!HandlerEntry>,
- * has_policy_recommendations: boolean,
- * is_default_handler_set_by_user: boolean,
+ * @typedef {{handlers: !Array<!HandlerEntry>,
* protocol: string}}
*/
let ProtocolEntry;
@@ -52,7 +50,7 @@ Polymer({
/**
* The targetted object for menu operations.
- * @private {?Object}
+ * @private {?HandlerEntry}
*/
actionMenuModel_: Object,
@@ -60,6 +58,12 @@ Polymer({
toggleOffLabel: String,
toggleOnLabel: String,
+ /**
+ * Array of ignored (blocked) protocols.
+ * @type {!Array<!HandlerEntry>}
+ */
+ ignoredProtocols: Array,
+
// <if expr="chromeos">
/** @private */
settingsAppAvailable_: {
@@ -114,17 +118,6 @@ Polymer({
},
/**
- * Returns whether the given index matches the default handler.
- * @param {number} index The index to evaluate.
- * @param {number} defaultHandler The default handler index.
- * @return {boolean} Whether the item is default.
- * @private
- */
- isDefault_: function(index, defaultHandler) {
- return defaultHandler == index;
- },
-
- /**
* Updates the main toggle to set it enabled/disabled.
* @param {boolean} enabled The state to set.
* @private
@@ -144,13 +137,21 @@ Polymer({
/**
* Updates the list of ignored protocol handlers.
- * @param {!Array<!ProtocolEntry>} args The new (ignored) protocol handler
- * list.
+ * @param {!Array<!HandlerEntry>} ignoredProtocols The new (ignored) protocol
+ * handler list.
* @private
*/
- setIgnoredProtocolHandlers_: function(args) {
- // TODO(finnur): Figure this out. Have yet to be able to trigger the C++
- // side to send this.
+ setIgnoredProtocolHandlers_: function(ignoredProtocols) {
+ this.ignoredProtocols = ignoredProtocols;
+ },
+
+ /**
+ * Closes action menu and resets action menu model
+ * @private
+ */
+ closeActionMenu_: function() {
+ this.$$('dialog[is=cr-action-menu]').close();
+ this.actionMenuModel_ = null;
},
/**
@@ -165,45 +166,38 @@ Polymer({
* The handler for when "Set Default" is selected in the action menu.
* @private
*/
- onDefaultTap_: function() {
- const item = this.actionMenuModel_.item;
-
- this.$$('dialog[is=cr-action-menu]').close();
- this.actionMenuModel_ = null;
+ onDefaultClick_: function() {
+ const item = this.actionMenuModel_;
this.browserProxy.setProtocolDefault(item.protocol, item.spec);
+ this.closeActionMenu_();
},
/**
* The handler for when "Remove" is selected in the action menu.
* @private
*/
- onRemoveTap_: function() {
- const item = this.actionMenuModel_.item;
-
- this.$$('dialog[is=cr-action-menu]').close();
- this.actionMenuModel_ = null;
+ onRemoveClick_: function() {
+ const item = this.actionMenuModel_;
this.browserProxy.removeProtocolHandler(item.protocol, item.spec);
+ this.closeActionMenu_();
},
/**
- * Checks whether or not the selected actionMenuModel is the default handler
- * for its protocol.
- * @return {boolean} if actionMenuModel_ is default handler of its protocol.
+ * Handler for removing handlers that were blocked
+ * @private
*/
- isModelDefault_: function() {
- return !!this.actionMenuModel_ &&
- (this.actionMenuModel_.index ==
- this.actionMenuModel_.protocol.default_handler);
+ onRemoveIgnored_: function(event) {
+ const item = event.model.item;
+ this.browserProxy.removeProtocolHandler(item.protocol, item.spec);
},
/**
* A handler to show the action menu next to the clicked menu button.
- * @param {!{model: !{protocol: HandlerEntry, item: ProtocolEntry,
- * index: number}}} event
+ * @param {!{model: !{item: HandlerEntry}}} event
* @private
*/
showMenu_: function(event) {
- this.actionMenuModel_ = event.model;
+ this.actionMenuModel_ = event.model.item;
/** @type {!CrActionMenuElement} */ (this.$$('dialog[is=cr-action-menu]'))
.showAt(
/** @type {!Element} */ (
@@ -215,7 +209,7 @@ Polymer({
* Opens an activity to handle App links (preferred apps).
* @private
*/
- onManageAndroidAppsTap_: function() {
+ onManageAndroidAppsClick_: function() {
this.browserProxy.showAndroidManageAppLinks();
},
// </if>
diff --git a/chromium/chrome/browser/resources/settings/site_settings/site_data.html b/chromium/chrome/browser/resources/settings/site_settings/site_data.html
index c50d94123c3..a92719b376e 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/site_data.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/site_data.html
@@ -25,7 +25,7 @@
}
paper-spinner-lite {
- @apply(--cr-icon-height-width);
+ @apply --cr-icon-height-width;
opacity: 0;
transition-delay: 1s;
}
@@ -46,7 +46,7 @@
<paper-spinner-lite active="[[isLoading_]]"></paper-spinner-lite>
<paper-button class="secondary-button"
disabled$="[[isLoading_]]" id="removeShowingSites"
- on-tap="onRemoveShowingSitesTap_" hidden$="[[!sites.length]]">
+ on-click="onRemoveShowingSitesTap_" hidden$="[[!sites.length]]">
[[computeRemoveLabel_(filter)]]
</paper-button>
</div>
@@ -54,7 +54,7 @@
scroll-target="[[subpageScrollTarget]]">
<template>
<div class="settings-box two-line site-item" first$="[[!index]]"
- on-tap="onSiteTap_" actionable>
+ on-click="onSiteTap_" actionable>
<div class="favicon-image"
style$="background-image: [[favicon_(item.site)]]">
</div>
@@ -67,7 +67,7 @@
<div class="separator"></div>
<button is="paper-icon-button-light" class="icon-delete-gray"
title$="[[i18n('siteSettingsCookieRemoveSite', item.site)]]"
- on-tap="onRemoveSiteTap_">
+ on-click="onRemoveSiteTap_">
</button>
</div>
</template>
@@ -81,10 +81,10 @@
</div>
<div slot="body">$i18n{siteSettingsCookieRemoveMultipleConfirmation}</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCloseDialog_">
+ <paper-button class="cancel-button" on-click="onCloseDialog_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onConfirmDelete_">
+ <paper-button class="action-button" on-click="onConfirmDelete_">
$i18n{siteSettingsCookiesClearAll}
</paper-button>
</div>
diff --git a/chromium/chrome/browser/resources/settings/site_settings/site_data_details_subpage.html b/chromium/chrome/browser/resources/settings/site_settings/site_data_details_subpage.html
index 958e28f8cb5..aedab0ae6d3 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/site_data_details_subpage.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/site_data_details_subpage.html
@@ -29,7 +29,7 @@
</cr-expand-button>
<div class="separator"></div>
<button is="paper-icon-button-light" data-id-path$="[[item.idPath]]"
- class="icon-clear" on-tap="onRemove_">
+ class="icon-clear" on-click="onRemove_">
</button>
</div>
<iron-collapse class="list-frame vertical-list"
diff --git a/chromium/chrome/browser/resources/settings/site_settings/site_details.html b/chromium/chrome/browser/resources/settings/site_settings/site_details.html
index 9a10a9ebf6b..b7f1908390e 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/site_details.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/site_details.html
@@ -44,10 +44,10 @@
$i18n{siteSettingsSiteResetConfirmation}
</div>
<div slot="button-container">
- <paper-button class="cancel-button" on-tap="onCloseDialog_">
+ <paper-button class="cancel-button" on-click="onCloseDialog_">
$i18n{cancel}
</paper-button>
- <paper-button class="action-button" on-tap="onClearAndReset_">
+ <paper-button class="action-button" on-click="onClearAndReset_">
$i18n{siteSettingsSiteResetAll}
</paper-button>
</div>
@@ -66,7 +66,8 @@
<div class="list-item" id="storage" hidden$="[[!storedData_]]">
<div class="start">[[storedData_]]</div>
<button is="paper-icon-button-light" class="icon-delete-gray"
- on-tap="onConfirmClearStorage_" alt="$i18n{siteSettingsDelete}">
+ on-click="onConfirmClearStorage_"
+ aria-label="$i18n{siteSettingsDelete}">
</button>
</div>
</div>
@@ -89,6 +90,12 @@
icon="settings:mic" id="mic"
label="$i18n{siteSettingsMic}">
</site-details-permission>
+ <site-details-permission
+ category="{{ContentSettingsTypes.SENSORS}}"
+ icon="settings:sensors" id="sensors"
+ label="$i18n{siteSettingsSensors}"
+ hidden$="[[!enableSensorsContentSetting_]]">
+ </site-details-permission>
<site-details-permission category="{{ContentSettingsTypes.NOTIFICATIONS}}"
icon="settings:notifications" id="notifications"
label="$i18n{siteSettingsNotifications}">
@@ -132,12 +139,6 @@
id="midiDevices" label="$i18n{siteSettingsMidiDevices}">
</site-details-permission>
<site-details-permission
- category="{{ContentSettingsTypes.CLIPBOARD}}"
- icon="settings:clipboard" id="clipboard"
- label="$i18n{siteSettingsClipboard}"
- hidden$="[[!enableClipboardContentSetting_]]">
- </site-details-permission>
- <site-details-permission
category="{{ContentSettingsTypes.UNSANDBOXED_PLUGINS}}"
icon="cr:extension" id="unsandboxedPlugins"
label="$i18n{siteSettingsUnsandboxedPlugins}">
@@ -149,10 +150,16 @@
label="$i18n{siteSettingsProtectedContentIdentifiers}">
</site-details-permission>
</if>
+ <site-details-permission
+ category="{{ContentSettingsTypes.CLIPBOARD}}"
+ icon="settings:clipboard" id="clipboard"
+ label="$i18n{siteSettingsClipboard}"
+ hidden$="[[!enableClipboardContentSetting_]]">
+ </site-details-permission>
</div>
<div id="clearAndReset" class="settings-box"
- on-tap="onConfirmClearSettings_" actionable>
+ on-click="onConfirmClearSettings_" actionable>
<div class="start">
$i18n{siteSettingsReset}
</div>
diff --git a/chromium/chrome/browser/resources/settings/site_settings/site_details.js b/chromium/chrome/browser/resources/settings/site_settings/site_details.js
index 6d4f9c7c7db..73bd087ea10 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/site_details.js
+++ b/chromium/chrome/browser/resources/settings/site_settings/site_details.js
@@ -74,6 +74,15 @@ Polymer({
},
},
+ /** @private */
+ enableSensorsContentSetting_: {
+ type: Boolean,
+ readOnly: true,
+ value: function() {
+ return loadTimeData.getBoolean('enableSensorsContentSetting');
+ },
+ },
+
/**
* The type of storage for the origin.
* @private
@@ -236,6 +245,8 @@ Polymer({
onClearAndReset_: function() {
this.browserProxy.setOriginPermissions(
this.origin, this.getCategoryList_(), settings.ContentSetting.DEFAULT);
+ if (this.getCategoryList_().includes(settings.ContentSettingsTypes.PLUGINS))
+ this.browserProxy.clearFlashPref(this.origin);
if (this.storedData_ != '')
this.onClearStorage_();
diff --git a/chromium/chrome/browser/resources/settings/site_settings/site_list.html b/chromium/chrome/browser/resources/settings/site_settings/site_list.html
index 4f0141e4d32..9090d7df259 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/site_list.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/site_list.html
@@ -30,29 +30,31 @@
<h2 class="start">[[categoryHeader]]</h2>
<paper-button id="addSite"
class="secondary-button header-aligned-button"
- hidden="[[readOnlyList]]" on-tap="onAddSiteTap_">
+ hidden="[[readOnlyList]]" on-click="onAddSiteTap_">
$i18n{add}
</paper-button>
</div>
<dialog is="cr-action-menu">
- <button class="dropdown-item" id="allow"
- on-tap="onAllowTap_" hidden$="[[!showAllowAction_]]">
+ <button slot="item" class="dropdown-item" id="allow"
+ on-click="onAllowTap_" hidden$="[[!showAllowAction_]]">
$i18n{siteSettingsActionAllow}
</button>
- <button class="dropdown-item" id="block"
- on-tap="onBlockTap_" hidden$="[[!showBlockAction_]]">
+ <button slot="item" class="dropdown-item" id="block"
+ on-click="onBlockTap_" hidden$="[[!showBlockAction_]]">
$i18n{siteSettingsActionBlock}
</button>
- <button class="dropdown-item" id="sessionOnly"
- on-tap="onSessionOnlyTap_"
+ <button slot="item" class="dropdown-item" id="sessionOnly"
+ on-click="onSessionOnlyTap_"
hidden$="[[!showSessionOnlyActionForSite_(actionMenuSite_)]]">
$i18n{siteSettingsActionSessionOnly}
</button>
- <button class="dropdown-item" id="edit" on-tap="onEditTap_">
+ <button slot="item" class="dropdown-item" id="edit"
+ on-click="onEditTap_">
$i18n{edit}
</button>
- <button class="dropdown-item" id="reset" on-tap="onResetTap_">
+ <button slot="item" class="dropdown-item" id="reset"
+ on-click="onResetTap_">
$i18n{siteSettingsActionReset}
</button>
</dialog>
@@ -64,7 +66,7 @@
<template is="dom-repeat" items="[[sites]]">
<div class="list-item">
<div class="settings-row"
- actionable$="[[enableSiteSettings_]]" on-tap="onOriginTap_">
+ actionable$="[[enableSiteSettings_]]" on-click="onOriginTap_">
<div class="favicon-image"
style$="[[computeSiteIcon(item.origin)]]">
</div>
@@ -76,7 +78,7 @@
id="siteDescription">[[computeSiteDescription_(item)]]</div>
</div>
<template is="dom-if" if="[[enableSiteSettings_]]">
- <div on-tap="onOriginTap_" actionable>
+ <div on-click="onOriginTap_" actionable>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label$="[[item.displayName]]"
aria-describedby="siteDescription"></button>
@@ -90,12 +92,12 @@
</cr-policy-pref-indicator>
</template>
<button is="paper-icon-button-light" id="resetSite"
- class="icon-delete-gray" on-tap="onResetButtonTap_"
+ class="icon-delete-gray" on-click="onResetButtonTap_"
hidden="[[shouldHideResetButton_(item, readOnlyList)]]"
- alt="$i18n{siteSettingsActionReset}">
+ aria-label="$i18n{siteSettingsActionReset}">
</button>
<button is="paper-icon-button-light" id="actionMenuButton"
- class="icon-more-vert" on-tap="onShowActionMenuTap_"
+ class="icon-more-vert" on-click="onShowActionMenuTap_"
hidden="[[shouldHideActionMenu_(item, readOnlyList)]]"
title="$i18n{moreActions}">
</button>
diff --git a/chromium/chrome/browser/resources/settings/site_settings/site_settings_prefs_browser_proxy.js b/chromium/chrome/browser/resources/settings/site_settings/site_settings_prefs_browser_proxy.js
index 81a019ec3c4..7974cfdf2a1 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/site_settings_prefs_browser_proxy.js
+++ b/chromium/chrome/browser/resources/settings/site_settings/site_settings_prefs_browser_proxy.js
@@ -137,6 +137,13 @@ cr.define('settings', function() {
setOriginPermissions(origin, contentTypes, blanketSetting) {}
/**
+ * Clears the flag that's set when the user has changed the Flash permission
+ * for this particular origin.
+ * @param {string} origin The origin to clear the Flash preference for.
+ */
+ clearFlashPref(origin) {}
+
+ /**
* Resets the category permission for a given origin (expressed as primary
* and secondary patterns). Only use this if intending to remove an
* exception - use setOriginPermissions() for origin-scoped settings.
@@ -307,6 +314,11 @@ cr.define('settings', function() {
}
/** @override */
+ clearFlashPref(origin) {
+ chrome.send('clearFlashPref', [origin]);
+ }
+
+ /** @override */
resetCategoryPermissionForPattern(
primaryPattern, secondaryPattern, contentType, incognito) {
chrome.send(
@@ -362,12 +374,12 @@ cr.define('settings', function() {
/** @override */
setProtocolDefault(protocol, url) {
- chrome.send('setDefault', [[protocol, url]]);
+ chrome.send('setDefault', [protocol, url]);
}
/** @override */
removeProtocolHandler(protocol, url) {
- chrome.send('removeHandler', [[protocol, url]]);
+ chrome.send('removeHandler', [protocol, url]);
}
/** @override */
diff --git a/chromium/chrome/browser/resources/settings/site_settings/usb_devices.html b/chromium/chrome/browser/resources/settings/site_settings/usb_devices.html
index 0d49492c635..9b4e5e046d2 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/usb_devices.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/usb_devices.html
@@ -33,7 +33,7 @@
style$="[[computeSiteIcon(item.origin)]]"></div>
<div class="middle">[[item.origin]]</div>
- <button is="paper-icon-button-light" on-tap="showMenu_"
+ <button is="paper-icon-button-light" on-click="showMenu_"
class="icon-more-vert" title="$i18n{moreActions}">
</button>
</div>
@@ -41,7 +41,8 @@
</template>
<dialog is="cr-action-menu">
- <button id="removeButton" class="dropdown-item" on-tap="onRemoveTap_">
+ <button id="removeButton" slot="item" class="dropdown-item"
+ on-click="onRemoveTap_">
$i18n{handlerRemove}
</button>
</dialog>
diff --git a/chromium/chrome/browser/resources/settings/site_settings/zoom_levels.html b/chromium/chrome/browser/resources/settings/site_settings/zoom_levels.html
index e651cd9ad88..facdc237e3e 100644
--- a/chromium/chrome/browser/resources/settings/site_settings/zoom_levels.html
+++ b/chromium/chrome/browser/resources/settings/site_settings/zoom_levels.html
@@ -36,7 +36,7 @@
<div class="zoom-label">[[item.zoom]]</div>
<div>
<button is="paper-icon-button-light" class="icon-clear"
- on-tap="removeZoomLevel_"
+ on-click="removeZoomLevel_"
title="$i18n{siteSettingsRemoveZoomLevel}"></button>
</div>
</div>
diff --git a/chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.html b/chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.html
index c523b2f8536..37210d136c7 100644
--- a/chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.html
+++ b/chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.html
@@ -18,7 +18,7 @@
</style>
<template is="dom-if" if="[[enableSiteSettings_]]">
<div class="settings-box first" category$="[[ALL_SITES]]"
- data-route="SITE_SETTINGS_ALL" on-tap="onTapNavigate_" actionable>
+ data-route="SITE_SETTINGS_ALL" on-click="onTapNavigate_" actionable>
<iron-icon icon="settings:list"></iron-icon>
<div class="middle">$i18n{siteSettingsCategoryAllSites}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
@@ -29,7 +29,7 @@
</template>
<div id="cookies" class="settings-box two-line first"
category$="[[ContentSettingsTypes.COOKIES]]"
- data-route="SITE_SETTINGS_COOKIES" on-tap="onTapNavigate_" actionable>
+ data-route="SITE_SETTINGS_COOKIES" on-click="onTapNavigate_" actionable>
<iron-icon icon="settings:cookie"></iron-icon>
<div class="middle">
$i18n{siteSettingsCookies}
@@ -47,7 +47,8 @@
</div>
<div id="location" class="settings-box two-line"
category$="[[ContentSettingsTypes.GEOLOCATION]]"
- data-route="SITE_SETTINGS_LOCATION" on-tap="onTapNavigate_" actionable>
+ data-route="SITE_SETTINGS_LOCATION" on-click="onTapNavigate_"
+ actionable>
<iron-icon icon="settings:location-on"></iron-icon>
<div class="middle">
$i18n{siteSettingsLocation}
@@ -65,7 +66,7 @@
<div id="camera" class="settings-box two-line"
category$="[[ContentSettingsTypes.CAMERA]]"
data-route="SITE_SETTINGS_CAMERA"
- on-tap="onTapNavigate_" actionable>
+ on-click="onTapNavigate_" actionable>
<iron-icon icon="settings:videocam"></iron-icon>
<div class="middle">
$i18n{siteSettingsCamera}
@@ -82,7 +83,7 @@
</div>
<div id="microphone" class="settings-box two-line"
category$="[[ContentSettingsTypes.MIC]]"
- data-route="SITE_SETTINGS_MICROPHONE" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_MICROPHONE" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:mic"></iron-icon>
<div class="middle">
@@ -98,9 +99,29 @@
aria-label="$i18n{siteSettingsMic}"
aria-describedby="micSecondary"></button>
</div>
+ <template is="dom-if" if="[[enableSensorsContentSetting_]]">
+ <div id="sensors" class="settings-box two-line"
+ category$="[[ContentSettingsTypes.SENSORS]]"
+ data-route="SITE_SETTINGS_SENSORS" on-click="onTapNavigate_"
+ actionable>
+ <iron-icon icon="settings:sensors"></iron-icon>
+ <div class="middle">
+ $i18n{siteSettingsSensors}
+ <div class="secondary" id="sensorsSecondary">
+ [[defaultSettingLabel_(
+ default_.sensors,
+ '$i18nPolymer{siteSettingsSensorsAllow}',
+ '$i18nPolymer{siteSettingsSensorsBlock}')]]
+ </div>
+ </div>
+ <button class="subpage-arrow" is="paper-icon-button-light"
+ aria-label="$i18n{siteSettingsSensors}"
+ aria-describedby="sensorsSecondary"></button>
+ </div>
+ </template>
<div id="notifications" class="settings-box two-line"
category$="[[ContentSettingsTypes.NOTIFICATIONS]]"
- data-route="SITE_SETTINGS_NOTIFICATIONS" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_NOTIFICATIONS" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:notifications"></iron-icon>
<div class="middle">
@@ -118,7 +139,7 @@
</div>
<div id="javascript" class="settings-box two-line"
category$="[[ContentSettingsTypes.JAVASCRIPT]]"
- data-route="SITE_SETTINGS_JAVASCRIPT" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_JAVASCRIPT" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:code"></iron-icon>
<div class="middle">
@@ -136,7 +157,7 @@
</div>
<div id="flash" class="settings-box two-line"
category$="[[ContentSettingsTypes.PLUGINS]]"
- data-route="SITE_SETTINGS_FLASH" on-tap="onTapNavigate_" actionable>
+ data-route="SITE_SETTINGS_FLASH" on-click="onTapNavigate_" actionable>
<iron-icon icon="cr:extension"></iron-icon>
<div class="middle">
$i18n{siteSettingsFlash}
@@ -153,7 +174,7 @@
</div>
<div id="images" class="settings-box two-line"
category$="[[ContentSettingsTypes.IMAGES]]"
- data-route="SITE_SETTINGS_IMAGES" on-tap="onTapNavigate_" actionable>
+ data-route="SITE_SETTINGS_IMAGES" on-click="onTapNavigate_" actionable>
<iron-icon icon="settings:photo"></iron-icon>
<div class="middle">
$i18n{siteSettingsImages}
@@ -170,7 +191,7 @@
</div>
<div id="popups" category$="[[ContentSettingsTypes.POPUPS]]"
class="settings-box two-line" data-route="SITE_SETTINGS_POPUPS"
- on-tap="onTapNavigate_" actionable>
+ on-click="onTapNavigate_" actionable>
<iron-icon icon="cr:open-in-new"></iron-icon>
<div class="middle">
$i18n{siteSettingsPopups}
@@ -188,7 +209,7 @@
<template is="dom-if" if="[[enableSafeBrowsingSubresourceFilter_]]">
<div id="ads" class="settings-box two-line"
category$="[[ContentSettingsTypes.ADS]]"
- data-route="SITE_SETTINGS_ADS" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_ADS" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:ads"></iron-icon>
<div class="middle">
@@ -207,7 +228,7 @@
</template>
<div id="background-sync" class="settings-box two-line"
category$="[[ContentSettingsTypes.BACKGROUND_SYNC]]"
- data-route="SITE_SETTINGS_BACKGROUND_SYNC" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_BACKGROUND_SYNC" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:sync"></iron-icon>
<div class="middle">
@@ -226,7 +247,7 @@
<template is="dom-if" if="[[enableSoundContentSetting_]]">
<div id="sound" class="settings-box two-line"
category$="[[ContentSettingsTypes.SOUND]]"
- data-route="SITE_SETTINGS_SOUND" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_SOUND" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:volume-up"></iron-icon>
<div class="middle">
@@ -246,7 +267,7 @@
<div id="automatic-downloads" class="settings-box two-line"
category$="[[ContentSettingsTypes.AUTOMATIC_DOWNLOADS]]"
data-route="SITE_SETTINGS_AUTOMATIC_DOWNLOADS"
- on-tap="onTapNavigate_" actionable>
+ on-click="onTapNavigate_" actionable>
<iron-icon icon="cr:file-download"></iron-icon>
<div class="middle">
$i18n{siteSettingsAutomaticDownloads}
@@ -264,7 +285,7 @@
<div id="unsandboxed-plugins" class="settings-box two-line"
category$="[[ContentSettingsTypes.UNSANDBOXED_PLUGINS]]"
data-route="SITE_SETTINGS_UNSANDBOXED_PLUGINS"
- on-tap="onTapNavigate_" actionable>
+ on-click="onTapNavigate_" actionable>
<iron-icon icon="cr:extension"></iron-icon>
<div class="middle">
$i18n{siteSettingsUnsandboxedPlugins}
@@ -283,7 +304,7 @@
<div id="protocol-handlers" class="settings-box two-line"
category$="[[ContentSettingsTypes.PROTOCOL_HANDLERS]]"
data-route="SITE_SETTINGS_HANDLERS"
- on-tap="onTapNavigate_" actionable>
+ on-click="onTapNavigate_" actionable>
<iron-icon icon="settings:protocol-handler"></iron-icon>
<div class="middle">
$i18n{siteSettingsHandlers}
@@ -302,7 +323,7 @@
<div id="midi-devices" class="settings-box two-line"
category$="[[ContentSettingsTypes.MIDI_DEVICES]]"
data-route="SITE_SETTINGS_MIDI_DEVICES"
- on-tap="onTapNavigate_" actionable>
+ on-click="onTapNavigate_" actionable>
<iron-icon icon="settings:midi"></iron-icon>
<div class="middle">
$i18n{siteSettingsMidiDevices}
@@ -317,29 +338,9 @@
aria-label="$i18n{siteSettingsMidiDevices}"
aria-describedby="midiDevicesSecondary"></button>
</div>
- <template is="dom-if" if="[[enableClipboardContentSetting_]]">
- <div id="clipboard" class="settings-box two-line"
- category$="[[ContentSettingsTypes.CLIPBOARD]]"
- data-route="SITE_SETTINGS_CLIPBOARD" on-tap="onTapNavigate_"
- actionable>
- <iron-icon icon="settings:clipboard"></iron-icon>
- <div class="middle">
- $i18n{siteSettingsClipboard}
- <div class="secondary" id="clipboardSecondary">
- [[defaultSettingLabel_(
- default_.clipboard,
- '$i18nPolymer{siteSettingsAskBeforeAccessing}',
- '$i18nPolymer{siteSettingsBlocked}')]]
- </div>
- </div>
- <button class="subpage-arrow" is="paper-icon-button-light"
- aria-label="$i18n{siteSettingsClipboard}"
- aria-describedby="clipboardSecondary"></button>
- </div>
- </template>
<div id="zoom-levels" class="settings-box"
category$="[[ContentSettingsTypes.ZOOM_LEVELS]]"
- data-route="SITE_SETTINGS_ZOOM_LEVELS" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_ZOOM_LEVELS" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:zoom-in"></iron-icon>
<div class="middle">$i18n{siteSettingsZoomLevels}</div>
@@ -348,7 +349,7 @@
</div>
<div id="usb-devices" class="settings-box"
category$="[[ContentSettingsTypes.USB_DEVICES]]"
- data-route="SITE_SETTINGS_USB_DEVICES" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_USB_DEVICES" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:usb"></iron-icon>
<div class="middle">$i18n{siteSettingsUsbDevices}</div>
@@ -356,7 +357,7 @@
aria-label="$i18n{siteSettingsUsbDevices}"></button>
</div>
<div id="pdf-documents" class="settings-box"
- data-route="SITE_SETTINGS_PDF_DOCUMENTS" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_PDF_DOCUMENTS" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:pdf"></iron-icon>
<div class="middle">$i18n{siteSettingsPdfDocuments}</div>
@@ -364,13 +365,33 @@
aria-label="$i18n{siteSettingsPdfDocuments}"></button>
</div>
<div id="protected-content" class="settings-box"
- data-route="SITE_SETTINGS_PROTECTED_CONTENT" on-tap="onTapNavigate_"
+ data-route="SITE_SETTINGS_PROTECTED_CONTENT" on-click="onTapNavigate_"
actionable>
<iron-icon icon="settings:security"></iron-icon>
<div class="middle">$i18n{siteSettingsProtectedContent}</div>
<button class="subpage-arrow" is="paper-icon-button-light"
aria-label="$i18n{siteSettingsProtectedContent}"></button>
</div>
+ <template is="dom-if" if="[[enableClipboardContentSetting_]]">
+ <div id="clipboard" class="settings-box two-line"
+ category$="[[ContentSettingsTypes.CLIPBOARD]]"
+ data-route="SITE_SETTINGS_CLIPBOARD" on-click="onTapNavigate_"
+ actionable>
+ <iron-icon icon="settings:clipboard"></iron-icon>
+ <div class="middle">
+ $i18n{siteSettingsClipboard}
+ <div class="secondary" id="clipboardSecondary">
+ [[defaultSettingLabel_(
+ default_.clipboard,
+ '$i18nPolymer{siteSettingsAskBeforeAccessing}',
+ '$i18nPolymer{siteSettingsBlocked}')]]
+ </div>
+ </div>
+ <button class="subpage-arrow" is="paper-icon-button-light"
+ aria-label="$i18n{siteSettingsClipboard}"
+ aria-describedby="clipboardSecondary"></button>
+ </div>
+ </template>
</template>
<script src="site_settings_page.js"></script>
</dom-module>
diff --git a/chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.js b/chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.js
index 0b1ce9dc1de..5a3b98d12af 100644
--- a/chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.js
+++ b/chromium/chrome/browser/resources/settings/site_settings_page/site_settings_page.js
@@ -67,6 +67,15 @@ Polymer({
}
},
+ /** @private */
+ enableSensorsContentSetting_: {
+ type: Boolean,
+ readOnly: true,
+ value: function() {
+ return loadTimeData.getBoolean('enableSensorsContentSetting');
+ }
+ },
+
/** @type {!Map<string, string>} */
focusConfig: {
type: Object,
@@ -105,6 +114,7 @@ Polymer({
[R.SITE_SETTINGS_PDF_DOCUMENTS, 'pdf-documents'],
[R.SITE_SETTINGS_PROTECTED_CONTENT, 'protected-content'],
[R.SITE_SETTINGS_CLIPBOARD, 'clipboard'],
+ [R.SITE_SETTINGS_SENSORS, 'sensors'],
].forEach(pair => {
const route = pair[0];
const id = pair[1];
diff --git a/chromium/chrome/browser/resources/settings/system_page/system_page.html b/chromium/chrome/browser/resources/settings/system_page/system_page.html
index 77bc47816f3..fb4ed5031ee 100644
--- a/chromium/chrome/browser/resources/settings/system_page/system_page.html
+++ b/chromium/chrome/browser/resources/settings/system_page/system_page.html
@@ -29,7 +29,7 @@
</paper-button>
</template>
</settings-toggle-button>
- <div id="proxy" class="settings-box" on-tap="onProxyTap_"
+ <div id="proxy" class="settings-box" on-click="onProxyTap_"
actionable$="[[!isProxyEnforcedByPolicy_]]">
<div class="start">$i18n{proxySettingsLabel}</div>
<button is="paper-icon-button-light" class="icon-external"
diff --git a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google.png b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google.png
index 3c38ca006b5..f7602a57b40 100644
--- a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google.png
+++ b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google_2x.png b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google_2x.png
index 3634aa48a93..76d098584c9 100644
--- a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google_2x.png
+++ b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/images/ic_google_2x.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.html b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.html
index 41dac3c5e3b..644c00a0c8f 100644
--- a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.html
+++ b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.html
@@ -81,9 +81,9 @@
}
#personalize-logo {
- fill: var(--google-blue-700);
/* Need the following rules to adjust for white spacing in the svg. */
-webkit-margin-end: 14px;
+ fill: var(--google-blue-700);
height: 18px;
width: 18px;
}
@@ -107,37 +107,52 @@
url(./images/ic_google_2x.png) 2x);
}
</style>
+
+ <!--
+ Use the 'consent-description' attribute to annotate all the UI elements
+ that are part of the text the user reads before consenting to the Sync
+ data collection . Similarly, use 'consent-confirmation' on UI elements on
+ which user clicks to indicate consent.
+ -->
+
<div id="illustration-container"></div>
- <h1 id="heading">$i18n{syncConfirmationTitle}</h1>
+ <h1 id="heading" consent-description>$i18n{syncConfirmationTitle}</h1>
<div class="message-container">
<!-- Container needed to contain the icon in a green circle. -->
<div id="sync-logo-container" class="logo">
<iron-icon icon="notification:sync" class="logo">
</iron-icon>
</div>
- <div>$i18n{syncConfirmationChromeSyncBody}</div>
+ <div consent-description>$i18n{syncConfirmationChromeSyncBody}</div>
</div>
<div class="message-container">
<iron-icon icon="image:assistant" id="personalize-logo" class="logo">
</iron-icon>
- <div>$i18n{syncConfirmationPersonalizeServicesBody}</div>
+ <div consent-description>
+ $i18n{syncConfirmationPersonalizeServicesBody}
+ </div>
</div>
<div class="message-container">
<div id="googleg-logo" class="logo"></div>
- <div>$i18n{syncConfirmationGoogleServicesBody}</div>
+ <div consent-description>$i18n{syncConfirmationGoogleServicesBody}</div>
</div>
<div class="footer">
<div class="message-container">
<iron-icon icon="icons:settings" class="logo"></iron-icon>
- <div>$i18nRaw{syncConfirmationSyncSettingsLinkBody}</div>
+ <div consent-description consent-confirmation>
+ $i18nRaw{syncConfirmationSyncSettingsLinkBody}
+ </div>
</div>
<div class="message-container">
<div class="logo"><!-- Spacer to line up with other texts --></div>
- <div>$i18n{syncConfirmationSyncSettingsDescription}</div>
+ <div consent-description>
+ $i18n{syncConfirmationSyncSettingsDescription}
+ </div>
</div>
</div>
<div class="action-container">
- <paper-button class="primary-action" id="confirmButton" on-tap="onConfirm_">
+ <paper-button class="primary-action" id="confirmButton"
+ on-tap="onConfirm_" consent-confirmation>
$i18n{syncConfirmationConfirmLabel}
</paper-button>
<paper-button class="secondary-action" id="undoButton" on-tap="onUndo_">
diff --git a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.js b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.js
index bb45993114c..fe19b8f3dd4 100644
--- a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.js
+++ b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_app.js
@@ -33,8 +33,9 @@ Polymer({
},
/** @private */
- onConfirm_: function() {
- this.syncConfirmationBrowserProxy_.confirm();
+ onConfirm_: function(e) {
+ this.syncConfirmationBrowserProxy_.confirm(
+ this.getConsentDescription_(), this.getConsentConfirmation_(e.path));
},
/** @private */
@@ -43,15 +44,41 @@ Polymer({
},
/** @private */
- onGoToSettings_: function() {
- this.syncConfirmationBrowserProxy_.goToSettings();
+ onGoToSettings_: function(e) {
+ this.syncConfirmationBrowserProxy_.goToSettings(
+ this.getConsentDescription_(), this.getConsentConfirmation_(e.path));
},
/** @private */
onKeyDown_: function(e) {
if (e.key == 'Enter' && !/^(A|PAPER-BUTTON)$/.test(e.path[0].tagName)) {
- this.onConfirm_();
+ this.onConfirm_(e);
e.preventDefault();
}
},
+
+ /**
+ * @param {!Array<!HTMLElement>} path Path of the click event. Must contain
+ * a consent confirmation element.
+ * @return {string} The text of the consent confirmation element.
+ * @private
+ */
+ getConsentConfirmation_: function(path) {
+ for (var element of path) {
+ if (element.hasAttribute('consent-confirmation'))
+ return element.innerHTML.trim();
+ }
+ assertNotReached('No consent confirmation element found.');
+ return '';
+ },
+
+ /** @return {!Array<string>} Text of the consent description elements. */
+ getConsentDescription_: function() {
+ var consentDescription =
+ Array.from(this.shadowRoot.querySelectorAll('[consent-description]'))
+ .filter(element => element.clientWidth * element.clientHeight > 0)
+ .map(element => element.innerHTML.trim());
+ assert(consentDescription);
+ return consentDescription;
+ }
});
diff --git a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_browser_proxy.js b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_browser_proxy.js
index 304c3024888..f6d3b1096be 100644
--- a/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_browser_proxy.js
+++ b/chromium/chrome/browser/resources/signin/dice_sync_confirmation/sync_confirmation_browser_proxy.js
@@ -11,9 +11,27 @@ cr.define('sync.confirmation', function() {
/** @interface */
class SyncConfirmationBrowserProxy {
- confirm() {}
+ /**
+ * Called when the user confirms the Sync Confirmation dialog.
+ * @param {!Array<string>} description Strings that the user was presented
+ * with in the UI.
+ * @param {string} confirmation Text of the element that the user
+ * clicked on.
+ */
+ confirm(description, confirmation) {}
+
+ /** Called when the user undoes the Sync confirmation. */
undo() {}
- goToSettings() {}
+
+ /**
+ * Called when the user clicks on the Settings link in
+ * the Sync Confirmation dialog.
+ * @param {!Array<string>} description Strings that the user was presented
+ * with in the UI.
+ * @param {string} confirmation Text of the element that the user
+ * clicked on.
+ */
+ goToSettings(description, confirmation) {}
/** @param {!Array<number>} height */
initializedWithSize(height) {}
@@ -22,8 +40,8 @@ cr.define('sync.confirmation', function() {
/** @implements {sync.confirmation.SyncConfirmationBrowserProxy} */
class SyncConfirmationBrowserProxyImpl {
/** @override */
- confirm() {
- chrome.send('confirm');
+ confirm(description, confirmation) {
+ chrome.send('confirm', [description, confirmation]);
}
/** @override */
@@ -32,8 +50,8 @@ cr.define('sync.confirmation', function() {
}
/** @override */
- goToSettings() {
- chrome.send('goToSettings');
+ goToSettings(description, confirmation) {
+ chrome.send('goToSettings', [description, confirmation]);
}
/** @override */
diff --git a/chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.html b/chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.html
index 99a1e1122d7..74acb08a2dd 100644
--- a/chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.html
+++ b/chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.html
@@ -18,8 +18,16 @@
</style>
</head>
<body>
+ <!--
+ Use the 'consent-description' attribute to annotate all the UI elements
+ that are part of the text the user reads before consenting to the Sync
+ data collection . Similarly, use 'consent-confirmation' on UI elements on
+ which user clicks to indicate consent.
+ -->
<div class="container">
- <div class="top-title-bar">$i18n{syncConfirmationTitle}</div>
+ <div class="top-title-bar" consent-description>
+ $i18n{syncConfirmationTitle}
+ </div>
<div class="details" id="syncConfirmationDetails">
<div id="picture-container">
<div id="illustration">
@@ -55,8 +63,12 @@
-->
<div id="chrome-logo" class="logo"></div>
<div>
- <div class="title">$i18n{syncConfirmationChromeSyncTitle}</div>
- <div class="body text">$i18n{syncConfirmationChromeSyncBody}</div>
+ <div class="title" consent-description>
+ $i18n{syncConfirmationChromeSyncTitle}
+ </div>
+ <div class="body text" consent-description>
+ $i18n{syncConfirmationChromeSyncBody}
+ </div>
</div>
</div>
<div class="message-container">
@@ -66,23 +78,28 @@
-->
<div id="googleg-logo" class="logo"></div>
<div>
- <div class="title">
+ <div class="title" consent-description>
$i18n{syncConfirmationPersonalizeServicesTitle}
</div>
- <div class="body text">
+ <div class="body text" consent-description>
$i18n{syncConfirmationPersonalizeServicesBody}
</div>
</div>
</div>
<div class="message-container">
- <div class="body">$i18nRaw{syncConfirmationSyncSettingsLinkBody}</div>
+ <div class="body" consent-description consent-confirmation>
+ $i18nRaw{syncConfirmationSyncSettingsLinkBody}
+ </div>
</div>
</div>
<div class="details" id="syncDisabledDetails">
- <div class="body text">$i18n{syncDisabledConfirmationDetails}</div>
+ <div class="body text" consent-description>
+ $i18n{syncDisabledConfirmationDetails}
+ </div>
</div>
<div class="action-container">
- <paper-button class="primary-action" id="confirmButton">
+ <paper-button class="primary-action" id="confirmButton"
+ consent-confirmation>
$i18n{syncConfirmationConfirmLabel}
</paper-button>
<paper-button class="secondary-action" id="undoButton">
diff --git a/chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.js b/chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.js
index 17eb3bfa71c..72d29205331 100644
--- a/chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.js
+++ b/chromium/chrome/browser/resources/signin/sync_confirmation/sync_confirmation.js
@@ -5,8 +5,35 @@
cr.define('sync.confirmation', function() {
'use strict';
+ /**
+ * @param {!Array<!HTMLElement>} path Path of the click event. Must contain
+ * a consent confirmation element.
+ * @return {string} The text of the consent confirmation element.
+ * @private
+ */
+ function getConsentConfirmation(path) {
+ var consentConfirmation;
+ for (var element of path) {
+ if (element.hasAttribute('consent-confirmation'))
+ return element.innerHTML.trim();
+ }
+ assertNotReached('No consent confirmation element found.');
+ return '';
+ }
+
+ /** @return {!Array<string>} Text of the consent description elements. */
+ function getConsentDescription() {
+ var consentDescription =
+ Array.from(document.querySelectorAll('[consent-description]'))
+ .filter(element => element.clientWidth * element.clientHeight > 0)
+ .map(element => element.innerHTML.trim());
+ assert(consentDescription);
+ return consentDescription;
+ }
+
function onConfirm(e) {
- chrome.send('confirm');
+ chrome.send(
+ 'confirm', [getConsentDescription(), getConsentConfirmation(e.path)]);
}
function onUndo(e) {
@@ -14,7 +41,9 @@ cr.define('sync.confirmation', function() {
}
function onGoToSettings(e) {
- chrome.send('goToSettings');
+ chrome.send(
+ 'goToSettings',
+ [getConsentDescription(), getConsentConfirmation(e.path)]);
}
function initialize() {
diff --git a/chromium/chrome/browser/resources/snippets_internals.html b/chromium/chrome/browser/resources/snippets_internals.html
index 27ee59e3a95..6b3cb663e44 100644
--- a/chromium/chrome/browser/resources/snippets_internals.html
+++ b/chromium/chrome/browser/resources/snippets_internals.html
@@ -90,11 +90,10 @@ found in the LICENSE file.
</div>
<div id="snippets">
- <h2>NTPSnippetsService</h2>
+ <h2>ContentSuggestionsService</h2>
<div class="forms">
<div>
- <button id="submit-download" type="button">Add snippets</button>
- <span id="remote-status" class="detail"></span>
+ <button id="submit-download" type="button">Reload suggestions</button>
</div>
<div>
<button id="debug-log-dump" type="button">Dump the debug log</button>
@@ -107,27 +106,36 @@ found in the LICENSE file.
</div>
</div>
- <div id="last-json" class="hidden">
- <h2>Last JSON from Server</h2>
- <a id="last-json-button">Show the last JSON &gt;&gt;</a>
- <div id="last-json-container" class="hidden">
- <div id="last-json-text"></div>
- <button id="last-json-dump" type="button">Dump the last JSON</button>
- </div>
- </div>
-
<div id="remote-content-suggestions">
<h2>Remote content suggestions</h2>
+ <table class="section-details">
+ <tr>
+ <td class="name">Last Fetch Status
+ <td id="remote-status" class="value">
+ <tr>
+ <td class="name">Last Fetch Type
+ <td id="remote-authenticated" class="value">
+ <tr>
+ <td class="name">Last Background Fetch Time:
+ <td id="last-background-fetch-time-label" class="value">
+ </table>
<div>
- <span>Last Background Fetch Time: </span>
- <span id="last-background-fetch-time-label"></span>
+ <button id="background-fetch-button" type="button">
+ Fetch remote suggestions in the background in 2 seconds
+ </button>
+ </div>
+ <div>
+ <button id="push-dummy-suggestion-10-seconds-button" type="button">
+ Push dummy suggestion in 10 seconds
+ </button>
+ </div>
+ <div>
+ <button id="last-json-button" type="button">Show the last JSON</button>
+ </div>
+ <div id="last-json-container" class="hidden">
+ <div id="last-json-text"></div>
+ <button id="last-json-dump" type="button">Dump the last JSON</button>
</div>
- <button id="background-fetch-button" type="button">
- Fetch remote suggestions in the background in 2 seconds
- </button>
- <button id="push-dummy-suggestion-10-seconds-button" type="button">
- Push dummy suggestion in 10 seconds
- </button>
</div>
<div id="notifications">
diff --git a/chromium/chrome/browser/resources/vr/assets/PRESUBMIT.py b/chromium/chrome/browser/resources/vr/assets/PRESUBMIT.py
index 5d740312570..c290f6d7b3d 100644
--- a/chromium/chrome/browser/resources/vr/assets/PRESUBMIT.py
+++ b/chromium/chrome/browser/resources/vr/assets/PRESUBMIT.py
@@ -12,10 +12,11 @@ def IsNewer(old_version, new_version):
old_version.minor < new_version.minor)))
-def CheckVersion(input_api, output_api):
+def CheckVersionAndAssetParity(input_api, output_api):
"""Checks that
- the version was upraded if assets files were changed,
- - the version was not downgraded.
+ - the version was not downgraded,
+ - both the google_chrome and the chromium assets have the same files.
"""
sys.path.append(input_api.PresubmitLocalPath())
import parse_version
@@ -24,9 +25,21 @@ def CheckVersion(input_api, output_api):
new_version = None
changed_assets = False
changed_version = False
+ changed_asset_files = {'google_chrome': [], 'chromium': []}
for file in input_api.AffectedFiles():
basename = input_api.os_path.basename(file.LocalPath())
extension = input_api.os_path.splitext(basename)[1][1:].strip().lower()
+ basename_without_extension = input_api.os_path.splitext(basename)[
+ 0].strip().lower()
+ if extension == 'sha1':
+ basename_without_extension = input_api.os_path.splitext(
+ basename_without_extension)[0]
+ dirname = input_api.os_path.basename(
+ input_api.os_path.dirname(file.LocalPath()))
+ action = file.Action()
+ if (dirname in changed_asset_files and extension in {'sha1', 'png'} and
+ action in {'A', 'D'}):
+ changed_asset_files[dirname].append((action, basename_without_extension))
if (extension == 'sha1' or basename == 'vr_assets_component_files.json'):
changed_assets = True
if (basename == 'VERSION'):
@@ -38,6 +51,15 @@ def CheckVersion(input_api, output_api):
input_api.os_path.dirname(input_api.AffectedFiles()[0].LocalPath()),
'VERSION')
+ if changed_asset_files['google_chrome'] != changed_asset_files['chromium']:
+ return [
+ output_api.PresubmitError(
+ 'Must have same asset files for %s in \'%s\'.' %
+ (changed_asset_files.keys(),
+ input_api.os_path.dirname(
+ input_api.AffectedFiles()[0].LocalPath())))
+ ]
+
if changed_version and (not old_version or not new_version):
return [
output_api.PresubmitError(
@@ -61,8 +83,8 @@ def CheckVersion(input_api, output_api):
def CheckChangeOnUpload(input_api, output_api):
- return CheckVersion(input_api, output_api)
+ return CheckVersionAndAssetParity(input_api, output_api)
def CheckChangeOnCommit(input_api, output_api):
- return CheckVersion(input_api, output_api)
+ return CheckVersionAndAssetParity(input_api, output_api)
diff --git a/chromium/chrome/browser/resources/vr/assets/VERSION b/chromium/chrome/browser/resources/vr/assets/VERSION
index 1dea3031bc3..75fb8d39112 100644
--- a/chromium/chrome/browser/resources/vr/assets/VERSION
+++ b/chromium/chrome/browser/resources/vr/assets/VERSION
@@ -1,2 +1,2 @@
MAJOR=1
-MINOR=2 \ No newline at end of file
+MINOR=3 \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/vr/assets/chromium/background.png b/chromium/chrome/browser/resources/vr/assets/chromium/background.png
new file mode 100644
index 00000000000..0fb3bfc35fc
--- /dev/null
+++ b/chromium/chrome/browser/resources/vr/assets/chromium/background.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/vr/assets/chromium/fullscreen_gradient.png b/chromium/chrome/browser/resources/vr/assets/chromium/fullscreen_gradient.png
new file mode 100644
index 00000000000..04c5d88e646
--- /dev/null
+++ b/chromium/chrome/browser/resources/vr/assets/chromium/fullscreen_gradient.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/vr/assets/chromium/incognito_gradient.png b/chromium/chrome/browser/resources/vr/assets/chromium/incognito_gradient.png
new file mode 100644
index 00000000000..4fb17499405
--- /dev/null
+++ b/chromium/chrome/browser/resources/vr/assets/chromium/incognito_gradient.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/vr/assets/chromium/normal_gradient.png b/chromium/chrome/browser/resources/vr/assets/chromium/normal_gradient.png
new file mode 100644
index 00000000000..0fb3bfc35fc
--- /dev/null
+++ b/chromium/chrome/browser/resources/vr/assets/chromium/normal_gradient.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/vr/assets/fullscreen_gradient.png.sha1 b/chromium/chrome/browser/resources/vr/assets/fullscreen_gradient.png.sha1
deleted file mode 100644
index 8286e261102..00000000000
--- a/chromium/chrome/browser/resources/vr/assets/fullscreen_gradient.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-4945728b62a01aa712b5b0bacbf10d5a1a95bd80 \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/vr/assets/background.png.sha1 b/chromium/chrome/browser/resources/vr/assets/google_chrome/background.png.sha1
index d3427cb5c7b..d3427cb5c7b 100644
--- a/chromium/chrome/browser/resources/vr/assets/background.png.sha1
+++ b/chromium/chrome/browser/resources/vr/assets/google_chrome/background.png.sha1
diff --git a/chromium/chrome/browser/resources/vr/assets/google_chrome/fullscreen_gradient.png.sha1 b/chromium/chrome/browser/resources/vr/assets/google_chrome/fullscreen_gradient.png.sha1
new file mode 100644
index 00000000000..201ca21adc8
--- /dev/null
+++ b/chromium/chrome/browser/resources/vr/assets/google_chrome/fullscreen_gradient.png.sha1
@@ -0,0 +1 @@
+be61cbcdc981d5592455d3b7a14b97e14d3acac4 \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/vr/assets/google_chrome/incognito_gradient.png.sha1 b/chromium/chrome/browser/resources/vr/assets/google_chrome/incognito_gradient.png.sha1
new file mode 100644
index 00000000000..1282cf18759
--- /dev/null
+++ b/chromium/chrome/browser/resources/vr/assets/google_chrome/incognito_gradient.png.sha1
@@ -0,0 +1 @@
+6b0e506b19b79ec1be4963a9bd92ab8b92ccca1c \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/vr/assets/google_chrome/normal_gradient.png.sha1 b/chromium/chrome/browser/resources/vr/assets/google_chrome/normal_gradient.png.sha1
new file mode 100644
index 00000000000..30a79ad3c93
--- /dev/null
+++ b/chromium/chrome/browser/resources/vr/assets/google_chrome/normal_gradient.png.sha1
@@ -0,0 +1 @@
+bbe1145ae7778d7d50b02c54fd1bf4b62bbc04db \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/vr/assets/incognito_gradient.png.sha1 b/chromium/chrome/browser/resources/vr/assets/incognito_gradient.png.sha1
deleted file mode 100644
index 88f59906b07..00000000000
--- a/chromium/chrome/browser/resources/vr/assets/incognito_gradient.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-a80f18117d1e8820404d50cdf49ba6bf6c3c69f0 \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/vr/assets/normal_gradient.png.sha1 b/chromium/chrome/browser/resources/vr/assets/normal_gradient.png.sha1
deleted file mode 100644
index 425681fc101..00000000000
--- a/chromium/chrome/browser/resources/vr/assets/normal_gradient.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-9888994181e2fa6cf6b510c02f21e183ad8efc46 \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/vr/assets/push_assets_component.py b/chromium/chrome/browser/resources/vr/assets/push_assets_component.py
index b259a6da904..b47f275ad0d 100755
--- a/chromium/chrome/browser/resources/vr/assets/push_assets_component.py
+++ b/chromium/chrome/browser/resources/vr/assets/push_assets_component.py
@@ -42,8 +42,8 @@ def main():
assets_dir = os.path.dirname(os.path.abspath(__file__))
files = []
- with open(
- os.path.join(assets_dir, 'vr_assets_component_files.json')) as json_file:
+ with open(os.path.join(assets_dir,
+ 'vr_assets_component_files.json')) as json_file:
files = json.load(json_file)
version = None
@@ -61,16 +61,20 @@ def main():
zip_path = os.path.join(zip_dir, 'vr-assets.zip')
os.makedirs(zip_dir)
+ zip_files = []
with zipfile.ZipFile(zip_path, 'w') as zip:
for file in files:
file_path = os.path.join(assets_dir, file)
zip.write(file_path, os.path.basename(file_path), zipfile.ZIP_DEFLATED)
+ for info in zip.infolist():
+ zip_files.append(info.filename)
# Upload component.
command = ['gsutil', 'cp', '-nR', '.', DEST_BUCKET]
PrintInfo('Going to run the following command', [' '.join(command)])
PrintInfo('In directory', [temp_dir])
PrintInfo('Which pushes the following file', [zip_path])
+ PrintInfo('Which contains the files', zip_files)
if raw_input('\nAre you sure (y/N) ').lower() != 'y':
print 'aborting'
diff --git a/chromium/chrome/browser/resources/vr/assets/vr_assets_component_files.json b/chromium/chrome/browser/resources/vr/assets/vr_assets_component_files.json
index 7601c295253..f58922a30a3 100644
--- a/chromium/chrome/browser/resources/vr/assets/vr_assets_component_files.json
+++ b/chromium/chrome/browser/resources/vr/assets/vr_assets_component_files.json
@@ -1,6 +1,6 @@
[
- "background.png",
- "fullscreen_gradient.png",
- "incognito_gradient.png",
- "normal_gradient.png"
+ "google_chrome/background.png",
+ "google_chrome/fullscreen_gradient.png",
+ "google_chrome/incognito_gradient.png",
+ "google_chrome/normal_gradient.png"
] \ No newline at end of file
diff --git a/chromium/chrome/browser/resources/vr_shell/OWNERS b/chromium/chrome/browser/resources/vr_shell/OWNERS
deleted file mode 100644
index 0fa23cfe0db..00000000000
--- a/chromium/chrome/browser/resources/vr_shell/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-file://chrome/browser/android/vr_shell/OWNERS
-
-# COMPONENT: UI>Browser>VR
diff --git a/chromium/chrome/browser/resources/vr_shell/ddcontroller.glb b/chromium/chrome/browser/resources/vr_shell/ddcontroller.glb
deleted file mode 100644
index fa28d6b1962..00000000000
--- a/chromium/chrome/browser/resources/vr_shell/ddcontroller.glb
+++ /dev/null
Binary files differ
diff --git a/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_app.png b/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_app.png
deleted file mode 100644
index 77594782ad3..00000000000
--- a/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_app.png
+++ /dev/null
Binary files differ
diff --git a/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_idle.png b/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_idle.png
deleted file mode 100644
index 5928f21896c..00000000000
--- a/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_idle.png
+++ /dev/null
Binary files differ
diff --git a/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_system.png b/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_system.png
deleted file mode 100644
index d4bae158510..00000000000
--- a/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_system.png
+++ /dev/null
Binary files differ
diff --git a/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_touchpad.png b/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_touchpad.png
deleted file mode 100644
index 7be802ee886..00000000000
--- a/chromium/chrome/browser/resources/vr_shell/tex/ddcontroller_touchpad.png
+++ /dev/null
Binary files differ
diff --git a/chromium/chrome/browser/resources/vr_shell_resources.grd b/chromium/chrome/browser/resources/vr_shell_resources.grd
deleted file mode 100644
index 06de759f2f6..00000000000
--- a/chromium/chrome/browser/resources/vr_shell_resources.grd
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<grit latest_public_release="0" current_release="1" output_all_resource_defines="false">
- <outputs>
- <output filename="grit/vr_shell_resources.h" type="rc_header">
- <emit emit_type='prepend'></emit>
- </output>
- <output filename="vr_shell_resources.pak" type="data_package" />
- </outputs>
- <release seq="1">
- <includes>
- <!-- TODO(vollick): add conditionals for test-specific generic assets (see crbug.com/743687) -->
- <include name="IDR_VR_SHELL_DDCONTROLLER_MODEL" file="vr_shell\ddcontroller.glb" type="BINDATA" />
- <include name="IDR_VR_SHELL_DDCONTROLLER_IDLE_TEXTURE" file="vr_shell\tex\ddcontroller_idle.png" type="BINDATA" />
- <include name="IDR_VR_SHELL_DDCONTROLLER_APP_PATCH" file="vr_shell\tex\ddcontroller_app.png" type="BINDATA" />
- <include name="IDR_VR_SHELL_DDCONTROLLER_TOUCHPAD_PATCH" file="vr_shell\tex\ddcontroller_touchpad.png" type="BINDATA" />
- <include name="IDR_VR_SHELL_DDCONTROLLER_SYSTEM_PATCH" file="vr_shell\tex\ddcontroller_system.png" type="BINDATA" />
- </includes>
- </release>
-</grit>