// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/capture_mode/capture_mode_behavior.h"
#include <memory>
#include <utility>
#include <vector>
#include "ash/capture_mode/base_capture_mode_session.h"
#include "ash/capture_mode/capture_mode_camera_controller.h"
#include "ash/capture_mode/capture_mode_constants.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_metrics.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/capture_mode/game_capture_bar_view.h"
#include "ash/capture_mode/normal_capture_bar_view.h"
#include "ash/capture_mode/sunfish_capture_bar_view.h"
#include "ash/constants/ash_features.h"
#include "ash/projector/projector_controller_impl.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/task/single_thread_task_runner.h"
#include "ui/aura/window_observer.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/public/cpp/notification.h"
namespace ash {
namespace {
// Width of the full capture bar, which includes all the elements of the normal
// capture bar.
constexpr int kFullCaptureBarWidth = 376;
// Width of the game capture bar.
constexpr int kGameCaptureBarWidth = 250;
// Returns the current configs before been overwritten by the client-initiated
// capture mode session
CaptureModeSessionConfigs GetCaptureModeSessionConfigs() {
CaptureModeController* controller = CaptureModeController::Get();
CaptureModeSessionConfigs configs = CaptureModeSessionConfigs{
controller->type(), controller->source(), controller->recording_type(),
controller->audio_recording_mode(), controller->enable_demo_tools()};
return configs;
}
void SetCaptureModeSessionConfigs(const CaptureModeSessionConfigs& configs) {
CaptureModeController* controller = CaptureModeController::Get();
controller->SetType(configs.type);
controller->SetSource(configs.source);
controller->SetRecordingType(configs.recording_type);
controller->SetAudioRecordingMode(configs.audio_recording_mode);
controller->EnableDemoTools(configs.demo_tools_enabled);
}
// -----------------------------------------------------------------------------
// DefaultBehavior:
// Implements the `CaptureModeBehavior` interface with behavior defined for the
// default capture mode.
class DefaultBehavior : public CaptureModeBehavior {
public:
DefaultBehavior()
: CaptureModeBehavior(
{CaptureModeType::kImage, CaptureModeSource::kRegion,
RecordingType::kWebM, AudioRecordingMode::kOff,
/*demo_tools_enabled=*/false},
BehaviorType::kDefault) {}
DefaultBehavior(const DefaultBehavior&) = delete;
DefaultBehavior& operator=(const DefaultBehavior&) = delete;
~DefaultBehavior() override = default;
};
// -----------------------------------------------------------------------------
// ProjectorBehavior:
// Implements the `CaptureModeBehavior` interface with behavior defined for the
// projector-initiated capture mode.
class ProjectorBehavior : public CaptureModeBehavior {
public:
ProjectorBehavior()
: CaptureModeBehavior(
{CaptureModeType::kVideo, CaptureModeSource::kFullscreen,
RecordingType::kWebM, AudioRecordingMode::kMicrophone,
/*demo_tools_enabled=*/true},
BehaviorType::kProjector) {}
ProjectorBehavior(const ProjectorBehavior&) = delete;
ProjectorBehavior& operator=(const ProjectorBehavior&) = delete;
~ProjectorBehavior() override = default;
// CaptureModeBehavior:
void AttachToSession() override {
cached_configs_ = GetCaptureModeSessionConfigs();
// Overwrite the current capture mode session with the projector
// configurations.
SetCaptureModeSessionConfigs(capture_mode_configs_);
}
void DetachFromSession() override {
CHECK(cached_configs_);
// Restore the capture mode configurations after being overwritten with the
// projector-specific configurations.
SetCaptureModeSessionConfigs(cached_configs_.value());
cached_configs_.reset();
}
bool ShouldImageCaptureTypeBeAllowed() const override { return false; }
bool ShouldSaveToSettingsBeIncluded() const override { return false; }
bool ShouldGifBeSupported() const override { return false; }
bool ShouldShowPreviewNotification() const override { return false; }
bool SupportsAudioRecordingMode(AudioRecordingMode mode) const override {
switch (mode) {
case AudioRecordingMode::kOff:
case AudioRecordingMode::kSystem:
// Projector does not support turning off audio recording nor recording
// the system audio separately without the microphone.
return false;
case AudioRecordingMode::kMicrophone:
case AudioRecordingMode::kSystemAndMicrophone:
return true;
}
}
bool ShouldCreateAnnotationsOverlayController() const override {
return true;
}
bool ShouldShowUserNudge() const override { return false; }
bool ShouldAutoSelectFirstCamera() const override { return true; }
bool RequiresCaptureFolderCreation() const override { return true; }
void CreateCaptureFolder(OnCaptureFolderCreatedCallback callback) override {
ProjectorControllerImpl::Get()->CreateScreencastContainerFolder(
base::BindOnce(&ProjectorBehavior::OnScreencastContainerFolderCreated,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
std::vector<RecordingType> GetSupportedRecordingTypes() const override {
return std::vector<RecordingType>{RecordingType::kWebM};
}
const char* GetClientMetricComponent() const override { return "Projector."; }
protected:
int GetCaptureBarWidth() const override {
return kFullCaptureBarWidth - capture_mode::kButtonSize.width() -
capture_mode::kSpaceBetweenCaptureModeTypeButtons;
}
private:
// Called when the Projector controller creates the DriveFS folder that will
// host the video file along with the associated metadata file created by the
// Projector session.
void OnScreencastContainerFolderCreated(
OnCaptureFolderCreatedCallback callback,
const base::FilePath& capture_file_full_path) {
base::FilePath path;
// An empty path is sent to indicate an error.
if (!capture_file_full_path.empty()) {
path = capture_file_full_path.AddExtension("webm");
}
std::move(callback).Run(path);
}
base::WeakPtrFactory<ProjectorBehavior> weak_ptr_factory_{this};
};
// -----------------------------------------------------------------------------
// GameDashboardBehavior:
// Implements the `CaptureModeBehavior` interface with behaviors defined by the
// game dashboard-initiated capture mode.
class GameDashboardBehavior : public CaptureModeBehavior,
public aura::WindowObserver {
public:
GameDashboardBehavior()
: CaptureModeBehavior(
{CaptureModeType::kVideo, CaptureModeSource::kWindow,
RecordingType::kWebM, AudioRecordingMode::kSystemAndMicrophone,
/*demo_tools_enabled=*/false},
BehaviorType::kGameDashboard) {}
GameDashboardBehavior(const GameDashboardBehavior&) = delete;
GameDashboardBehavior operator=(const GameDashboardBehavior&) = delete;
~GameDashboardBehavior() override = default;
// CaptureModeBehavior:
void AttachToSession() override {
cached_configs_ = GetCaptureModeSessionConfigs();
SetCaptureModeSessionConfigs(capture_mode_configs_);
CaptureModeController* controller = CaptureModeController::Get();
BaseCaptureModeSession* session = controller->capture_mode_session();
CHECK(session);
if (!pre_selected_window_) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
[](base::WeakPtr<GameDashboardBehavior> game_dashboard_behavior,
CaptureModeController* controller) {
if (controller->IsActive()) {
controller->Stop();
}
},
weak_ptr_factory_.GetWeakPtr(), controller));
} else {
session->SetPreSelectedWindow(pre_selected_window_);
}
}
void DetachFromSession() override {
CHECK(cached_configs_);
SetCaptureModeSessionConfigs(cached_configs_.value());
cached_configs_.reset();
if (pre_selected_window_) {
pre_selected_window_->RemoveObserver(this);
pre_selected_window_ = nullptr;
}
}
bool ShouldImageCaptureTypeBeAllowed() const override { return false; }
bool ShouldFulscreenCaptureSourceBeAllowed() const override { return false; }
bool ShouldRegionCaptureSourceBeAllowed() const override { return false; }
bool ShouldDemoToolsSettingsBeIncluded() const override { return false; }
bool ShouldGifBeSupported() const override { return false; }
bool ShouldShowUserNudge() const override { return false; }
bool ShouldAutoSelectFirstCamera() const override {
return !CaptureModeController::Get()
->camera_controller()
->did_user_ever_change_camera();
}
std::unique_ptr<CaptureModeBarView> CreateCaptureModeBarView() override {
return std::make_unique<GameCaptureBarView>();
}
void SetPreSelectedWindow(aura::Window* pre_selected_window) override {
CHECK(!pre_selected_window_);
pre_selected_window_ = pre_selected_window;
pre_selected_window_->AddObserver(this);
}
const char* GetClientMetricComponent() const override {
return "GameDashboard.";
}
std::vector<message_center::ButtonInfo> GetNotificationButtonsInfo(
bool for_video) const override {
return {message_center::ButtonInfo{l10n_util::GetStringUTF16(
for_video ? IDS_ASH_SCREEN_CAPTURE_SHARE_TO_YOUTUBE
: IDS_ASH_SCREEN_CAPTURE_BUTTON_EDIT)},
message_center::ButtonInfo{l10n_util::GetStringUTF16(
IDS_ASH_SCREEN_CAPTURE_BUTTON_DELETE)}};
}
void OnAudioRecordingModeChanged() override {
capture_mode_configs_.audio_recording_mode =
CaptureModeController::Get()->audio_recording_mode();
}
void OnDemoToolsSettingsChanged() override {
capture_mode_configs_.demo_tools_enabled =
CaptureModeController::Get()->enable_demo_tools();
}
// aura::WindowObserver:
void OnWindowDestroying(aura::Window* window) override {
CHECK_EQ(window, pre_selected_window_);
pre_selected_window_->RemoveObserver(this);
pre_selected_window_ = nullptr;
}
protected:
// CaptureModeBehavior:
gfx::Rect GetBarAnchorBoundsInScreen(aura::Window* root) const override {
CHECK(pre_selected_window_);
return pre_selected_window_->GetBoundsInScreen();
}
int GetCaptureBarBottomPadding() const override {
return capture_mode::kGameCaptureBarBottomPadding;
}
int GetCaptureBarWidth() const override { return kGameCaptureBarWidth; }
private:
raw_ptr<aura::Window> pre_selected_window_ = nullptr;
base::WeakPtrFactory<GameDashboardBehavior> weak_ptr_factory_{this};
};
// -----------------------------------------------------------------------------
// SunfishBehavior:
// Implements the `CaptureModeBehavior` interface with behavior defined for the
// sunfish capture mode.
class SunfishBehavior : public CaptureModeBehavior {
public:
SunfishBehavior()
: CaptureModeBehavior(
{CaptureModeType::kImage, CaptureModeSource::kRegion,
RecordingType::kWebM, AudioRecordingMode::kOff,
/*demo_tools_enabled=*/false},
BehaviorType::kSunfish) {}
SunfishBehavior(const SunfishBehavior&) = delete;
SunfishBehavior& operator=(const SunfishBehavior&) = delete;
~SunfishBehavior() override = default;
// CaptureModeBehavior:
bool ShouldShowUserNudge() const override { return false; }
const std::u16string GetCaptureLabelRegionText() const override {
return l10n_util::GetStringUTF16(IDS_ASH_SUNFISH_CAPTURE_LABEL);
}
int GetCaptureBarWidth() const override {
// Return the height so the button is circular.
return capture_mode::kCaptureBarHeight;
}
std::unique_ptr<CaptureModeBarView> CreateCaptureModeBarView() override {
return std::make_unique<SunfishCaptureBarView>();
}
bool OnRegionSelected() override {
CaptureModeController::Get()->PerformImageSearch();
return true;
}
};
} // namespace
// -----------------------------------------------------------------------------
// CaptureModeBehavior:
CaptureModeBehavior::CaptureModeBehavior(
const CaptureModeSessionConfigs& configs,
const BehaviorType behavior_type)
: capture_mode_configs_(configs), behavior_type_(behavior_type) {}
// static
std::unique_ptr<CaptureModeBehavior> CaptureModeBehavior::Create(
BehaviorType behavior_type) {
switch (behavior_type) {
case BehaviorType::kProjector:
return std::make_unique<ProjectorBehavior>();
case BehaviorType::kGameDashboard:
return std::make_unique<GameDashboardBehavior>();
case BehaviorType::kDefault:
return std::make_unique<DefaultBehavior>();
case BehaviorType::kSunfish:
return std::make_unique<SunfishBehavior>();
}
}
void CaptureModeBehavior::AttachToSession() {}
void CaptureModeBehavior::DetachFromSession() {}
bool CaptureModeBehavior::ShouldImageCaptureTypeBeAllowed() const {
return true;
}
bool CaptureModeBehavior::ShouldVideoCaptureTypeBeAllowed() const {
return true;
}
bool CaptureModeBehavior::ShouldFulscreenCaptureSourceBeAllowed() const {
return true;
}
bool CaptureModeBehavior::ShouldRegionCaptureSourceBeAllowed() const {
return true;
}
bool CaptureModeBehavior::ShouldWindowCaptureSourceBeAllowed() const {
return true;
}
bool CaptureModeBehavior::SupportsAudioRecordingMode(
AudioRecordingMode mode) const {
switch (mode) {
case AudioRecordingMode::kOff:
case AudioRecordingMode::kMicrophone:
case AudioRecordingMode::kSystem:
case AudioRecordingMode::kSystemAndMicrophone:
return true;
}
}
bool CaptureModeBehavior::ShouldCameraSelectionSettingsBeIncluded() const {
return true;
}
bool CaptureModeBehavior::ShouldDemoToolsSettingsBeIncluded() const {
return true;
}
bool CaptureModeBehavior::ShouldSaveToSettingsBeIncluded() const {
return true;
}
bool CaptureModeBehavior::ShouldGifBeSupported() const {
return true;
}
bool CaptureModeBehavior::ShouldShowPreviewNotification() const {
return true;
}
bool CaptureModeBehavior::ShouldSkipVideoRecordingCountDown() const {
return false;
}
bool CaptureModeBehavior::ShouldCreateAnnotationsOverlayController() const {
if (base::FeatureList::IsEnabled(ash::features::kAnnotatorMode)) {
return true;
}
return false;
}
bool CaptureModeBehavior::ShouldShowUserNudge() const {
return true;
}
bool CaptureModeBehavior::ShouldAutoSelectFirstCamera() const {
return false;
}
bool CaptureModeBehavior::RequiresCaptureFolderCreation() const {
return false;
}
void CaptureModeBehavior::CreateCaptureFolder(
OnCaptureFolderCreatedCallback callback) {
NOTREACHED();
}
std::vector<RecordingType> CaptureModeBehavior::GetSupportedRecordingTypes()
const {
std::vector<RecordingType> supported_recording_types;
supported_recording_types.push_back(RecordingType::kWebM);
if (features::IsGifRecordingEnabled()) {
supported_recording_types.push_back(RecordingType::kGif);
}
return supported_recording_types;
}
void CaptureModeBehavior::SetPreSelectedWindow(
aura::Window* pre_selected_window) {
NOTREACHED();
}
const char* CaptureModeBehavior::GetClientMetricComponent() const {
return "";
}
std::vector<message_center::ButtonInfo>
CaptureModeBehavior::GetNotificationButtonsInfo(bool for_video) const {
std::vector<message_center::ButtonInfo> buttons_info;
if (!for_video &&
!Shell::Get()->session_controller()->IsUserSessionBlocked()) {
buttons_info.emplace_back(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_BUTTON_EDIT));
}
buttons_info.emplace_back(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_BUTTON_DELETE));
return buttons_info;
}
const std::u16string CaptureModeBehavior::GetCaptureLabelRegionText() const {
CaptureModeController* controller = CaptureModeController::Get();
DCHECK(controller->user_capture_region().IsEmpty());
return l10n_util::GetStringUTF16(
controller->type() == CaptureModeType::kImage
? IDS_ASH_SCREEN_CAPTURE_LABEL_REGION_IMAGE_CAPTURE
: IDS_ASH_SCREEN_CAPTURE_LABEL_REGION_VIDEO_RECORD);
}
std::unique_ptr<CaptureModeBarView>
CaptureModeBehavior::CreateCaptureModeBarView() {
return std::make_unique<NormalCaptureBarView>(this);
}
gfx::Rect CaptureModeBehavior::GetCaptureBarBounds(aura::Window* root) const {
auto bounds = GetBarAnchorBoundsInScreen(root);
const int bar_y = bounds.bottom() - GetCaptureBarBottomPadding() -
capture_mode::kCaptureBarHeight;
bounds.ClampToCenteredSize(
gfx::Size(GetCaptureBarWidth(), capture_mode::kCaptureBarHeight));
bounds.set_y(bar_y);
return bounds;
}
gfx::Rect CaptureModeBehavior::GetBarAnchorBoundsInScreen(
aura::Window* root) const {
CHECK(root);
auto bounds = root->GetBoundsInScreen();
int new_bottom = bounds.bottom();
Shelf* shelf = Shelf::ForWindow(root);
if (shelf->IsHorizontalAlignment()) {
// Get the widget which has the shelf icons. This is the hotseat widget if
// the hotseat is extended, shelf widget otherwise.
const bool hotseat_extended =
shelf->shelf_layout_manager()->hotseat_state() ==
HotseatState::kExtended;
views::Widget* shelf_widget =
hotseat_extended ? static_cast<views::Widget*>(shelf->hotseat_widget())
: static_cast<views::Widget*>(shelf->shelf_widget());
new_bottom = shelf_widget->GetWindowBoundsInScreen().y();
}
bounds.set_height(new_bottom - bounds.y());
return bounds;
}
int CaptureModeBehavior::GetCaptureBarBottomPadding() const {
return capture_mode::kCaptureBarBottomPadding;
}
int CaptureModeBehavior::GetCaptureBarWidth() const {
return kFullCaptureBarWidth;
}
void CaptureModeBehavior::OnAudioRecordingModeChanged() {}
void CaptureModeBehavior::OnDemoToolsSettingsChanged() {}
bool CaptureModeBehavior::OnRegionSelected() {
return false;
}
} // namespace ash