| // 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/ash/wallpaper/wallpaper_drivefs_delegate_impl.h" |
| |
| #include <map> |
| |
| #include "ash/public/cpp/image_downloader.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/time.h" |
| #include "base/unguessable_token.h" |
| #include "chrome/browser/ash/drive/drive_integration_service.h" |
| #include "chrome/browser/ash/drive/file_system_util.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chromeos/ash/components/drivefs/drivefs_host.h" |
| #include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h" |
| #include "components/account_id/account_id.h" |
| #include "components/drive/file_errors.h" |
| #include "components/drive/file_system_core_util.h" |
| #include "google_apis/common/api_error_codes.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "url/gurl.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| constexpr net::NetworkTrafficAnnotationTag kDriveFsDownloadWallpaperTag = |
| net::DefineNetworkTrafficAnnotation("wallpaper_drivefs_delegate", R"( |
| semantics { |
| sender: "Wallpaper DriveFs" |
| description: "Download wallpaper images from Google Drive." |
| trigger: |
| "Triggered when another device owned by the same user has uploaded a " |
| "wallpaper image to Google Drive to sync to this device." |
| data: "Readonly OAuth token for Google Drive" |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "Users can disable downloading wallpapers from drive by toggling " |
| "Disconnect Google Drive account in chrome://os-settings/files, or " |
| "by turning off wallpaper sync in chrome://os-settings/osSync" |
| chrome_policy: { |
| DriveDisabled { |
| DriveDisabled: true |
| } |
| SyncDisabled { |
| SyncDisabled: true |
| } |
| WallpaperImage { |
| WallpaperImage: "png or jpg encoded image data" |
| } |
| } |
| } |
| )"); |
| |
| constexpr char kDriveFsWallpaperDirName[] = "Chromebook Wallpaper"; |
| constexpr char kDriveFsWallpaperFileName[] = "wallpaper.jpg"; |
| constexpr char kDriveFsTempWallpaperFileName[] = "wallpaper-tmp.jpg"; |
| |
| // Gets a pointer to `DriveIntegrationService` to interact with DriveFS. If |
| // DriveFS is not enabled or mounted for this `account_id`, responds with |
| // `nullptr`. Caller must check null safety carefully, as DriveFS can crash, |
| // disconnect, or unmount itself and this function will start returning |
| // `nullptr`. |
| // If the pointer to `DriveIntegrationService` is held for a long duration, the |
| // owner must implement `DriveIntegrationService::Observer` to avoid |
| // use-after-free. |
| drive::DriveIntegrationService* GetDriveIntegrationService( |
| const AccountId& account_id) { |
| Profile* profile = ProfileHelper::Get()->GetProfileByAccountId(account_id); |
| if (!profile) { |
| VLOG(1) << "No profile for account_id"; |
| return nullptr; |
| } |
| |
| drive::DriveIntegrationService* drive_integration_service = |
| drive::util::GetIntegrationServiceByProfile(profile); |
| |
| if (!drive_integration_service || !drive_integration_service->is_enabled() || |
| !drive_integration_service->IsMounted()) { |
| return nullptr; |
| } |
| |
| return drive_integration_service; |
| } |
| |
| // Gets a relative path from the DriveFS mount point to the wallpaper file. |
| base::FilePath GetWallpaperRelativePath() { |
| return base::FilePath(drive::util::kDriveMyDriveRootDirName) |
| .Append(kDriveFsWallpaperDirName) |
| .Append(kDriveFsWallpaperFileName); |
| } |
| |
| base::Time GetModificationTimeFromDriveMetadata( |
| drive::FileError error, |
| drivefs::mojom::FileMetadataPtr metadata) { |
| if (error != drive::FILE_ERROR_OK || !metadata) { |
| VLOG(1) << "Unable to get metadata for DriveFs wallpaper file. Error: " |
| << error; |
| return base::Time(); |
| } |
| return metadata->modification_time; |
| } |
| |
| // Copies file `source` from outside DriveFS, to `destination` inside DriveFS. |
| // `destination` must be the path to the DriveFS wallpaper file. Copies the |
| // file to a temporary file first, and then swaps it to the final file path, in |
| // order to avoid partial writes corrupting the wallpaper image saved to |
| // DriveFS. Must be run on a blocking task runner. |
| bool CopyFileToDriveFsBlocking(const base::FilePath& source, |
| const base::FilePath& destination) { |
| DCHECK_EQ(destination.BaseName().value(), kDriveFsWallpaperFileName); |
| const base::FilePath directory = destination.DirName(); |
| DCHECK_EQ(directory.BaseName().value(), kDriveFsWallpaperDirName); |
| if (!base::DirectoryExists(directory) && !base::CreateDirectory(directory)) { |
| DVLOG(1) << "Failed to create DriveFS '" << kDriveFsWallpaperDirName |
| << "' directory"; |
| return false; |
| } |
| |
| base::FilePath temp_file_path = |
| directory.Append(base::UnguessableToken::Create().ToString().append( |
| kDriveFsTempWallpaperFileName)); |
| |
| if (!base::CopyFile(source, temp_file_path)) { |
| DVLOG(1) << "Failed to copy wallpaper file to DriveFs"; |
| base::DeleteFile(temp_file_path); |
| return false; |
| } |
| |
| base::File::Error error; |
| if (!base::ReplaceFile(temp_file_path, destination, &error)) { |
| DVLOG(1) << "Failed to move temp wallpaper file with error '" << error |
| << "'"; |
| base::DeleteFile(temp_file_path); |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| WallpaperChangeWaiter::WallpaperChangeWaiter( |
| const AccountId& account_id, |
| base::OnceCallback<void(bool success)> callback) |
| : account_id_(account_id), |
| path_to_watch_(base::FilePath(base::FilePath::kSeparators) |
| .Append(GetWallpaperRelativePath())), |
| callback_(std::move(callback)) { |
| DCHECK(callback_); |
| auto* drive_integration_service = GetDriveIntegrationService(account_id); |
| if (!drive_integration_service) { |
| std::move(callback_).Run(/*success=*/false); |
| return; |
| } |
| |
| Observe(drive_integration_service->GetDriveFsHost()); |
| } |
| |
| WallpaperChangeWaiter::~WallpaperChangeWaiter() { |
| if (callback_) { |
| std::move(callback_).Run(/*success=*/false); |
| } |
| } |
| |
| void WallpaperChangeWaiter::OnUnmounted() { |
| if (callback_) { |
| std::move(callback_).Run(/*success=*/false); |
| } |
| } |
| |
| void WallpaperChangeWaiter::OnError(const drivefs::mojom::DriveError& error) { |
| if (error.path == path_to_watch_ && callback_) { |
| LOG(WARNING) << "DriveFS wallpaper error: " << error.type; |
| std::move(callback_).Run(/*success=*/false); |
| } |
| } |
| |
| void WallpaperChangeWaiter::OnFilesChanged( |
| const std::vector<drivefs::mojom::FileChange>& changes) { |
| for (const auto& change : changes) { |
| if (change.path == path_to_watch_ && callback_) { |
| std::move(callback_).Run(/*success=*/true); |
| return; |
| } |
| } |
| } |
| |
| WallpaperDriveFsDelegateImpl::WallpaperDriveFsDelegateImpl() |
| : blocking_task_runner_(base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {} |
| |
| WallpaperDriveFsDelegateImpl::~WallpaperDriveFsDelegateImpl() = default; |
| |
| base::FilePath WallpaperDriveFsDelegateImpl::GetWallpaperPath( |
| const AccountId& account_id) { |
| auto* drive_integration_service = GetDriveIntegrationService(account_id); |
| if (!drive_integration_service) { |
| VLOG(1) |
| << "Cannot get DriveFS Wallpaper path because DriveFS is unavailable"; |
| return base::FilePath(); |
| } |
| auto mount_path = drive_integration_service->GetMountPointPath(); |
| DCHECK(!mount_path.empty()); |
| return mount_path.Append(GetWallpaperRelativePath()); |
| } |
| |
| void WallpaperDriveFsDelegateImpl::SaveWallpaper( |
| const AccountId& account_id, |
| const base::FilePath& source, |
| base::OnceCallback<void(bool)> callback) { |
| auto* drive_integration_service = GetDriveIntegrationService(account_id); |
| if (!drive_integration_service) { |
| DVLOG(1) |
| << "Not saving wallpaper to DriveFS because DriveFS is unavailable"; |
| std::move(callback).Run(/*success=*/false); |
| return; |
| } |
| blocking_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&CopyFileToDriveFsBlocking, source, |
| GetWallpaperPath(account_id)), |
| std::move(callback)); |
| } |
| |
| void WallpaperDriveFsDelegateImpl::WaitForWallpaperChange( |
| const AccountId& account_id, |
| WaitForWallpaperChangeCallback callback) { |
| // Remove any old `WallpaperChangeWaiter` and create a new one. The old one |
| // will run any pending callbacks on deletion. |
| wallpaper_change_waiters_.erase(account_id); |
| wallpaper_change_waiters_.try_emplace( |
| account_id, account_id, |
| base::BindOnce(&WallpaperDriveFsDelegateImpl::OnDriveFsWallpaperChanged, |
| weak_ptr_factory_.GetWeakPtr(), account_id, |
| std::move(callback))); |
| } |
| |
| void WallpaperDriveFsDelegateImpl::GetWallpaperModificationTime( |
| const AccountId& account_id, |
| base::OnceCallback<void(base::Time modification_time)> callback) { |
| auto* drive_integration_service = GetDriveIntegrationService(account_id); |
| if (!drive_integration_service) { |
| std::move(callback).Run(base::Time()); |
| return; |
| } |
| |
| drive_integration_service->GetMetadata( |
| GetWallpaperPath(account_id), |
| base::BindOnce(&GetModificationTimeFromDriveMetadata) |
| .Then(std::move(callback))); |
| } |
| |
| void WallpaperDriveFsDelegateImpl::DownloadAndDecodeWallpaper( |
| const AccountId& account_id, |
| ImageDownloader::DownloadCallback callback) { |
| auto* drive_integration_service = GetDriveIntegrationService(account_id); |
| if (!drive_integration_service) { |
| VLOG(1) << "Skip downloading custom wallpaper because DriveFS is not " |
| "available."; |
| std::move(callback).Run(gfx::ImageSkia()); |
| return; |
| } |
| |
| drive_integration_service->GetMetadata( |
| GetWallpaperPath(account_id), |
| base::BindOnce(&WallpaperDriveFsDelegateImpl::OnGetDownloadUrlMetadata, |
| weak_ptr_factory_.GetWeakPtr(), account_id, |
| std::move(callback))); |
| } |
| |
| void WallpaperDriveFsDelegateImpl::OnGetDownloadUrlMetadata( |
| const AccountId& account_id, |
| ImageDownloader::DownloadCallback callback, |
| drive::FileError error, |
| drivefs::mojom::FileMetadataPtr metadata) { |
| if (error != drive::FileError::FILE_ERROR_OK || !metadata || |
| metadata->download_url.empty()) { |
| VLOG(1) << "Unable to get download url for DriveFS wallpaper"; |
| std::move(callback).Run(gfx::ImageSkia()); |
| return; |
| } |
| |
| auto* drive_integration_service = GetDriveIntegrationService(account_id); |
| if (!drive_integration_service) { |
| VLOG(1) << "DriveFS no longer mounted, cannot fetch authentication token"; |
| std::move(callback).Run(gfx::ImageSkia()); |
| return; |
| } |
| |
| drive_integration_service->GetReadOnlyAuthenticationToken(base::BindOnce( |
| &WallpaperDriveFsDelegateImpl::OnGetDownloadUrlAndAuthentication, |
| weak_ptr_factory_.GetWeakPtr(), account_id, std::move(callback), |
| GURL(metadata->download_url))); |
| } |
| |
| void WallpaperDriveFsDelegateImpl::OnGetDownloadUrlAndAuthentication( |
| const AccountId& account_id, |
| ImageDownloader::DownloadCallback callback, |
| const GURL& download_url, |
| google_apis::ApiErrorCode error_code, |
| const std::string& authentication_token) { |
| if (error_code != google_apis::HTTP_SUCCESS || authentication_token.empty()) { |
| VLOG(1) << "Unable to fetch authentication token for DriveFS with error " |
| << error_code; |
| std::move(callback).Run(gfx::ImageSkia()); |
| return; |
| } |
| net::HttpRequestHeaders headers; |
| headers.SetHeader(net::HttpRequestHeaders::kAuthorization, |
| "Bearer " + authentication_token); |
| ImageDownloader::Get()->Download(download_url, kDriveFsDownloadWallpaperTag, |
| account_id, std::move(headers), |
| std::move(callback)); |
| } |
| |
| void WallpaperDriveFsDelegateImpl::OnDriveFsWallpaperChanged( |
| const AccountId& account_id, |
| WaitForWallpaperChangeCallback callback, |
| bool success) { |
| std::move(callback).Run(success); |
| wallpaper_change_waiters_.erase(account_id); |
| } |
| |
| } // namespace ash |