blob: 23d817389735b6ba316bd5e0d1d0e7381dac9291 [file] [log] [blame]
// 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/ui/views/profiles/avatar_toolbar_button_delegate.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_avatar_icon_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/signin/account_consistency_mode_manager.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/signin/signin_ui_util.h"
#include "chrome/browser/sync/profile_sync_service_factory.h"
#include "chrome/browser/sync/sync_ui_util.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/ui_features.h"
namespace {
constexpr base::TimeDelta kIdentityAnimationDuration =
base::TimeDelta::FromSeconds(3);
constexpr base::TimeDelta kAvatarHighlightAnimationDuration =
base::TimeDelta::FromSeconds(2);
ProfileAttributesStorage& GetProfileAttributesStorage() {
return g_browser_process->profile_manager()->GetProfileAttributesStorage();
}
ProfileAttributesEntry* GetProfileAttributesEntry(Profile* profile) {
ProfileAttributesEntry* entry;
if (!GetProfileAttributesStorage().GetProfileAttributesWithPath(
profile->GetPath(), &entry)) {
return nullptr;
}
return entry;
}
bool IsGenericProfile(const ProfileAttributesEntry& entry) {
// If the profile is using the placeholder avatar, fall back on the generic
// profile's themeable vector icon instead.
if (entry.GetAvatarIconIndex() == profiles::GetPlaceholderAvatarIndex())
return true;
return entry.GetAvatarIconIndex() == 0 &&
GetProfileAttributesStorage().GetNumberOfProfiles() == 1;
}
// Returns the avatar image for the current profile. May be called only in
// "normal" states where the user is guaranteed to have an avatar image (i.e.
// not kGenericProfile, not kGuestSession and not kIncognitoProfile).
const gfx::Image& GetAvatarImage(Profile* profile,
const gfx::Image& user_identity_image) {
ProfileAttributesEntry* entry = GetProfileAttributesEntry(profile);
DCHECK(entry);
// TODO(crbug.com/1012179): it should suffice to call entry->GetAvatarIcon().
// For this to work well, this class needs to observe ProfileAttributesStorage
// instead of (or on top of) IdentityManager. Only then we can rely on |entry|
// being up to date (as the storage also observes IdentityManager so there's
// no guarantee on the order of notifications).
if (entry->IsUsingGAIAPicture() && entry->GetGAIAPicture())
return *entry->GetGAIAPicture();
// Show |user_identity_image| when the following conditions are satisfied:
// - the user is migrated to Dice
// - the user isn't syncing
// - the profile icon wasn't explicitly changed
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
if (!user_identity_image.IsEmpty() &&
AccountConsistencyModeManager::IsDiceEnabledForProfile(profile) &&
!identity_manager->HasPrimaryAccount() && entry->IsUsingDefaultAvatar()) {
return user_identity_image;
}
return entry->GetAvatarIcon();
}
} // namespace
AvatarToolbarButtonDelegate::AvatarToolbarButtonDelegate() = default;
AvatarToolbarButtonDelegate::~AvatarToolbarButtonDelegate() {
BrowserList::RemoveObserver(this);
}
void AvatarToolbarButtonDelegate::Init(AvatarToolbarButton* button,
Profile* profile) {
avatar_toolbar_button_ = button;
profile_ = profile;
#if !defined(OS_CHROMEOS)
error_controller_ =
std::make_unique<AvatarButtonErrorController>(this, profile_);
#endif // !defined(OS_CHROMEOS)
profile_observer_.Add(&GetProfileAttributesStorage());
AvatarToolbarButton::State state = GetState();
if (state == AvatarToolbarButton::State::kIncognitoProfile) {
BrowserList::AddObserver(this);
} else if (state != AvatarToolbarButton::State::kGuestSession) {
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile_);
identity_manager_observer_.Add(identity_manager);
if (identity_manager->AreRefreshTokensLoaded())
OnRefreshTokensLoaded();
}
#if defined(OS_CHROMEOS)
// On CrOS this button should only show as badging for Incognito and Guest
// sessions. It's only enabled for Incognito where a menu is available for
// closing all Incognito windows.
DCHECK(state == AvatarToolbarButton::State::kIncognitoProfile ||
state == AvatarToolbarButton::State::kGuestSession);
avatar_toolbar_button_->SetEnabled(
state == AvatarToolbarButton::State::kIncognitoProfile);
#endif // !defined(OS_CHROMEOS)
}
base::string16 AvatarToolbarButtonDelegate::GetProfileName() const {
DCHECK_NE(GetState(), AvatarToolbarButton::State::kIncognitoProfile);
return profiles::GetAvatarNameForProfile(profile_->GetPath());
}
base::string16 AvatarToolbarButtonDelegate::GetShortProfileName() const {
return signin_ui_util::GetShortProfileIdentityToDisplay(
*GetProfileAttributesEntry(profile_), profile_);
}
gfx::Image AvatarToolbarButtonDelegate::GetGaiaAccountImage() const {
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile_);
if (identity_manager && identity_manager->HasUnconsentedPrimaryAccount()) {
base::Optional<AccountInfo> account_info =
identity_manager
->FindExtendedAccountInfoForAccountWithRefreshTokenByAccountId(
identity_manager->GetUnconsentedPrimaryAccountId());
if (account_info.has_value())
return account_info->account_image;
}
return gfx::Image();
}
gfx::Image AvatarToolbarButtonDelegate::GetProfileAvatarImage(
gfx::Image gaia_account_image) const {
return GetAvatarImage(profile_, gaia_account_image);
}
int AvatarToolbarButtonDelegate::GetIncognitoWindowsCount() const {
return BrowserList::GetIncognitoSessionsActiveForProfile(profile_);
}
AvatarToolbarButton::State AvatarToolbarButtonDelegate::GetState() const {
if (profile_->IsIncognitoProfile())
return AvatarToolbarButton::State::kIncognitoProfile;
if (profile_->IsGuestSession())
return AvatarToolbarButton::State::kGuestSession;
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile_);
ProfileAttributesEntry* entry = GetProfileAttributesEntry(profile_);
if (!entry || // This can happen if the user deletes the current profile.
(!identity_manager->HasUnconsentedPrimaryAccount() &&
IsGenericProfile(*entry))) {
return AvatarToolbarButton::State::kGenericProfile;
}
if (identity_animation_state_ ==
IdentityAnimationState::kShowingUntilTimeout ||
identity_animation_state_ ==
IdentityAnimationState::kShowingUntilNoLongerInUse) {
return AvatarToolbarButton::State::kAnimatedUserIdentity;
}
#if !defined(OS_CHROMEOS)
if (identity_manager->HasPrimaryAccount() &&
ProfileSyncServiceFactory::IsSyncAllowed(profile_) &&
error_controller_->HasAvatarError()) {
// When DICE is enabled and the error is an auth error, the sync-paused
// icon is shown.
int unused;
const sync_ui_util::AvatarSyncErrorType error =
sync_ui_util::GetMessagesForAvatarSyncError(profile_, &unused, &unused);
if (AccountConsistencyModeManager::IsDiceEnabledForProfile(profile_) &&
error == sync_ui_util::AUTH_ERROR) {
return AvatarToolbarButton::State::kSyncPaused;
}
if (error == sync_ui_util::TRUSTED_VAULT_KEY_MISSING_FOR_PASSWORDS_ERROR) {
return AvatarToolbarButton::State::kPasswordsOnlySyncError;
}
return AvatarToolbarButton::State::kSyncError;
}
#endif // !defined(OS_CHROMEOS)
return AvatarToolbarButton::State::kNormal;
}
void AvatarToolbarButtonDelegate::ShowHighlightAnimation() {
signin_ui_util::RecordAvatarIconHighlighted(profile_);
highlight_animation_visible_ = true;
DCHECK_NE(GetState(), AvatarToolbarButton::State::kIncognitoProfile);
DCHECK_NE(GetState(), AvatarToolbarButton::State::kGuestSession);
avatar_toolbar_button_->UpdateText();
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AvatarToolbarButtonDelegate::HideHighlightAnimation,
weak_ptr_factory_.GetWeakPtr()),
kAvatarHighlightAnimationDuration);
}
bool AvatarToolbarButtonDelegate::IsHighlightAnimationVisible() const {
return highlight_animation_visible_;
}
void AvatarToolbarButtonDelegate::ShowIdentityAnimation(
const gfx::Image& gaia_account_image) {
// TODO(crbug.com/990286): Get rid of this logic completely when we cache the
// Google account image in the profile cache and thus it is always available.
if (identity_animation_state_ != IdentityAnimationState::kWaitingForImage ||
gaia_account_image.IsEmpty()) {
return;
}
// Check that the user is still signed in. See https://crbug.com/1025674
CoreAccountInfo user_identity =
IdentityManagerFactory::GetForProfile(profile_)
->GetUnconsentedPrimaryAccountInfo();
if (user_identity.IsEmpty()) {
identity_animation_state_ = IdentityAnimationState::kNotShowing;
return;
}
identity_animation_state_ = IdentityAnimationState::kShowingUntilTimeout;
avatar_toolbar_button_->UpdateText();
// Hide the pill after a while.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AvatarToolbarButtonDelegate::OnIdentityAnimationTimeout,
weak_ptr_factory_.GetWeakPtr()),
kIdentityAnimationDuration);
}
void AvatarToolbarButtonDelegate::NotifyClick() {
MaybeHideIdentityAnimation();
}
void AvatarToolbarButtonDelegate::OnMouseExited() {
MaybeHideIdentityAnimation();
}
void AvatarToolbarButtonDelegate::OnBlur() {
MaybeHideIdentityAnimation();
}
void AvatarToolbarButtonDelegate::OnHighlightChanged() {
MaybeHideIdentityAnimation();
}
void AvatarToolbarButtonDelegate::OnBrowserAdded(Browser* browser) {
avatar_toolbar_button_->UpdateIcon();
avatar_toolbar_button_->UpdateText();
}
void AvatarToolbarButtonDelegate::OnBrowserRemoved(Browser* browser) {
avatar_toolbar_button_->UpdateIcon();
avatar_toolbar_button_->UpdateText();
}
void AvatarToolbarButtonDelegate::OnProfileAdded(
const base::FilePath& profile_path) {
// Adding any profile changes the profile count, we might go from showing a
// generic avatar button to profile pictures here. Update icon accordingly.
avatar_toolbar_button_->UpdateIcon();
}
void AvatarToolbarButtonDelegate::OnProfileWasRemoved(
const base::FilePath& profile_path,
const base::string16& profile_name) {
// Removing a profile changes the profile count, we might go from showing
// per-profile icons back to a generic avatar icon. Update icon accordingly.
avatar_toolbar_button_->UpdateIcon();
}
void AvatarToolbarButtonDelegate::OnProfileAvatarChanged(
const base::FilePath& profile_path) {
avatar_toolbar_button_->UpdateIcon();
}
void AvatarToolbarButtonDelegate::OnProfileHighResAvatarLoaded(
const base::FilePath& profile_path) {
avatar_toolbar_button_->UpdateIcon();
}
void AvatarToolbarButtonDelegate::OnProfileNameChanged(
const base::FilePath& profile_path,
const base::string16& old_profile_name) {
avatar_toolbar_button_->UpdateText();
}
void AvatarToolbarButtonDelegate::OnUnconsentedPrimaryAccountChanged(
const CoreAccountInfo& unconsented_primary_account_info) {
if (unconsented_primary_account_info.IsEmpty())
return;
OnUserIdentityChanged(features::kAnimatedAvatarButtonOnSignIn);
}
void AvatarToolbarButtonDelegate::OnRefreshTokensLoaded() {
if (refresh_tokens_loaded_) {
// This is possible, if |AvatarToolbarButtonDelegate::Init| is called within
// the loop in |IdentityManager::OnRefreshTokensLoaded()| to notify
// observers. In that case, |OnRefreshTokensLoaded| will be called twice,
// once from |AvatarToolbarButtonDelegate::Init| and another time from the
// |IdentityManager|. This happens for new signed in profiles.
// See https://crbug.com/1035480
return;
}
refresh_tokens_loaded_ = true;
if (!signin_ui_util::ShouldShowAnimatedIdentityOnOpeningWindow(
GetProfileAttributesStorage(), profile_)) {
return;
}
CoreAccountInfo account = IdentityManagerFactory::GetForProfile(profile_)
->GetUnconsentedPrimaryAccountInfo();
if (account.IsEmpty())
return;
OnUserIdentityChanged(features::kAnimatedAvatarButtonOnOpeningWindow);
}
void AvatarToolbarButtonDelegate::OnAccountsInCookieUpdated(
const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
const GoogleServiceAuthError& error) {
avatar_toolbar_button_->UpdateIcon();
}
void AvatarToolbarButtonDelegate::OnExtendedAccountInfoUpdated(
const AccountInfo& info) {
avatar_toolbar_button_->UpdateIcon();
}
void AvatarToolbarButtonDelegate::OnExtendedAccountInfoRemoved(
const AccountInfo& info) {
avatar_toolbar_button_->UpdateIcon();
}
void AvatarToolbarButtonDelegate::OnAvatarErrorChanged() {
avatar_toolbar_button_->UpdateIcon();
avatar_toolbar_button_->UpdateText();
}
void AvatarToolbarButtonDelegate::OnUserIdentityChanged(
const base::Feature& triggering_feature) {
// Record the last time the animated identity was set. This is done even if
// the feature is disabled, to allow comparing metrics between experimental
// groups.
signin_ui_util::RecordAnimatedIdentityTriggered(profile_);
if (!base::FeatureList::IsEnabled(triggering_feature) ||
!base::FeatureList::IsEnabled(features::kAnimatedAvatarButton)) {
return;
}
identity_animation_state_ = IdentityAnimationState::kWaitingForImage;
// If we already have a gaia image, the pill will be immediately displayed by
// UpdateIcon().
avatar_toolbar_button_->UpdateIcon();
}
void AvatarToolbarButtonDelegate::OnIdentityAnimationTimeout() {
DCHECK_EQ(identity_animation_state_,
IdentityAnimationState::kShowingUntilTimeout);
identity_animation_state_ =
IdentityAnimationState::kShowingUntilNoLongerInUse;
MaybeHideIdentityAnimation();
}
void AvatarToolbarButtonDelegate::MaybeHideIdentityAnimation() {
// No-op if not showing or if the timeout hasn't passed, yet.
if (identity_animation_state_ !=
IdentityAnimationState::kShowingUntilNoLongerInUse) {
return;
}
// Keep identity visible if this button is in use (hovered or has focus) or
// if its parent is in use (which makes it highlighted). We should not move
// things around when the user wants to click on |this| or another button in
// the parent.
if (avatar_toolbar_button_->IsMouseHovered() ||
avatar_toolbar_button_->HasFocus()) {
return;
}
identity_animation_state_ = IdentityAnimationState::kNotShowing;
// Update the text to the pre-shown state. This also makes sure that we now
// reflect changes that happened while the identity pill was shown.
avatar_toolbar_button_->UpdateText();
}
void AvatarToolbarButtonDelegate::HideHighlightAnimation() {
DCHECK_NE(GetState(), AvatarToolbarButton::State::kIncognitoProfile);
DCHECK_NE(GetState(), AvatarToolbarButton::State::kGuestSession);
highlight_animation_visible_ = false;
avatar_toolbar_button_->UpdateText();
avatar_toolbar_button_->NotifyHighlightAnimationFinished();
}