| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/notifications/win/notification_image_retainer.h" |
| |
| #include <algorithm> |
| #include <set> |
| |
| #include "base/bind.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_util.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/path_service.h" |
| #include "base/stl_util.h" |
| #include "base/task/post_task.h" |
| #include "base/time/default_tick_clock.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "ui/gfx/image/image.h" |
| |
| namespace { |
| |
| constexpr base::FilePath::CharType kImageRoot[] = |
| FILE_PATH_LITERAL("Notification Resources"); |
| |
| // How long to keep the temp files before deleting them. The formula for picking |
| // the delay is t * (n + 1), where t is the default on-screen display time for |
| // an Action Center notification (6 seconds) and n is the number of |
| // notifications that can be shown on-screen at once (1). |
| constexpr base::TimeDelta kDeletionDelay = base::TimeDelta::FromSeconds(12); |
| |
| // Returns the temporary directory within the user data directory. The regular |
| // temporary directory is not used to minimize the risk of files getting deleted |
| // by accident. It is also not profile-bound because the notification bridge |
| // is profile-agnostic. |
| base::FilePath DetermineImageDirectory() { |
| base::FilePath data_dir; |
| bool success = base::PathService::Get(chrome::DIR_USER_DATA, &data_dir); |
| DCHECK(success); |
| return data_dir.Append(kImageRoot); |
| } |
| |
| // Returns the full paths to all immediate file and directory children of |dir|, |
| // excluding those present in |registered_names|. |
| std::vector<base::FilePath> GetFilesFromPrevSessions( |
| const base::FilePath& dir, |
| const std::set<base::FilePath>& registered_names) { |
| // |dir| may have sub-dirs, created by the old implementation. |
| base::FileEnumerator file_enumerator( |
| dir, /*recursive=*/false, |
| base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES, |
| FILE_PATH_LITERAL("*")); |
| std::vector<base::FilePath> files; |
| |
| for (base::FilePath current = file_enumerator.Next(); !current.empty(); |
| current = file_enumerator.Next()) { |
| // Exclude any new file created in this session. |
| if (!base::ContainsKey(registered_names, current.BaseName())) |
| files.push_back(std::move(current)); |
| } |
| |
| return files; |
| } |
| |
| // Deletes files in |paths|. |
| void DeleteFiles(std::vector<base::FilePath> paths) { |
| // |file_path| can be a directory, created by the old implementation, so |
| // delete it recursively. |
| for (const auto& file_path : paths) |
| base::DeleteFile(file_path, /*recursive=*/true); |
| } |
| |
| } // namespace |
| |
| NotificationImageRetainer::NotificationImageRetainer( |
| scoped_refptr<base::SequencedTaskRunner> deletion_task_runner, |
| const base::TickClock* tick_clock) |
| : deletion_task_runner_(std::move(deletion_task_runner)), |
| image_dir_(DetermineImageDirectory()), |
| tick_clock_(tick_clock), |
| deletion_timer_(tick_clock), |
| weak_ptr_factory_(this) { |
| DCHECK(deletion_task_runner_); |
| DCHECK(tick_clock); |
| |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| NotificationImageRetainer::NotificationImageRetainer() |
| : NotificationImageRetainer( |
| base::CreateSequencedTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}), |
| base::DefaultTickClock::GetInstance()) {} |
| |
| NotificationImageRetainer::~NotificationImageRetainer() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| void NotificationImageRetainer::CleanupFilesFromPrevSessions() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Store all file names from registered_images in an ordered set for quick |
| // search. |
| std::set<base::FilePath> registered_names; |
| for (const auto& pair : registered_images_) |
| registered_names.insert(pair.first); |
| |
| std::vector<base::FilePath> files = |
| GetFilesFromPrevSessions(image_dir_, registered_names); |
| |
| // This method is run in an "after startup" task, so it is fine to directly |
| // post the DeleteFiles task to the runner. |
| if (!files.empty()) { |
| deletion_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&DeleteFiles, std::move(files))); |
| } |
| } |
| |
| base::FilePath NotificationImageRetainer::RegisterTemporaryImage( |
| const gfx::Image& image) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| scoped_refptr<base::RefCountedMemory> data = image.As1xPNGBytes(); |
| if (data->size() == 0) |
| return base::FilePath(); |
| |
| // Create the image directory. Since Chrome doesn't delete this directory |
| // after showing notifications, this directory creation should happen exactly |
| // once until Chrome is re-installed. |
| if (!base::CreateDirectory(image_dir_)) |
| return base::FilePath(); |
| |
| base::FilePath temp_file; |
| if (!base::CreateTemporaryFileInDir(image_dir_, &temp_file)) |
| return base::FilePath(); |
| |
| const base::TimeTicks now = tick_clock_->NowTicks(); |
| DCHECK(registered_images_.empty() || now >= registered_images_.back().second); |
| registered_images_.emplace_back(temp_file.BaseName(), now); |
| |
| // At this point, a temp file is already created. We need to clean it up even |
| // if it fails to write the image data to this file. |
| int data_len = base::checked_cast<int>(data->size()); |
| bool data_write_success = (base::WriteFile(temp_file, data->front_as<char>(), |
| data_len) == data_len); |
| |
| // Start the timer if it hasn't to delete the expired files in batch. This |
| // avoids creating a deletion task for each file, otherwise the overhead can |
| // be large when there is a steady stream of notifications coming rapidly. |
| if (!deletion_timer_.IsRunning()) { |
| deletion_timer_.Start( |
| FROM_HERE, kDeletionDelay, |
| base::BindRepeating(&NotificationImageRetainer::DeleteExpiredFiles, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| return data_write_success ? temp_file : base::FilePath(); |
| } |
| |
| base::OnceClosure NotificationImageRetainer::GetCleanupTask() { |
| return base::BindOnce( |
| &NotificationImageRetainer::CleanupFilesFromPrevSessions, |
| weak_ptr_factory_.GetWeakPtr()); |
| } |
| |
| void NotificationImageRetainer::DeleteExpiredFiles() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!registered_images_.empty()); |
| |
| // Find the first file that should not be deleted. |
| const base::TimeTicks then = tick_clock_->NowTicks() - kDeletionDelay; |
| const auto end = |
| std::upper_bound(registered_images_.begin(), registered_images_.end(), |
| std::make_pair(base::FilePath(), then), |
| [](const NameAndTime& a, const NameAndTime& b) { |
| return a.second < b.second; |
| }); |
| if (end == registered_images_.begin()) |
| return; // Nothing to delete yet. |
| |
| // Ship the files to be deleted off to the deletion task runner. |
| std::vector<base::FilePath> files_to_delete; |
| files_to_delete.reserve(end - registered_images_.begin()); |
| for (auto iter = registered_images_.begin(); iter < end; ++iter) |
| files_to_delete.push_back(image_dir_.Append(iter->first)); |
| |
| deletion_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&DeleteFiles, std::move(files_to_delete))); |
| |
| // Erase the items to be deleted from registered_images_. |
| registered_images_.erase(registered_images_.begin(), end); |
| |
| // Stop the recurring timer if all files have been deleted. |
| if (registered_images_.empty()) |
| deletion_timer_.Stop(); |
| } |