blob: 097656f8d11c80866baee3ce5a1092df8345a0aa [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/signin/model/system_account_updater.h"
#import "base/barrier_callback.h"
#import "base/check_deref.h"
#import "base/task/bind_post_task.h"
#import "base/task/single_thread_task_runner.h"
#import "base/task/task_traits.h"
#import "base/task/thread_pool.h"
#import "base/threading/scoped_blocking_call.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/signin/model/constants.h"
#import "ios/chrome/browser/signin/model/resized_avatar_cache.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/chrome/browser/widget_kit/model/features.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/ui/util/image_util.h"
#if BUILDFLAG(ENABLE_WIDGETS_FOR_MIM)
#import "base/check_is_test.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/features.h"
#import "ios/chrome/browser/widget_kit/model/model_swift.h" // nogncheck
#endif
namespace {
// Updates all widget timelines with the updated data.
#if BUILDFLAG(ENABLE_WIDGETS_FOR_MIM)
void ReloadAllTimelines() {
[WidgetTimelinesUpdater reloadAllTimelines];
}
#endif
// Stores information about a SystemIdentity.
class SystemIdentityInfo {
public:
SystemIdentityInfo(id<SystemIdentity> identity, UIImage* cached_avatar)
: gaia_id_(identity.gaiaID),
full_name_(identity.userFullName ?: @""),
user_email_(identity.userEmail),
cached_avatar_(cached_avatar) {}
~SystemIdentityInfo() = default;
NSString* gaia_id() const { return gaia_id_; }
NSString* full_name() const { return full_name_; }
NSString* user_email() const { return user_email_; }
UIImage* cached_avatar() const { return cached_avatar_; }
NSDictionary* AsDictionary() const {
NSMutableDictionary* dictionary = [[NSMutableDictionary alloc] init];
[dictionary setObject:user_email_ forKey:app_group::kEmail];
[dictionary setObject:full_name_ forKey:app_group::kFullName];
return dictionary;
}
private:
NSString* gaia_id_;
NSString* full_name_;
NSString* user_email_;
UIImage* cached_avatar_;
};
// Stores information about a SystemIdentity in a serializable format.
class SystemIdentityInfoData {
public:
explicit SystemIdentityInfoData(const SystemIdentityInfo& info)
: gaia_id_(info.gaia_id()), avatar_data_(nil) {
UIImage* image = info.cached_avatar();
if (image) {
const CGSize size = CGSizeMake(32.0, 32.0);
if (!CGSizeEqualToSize(image.size, size)) {
image = ResizeImage(image, size, ProjectionMode::kAspectFit);
}
avatar_data_ = UIImagePNGRepresentation(image);
}
}
~SystemIdentityInfoData() = default;
NSString* gaia_id() const { return gaia_id_; }
NSData* avatar_data() const { return avatar_data_; }
NSURL* avatar_path(NSURL* directory) const {
return [directory
URLByAppendingPathComponent:[gaia_id_
stringByAppendingPathExtension:@"png"]];
}
private:
NSString* gaia_id_;
NSData* avatar_data_;
};
// Converts a SystemIdentityInfo into SystemIdentityInfoData.
SystemIdentityInfoData ConvertSystemIdentityInfo(SystemIdentityInfo info) {
return SystemIdentityInfoData(info);
}
// A list of SystemIdentityInfo.
using SystemIdentityInfoList = std::vector<SystemIdentityInfo>;
using SystemIdentityInfoDataList = std::vector<SystemIdentityInfoData>;
// Used as iterator for SystemIdentityManager.
class SystemIdentityInfoCollector {
public:
using IteratorResult = SystemIdentityManager::IteratorResult;
SystemIdentityInfoCollector(SystemIdentityInfoList& list,
SystemIdentityManager& manager)
: list_(list), manager_(manager) {}
~SystemIdentityInfoCollector() = default;
IteratorResult AddIdentity(id<SystemIdentity> identity) {
list_->push_back(SystemIdentityInfo(
identity, manager_->GetCachedAvatarForIdentity(identity)));
return IteratorResult::kContinueIteration;
}
private:
raw_ref<SystemIdentityInfoList> list_;
raw_ref<SystemIdentityManager> manager_;
};
// Collects the information about the SystemIdentity available on device.
SystemIdentityInfoList CollectSystemIdentityInfo(
SystemIdentityManager& manager) {
SystemIdentityInfoList result;
SystemIdentityInfoCollector iterator(result, manager);
// Using base::Unretained(...) is safe since IterateOverIdentities(...)
// will synchronously call the callback during its execution and will
// not let it escape.
manager.IterateOverIdentities(base::BindRepeating(
&SystemIdentityInfoCollector::AddIdentity, base::Unretained(&iterator)));
return result;
}
// Writes the avatars' image to disk, deleting obsolete avatars.
void WriteAvatars(const SystemIdentityInfoDataList& list) {
base::ScopedBlockingCall may_block(FROM_HERE, base::BlockingType::MAY_BLOCK);
NSURL* avatar_folder = app_group::WidgetsAvatarFolder();
if (!avatar_folder) {
return;
}
NSFileManager* manager = [NSFileManager defaultManager];
NSMutableSet<NSURL*>* avatars = [[NSMutableSet alloc] init];
for (const SystemIdentityInfoData& info : list) {
NSData* data = info.avatar_data();
if (data) {
NSURL* path = info.avatar_path(avatar_folder);
if ([data writeToURL:path atomically:YES]) {
[avatars addObject:path];
}
}
}
const auto options = NSDirectoryEnumerationSkipsSubdirectoryDescendants;
for (NSURL* url in [manager contentsOfDirectoryAtURL:avatar_folder
includingPropertiesForKeys:nil
options:options
error:nil]) {
if (![avatars containsObject:url]) {
[manager removeItemAtURL:url error:nil];
}
}
}
// Writes the avatar's image to disk, or delete it if the data is missing.
void WriteAvatar(const SystemIdentityInfoData& info) {
base::ScopedBlockingCall may_block(FROM_HERE, base::BlockingType::MAY_BLOCK);
NSURL* avatar_folder = app_group::WidgetsAvatarFolder();
if (!avatar_folder) {
return;
}
NSURL* path = info.avatar_path(avatar_folder);
if (NSData* data = info.avatar_data()) {
[data writeToURL:path atomically:YES];
} else {
NSFileManager* manager = [NSFileManager defaultManager];
[manager removeItemAtURL:path error:nil];
}
}
// Removes items for unknown accounts from `defaults`.
void RemoveUnknownAccounts(NSUserDefaults* defaults,
NSDictionary* accounts,
NSString* key) {
NSDictionary* value = [defaults objectForKey:key];
if (!value) {
return;
}
NSMutableDictionary* copy = [value mutableCopy];
for (NSString* gaia_id in value) {
if ([accounts objectForKey:gaia_id] ||
[gaia_id isEqualToString:app_group::kDefault] ||
[gaia_id isEqualToString:app_group::kNoAccount]) {
continue;
}
[copy removeObjectForKey:gaia_id];
}
[defaults setObject:copy forKey:key];
}
} // namespace
SystemAccountUpdater::SystemAccountUpdater(
SystemIdentityManager* system_identity_manager)
: system_identity_manager_(CHECK_DEREF(system_identity_manager)),
task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
system_identity_manager_observation_.Observe(&*system_identity_manager_);
HandleMigrationIfNeeded();
}
SystemAccountUpdater::~SystemAccountUpdater() = default;
void SystemAccountUpdater::OnIdentityListChanged() {
UpdateLoadedAccounts();
}
void SystemAccountUpdater::OnIdentityUpdated(id<SystemIdentity> identity) {
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&WriteAvatar,
ConvertSystemIdentityInfo(SystemIdentityInfo(
identity, system_identity_manager_->GetCachedAvatarForIdentity(
identity)))));
}
#pragma mark - Private
void SystemAccountUpdater::UpdateLoadedAccounts() {
SystemIdentityInfoList list =
CollectSystemIdentityInfo(*system_identity_manager_);
auto callback = base::BarrierCallback<SystemIdentityInfoData>(
list.size(),
base::BindPostTask(task_runner_, base::BindOnce(&WriteAvatars)));
NSMutableDictionary* accounts = [[NSMutableDictionary alloc] init];
for (auto& info : list) {
[accounts setObject:info.AsDictionary() forKey:info.gaia_id()];
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&ConvertSystemIdentityInfo, std::move(info))
.Then(callback));
}
NSUserDefaults* defaults = app_group::GetGroupUserDefaults();
[defaults setObject:accounts forKey:app_group::kAccountsOnDevice];
RemoveUnknownAccounts(defaults, accounts,
app_group::kSuggestedItemsForMultiprofile);
RemoveUnknownAccounts(
defaults, accounts,
app_group::kSuggestedItemsLastModificationDateForMultiprofile);
}
void SystemAccountUpdater::HandleMigrationIfNeeded() {
// Perform migration only if the flag is enabled.
#if BUILDFLAG(ENABLE_WIDGETS_FOR_MIM)
PrefService* local_state = GetApplicationContext()->GetLocalState();
if (!local_state) {
// Skip if there is no local_state. This can happen only in tests.
CHECK_IS_TEST();
return;
}
bool migration_performed =
local_state->GetBoolean(prefs::kMigrateWidgetsPrefs);
if (!migration_performed) {
// Only migrate prefs if a migration was never performed.
local_state->SetBoolean(prefs::kMigrateWidgetsPrefs, true);
UpdateLoadedAccounts();
} else if (!local_state->GetBoolean(prefs::kWidgetsForMultiProfile) &&
AreSeparateProfilesForManagedAccountsEnabled()) {
// Reload timelines if multi-profile was enabled since last build.
local_state->SetBoolean(prefs::kWidgetsForMultiProfile, true);
ReloadAllTimelines();
} else if (local_state->GetBoolean(prefs::kWidgetsForMultiProfile) &&
!AreSeparateProfilesForManagedAccountsEnabled()) {
// Reload timelines if multi-profile was disabled since last build.
local_state->SetBoolean(prefs::kWidgetsForMultiProfile, false);
ReloadAllTimelines();
}
#endif
}