// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/ambient/ambient_client_impl.h"
#include <string>
#include <utility>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/ambient/ambient_prefs.h"
#include "ash/public/cpp/image_downloader.h"
#include "base/check.h"
#include "base/functional/callback.h"
#include "chrome/browser/ash/login/demo_mode/demo_session.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/common/channel_info.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/access_token_fetcher.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/scope_set.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "components/version_info/channel.h"
#include "content/public/browser/device_service.h"
#include "content/public/browser/storage_partition.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace {
constexpr char kPhotosOAuthScope[] = "https://www.googleapis.com/auth/photos";
constexpr char kBackdropOAuthScope[] =
"https://www.googleapis.com/auth/cast.backdrop";
const user_manager::User* GetActiveUser() {
return user_manager::UserManager::Get()->GetActiveUser();
}
const user_manager::User* GetPrimaryUser() {
return user_manager::UserManager::Get()->GetPrimaryUser();
}
constexpr net::NetworkTrafficAnnotationTag kAmbientClientNetworkTag =
net::DefineNetworkTrafficAnnotation("ambient_client", R"(
semantics {
sender: "Ambient photo"
description:
"Get ambient photo from url to store limited number of photos in "
"the device cache. This is used to show the screensaver when the "
"user is idle. The url can be Backdrop service to provide pictures"
" from internal gallery, weather/time photos served by Google, or "
"user selected album from Google photos."
trigger:
"Triggered by a photo refresh timer, after the device has been "
"idle and the battery is charging."
data: "None."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting:
"This feature is off by default and can be overridden by users."
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."
})");
Profile* GetProfileForActiveUser() {
const user_manager::User* const active_user = GetActiveUser();
DCHECK(active_user);
return ash::ProfileHelper::Get()->GetProfileByUser(active_user);
}
bool IsPrimaryUser() {
return GetActiveUser() == GetPrimaryUser();
}
bool HasPrimaryAccount(const Profile* profile) {
auto* identity_manager =
IdentityManagerFactory::GetForProfileIfExists(profile);
if (!identity_manager)
return false;
return identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSignin);
}
bool IsEmailDomainSupported(const user_manager::User* user) {
const std::string email = user->GetAccountId().GetUserEmail();
DCHECK(!email.empty());
constexpr char kGmailDomain[] = "gmail.com";
constexpr char kGooglemailDomain[] = "googlemail.com";
return (gaia::ExtractDomainName(email) == kGmailDomain ||
gaia::ExtractDomainName(email) == kGooglemailDomain ||
gaia::IsGoogleInternalAccountEmail(email));
}
} // namespace
AmbientClientImpl::AmbientClientImpl() = default;
AmbientClientImpl::~AmbientClientImpl() = default;
bool AmbientClientImpl::IsAmbientModeAllowed() {
if (is_allowed_for_testing_.has_value()) {
return is_allowed_for_testing_.value();
}
if (ash::DemoSession::IsDeviceInDemoMode())
return false;
const user_manager::User* const active_user = GetActiveUser();
if (!active_user || !active_user->HasGaiaAccount())
return false;
if (!IsPrimaryUser())
return false;
// When this check is removed to start supporting enterprise users,
// please update kAmbientClientNetworkTag and network annotation tags
// in ash/ambient package to reflect that this is an enterprise feature.
if (!IsEmailDomainSupported(active_user))
return false;
auto* profile = GetProfileForActiveUser();
if (!profile)
return false;
// Primary account might be missing during unittests.
if (!HasPrimaryAccount(profile))
return false;
if (profile->IsOffTheRecord())
return false;
return true;
}
void AmbientClientImpl::SetAmbientModeAllowedForTesting(bool allowed) {
is_allowed_for_testing_ = allowed;
}
void AmbientClientImpl::RequestAccessToken(GetAccessTokenCallback callback) {
auto* profile = GetProfileForActiveUser();
DCHECK(profile);
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
DCHECK(identity_manager);
CoreAccountInfo account_info =
identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
const signin::ScopeSet scopes{kPhotosOAuthScope, kBackdropOAuthScope};
auto fetcher_id = base::UnguessableToken::Create();
auto access_token_fetcher =
identity_manager->CreateAccessTokenFetcherForAccount(
account_info.account_id,
/*oauth_consumer_name=*/"ChromeOS_AmbientMode", scopes,
base::BindOnce(&AmbientClientImpl::OnGetAccessToken,
weak_factory_.GetWeakPtr(), std::move(callback),
fetcher_id, account_info.gaia),
signin::AccessTokenFetcher::Mode::kImmediate);
token_fetchers_.insert({fetcher_id, std::move(access_token_fetcher)});
}
void AmbientClientImpl::DownloadImage(
const std::string& url,
ash::ImageDownloader::DownloadCallback callback) {
RequestAccessToken(base::BindOnce(
[](const std::string& url,
ash::ImageDownloader::DownloadCallback callback,
const std::string& gaia_id, const std::string& access_token,
const base::Time& expiration_time) {
if (access_token.empty()) {
std::move(callback).Run({});
return;
}
const auto* user = GetActiveUser();
DCHECK(user);
net::HttpRequestHeaders headers;
headers.SetHeader("Authorization", "Bearer " + access_token);
ash::ImageDownloader::Get()->Download(
GURL(url), kAmbientClientNetworkTag, user->GetAccountId(), headers,
std::move(callback));
},
url, std::move(callback)));
}
scoped_refptr<network::SharedURLLoaderFactory>
AmbientClientImpl::GetURLLoaderFactory() {
auto* profile = GetProfileForActiveUser();
DCHECK(profile);
return profile->GetURLLoaderFactory();
}
scoped_refptr<network::SharedURLLoaderFactory>
AmbientClientImpl::GetSigninURLLoaderFactory() {
content::BrowserContext* browser_context =
ash::BrowserContextHelper::Get()->GetSigninBrowserContext();
CHECK(browser_context);
Profile* profile = Profile::FromBrowserContext(browser_context);
CHECK(profile);
return profile->GetURLLoaderFactory();
}
void AmbientClientImpl::RequestWakeLockProvider(
mojo::PendingReceiver<device::mojom::WakeLockProvider> receiver) {
content::GetDeviceService().BindWakeLockProvider(std::move(receiver));
}
bool AmbientClientImpl::ShouldUseProdServer() {
if (ash::features::IsAmbientModeDevUseProdEnabled())
return true;
auto channel = chrome::GetChannel();
return channel == version_info::Channel::STABLE ||
channel == version_info::Channel::BETA;
}
void AmbientClientImpl::OnGetAccessToken(
GetAccessTokenCallback callback,
base::UnguessableToken fetcher_id,
const std::string& gaia_id,
GoogleServiceAuthError error,
signin::AccessTokenInfo access_token_info) {
if (error.state() == GoogleServiceAuthError::NONE) {
std::move(callback).Run(gaia_id, access_token_info.token,
access_token_info.expiration_time);
} else {
LOG(ERROR) << "Failed to retrieve token, error: " << error.ToString();
std::move(callback).Run(/*gaia_id=*/std::string(),
/*access_token=*/std::string(),
/*expiration_time=*/base::Time::Now());
}
token_fetchers_.erase(fetcher_id);
}