blob: 522ec8da3b120b1beeb360b16b6ecd35d362aded [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_framework_service.h"
#include <utility>
#include <vector>
#include "ash/accelerators/accelerator_controller.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/memory/singleton.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/task_scheduler/post_task.h"
#include "chrome/browser/chromeos/login/helper.h"
#include "chrome/browser/chromeos/login/ui/login_display_host_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.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/webui/chromeos/login/oobe_ui.h"
#include "chrome/common/pref_names.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_util.h"
#include "components/arc/instance_holder.h"
#include "content/public/browser/browser_thread.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_owner.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_util.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/snapshot/snapshot.h"
#include "ui/snapshot/snapshot_aura.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_client.h"
namespace arc {
namespace {
using LayerSet = base::flat_set<const ui::Layer*>;
// Time out for a context query from container since user initiated
// interaction. This must be strictly less than
// kMaxTimeSinceUserInteractionForHistogram so that the histogram
// could cover the range of normal operations.
constexpr base::TimeDelta kAllowedTimeSinceUserInteraction =
base::TimeDelta::FromSeconds(2);
constexpr base::TimeDelta kMaxTimeSinceUserInteractionForHistogram =
base::TimeDelta::FromSeconds(5);
constexpr int32_t kContextRequestMaxRemainingCount = 2;
void ScreenshotCallback(
const mojom::VoiceInteractionFrameworkHost::CaptureFocusedWindowCallback&
callback,
scoped_refptr<base::RefCountedMemory> data) {
if (data.get() == nullptr) {
callback.Run(std::vector<uint8_t>{});
return;
}
std::vector<uint8_t> result(data->front(), data->front() + data->size());
callback.Run(result);
}
std::unique_ptr<ui::LayerTreeOwner> CreateLayerTreeForSnapshot(
aura::Window* root_window) {
LayerSet blocked_layers;
for (auto* browser : *BrowserList::GetInstance()) {
if (browser->profile()->IsOffTheRecord())
blocked_layers.insert(browser->window()->GetNativeWindow()->layer());
}
LayerSet excluded_layers;
// Exclude metalayer-related layers. This will also include other layers
// under kShellWindowId_OverlayContainer which is fine.
aura::Window* overlay_container = ash::Shell::GetContainer(
root_window, ash::kShellWindowId_OverlayContainer);
if (overlay_container != nullptr)
excluded_layers.insert(overlay_container->layer());
auto layer_tree_owner = ::wm::RecreateLayersWithClosure(
root_window, base::BindRepeating(
[](LayerSet blocked_layers, LayerSet excluded_layers,
ui::LayerOwner* owner) -> std::unique_ptr<ui::Layer> {
// Parent layer is excluded meaning that it's pointless
// to clone current child and all its descendants. This
// reduces the number of layers to create.
if (blocked_layers.count(owner->layer()->parent()))
return nullptr;
if (blocked_layers.count(owner->layer())) {
auto layer = base::MakeUnique<ui::Layer>(
ui::LayerType::LAYER_SOLID_COLOR);
layer->SetBounds(owner->layer()->bounds());
layer->SetColor(SK_ColorBLACK);
return layer;
}
if (excluded_layers.count(owner->layer()))
return nullptr;
return owner->RecreateLayer();
},
std::move(blocked_layers), std::move(excluded_layers)));
// layer_tree_owner cannot be null since we are starting off from the root
// window, which could never be incognito.
DCHECK(layer_tree_owner);
auto* root_layer = layer_tree_owner->root();
// The root layer might have a scaling transform applied (if the user has
// changed the UI scale via Ctrl-Shift-Plus/Minus).
// Clear the transform so that the snapshot is taken at 1:1 scale relative
// to screen pixels.
root_layer->SetTransform(gfx::Transform());
root_window->layer()->Add(root_layer);
root_window->layer()->StackAtBottom(root_layer);
return layer_tree_owner;
}
void EncodeAndReturnImage(
const ArcVoiceInteractionFrameworkService::CaptureFullscreenCallback&
callback,
std::unique_ptr<ui::LayerTreeOwner> old_layer_owner,
const gfx::Image& image) {
old_layer_owner.reset();
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE,
base::TaskTraits{base::MayBlock(), base::TaskPriority::USER_BLOCKING},
base::Bind(
[](const gfx::Image& image) -> std::vector<uint8_t> {
std::vector<uint8_t> res;
gfx::JPEG1xEncodedDataFromImage(image, 100, &res);
return res;
},
image),
callback);
}
// Singleton factory for ArcVoiceInteractionFrameworkService.
class ArcVoiceInteractionFrameworkServiceFactory
: public internal::ArcBrowserContextKeyedServiceFactoryBase<
ArcVoiceInteractionFrameworkService,
ArcVoiceInteractionFrameworkServiceFactory> {
public:
// Factory name used by ArcBrowserContextKeyedServiceFactoryBase.
static constexpr const char* kName =
"ArcVoiceInteractionFrameworkServiceFactory";
static ArcVoiceInteractionFrameworkServiceFactory* GetInstance() {
return base::Singleton<ArcVoiceInteractionFrameworkServiceFactory>::get();
}
private:
friend base::DefaultSingletonTraits<
ArcVoiceInteractionFrameworkServiceFactory>;
ArcVoiceInteractionFrameworkServiceFactory() = default;
~ArcVoiceInteractionFrameworkServiceFactory() override = default;
// BrowserContextKeyedServiceFactory override:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* context) const override {
if (!chromeos::switches::IsVoiceInteractionEnabled())
return nullptr;
return ArcBrowserContextKeyedServiceFactoryBase::BuildServiceInstanceFor(
context);
}
};
void SetVoiceInteractionPrefs(mojom::VoiceInteractionStatusPtr status) {
PrefService* prefs = ProfileManager::GetActiveUserProfile()->GetPrefs();
prefs->SetBoolean(prefs::kVoiceInteractionPrefSynced, status->configured);
prefs->SetBoolean(prefs::kVoiceInteractionEnabled,
status->configured && status->voice_interaction_enabled);
prefs->SetBoolean(prefs::kVoiceInteractionContextEnabled,
status->configured && status->context_enabled);
}
} // namespace
// static
ArcVoiceInteractionFrameworkService*
ArcVoiceInteractionFrameworkService::GetForBrowserContext(
content::BrowserContext* context) {
return ArcVoiceInteractionFrameworkServiceFactory::GetForBrowserContext(
context);
}
KeyedServiceBaseFactory* ArcVoiceInteractionFrameworkService::GetFactory() {
return ArcVoiceInteractionFrameworkServiceFactory::GetInstance();
}
ArcVoiceInteractionFrameworkService::ArcVoiceInteractionFrameworkService(
content::BrowserContext* context,
ArcBridgeService* bridge_service)
: context_(context), arc_bridge_service_(bridge_service), binding_(this) {
arc_bridge_service_->voice_interaction_framework()->AddObserver(this);
ArcSessionManager::Get()->AddObserver(this);
}
ArcVoiceInteractionFrameworkService::~ArcVoiceInteractionFrameworkService() {
ArcSessionManager::Get()->RemoveObserver(this);
arc_bridge_service_->voice_interaction_framework()->RemoveObserver(this);
}
void ArcVoiceInteractionFrameworkService::OnInstanceReady() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
mojom::VoiceInteractionFrameworkInstance* framework_instance =
ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->voice_interaction_framework(), Init);
DCHECK(framework_instance);
mojom::VoiceInteractionFrameworkHostPtr host_proxy;
binding_.Bind(mojo::MakeRequest(&host_proxy));
framework_instance->Init(std::move(host_proxy));
if (is_request_pending_) {
is_request_pending_ = false;
framework_instance->StartVoiceInteractionSession();
}
}
void ArcVoiceInteractionFrameworkService::OnInstanceClosed() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CallAndResetMetalayerCallback();
metalayer_enabled_ = false;
}
void ArcVoiceInteractionFrameworkService::CaptureFocusedWindow(
const CaptureFocusedWindowCallback& callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!ValidateTimeSinceUserInteraction()) {
callback.Run(std::vector<uint8_t>{});
return;
}
aura::Window* window =
ash::Shell::Get()->activation_client()->GetActiveWindow();
if (window == nullptr) {
callback.Run(std::vector<uint8_t>{});
return;
}
ui::GrabWindowSnapshotAsyncJPEG(
window, gfx::Rect(window->bounds().size()),
base::Bind(&ScreenshotCallback, callback));
}
void ArcVoiceInteractionFrameworkService::CaptureFullscreen(
const CaptureFullscreenCallback& callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!ValidateTimeSinceUserInteraction()) {
callback.Run(std::vector<uint8_t>{});
return;
}
// Since ARC currently only runs in primary display, we restrict
// the screenshot to it.
aura::Window* window = ash::Shell::GetPrimaryRootWindow();
DCHECK(window);
auto old_layer_owner = CreateLayerTreeForSnapshot(window);
ui::GrabLayerSnapshotAsync(
old_layer_owner->root(), gfx::Rect(window->bounds().size()),
base::Bind(&EncodeAndReturnImage, callback,
base::Passed(std::move(old_layer_owner))));
}
void ArcVoiceInteractionFrameworkService::SetVoiceInteractionRunning(
bool running) {
ash::Shell::Get()->NotifyVoiceInteractionStatusChanged(running);
}
void ArcVoiceInteractionFrameworkService::OnMetalayerClosed() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CallAndResetMetalayerCallback();
}
void ArcVoiceInteractionFrameworkService::SetMetalayerEnabled(bool enabled) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
metalayer_enabled_ = enabled;
if (!metalayer_enabled_)
CallAndResetMetalayerCallback();
}
bool ArcVoiceInteractionFrameworkService::IsMetalayerSupported() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return metalayer_enabled_;
}
void ArcVoiceInteractionFrameworkService::ShowMetalayer(
const base::Closure& closed) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!metalayer_closed_callback_.is_null()) {
LOG(ERROR) << "Metalayer is already enabled";
return;
}
metalayer_closed_callback_ = closed;
}
void ArcVoiceInteractionFrameworkService::HideMetalayer() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (metalayer_closed_callback_.is_null()) {
LOG(ERROR) << "Metalayer is already hidden";
return;
}
metalayer_closed_callback_ = base::Closure();
}
void ArcVoiceInteractionFrameworkService::OnArcPlayStoreEnabledChanged(
bool enabled) {
if (enabled)
return;
mojom::VoiceInteractionStatusPtr status =
mojom::VoiceInteractionStatus::New();
status->configured = false;
status->voice_interaction_enabled = false;
status->context_enabled = false;
SetVoiceInteractionPrefs(std::move(status));
}
void ArcVoiceInteractionFrameworkService::StartVoiceInteractionSetupWizard() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
arc::mojom::VoiceInteractionFrameworkInstance* framework_instance =
ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->voice_interaction_framework(),
StartVoiceInteractionSetupWizard);
if (!framework_instance)
return;
framework_instance->StartVoiceInteractionSetupWizard();
}
void ArcVoiceInteractionFrameworkService::CallAndResetMetalayerCallback() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (metalayer_closed_callback_.is_null())
return;
base::ResetAndReturn(&metalayer_closed_callback_).Run();
}
void ArcVoiceInteractionFrameworkService::SetVoiceInteractionEnabled(
bool enable) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
mojom::VoiceInteractionFrameworkInstance* framework_instance =
ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->voice_interaction_framework(),
SetVoiceInteractionEnabled);
if (!framework_instance)
return;
framework_instance->SetVoiceInteractionEnabled(enable);
}
void ArcVoiceInteractionFrameworkService::SetVoiceInteractionContextEnabled(
bool enable) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
mojom::VoiceInteractionFrameworkInstance* framework_instance =
ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->voice_interaction_framework(),
SetVoiceInteractionContextEnabled);
if (!framework_instance)
return;
framework_instance->SetVoiceInteractionContextEnabled(enable);
}
void ArcVoiceInteractionFrameworkService::UpdateVoiceInteractionPrefs() {
if (ProfileManager::GetActiveUserProfile()->GetPrefs()->GetBoolean(
prefs::kVoiceInteractionPrefSynced)) {
return;
}
mojom::VoiceInteractionFrameworkInstance* framework_instance =
ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->voice_interaction_framework(),
GetVoiceInteractionSettings);
if (!framework_instance)
return;
framework_instance->GetVoiceInteractionSettings(
base::Bind(&SetVoiceInteractionPrefs));
}
void ArcVoiceInteractionFrameworkService::StartSessionFromUserInteraction(
const gfx::Rect& rect) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!Profile::FromBrowserContext(context_)->GetPrefs()->GetBoolean(
prefs::kArcVoiceInteractionValuePropAccepted)) {
// If voice interaction value prop already showing, return.
if (chromeos::LoginDisplayHost::default_host())
return;
// If voice interaction value prop has not been accepted, show the value
// prop OOBE page again.
gfx::Rect screen_bounds(chromeos::CalculateScreenBounds(gfx::Size()));
// The display host will be destructed at the end of OOBE flow.
chromeos::LoginDisplayHostImpl* display_host =
new chromeos::LoginDisplayHostImpl(screen_bounds);
display_host->StartVoiceInteractionOobe();
return;
}
if (!arc_bridge_service_->voice_interaction_framework()->has_instance()) {
SetArcCpuRestriction(false);
is_request_pending_ = true;
return;
}
if (!InitiateUserInteraction())
return;
if (rect.IsEmpty()) {
mojom::VoiceInteractionFrameworkInstance* framework_instance =
ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->voice_interaction_framework(),
StartVoiceInteractionSession);
DCHECK(framework_instance);
framework_instance->StartVoiceInteractionSession();
} else {
mojom::VoiceInteractionFrameworkInstance* framework_instance =
ARC_GET_INSTANCE_FOR_METHOD(
arc_bridge_service_->voice_interaction_framework(),
StartVoiceInteractionSessionForRegion);
DCHECK(framework_instance);
framework_instance->StartVoiceInteractionSessionForRegion(rect);
}
}
bool ArcVoiceInteractionFrameworkService::ValidateTimeSinceUserInteraction() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!context_request_remaining_count_) {
// Allowed number of requests used up. But we still have additional
// requests. It's likely that there is something malicious going on.
LOG(ERROR) << "Illegal context request from container.";
UMA_HISTOGRAM_BOOLEAN("VoiceInteraction.IllegalContextRequest", true);
return false;
}
auto elapsed = base::TimeTicks::Now() - user_interaction_start_time_;
elapsed = elapsed > kMaxTimeSinceUserInteractionForHistogram
? kMaxTimeSinceUserInteractionForHistogram
: elapsed;
UMA_HISTOGRAM_CUSTOM_COUNTS(
"VoiceInteraction.UserInteractionToRequestArrival",
elapsed.InMilliseconds(), 1,
kMaxTimeSinceUserInteractionForHistogram.InMilliseconds(), 20);
if (elapsed > kAllowedTimeSinceUserInteraction) {
LOG(ERROR) << "Timed out since last user interaction.";
context_request_remaining_count_ = 0;
return false;
}
context_request_remaining_count_--;
return true;
}
bool ArcVoiceInteractionFrameworkService::InitiateUserInteraction() {
auto start_time = base::TimeTicks::Now();
if ((start_time - user_interaction_start_time_) <
kAllowedTimeSinceUserInteraction &&
context_request_remaining_count_) {
// If next request starts too soon and there is an active session in action,
// we should drop it.
return false;
}
user_interaction_start_time_ = start_time;
context_request_remaining_count_ = kContextRequestMaxRemainingCount;
return true;
}
} // namespace arc