| // 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 "ash/ambient/ambient_photo_controller.h" |
| |
| #include <array> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/ambient/ambient_constants.h" |
| #include "ash/ambient/ambient_controller.h" |
| #include "ash/ambient/ambient_photo_cache.h" |
| #include "ash/ambient/model/ambient_backend_model.h" |
| #include "ash/public/cpp/ambient/ambient_backend_controller.h" |
| #include "ash/public/cpp/ambient/ambient_client.h" |
| #include "ash/public/cpp/image_downloader.h" |
| #include "ash/shell.h" |
| #include "base/barrier_closure.h" |
| #include "base/base64.h" |
| #include "base/base_paths.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/guid.h" |
| #include "base/hash/sha1.h" |
| #include "base/optional.h" |
| #include "base/path_service.h" |
| #include "base/rand_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/post_task.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/task_runner_util.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "url/gurl.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // TODO(b/161357364): refactor utility functions and constants |
| |
| constexpr net::BackoffEntry::Policy kFetchTopicRetryBackoffPolicy = { |
| 10, // Number of initial errors to ignore. |
| 500, // Initial delay in ms. |
| 2.0, // Factor by which the waiting time will be multiplied. |
| 0.2, // Fuzzing percentage. |
| 2 * 60 * 1000, // Maximum delay in ms. |
| -1, // Never discard the entry. |
| true, // Use initial delay. |
| }; |
| |
| constexpr net::BackoffEntry::Policy kResumeFetchImageBackoffPolicy = { |
| kMaxConsecutiveReadPhotoFailures, // Number of initial errors to ignore. |
| 500, // Initial delay in ms. |
| 2.0, // Factor by which the waiting time will be multiplied. |
| 0.2, // Fuzzing percentage. |
| 8 * 60 * 1000, // Maximum delay in ms. |
| -1, // Never discard the entry. |
| true, // Use initial delay. |
| }; |
| |
| void DownloadImageFromUrl( |
| const std::string& url, |
| base::OnceCallback<void(const gfx::ImageSkia&)> callback) { |
| DCHECK(!url.empty()); |
| |
| // During shutdown, we may not have `ImageDownloader` when reach here. |
| if (!ImageDownloader::Get()) |
| return; |
| |
| ImageDownloader::Get()->Download(GURL(url), NO_TRAFFIC_ANNOTATION_YET, |
| base::BindOnce(std::move(callback))); |
| } |
| |
| base::TaskTraits GetTaskTraits() { |
| return {base::MayBlock(), base::TaskPriority::USER_BLOCKING, |
| base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}; |
| } |
| |
| // Get the root path for ambient mode. |
| base::FilePath GetRootPath() { |
| base::FilePath home_dir; |
| CHECK(base::PathService::Get(base::DIR_HOME, &home_dir)); |
| return home_dir.Append(FILE_PATH_LITERAL(kAmbientModeDirectoryName)); |
| } |
| |
| base::FilePath GetCachePath() { |
| return GetRootPath().Append( |
| FILE_PATH_LITERAL(kAmbientModeCacheDirectoryName)); |
| } |
| |
| base::FilePath GetBackupCachePath() { |
| return GetRootPath().Append( |
| FILE_PATH_LITERAL(kAmbientModeBackupCacheDirectoryName)); |
| } |
| |
| base::FilePath GetBackupFilePath(size_t index) { |
| return GetBackupCachePath().Append(base::NumberToString(index) + |
| kPhotoFileExt); |
| } |
| |
| base::FilePath GetRelatedFilePath(const std::string& file_name) { |
| return GetCachePath().Append(file_name + kRelatedPhotoSuffix + kPhotoFileExt); |
| } |
| |
| bool CreateDirIfNotExists(const base::FilePath& path) { |
| return base::DirectoryExists(path) || base::CreateDirectory(path); |
| } |
| |
| void WriteFile(const base::FilePath& path, const std::string& data) { |
| if (!CreateDirIfNotExists(GetCachePath())) { |
| LOG(ERROR) << "Cannot create ambient mode directory."; |
| return; |
| } |
| |
| if (base::SysInfo::AmountOfFreeDiskSpace(GetRootPath()) < |
| kMaxReservedAvailableDiskSpaceByte) { |
| LOG(WARNING) << "Not enough disk space left."; |
| return; |
| } |
| |
| // Create a temp file. |
| base::FilePath temp_file; |
| if (!base::CreateTemporaryFileInDir(path.DirName(), &temp_file)) { |
| LOG(ERROR) << "Cannot create a temporary file."; |
| return; |
| } |
| |
| // Write to the tmp file. |
| const int size = data.size(); |
| int written_size = base::WriteFile(temp_file, data.data(), size); |
| if (written_size != size) { |
| LOG(ERROR) << "Cannot write the temporary file."; |
| base::DeleteFile(temp_file); |
| return; |
| } |
| |
| // Replace the current file with the temp file. |
| if (!base::ReplaceFile(temp_file, path, /*error=*/nullptr)) |
| LOG(ERROR) << "Cannot replace the temporary file."; |
| } |
| |
| const std::array<const char*, 2>& GetBackupPhotoUrls() { |
| return Shell::Get() |
| ->ambient_controller() |
| ->ambient_backend_controller() |
| ->GetBackupPhotoUrls(); |
| } |
| |
| } // namespace |
| |
| AmbientPhotoController::AmbientPhotoController() |
| : fetch_topic_retry_backoff_(&kFetchTopicRetryBackoffPolicy), |
| resume_fetch_image_backoff_(&kResumeFetchImageBackoffPolicy), |
| photo_cache_(AmbientPhotoCache::Create()), |
| task_runner_( |
| base::ThreadPool::CreateSequencedTaskRunner(GetTaskTraits())) { |
| ambient_backend_model_observer_.Add(&ambient_backend_model_); |
| } |
| |
| AmbientPhotoController::~AmbientPhotoController() = default; |
| |
| void AmbientPhotoController::StartScreenUpdate() { |
| FetchTopics(); |
| FetchWeather(); |
| weather_refresh_timer_.Start( |
| FROM_HERE, kWeatherRefreshInterval, |
| base::BindRepeating(&AmbientPhotoController::FetchWeather, |
| weak_factory_.GetWeakPtr())); |
| if (backup_photo_refresh_timer_.IsRunning()) { |
| // Would use |timer_.FireNow()| but this does not execute if screen is |
| // locked. Manually call the expected callback instead. |
| backup_photo_refresh_timer_.Stop(); |
| PrepareFetchBackupImages(); |
| } |
| } |
| |
| void AmbientPhotoController::StopScreenUpdate() { |
| photo_refresh_timer_.Stop(); |
| weather_refresh_timer_.Stop(); |
| topic_index_ = 0; |
| image_refresh_started_ = false; |
| retries_to_read_from_cache_ = kMaxNumberOfCachedImages; |
| backup_retries_to_read_from_cache_ = GetBackupPhotoUrls().size(); |
| fetch_topic_retry_backoff_.Reset(); |
| resume_fetch_image_backoff_.Reset(); |
| ambient_backend_model_.Clear(); |
| weak_factory_.InvalidateWeakPtrs(); |
| } |
| |
| void AmbientPhotoController::ScheduleFetchBackupImages() { |
| if (backup_photo_refresh_timer_.IsRunning()) |
| return; |
| |
| backup_photo_refresh_timer_.Start( |
| FROM_HERE, |
| std::max(kBackupPhotoRefreshDelay, |
| resume_fetch_image_backoff_.GetTimeUntilRelease()), |
| base::BindOnce(&AmbientPhotoController::PrepareFetchBackupImages, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void AmbientPhotoController::OnTopicsChanged() { |
| if (ambient_backend_model_.topics().size() < kMaxNumberOfCachedImages) |
| ScheduleFetchTopics(/*backoff=*/false); |
| |
| if (!image_refresh_started_) { |
| image_refresh_started_ = true; |
| ScheduleRefreshImage(); |
| } |
| } |
| |
| void AmbientPhotoController::FetchTopics() { |
| Shell::Get() |
| ->ambient_controller() |
| ->ambient_backend_controller() |
| ->FetchScreenUpdateInfo( |
| kTopicsBatchSize, |
| base::BindOnce(&AmbientPhotoController::OnScreenUpdateInfoFetched, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void AmbientPhotoController::FetchWeather() { |
| Shell::Get() |
| ->ambient_controller() |
| ->ambient_backend_controller() |
| ->FetchWeather(base::BindOnce( |
| &AmbientPhotoController::StartDownloadingWeatherConditionIcon, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void AmbientPhotoController::ClearCache() { |
| task_runner_->PostTask(FROM_HERE, |
| base::BindOnce( |
| [](const base::FilePath& file_path) { |
| base::DeletePathRecursively(file_path); |
| }, |
| GetCachePath())); |
| } |
| |
| void AmbientPhotoController::ScheduleFetchTopics(bool backoff) { |
| // If retry, using the backoff delay, otherwise the default delay. |
| const base::TimeDelta delay = |
| backoff ? fetch_topic_retry_backoff_.GetTimeUntilRelease() |
| : kTopicFetchInterval; |
| base::SequencedTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&AmbientPhotoController::FetchTopics, |
| weak_factory_.GetWeakPtr()), |
| delay); |
| } |
| |
| void AmbientPhotoController::ScheduleRefreshImage() { |
| photo_refresh_timer_.Start( |
| FROM_HERE, ambient_backend_model_.GetPhotoRefreshInterval(), |
| base::BindOnce(&AmbientPhotoController::FetchPhotoRawData, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void AmbientPhotoController::PrepareFetchBackupImages() { |
| task_runner_->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce([]() { CreateDirIfNotExists(GetBackupCachePath()); }), |
| base::BindOnce(&AmbientPhotoController::FetchBackupImages, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void AmbientPhotoController::FetchBackupImages() { |
| const auto& backup_photo_urls = GetBackupPhotoUrls(); |
| backup_retries_to_read_from_cache_ = backup_photo_urls.size(); |
| for (size_t i = 0; i < backup_photo_urls.size(); i++) { |
| photo_cache_->DownloadPhotoToFile( |
| backup_photo_urls.at(i), |
| base::BindOnce(&AmbientPhotoController::OnBackupImageFetched, |
| weak_factory_.GetWeakPtr()), |
| GetBackupFilePath(i)); |
| } |
| } |
| |
| void AmbientPhotoController::OnBackupImageFetched(base::FilePath file_path) { |
| if (file_path.empty()) { |
| // TODO(b/169807068) Change to retry individual failed images. |
| resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/false); |
| LOG(WARNING) << "Downloading backup image failed."; |
| ScheduleFetchBackupImages(); |
| return; |
| } |
| resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/true); |
| } |
| |
| const AmbientModeTopic* AmbientPhotoController::GetNextTopic() { |
| const auto& topics = ambient_backend_model_.topics(); |
| // If no more topics, will read from cache. |
| if (topic_index_ == topics.size()) { |
| DVLOG(3) << "No more topics"; |
| return nullptr; |
| } |
| |
| return &topics[topic_index_++]; |
| } |
| |
| void AmbientPhotoController::OnScreenUpdateInfoFetched( |
| const ash::ScreenUpdate& screen_update) { |
| // It is possible that |screen_update| is an empty instance if fatal errors |
| // happened during the fetch. |
| if (screen_update.next_topics.empty()) { |
| LOG(WARNING) << "The screen update has no topics."; |
| |
| fetch_topic_retry_backoff_.InformOfRequest(/*succeeded=*/false); |
| ScheduleFetchTopics(/*backoff=*/true); |
| if (!image_refresh_started_) { |
| image_refresh_started_ = true; |
| ScheduleRefreshImage(); |
| } |
| return; |
| } |
| fetch_topic_retry_backoff_.InformOfRequest(/*succeeded=*/true); |
| ambient_backend_model_.AppendTopics(screen_update.next_topics); |
| StartDownloadingWeatherConditionIcon(screen_update.weather_info); |
| } |
| |
| void AmbientPhotoController::ResetImageData() { |
| image_data_.reset(); |
| related_image_data_.reset(); |
| image_details_.reset(); |
| |
| image_ = gfx::ImageSkia(); |
| related_image_ = gfx::ImageSkia(); |
| } |
| |
| void AmbientPhotoController::FetchPhotoRawData() { |
| const AmbientModeTopic* topic = GetNextTopic(); |
| ResetImageData(); |
| |
| if (topic) { |
| const int num_callbacks = (topic->related_image_url) ? 2 : 1; |
| auto on_done = base::BarrierClosure( |
| num_callbacks, |
| base::BindOnce(&AmbientPhotoController::OnAllPhotoRawDataAvailable, |
| weak_factory_.GetWeakPtr(), |
| /*from_downloading=*/true)); |
| |
| photo_cache_->DownloadPhoto( |
| topic->url, |
| base::BindOnce(&AmbientPhotoController::OnPhotoRawDataAvailable, |
| weak_factory_.GetWeakPtr(), |
| /*from_downloading=*/true, |
| /*is_related_image=*/false, on_done, |
| std::make_unique<std::string>(topic->details))); |
| |
| if (topic->related_image_url) { |
| photo_cache_->DownloadPhoto( |
| *(topic->related_image_url), |
| base::BindOnce(&AmbientPhotoController::OnPhotoRawDataAvailable, |
| weak_factory_.GetWeakPtr(), |
| /*from_downloading=*/true, |
| /*is_related_image=*/true, on_done, |
| std::make_unique<std::string>(topic->details))); |
| } |
| return; |
| } |
| |
| // If |topic| is nullptr, will try to read from disk cache. |
| TryReadPhotoRawData(); |
| } |
| |
| void AmbientPhotoController::TryReadPhotoRawData() { |
| ResetImageData(); |
| // Stop reading from cache after the max number of retries. |
| if (retries_to_read_from_cache_ == 0) { |
| if (backup_retries_to_read_from_cache_ == 0) { |
| LOG(WARNING) << "Failed to read from cache"; |
| ambient_backend_model_.AddImageFailure(); |
| // Do not refresh image if image loading has failed repeatedly, or there |
| // are no more topics to retry. |
| if (ambient_backend_model_.ImageLoadingFailed() || |
| topic_index_ == ambient_backend_model_.topics().size()) { |
| LOG(WARNING) << "Not attempting image refresh"; |
| image_refresh_started_ = false; |
| return; |
| } |
| |
| // Try to resume normal workflow with backoff. |
| const base::TimeDelta delay = |
| resume_fetch_image_backoff_.GetTimeUntilRelease(); |
| base::SequencedTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&AmbientPhotoController::ScheduleRefreshImage, |
| weak_factory_.GetWeakPtr()), |
| delay); |
| return; |
| } |
| |
| --backup_retries_to_read_from_cache_; |
| // Try to read a backup image. |
| auto photo_data = std::make_unique<std::string>(); |
| auto* photo_data_ptr = photo_data.get(); |
| auto on_done = base::BindRepeating( |
| &AmbientPhotoController::OnAllPhotoRawDataAvailable, |
| weak_factory_.GetWeakPtr(), /*from_downloading=*/false); |
| |
| task_runner_->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce( |
| [](size_t index, std::string* data) { |
| if (!base::ReadFileToString(GetBackupFilePath(index), data)) { |
| LOG(ERROR) << "Unable to read from backup cache."; |
| data->clear(); |
| } |
| }, |
| backup_cache_index_for_display_, photo_data_ptr), |
| base::BindOnce(&AmbientPhotoController::OnPhotoRawDataAvailable, |
| weak_factory_.GetWeakPtr(), /*from_downloading=*/false, |
| /*is_related_image=*/false, std::move(on_done), |
| /*details=*/std::make_unique<std::string>(), |
| std::move(photo_data))); |
| |
| backup_cache_index_for_display_++; |
| if (backup_cache_index_for_display_ == GetBackupPhotoUrls().size()) |
| backup_cache_index_for_display_ = 0; |
| return; |
| } |
| |
| --retries_to_read_from_cache_; |
| std::string file_name = base::NumberToString(cache_index_for_display_); |
| |
| ++cache_index_for_display_; |
| if (cache_index_for_display_ == kMaxNumberOfCachedImages) |
| cache_index_for_display_ = 0; |
| |
| auto photo_data = std::make_unique<std::string>(); |
| auto photo_details = std::make_unique<std::string>(); |
| auto* photo_data_ptr = photo_data.get(); |
| auto* photo_details_ptr = photo_details.get(); |
| |
| auto on_done = base::BarrierClosure( |
| /*num_closures=*/2, |
| base::BindOnce(&AmbientPhotoController::OnAllPhotoRawDataAvailable, |
| weak_factory_.GetWeakPtr(), |
| /*from_downloading=*/false)); |
| |
| task_runner_->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce( |
| [](const std::string& file_name, std::string* photo_data, |
| std::string* photo_details) { |
| if (!base::ReadFileToString( |
| GetCachePath().Append(file_name + kPhotoFileExt), |
| photo_data)) { |
| photo_data->clear(); |
| } |
| if (!base::ReadFileToString( |
| GetCachePath().Append(file_name + kPhotoDetailsFileExt), |
| photo_details)) { |
| photo_details->clear(); |
| } |
| }, |
| file_name, photo_data_ptr, photo_details_ptr), |
| base::BindOnce(&AmbientPhotoController::OnPhotoRawDataAvailable, |
| weak_factory_.GetWeakPtr(), /*from_downloading=*/false, |
| /*is_related_image=*/false, on_done, |
| std::move(photo_details), std::move(photo_data))); |
| |
| auto related_photo_data = std::make_unique<std::string>(); |
| auto* related_photo_data_ptr = related_photo_data.get(); |
| task_runner_->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce( |
| [](const std::string& file_name, std::string* related_photo_data) { |
| const base::FilePath& file = GetRelatedFilePath(file_name); |
| if (!base::PathExists(file) || |
| !base::ReadFileToString(file, related_photo_data)) { |
| related_photo_data->clear(); |
| } |
| }, |
| file_name, related_photo_data_ptr), |
| base::BindOnce(&AmbientPhotoController::OnPhotoRawDataAvailable, |
| weak_factory_.GetWeakPtr(), /*from_downloading=*/false, |
| /*is_related_image=*/true, on_done, |
| /*details=*/std::make_unique<std::string>(), |
| std::move(related_photo_data))); |
| } |
| |
| void AmbientPhotoController::OnPhotoRawDataAvailable( |
| bool from_downloading, |
| bool is_related_image, |
| base::RepeatingClosure on_done, |
| std::unique_ptr<std::string> details, |
| std::unique_ptr<std::string> data) { |
| if (is_related_image) { |
| related_image_data_ = std::move(data); |
| } else { |
| image_data_ = std::move(data); |
| image_details_ = std::move(details); |
| } |
| std::move(on_done).Run(); |
| } |
| |
| void AmbientPhotoController::OnAllPhotoRawDataAvailable(bool from_downloading) { |
| if (!image_data_ || image_data_->empty()) { |
| if (from_downloading) { |
| LOG(ERROR) << "Failed to download image"; |
| resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/false); |
| } |
| // Try to read from cache when failure happens. |
| TryReadPhotoRawData(); |
| return; |
| } |
| DVLOG_IF(3, from_downloading) |
| << "Save photo to cache index: " << cache_index_for_store_; |
| const std::string file_name = base::NumberToString(cache_index_for_store_); |
| // If the data is fetched from downloading, write to disk. |
| // Note: WriteFile() could fail. The saved file name may not be continuous. |
| if (from_downloading) |
| ++cache_index_for_store_; |
| if (cache_index_for_store_ == kMaxNumberOfCachedImages) |
| cache_index_for_store_ = 0; |
| |
| const int num_callbacks = related_image_data_ ? 2 : 1; |
| auto on_done = base::BarrierClosure( |
| num_callbacks, |
| base::BindOnce(&AmbientPhotoController::OnAllPhotoDecoded, |
| weak_factory_.GetWeakPtr(), from_downloading, |
| /*hash=*/base::SHA1HashString(*image_data_))); |
| |
| base::Optional<std::string> related_image_data; |
| if (related_image_data_) |
| related_image_data = *related_image_data_; |
| |
| task_runner_->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce( |
| [](const std::string& file_name, bool need_to_save, |
| const std::string& data, const std::string& details, |
| const base::Optional<std::string>& related_data) { |
| if (need_to_save) { |
| WriteFile(GetCachePath().Append(file_name + kPhotoFileExt), data); |
| WriteFile(GetCachePath().Append(file_name + kPhotoDetailsFileExt), |
| details); |
| const base::FilePath& related_data_file = |
| GetRelatedFilePath(file_name); |
| if (related_data) { |
| WriteFile(related_data_file, *related_data); |
| } else { |
| if (base::PathExists(related_data_file)) |
| base::DeleteFile(related_data_file); |
| } |
| } |
| }, |
| file_name, from_downloading, *image_data_, *image_details_, |
| std::move(related_image_data)), |
| base::BindOnce(&AmbientPhotoController::DecodePhotoRawData, |
| weak_factory_.GetWeakPtr(), from_downloading, |
| /*is_related_image=*/false, on_done, |
| std::move(image_data_))); |
| |
| if (related_image_data_) { |
| DecodePhotoRawData(from_downloading, /*is_related_image=*/true, on_done, |
| std::move(related_image_data_)); |
| } |
| } |
| |
| void AmbientPhotoController::DecodePhotoRawData( |
| bool from_downloading, |
| bool is_related_image, |
| base::RepeatingClosure on_done, |
| std::unique_ptr<std::string> data) { |
| photo_cache_->DecodePhoto( |
| std::move(data), |
| base::BindOnce(&AmbientPhotoController::OnPhotoDecoded, |
| weak_factory_.GetWeakPtr(), from_downloading, |
| is_related_image, std::move(on_done))); |
| } |
| |
| void AmbientPhotoController::OnPhotoDecoded(bool from_downloading, |
| bool is_related_image, |
| base::RepeatingClosure on_done, |
| const gfx::ImageSkia& image) { |
| if (is_related_image) |
| related_image_ = image; |
| else |
| image_ = image; |
| |
| std::move(on_done).Run(); |
| } |
| |
| void AmbientPhotoController::OnAllPhotoDecoded(bool from_downloading, |
| const std::string& hash) { |
| if (image_.isNull()) { |
| LOG(WARNING) << "Image decoding failed"; |
| if (from_downloading) |
| resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/false); |
| |
| // Try to read from cache when failure happens. |
| TryReadPhotoRawData(); |
| return; |
| } else if (ambient_backend_model_.HashMatchesNextImage(hash)) { |
| LOG(WARNING) << "Skipping loading duplicate image."; |
| TryReadPhotoRawData(); |
| return; |
| } |
| |
| retries_to_read_from_cache_ = kMaxNumberOfCachedImages; |
| backup_retries_to_read_from_cache_ = GetBackupPhotoUrls().size(); |
| |
| if (from_downloading) |
| resume_fetch_image_backoff_.InformOfRequest(/*succeeded=*/true); |
| |
| PhotoWithDetails detailed_photo; |
| detailed_photo.photo = image_; |
| detailed_photo.related_photo = related_image_; |
| detailed_photo.details = *image_details_; |
| detailed_photo.hash = hash; |
| |
| ResetImageData(); |
| |
| ambient_backend_model_.AddNextImage(std::move(detailed_photo)); |
| |
| ScheduleRefreshImage(); |
| } |
| |
| void AmbientPhotoController::StartDownloadingWeatherConditionIcon( |
| const base::Optional<WeatherInfo>& weather_info) { |
| if (!weather_info) { |
| LOG(WARNING) << "No weather info included in the response."; |
| return; |
| } |
| |
| if (!weather_info->temp_f.has_value()) { |
| LOG(WARNING) << "No temperature included in weather info."; |
| return; |
| } |
| |
| if (weather_info->condition_icon_url.value_or(std::string()).empty()) { |
| LOG(WARNING) << "No value found for condition icon url in the weather info " |
| "response."; |
| return; |
| } |
| |
| // Ideally we should avoid downloading from the same url again to reduce the |
| // overhead, as it's unlikely that the weather condition is changing |
| // frequently during the day. |
| // TODO(meilinw): avoid repeated downloading by caching the last N url hashes, |
| // where N should depend on the icon image size. |
| DownloadImageFromUrl( |
| weather_info->condition_icon_url.value(), |
| base::BindOnce(&AmbientPhotoController::OnWeatherConditionIconDownloaded, |
| weak_factory_.GetWeakPtr(), weather_info->temp_f.value(), |
| weather_info->show_celsius)); |
| } |
| |
| void AmbientPhotoController::OnWeatherConditionIconDownloaded( |
| float temp_f, |
| bool show_celsius, |
| const gfx::ImageSkia& icon) { |
| // For now we only show the weather card when both fields have values. |
| // TODO(meilinw): optimize the behavior with more specific error handling. |
| if (icon.isNull()) |
| return; |
| |
| ambient_backend_model_.UpdateWeatherInfo(icon, temp_f, show_celsius); |
| } |
| |
| void AmbientPhotoController::FetchTopicsForTesting() { |
| FetchTopics(); |
| } |
| |
| void AmbientPhotoController::FetchImageForTesting() { |
| FetchPhotoRawData(); |
| } |
| |
| void AmbientPhotoController::FetchBackupImagesForTesting() { |
| PrepareFetchBackupImages(); |
| } |
| |
| } // namespace ash |