| // 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 "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_runner_util.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/media/feeds/media_feeds_service.h" |
| #include "chrome/browser/media/history/media_history_feed_items_table.h" |
| #include "chrome/browser/media/history/media_history_feeds_table.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" |
| |
| #if !defined(OS_ANDROID) |
| #include "chrome/browser/media/feeds/media_feeds_service.h" |
| #endif // !defined(OS_ANDROID) |
| |
| namespace { |
| |
| constexpr int kCurrentVersionNumber = 5; |
| 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. |
| ignore_result(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. |
| const int target_version = 2; |
| |
| // The mediaFeed table might not exist if the feature is disabled. |
| if (!db->DoesTableExist("mediaFeed")) { |
| meta_table->SetVersionNumber(target_version); |
| return target_version; |
| } |
| |
| static const char k1To2Sql[] = |
| "ALTER TABLE mediaFeed ADD COLUMN cookie_name_filter TEXT;"; |
| sql::Transaction transaction(db); |
| if (transaction.Begin() && db->Execute(k1To2Sql) && transaction.Commit()) { |
| meta_table->SetVersionNumber(target_version); |
| return target_version; |
| } |
| return 1; |
| } |
| |
| int MigrateFrom2To3(sql::Database* db, sql::MetaTable* meta_table) { |
| // Version 3 drops the mediaFeedAssociatedOrigin table. |
| const int target_version = 3; |
| |
| // The mediaFeedAssociatedOrigin table might not exist if the feature is |
| // disabled. |
| if (!db->DoesTableExist("mediaFeedAssociatedOrigin")) { |
| meta_table->SetVersionNumber(target_version); |
| return target_version; |
| } |
| |
| static const char k2To3Sql[] = "DROP TABLE mediaFeedAssociatedOrigin;"; |
| sql::Transaction transaction(db); |
| if (transaction.Begin() && db->Execute(k2To3Sql) && transaction.Commit()) { |
| meta_table->SetVersionNumber(target_version); |
| return target_version; |
| } |
| return 2; |
| } |
| |
| int MigrateFrom3To4(sql::Database* db, sql::MetaTable* meta_table) { |
| // Version 4 adds a new column to mediaFeed. |
| const int target_version = 3; |
| |
| // The mediaFeed table might not exist if the feature is disabled. |
| if (!db->DoesTableExist("mediaFeed")) { |
| meta_table->SetVersionNumber(target_version); |
| return target_version; |
| } |
| |
| static const char k3To4Sql[] = |
| "ALTER TABLE mediaFeed ADD COLUMN safe_search_result INTEGER DEFAULT 0;"; |
| sql::Transaction transaction(db); |
| if (transaction.Begin() && db->Execute(k3To4Sql) && transaction.Commit()) { |
| meta_table->SetVersionNumber(target_version); |
| return target_version; |
| } |
| return 3; |
| } |
| |
| int MigrateFrom4To5(sql::Database* db, sql::MetaTable* meta_table) { |
| // Version 5 adds a new column to mediaFeed. |
| const int target_version = 5; |
| |
| // The mediaFeed table might not exist if the feature is disabled. |
| if (!db->DoesTableExist("mediaFeed")) { |
| meta_table->SetVersionNumber(target_version); |
| return target_version; |
| } |
| |
| static const char k4To5Sql[] = |
| "ALTER TABLE mediaFeed ADD COLUMN favicon TEXT DEFAULT 0;"; |
| sql::Transaction transaction(db); |
| if (transaction.Begin() && db->Execute(k4To5Sql) && transaction.Commit()) { |
| meta_table->SetVersionNumber(target_version); |
| return target_version; |
| } |
| return 4; |
| } |
| |
| bool IsCauseFromExpiration(const net::CookieChangeCause& cause) { |
| return cause == net::CookieChangeCause::UNKNOWN_DELETION || |
| cause == net::CookieChangeCause::EXPIRED || |
| cause == net::CookieChangeCause::EXPIRED_OVERWRITE || |
| cause == net::CookieChangeCause::EXPLICIT || |
| cause == net::CookieChangeCause::EVICTED; |
| } |
| |
| bool IsMediaFeedsEnabled() { |
| #if defined(OS_ANDROID) |
| return false; |
| #else |
| return media_feeds::MediaFeedsService::IsEnabled(); |
| #endif // defined(OS_ANDROID) |
| } |
| |
| } // 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_)), |
| feeds_table_(IsMediaFeedsEnabled() |
| ? new MediaHistoryFeedsTable(db_task_runner_) |
| : nullptr), |
| feed_items_table_(IsMediaFeedsEnabled() |
| ? new MediaHistoryFeedItemsTable(db_task_runner_) |
| : nullptr), |
| 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 == 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()); |
| if (feeds_table_ && status == sql::INIT_OK) |
| status = feeds_table_->Initialize(db_.get()); |
| if (feed_items_table_ && status == sql::INIT_OK) |
| status = feed_items_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_master 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::TimeDelta::FromSeconds(statement.ColumnInt64(1))) |
| .ToJsTime(); |
| origin->cached_audio_video_watchtime = |
| base::TimeDelta::FromSeconds(statement.ColumnInt64(2)); |
| origin->actual_audio_video_watchtime = |
| base::TimeDelta::FromSeconds(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(); |
| } |
| |
| std::vector<media_feeds::mojom::MediaFeedItemPtr> |
| MediaHistoryStore::GetMediaFeedItems( |
| const MediaHistoryKeyedService::GetMediaFeedItemsRequest& request) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!CanAccessDatabase() || !feed_items_table_) |
| return std::vector<media_feeds::mojom::MediaFeedItemPtr>(); |
| |
| return feed_items_table_->GetItems(request); |
| } |
| |
| std::vector<media_feeds::mojom::MediaFeedPtr> MediaHistoryStore::GetMediaFeeds( |
| const MediaHistoryKeyedService::GetMediaFeedsRequest& request) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!CanAccessDatabase() || !feeds_table_) |
| return std::vector<media_feeds::mojom::MediaFeedPtr>(); |
| |
| return feeds_table_->GetRows(request); |
| } |
| |
| 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 base::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, base::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( |
| base::Optional<unsigned int> num_sessions, |
| base::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::DiscoverMediaFeed(const GURL& url, |
| const base::Optional<GURL>& favicon) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feeds_table_) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| if (!(CreateOriginId(url::Origin::Create(url)) && |
| feeds_table_->DiscoverFeed(url, favicon))) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| |
| DB()->CommitTransaction(); |
| } |
| |
| void MediaHistoryStore::StoreMediaFeedFetchResult( |
| MediaHistoryKeyedService::MediaFeedFetchResult result) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feeds_table_ || !feed_items_table_) |
| return; |
| |
| auto fetch_details = feeds_table_->GetFetchDetails(result.feed_id); |
| if (!fetch_details) |
| return; |
| |
| // If the reset token does not match then we should store a fetch failure. |
| if (fetch_details->reset_token != result.reset_token) { |
| MediaHistoryKeyedService::MediaFeedFetchResult new_result; |
| new_result.feed_id = result.feed_id; |
| new_result.status = |
| media_feeds::mojom::FetchResult::kFailedDueToResetWhileInflight; |
| StoreMediaFeedFetchResultInternal(std::move(new_result)); |
| return; |
| } |
| |
| StoreMediaFeedFetchResultInternal(std::move(result)); |
| } |
| |
| void MediaHistoryStore::StoreMediaFeedFetchResultInternal( |
| MediaHistoryKeyedService::MediaFeedFetchResult result) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feeds_table_ || !feed_items_table_) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| // Remove all the items currently associated with this feed. |
| if (!feed_items_table_->DeleteItems(result.feed_id)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| |
| int item_play_next_count = 0; |
| int item_content_types = 0; |
| int item_safe_count = 0; |
| |
| for (auto& item : result.items) { |
| // Save each item to the table. |
| if (!feed_items_table_->SaveItem(result.feed_id, item)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| |
| // If the item has a play next candidate or the user is currently watching |
| // this media then we should add it to the play next count. |
| if (item->play_next_candidate || |
| item->action_status == |
| media_feeds::mojom::MediaFeedItemActionStatus::kActive) { |
| item_play_next_count++; |
| } |
| |
| // If the item is marked as safe then we should add it to the safe count. |
| if (item->safe_search_result == |
| media_feeds::mojom::SafeSearchResult::kSafe) { |
| item_safe_count++; |
| } |
| |
| item_content_types |= static_cast<int>(item->type); |
| } |
| |
| const media_feeds::mojom::UserIdentifier* user_identifier = |
| result.user_identifier ? result.user_identifier.get() : nullptr; |
| |
| // Update the metadata associated with this feed. |
| if (!feeds_table_->UpdateFeedFromFetch( |
| result.feed_id, result.status, result.was_fetched_from_cache, |
| result.items.size(), item_play_next_count, item_content_types, |
| result.logos, user_identifier, result.display_name, item_safe_count, |
| result.cookie_name_filter)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| |
| if (result.status != |
| media_feeds::mojom::FetchResult::kFailedDueToResetWhileInflight) { |
| if (!feeds_table_->ClearResetReason(result.feed_id)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| } |
| |
| DB()->CommitTransaction(); |
| } |
| |
| MediaHistoryKeyedService::PendingSafeSearchCheckList |
| MediaHistoryStore::GetPendingSafeSearchCheckMediaFeedItems() { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| |
| if (!CanAccessDatabase() || !feed_items_table_ || !feeds_table_) |
| return MediaHistoryKeyedService::PendingSafeSearchCheckList(); |
| |
| auto items = feeds_table_->GetPendingSafeSearchCheckItems(); |
| for (auto& item : feed_items_table_->GetPendingSafeSearchCheckItems()) |
| items.push_back(std::move(item)); |
| |
| return items; |
| } |
| |
| void MediaHistoryStore::StoreMediaFeedItemSafeSearchResults( |
| std::map<MediaHistoryKeyedService::SafeSearchID, |
| media_feeds::mojom::SafeSearchResult> results) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feeds_table_ || !feed_items_table_) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| std::set<int64_t> feed_ids; |
| for (auto& entry : results) { |
| if (entry.first.first == |
| MediaHistoryKeyedService::SafeSearchCheckedType::kFeed) { |
| if (!feeds_table_->StoreSafeSearchResult(entry.first.second, |
| entry.second)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| |
| continue; |
| } |
| |
| auto feed_id = feed_items_table_->StoreSafeSearchResult(entry.first.second, |
| entry.second); |
| |
| if (!feed_id.has_value()) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| |
| feed_ids.insert(*feed_id); |
| } |
| |
| for (auto& feed_id : feed_ids) { |
| if (!feeds_table_->RecalculateSafeSearchItemCount(feed_id)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| } |
| |
| DB()->CommitTransaction(); |
| } |
| |
| 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(), feeds_table_.get(), |
| feed_items_table_.get(), |
| }; |
| |
| for (auto* table : tables) { |
| if (table) |
| table->SetCancelled(); |
| } |
| } |
| |
| void MediaHistoryStore::IncrementMediaFeedItemsShownCount( |
| const std::set<int64_t> feed_item_ids) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feed_items_table_) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| for (auto& feed_item_id : feed_item_ids) { |
| if (!feed_items_table_->IncrementShownCount(feed_item_id)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| } |
| |
| DB()->CommitTransaction(); |
| } |
| |
| void MediaHistoryStore::MarkMediaFeedItemAsClicked( |
| const int64_t& feed_item_id) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feed_items_table_) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| if (feed_items_table_->MarkAsClicked(feed_item_id)) { |
| DB()->CommitTransaction(); |
| } else { |
| DB()->RollbackTransaction(); |
| } |
| } |
| |
| bool MediaHistoryStore::CanAccessDatabase() const { |
| return !IsCancelled() && initialization_successful_ && db_ && db_->is_open(); |
| } |
| |
| bool MediaHistoryStore::IsCancelled() const { |
| return cancelled_.IsSet(); |
| } |
| |
| void MediaHistoryStore::UpdateMediaFeedDisplayTime(const int64_t feed_id) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialization_successful_) |
| return; |
| |
| if (!feeds_table_) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| if (!feeds_table_->UpdateDisplayTime(feed_id)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| |
| DB()->CommitTransaction(); |
| } |
| |
| void MediaHistoryStore::ResetMediaFeed(const url::Origin& origin, |
| media_feeds::mojom::ResetReason reason) { |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feeds_table_ || !feed_items_table_) |
| return; |
| |
| // Get the feed for |origin|. |
| base::Optional<int64_t> feed_id = feeds_table_->GetFeedForOrigin(origin); |
| if (!feed_id.has_value()) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| if (ResetMediaFeedInternal({*feed_id}, reason)) { |
| DB()->CommitTransaction(); |
| } else { |
| DB()->RollbackTransaction(); |
| } |
| } |
| |
| void MediaHistoryStore::ResetMediaFeedDueToCookies( |
| const url::Origin& origin, |
| const bool include_subdomains, |
| const std::string& name, |
| const net::CookieChangeCause& cause) { |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feeds_table_ || !feed_items_table_) |
| return; |
| |
| // Get all the feeds for |origin| possibly including subdomains. |
| std::set<int64_t> feed_ids; |
| |
| if (include_subdomains) |
| feed_ids = feeds_table_->GetFeedsForOriginSubdomain(origin); |
| |
| base::Optional<int64_t> feed_id = feeds_table_->GetFeedForOrigin(origin); |
| if (feed_id.has_value()) |
| feed_ids.insert(*feed_id); |
| |
| if (feed_ids.empty()) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| std::set<int64_t> feed_ids_to_reset; |
| for (auto feed_id : feed_ids) { |
| auto cookie_name_filter = feeds_table_->GetCookieNameFilter(feed_id); |
| |
| // If the cookie name filter is empty then we only allow feeds to be reset |
| // if the cookie change was from expiration. |
| if (cookie_name_filter.empty() && IsCauseFromExpiration(cause)) |
| feed_ids_to_reset.insert(feed_id); |
| |
| // If we have a cookie name filter and the current cookie matches that name |
| // then we allow any type of cookie change to reset the feed because we |
| // can be more specific. |
| if (!cookie_name_filter.empty() && cookie_name_filter == name) |
| feed_ids_to_reset.insert(feed_id); |
| } |
| |
| if (ResetMediaFeedInternal(feed_ids_to_reset, |
| media_feeds::mojom::ResetReason::kCookies)) { |
| DB()->CommitTransaction(); |
| } else { |
| DB()->RollbackTransaction(); |
| } |
| } |
| |
| void MediaHistoryStore::ResetMediaFeedDueToCacheClearing( |
| const base::Time& start_time, |
| const base::Time& end_time, |
| MediaHistoryKeyedService::CacheClearingFilter filter) { |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feeds_table_) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| const auto start_time_s = start_time.ToDeltaSinceWindowsEpoch().InSeconds(); |
| const auto end_time_s = end_time.ToDeltaSinceWindowsEpoch().InSeconds(); |
| |
| sql::Statement statement(DB()->GetCachedStatement( |
| SQL_FROM_HERE, |
| "SELECT id, url FROM mediaFeed WHERE last_fetch_time_s >= ? AND " |
| "last_fetch_time_s <= ?")); |
| statement.BindInt64(0, start_time_s); |
| statement.BindInt64(1, end_time_s); |
| |
| std::set<int64_t> feed_ids; |
| while (statement.Step()) { |
| GURL url(statement.ColumnString(1)); |
| |
| if (!filter.is_null() && !filter.Run(url)) |
| continue; |
| |
| feed_ids.insert(statement.ColumnInt64(0)); |
| } |
| |
| if (ResetMediaFeedInternal(feed_ids, |
| media_feeds::mojom::ResetReason::kCache)) { |
| DB()->CommitTransaction(); |
| } else { |
| DB()->RollbackTransaction(); |
| } |
| } |
| |
| bool MediaHistoryStore::ResetMediaFeedInternal( |
| const std::set<int64_t>& feed_ids, |
| media_feeds::mojom::ResetReason reason) { |
| DCHECK_LT(0, DB()->transaction_nesting()); |
| if (!CanAccessDatabase()) |
| return false; |
| |
| for (auto& feed_id : feed_ids) { |
| // Remove all the items currently associated with this feed. |
| if (!feeds_table_->Reset(feed_id, reason)) |
| return false; |
| |
| // Remove all the items currently associated with this feed. |
| if (!feed_items_table_->DeleteItems(feed_id)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void MediaHistoryStore::DeleteMediaFeed(const int64_t feed_id) { |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feeds_table_) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| LOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| if (!feeds_table_->Delete(feed_id)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| |
| DB()->CommitTransaction(); |
| } |
| |
| base::Optional<MediaHistoryKeyedService::MediaFeedFetchDetails> |
| MediaHistoryStore::GetMediaFeedFetchDetails(const int64_t feed_id) { |
| DCHECK(db_task_runner_->RunsTasksInCurrentSequence()); |
| if (!CanAccessDatabase() || !feeds_table_) |
| return base::nullopt; |
| |
| return feeds_table_->GetFetchDetails(feed_id); |
| } |
| |
| void MediaHistoryStore::UpdateFeedUserStatus( |
| const int64_t feed_id, |
| media_feeds::mojom::FeedUserStatus status) { |
| if (!CanAccessDatabase()) |
| return; |
| |
| if (!feeds_table_) |
| return; |
| |
| if (!DB()->BeginTransaction()) { |
| DLOG(ERROR) << "Failed to begin the transaction."; |
| return; |
| } |
| |
| if (!feeds_table_->UpdateFeedUserStatus(feed_id, status)) { |
| DB()->RollbackTransaction(); |
| return; |
| } |
| |
| DB()->CommitTransaction(); |
| } |
| |
| } // namespace media_history |