blob: 6c255483e649725a89f861d7086874d0cbc7cf33 [file] [log] [blame]
// 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();
}