blob: 53513573fee78939915aa4624f454f2b133d600d [file] [log] [blame]
// Copyright 2019 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 "ash/shelf/app_list_button_controller.h"
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/assistant/assistant_controller.h"
#include "ash/home_screen/home_screen_controller.h"
#include "ash/session/session_controller.h"
#include "ash/shelf/app_list_button.h"
#include "ash/shelf/assistant_overlay.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shell.h"
#include "ash/shell_state.h"
#include "ash/voice_interaction/voice_interaction_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "base/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/timer/timer.h"
#include "chromeos/constants/chromeos_switches.h"
#include "components/account_id/account_id.h"
#include "ui/display/screen.h"
#include "ui/views/animation/ink_drop_state.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
constexpr int kVoiceInteractionAnimationDelayMs = 200;
constexpr int kVoiceInteractionAnimationHideDelayMs = 500;
// Returns true if the button should appear activatable.
bool CanActivate() {
return Shell::Get()->home_screen_controller()->IsHomeScreenAvailable() ||
!Shell::Get()->app_list_controller()->IsVisible();
}
} // namespace
AppListButtonController::AppListButtonController(AppListButton* button,
Shelf* shelf)
: button_(button), shelf_(shelf) {
DCHECK(button_);
DCHECK(shelf_);
Shell* shell = Shell::Get();
// AppListController is only available in non-KioskNext sessions.
if (shell->app_list_controller())
shell->app_list_controller()->AddObserver(this);
shell->session_controller()->AddObserver(this);
shell->tablet_mode_controller()->AddObserver(this);
shell->voice_interaction_controller()->AddLocalObserver(this);
// Initialize voice interaction overlay and sync the flags if active user
// session has already started. This could happen when an external monitor
// is plugged in.
if (shell->session_controller()->IsActiveUserSessionStarted() &&
chromeos::switches::IsAssistantEnabled()) {
InitializeVoiceInteractionOverlay();
}
}
AppListButtonController::~AppListButtonController() {
Shell* shell = Shell::Get();
// AppListController and TabletModeController are destroyed early when Shell
// is being destroyed, so they may not exist.
if (shell->app_list_controller())
shell->app_list_controller()->RemoveObserver(this);
if (shell->tablet_mode_controller())
shell->tablet_mode_controller()->RemoveObserver(this);
shell->session_controller()->RemoveObserver(this);
shell->voice_interaction_controller()->RemoveLocalObserver(this);
}
bool AppListButtonController::MaybeHandleGestureEvent(ui::GestureEvent* event,
ShelfView* shelf_view) {
switch (event->type()) {
case ui::ET_GESTURE_TAP:
case ui::ET_GESTURE_TAP_CANCEL:
if (IsVoiceInteractionAvailable()) {
assistant_overlay_->EndAnimation();
assistant_animation_delay_timer_->Stop();
}
if (CanActivate())
button_->AnimateInkDrop(views::InkDropState::ACTION_TRIGGERED, event);
// After animating the ripple, let the button handle the event.
return false;
case ui::ET_GESTURE_TAP_DOWN:
// If |!ShouldEnterPushedState|, Button::OnGestureEvent will not set the
// |ET_GESTURE_TAP_DOWN| event to be handled. This will cause the
// |ET_GESTURE_TAP| or |ET_GESTURE_TAP_CANCEL| not to be sent to
// |button_|, therefore leaving the assistant overlay ripple visible.
if (!shelf_view->ShouldEventActivateButton(button_, *event))
return true;
if (IsVoiceInteractionAvailable()) {
assistant_animation_delay_timer_->Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(
kVoiceInteractionAnimationDelayMs),
base::BindOnce(
&AppListButtonController::StartVoiceInteractionAnimation,
base::Unretained(this)));
}
if (CanActivate())
button_->AnimateInkDrop(views::InkDropState::ACTION_PENDING, event);
return false;
case ui::ET_GESTURE_LONG_PRESS:
// Only consume the long press event if voice interaction is available.
if (!IsVoiceInteractionAvailable())
return false;
base::RecordAction(base::UserMetricsAction(
"VoiceInteraction.Started.AppListButtonLongPress"));
assistant_overlay_->BurstAnimation();
event->SetHandled();
Shell::Get()->shell_state()->SetRootWindowForNewWindows(
button_->GetWidget()->GetNativeWindow()->GetRootWindow());
Shell::Get()->assistant_controller()->ui_controller()->ShowUi(
AssistantEntryPoint::kLongPressLauncher);
return true;
case ui::ET_GESTURE_LONG_TAP:
// Only consume the long tap event if voice interaction is available.
if (!IsVoiceInteractionAvailable())
return false;
// This event happens after the user long presses and lifts the finger.
button_->AnimateInkDrop(views::InkDropState::HIDDEN, event);
// We already handled the long press; consume the long tap to avoid
// bringing up the context menu again.
event->SetHandled();
return true;
default:
return false;
}
}
bool AppListButtonController::IsVoiceInteractionAvailable() {
VoiceInteractionController* controller =
Shell::Get()->voice_interaction_controller();
bool settings_enabled = controller->settings_enabled().value_or(false);
bool consent_given = controller->consent_status() ==
mojom::ConsentStatus::kActivityControlAccepted;
bool feature_allowed =
controller->allowed_state() == mojom::AssistantAllowedState::ALLOWED;
return assistant_overlay_ && feature_allowed &&
(settings_enabled || !consent_given);
}
bool AppListButtonController::IsVoiceInteractionRunning() {
return Shell::Get()
->voice_interaction_controller()
->voice_interaction_state()
.value_or(mojom::VoiceInteractionState::STOPPED) ==
mojom::VoiceInteractionState::RUNNING;
}
void AppListButtonController::OnAppListVisibilityChanged(bool shown,
int64_t display_id) {
if (display::Screen::GetScreen()
->GetDisplayNearestWindow(button_->GetWidget()->GetNativeWindow())
.id() != display_id) {
return;
}
if (shown)
OnAppListShown();
else
OnAppListDismissed();
}
void AppListButtonController::OnActiveUserSessionChanged(
const AccountId& account_id) {
button_->OnVoiceInteractionAvailabilityChanged();
// Initialize voice interaction overlay when primary user session becomes
// active.
if (Shell::Get()->session_controller()->IsUserPrimary() &&
!assistant_overlay_ && chromeos::switches::IsAssistantEnabled()) {
InitializeVoiceInteractionOverlay();
}
}
void AppListButtonController::OnTabletModeStarted() {
button_->AnimateInkDrop(views::InkDropState::DEACTIVATED, nullptr);
}
void AppListButtonController::OnVoiceInteractionStatusChanged(
mojom::VoiceInteractionState state) {
button_->OnVoiceInteractionAvailabilityChanged();
if (!assistant_overlay_)
return;
switch (state) {
case mojom::VoiceInteractionState::STOPPED:
UMA_HISTOGRAM_TIMES(
"VoiceInteraction.OpenDuration",
base::TimeTicks::Now() - voice_interaction_start_timestamp_);
break;
case mojom::VoiceInteractionState::NOT_READY:
// If we are showing the bursting or waiting animation, no need to do
// anything. Otherwise show the waiting animation now.
// NOTE: No waiting animation for native assistant.
if (!chromeos::switches::IsAssistantEnabled() &&
!assistant_overlay_->IsBursting() &&
!assistant_overlay_->IsWaiting()) {
assistant_overlay_->WaitingAnimation();
}
break;
case mojom::VoiceInteractionState::RUNNING:
// we start hiding the animation if it is running.
if (assistant_overlay_->IsBursting() || assistant_overlay_->IsWaiting()) {
assistant_animation_hide_delay_timer_->Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(
kVoiceInteractionAnimationHideDelayMs),
base::BindOnce(&AssistantOverlay::HideAnimation,
base::Unretained(assistant_overlay_)));
}
voice_interaction_start_timestamp_ = base::TimeTicks::Now();
break;
}
}
void AppListButtonController::OnVoiceInteractionSettingsEnabled(bool enabled) {
button_->OnVoiceInteractionAvailabilityChanged();
}
void AppListButtonController::OnVoiceInteractionConsentStatusUpdated(
mojom::ConsentStatus consent_status) {
button_->OnVoiceInteractionAvailabilityChanged();
}
void AppListButtonController::StartVoiceInteractionAnimation() {
assistant_overlay_->StartAnimation(false);
}
void AppListButtonController::OnAppListShown() {
// Do not show a highlight if the home screen is available, since the home
// screen view is always open in the background.
if (!Shell::Get()->home_screen_controller()->IsHomeScreenAvailable())
button_->AnimateInkDrop(views::InkDropState::ACTIVATED, nullptr);
is_showing_app_list_ = true;
shelf_->UpdateAutoHideState();
}
void AppListButtonController::OnAppListDismissed() {
button_->AnimateInkDrop(views::InkDropState::DEACTIVATED, nullptr);
is_showing_app_list_ = false;
shelf_->UpdateAutoHideState();
}
void AppListButtonController::InitializeVoiceInteractionOverlay() {
assistant_overlay_ = new AssistantOverlay(button_);
button_->AddChildView(assistant_overlay_);
assistant_overlay_->SetVisible(false);
assistant_animation_delay_timer_ = std::make_unique<base::OneShotTimer>();
assistant_animation_hide_delay_timer_ =
std::make_unique<base::OneShotTimer>();
}
} // namespace ash