blob: 61bfe44a90cdf67cf61e0dbc96d3af8ebe8e171c [file] [log] [blame]
// 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);
}