blob: 7332100ed280c974ff3d4d7c5a68b34bfb1e727d [file] [log] [blame]
// 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.
#include "chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_arc_home_service.h"
#include <utility>
#include <vector>
#include "ash/public/cpp/scale_utility.h"
#include "ash/shell.h"
#include "base/bind.h"
#include "base/logging.h"
#include "base/memory/singleton.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chromeos/arc/arc_session_manager.h"
#include "chrome/browser/chromeos/arc/voice_interaction/arc_voice_interaction_framework_service.h"
#include "chrome/browser/chromeos/first_run/first_run.h"
#include "chrome/browser/ui/app_list/arc/arc_app_list_prefs_factory.h"
#include "chrome/browser/ui/app_list/arc/arc_pai_starter.h"
#include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chromeos/chromeos_switches.h"
#include "components/arc/arc_bridge_service.h"
#include "components/arc/arc_browser_context_keyed_service_factory_base.h"
#include "components/arc/arc_prefs.h"
#include "components/arc/arc_service_manager.h"
#include "components/arc/connection_holder.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "ui/accessibility/platform/ax_snapshot_node_android_platform.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/snapshot/snapshot.h"
#include "ui/wm/public/activation_client.h"
#include "url/gurl.h"
namespace arc {
namespace {
constexpr base::TimeDelta kAssistantStartedTimeout =
base::TimeDelta::FromMinutes(1);
constexpr base::TimeDelta kWizardCompletedTimeout =
base::TimeDelta::FromMinutes(1);
mojom::VoiceInteractionStructurePtr CreateVoiceInteractionStructure(
const ui::AXSnapshotNodeAndroid& view_structure) {
auto structure = mojom::VoiceInteractionStructure::New();
structure->text = view_structure.text;
structure->text_size = view_structure.text_size;
structure->bold = view_structure.bold;
structure->italic = view_structure.italic;
structure->underline = view_structure.underline;
structure->line_through = view_structure.line_through;
structure->color = view_structure.color;
structure->bgcolor = view_structure.bgcolor;
structure->role = view_structure.role;
structure->class_name = view_structure.class_name;
structure->rect = view_structure.rect;
if (view_structure.has_selection) {
structure->selection = gfx::Range(view_structure.start_selection,
view_structure.end_selection);
}
for (auto& child : view_structure.children)
structure->children.push_back(CreateVoiceInteractionStructure(*child));
return structure;
}
void RequestVoiceInteractionStructureCallback(
ArcVoiceInteractionArcHomeService::GetVoiceInteractionStructureCallback
callback,
const gfx::Rect& bounds,
const std::string& web_url,
const base::string16& title,
const ui::AXTreeUpdate& update) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// The assist structure starts with 2 dummy nodes: Url node and title
// node. Then we attach all nodes in view hierarchy.
auto root = mojom::VoiceInteractionStructure::New();
root->rect = bounds;
root->class_name = "android.view.dummy.root.WebUrl";
root->text = base::UTF8ToUTF16(web_url);
auto title_node = mojom::VoiceInteractionStructure::New();
title_node->rect = gfx::Rect(bounds.size());
title_node->class_name = "android.view.dummy.WebTitle";
title_node->text = title;
title_node->children.push_back(CreateVoiceInteractionStructure(
*ui::AXSnapshotNodeAndroid::Create(update, false)));
root->children.push_back(std::move(title_node));
std::move(callback).Run(std::move(root));
}
// Singleton factory for ArcVoiceInteractionArcHomeService.
class ArcVoiceInteractionArcHomeServiceFactory
: public internal::ArcBrowserContextKeyedServiceFactoryBase<
ArcVoiceInteractionArcHomeService,
ArcVoiceInteractionArcHomeServiceFactory> {
public:
// Factory name used by ArcBrowserContextKeyedServiceFactoryBase.
static constexpr const char* kName =
"ArcVoiceInteractionArcHomeServiceFactory";
static ArcVoiceInteractionArcHomeServiceFactory* GetInstance() {
return base::Singleton<ArcVoiceInteractionArcHomeServiceFactory>::get();
}
private:
friend base::DefaultSingletonTraits<ArcVoiceInteractionArcHomeServiceFactory>;
ArcVoiceInteractionArcHomeServiceFactory() {
DependsOn(ArcAppListPrefsFactory::GetInstance());
DependsOn(ArcVoiceInteractionFrameworkService::GetFactory());
}
~ArcVoiceInteractionArcHomeServiceFactory() override = default;
// BrowserContextKeyedServiceFactory override:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* context) const override {
if (!chromeos::switches::IsVoiceInteractionEnabled())
return nullptr;
return ArcBrowserContextKeyedServiceFactoryBase::BuildServiceInstanceFor(
context);
}
};
} // namespace
// static
const char ArcVoiceInteractionArcHomeService::kAssistantPackageName[] =
"com.google.android.googlequicksearchbox";
// static
ArcVoiceInteractionArcHomeService*
ArcVoiceInteractionArcHomeService::GetForBrowserContext(
content::BrowserContext* context) {
return ArcVoiceInteractionArcHomeServiceFactory::GetForBrowserContext(
context);
}
ArcVoiceInteractionArcHomeService::ArcVoiceInteractionArcHomeService(
content::BrowserContext* context,
ArcBridgeService* bridge_service)
: context_(context),
arc_bridge_service_(bridge_service),
assistant_started_timeout_(kAssistantStartedTimeout),
wizard_completed_timeout_(kWizardCompletedTimeout) {
arc_bridge_service_->voice_interaction_arc_home()->SetHost(this);
arc_bridge_service_->voice_interaction_arc_home()->AddObserver(this);
ArcSessionManager::Get()->AddObserver(this);
}
ArcVoiceInteractionArcHomeService::~ArcVoiceInteractionArcHomeService() =
default;
void ArcVoiceInteractionArcHomeService::Shutdown() {
ResetTimeouts();
arc_bridge_service_->voice_interaction_arc_home()->RemoveObserver(this);
arc_bridge_service_->voice_interaction_arc_home()->SetHost(nullptr);
ArcSessionManager::Get()->RemoveObserver(this);
}
void ArcVoiceInteractionArcHomeService::OnArcPlayStoreEnabledChanged(
bool enabled) {
if (!pending_pai_lock_)
return;
pending_pai_lock_ = false;
LockPai();
}
void ArcVoiceInteractionArcHomeService::LockPai() {
ResetTimeouts();
arc::ArcPaiStarter* pai_starter =
arc::ArcSessionManager::Get()->pai_starter();
if (!pai_starter) {
DLOG(ERROR) << "There is no PAI starter.";
// We could be starting before ARC session is started when user initiated
// voice interaction first before ARC is enabled. We will remember this
// and wait for ARC session started to try locking again.
pending_pai_lock_ = true;
return;
}
pai_starter->AcquireLock();
}
void ArcVoiceInteractionArcHomeService::UnlockPai() {
ResetTimeouts();
arc::ArcPaiStarter* pai_starter =
arc::ArcSessionManager::Get()->pai_starter();
if (!pai_starter || !pai_starter->locked())
return;
pai_starter->ReleaseLock();
}
void ArcVoiceInteractionArcHomeService::OnAssistantStarted() {
VLOG(1) << "Assistant flow started";
LockPai();
}
void ArcVoiceInteractionArcHomeService::OnAssistantAppRequested() {
VLOG(1) << "Assistant app start request";
ResetTimeouts();
ArcAppListPrefs::Get(context_)->AddObserver(this);
assistant_started_timer_.Start(
FROM_HERE, assistant_started_timeout_,
base::Bind(&ArcVoiceInteractionArcHomeService::OnAssistantStartTimeout,
base::Unretained(this)));
}
void ArcVoiceInteractionArcHomeService::OnAssistantCanceled() {
VLOG(1) << "Assistant flow canceled";
UnlockPai();
}
void ArcVoiceInteractionArcHomeService::OnTaskCreated(
int32_t task_id,
const std::string& package_name,
const std::string& activity,
const std::string& intent) {
if (package_name != kAssistantPackageName)
return;
VLOG(1) << "Assistant app created";
DCHECK_EQ(-1, assistant_task_id_);
assistant_task_id_ = task_id;
assistant_started_timer_.Stop();
}
void ArcVoiceInteractionArcHomeService::OnTaskDestroyed(int32_t task_id) {
if (task_id != assistant_task_id_)
return;
VLOG(1) << "Assistant app exited";
ResetTimeouts();
wizard_completed_timer_.Start(
FROM_HERE, wizard_completed_timeout_,
base::Bind(&ArcVoiceInteractionArcHomeService::OnWizardCompleteTimeout,
base::Unretained(this)));
}
void ArcVoiceInteractionArcHomeService::ResetTimeouts() {
ArcAppListPrefs* arc_prefs = ArcAppListPrefs::Get(context_);
if (arc_prefs)
arc_prefs->RemoveObserver(this);
assistant_started_timer_.Stop();
wizard_completed_timer_.Stop();
}
void ArcVoiceInteractionArcHomeService::OnAssistantStartTimeout() {
LOG(WARNING) << "Failed to start Assistant app.";
UnlockPai();
}
void ArcVoiceInteractionArcHomeService::OnWizardCompleteTimeout() {
LOG(WARNING) << "Assistant app was not completed successfully.";
UnlockPai();
}
void ArcVoiceInteractionArcHomeService::OnConnectionClosed() {
VLOG(1) << "Voice interaction instance is closed.";
UnlockPai();
}
void ArcVoiceInteractionArcHomeService::GetVoiceInteractionStructure(
GetVoiceInteractionStructureCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
PrefService* prefs = Profile::FromBrowserContext(context_)->GetPrefs();
auto* framework_service =
ArcVoiceInteractionFrameworkService::GetForBrowserContext(context_);
if (!framework_service->ValidateTimeSinceUserInteraction() ||
!prefs->GetBoolean(prefs::kVoiceInteractionEnabled) ||
!prefs->GetBoolean(prefs::kVoiceInteractionContextEnabled)) {
std::move(callback).Run(mojom::VoiceInteractionStructure::New());
return;
}
Browser* browser = BrowserList::GetInstance()->GetLastActive();
if (!browser || !browser->window()->IsActive()) {
// TODO(muyuanli): retrieve context for apps.
LOG(ERROR) << "Retrieving context from apps is not implemented.";
std::move(callback).Run(mojom::VoiceInteractionStructure::New());
return;
}
VLOG(1) << "Retrieving voice interaction context";
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
// Do not process incognito tab.
if (web_contents->GetBrowserContext()->IsOffTheRecord()) {
std::move(callback).Run(mojom::VoiceInteractionStructure::New());
return;
}
auto transform = browser->window()
->GetNativeWindow()
->GetRootWindow()
->GetHost()
->GetRootTransform();
float scale_factor = ash::GetScaleFactorForTransform(transform);
web_contents->RequestAXTreeSnapshot(base::BindOnce(
&RequestVoiceInteractionStructureCallback, std::move(callback),
gfx::ConvertRectToPixel(scale_factor, browser->window()->GetBounds()),
web_contents->GetLastCommittedURL().spec(), web_contents->GetTitle()));
}
void ArcVoiceInteractionArcHomeService::OnVoiceInteractionOobeSetupComplete() {
VLOG(1) << "Assistant wizard is completed.";
UnlockPai();
chromeos::first_run::MaybeLaunchDialogImmediately();
}
// static
mojom::VoiceInteractionStructurePtr
ArcVoiceInteractionArcHomeService::CreateVoiceInteractionStructureForTesting(
const ui::AXSnapshotNodeAndroid& view_structure) {
return CreateVoiceInteractionStructure(view_structure);
}
} // namespace arc