// 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/constants/ash_pref_names.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 "components/prefs/pref_service.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));
}
PrefService* GetPrefService() {
return Shell::Get()->session_controller()->GetLastActiveUserPrefService();
}
} // 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) {
OnPermissionChanged();
}
void AmbientWeatherController::OnActiveUserPrefServiceChanged(
PrefService* pref_service) {
pref_change_registrar_.Reset();
pref_change_registrar_.Init(pref_service);
pref_change_registrar_.Add(
prefs::kContextualGoogleIntegrationsConfiguration,
base::BindRepeating(
&AmbientWeatherController::OnWeatherIntegrationPreferenceChanged,
base::Unretained(this)));
}
std::unique_ptr<AmbientWeatherController::ScopedRefresher>
AmbientWeatherController::CreateScopedRefresher() {
++num_active_scoped_refreshers_;
if (!weather_refresh_timer_.IsRunning() && IsGeolocationUsageAllowed() &&
!IsWeatherDisabledByPolicy()) {
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();
}
bool AmbientWeatherController::IsWeatherDisabledByPolicy() {
const auto* pref_service = GetPrefService();
return !pref_service ||
!base::Contains(pref_service->GetList(
prefs::kContextualGoogleIntegrationsConfiguration),
prefs::kWeatherIntegrationName);
}
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();
}
}
void AmbientWeatherController::OnWeatherIntegrationPreferenceChanged(
const std::string& pref_name) {
OnPermissionChanged();
}
void AmbientWeatherController::OnPermissionChanged() {
// When system permission is blocked, stop scheduling new requests and drop
// all pending requests. Also clears the weather model cache for privacy
// reasons.
if (!IsGeolocationUsageAllowed() || IsWeatherDisabledByPolicy()) {
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);
}
}
} // namespace ash