blob: 34579acc22b2fbc0fca9a25053f0399292dfcb3c [file] [log] [blame]
// Copyright 2019 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/media/history/media_history_store.h"
#include <tuple>
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/stringprintf.h"
#include "base/task/task_runner_util.h"
#include "build/build_config.h"
#include "chrome/browser/media/history/media_history_images_table.h"
#include "chrome/browser/media/history/media_history_origin_table.h"
#include "chrome/browser/media/history/media_history_playback_table.h"
#include "chrome/browser/media/history/media_history_session_images_table.h"
#include "chrome/browser/media/history/media_history_session_table.h"
#include "content/public/browser/media_player_watch_time.h"
#include "net/cookies/cookie_change_dispatcher.h"
#include "services/media_session/public/cpp/media_image.h"
#include "services/media_session/public/cpp/media_position.h"
#include "sql/database.h"
#include "sql/recovery.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "url/origin.h"
namespace {
constexpr int kCurrentVersionNumber = 6;
constexpr int kCompatibleVersionNumber = 1;
constexpr base::FilePath::CharType kMediaHistoryDatabaseName[] =
FILE_PATH_LITERAL("Media History");
void DatabaseErrorCallback(sql::Database* db,
const base::FilePath& db_path,
int extended_error,
sql::Statement* stmt) {
if (sql::Recovery::ShouldRecover(extended_error)) {
// Prevent reentrant calls.
db->reset_error_callback();
// After this call, the |db| handle is poisoned so that future calls will
// return errors until the handle is re-opened.
sql::Recovery::RecoverDatabase(db, db_path);
// The DLOG(FATAL) below is intended to draw immediate attention to errors
// in newly-written code. Database corruption is generally a result of OS
// or hardware issues, not coding errors at the client level, so displaying
// the error would probably lead to confusion. The ignored call signals the
// test-expectation framework that the error was handled.
std::ignore = sql::Database::IsExpectedSqliteError(extended_error);
return;
}
// The default handling is to assert on debug and to ignore on release.
if (!sql::Database::IsExpectedSqliteError(extended_error))
DLOG(FATAL) << db->GetErrorMessage();
}
base::FilePath GetDBPath(Profile* profile) {
// If this is a testing profile then we should use an in-memory database.
if (profile->AsTestingProfile())
return base::FilePath();
return profile->GetPath().Append(kMediaHistoryDatabaseName);
}
int MigrateFrom1To2(sql::Database* db, sql::MetaTable* meta_table) {
// Version 2 adds a new column to mediaFeed. However, a later version removes
// the mediaFeed table, so just bump the version number and return 2 to
// indicate success.
const int kTargetVersion = 2;
meta_table->SetVersionNumber(kTargetVersion);
return kTargetVersion;
}
int MigrateFrom2To3(sql::Database* db, sql::MetaTable* meta_table) {
// Version 3 drops the mediaFeedAssociatedOrigin table.
const int kTargetVersion = 3;
static const char k2To3Sql[] =
"DROP TABLE IF EXISTS mediaFeedAssociatedOrigin;";
sql::Transaction transaction(db);
if (transaction.Begin() && db->Execute(k2To3Sql) && transaction.Commit()) {
meta_table->SetVersionNumber(kTargetVersion);
return kTargetVersion;
}
return 2;
}
int MigrateFrom3To4(sql::Database* db, sql::MetaTable* meta_table) {
// Version 4 adds a new column to mediaFeed. However, a later version removes
// the mediaFeed table, so just bump the version number and return 4 to
// indicate success.
const int kTargetVersion = 4;
meta_table->SetVersionNumber(kTargetVersion);
return kTargetVersion;
}
int MigrateFrom4To5(sql::Database* db, sql::MetaTable* meta_table) {
// Version 5 adds a new column to mediaFeed. However, a later version removes
// the mediaFeed table, so just bump the version number and return 5 to
// indicate success.
const int kTargetVersion = 5;
meta_table->SetVersionNumber(kTargetVersion);
return kTargetVersion;
}
int MigrateFrom5To6(sql::Database* db, sql::MetaTable* meta_table) {
// Version 6 drops the mediaFeed and mediaFeedItem tables.
const int kTargetVersion = 6;
sql::Transaction transaction(db);
if (!transaction.Begin() || !db->Execute("DROP TABLE IF EXISTS mediaFeed") ||
!db->Execute("DROP TABLE IF EXISTS mediaFeedItem") ||
!transaction.Commit()) {
return 5;
}
meta_table->SetVersionNumber(kTargetVersion);
return kTargetVersion;
}
} // namespace
int GetCurrentVersion() {
return kCurrentVersionNumber;
}
namespace media_history {
const char MediaHistoryStore::kInitResultHistogramName[] =
"Media.History.Init.Result";
const char MediaHistoryStore::kInitResultAfterDeleteHistogramName[] =
"Media.History.Init.ResultAfterDelete";
const char MediaHistoryStore::kPlaybackWriteResultHistogramName[] =
"Media.History.Playback.WriteResult";
const char MediaHistoryStore::kSessionWriteResultHistogramName[] =
"Media.History.Session.WriteResult";
const char MediaHistoryStore::kDatabaseSizeKbHistogramName[] =
"Media.History.DatabaseSize";
MediaHistoryStore::MediaHistoryStore(
Profile* profile,
scoped_refptr<base::UpdateableSequencedTaskRunner> db_task_runner)
: db_task_runner_(db_task_runner),
db_path_(GetDBPath(profile)),
db_(std::make_unique<sql::Database>(
sql::DatabaseOptions{.exclusive_locking = true,
.page_size = 4096,
.cache_size = 500})),
meta_table_(std::make_unique<sql::MetaTable>()),
origin_table_(new MediaHistoryOriginTable(db_task_runner_)),
playback_table_(new MediaHistoryPlaybackTable(db_task_runner_)),
session_table_(new MediaHistorySessionTable(db_task_runner_)),
session_images_table_(
new MediaHistorySessionImagesTable(db_task_runner_)),
images_table_(new MediaHistoryImagesTable(db_task_runner_)),
initialization_successful_(false) {
db_->set_histogram_tag("MediaHistory");
// To recover from corruption.
db_->set_error_callback(
base::BindRepeating(&DatabaseErrorCallback, db_.get(), db_path_));
}
MediaHistoryStore::~MediaHistoryStore() {
// The connection pointer needs to be deleted on the DB sequence since there
// might be a task in progress on the DB sequence which uses this connection.
if (meta_table_)
db_task_runner_->DeleteSoon(FROM_HERE, meta_table_.release());
if (db_)
db_task_runner_->DeleteSoon(FROM_HERE, db_.release());
}
sql::Database* MediaHistoryStore::DB() {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
return db_.get();
}
void MediaHistoryStore::SavePlayback(
std::unique_ptr<content::MediaPlayerWatchTime> watch_time) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return;
if (!DB()->BeginTransaction()) {
LOG(ERROR) << "Failed to begin the transaction.";
base::UmaHistogramEnumeration(
MediaHistoryStore::kPlaybackWriteResultHistogramName,
MediaHistoryStore::PlaybackWriteResult::kFailedToEstablishTransaction);
return;
}
// TODO(https://crbug.com/1052436): Remove the separate origin.
auto origin = url::Origin::Create(watch_time->origin);
if (origin != url::Origin::Create(watch_time->url)) {
DB()->RollbackTransaction();
base::UmaHistogramEnumeration(
MediaHistoryStore::kPlaybackWriteResultHistogramName,
MediaHistoryStore::PlaybackWriteResult::kFailedToWriteBadOrigin);
return;
}
if (!CreateOriginId(origin)) {
DB()->RollbackTransaction();
base::UmaHistogramEnumeration(
MediaHistoryStore::kPlaybackWriteResultHistogramName,
MediaHistoryStore::PlaybackWriteResult::kFailedToWriteOrigin);
return;
}
if (!playback_table_->SavePlayback(*watch_time)) {
DB()->RollbackTransaction();
base::UmaHistogramEnumeration(
MediaHistoryStore::kPlaybackWriteResultHistogramName,
MediaHistoryStore::PlaybackWriteResult::kFailedToWritePlayback);
return;
}
if (watch_time->has_audio && watch_time->has_video) {
if (!origin_table_->IncrementAggregateAudioVideoWatchTime(
origin, watch_time->cumulative_watch_time)) {
DB()->RollbackTransaction();
base::UmaHistogramEnumeration(
MediaHistoryStore::kPlaybackWriteResultHistogramName,
MediaHistoryStore::PlaybackWriteResult::
kFailedToIncrementAggreatedWatchtime);
return;
}
}
DB()->CommitTransaction();
base::UmaHistogramEnumeration(
MediaHistoryStore::kPlaybackWriteResultHistogramName,
MediaHistoryStore::PlaybackWriteResult::kSuccess);
}
void MediaHistoryStore::Initialize(const bool should_reset) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (should_reset) {
if (!sql::Database::Delete(db_path_)) {
LOG(ERROR) << "Failed to delete the old database.";
base::UmaHistogramEnumeration(
MediaHistoryStore::kInitResultHistogramName,
MediaHistoryStore::InitResult::kFailedToDeleteOldDatabase);
return;
}
}
if (IsCancelled())
return;
auto result = InitializeInternal();
if (IsCancelled()) {
meta_table_.reset();
db_.reset();
return;
}
base::UmaHistogramEnumeration(MediaHistoryStore::kInitResultHistogramName,
result);
// In some edge cases the DB might be corrupted and unrecoverable so we should
// delete the database and recreate it.
if (result != InitResult::kSuccess) {
db_ = std::make_unique<sql::Database>();
meta_table_ = std::make_unique<sql::MetaTable>();
sql::Database::Delete(db_path_);
base::UmaHistogramEnumeration(
MediaHistoryStore::kInitResultAfterDeleteHistogramName,
InitializeInternal());
}
}
MediaHistoryStore::InitResult MediaHistoryStore::InitializeInternal() {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (db_path_.empty()) {
if (IsCancelled() || !db_ || !db_->OpenInMemory()) {
LOG(ERROR) << "Failed to open the in-memory database.";
return MediaHistoryStore::InitResult::kFailedToOpenDatabase;
}
} else {
base::File::Error err;
if (IsCancelled() ||
!base::CreateDirectoryAndGetError(db_path_.DirName(), &err)) {
LOG(ERROR) << "Failed to create the directory.";
return MediaHistoryStore::InitResult::kFailedToCreateDirectory;
}
if (IsCancelled() || !db_ || !db_->Open(db_path_)) {
LOG(ERROR) << "Failed to open the database.";
return MediaHistoryStore::InitResult::kFailedToOpenDatabase;
}
}
if (IsCancelled() || !db_ || !db_->Execute("PRAGMA foreign_keys=1")) {
LOG(ERROR) << "Failed to enable foreign keys on the media history store.";
return MediaHistoryStore::InitResult::kFailedNoForeignKeys;
}
if (IsCancelled() || !db_ || !meta_table_ ||
!meta_table_->Init(db_.get(), GetCurrentVersion(),
kCompatibleVersionNumber)) {
LOG(ERROR) << "Failed to create the meta table.";
return MediaHistoryStore::InitResult::kFailedToCreateMetaTable;
}
if (IsCancelled() || !db_ || !db_->BeginTransaction()) {
LOG(ERROR) << "Failed to begin the transaction.";
return MediaHistoryStore::InitResult::kFailedToEstablishTransaction;
}
sql::InitStatus status = CreateOrUpgradeIfNeeded();
if (status != sql::INIT_OK) {
LOG(ERROR) << "Failed to create or update the media history store.";
return MediaHistoryStore::InitResult::kFailedDatabaseTooNew;
}
status = InitializeTables();
if (status != sql::INIT_OK) {
LOG(ERROR) << "Failed to initialize the media history store tables.";
return MediaHistoryStore::InitResult::kFailedInitializeTables;
}
if (IsCancelled() || !db_ || !DB()->CommitTransaction()) {
LOG(ERROR) << "Failed to commit transaction.";
return MediaHistoryStore::InitResult::kFailedToCommitTransaction;
}
initialization_successful_ = true;
// Get the size in bytes.
int64_t file_size = 0;
base::GetFileSize(db_path_, &file_size);
// Record the size in KB.
if (file_size > 0) {
base::UmaHistogramMemoryKB(MediaHistoryStore::kDatabaseSizeKbHistogramName,
file_size / 1000);
}
return MediaHistoryStore::InitResult::kSuccess;
}
sql::InitStatus MediaHistoryStore::CreateOrUpgradeIfNeeded() {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (IsCancelled() || !meta_table_)
return sql::INIT_FAILURE;
int cur_version = meta_table_->GetVersionNumber();
if (meta_table_->GetCompatibleVersionNumber() > kCurrentVersionNumber) {
LOG(WARNING) << "Media history database is too new.";
return sql::INIT_TOO_NEW;
}
// Versions 0 and below are unexpected.
if (cur_version <= 0)
return sql::INIT_FAILURE;
// NOTE: Insert schema upgrade scripts here when required.
if (cur_version == 1)
cur_version = MigrateFrom1To2(db_.get(), meta_table_.get());
if (cur_version == 2)
cur_version = MigrateFrom2To3(db_.get(), meta_table_.get());
if (cur_version == 3)
cur_version = MigrateFrom3To4(db_.get(), meta_table_.get());
if (cur_version == 4)
cur_version = MigrateFrom4To5(db_.get(), meta_table_.get());
if (cur_version == 5)
cur_version = MigrateFrom5To6(db_.get(), meta_table_.get());
if (cur_version == kCurrentVersionNumber)
return sql::INIT_OK;
return sql::INIT_FAILURE;
}
sql::InitStatus MediaHistoryStore::InitializeTables() {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (IsCancelled() || !db_)
return sql::INIT_FAILURE;
sql::InitStatus status = origin_table_->Initialize(db_.get());
if (status == sql::INIT_OK)
status = playback_table_->Initialize(db_.get());
if (status == sql::INIT_OK)
status = session_table_->Initialize(db_.get());
if (status == sql::INIT_OK)
status = session_images_table_->Initialize(db_.get());
if (status == sql::INIT_OK)
status = images_table_->Initialize(db_.get());
return status;
}
bool MediaHistoryStore::CreateOriginId(const url::Origin& origin) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return false;
return origin_table_->CreateOriginId(origin);
}
mojom::MediaHistoryStatsPtr MediaHistoryStore::GetMediaHistoryStats() {
mojom::MediaHistoryStatsPtr stats(mojom::MediaHistoryStats::New());
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return stats;
sql::Statement statement(DB()->GetUniqueStatement(
"SELECT name FROM sqlite_schema WHERE type='table' "
"AND name NOT LIKE 'sqlite_%';"));
std::vector<std::string> table_names;
while (statement.Step()) {
auto table_name = statement.ColumnString(0);
stats->table_row_counts.emplace(table_name, GetTableRowCount(table_name));
}
DCHECK(statement.Succeeded());
return stats;
}
std::vector<mojom::MediaHistoryOriginRowPtr>
MediaHistoryStore::GetOriginRowsForDebug() {
std::vector<mojom::MediaHistoryOriginRowPtr> origins;
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return origins;
sql::Statement statement(DB()->GetUniqueStatement(
base::StringPrintf(
"SELECT O.origin, O.last_updated_time_s, "
"O.aggregate_watchtime_audio_video_s, "
"(SELECT SUM(watch_time_s) FROM %s WHERE origin_id = O.id AND "
"has_video = 1 AND has_audio = 1) AS accurate_watchtime "
"FROM %s O",
MediaHistoryPlaybackTable::kTableName,
MediaHistoryOriginTable::kTableName)
.c_str()));
std::vector<std::string> table_names;
while (statement.Step()) {
mojom::MediaHistoryOriginRowPtr origin(mojom::MediaHistoryOriginRow::New());
origin->origin = url::Origin::Create(GURL(statement.ColumnString(0)));
origin->last_updated_time = base::Time::FromDeltaSinceWindowsEpoch(
base::Seconds(statement.ColumnInt64(1)))
.ToJsTime();
origin->cached_audio_video_watchtime =
base::Seconds(statement.ColumnInt64(2));
origin->actual_audio_video_watchtime =
base::Seconds(statement.ColumnInt64(3));
origins.push_back(std::move(origin));
}
DCHECK(statement.Succeeded());
return origins;
}
std::vector<url::Origin> MediaHistoryStore::GetHighWatchTimeOrigins(
const base::TimeDelta& audio_video_watchtime_min) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
return origin_table_->GetHighWatchTimeOrigins(audio_video_watchtime_min);
}
std::vector<mojom::MediaHistoryPlaybackRowPtr>
MediaHistoryStore::GetMediaHistoryPlaybackRowsForDebug() {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return std::vector<mojom::MediaHistoryPlaybackRowPtr>();
return playback_table_->GetPlaybackRows();
}
int MediaHistoryStore::GetTableRowCount(const std::string& table_name) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return -1;
sql::Statement statement(DB()->GetUniqueStatement(
base::StringPrintf("SELECT count(*) from %s", table_name.c_str())
.c_str()));
while (statement.Step()) {
return statement.ColumnInt(0);
}
return -1;
}
void MediaHistoryStore::SavePlaybackSession(
const GURL& url,
const media_session::MediaMetadata& metadata,
const absl::optional<media_session::MediaPosition>& position,
const std::vector<media_session::MediaImage>& artwork) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return;
if (!DB()->BeginTransaction()) {
LOG(ERROR) << "Failed to begin the transaction.";
base::UmaHistogramEnumeration(
MediaHistoryStore::kSessionWriteResultHistogramName,
MediaHistoryStore::SessionWriteResult::kFailedToEstablishTransaction);
return;
}
auto origin = url::Origin::Create(url);
if (!CreateOriginId(origin)) {
DB()->RollbackTransaction();
base::UmaHistogramEnumeration(
MediaHistoryStore::kSessionWriteResultHistogramName,
MediaHistoryStore::SessionWriteResult::kFailedToWriteOrigin);
return;
}
auto session_id =
session_table_->SavePlaybackSession(url, origin, metadata, position);
if (!session_id) {
DB()->RollbackTransaction();
base::UmaHistogramEnumeration(
MediaHistoryStore::kSessionWriteResultHistogramName,
MediaHistoryStore::SessionWriteResult::kFailedToWriteSession);
return;
}
for (auto& image : artwork) {
auto image_id =
images_table_->SaveOrGetImage(image.src, origin, image.type);
if (!image_id) {
DB()->RollbackTransaction();
base::UmaHistogramEnumeration(
MediaHistoryStore::kSessionWriteResultHistogramName,
MediaHistoryStore::SessionWriteResult::kFailedToWriteImage);
return;
}
// If we do not have any sizes associated with the image we should save a
// link with a null size. Otherwise, we should save a link for each size.
if (image.sizes.empty()) {
session_images_table_->LinkImage(*session_id, *image_id, absl::nullopt);
} else {
for (auto& size : image.sizes) {
session_images_table_->LinkImage(*session_id, *image_id, size);
}
}
}
DB()->CommitTransaction();
base::UmaHistogramEnumeration(
MediaHistoryStore::kSessionWriteResultHistogramName,
MediaHistoryStore::SessionWriteResult::kSuccess);
}
std::vector<mojom::MediaHistoryPlaybackSessionRowPtr>
MediaHistoryStore::GetPlaybackSessions(
absl::optional<unsigned int> num_sessions,
absl::optional<MediaHistoryStore::GetPlaybackSessionsFilter> filter) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return std::vector<mojom::MediaHistoryPlaybackSessionRowPtr>();
auto sessions =
session_table_->GetPlaybackSessions(num_sessions, std::move(filter));
for (auto& session : sessions) {
session->artwork = session_images_table_->GetImagesForSession(session->id);
}
return sessions;
}
void MediaHistoryStore::DeleteAllOriginData(
const std::set<url::Origin>& origins) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return;
if (!DB()->BeginTransaction()) {
LOG(ERROR) << "Failed to begin the transaction.";
return;
}
for (auto& origin : origins) {
if (!origin_table_->Delete(origin)) {
DB()->RollbackTransaction();
return;
}
}
DB()->CommitTransaction();
}
void MediaHistoryStore::DeleteAllURLData(const std::set<GURL>& urls) {
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return;
if (!DB()->BeginTransaction()) {
LOG(ERROR) << "Failed to begin the transaction.";
return;
}
MediaHistoryTableBase* tables[] = {
playback_table_.get(),
session_table_.get(),
};
std::set<url::Origin> origins_with_deletions;
for (auto& url : urls) {
origins_with_deletions.insert(url::Origin::Create(url));
for (auto* table : tables) {
if (!table->DeleteURL(url)) {
DB()->RollbackTransaction();
return;
}
}
}
for (auto& origin : origins_with_deletions) {
if (!origin_table_->RecalculateAggregateAudioVideoWatchTime(origin)) {
DB()->RollbackTransaction();
return;
}
}
// The mediaImages table will not be automatically cleared when we remove
// single sessions so we should remove them manually.
sql::Statement statement(DB()->GetUniqueStatement(
"DELETE FROM mediaImage WHERE id IN ("
" SELECT id FROM mediaImage LEFT JOIN sessionImage"
" ON sessionImage.image_id = mediaImage.id"
" WHERE sessionImage.session_id IS NULL)"));
if (!statement.Run()) {
DB()->RollbackTransaction();
} else {
DB()->CommitTransaction();
}
}
std::set<GURL> MediaHistoryStore::GetURLsInTableForTest(
const std::string& table) {
std::set<GURL> urls;
DCHECK(db_task_runner_->RunsTasksInCurrentSequence());
if (!CanAccessDatabase())
return urls;
sql::Statement statement(DB()->GetUniqueStatement(
base::StringPrintf("SELECT url from %s", table.c_str()).c_str()));
while (statement.Step()) {
urls.insert(GURL(statement.ColumnString(0)));
}
DCHECK(statement.Succeeded());
return urls;
}
void MediaHistoryStore::SetCancelled() {
DCHECK(!db_task_runner_->RunsTasksInCurrentSequence());
cancelled_.Set();
MediaHistoryTableBase* tables[] = {
origin_table_.get(), playback_table_.get(), session_table_.get(),
session_images_table_.get(), images_table_.get(),
};
for (auto* table : tables) {
if (table)
table->SetCancelled();
}
}
bool MediaHistoryStore::CanAccessDatabase() const {
return !IsCancelled() && initialization_successful_ && db_ && db_->is_open();
}
bool MediaHistoryStore::IsCancelled() const {
return cancelled_.IsSet();
}
} // namespace media_history