// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "base/containers/enum_set.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/types/cxx23_to_underlying.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/arc/fileapi/arc_documents_provider_util.h"
#include "chrome/browser/ash/extensions/file_manager/event_router_factory.h"
#include "chrome/browser/ash/file_manager/file_tasks.h"
#include "chrome/browser/ash/file_manager/io_task.h"
#include "chrome/browser/ash/file_manager/office_file_tasks.h"
#include "chrome/browser/ash/file_manager/open_util.h"
#include "chrome/browser/ash/file_manager/open_with_browser.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/file_system_provider/mount_path_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/upload_office_to_cloud/upload_office_to_cloud.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload.mojom-forward.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload.mojom-shared.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload.mojom.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_ui.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/hats_office_trigger.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.h"
#include "chrome/browser/ui/webui/ash/office_fallback/office_fallback_ui.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/user_manager/user_manager.h"
#include "extensions/browser/api/file_handlers/mime_util.h"
#include "extensions/browser/entry_info.h"
#include "extensions/common/constants.h"
#include "net/base/url_util.h"
#include "storage/browser/file_system/file_system_url.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/chromeos/strings/grit/ui_chromeos_strings.h"
#include "ui/gfx/geometry/size.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
namespace ash::cloud_upload {
namespace {
namespace fm_tasks = file_manager::file_tasks;
using ash::file_system_provider::ProvidedFileSystemInfo;
using ash::file_system_provider::ProviderId;
using ash::file_system_provider::Service;
constexpr char kAndroidOneDriveAuthority[] =
"com.microsoft.skydrive.content.StorageAccessProvider";
constexpr char kNotificationId[] = "cloud_upload_open_failure";
constexpr char kFileHandlerSelectionMetricName[] =
"FileBrowser.OfficeFiles.Setup.FileHandlerSelection";
constexpr char kFirstTimeMicrosoft365AvailabilityMetric[] =
"FileBrowser.OfficeFiles.Setup.FirstTimeMicrosoft365Availability";
// Records the file handler selected on the first page of Office setup.
//
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class OfficeSetupFileHandler {
kGoogleDocs = 0,
kGoogleSheets = 1,
kGoogleSlides = 2,
kMicrosoft365 = 3,
kOtherLocalHandler = 4,
kQuickOffice = 5,
kMaxValue = kQuickOffice,
};
// Represents (as a bitmask) whether or not Microsoft 365 PWA and ODFS are set
// up. Used to record this state when setup is launched.
//
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class Microsoft365Availability {
kPWA = 0,
kODFS = 1,
kMinValue = kPWA,
kMaxValue = kODFS,
};
// Handle system error notification "Sign in" click.
void HandleSignInClick(Profile* profile, std::optional<int> button_index) {
// If the "Sign in" button was pressed, rather than a click to somewhere
// else in the notification.
if (button_index) {
// TODO(b/282619291) decide what callback should be.
// Request an ODFS mount which will trigger reauthentication.
RequestODFSMount(profile, base::DoNothing());
}
NotificationDisplayService* notification_service =
NotificationDisplayServiceFactory::GetForProfile(profile);
notification_service->Close(NotificationHandler::Type::TRANSIENT,
kNotificationId);
}
// TODO(b/288038136): Use a notification manager to handle error notifications.
// TODO(b/242685536) Use "files" in the title for multi-files when support for
// multi-files is added.
// Show system notification to communicate that their file can't be opened. If
// the user needs to reauthenticate to OneDrive, prompt the user to
// reauthenticate to ODFS via a "Sign in" button.
void ShowUnableToOpenNotification(
Profile* profile,
std::string message = GetGenericErrorMessage(),
std::string title =
l10n_util::GetPluralStringFUTF8(IDS_OFFICE_UPLOAD_ERROR_CANT_OPEN_FILE,
1),
message_center::SystemNotificationWarningLevel warning_level =
message_center::SystemNotificationWarningLevel::WARNING) {
std::vector<message_center::ButtonInfo> notification_buttons;
if (message == GetReauthenticationRequiredMessage()) {
// Special case of |FILE_ERROR_ACCESS_DENIED| where the user needs to
// reauthenticate to OneDrive.
// Add "Sign in" button.
notification_buttons.emplace_back(
l10n_util::GetStringUTF16(IDS_OFFICE_NOTIFICATION_SIGN_IN_BUTTON));
}
auto notification = ash::CreateSystemNotificationPtr(
/*type=*/message_center::NOTIFICATION_TYPE_SIMPLE,
/*id=*/kNotificationId,
/*title*/ base::UTF8ToUTF16(title),
/*message=*/base::UTF8ToUTF16(message),
/*display_source=*/
l10n_util::GetStringUTF16(IDS_ASH_MESSAGE_CENTER_SYSTEM_APP_NAME_FILES),
/*origin_url=*/GURL(),
/*notifier_id=*/message_center::NotifierId(),
/*optional_fields=*/{},
/*delegate=*/
base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
base::BindRepeating(&HandleSignInClick, profile)),
/*small_image=*/ash::kFolderIcon, warning_level);
notification->set_buttons(notification_buttons);
// Set never_timeout with the highest priority, SYSTEM_PRIORITY, so that the
// notification never times out.
notification->set_never_timeout(true);
notification->SetSystemPriority();
NotificationDisplayService* notification_service =
NotificationDisplayServiceFactory::GetForProfile(profile);
notification_service->Display(NotificationHandler::Type::TRANSIENT,
*notification,
/*metadata=*/nullptr);
}
// Check if reauthentication to OneDrive is required from the ODFS metadata
// and show the reuathentication is required notification if true. Otherwise
// show the generic access error notification.
void OnGetReauthenticationRequired(
Profile* profile,
base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback,
base::expected<ODFSMetadata, base::File::Error> metadata) {
bool reauthentication_required = false;
if (metadata.has_value()) {
// TODO(b/330786891): Only query account_state once
// reauthentication_required is no longer needed for backwards compatibility
// with ODFS.
reauthentication_required =
metadata->reauthentication_required ||
(metadata->account_state.has_value() &&
metadata->account_state.value() ==
OdfsAccountState::kReauthenticationRequired);
} else {
LOG(ERROR) << "Failed to get reauthentication required state: "
<< metadata.error();
}
if (reauthentication_required) {
ShowUnableToOpenNotification(profile, GetReauthenticationRequiredMessage());
std::move(callback).Run(
OfficeOneDriveOpenErrors::kGetActionsReauthRequired);
return;
}
ShowUnableToOpenNotification(profile);
std::move(callback).Run(OfficeOneDriveOpenErrors::kGetActionsAccessDenied);
}
// Open file with |file_path| from ODFS |file_system|. Open in the OneDrive PWA
// without link capturing.
void OpenFileFromODFS(
Profile* profile,
file_system_provider::ProvidedFileSystemInterface* file_system,
const base::FilePath& file_path,
base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback) {
GetODFSEntryMetadata(
file_system, file_path,
base::BindOnce(
[](Profile* profile,
file_system_provider::ProvidedFileSystemInterface* file_system,
base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback,
base::expected<ODFSEntryMetadata, base::File::Error> metadata) {
if (!metadata.has_value()) {
switch (metadata.error()) {
case base::File::Error::FILE_ERROR_ACCESS_DENIED:
// Query authentication state to determine which error message
// to show.
GetODFSMetadata(file_system,
base::BindOnce(&OnGetReauthenticationRequired,
profile, std::move(callback)));
break;
default:
ShowUnableToOpenNotification(profile);
std::move(callback).Run(
OfficeOneDriveOpenErrors::kGetActionsGenericError);
break;
}
return;
}
if (!metadata->url) {
ShowUnableToOpenNotification(profile);
std::move(callback).Run(
OfficeOneDriveOpenErrors::kGetActionsNoUrl);
return;
}
GURL url(*metadata->url);
if (!url.is_valid()) {
ShowUnableToOpenNotification(profile);
std::move(callback).Run(
OfficeOneDriveOpenErrors::kGetActionsInvalidUrl);
return;
}
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
proxy->LaunchAppWithUrl(web_app::kMicrosoft365AppId,
/*event_flags=*/ui::EF_NONE, url,
apps::LaunchSource::kFromFileManager,
/*window_info=*/nullptr);
if (base::FeatureList::IsEnabled(
::features::kHappinessTrackingOffice)) {
ash::cloud_upload::HatsOfficeTrigger::Get()
.ShowSurveyAfterAppInactive(
web_app::kMicrosoft365AppId,
ash::cloud_upload::HatsOfficeLaunchingApp::kMS365);
}
std::move(callback).Run(OfficeOneDriveOpenErrors::kSuccess);
},
profile, file_system, std::move(callback)));
}
// Open office file using the ODFS |url|.
void OpenODFSUrl(Profile* profile,
const storage::FileSystemURL& url,
base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback) {
if (!url.is_valid()) {
LOG(ERROR) << "Invalid uploaded file URL";
std::move(callback).Run(OfficeOneDriveOpenErrors::kNoFileSystemURL);
return;
}
ash::file_system_provider::util::FileSystemURLParser parser(url);
if (!parser.Parse()) {
LOG(ERROR) << "Path not in FSP";
std::move(callback).Run(OfficeOneDriveOpenErrors::kInvalidFileSystemURL);
return;
}
OpenFileFromODFS(profile, parser.file_system(), parser.file_path(),
std::move(callback));
}
bool HasFileWithExtensionFromSet(
const std::vector<storage::FileSystemURL>& file_urls,
const std::set<std::string>& extensions) {
return base::ranges::any_of(file_urls, [&extensions](const auto& file_url) {
return base::ranges::any_of(extensions, [&file_url](const auto& extension) {
return file_url.path().MatchesExtension(extension);
});
});
}
bool HasWordFile(const std::vector<storage::FileSystemURL>& file_urls) {
return HasFileWithExtensionFromSet(file_urls,
fm_tasks::WordGroupExtensions());
}
bool HasExcelFile(const std::vector<storage::FileSystemURL>& file_urls) {
return HasFileWithExtensionFromSet(file_urls,
fm_tasks::ExcelGroupExtensions());
}
bool HasPowerPointFile(const std::vector<storage::FileSystemURL>& file_urls) {
return HasFileWithExtensionFromSet(file_urls,
fm_tasks::PowerPointGroupExtensions());
}
// This indicates we ran Office setup and set a preference, or the user had a
// pre-existing preference for these file types.
bool HaveExplicitFileHandlers(
Profile* profile,
const std::vector<storage::FileSystemURL>& file_urls) {
return base::ranges::all_of(file_urls, [profile](const auto& url) {
return fm_tasks::HasExplicitDefaultFileHandler(profile,
url.path().FinalExtension());
});
}
// This indicates we ran Office setup and set a preference, or the user had a
// pre-existing preference for these file types.
bool HaveExplicitFileHandlers(Profile* profile,
const std::set<std::string>& extensions) {
return base::ranges::all_of(extensions, [profile](const auto& extension) {
return fm_tasks::HasExplicitDefaultFileHandler(profile, extension);
});
}
void RecordMicrosoft365Availability(const char* metric, Profile* profile) {
base::EnumSet<Microsoft365Availability, Microsoft365Availability::kMinValue,
Microsoft365Availability::kMaxValue>
ms365_state;
if (IsOfficeWebAppInstalled(profile)) {
ms365_state.Put(Microsoft365Availability::kPWA);
}
if (IsODFSMounted(profile)) {
ms365_state.Put(Microsoft365Availability::kODFS);
}
base::UmaHistogramExactLinear(
metric, ms365_state.ToEnumBitmask(),
decltype(ms365_state)::All().ToEnumBitmask() + 1);
}
mojom::OperationType UploadTypeToOperationType(UploadType upload_type) {
switch (upload_type) {
case UploadType::kMove:
return mojom::OperationType::kMove;
case UploadType::kCopy:
return mojom::OperationType::kCopy;
}
}
void OnWaitingForAndroidUnsupportedPathFallbackChoiceReceived(
Profile* profile,
const fm_tasks::TaskDescriptor& task,
const std::vector<storage::FileSystemURL>& file_urls,
ash::office_fallback::FallbackReason fallback_reason,
std::unique_ptr<ash::cloud_upload::CloudOpenMetrics> cloud_open_metrics,
std::optional<const std::string> choice) {
if (!IsOpenInOfficeTask(task)) {
DUMP_WILL_BE_NOTREACHED();
return;
}
if (!choice.has_value()) {
// The user's choice was unable to be retrieved.
fm_tasks::LogOneDriveMetricsAfterFallback(
fallback_reason,
ash::cloud_upload::OfficeTaskResult::kCannotGetFallbackChoiceAfterOpen,
std::move(cloud_open_metrics));
return;
}
if (choice.value() == ash::office_fallback::kDialogChoiceQuickOffice) {
fm_tasks::LogOneDriveMetricsAfterFallback(
fallback_reason,
ash::cloud_upload::OfficeTaskResult::kFallbackQuickOfficeAfterOpen,
std::move(cloud_open_metrics));
fm_tasks::LaunchQuickOffice(profile, file_urls);
} else if (choice.value() == ash::office_fallback::kDialogChoiceTryAgain) {
LOG(ERROR) << "Unexpected response: " << choice.value();
} else if (choice.value() == ash::office_fallback::kDialogChoiceCancel) {
fm_tasks::LogOneDriveMetricsAfterFallback(
fallback_reason,
ash::cloud_upload::OfficeTaskResult::kCancelledAtFallbackAfterOpen,
std::move(cloud_open_metrics));
} else if (choice.value() == ash::office_fallback::kDialogChoiceOk) {
fm_tasks::LogOneDriveMetricsAfterFallback(
fallback_reason,
ash::cloud_upload::OfficeTaskResult::kOkAtFallbackAfterOpen,
std::move(cloud_open_metrics));
} else if (!choice.value().empty()) {
LOG(ERROR) << "Unhandled response: " << choice.value();
} else {
// Always map an empty user response to a Cancel user response.
// This can occur when the user logs out of the session. However,
// since there could be other unknown causes, leave a log.
LOG(ERROR) << "Empty user response";
fm_tasks::LogOneDriveMetricsAfterFallback(
fallback_reason,
ash::cloud_upload::OfficeTaskResult::kCancelledAtFallbackAfterOpen,
std::move(cloud_open_metrics));
}
}
bool BringDialogToFrontIfItExists(const std::string& id) {
SystemWebDialogDelegate* existing_dialog =
SystemWebDialogDelegate::FindInstance(id);
if (!existing_dialog) {
return false;
}
existing_dialog->StackAtTop();
return true;
}
} // namespace
// static
// Creates an instance of CloudOpenTask that effectively owns itself by keeping
// a reference alive in the TaskFinished callback.
bool CloudOpenTask::Execute(
Profile* profile,
const std::vector<storage::FileSystemURL>& file_urls,
const fm_tasks::TaskDescriptor& task,
const CloudProvider cloud_provider,
std::unique_ptr<CloudOpenMetrics> cloud_open_metrics) {
DCHECK(!file_urls.empty());
auto* event_router = file_manager::EventRouterFactory::GetForProfile(profile);
// TODO(b/242685536) add support for multiple files.
if (event_router) {
if (!event_router->AddCloudOpenTask(file_urls.front())) {
LOG(ERROR) << "File already being opened";
// If a cloud upload dialog already exists, bring it to the front to
// prompt the user to keep going.
BringDialogToFrontIfItExists(chrome::kChromeUICloudUploadURL);
// Notify the user that a file is already being opened. Nothing is wrong
// when the file is already being opened, so use a normal level
// notification
ShowUnableToOpenNotification(
profile, GetAlreadyBeingOpenedMessage(), GetAlreadyBeingOpenedTitle(),
/*warning_level=*/
message_center::SystemNotificationWarningLevel::NORMAL);
cloud_open_metrics->LogTaskResult(
OfficeTaskResult::kFileAlreadyBeingOpened);
return false;
}
} else {
LOG(ERROR) << "Cannot get EventRouter";
}
scoped_refptr<CloudOpenTask> upload_task = WrapRefCounted(new CloudOpenTask(
profile, file_urls, task, cloud_provider, std::move(cloud_open_metrics)));
// Keep `upload_task` alive until `TaskFinished` executes.
bool status = upload_task->ExecuteInternal();
return status;
}
CloudOpenTask::CloudOpenTask(
Profile* profile,
std::vector<storage::FileSystemURL> file_urls,
const fm_tasks::TaskDescriptor& task,
const CloudProvider cloud_provider,
std::unique_ptr<CloudOpenMetrics> cloud_open_metrics)
: profile_(profile),
file_urls_(file_urls),
task_(task),
cloud_provider_(cloud_provider),
cloud_open_metrics_(std::move(cloud_open_metrics)) {
BrowserList::AddObserver(this);
}
CloudOpenTask::~CloudOpenTask() {
auto* event_router =
file_manager::EventRouterFactory::GetForProfile(profile_);
DCHECK(!file_urls_.empty());
if (event_router) {
event_router->RemoveCloudOpenTask(file_urls_.front());
} else {
LOG(ERROR) << "Cannot get EventRouter";
}
BrowserList::RemoveObserver(this);
}
// Runs setup if it's never been completed. Runs the fixup version of setup if
// there are any issues, e.g. ODFS is not mounted. Otherwise, attempts to move
// files to the correct cloud or open the files if they are already there.
bool CloudOpenTask::ExecuteInternal() {
if (file_urls_.empty()) {
LOG(ERROR) << "No files to open";
cloud_open_metrics_->LogTaskResult(OfficeTaskResult::kNoFilesToOpen);
return false;
}
// Run the setup flow if we don't have explicit default file handlers set for
// these files in preferences. This indicates we haven't run setup, because
// setup sets default handlers at the end. If the user has a default set for
// another, non-office handler, then we won't get here except via the 'Open
// With' menu. In that case we might need to run fixup or just open/move the
// file, but without changing stored user file handler preferences.
if (!HaveExplicitFileHandlers(profile_, file_urls_)) {
RecordMicrosoft365Availability(kFirstTimeMicrosoft365AvailabilityMetric,
profile_);
return InitAndShowSetupOrMoveDialog(
SetupOrMoveDialogPage::kFileHandlerDialog);
}
return MaybeRunFixupFlow();
}
// Runs the fixup version of setup if there are any issues, e.g. ODFS is not
// mounted. Otherwise, attempts to move files to the correct cloud or open the
// files if they are already there.
bool CloudOpenTask::MaybeRunFixupFlow() {
if (ShouldFixUpOffice(profile_, cloud_provider_)) {
// TODO(cassycc): Use page specifically for fix up.
return InitAndShowSetupOrMoveDialog(SetupOrMoveDialogPage::kOneDriveSetup);
}
return OpenOrMoveFiles();
}
// Opens office files if they are in the correct cloud already. Otherwise moves
// the files before opening.
bool CloudOpenTask::OpenOrMoveFiles() {
// Record the source volume type of the opened file.
OfficeFilesSourceVolume source_volume;
if (UrlIsOnODFS(file_urls_.front())) {
source_volume = OfficeFilesSourceVolume::kMicrosoftOneDrive;
} else if (UrlIsOnAndroidOneDrive(profile_, file_urls_.front())) {
source_volume = OfficeFilesSourceVolume::kAndroidOneDriveDocumentsProvider;
} else {
auto* volume_manager = file_manager::VolumeManager::Get(profile_);
base::WeakPtr<file_manager::Volume> source =
volume_manager->FindVolumeFromPath(file_urls_.front().path());
if (source) {
source_volume = VolumeTypeToSourceVolume(source->type());
} else {
source_volume = OfficeFilesSourceVolume::kUnknown;
}
}
cloud_open_metrics_->LogSourceVolume(source_volume);
if (cloud_provider_ == CloudProvider::kGoogleDrive &&
PathIsOnDriveFS(profile_, file_urls_.front().path())) {
// The files are on Drive already.
transfer_required_ = OfficeFilesTransferRequired::kNotRequired;
cloud_open_metrics_->LogTransferRequired(
OfficeFilesTransferRequired::kNotRequired);
OpenAlreadyHostedDriveUrls();
return true;
}
if (cloud_provider_ == CloudProvider::kOneDrive &&
source_volume == OfficeFilesSourceVolume::kMicrosoftOneDrive) {
// The files are on OneDrive already, selected from ODFS.
transfer_required_ = OfficeFilesTransferRequired::kNotRequired;
cloud_open_metrics_->LogTransferRequired(
OfficeFilesTransferRequired::kNotRequired);
OpenODFSUrls(OfficeTaskResult::kOpened);
return true;
}
if (cloud_provider_ == CloudProvider::kOneDrive &&
source_volume ==
OfficeFilesSourceVolume::kAndroidOneDriveDocumentsProvider) {
// The files are on OneDrive already, selected from Android OneDrive.
transfer_required_ = OfficeFilesTransferRequired::kNotRequired;
cloud_open_metrics_->LogTransferRequired(
OfficeFilesTransferRequired::kNotRequired);
// Get ODFS email address, compare against Android OneDrive's email address
// and open URLs.
GetODFSMetadata(
GetODFS(profile_),
base::BindOnce(&CloudOpenTask::CheckEmailAndOpenAndroidOneDriveURLs,
this));
return true;
}
// The files need to be moved.
auto operation =
GetUploadType(profile_, file_urls_.front()) == UploadType::kCopy
? OfficeFilesTransferRequired::kCopy
: OfficeFilesTransferRequired::kMove;
transfer_required_ = operation;
cloud_open_metrics_->LogTransferRequired(operation);
return ConfirmMoveOrStartUpload();
}
void CloudOpenTask::OpenAlreadyHostedDriveUrls() {
drive::DriveIntegrationService* integration_service =
drive::DriveIntegrationServiceFactory::FindForProfile(profile_);
base::FilePath relative_path;
for (const auto& file_url : file_urls_) {
if (integration_service->GetRelativeDrivePath(file_url.path(),
&relative_path)) {
integration_service->GetDriveFsInterface()->GetMetadata(
relative_path,
base::BindOnce(&CloudOpenTask::OnGoogleDriveGetMetadata, this));
} else {
LOG(ERROR) << "Unexpected error obtaining the relative path ";
LogGoogleDriveOpenResultUMA(
OfficeTaskResult::kOpened,
OfficeDriveOpenErrors::kCannotGetRelativePath);
}
}
}
// Open an already hosted MS Office file e.g. .docx, from a url hosted in
// DriveFS. Check there was no error retrieving the file's metadata.
void CloudOpenTask::OnGoogleDriveGetMetadata(
drive::FileError error,
drivefs::mojom::FileMetadataPtr metadata) {
OfficeDriveOpenErrors open_result = OfficeDriveOpenErrors::kSuccess;
GURL hosted_url(metadata->alternate_url);
if (error != drive::FILE_ERROR_OK) {
LOG(ERROR) << "Drive metadata error: " << error;
open_result = OfficeDriveOpenErrors::kNoMetadata;
} else if (hosted_url.is_empty() &&
metadata->item_id.value_or("").starts_with("local-")) {
LOG(ERROR) << "Local item id, the file hasn't been uploaded";
open_result = OfficeDriveOpenErrors::kWaitingForUpload;
GetUserFallbackChoice(
profile_, task_, file_urls_,
ash::office_fallback::FallbackReason::kWaitingForUpload,
base::DoNothing());
} else if (hosted_url.is_empty()) {
LOG(ERROR) << "Empty URL";
open_result = OfficeDriveOpenErrors::kEmptyAlternateUrl;
} else if (!hosted_url.is_valid()) {
LOG(ERROR) << "Invalid URL";
open_result = OfficeDriveOpenErrors::kInvalidAlternateUrl;
} else if (hosted_url.host() == "drive.google.com") {
LOG(ERROR) << "URL was from drive.google.com";
open_result = OfficeDriveOpenErrors::kDriveAlternateUrl;
} else if (hosted_url.host() != "docs.google.com") {
LOG(ERROR) << "URL was not from docs.google.com";
open_result = OfficeDriveOpenErrors::kUnexpectedAlternateUrl;
} else {
// TODO(b/242685536) add support for multiple files.
::file_manager::util::OpenHostedFileInNewTabOrApp(
profile_, file_urls_.front().path(), base::DoNothing(),
net::AppendOrReplaceQueryParameter(hosted_url, "cros_files", "true"));
}
LogGoogleDriveOpenResultUMA(OfficeTaskResult::kOpened, open_result);
}
// Open a hosted MS Office file e.g. .docx, from a url hosted in
// DriveFS. Check the file was successfully uploaded to DriveFS.
void CloudOpenTask::OpenUploadedDriveUrl(const GURL& url,
const OfficeTaskResult task_result) {
// TODO(b/242685536) add support for multiple files.
::file_manager::util::OpenHostedFileInNewTabOrApp(
profile_, file_urls_.front().path(), base::DoNothing(),
net::AppendOrReplaceQueryParameter(url, "cros_files", "true"));
// TODO(b/296950967): This function logs both open result and task result (but
// only if open fails) metrics internally, pull them up to a higher level so
// all the metrics are logged in one place.
LogGoogleDriveOpenResultUMA(task_result, OfficeDriveOpenErrors::kSuccess);
}
void CloudOpenTask::OpenODFSUrls(const OfficeTaskResult task_result_uma) {
for (const auto& file_url : file_urls_) {
OpenODFSUrl(profile_, file_url,
base::BindOnce(&CloudOpenTask::LogOneDriveOpenResultUMA, this,
task_result_uma));
}
}
// Returns True if the confirmation dialog should be shown before uploading a
// file to a cloud location and opening it.
bool CloudOpenTask::ShouldShowConfirmationDialog() {
bool force_show_confirmation_dialog = false;
SourceType source_type = GetSourceType(profile_, file_urls_[0]);
if (cloud_provider_ == CloudProvider::kGoogleDrive) {
switch (source_type) {
case SourceType::READ_ONLY:
force_show_confirmation_dialog =
!fm_tasks::GetOfficeMoveConfirmationShownForLocalToDrive(
profile_) &&
!fm_tasks::GetOfficeMoveConfirmationShownForCloudToDrive(profile_);
break;
case SourceType::LOCAL:
force_show_confirmation_dialog =
!fm_tasks::GetOfficeMoveConfirmationShownForLocalToDrive(profile_);
break;
case SourceType::CLOUD:
force_show_confirmation_dialog =
!fm_tasks::GetOfficeMoveConfirmationShownForCloudToDrive(profile_);
break;
}
return force_show_confirmation_dialog ||
!fm_tasks::GetAlwaysMoveOfficeFilesToDrive(profile_);
} else if (cloud_provider_ == CloudProvider::kOneDrive) {
switch (source_type) {
case SourceType::READ_ONLY:
force_show_confirmation_dialog =
!fm_tasks::GetOfficeMoveConfirmationShownForLocalToOneDrive(
profile_) &&
!fm_tasks::GetOfficeMoveConfirmationShownForCloudToOneDrive(
profile_);
break;
case SourceType::LOCAL:
force_show_confirmation_dialog =
!fm_tasks::GetOfficeMoveConfirmationShownForLocalToOneDrive(
profile_);
break;
case SourceType::CLOUD:
force_show_confirmation_dialog =
!fm_tasks::GetOfficeMoveConfirmationShownForCloudToOneDrive(
profile_);
break;
}
return force_show_confirmation_dialog ||
!fm_tasks::GetAlwaysMoveOfficeFilesToOneDrive(profile_);
}
NOTREACHED_IN_MIGRATION();
return true;
}
bool CloudOpenTask::ConfirmMoveOrStartUpload() {
bool show_confirmation_dialog = ShouldShowConfirmationDialog();
if (show_confirmation_dialog) {
SetupOrMoveDialogPage dialog_page =
cloud_provider_ == CloudProvider::kGoogleDrive
? SetupOrMoveDialogPage::kMoveConfirmationGoogleDrive
: SetupOrMoveDialogPage::kMoveConfirmationOneDrive;
return InitAndShowSetupOrMoveDialog(dialog_page);
}
StartUpload();
return true;
}
bool ShouldFixUpOffice(Profile* profile, const CloudProvider cloud_provider) {
return cloud_provider == CloudProvider::kOneDrive &&
!(IsODFSMounted(profile) && IsOfficeWebAppInstalled(profile));
}
bool UrlIsOnAndroidOneDrive(Profile* profile, const FileSystemURL& url) {
std::string authority;
std::string root_id;
base::FilePath path;
return arc::ParseDocumentsProviderUrl(url, &authority, &root_id, &path) &&
authority == kAndroidOneDriveAuthority;
}
void CloudOpenTask::CheckEmailAndOpenAndroidOneDriveURLs(
base::expected<ODFSMetadata, base::File::Error> metadata_or_error) {
if (!metadata_or_error.has_value()) {
LOG(ERROR) << "Failed to get user email: " << metadata_or_error.error();
LogOneDriveOpenResultUMA(OfficeTaskResult::kOpened,
OfficeOneDriveOpenErrors::kGetActionsGenericError);
return;
}
if (metadata_or_error->user_email.empty()) {
LOG(ERROR) << "User email is empty";
LogOneDriveOpenResultUMA(OfficeTaskResult::kOpened,
OfficeOneDriveOpenErrors::kGetActionsNoEmail);
return;
}
// In Android OneDrive, the DocumentsProvider uses the email account
// associated with it as the root_id.
std::string authority;
std::string android_onedrive_email;
base::FilePath path;
if (!arc::ParseDocumentsProviderUrl(file_urls_.front(), &authority,
&android_onedrive_email, &path)) {
LogOneDriveOpenResultUMA(OfficeTaskResult::kOpened,
OfficeOneDriveOpenErrors::kInvalidFileSystemURL);
return;
}
// Proceed only if the Android OneDrive and ODFS email addresses match.
if (base::ToLowerASCII(android_onedrive_email) !=
base::ToLowerASCII(metadata_or_error->user_email)) {
LOG(ERROR) << "Email accounts associated with ODFS and "
"Android OneDrive don't match.";
LogOneDriveOpenResultUMA(OfficeTaskResult::kOpened,
OfficeOneDriveOpenErrors::kEmailsDoNotMatch);
return;
}
// TODO(b/242685536) add support for multiple files.
OpenAndroidOneDriveUrl(file_urls_[0]);
}
// Open office file, originally selected from Android OneDrive, from ODFS. First
// convert the |android_onedrive_urls| to ODFS file paths, then open them from
// ODFS in the MS 365 PWA.
void CloudOpenTask::OpenAndroidOneDriveUrl(
const FileSystemURL& android_onedrive_file_url) {
// TODO(b/269364287): Handle when Android OneDrive file can't be opened.
if (!UrlIsOnAndroidOneDrive(profile_, android_onedrive_file_url)) {
LOG(ERROR) << "File not on Android OneDrive";
LogOneDriveOpenResultUMA(
OfficeTaskResult::kOpened,
OfficeOneDriveOpenErrors::kConversionToODFSUrlError);
return;
}
// Get the ODFS mount path.
std::optional<ProvidedFileSystemInfo> odfs_file_system_info =
GetODFSInfo(profile_);
if (!odfs_file_system_info.has_value()) {
LOG(ERROR) << "ODFS not found";
LogOneDriveOpenResultUMA(
OfficeTaskResult::kOpened,
OfficeOneDriveOpenErrors::kConversionToODFSUrlError);
return;
}
base::FilePath odfs_path = odfs_file_system_info->mount_path();
// Find the relative path from Android OneDrive Url.
std::string authority;
std::string root_id;
base::FilePath path;
if (!arc::ParseDocumentsProviderUrl(android_onedrive_file_url, &authority,
&root_id, &path)) {
LOG(ERROR) << "Could not parse Android OneDrive Url";
LogOneDriveOpenResultUMA(
OfficeTaskResult::kOpened,
OfficeOneDriveOpenErrors::kConversionToODFSUrlError);
return;
}
// Format for Android OneDrive documents provider `path` is:
// Files/<rel_path>
std::vector<base::FilePath::StringType> components =
base::FilePath(path.value()).GetComponents();
if (components.size() < 2) {
LOG(ERROR)
<< "Android OneDrive documents provider path is not as expected.";
LogOneDriveOpenResultUMA(
OfficeTaskResult::kOpened,
OfficeOneDriveOpenErrors::kAndroidOneDriveInvalidUrl);
return;
}
if (components[0] != "Files") {
ash::office_fallback::FallbackReason fallback_reason = ash::
office_fallback::FallbackReason::kAndroidOneDriveUnsupportedLocation;
// `cloud_open_metrics_` can be safely moved since CloudUploadTask is
// expected to be destructed straight after.
GetUserFallbackChoice(
profile_, task_, file_urls_, fallback_reason,
base::BindOnce(
&OnWaitingForAndroidUnsupportedPathFallbackChoiceReceived, profile_,
task_, file_urls_, fallback_reason,
std::move(cloud_open_metrics_)));
return;
}
// Append relative path from Android OneDrive Url.
for (size_t i = 1; i < components.size(); i++) {
odfs_path = odfs_path.Append(components[i]);
}
ash::file_system_provider::util::LocalPathParser parser(profile_, odfs_path);
if (!parser.Parse()) {
LOG(ERROR) << "Path not in FSP";
LogOneDriveOpenResultUMA(
OfficeTaskResult::kOpened,
OfficeOneDriveOpenErrors::kConversionToODFSUrlError);
return;
}
OpenFileFromODFS(profile_, parser.file_system(), parser.file_path(),
base::BindOnce(&CloudOpenTask::LogOneDriveOpenResultUMA,
this, OfficeTaskResult::kOpened));
return;
}
void CloudOpenTask::StartUpload() {
DCHECK_EQ(file_urls_idx_, 0UL);
upload_timer_ = base::ElapsedTimer();
// CloudOpenTask is the only owner of the `CloudOpenMetrics` object and will
// still be alive after the upload handler completes. Thus, pass a `SafeRef`
// of `CloudOpenMetrics` to the upload handler.
if (cloud_provider_ == CloudProvider::kGoogleDrive) {
StartNextGoogleDriveUpload();
} else if (cloud_provider_ == CloudProvider::kOneDrive) {
StartNextOneDriveUpload();
}
}
void CloudOpenTask::StartNextGoogleDriveUpload() {
DCHECK_LT(file_urls_idx_, file_urls_.size());
drive_upload_handler_ = std::make_unique<DriveUploadHandler>(
profile_, file_urls_[file_urls_idx_],
base::BindOnce(&CloudOpenTask::FinishedDriveUpload, this),
cloud_open_metrics_->GetSafeRef());
drive_upload_handler_->Run();
}
void CloudOpenTask::StartNextOneDriveUpload() {
DCHECK_LT(file_urls_idx_, file_urls_.size());
one_drive_upload_handler_ = std::make_unique<OneDriveUploadHandler>(
profile_, file_urls_[file_urls_idx_],
base::BindOnce(&CloudOpenTask::FinishedOneDriveUpload, this,
profile_->GetWeakPtr()),
cloud_open_metrics_->GetSafeRef());
one_drive_upload_handler_->Run();
}
void CloudOpenTask::FinishedDriveUpload(OfficeTaskResult task_result,
std::optional<GURL> url,
int64_t size) {
DCHECK_LT(file_urls_idx_, file_urls_.size());
if (url.has_value()) {
upload_total_size_ += size;
fm_tasks::SetOfficeFileMovedToGoogleDrive(profile_, base::Time::Now());
// Log TaskResult after open is tried.
OpenUploadedDriveUrl(url.value(), task_result);
} else {
cloud_open_metrics_->LogTaskResult(task_result);
has_upload_errors_ = has_upload_errors_ ||
(task_result == OfficeTaskResult::kFailedToUpload);
}
file_urls_idx_++;
if (file_urls_idx_ < file_urls_.size()) {
StartNextGoogleDriveUpload();
return;
}
if (!has_upload_errors_) {
RecordUploadLatencyUMA();
}
}
void CloudOpenTask::FinishedOneDriveUpload(
base::WeakPtr<Profile> profile_weak_ptr,
OfficeTaskResult task_result,
std::optional<storage::FileSystemURL> url,
int64_t size) {
DCHECK_LT(file_urls_idx_, file_urls_.size());
if (url.has_value()) {
upload_total_size_ += size;
Profile* profile = profile_weak_ptr.get();
if (!profile) {
// TODO(b/296950967): metric to log here?
return;
}
fm_tasks::SetOfficeFileMovedToOneDrive(profile, base::Time::Now());
// Log TaskResult after open is tried.
OpenODFSUrl(profile, url.value(),
base::BindOnce(&CloudOpenTask::LogOneDriveOpenResultUMA, this,
task_result));
} else {
cloud_open_metrics_->LogTaskResult(task_result);
has_upload_errors_ = has_upload_errors_ ||
(task_result == OfficeTaskResult::kFailedToUpload);
}
file_urls_idx_++;
if (file_urls_idx_ < file_urls_.size()) {
StartNextOneDriveUpload();
return;
}
if (!has_upload_errors_) {
RecordUploadLatencyUMA();
}
}
// Logs UMA when the Drive task ends with an attempt to open a file.
void CloudOpenTask::LogGoogleDriveOpenResultUMA(
OfficeTaskResult success_task_result,
OfficeDriveOpenErrors open_result) {
cloud_open_metrics_->LogGoogleDriveOpenError(open_result);
cloud_open_metrics_->LogTaskResult(open_result ==
OfficeDriveOpenErrors::kSuccess
? success_task_result
: OfficeTaskResult::kFailedToOpen);
}
// Logs UMA when the OneDrive task ends with an attempt to open a file.
void CloudOpenTask::LogOneDriveOpenResultUMA(
OfficeTaskResult success_task_result,
OfficeOneDriveOpenErrors open_result) {
cloud_open_metrics_->LogOneDriveOpenError(open_result);
cloud_open_metrics_->LogTaskResult(open_result ==
OfficeOneDriveOpenErrors::kSuccess
? success_task_result
: OfficeTaskResult::kFailedToOpen);
}
void CloudOpenTask::RecordUploadLatencyUMA() {
constexpr int64_t kMegabyte = 1000 * 1000;
std::string uma_size;
if (upload_total_size_ > 1000 * kMegabyte) {
uma_size = "1000MB-and-above";
} else if (upload_total_size_ > 100 * kMegabyte) {
uma_size = "0100MB-to-1GB";
} else if (upload_total_size_ > 10 * kMegabyte) {
uma_size = "0010MB-to-100MB";
} else if (upload_total_size_ > 1 * kMegabyte) {
uma_size = "0001MB-to-10MB";
} else if (upload_total_size_ <= 1 * kMegabyte) {
uma_size = "0000MB-to-1MB";
}
auto* transfer =
(transfer_required_ == OfficeFilesTransferRequired::kCopy ? "Copy"
: "Move");
auto* provider =
(cloud_provider_ == CloudProvider::kGoogleDrive ? "GoogleDrive"
: "OneDrive");
const auto metric = base::StrCat({"FileBrowser.OfficeFiles.FileOpen.Time.",
transfer, ".", uma_size, ".To.", provider});
base::UmaHistogramMediumTimes(metric, upload_timer_.Elapsed());
}
// Create the arguments necessary for showing the dialog. We first need to
// collect local file tasks, if we are trying to show the kFileHandlerDialog
// page.
bool CloudOpenTask::InitAndShowSetupOrMoveDialog(
SetupOrMoveDialogPage dialog_page) {
// Allow no more than one upload dialog at a time. If one already exists,
// bring it to the front to prompt the user to keep going. In the case of
// multiple upload requests, they should either be handled simultaneously or
// queued.
if (BringDialogToFrontIfItExists(chrome::kChromeUICloudUploadURL)) {
LOG(WARNING) << "Another cloud upload dialog is already being shown";
if (dialog_page == SetupOrMoveDialogPage::kMoveConfirmationGoogleDrive ||
dialog_page == SetupOrMoveDialogPage::kMoveConfirmationOneDrive) {
cloud_open_metrics_->LogTaskResult(
OfficeTaskResult::kCannotShowMoveConfirmation);
} else {
cloud_open_metrics_->LogTaskResult(
OfficeTaskResult::kCannotShowSetupDialog);
}
return false;
}
mojom::DialogArgsPtr args = CreateDialogArgs(dialog_page);
// Display local file handlers (tasks) only for the file handler dialog.
if (dialog_page == SetupOrMoveDialogPage::kFileHandlerDialog) {
// Callback to show the dialog after the tasks have been found.
fm_tasks::FindTasksCallback find_all_types_of_tasks_callback =
base::BindOnce(&CloudOpenTask::ShowDialog, this, dialog_page,
std::move(args));
// Find the file tasks that can open the `file_urls_` and then run
// `ShowDialog`.
FindTasksForDialog(std::move(find_all_types_of_tasks_callback));
} else {
ShowDialog(dialog_page, std::move(args), nullptr);
}
return true;
}
mojom::DialogArgsPtr CloudOpenTask::CreateDialogArgs(
SetupOrMoveDialogPage dialog_page) {
mojom::DialogArgsPtr args = mojom::DialogArgs::New();
for (const auto& file_url : file_urls_) {
args->file_names.push_back(file_url.path().BaseName().value());
}
switch (dialog_page) {
case SetupOrMoveDialogPage::kFileHandlerDialog: {
auto file_handler_dialog_args = mojom::FileHandlerDialogArgs::New();
file_handler_dialog_args->show_google_workspace_task =
chromeos::cloud_upload::IsGoogleWorkspaceCloudUploadAllowed(profile_);
file_handler_dialog_args->show_microsoft_office_task =
chromeos::cloud_upload::IsMicrosoftOfficeCloudUploadAllowed(profile_);
args->dialog_specific_args =
mojom::DialogSpecificArgs::NewFileHandlerDialogArgs(
std::move(file_handler_dialog_args));
break;
}
case SetupOrMoveDialogPage::kOneDriveSetup: {
auto one_drive_setup_dialog_args = mojom::OneDriveSetupDialogArgs::New();
one_drive_setup_dialog_args->set_office_as_default_handler =
!HaveExplicitFileHandlers(profile_, file_urls_);
args->dialog_specific_args =
mojom::DialogSpecificArgs::NewOneDriveSetupDialogArgs(
std::move(one_drive_setup_dialog_args));
break;
}
case SetupOrMoveDialogPage::kMoveConfirmationOneDrive: {
auto move_confirmation_one_drive_dialog_args =
mojom::MoveConfirmationOneDriveDialogArgs::New();
move_confirmation_one_drive_dialog_args->operation_type =
UploadTypeToOperationType(GetUploadType(profile_, file_urls_[0]));
args->dialog_specific_args =
mojom::DialogSpecificArgs::NewMoveConfirmationOneDriveDialogArgs(
std::move(move_confirmation_one_drive_dialog_args));
break;
}
case SetupOrMoveDialogPage::kMoveConfirmationGoogleDrive: {
auto move_confirmation_google_drive_dialog_args =
mojom::MoveConfirmationGoogleDriveDialogArgs::New();
move_confirmation_google_drive_dialog_args->operation_type =
UploadTypeToOperationType(GetUploadType(profile_, file_urls_[0]));
args->dialog_specific_args =
mojom::DialogSpecificArgs::NewMoveConfirmationGoogleDriveDialogArgs(
std::move(move_confirmation_google_drive_dialog_args));
break;
}
}
return args;
}
// Creates and shows a new dialog for the cloud upload workflow. If there are
// local file tasks from `resulting_tasks`, include them in the dialog
// arguments. These tasks are can be selected by the user to open the files
// instead of using a cloud provider. If there is no Files app window currently
// open to use as a modal parent for the dialog, first launches a new Files app
// window, which we listen for in OnBrowserAdded().
void CloudOpenTask::ShowDialog(
SetupOrMoveDialogPage dialog_page,
mojom::DialogArgsPtr args,
std::unique_ptr<fm_tasks::ResultingTasks> resulting_tasks) {
if (resulting_tasks) {
SetTaskArgs(args, std::move(resulting_tasks));
if (chromeos::features::IsUploadOfficeToCloudForEnterpriseEnabled()) {
const auto& file_handler_dialog_args =
args->dialog_specific_args->get_file_handler_dialog_args();
// When there is only one possible task (Microsoft or Google) and no
// further local tasks, skip the file handler page and either show the
// OneDrive setup if necessary, or go straight to opening/moving the
// files.
if ((!file_handler_dialog_args->show_microsoft_office_task ||
!file_handler_dialog_args->show_google_workspace_task) &&
local_tasks_.empty()) {
// Validate that `cloud_provider_` differs from the disabled task.
CHECK(!(cloud_provider_ == CloudProvider::kOneDrive &&
!file_handler_dialog_args->show_microsoft_office_task));
CHECK(!(cloud_provider_ == CloudProvider::kGoogleDrive &&
!file_handler_dialog_args->show_google_workspace_task));
MaybeRunFixupFlow();
return;
}
}
}
bool office_move_confirmation_shown =
cloud_provider_ == CloudProvider::kGoogleDrive
? fm_tasks::GetOfficeMoveConfirmationShownForDrive(profile_)
: fm_tasks::GetOfficeMoveConfirmationShownForOneDrive(profile_);
base::OnceCallback<void(const std::string&)> dialog_callback;
switch (dialog_page) {
case SetupOrMoveDialogPage::kFileHandlerDialog:
case SetupOrMoveDialogPage::kOneDriveSetup:
dialog_callback =
base::BindOnce(&CloudOpenTask::OnSetupDialogComplete, this);
break;
case SetupOrMoveDialogPage::kMoveConfirmationOneDrive:
case SetupOrMoveDialogPage::kMoveConfirmationGoogleDrive:
dialog_callback =
base::BindOnce(&CloudOpenTask::OnMoveConfirmationComplete, this);
break;
}
// This CloudUploadDialog pointer is managed by an instance of
// `views::WebDialogView` and deleted in
// `SystemWebDialogDelegate::OnDialogClosed`.
CloudUploadDialog* dialog =
new CloudUploadDialog(std::move(args), std::move(dialog_callback),
office_move_confirmation_shown);
// Get Files App window, if it exists.
files_app_browser_ =
FindSystemWebAppBrowser(profile_, ash::SystemWebAppType::FILE_MANAGER);
gfx::NativeWindow modal_parent =
files_app_browser_ ? files_app_browser_->window()->GetNativeWindow()
: nullptr;
if (!modal_parent) {
need_new_files_app_ = true;
DCHECK(!pending_dialog_);
pending_dialog_ = dialog;
// Create a files app window and use it as the modal parent. CloudOpenTask
// is kept alive by the callback passed to CloudUploadDialog above. We
// expect this to trigger OnBrowserAdded, which then shows the dialog.
file_manager::util::ShowItemInFolder(profile_, file_urls_.at(0).path(),
base::DoNothing());
} else {
dialog->ShowSystemDialog(modal_parent);
}
}
// Stores constructed tasks into
// `args->dialog_specific_args->file_handler_dialog_args->local_tasks` and
// `local_tasks_`.
void CloudOpenTask::SetTaskArgs(
mojom::DialogArgsPtr& args,
std::unique_ptr<fm_tasks::ResultingTasks> resulting_tasks) {
int nextPosition = 0;
auto& file_handler_dialog_args =
args->dialog_specific_args->get_file_handler_dialog_args();
for (fm_tasks::FullTaskDescriptor& task : resulting_tasks->tasks) {
// Ignore Google Docs and MS Office tasks as they are already
// set up to show in the dialog.
if (fm_tasks::IsWebDriveOfficeTask(task.task_descriptor) ||
fm_tasks::IsOpenInOfficeTask(task.task_descriptor)) {
continue;
}
mojom::DialogTaskPtr dialog_task = mojom::DialogTask::New();
// The (unique and positive) `position` of the task in the `tasks` vector.
// If the user responds with the `position`, the task will be launched via
// `LaunchLocalFileTask()`.
dialog_task->position = nextPosition++;
dialog_task->title = task.task_title;
dialog_task->icon_url = task.icon_url.spec();
dialog_task->app_id = task.task_descriptor.app_id;
file_handler_dialog_args->local_tasks.push_back(std::move(dialog_task));
local_tasks_.push_back(std::move(task.task_descriptor));
}
}
void CloudOpenTask::OnBrowserAdded(Browser* browser) {
if (!need_new_files_app_) {
return;
}
// TODO(petermarshall): Add a timeout. If Files app never launches for some
// reason, then we will never show the dialog.
DCHECK(pending_dialog_);
if (!IsBrowserForSystemWebApp(browser, SystemWebAppType::FILE_MANAGER)) {
// Wait for Files app to launch.
LOG(WARNING) << "Browser did not match Files app";
return;
}
need_new_files_app_ = false;
files_app_browser_ = browser;
pending_dialog_->ShowSystemDialog(
files_app_browser_->window()->GetNativeWindow());
// The dialog is deleted in `SystemWebDialogDelegate::OnDialogClosed`.
pending_dialog_ = nullptr;
}
void CloudOpenTask::OnBrowserClosing(Browser* browser) {
if (browser == files_app_browser_) {
// The Files app that the dialog is modal to is closed. This will close the
// dialog with an empty user response.
files_app_closed_ = true;
}
}
// Receive user's setup dialog response and acts accordingly. `user_response` is
// either a particular ash::cloud_upload::mojom::UserAction or the id (position)
// of the task in `local_tasks_` to launch. We never use the return value but
// it's necessary to make sure that we delete CloudOpenTask when we're done.
void CloudOpenTask::OnSetupDialogComplete(const std::string& user_response) {
if (user_response == kUserActionConfirmOrUploadToGoogleDrive) {
cloud_provider_ = CloudProvider::kGoogleDrive;
cloud_open_metrics_->set_cloud_provider(cloud_provider_);
// Because we treat Docs/Sheets/Slides as three separate apps, only set
// the default handler for the types that we are dealing with.
// We don't currently check MIME types, which could mean we get into edge
// cases if the MIME type doesn't match the file extension.
if (HasWordFile(file_urls_)) {
UMA_HISTOGRAM_ENUMERATION(kFileHandlerSelectionMetricName,
OfficeSetupFileHandler::kGoogleDocs);
fm_tasks::SetWordFileHandlerToFilesSWA(
profile_, fm_tasks::kActionIdWebDriveOfficeWord);
}
if (HasExcelFile(file_urls_)) {
UMA_HISTOGRAM_ENUMERATION(kFileHandlerSelectionMetricName,
OfficeSetupFileHandler::kGoogleSheets);
fm_tasks::SetExcelFileHandlerToFilesSWA(
profile_, fm_tasks::kActionIdWebDriveOfficeExcel);
}
if (HasPowerPointFile(file_urls_)) {
UMA_HISTOGRAM_ENUMERATION(kFileHandlerSelectionMetricName,
OfficeSetupFileHandler::kGoogleSlides);
fm_tasks::SetPowerPointFileHandlerToFilesSWA(
profile_, fm_tasks::kActionIdWebDriveOfficePowerPoint);
}
OpenOrMoveFiles();
} else if (user_response == kUserActionConfirmOrUploadToOneDrive) {
// Default handlers have already been set by this point for
// Office/OneDrive.
OpenOrMoveFiles();
} else if (user_response == kUserActionSetUpOneDrive) {
UMA_HISTOGRAM_ENUMERATION(kFileHandlerSelectionMetricName,
OfficeSetupFileHandler::kMicrosoft365);
cloud_provider_ = CloudProvider::kOneDrive;
cloud_open_metrics_->set_cloud_provider(cloud_provider_);
InitAndShowSetupOrMoveDialog(SetupOrMoveDialogPage::kOneDriveSetup);
} else if (user_response == kUserActionCancel) {
cloud_open_metrics_->LogTaskResult(OfficeTaskResult::kCancelledAtSetup);
// Do nothing.
} else if (!user_response.empty()) {
cloud_open_metrics_->LogTaskResult(OfficeTaskResult::kLocalFileTask);
LaunchLocalFileTask(user_response);
} else {
// Always map an empty user response to a Cancel user response. This can
// occur when the Files app the dialog was modal to is closed.
if (!files_app_closed_) {
// This can also occur when the user logs out of the session. However,
// since there could be other unknown causes, leave a log.
LOG(ERROR) << "Empty user response not due to the files app closing";
}
cloud_open_metrics_->LogTaskResult(OfficeTaskResult::kCancelledAtSetup);
}
}
// Receive user's move confirmation response and acts accordingly.
// `user_response` is a particular ash::cloud_upload::mojom::UserAction. We
// never use the return value but it's necessary to make sure that we delete
// CloudOpenTask when we're done.
void CloudOpenTask::OnMoveConfirmationComplete(
const std::string& user_response) {
// TODO(petermarshall): Don't need separate actions for drive/onedrive now
// (and for StartUpload?).
if (user_response == kUserActionUploadToGoogleDrive) {
fm_tasks::SetOfficeMoveConfirmationShownForDrive(profile_, true);
SourceType source_type = GetSourceType(profile_, file_urls_[0]);
switch (source_type) {
case SourceType::LOCAL:
fm_tasks::SetOfficeMoveConfirmationShownForLocalToDrive(profile_, true);
break;
case SourceType::CLOUD:
fm_tasks::SetOfficeMoveConfirmationShownForCloudToDrive(profile_, true);
break;
case SourceType::READ_ONLY:
// TODO (jboulic): Clarify UX.
break;
}
StartUpload();
} else if (user_response == kUserActionUploadToOneDrive) {
fm_tasks::SetOfficeMoveConfirmationShownForOneDrive(profile_, true);
SourceType source_type = GetSourceType(profile_, file_urls_[0]);
switch (source_type) {
case SourceType::LOCAL:
fm_tasks::SetOfficeMoveConfirmationShownForLocalToOneDrive(profile_,
true);
break;
case SourceType::CLOUD:
fm_tasks::SetOfficeMoveConfirmationShownForCloudToOneDrive(profile_,
true);
break;
case SourceType::READ_ONLY:
// TODO (jboulic): Clarify UX.
break;
}
StartUpload();
} else if (user_response == kUserActionCancelGoogleDrive ||
user_response == kUserActionCancelOneDrive) {
cloud_open_metrics_->LogTaskResult(
OfficeTaskResult::kCancelledAtConfirmation);
} else if (!user_response.empty()) {
LOG(ERROR) << "Unhandled response: " << user_response;
} else {
// Always map an empty user response to a Cancel user response. This can
// occur when the Files app the dialog was modal to is closed.
if (!files_app_closed_) {
// This can also occur when the user logs out of the session. However,
// since there could be other unknown causes, leave a log.
LOG(ERROR) << "Empty user response not due to the files app closing";
}
cloud_open_metrics_->LogTaskResult(
OfficeTaskResult::kCancelledAtConfirmation);
}
}
// Launch the local file task in `local_tasks_` with the position specified by
// `string_task_position`.
void CloudOpenTask::LaunchLocalFileTask(
const std::string& string_task_position) {
// Convert the `string_task_position` - the string of the task position in
// `local_tasks_` - to an int. Ensure that it is within the range of
// `local_tasks_`.
int task_position;
if (!base::StringToInt(string_task_position, &task_position) ||
task_position < 0 ||
static_cast<size_t>(task_position) >= local_tasks_.size()) {
LOG(ERROR) << "Position for local file task is unexpectedly unable to be "
"retrieved. Retrieved position: "
<< string_task_position
<< " from user response: " << string_task_position;
return;
}
// Launch the task.
fm_tasks::TaskDescriptor& task = local_tasks_[task_position];
UMA_HISTOGRAM_ENUMERATION(kFileHandlerSelectionMetricName,
extension_misc::IsQuickOfficeExtension(task.app_id)
? OfficeSetupFileHandler::kQuickOffice
: OfficeSetupFileHandler::kOtherLocalHandler);
fm_tasks::ExecuteFileTask(
profile_, task, file_urls_,
base::BindOnce(&CloudOpenTask::LocalTaskExecuted, this, task));
}
// We never use the return value but it's necessary to make sure that we delete
// CloudOpenTask when we're done.
void CloudOpenTask::LocalTaskExecuted(
const fm_tasks::TaskDescriptor& task,
extensions::api::file_manager_private::TaskResult result,
std::string error_message) {
if (!error_message.empty()) {
LOG(ERROR) << "Execution of local file task with app id " << task.app_id
<< " to open office files. Led to error message: "
<< error_message
<< " and result: " << base::to_underlying(result);
return;
}
if (HasWordFile(file_urls_)) {
fm_tasks::SetWordFileHandler(profile_, task);
}
if (HasExcelFile(file_urls_)) {
fm_tasks::SetExcelFileHandler(profile_, task);
}
if (HasPowerPointFile(file_urls_)) {
fm_tasks::SetPowerPointFileHandler(profile_, task);
}
}
// Find the file tasks that can open the `file_urls` and pass them to the
// `find_all_types_of_tasks_callback`.
void CloudOpenTask::FindTasksForDialog(
fm_tasks::FindTasksCallback find_all_types_of_tasks_callback) {
using extensions::app_file_handler_util::MimeTypeCollector;
// Get the file info for finding the tasks.
std::vector<base::FilePath> local_paths;
std::vector<GURL> gurls;
for (const auto& file_url : file_urls_) {
local_paths.push_back(file_url.path());
gurls.push_back(file_url.ToGURL());
}
// Get the mime types of the files and then pass them to the callback to
// get the entries.
std::unique_ptr<MimeTypeCollector> mime_collector =
std::make_unique<MimeTypeCollector>(profile_);
auto* mime_collector_ptr = mime_collector.get();
mime_collector_ptr->CollectForLocalPaths(
local_paths,
base::BindOnce(&CloudOpenTask::ConstructEntriesAndFindTasks, this,
local_paths, gurls, std::move(mime_collector),
std::move(find_all_types_of_tasks_callback)));
}
void CloudOpenTask::ConstructEntriesAndFindTasks(
const std::vector<base::FilePath>& file_paths,
const std::vector<GURL>& gurls,
std::unique_ptr<extensions::app_file_handler_util::MimeTypeCollector>
mime_collector,
fm_tasks::FindTasksCallback find_all_types_of_tasks_callback,
std::unique_ptr<std::vector<std::string>> mime_types) {
std::vector<extensions::EntryInfo> entries;
DCHECK_EQ(file_paths.size(), mime_types->size());
for (size_t i = 0; i < file_paths.size(); ++i) {
entries.emplace_back(file_paths[i], (*mime_types)[i], false);
}
const std::vector<std::string> dlp_source_urls(entries.size(), "");
fm_tasks::FindAllTypesOfTasks(profile_, entries, gurls, dlp_source_urls,
std::move(find_all_types_of_tasks_callback));
}
void CloudOpenTask::SetTasksForTest(
const std::vector<fm_tasks::TaskDescriptor>& tasks) {
local_tasks_ = tasks;
}
void CloudUploadDialog::OnDialogShown(content::WebUI* webui) {
CHECK(dialog_args_);
SystemWebDialogDelegate::OnDialogShown(webui);
static_cast<CloudUploadUI*>(webui->GetController())
->SetDialogArgs(dialog_args_.Clone());
}
void CloudUploadDialog::OnDialogClosed(const std::string& json_retval) {
UploadRequestCallback callback = std::move(callback_);
// Deletes this, so we store the `callback` first.
SystemWebDialogDelegate::OnDialogClosed(json_retval);
// The callback can create a new dialog. It must be called last because we
// can only have one of these dialogs at a time.
if (callback) {
std::move(callback).Run(json_retval);
}
}
CloudUploadDialog::CloudUploadDialog(mojom::DialogArgsPtr args,
UploadRequestCallback callback,
bool office_move_confirmation_shown)
: SystemWebDialogDelegate(GURL(chrome::kChromeUICloudUploadURL),
std::u16string() /* title */),
dialog_args_(std::move(args)),
callback_(std::move(callback)),
office_move_confirmation_shown_(office_move_confirmation_shown) {}
CloudUploadDialog::~CloudUploadDialog() = default;
ui::mojom::ModalType CloudUploadDialog::GetDialogModalType() const {
return ui::mojom::ModalType::kWindow;
}
bool CloudUploadDialog::ShouldCloseDialogOnEscape() const {
// All the dialogs handle an Escape keydown.
return false;
}
bool CloudUploadDialog::ShouldShowCloseButton() const {
return false;
}
namespace {
constexpr int kDialogWidthForOneDriveSetup = 512;
constexpr int kDialogHeightForOneDriveSetup = 556;
constexpr int kDialogWidthForFileHandlerDialog = 512;
constexpr int kDialogHeightForFileHandlerDialog = 379;
constexpr int kDialogHeightForFileHandlerDialogNoLocalApp = 315;
constexpr int kDialogHeightForFileHandlerDialogOneHandlerMissing = 295;
constexpr int kDialogWidthForMoveConfirmation = 512;
constexpr int kDialogHeightForMoveConfirmationWithCheckbox = 524;
constexpr int kDialogHeightForMoveConfirmationWithoutCheckbox = 472;
constexpr int kDialogWidthForConnectToOneDrive = 512;
constexpr int kDialogHeightForConnectToOneDrive = 556;
} // namespace
void CloudUploadDialog::GetDialogSize(gfx::Size* size) const {
const auto& dialog_specific_args = dialog_args_->dialog_specific_args;
if (dialog_specific_args->is_file_handler_dialog_args()) {
const auto& file_handler_dialog_args =
dialog_specific_args->get_file_handler_dialog_args();
const bool has_local_tasks = !file_handler_dialog_args->local_tasks.empty();
const bool is_microsoft_office_or_google_workspace_disabled_by_policy =
!file_handler_dialog_args->show_microsoft_office_task ||
!file_handler_dialog_args->show_google_workspace_task;
size->set_width(kDialogWidthForFileHandlerDialog);
if (is_microsoft_office_or_google_workspace_disabled_by_policy) {
CHECK(has_local_tasks);
size->set_height(kDialogHeightForFileHandlerDialogOneHandlerMissing);
} else {
size->set_height(has_local_tasks
? kDialogHeightForFileHandlerDialog
: kDialogHeightForFileHandlerDialogNoLocalApp);
}
} else if (dialog_specific_args->is_one_drive_setup_dialog_args()) {
size->set_width(kDialogWidthForOneDriveSetup);
size->set_height(kDialogHeightForOneDriveSetup);
} else if (dialog_specific_args
->is_move_confirmation_google_drive_dialog_args() ||
dialog_specific_args
->is_move_confirmation_one_drive_dialog_args()) {
size->set_width(kDialogWidthForMoveConfirmation);
if (office_move_confirmation_shown_) {
size->set_height(kDialogHeightForMoveConfirmationWithCheckbox);
} else {
size->set_height(kDialogHeightForMoveConfirmationWithoutCheckbox);
}
} else if (dialog_specific_args->is_connect_to_one_drive_dialog_args()) {
size->set_width(kDialogWidthForConnectToOneDrive);
size->set_height(kDialogHeightForConnectToOneDrive);
} else {
NOTREACHED_IN_MIGRATION();
}
}
bool ShowConnectOneDriveDialog(gfx::NativeWindow modal_parent) {
// Allow no more than one upload dialog at a time. If one already exists,
// bring it to the front to prompt the user to keep going. Only one of either
// this dialog, or CloudOpenTask can be shown at a time because they use the
// same WebUI for dialogs.
if (BringDialogToFrontIfItExists(chrome::kChromeUICloudUploadURL)) {
LOG(WARNING) << "Another cloud upload dialog is already being shown";
return false;
}
mojom::DialogArgsPtr args = mojom::DialogArgs::New();
args->dialog_specific_args =
mojom::DialogSpecificArgs::NewConnectToOneDriveDialogArgs(
mojom::ConnectToOneDriveDialogArgs::New());
// This CloudUploadDialog pointer is managed by an instance of
// `views::WebDialogView` and deleted in
// `SystemWebDialogDelegate::OnDialogClosed`.
CloudUploadDialog* dialog =
new CloudUploadDialog(std::move(args), base::DoNothing(),
/*office_move_confirmation_shown=*/false);
dialog->ShowSystemDialog(modal_parent);
return true;
}
void LaunchMicrosoft365Setup(Profile* profile, gfx::NativeWindow modal_parent) {
mojom::DialogArgsPtr args = mojom::DialogArgs::New();
auto one_drive_setup_dialog_args = mojom::OneDriveSetupDialogArgs::New();
// If `set_office_as_default_handler` is false, it indicates that we already
// ran the Office setup and set file handler preferences for all handled
// Office file types, or that the user has pre-existing preferences for these
// file types.
one_drive_setup_dialog_args->set_office_as_default_handler =
!HaveExplicitFileHandlers(profile, fm_tasks::WordGroupExtensions()) ||
!HaveExplicitFileHandlers(profile, fm_tasks::ExcelGroupExtensions()) ||
!HaveExplicitFileHandlers(profile, fm_tasks::PowerPointGroupExtensions());
args->dialog_specific_args =
mojom::DialogSpecificArgs::NewOneDriveSetupDialogArgs(
std::move(one_drive_setup_dialog_args));
// This CloudUploadDialog pointer is managed by an instance of
// `views::WebDialogView` and deleted in
// `SystemWebDialogDelegate::OnDialogClosed`.
CloudUploadDialog* dialog =
new CloudUploadDialog(std::move(args), base::DoNothing(),
/*office_move_confirmation_shown=*/false);
dialog->ShowSystemDialog(modal_parent);
}
} // namespace ash::cloud_upload