| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/ambient/ambient_weather_controller.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| |
| #include "ash/ambient/ambient_constants.h" |
| #include "ash/ambient/ambient_controller.h" |
| #include "ash/ambient/model/ambient_weather_model.h" |
| #include "ash/public/cpp/ambient/ambient_backend_controller.h" |
| #include "ash/public/cpp/image_downloader.h" |
| #include "ash/public/cpp/session/session_types.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "base/check.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/location.h" |
| #include "base/memory/ptr_util.h" |
| #include "chromeos/ash/components/geolocation/simple_geolocation_provider.h" |
| #include "components/account_id/account_id.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "ui/gfx/image/image_skia.h" |
| |
| namespace ash { |
| namespace { |
| |
| // TODO(jamescook): Rename to "ambient weather". |
| constexpr net::NetworkTrafficAnnotationTag kAmbientPhotoControllerTag = |
| net::DefineNetworkTrafficAnnotation("ambient_photo_controller", R"( |
| semantics { |
| sender: "Ambient photo" |
| description: |
| "Download ambient image weather icon from Google." |
| trigger: |
| "Triggered periodically when the battery is charged and the user " |
| "is idle." |
| data: "None." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This feature is off by default and can be overridden by user." |
| policy_exception_justification: |
| "This feature is set by user settings.ambient_mode.enabled pref. " |
| "The user setting is per device and cannot be overriden by admin." |
| })"); |
| |
| void DownloadImageFromUrl(const std::string& url, |
| ImageDownloader::DownloadCallback callback) { |
| DCHECK(!url.empty()); |
| |
| // During shutdown, we may not have `ImageDownloader` when reach here. |
| if (!ImageDownloader::Get()) { |
| return; |
| } |
| |
| const UserSession* active_user_session = |
| Shell::Get()->session_controller()->GetUserSession(0); |
| DCHECK(active_user_session); |
| |
| ImageDownloader::Get()->Download(GURL(url), kAmbientPhotoControllerTag, |
| active_user_session->user_info.account_id, |
| std::move(callback)); |
| } |
| |
| } // namespace |
| |
| AmbientWeatherController::ScopedRefresher::ScopedRefresher( |
| AmbientWeatherController* controller) |
| : controller_(controller) { |
| CHECK(controller_); |
| } |
| |
| AmbientWeatherController::ScopedRefresher::~ScopedRefresher() { |
| controller_->OnScopedRefresherDestroyed(); |
| } |
| |
| AmbientWeatherController::AmbientWeatherController( |
| SimpleGeolocationProvider* const location_permission_provider) |
| : location_permission_provider_(location_permission_provider), |
| weather_model_(std::make_unique<AmbientWeatherModel>()) { |
| CHECK_NE(location_permission_provider_, nullptr); |
| location_permission_provider_->AddObserver(this); |
| } |
| |
| AmbientWeatherController::~AmbientWeatherController() { |
| CHECK_NE(location_permission_provider_, nullptr); |
| location_permission_provider_->RemoveObserver(this); |
| } |
| |
| void AmbientWeatherController::OnGeolocationPermissionChanged(bool enabled) { |
| // When system permission is blocked, stop scheduling new requests and drop |
| // all pending requests. Also clears the weather model cache for privacy |
| // reasons. |
| if (!enabled) { |
| weather_refresh_timer_.Stop(); |
| weak_factory_.InvalidateWeakPtrs(); |
| ClearAmbientWeatherModel(); |
| return; |
| } |
| |
| // System permission is granted, resume scheduler if needed. |
| if (num_active_scoped_refreshers_ > 0) { |
| FetchWeather(); |
| weather_refresh_timer_.Start(FROM_HERE, kWeatherRefreshInterval, this, |
| &AmbientWeatherController::FetchWeather); |
| } |
| } |
| |
| std::unique_ptr<AmbientWeatherController::ScopedRefresher> |
| AmbientWeatherController::CreateScopedRefresher() { |
| ++num_active_scoped_refreshers_; |
| if (!weather_refresh_timer_.IsRunning() && IsGeolocationUsageAllowed()) { |
| FetchWeather(); |
| weather_refresh_timer_.Start(FROM_HERE, kWeatherRefreshInterval, this, |
| &AmbientWeatherController::FetchWeather); |
| } |
| // `WrapUnique()` needed for ScopedRefresher's private constructor. |
| return base::WrapUnique(new ScopedRefresher(this)); |
| } |
| |
| void AmbientWeatherController::FetchWeather() { |
| Shell::Get() |
| ->ambient_controller() |
| ->ambient_backend_controller() |
| ->FetchWeather( |
| /*weather_client_id=*/std::nullopt, |
| /*prefer_alpha_endpoint=*/false, |
| base::BindOnce( |
| &AmbientWeatherController::StartDownloadingWeatherConditionIcon, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void AmbientWeatherController::StartDownloadingWeatherConditionIcon( |
| const std::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( |
| &AmbientWeatherController::OnWeatherConditionIconDownloaded, |
| weak_factory_.GetWeakPtr(), weather_info->temp_f.value(), |
| weather_info->show_celsius)); |
| } |
| |
| void AmbientWeatherController::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; |
| |
| weather_model_->UpdateWeatherInfo(icon, temp_f, show_celsius); |
| } |
| |
| bool AmbientWeatherController::IsGeolocationUsageAllowed() { |
| return location_permission_provider_->IsGeolocationUsageAllowedForSystem(); |
| } |
| |
| void AmbientWeatherController::ClearAmbientWeatherModel() { |
| weather_model_->UpdateWeatherInfo(gfx::ImageSkia(), 0.0f, true); |
| } |
| |
| void AmbientWeatherController::OnScopedRefresherDestroyed() { |
| --num_active_scoped_refreshers_; |
| CHECK_GE(num_active_scoped_refreshers_, 0); |
| if (num_active_scoped_refreshers_ == 0) { |
| // This may not have user-visible effects, but refreshing the weather when |
| // there's no UI using it is wasting network/server resources. |
| weather_refresh_timer_.Stop(); |
| } |
| } |
| |
| } // namespace ash |