chromium/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h

// 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.

#ifndef CHROME_BROWSER_UI_WEBUI_ASH_CLOUD_UPLOAD_CLOUD_UPLOAD_DIALOG_H_
#define CHROME_BROWSER_UI_WEBUI_ASH_CLOUD_UPLOAD_CLOUD_UPLOAD_DIALOG_H_

#include <optional>
#include <vector>

#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/timer/elapsed_timer.h"
#include "chrome/browser/ash/file_manager/file_tasks.h"
#include "chrome/browser/ash/file_system_provider/mount_path_util.h"
#include "chrome/browser/ash/file_system_provider/provided_file_system_interface.h"
#include "chrome/browser/ash/file_system_provider/provider_interface.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/ui/browser_list_observer.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload.mojom.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/one_drive_upload_handler.h"
#include "chrome/browser/ui/webui/ash/system_web_dialog_delegate.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom-forward.h"
#include "components/drive/file_errors.h"
#include "storage/browser/file_system/file_system_url.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/native_widget_types.h"

class Profile;

namespace extensions::app_file_handler_util {
class MimeTypeCollector;
}  // namespace extensions::app_file_handler_util

namespace file_manager::file_tasks {
FORWARD_DECLARE_TEST(DriveTest, OpenFileInDrive);
FORWARD_DECLARE_TEST(OneDriveTest, OpenFileFromODFS);
FORWARD_DECLARE_TEST(OneDriveTest, OpenFileNotFromODFS);
FORWARD_DECLARE_TEST(OneDriveTest,
                     FailToOpenFileFromODFSReauthenticationRequired);
FORWARD_DECLARE_TEST(OneDriveTest, FailToOpenFileFromODFSOtherAccessError);
FORWARD_DECLARE_TEST(OneDriveTest, OpenFileFromAndroidOneDriveViaODFS);
FORWARD_DECLARE_TEST(OneDriveTest,
                     OpenFileFromAndroidOneDriveViaODFSDiffCaseEmail);
FORWARD_DECLARE_TEST(OneDriveTest,
                     FailToOpenFileFromAndroidOneDriveViaODFSDiffEmail);
FORWARD_DECLARE_TEST(OneDriveTest, FailToOpenFileFromAndroidOneDriveNotOnODFS);
FORWARD_DECLARE_TEST(
    OneDriveTest,
    FailToOpenFileFromAndroidOneDriveDirectoryNotAccessibleToODFS);
}  // namespace file_manager::file_tasks

namespace ash::cloud_upload {

struct ODFSFileSystemAndPath {
  raw_ptr<file_system_provider::ProvidedFileSystemInterface> file_system;
  base::FilePath file_path_within_odfs;
};

// The string conversions of ash::cloud_upload::mojom::UserAction.
constexpr char kUserActionCancel[] = "cancel";
constexpr char kUserActionCancelGoogleDrive[] = "cancel-drive";
constexpr char kUserActionCancelOneDrive[] = "cancel-onedrive";
constexpr char kUserActionSetUpOneDrive[] = "setup-onedrive";
constexpr char kUserActionUploadToGoogleDrive[] = "upload-drive";
constexpr char kUserActionUploadToOneDrive[] = "upload-onedrive";
constexpr char kUserActionConfirmOrUploadToGoogleDrive[] =
    "confirm-or-upload-google-drive";
constexpr char kUserActionConfirmOrUploadToOneDrive[] =
    "confirm-or-upload-onedrive";

// Options for which setup or move confirmation sub-page/flow we want to show.
enum class SetupOrMoveDialogPage {
  // The user can choose between apps for handling office files.
  kFileHandlerDialog,
  // Set up OneDrive (multi-page).
  kOneDriveSetup,
  // Confirm that the user wants to move the file to OneDrive.
  kMoveConfirmationOneDrive,
  // Confirm that the user wants to move the file to Google Drive.
  kMoveConfirmationGoogleDrive,
};

class CloudUploadDialog;

// The business logic for running setup, moving files to a cloud provider, and
// opening files on cloud providers. Spawns instances of `CloudUploadDialog` if
// necessary to run setup or get confirmation from the user.
class CloudOpenTask : public BrowserListObserver,
                      public base::RefCounted<CloudOpenTask> {
 public:
  CloudOpenTask(const CloudOpenTask&) = delete;
  CloudOpenTask& operator=(const CloudOpenTask&) = delete;

  // This is the main public entry-point to the functionality in this file.
  // Opens the `file_urls` from the `cloud_provider`. Runs setup for Office
  // files if it has not been run before. Uploads the files to the cloud if
  // needed. Returns false if `file_urls` was empty or the setup/upload process
  // failed before any async step started. `CloudOpenTask` is refcounted and
  // lives until the task is complete or has failed, via bound `this` pointers
  // in the closures used for async steps.
  static bool Execute(Profile* profile,
                      const std::vector<storage::FileSystemURL>& file_urls,
                      const ::file_manager::file_tasks::TaskDescriptor& task,
                      const CloudProvider cloud_provider,
                      std::unique_ptr<CloudOpenMetrics> cloud_open_metrics);

  // Set the local tasks that are passed to the File Handler dialog. Normally
  // tasks are calculated internally by this class before displaying this
  // dialog. Some tests don't display the dialog, but only test post-dialog
  // logic.
  void SetTasksForTest(
      const std::vector<::file_manager::file_tasks::TaskDescriptor>& tasks);

  // BrowserListObserver implementation.
  // Use this to check if a new Files app window has been launched when there
  // wasn't already one to be used as the modal parent. This is triggered by
  // ShowDialog().
  void OnBrowserAdded(Browser* browser) override;
  // Use this to check if the Files app window the dialog is modal to has
  // closed.
  void OnBrowserClosing(Browser* browser) override;

  FRIEND_TEST_ALL_PREFIXES(FileHandlerDialogBrowserTest,
                           OnSetupDialogCompleteOpensFileTasks);
  FRIEND_TEST_ALL_PREFIXES(FileHandlerDialogBrowserTest,
                           OnSetupDialogCompleteNoCrash);
  FRIEND_TEST_ALL_PREFIXES(FixUpFlowBrowserTest,
                           OneDriveSetUpChangesDefaultTaskWhenSetUpIncomplete);
  FRIEND_TEST_ALL_PREFIXES(
      FixUpFlowBrowserTest,
      OneDriveSetUpDoesNotChangeDefaultTaskWhenSetUpComplete);
  FRIEND_TEST_ALL_PREFIXES(::file_manager::file_tasks::DriveTest,
                           OpenFileInDrive);
  FRIEND_TEST_ALL_PREFIXES(::file_manager::file_tasks::OneDriveTest,
                           OpenFileFromODFS);
  FRIEND_TEST_ALL_PREFIXES(::file_manager::file_tasks::OneDriveTest,
                           OpenFileNotFromODFS);
  FRIEND_TEST_ALL_PREFIXES(::file_manager::file_tasks::OneDriveTest,
                           FailToOpenFileFromODFSReauthenticationRequired);
  FRIEND_TEST_ALL_PREFIXES(::file_manager::file_tasks::OneDriveTest,
                           FailToOpenFileFromODFSOtherAccessError);
  FRIEND_TEST_ALL_PREFIXES(::file_manager::file_tasks::OneDriveTest,
                           OpenFileFromAndroidOneDriveViaODFS);
  FRIEND_TEST_ALL_PREFIXES(::file_manager::file_tasks::OneDriveTest,
                           OpenFileFromAndroidOneDriveViaODFSDiffCaseEmail);
  FRIEND_TEST_ALL_PREFIXES(::file_manager::file_tasks::OneDriveTest,
                           FailToOpenFileFromAndroidOneDriveViaODFSDiffEmail);
  FRIEND_TEST_ALL_PREFIXES(::file_manager::file_tasks::OneDriveTest,
                           FailToOpenFileFromAndroidOneDriveNotOnODFS);
  FRIEND_TEST_ALL_PREFIXES(
      ::file_manager::file_tasks::OneDriveTest,
      FailToOpenFileFromAndroidOneDriveDirectoryNotAccessibleToODFS);

 private:
  friend class RefCounted<CloudOpenTask>;  // Allow destruction by RefCounted<>.
  friend class CloudOpenTaskBrowserTest;

  CloudOpenTask(Profile* profile,
                std::vector<storage::FileSystemURL> file_urls,
                const ::file_manager::file_tasks::TaskDescriptor& task,
                const CloudProvider cloud_provider,
                std::unique_ptr<CloudOpenMetrics> cloud_open_metrics);

  ~CloudOpenTask() override;

  // See the .cc implementation for comments on private methods.
  bool ExecuteInternal();
  bool MaybeRunFixupFlow();
  bool OpenOrMoveFiles();
  void OpenAlreadyHostedDriveUrls();
  void OnGoogleDriveGetMetadata(drive::FileError error,
                                drivefs::mojom::FileMetadataPtr metadata);
  void OpenUploadedDriveUrl(const GURL& url,
                            const OfficeTaskResult task_result);
  void OpenODFSUrls(const OfficeTaskResult task_result_uma);
  void CheckEmailAndOpenAndroidOneDriveURLs(
      base::expected<cloud_upload::ODFSMetadata, base::File::Error>
          metadata_or_error);
  void OpenAndroidOneDriveUrl(
      const storage::FileSystemURL& android_onedrive_url);

  bool ShouldShowConfirmationDialog();
  bool ConfirmMoveOrStartUpload();
  void StartUpload();
  void StartNextGoogleDriveUpload();
  void StartNextOneDriveUpload();

  // Callbacks from `DriveUploadHandler` and `OneDriveUploadHandler`. URL passed
  // to these callbacks will be `std::nullopt` and size will be 0 if upload
  // fails.
  void FinishedDriveUpload(OfficeTaskResult task_result,
                           std::optional<GURL> url,
                           int64_t size);
  void FinishedOneDriveUpload(base::WeakPtr<Profile> profile_weak_ptr,
                              OfficeTaskResult task_result,
                              std::optional<storage::FileSystemURL> url,
                              int64_t size);

  void LogGoogleDriveOpenResultUMA(OfficeTaskResult success_task_result,
                                   OfficeDriveOpenErrors open_result);
  void LogOneDriveOpenResultUMA(OfficeTaskResult success_task_result,
                                OfficeOneDriveOpenErrors open_result);

  bool InitAndShowSetupOrMoveDialog(SetupOrMoveDialogPage dialog_page);
  mojom::DialogArgsPtr CreateDialogArgs(SetupOrMoveDialogPage dialog_page);
  void ShowDialog(SetupOrMoveDialogPage dialog_page,
                  mojom::DialogArgsPtr args,
                  std::unique_ptr<::file_manager::file_tasks::ResultingTasks>
                      resulting_tasks);
  void SetTaskArgs(mojom::DialogArgsPtr& args,
                   std::unique_ptr<::file_manager::file_tasks::ResultingTasks>
                       resulting_tasks);

  void OnSetupDialogComplete(const std::string& user_response);
  void OnMoveConfirmationComplete(const std::string& user_response);
  void LaunchLocalFileTask(const std::string& string_task_position);

  void LocalTaskExecuted(
      const ::file_manager::file_tasks::TaskDescriptor& task,
      extensions::api::file_manager_private::TaskResult result,
      std::string error_message);

  void FindTasksForDialog(::file_manager::file_tasks::FindTasksCallback
                              find_all_types_of_tasks_callback);
  void ConstructEntriesAndFindTasks(
      const std::vector<base::FilePath>& file_paths,
      const std::vector<GURL>& gurls,
      std::unique_ptr<extensions::app_file_handler_util::MimeTypeCollector>
          mime_collector,
      ::file_manager::file_tasks::FindTasksCallback
          find_all_types_of_tasks_callback,
      std::unique_ptr<std::vector<std::string>> mime_types);
  void RecordUploadLatencyUMA();

  raw_ptr<Profile> profile_;
  std::vector<storage::FileSystemURL> file_urls_;
  // File being currently uploaded.
  size_t file_urls_idx_ = 0;
  const ::file_manager::file_tasks::TaskDescriptor task_;
  CloudProvider cloud_provider_;
  std::unique_ptr<CloudOpenMetrics> cloud_open_metrics_;
  std::unique_ptr<DriveUploadHandler> drive_upload_handler_;
  std::unique_ptr<OneDriveUploadHandler> one_drive_upload_handler_;
  std::vector<::file_manager::file_tasks::TaskDescriptor> local_tasks_;
  raw_ptr<CloudUploadDialog> pending_dialog_ = nullptr;
  base::ElapsedTimer upload_timer_;
  int64_t upload_total_size_ = 0;
  // True when there is at least one upload error.
  bool has_upload_errors_ = false;
  OfficeFilesTransferRequired transfer_required_ =
      OfficeFilesTransferRequired::kNotRequired;
  bool need_new_files_app_ = false;
  raw_ptr<Browser> files_app_browser_;
  bool files_app_closed_ = false;
};

// Returns True if OneDrive is the selected `cloud_provider` but either ODFS
// is not mounted or the Office PWA is not installed. Returns False otherwise.
bool ShouldFixUpOffice(Profile* profile, const CloudProvider cloud_provider);

// Returns True if the url is on the Android OneDrive DocumentsProvider.
bool UrlIsOnAndroidOneDrive(Profile* profile, const FileSystemURL& url);

// Return the email from the Root Document Id of the Android OneDrive
// DocumentsProvider.
std::optional<std::string> GetEmailFromAndroidOneDriveRootDoc(
    const std::string& root_document_id);

// Converts the |android_onedrive_file_url| for a file in OneDrive to the
// equivalent ODFS file path which is then parsed to detect the corresponding
// ODFS ProvidedFileSystemInterface and relative file path. There may or may not
// exist a file for the returned relative file path. The conversion can be done
// for files in OneDrive that can be accessed via Android OneDrive or ODFS.
// These are the users' own files - in the Android OneDrive "Files" directory.
// Fails if an equivalent ODFS file path can't be constructed.
std::optional<ODFSFileSystemAndPath> AndroidOneDriveUrlToODFS(
    Profile* profile,
    const FileSystemURL& android_onedrive_file_url);

// Launches the 'Connect OneDrive' dialog which is triggered from the Services
// menu in Files app. This is a simplified version of setup where we just
// connect to OneDrive. There is no 'done' callback because files app doesn't
// need it.
bool ShowConnectOneDriveDialog(gfx::NativeWindow modal_parent);

// Launches the setup flow to set up opening Office files in Microsoft 365.
void LaunchMicrosoft365Setup(Profile* profile, gfx::NativeWindow modal_parent);

// Defines the web dialog used to help users upload Office files to the cloud.
class CloudUploadDialog : public SystemWebDialogDelegate {
 public:
  using UploadRequestCallback =
      base::OnceCallback<void(const std::string& user_response)>;

  CloudUploadDialog(const CloudUploadDialog&) = delete;
  CloudUploadDialog& operator=(const CloudUploadDialog&) = delete;

  CloudUploadDialog(mojom::DialogArgsPtr args,
                    UploadRequestCallback callback,
                    bool office_move_confirmation_shown);

  void OnDialogShown(content::WebUI* webui) override;
  void OnDialogClosed(const std::string& json_retval) override;

 protected:
  ~CloudUploadDialog() override;
  ui::mojom::ModalType GetDialogModalType() const override;
  bool ShouldCloseDialogOnEscape() const override;
  bool ShouldShowCloseButton() const override;
  void GetDialogSize(gfx::Size* size) const override;

 private:
  FRIEND_TEST_ALL_PREFIXES(FixUpFlowBrowserTest,
                           OneDriveSetUpChangesDefaultTaskWhenSetUpIncomplete);
  FRIEND_TEST_ALL_PREFIXES(
      FixUpFlowBrowserTest,
      OneDriveSetUpDoesNotChangeDefaultTaskWhenSetUpComplete);

  mojom::DialogArgsPtr dialog_args_;
  UploadRequestCallback callback_;
  bool office_move_confirmation_shown_;
};

}  // namespace ash::cloud_upload

#endif  // CHROME_BROWSER_UI_WEBUI_ASH_CLOUD_UPLOAD_CLOUD_UPLOAD_DIALOG_H_