blob: 99e917a4fb7c0471b4afe37553542dab479f4893 [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/check_op.h"
#include "build/chromeos_buildflags.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/sync_service_factory.h"
#include "chrome/browser/sync/sync_ui_util.h"
#include "chrome/browser/ui/browser_list.h"
#include "components/signin/public/identity_manager/consent_level.h"
#include "components/sync/driver/sync_service.h"
#include "ui/base/resource/resource_bundle.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/constants/ash_features.h"
#endif
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) {
return GetProfileAttributesStorage().GetProfileAttributesWithPath(
profile->GetPath());
}
// 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 kGuestSession and not kIncognitoProfile).
gfx::Image GetAvatarImage(Profile* profile,
const gfx::Image& user_identity_image,
int preferred_size) {
ProfileAttributesEntry* entry = GetProfileAttributesEntry(profile);
if (!entry) { // This can happen if the user deletes the current profile.
return ui::ResourceBundle::GetSharedInstance().GetImageNamed(
profiles::GetPlaceholderAvatarIconResourceID());
}
// 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(signin::ConsentLevel::kSync) &&
entry->IsUsingDefaultAvatar()) {
return user_identity_image;
}
return entry->GetAvatarIcon(preferred_size);
}
} // namespace
AvatarToolbarButtonDelegate::AvatarToolbarButtonDelegate(
AvatarToolbarButton* button,
Profile* profile)
: avatar_toolbar_button_(button),
profile_(profile),
last_avatar_error_(::GetAvatarSyncErrorType(profile)) {
profile_observation_.Observe(&GetProfileAttributesStorage());
if (auto* sync_service = SyncServiceFactory::GetForProfile(profile_))
sync_service_observation_.Observe(sync_service);
AvatarToolbarButton::State state = GetState();
if (state == AvatarToolbarButton::State::kIncognitoProfile ||
state == AvatarToolbarButton::State::kGuestSession) {
BrowserList::AddObserver(this);
} else {
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile_);
identity_manager_observation_.Observe(identity_manager);
if (identity_manager->AreRefreshTokensLoaded())
OnRefreshTokensLoaded();
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (!base::FeatureList::IsEnabled(chromeos::features::kAvatarToolbarButton)) {
// 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.
avatar_toolbar_button_->SetEnabled(
state == AvatarToolbarButton::State::kIncognitoProfile);
}
#endif // !BUILDFLAG(IS_CHROMEOS_ASH)
}
AvatarToolbarButtonDelegate::~AvatarToolbarButtonDelegate() {
BrowserList::RemoveObserver(this);
}
std::u16string AvatarToolbarButtonDelegate::GetProfileName() const {
DCHECK_NE(GetState(), AvatarToolbarButton::State::kIncognitoProfile);
return profiles::GetAvatarNameForProfile(profile_->GetPath());
}
std::u16string 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->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
return identity_manager
->FindExtendedAccountInfoByAccountId(
identity_manager->GetPrimaryAccountId(
signin::ConsentLevel::kSignin))
.account_image;
}
return gfx::Image();
}
gfx::Image AvatarToolbarButtonDelegate::GetProfileAvatarImage(
gfx::Image gaia_account_image,
int preferred_size) const {
return GetAvatarImage(profile_, gaia_account_image, preferred_size);
}
int AvatarToolbarButtonDelegate::GetWindowCount() const {
if (profile_->IsGuestSession())
return BrowserList::GetGuestBrowserCount();
DCHECK(profile_->IsOffTheRecord());
return BrowserList::GetOffTheRecordBrowsersActiveForProfile(profile_);
}
AvatarToolbarButton::State AvatarToolbarButtonDelegate::GetState() const {
if (profile_->IsGuestSession())
return AvatarToolbarButton::State::kGuestSession;
// Return |kIncognitoProfile| state for all OffTheRecord profile types except
// guest mode.
if (profile_->IsOffTheRecord())
return AvatarToolbarButton::State::kIncognitoProfile;
if (identity_animation_state_ == IdentityAnimationState::kShowing)
return AvatarToolbarButton::State::kAnimatedUserIdentity;
// Show any existing sync errors (sync-the-feature or sync-the-transport).
// |last_avatar_error_| should be checked here rather than
// ::GetAvatarSyncErrorType(), so the result agrees with
// AvatarToolbarButtonDelegate::GetAvatarSyncErrorType().
if (!last_avatar_error_)
return AvatarToolbarButton::State::kNormal;
if (last_avatar_error_ == AvatarSyncErrorType::kAuthError &&
AccountConsistencyModeManager::IsDiceEnabledForProfile(profile_)) {
return AvatarToolbarButton::State::kSyncPaused;
}
return AvatarToolbarButton::State::kSyncError;
}
absl::optional<AvatarSyncErrorType>
AvatarToolbarButtonDelegate::GetAvatarSyncErrorType() const {
return last_avatar_error_;
}
bool AvatarToolbarButtonDelegate::IsSyncFeatureEnabled() const {
return IdentityManagerFactory::GetForProfile(profile_)->HasPrimaryAccount(
signin::ConsentLevel::kSync);
}
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::MaybeShowIdentityAnimation(
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
if (!IdentityManagerFactory::GetForProfile(profile_)->HasPrimaryAccount(
signin::ConsentLevel::kSignin)) {
identity_animation_state_ = IdentityAnimationState::kNotShowing;
return;
}
ShowIdentityAnimation();
}
void AvatarToolbarButtonDelegate::SetHasInProductHelpPromo(bool has_promo) {
if (has_in_product_help_promo_ == has_promo)
return;
has_in_product_help_promo_ = has_promo;
// Trigger a new animation, even if the IPH is being removed. This keeps the
// pill open a little more and avoids jankiness caused by the two animations
// (IPH and identity pill) happening concurrently.
// See https://crbug.com/1198907
ShowIdentityAnimation();
}
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 std::u16string& 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 std::u16string& old_profile_name) {
avatar_toolbar_button_->UpdateText();
}
void AvatarToolbarButtonDelegate::OnPrimaryAccountChanged(
const signin::PrimaryAccountChangeEvent& event) {
if (event.GetEventTypeFor(signin::ConsentLevel::kSignin) !=
signin::PrimaryAccountChangeEvent::Type::kSet) {
return;
}
OnUserIdentityChanged();
}
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_)->GetPrimaryAccountInfo(
signin::ConsentLevel::kSignin);
if (account.IsEmpty())
return;
OnUserIdentityChanged();
}
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::OnStateChanged(syncer::SyncService*) {
const absl::optional<AvatarSyncErrorType> error =
::GetAvatarSyncErrorType(profile_);
if (last_avatar_error_ == error)
return;
last_avatar_error_ = error;
avatar_toolbar_button_->UpdateIcon();
avatar_toolbar_button_->UpdateText();
}
void AvatarToolbarButtonDelegate::OnUserIdentityChanged() {
signin_ui_util::RecordAnimatedIdentityTriggered(profile_);
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() {
--identity_animation_timeout_count_;
// If the count is > 0, there's at least one more pending
// OnIdentityAnimationTimeout() that will hide it after the proper delay.
if (identity_animation_timeout_count_ > 0)
return;
DCHECK_EQ(identity_animation_state_, IdentityAnimationState::kShowing);
MaybeHideIdentityAnimation();
}
void AvatarToolbarButtonDelegate::MaybeHideIdentityAnimation() {
// No-op if not showing or if the timeout hasn't passed, yet.
if (identity_animation_state_ != IdentityAnimationState::kShowing ||
identity_animation_timeout_count_ > 0) {
return;
}
// Keep identity visible if this button is in use (hovered or has focus) or
// has an associated In-Product-Help promo. 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() || has_in_product_help_promo_) {
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();
}
void AvatarToolbarButtonDelegate::ShowIdentityAnimation() {
identity_animation_state_ = IdentityAnimationState::kShowing;
avatar_toolbar_button_->UpdateText();
// Hide the pill after a while.
++identity_animation_timeout_count_;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AvatarToolbarButtonDelegate::OnIdentityAnimationTimeout,
weak_ptr_factory_.GetWeakPtr()),
kIdentityAnimationDuration);
}