| // 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 "chrome/browser/win/settings_app_monitor.h" |
| |
| #include <wrl/client.h> |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/location.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/strings/pattern.h" |
| #include "base/strings/string16.h" |
| #include "base/synchronization/lock.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "base/win/scoped_variant.h" |
| #include "chrome/browser/win/automation_controller.h" |
| #include "chrome/browser/win/ui_automation_util.h" |
| #include "chrome/common/chrome_features.h" |
| |
| namespace { |
| |
| // Each item represent one UI element in the Settings App. |
| enum class ElementType { |
| // The "Web browser" element in the "Default apps" pane. |
| DEFAULT_BROWSER, |
| // The element representing a browser in the "Choose an app" popup. |
| BROWSER_BUTTON, |
| // The button labeled "Check it out" that leaves Edge as the default browser. |
| CHECK_IT_OUT, |
| // The button labeled "Switch Anyway" that dismisses the Edge promo. |
| SWITCH_ANYWAY, |
| // Any other element. |
| UNKNOWN, |
| }; |
| |
| // Configures a cache request so that it includes all properties needed by |
| // DetectElementType() to detect the elements of interest. |
| void ConfigureCacheRequest(IUIAutomationCacheRequest* cache_request) { |
| DCHECK(cache_request); |
| cache_request->AddProperty(UIA_AutomationIdPropertyId); |
| cache_request->AddProperty(UIA_NamePropertyId); |
| cache_request->AddProperty(UIA_ClassNamePropertyId); |
| cache_request->AddPattern(UIA_InvokePatternId); |
| } |
| |
| // Helper function to get the parent element with class name "Flyout". Used to |
| // determine the |element|'s type. |
| base::string16 GetFlyoutParentAutomationId(IUIAutomation* automation, |
| IUIAutomationElement* element) { |
| // Create a condition that will include only elements with the right class |
| // name in the tree view. |
| base::win::ScopedVariant class_name(L"Flyout"); |
| Microsoft::WRL::ComPtr<IUIAutomationCondition> condition; |
| HRESULT result = automation->CreatePropertyCondition( |
| UIA_ClassNamePropertyId, class_name, condition.GetAddressOf()); |
| if (FAILED(result)) |
| return base::string16(); |
| |
| Microsoft::WRL::ComPtr<IUIAutomationTreeWalker> tree_walker; |
| result = |
| automation->CreateTreeWalker(condition.Get(), tree_walker.GetAddressOf()); |
| if (FAILED(result)) |
| return base::string16(); |
| |
| Microsoft::WRL::ComPtr<IUIAutomationCacheRequest> cache_request; |
| result = automation->CreateCacheRequest(cache_request.GetAddressOf()); |
| if (FAILED(result)) |
| return base::string16(); |
| ConfigureCacheRequest(cache_request.Get()); |
| |
| // From MSDN, NormalizeElementBuildCache() "Retrieves the ancestor element |
| // nearest to the specified Microsoft UI Automation element in the tree view". |
| IUIAutomationElement* flyout_element = nullptr; |
| result = tree_walker->NormalizeElementBuildCache(element, cache_request.Get(), |
| &flyout_element); |
| if (FAILED(result) || !flyout_element) |
| return base::string16(); |
| |
| return GetCachedBstrValue(flyout_element, UIA_AutomationIdPropertyId); |
| } |
| |
| ElementType DetectElementType(IUIAutomation* automation, |
| IUIAutomationElement* sender) { |
| DCHECK(automation); |
| DCHECK(sender); |
| base::string16 aid(GetCachedBstrValue(sender, UIA_AutomationIdPropertyId)); |
| if (aid == L"SystemSettings_DefaultApps_Browser_Button") |
| return ElementType::DEFAULT_BROWSER; |
| if (aid == L"SystemSettings_DefaultApps_Browser_App0_HyperlinkButton") |
| return ElementType::SWITCH_ANYWAY; |
| if (base::MatchPattern(aid, L"SystemSettings_DefaultApps_Browser_*_Button")) { |
| // This element type depends on the automation id of one of its ancestors. |
| base::string16 automation_id = |
| GetFlyoutParentAutomationId(automation, sender); |
| if (automation_id == L"settingsFlyout") |
| return ElementType::CHECK_IT_OUT; |
| else if (automation_id == L"DefaultAppsFlyoutPresenter") |
| return ElementType::BROWSER_BUTTON; |
| } |
| return ElementType::UNKNOWN; |
| } |
| |
| } // namespace |
| |
| class SettingsAppMonitor::AutomationControllerDelegate |
| : public AutomationController::Delegate { |
| public: |
| AutomationControllerDelegate( |
| scoped_refptr<base::SequencedTaskRunner> monitor_runner, |
| base::WeakPtr<SettingsAppMonitor> monitor); |
| ~AutomationControllerDelegate() override; |
| |
| // AutomationController::Delegate: |
| void OnInitialized(HRESULT result) const override; |
| void ConfigureCacheRequest( |
| IUIAutomationCacheRequest* cache_request) const override; |
| void OnAutomationEvent(IUIAutomation* automation, |
| IUIAutomationElement* sender, |
| EVENTID event_id) const override; |
| void OnFocusChangedEvent(IUIAutomation* automation, |
| IUIAutomationElement* sender) const override; |
| |
| private: |
| // Invokes the |browser_button| if the Win10AcceleratedDefaultBrowserFlow |
| // feature is enabled. |
| void MaybeInvokeChooser(IUIAutomationElement* browser_button) const; |
| |
| // The task runner on which the SettingsAppMonitor lives. |
| const scoped_refptr<base::SequencedTaskRunner> monitor_runner_; |
| |
| // Only used to post callbacks to |monitor_runner_|; |
| const base::WeakPtr<SettingsAppMonitor> monitor_; |
| |
| // Protect against concurrent accesses to |last_focused_element_|. |
| mutable base::Lock last_focused_element_lock_; |
| |
| // State to suppress duplicate "focus changed" events. |
| mutable ElementType last_focused_element_; |
| |
| // Protect against concurrent accesses to |browser_chooser_invoked_|. |
| mutable base::Lock browser_chooser_invoked_lock_; |
| |
| // The browser chooser must only be invoked once. |
| mutable bool browser_chooser_invoked_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AutomationControllerDelegate); |
| }; |
| |
| SettingsAppMonitor::AutomationControllerDelegate::AutomationControllerDelegate( |
| scoped_refptr<base::SequencedTaskRunner> monitor_runner, |
| base::WeakPtr<SettingsAppMonitor> monitor) |
| : monitor_runner_(monitor_runner), |
| monitor_(std::move(monitor)), |
| last_focused_element_(ElementType::UNKNOWN), |
| browser_chooser_invoked_(false) {} |
| |
| SettingsAppMonitor::AutomationControllerDelegate:: |
| ~AutomationControllerDelegate() = default; |
| |
| void SettingsAppMonitor::AutomationControllerDelegate::OnInitialized( |
| HRESULT result) const { |
| monitor_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SettingsAppMonitor::OnInitialized, monitor_, result)); |
| } |
| |
| void SettingsAppMonitor::AutomationControllerDelegate::ConfigureCacheRequest( |
| IUIAutomationCacheRequest* cache_request) const { |
| ::ConfigureCacheRequest(cache_request); |
| } |
| |
| void SettingsAppMonitor::AutomationControllerDelegate::OnAutomationEvent( |
| IUIAutomation* automation, |
| IUIAutomationElement* sender, |
| EVENTID event_id) const { |
| switch (DetectElementType(automation, sender)) { |
| case ElementType::DEFAULT_BROWSER: |
| monitor_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SettingsAppMonitor::OnChooserInvoked, monitor_)); |
| break; |
| case ElementType::BROWSER_BUTTON: { |
| base::string16 browser_name( |
| GetCachedBstrValue(sender, UIA_NamePropertyId)); |
| if (!browser_name.empty()) { |
| monitor_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&SettingsAppMonitor::OnBrowserChosen, |
| monitor_, browser_name)); |
| } |
| break; |
| } |
| case ElementType::SWITCH_ANYWAY: |
| monitor_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&SettingsAppMonitor::OnPromoChoiceMade, |
| monitor_, false)); |
| break; |
| case ElementType::CHECK_IT_OUT: |
| monitor_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&SettingsAppMonitor::OnPromoChoiceMade, |
| monitor_, true)); |
| break; |
| case ElementType::UNKNOWN: |
| break; |
| } |
| } |
| |
| void SettingsAppMonitor::AutomationControllerDelegate::OnFocusChangedEvent( |
| IUIAutomation* automation, |
| IUIAutomationElement* sender) const { |
| ElementType element_type = DetectElementType(automation, sender); |
| { |
| // Duplicate focus changed events are suppressed. |
| base::AutoLock auto_lock(last_focused_element_lock_); |
| if (last_focused_element_ == element_type) |
| return; |
| last_focused_element_ = element_type; |
| } |
| |
| if (element_type == ElementType::DEFAULT_BROWSER) { |
| MaybeInvokeChooser(sender); |
| monitor_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&SettingsAppMonitor::OnAppFocused, monitor_)); |
| } else if (element_type == ElementType::CHECK_IT_OUT) { |
| monitor_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SettingsAppMonitor::OnPromoFocused, monitor_)); |
| } |
| } |
| |
| void SettingsAppMonitor::AutomationControllerDelegate::MaybeInvokeChooser( |
| IUIAutomationElement* browser_button) const { |
| if (!base::FeatureList::IsEnabled( |
| features::kWin10AcceleratedDefaultBrowserFlow)) { |
| return; |
| } |
| |
| { |
| // Only invoke the browser chooser once. |
| base::AutoLock auto_lock(browser_chooser_invoked_lock_); |
| if (browser_chooser_invoked_) |
| return; |
| browser_chooser_invoked_ = true; |
| } |
| |
| // Invoke the dialog and record whether it was successful. |
| Microsoft::WRL::ComPtr<IUIAutomationInvokePattern> invoke_pattern; |
| bool succeeded = SUCCEEDED(browser_button->GetCachedPatternAs( |
| UIA_InvokePatternId, IID_PPV_ARGS(&invoke_pattern))) && |
| invoke_pattern && SUCCEEDED(invoke_pattern->Invoke()); |
| |
| UMA_HISTOGRAM_BOOLEAN("DefaultBrowser.Win10ChooserInvoked", succeeded); |
| } |
| |
| SettingsAppMonitor::SettingsAppMonitor(Delegate* delegate) |
| : delegate_(delegate), weak_ptr_factory_(this) { |
| // A fully initialized WeakPtrFactory is needed to create the |
| // AutomationControllerDelegate. |
| auto automation_controller_delegate = |
| std::make_unique<SettingsAppMonitor::AutomationControllerDelegate>( |
| base::SequencedTaskRunnerHandle::Get(), |
| weak_ptr_factory_.GetWeakPtr()); |
| |
| automation_controller_ = std::make_unique<AutomationController>( |
| std::move(automation_controller_delegate)); |
| } |
| |
| SettingsAppMonitor::~SettingsAppMonitor() = default; |
| |
| void SettingsAppMonitor::OnInitialized(HRESULT result) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| delegate_->OnInitialized(result); |
| } |
| |
| void SettingsAppMonitor::OnAppFocused() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| delegate_->OnAppFocused(); |
| } |
| |
| void SettingsAppMonitor::OnChooserInvoked() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| delegate_->OnChooserInvoked(); |
| } |
| |
| void SettingsAppMonitor::OnBrowserChosen(const base::string16& browser_name) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| delegate_->OnBrowserChosen(browser_name); |
| } |
| |
| void SettingsAppMonitor::OnPromoFocused() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| delegate_->OnPromoFocused(); |
| } |
| |
| void SettingsAppMonitor::OnPromoChoiceMade(bool accept_promo) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| delegate_->OnPromoChoiceMade(accept_promo); |
| } |