blob: d6c5080c62a7011f974e99ebd322d337720e85ef [file] [log] [blame]
// 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.
#import "ios/chrome/browser/push_notification/model/push_notification_delegate.h"
#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/check_is_test.h"
#import "base/files/file_path.h"
#import "base/functional/callback_helpers.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "base/timer/timer.h"
#import "base/values.h"
#import "components/prefs/pref_service.h"
#import "components/send_tab_to_self/features.h"
#import "components/sync_device_info/device_info_sync_service.h"
#import "google_apis/gaia/gaia_id.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/app_state_observer.h"
#import "ios/chrome/app/change_profile_commands.h"
#import "ios/chrome/app/change_profile_continuation.h"
#import "ios/chrome/app/profile/profile_init_stage.h"
#import "ios/chrome/app/profile/profile_state.h"
#import "ios/chrome/app/profile/profile_state_observer.h"
#import "ios/chrome/app/startup/app_launch_metrics.h"
#import "ios/chrome/browser/content_notification/model/content_notification_nau_configuration.h"
#import "ios/chrome/browser/content_notification/model/content_notification_service.h"
#import "ios/chrome/browser/content_notification/model/content_notification_service_factory.h"
#import "ios/chrome/browser/content_notification/model/content_notification_settings_action.h"
#import "ios/chrome/browser/content_notification/model/content_notification_util.h"
#import "ios/chrome/browser/push_notification/model/constants.h"
#import "ios/chrome/browser/push_notification/model/notification_metrics_recorder.h"
#import "ios/chrome/browser/push_notification/model/provisional_push_notification_service.h"
#import "ios/chrome/browser/push_notification/model/provisional_push_notification_service_factory.h"
#import "ios/chrome/browser/push_notification/model/push_notification_account_context_manager.h"
#import "ios/chrome/browser/push_notification/model/push_notification_client_id.h"
#import "ios/chrome/browser/push_notification/model/push_notification_client_manager.h"
#import "ios/chrome/browser/push_notification/model/push_notification_configuration.h"
#import "ios/chrome/browser/push_notification/model/push_notification_profile_service.h"
#import "ios/chrome/browser/push_notification/model/push_notification_profile_service_factory.h"
#import "ios/chrome/browser/push_notification/model/push_notification_service.h"
#import "ios/chrome/browser/push_notification/model/push_notification_settings_util.h"
#import "ios/chrome/browser/push_notification/model/push_notification_util.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state_observer.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_util.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider_interface.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/features.h"
#import "ios/chrome/browser/shared/model/profile/profile_attributes_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_attributes_storage_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_ios.h"
#import "ios/chrome/browser/shared/model/profile/scoped_profile_keep_alive_ios.h"
#import "ios/chrome/browser/shared/model/utils/first_run_util.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/account_profile_mapper.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/sync/model/device_info_sync_service_factory.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "third_party/search_engines_data/resources/definitions/prepopulated_engines.h"
namespace {
// The time range's expected min and max values for custom histograms.
constexpr base::TimeDelta kTimeRangeIncomingNotificationHistogramMin =
base::Milliseconds(1);
constexpr base::TimeDelta kTimeRangeIncomingNotificationHistogramMax =
base::Seconds(30);
// Number of buckets for the time range histograms.
constexpr int kTimeRangeHistogramBucketCount = 30;
// The histogram used to record a push notification's current lifecycle state on
// the device.
const char kLifecycleEventsHistogram[] = "IOS.PushNotification.LifecyleEvents";
// This enum is used to represent a point along the push notification's
// lifecycle.
enum class PushNotificationLifecycleEvent {
kNotificationReception,
kNotificationForegroundPresentation,
kNotificationInteraction,
kMaxValue = kNotificationInteraction
};
// Extract the notification information from `attr`, and store them into
// `mapping`. Will also copy the notification permission from the profile's
// pref into the `attr` if the profile is loaded.
void ExtractNotificationInformation(ProfileManagerIOS* manager,
NSMutableDictionary* mapping,
ProfileAttributesIOS& attr) {
const GaiaId& gaia_id = attr.GetGaiaId();
if (gaia_id.empty()) {
return;
}
// Get the permissions from `attr` but if they are missing, check if they
// can be found in the profile (if it is loaded).
const base::Value::Dict* permissions = attr.GetNotificationPermissions();
if (!permissions) {
ProfileIOS* profile = manager->GetProfileWithName(attr.GetProfileName());
if (profile) {
const base::Value::Dict& profile_permissions =
profile->GetPrefs()->GetDict(
prefs::kFeaturePushNotificationPermissions);
attr.SetNotificationPermissions(profile_permissions.Clone());
permissions = attr.GetNotificationPermissions();
}
}
// There is no permissions for the profile in attr (or no permission could
// be copied from the profile's pref, possibly because the profile is not
// yet loaded).
if (!permissions) {
return;
}
NSMutableDictionary* permissions_map = [[NSMutableDictionary alloc] init];
for (const auto pair : *permissions) {
permissions_map[base::SysUTF8ToNSString(pair.first)] =
[NSNumber numberWithBool:pair.second.GetBool()];
}
mapping[gaia_id.ToNSString()] = permissions_map;
}
// This function creates a dictionary that maps signed-in user's GAIA IDs to a
// map of each user's preferences for each push notification enabled feature.
GaiaIdToPushNotificationPreferenceMap*
GaiaIdToPushNotificationPreferenceMapFromCache() {
ProfileManagerIOS* manager = GetApplicationContext()->GetProfileManager();
ProfileAttributesStorageIOS* storage = manager->GetProfileAttributesStorage();
NSMutableDictionary* account_preference_map =
[[NSMutableDictionary alloc] init];
storage->IterateOverProfileAttributes(base::BindRepeating(
&ExtractNotificationInformation, manager, account_preference_map));
return account_preference_map;
}
// Call ContentNotificationService::SendNAUForConfiguration() after fetching
// the notification settings if `weak_profile` is still valid.
void SendNAUFConfigurationForProfileWithSettings(
base::WeakPtr<ProfileIOS> weak_profile,
UNNotificationSettings* settings) {
ProfileIOS* profile = weak_profile.get();
if (!profile) {
return;
}
UNAuthorizationStatus previousAuthStatus =
[PushNotificationUtil getSavedPermissionSettings];
ContentNotificationNAUConfiguration* config =
[[ContentNotificationNAUConfiguration alloc] init];
ContentNotificationSettingsAction* settingsAction =
[[ContentNotificationSettingsAction alloc] init];
settingsAction.previousAuthorizationStatus = previousAuthStatus;
settingsAction.currentAuthorizationStatus = settings.authorizationStatus;
config.settingsAction = settingsAction;
ContentNotificationServiceFactory::GetForProfile(profile)
->SendNAUForConfiguration(config);
}
// Records a failure to access the PushNotificationClientManager at a specific
// point.
void RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint failure_point) {
base::UmaHistogramEnumeration(
"IOS.PushNotification.ClientManagerAccessFailure", failure_point);
}
// Records the outcome of handling a multi-profile notification interaction.
void RecordPushNotificationTargetProfileHandlingResult(
PushNotificationTargetProfileHandlingResult result) {
base::UmaHistogramEnumeration("IOS.PushNotification.MultiProfile."
"PushNotificationTargetProfileHandlingResult",
result);
}
// Helper function to get the profile-specific PushNotificationClientManager
// directly from a ProfileIOS object. Returns nullptr if the manager cannot be
// retrieved.
PushNotificationClientManager* GetClientManagerForProfile(ProfileIOS* profile) {
CHECK(IsMultiProfilePushNotificationHandlingEnabled());
if (!profile) {
RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
kGetClientManagerNullProfileInput);
return nullptr;
}
PushNotificationProfileService* profile_service =
PushNotificationProfileServiceFactory::GetForProfile(profile);
if (!profile_service) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::
kGetClientManagerMissingProfileService);
return nullptr;
}
return profile_service->GetPushNotificationClientManager();
}
// Determines the associated Profile name using `user_info`.
//
// It first looks for `kOriginatingProfileNameKey`. If absent or otherwise
// invalid, it falls back to checking `kOriginatingGaiaIDKey` and uses
// `AccountProfileMapper` to map the Gaia ID to a Profile name.
//
// Returns the Profile name if found, otherwise returns an empty string. Logs
// specific reasons for failure to UMA.
//
// Note: This function should only be called when
// `IsMultiProfilePushNotificationHandlingEnabled()` is true.
std::string GetProfileNameFromUserInfo(NSDictionary* user_info) {
CHECK(IsMultiProfilePushNotificationHandlingEnabled());
ProfileManagerIOS* profile_manager =
GetApplicationContext()->GetProfileManager();
if (!profile_manager) {
CHECK_IS_TEST();
return "";
}
NSString* profile_name_ns = user_info[kOriginatingProfileNameKey];
if (profile_name_ns) {
if (profile_name_ns.length == 0) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::
kGetProfileNameEmptyNameProvided);
// Definite failure: An empty Profile name was explicitly provided. Cannot
// proceed or fallback.
return "";
}
std::string profile_name = base::SysNSStringToUTF8(profile_name_ns);
if (!profile_manager->HasProfileWithName(profile_name)) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::
kGetProfileNameDirectNameNotFoundInStorage);
// Definite failure: An invalid Profile name was explicitly provided.
// Cannot proceed or fallback.
return "";
}
// Definite success: Found a valid, existing Profile name directly via
// `kOriginatingProfileNameKey`.
return profile_name;
}
NSString* gaia_id_ns = user_info[kOriginatingGaiaIDKey];
GaiaId gaia_id = GaiaId(gaia_id_ns);
if (gaia_id.empty()) {
RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
kGetProfileNameMissingOrEmptyGaiaID);
// Definite failure: The string provided for kOriginatingGaiaIDKey was
// either missing or empty.
return "";
}
std::optional<std::string> mapped_profile_name =
GetApplicationContext()
->GetAccountProfileMapper()
->FindProfileNameForGaiaID(gaia_id);
if (!mapped_profile_name.has_value()) {
RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
kGetProfileNameGaiaIdNotMapped);
// Definite failure: The Gaia ID was valid but is not associated with any
// known Profile according to the AccountProfileMapper.
return "";
}
if (!profile_manager->HasProfileWithName(mapped_profile_name.value())) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::
kGetProfileNameMappedNameNotFoundInStorage);
// Definite failure: Gaia ID mapped successfully to a Profile name, but that
// Profile name does not exist in ProfileAttributesStorageIOS (e.g., stale
// mapping or recently deleted profile).
return "";
}
return mapped_profile_name.value();
}
// Callback used to asynchronously retrieve a `PushNotificationClientManager`.
using ClientManagerCallback =
base::OnceCallback<void(PushNotificationClientManager*)>;
// Helper function invoked after an asynchronous Profile load attempt.
// It retrieves the `PushNotificationClientManager` for the loaded Profile (if
// successful) and runs the original callback with the result.
//
// TODO(crbug.com/418696432): Ensure that the ScopedProfileKeepAliveIOS is
// not destroyed before `original_callback` and any background processing,
// if any, is complete. Currently the profile will be unloaded as soon as
// the current function returns, even if `original_callback` starts any
// background processing.
void OnProfileLoadedForClientManager(ClientManagerCallback original_callback,
ScopedProfileKeepAliveIOS keep_alive) {
CHECK(IsMultiProfilePushNotificationHandlingEnabled());
ProfileIOS* profile_after_load = keep_alive.profile();
if (!profile_after_load) {
RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
kGetClientManagerProfileLoadFailed);
std::move(original_callback).Run(nullptr);
return;
}
PushNotificationClientManager* manager =
GetClientManagerForProfile(profile_after_load);
std::move(original_callback).Run(manager);
}
// Gets the appropriate `PushNotificationClientManager` based on `user_info`.
// Checks for Profile-identifying keys (`kOriginatingProfileNameKey`,
// `kOriginatingGaiaIDKey`). If neither key is present, it synchronously returns
// the app-wide manager via the `callback`. If keys are present, it attempts to
// retrieve the Profile-specific manager, potentially loading the Profile
// asynchronously. The callback receives the retrieved manager or `nullptr` if
// the Profile-specific lookup or load fails.
void GetClientManagerForUserInfo(NSDictionary* user_info,
ClientManagerCallback callback) {
CHECK(IsMultiProfilePushNotificationHandlingEnabled());
BOOL hasProfileKey = (user_info[kOriginatingProfileNameKey] != nil);
BOOL hasGaiaKey = (user_info[kOriginatingGaiaIDKey] != nil);
// If the notification payload contains neither the originating Profile name
// key (`kOriginatingProfileNameKey`) nor the originating Gaia ID key
// (`kOriginatingGaiaIDKey`), assume it's intended only for the app-wide
// client manager.
if (!hasProfileKey && !hasGaiaKey) {
PushNotificationClientManager* app_wide_manager =
GetApplicationContext()
->GetPushNotificationService()
->GetPushNotificationClientManager();
std::move(callback).Run(app_wide_manager);
return;
}
std::string profile_name = GetProfileNameFromUserInfo(user_info);
if (profile_name.empty()) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::
kGetClientManagerFailedToGetProfileName);
std::move(callback).Run(nullptr);
return;
}
ProfileManagerIOS* profile_manager =
GetApplicationContext()->GetProfileManager();
ProfileIOS* loaded_profile =
profile_manager->GetProfileWithName(profile_name);
if (loaded_profile) {
PushNotificationClientManager* manager =
GetClientManagerForProfile(loaded_profile);
std::move(callback).Run(manager);
return;
}
if (!profile_manager->HasProfileWithName(profile_name)) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::
kGetClientManagerProfileNotFoundByName);
std::move(callback).Run(nullptr);
return;
}
profile_manager->LoadProfileAsync(
profile_name,
base::BindOnce(&OnProfileLoadedForClientManager, std::move(callback)));
}
// Callback executed after a Profile switch initiated by a notification
// interaction. This function validates the state of the `new_scene_state` and
// its associated Profile, retrieves the appropriate
// `PushNotificationClientManager` for the now-active `switched_profile`, and
// forwards the original `response` for handling by that manager. Logs failures
// to UMA.
void HandleNotificationInteractionAfterProfileSwitch(
UNNotificationResponse* response,
SceneState* new_scene_state,
base::OnceClosure completion_closure) {
base::ScopedClosureRunner run_completion_closure(
std::move(completion_closure));
CHECK(new_scene_state.profileState);
CHECK_GE(new_scene_state.profileState.initStage,
ProfileInitStage::kProfileLoaded);
ProfileIOS* switched_profile = new_scene_state.profileState.profile;
CHECK(switched_profile);
PushNotificationClientManager* client_manager =
GetClientManagerForProfile(switched_profile);
if (!client_manager) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::
kInteractionContinuationMissingClientManager);
return;
}
client_manager->HandleNotificationInteraction(response);
// Also allow the app-scoped clients the opportunity to handle interactions.
GetApplicationContext()
->GetPushNotificationService()
->GetPushNotificationClientManager()
->HandleNotificationInteraction(response);
}
// Creates a `ChangeProfileContinuation` callback bound with the original
// notification `response`. `HandleNotificationInteractionAfterProfileSwitch()`
// will be invoked by the Profile switching mechanism if a switch occurs,
// allowing the notification interaction to be handled in the context of the
// newly switched Profile.
ChangeProfileContinuation CreateNotificationInteractionContinuation(
UNNotificationResponse* response) {
return base::BindOnce(&HandleNotificationInteractionAfterProfileSwitch,
response);
}
// Handles notification reception using the app-wide client manager and calls
// the final completion block.
void HandleNotificationReceptionWithAppWideManager(
NSDictionary* user_info,
void (^completion_block)(UIBackgroundFetchResult /* result */)) {
PushNotificationClientManager* app_wide_manager =
GetApplicationContext()
->GetPushNotificationService()
->GetPushNotificationClientManager();
CHECK(app_wide_manager);
UIBackgroundFetchResult result =
app_wide_manager->HandleNotificationReception(user_info);
if (completion_block) {
completion_block(result);
}
}
// Callback invoked after asynchronously attempting to retrieve the
// Profile-specific `PushNotificationClientManager`. Falls back to app-wide
// manager, if necessary.
void OnClientManagerReadyForReception(
NSDictionary* user_info,
PushNotificationClientManagerFailurePoint failure_point,
void (^completion_block)(UIBackgroundFetchResult result),
PushNotificationClientManager* client_manager) {
if (!client_manager) {
RecordClientManagerAccessFailure(failure_point);
if (completion_block) {
completion_block(UIBackgroundFetchResultNoData);
}
return;
}
UIBackgroundFetchResult result =
client_manager->HandleNotificationReception(user_info);
if (completion_block) {
completion_block(result);
}
}
// Processes an incoming notification by attempting to use a Profile-specific
// client manager, falling back to the app-wide manager, if necessary.
void ProcessIncomingNotification(
NSDictionary* user_info,
PushNotificationClientManagerFailurePoint failure_point,
void (^completion_block)(UIBackgroundFetchResult result)) {
CHECK(IsMultiProfilePushNotificationHandlingEnabled());
ClientManagerCallback manager_ready_callback =
base::BindOnce(&OnClientManagerReadyForReception, user_info,
failure_point, completion_block);
// Start the async process to get the Profile-specific manager
GetClientManagerForUserInfo(user_info, std::move(manager_ready_callback));
}
} // anonymous namespace
@interface PushNotificationDelegate () <AppStateObserver,
NotificationClassifier,
ProfileStateObserver,
SceneStateObserver>
// The first connected scene that is foreground active, or nil if there are
// none.
@property(nonatomic, readonly) SceneState* foregroundActiveScene;
// The client manager for notification clients that are app-scoped (rather than
// profile-scoped).
@property(nonatomic, readonly)
PushNotificationClientManager* appWideClientManager;
// The object that records metrics about push notifications.
@property(nonatomic, readonly) NotificationMetricsRecorder* metricsRecorder;
@end
@implementation PushNotificationDelegate {
__weak AppState* _appState;
// Stores blocks to execute once the app is finished foregrounding.
NSMutableArray<ProceduralBlock>* _runAfterForeground;
}
- (instancetype)initWithAppState:(AppState*)appState
userNotificationCenter:
(UNUserNotificationCenter*)userNotificationCenter {
if ((self = [super init])) {
_appState = appState;
[_appState addObserver:self];
_metricsRecorder = [[NotificationMetricsRecorder alloc]
initWithNotificationCenter:userNotificationCenter];
_metricsRecorder.classifier = self;
}
return self;
}
#pragma mark - UNUserNotificationCenterDelegate -
- (void)userNotificationCenter:(UNUserNotificationCenter*)center
didReceiveNotificationResponse:(UNNotificationResponse*)response
withCompletionHandler:(void (^)(void))completionHandler {
// This method is invoked by iOS to process the user's response to a delivered
// notification.
[self recordLifeCycleEvent:PushNotificationLifecycleEvent::
kNotificationInteraction];
__weak __typeof(self) weakSelf = self;
[self executeWhenForeground:^{
[weakSelf handleNotificationResponse:response];
[weakSelf.metricsRecorder recordInteraction:response.notification];
}];
// TODO(crbug.com/401537165): Consider changing when completionHandler is
// called.
if (completionHandler) {
completionHandler();
}
base::UmaHistogramEnumeration(kAppLaunchSource,
AppLaunchSource::NOTIFICATION);
}
- (void)userNotificationCenter:(UNUserNotificationCenter*)center
willPresentNotification:(UNNotification*)notification
withCompletionHandler:
(void (^)(UNNotificationPresentationOptions options))
completionHandler {
__weak __typeof(self) weakSelf = self;
[self executeWhenForeground:^{
[weakSelf handleWillPresentNotification:notification
withCompletionHandler:completionHandler];
}];
}
- (void)userNotificationCenter:(UNUserNotificationCenter*)center
openSettingsForNotification:(UNNotification*)notification {
__weak __typeof(self) weakSelf = self;
if (IsMultiProfilePushNotificationHandlingEnabled()) {
std::string profileName =
GetProfileNameFromUserInfo(notification.request.content.userInfo);
if (!profileName.empty()) {
[self executeWhenForeground:^{
[weakSelf openSettingsForNotification:notification
profileName:profileName];
}];
return;
}
}
[self executeWhenForeground:^{
[weakSelf openSettingsForNotification:notification
scene:weakSelf.foregroundActiveScene
completion:base::DoNothing()];
}];
}
#pragma mark - PushNotificationDelegate
- (void)applicationWillProcessIncomingRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:
(void (^)(UIBackgroundFetchResult result))
completionHandler {
[self recordLifeCycleEvent:PushNotificationLifecycleEvent::
kNotificationReception];
double incomingNotificationTime =
base::Time::Now().InSecondsFSinceUnixEpoch();
auto recordMetricsAndComplete = ^(UIBackgroundFetchResult result) {
double processingTime =
base::Time::Now().InSecondsFSinceUnixEpoch() - incomingNotificationTime;
UmaHistogramCustomTimes(
"IOS.PushNotification.IncomingNotificationProcessingTime",
base::Milliseconds(processingTime),
kTimeRangeIncomingNotificationHistogramMin,
kTimeRangeIncomingNotificationHistogramMax,
kTimeRangeHistogramBucketCount);
if (completionHandler) {
completionHandler(result);
}
};
if (IsMultiProfilePushNotificationHandlingEnabled()) {
ProcessIncomingNotification(userInfo,
PushNotificationClientManagerFailurePoint::
kWillProcessIncomingRemoteNotification,
recordMetricsAndComplete);
} else {
HandleNotificationReceptionWithAppWideManager(userInfo,
recordMetricsAndComplete);
}
}
- (void)applicationDidRegisterWithAPNS:(NSData*)deviceToken
profile:(ProfileIOS*)profile {
GaiaIdToPushNotificationPreferenceMap* accountPreferenceMap =
GaiaIdToPushNotificationPreferenceMapFromCache();
// Return early if no accounts are signed into Chrome.
if (!accountPreferenceMap.count) {
return;
}
if (IsMultiProfilePushNotificationHandlingEnabled()) {
PushNotificationClientManager* clientManager =
GetClientManagerForProfile(profile);
// Gracefully handle the case where a clientManager couldn't be retrieved
// (e.g., if the Profile is `nullptr` or its service isn't available).
if (clientManager) {
// Registers Chrome's PushNotificationClients' Actionable Notifications
// with iOS.
clientManager->RegisterActionableNotifications();
} else {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::kDidRegisterWithAPNS);
}
}
// Registers Chrome's PushNotificationClients' Actionable Notifications with
// iOS.
self.appWideClientManager->RegisterActionableNotifications();
PushNotificationConfiguration* config =
[[PushNotificationConfiguration alloc] init];
config.accountIDs = accountPreferenceMap.allKeys;
config.preferenceMap = accountPreferenceMap;
config.deviceToken = deviceToken;
config.singleSignOnService =
GetApplicationContext()->GetSingleSignOnService();
if (profile) {
config.shouldRegisterContentNotification =
[self isContentNotificationAvailable:profile];
if (config.shouldRegisterContentNotification) {
AuthenticationService* authService =
AuthenticationServiceFactory::GetForProfile(profile);
id<SystemIdentity> identity =
authService->GetPrimaryIdentity(signin::ConsentLevel::kSignin);
config.primaryAccount = identity;
// Send an initial NAU to share the OS auth status and channel status with
// the server. Send an NAU on every foreground to report the OS Auth
// Settings.
[self sendSettingsChangeNAUForProfile:profile];
}
}
__weak __typeof(self) weakSelf = self;
base::WeakPtr<ProfileIOS> weakProfile =
profile ? profile->AsWeakPtr() : base::WeakPtr<ProfileIOS>{};
PushNotificationService* notificationService =
GetApplicationContext()->GetPushNotificationService();
notificationService->RegisterDevice(config, ^(NSError* error) {
[weakSelf deviceRegistrationForProfile:weakProfile withError:error];
});
}
- (void)deviceRegistrationForProfile:(base::WeakPtr<ProfileIOS>)weakProfile
withError:(NSError*)error {
base::UmaHistogramBoolean("IOS.PushNotification.ChimeDeviceRegistration",
!error);
if (!error) {
if (ProfileIOS* profile = weakProfile.get()) {
if (base::FeatureList::IsEnabled(
send_tab_to_self::kSendTabToSelfIOSPushNotifications)) {
[self setUpAndEnableSendTabNotificationsWithProfile:profile];
}
}
}
}
#pragma mark - NotificationClassifier
- (NotificationType)classifyNotification:(UNNotification*)notification {
// Use an arbitrary profile that is loaded - a specific profile is not needed
// to determine the type of notification.
std::vector<ProfileIOS*> loaded_profiles =
GetApplicationContext()->GetProfileManager()->GetLoadedProfiles();
CHECK(!loaded_profiles.empty());
ProfileIOS* profile = loaded_profiles.back();
PushNotificationClient* client = [self clientForNotification:notification
profile:profile];
if (!client) {
return NotificationType::kUnknown;
}
std::optional<NotificationType> type =
client->GetNotificationType(notification);
return type.value_or(NotificationType::kUnknown);
}
#pragma mark - AppStateObserver
- (void)appState:(AppState*)appState
sceneDidBecomeActive:(SceneState*)sceneState {
if (sceneState.profileState.initStage < ProfileInitStage::kFinal) {
[sceneState.profileState addObserver:self];
return;
}
if (sceneState.activationLevel < SceneActivationLevelForegroundActive) {
return;
}
[self appDidEnterForeground:sceneState];
}
- (void)appState:(AppState*)appState sceneConnected:(SceneState*)sceneState {
[sceneState addObserver:self];
[self sceneState:sceneState
transitionedToActivationLevel:sceneState.activationLevel];
}
#pragma mark - ProfileStateObserver
- (void)profileState:(ProfileState*)profileState
didTransitionToInitStage:(ProfileInitStage)nextInitStage
fromInitStage:(ProfileInitStage)fromInitStage {
if (nextInitStage < ProfileInitStage::kFinal) {
return;
}
for (SceneState* sceneState in profileState.connectedScenes) {
if (sceneState.activationLevel < SceneActivationLevelForegroundActive) {
continue;
}
[self appDidEnterForeground:sceneState];
}
[profileState removeObserver:self];
}
#pragma mark - SceneStateObserver
- (void)sceneState:(SceneState*)sceneState
transitionedToActivationLevel:(SceneActivationLevel)level {
if (level < SceneActivationLevelForegroundActive) {
return;
}
if (sceneState.profileState.initStage < ProfileInitStage::kFinal) {
[sceneState.profileState addObserver:self];
return;
}
[self appDidEnterForeground:sceneState];
}
- (void)sceneState:(SceneState*)sceneState
profileStateConnected:(ProfileState*)profileState {
[profileState addObserver:self];
}
#pragma mark - Property accessors
- (SceneState*)foregroundActiveScene {
for (SceneState* sceneState in _appState.connectedScenes) {
if (sceneState.activationLevel < SceneActivationLevelForegroundActive) {
continue;
}
if (sceneState.profileState.initStage < ProfileInitStage::kFinal) {
continue;
}
return sceneState;
}
return nil;
}
- (PushNotificationClientManager*)appWideClientManager {
return GetApplicationContext()
->GetPushNotificationService()
->GetPushNotificationClientManager();
}
#pragma mark - Private
// Handles a notification that is about to be presented.
- (void)handleWillPresentNotification:(UNNotification*)notification
withCompletionHandler:
(void (^)(UNNotificationPresentationOptions options))
completionHandler {
[self.metricsRecorder recordReceived:notification];
[self recordLifeCycleEvent:PushNotificationLifecycleEvent::
kNotificationForegroundPresentation];
NSDictionary* userInfo = notification.request.content.userInfo;
__weak __typeof(self) weakSelf = self;
void (^presentationCompletionBlock)(UIBackgroundFetchResult result) =
^(UIBackgroundFetchResult /* result */) {
[weakSelf handlePresentationCompletionWithUserInfo:userInfo
completionHandler:completionHandler];
};
if (IsMultiProfilePushNotificationHandlingEnabled()) {
ProcessIncomingNotification(
userInfo,
PushNotificationClientManagerFailurePoint::kWillPresentNotification,
presentationCompletionBlock);
} else {
HandleNotificationReceptionWithAppWideManager(userInfo,
presentationCompletionBlock);
}
}
// Determines how a notification should be presented when received while the app
// is in the foreground and invokes the system completion handler with the
// appropriate options. It also logs a histogram for the notification event.
- (void)handlePresentationCompletionWithUserInfo:(NSDictionary*)userInfo
completionHandler:
(void (^)(UNNotificationPresentationOptions
options))completionHandler {
if (completionHandler) {
BOOL isSendTab = ([userInfo[kPushNotificationClientIdKey] intValue] ==
static_cast<int>(PushNotificationClientId::kSendTab));
BOOL isForeground = (self.foregroundActiveScene != nil);
UNNotificationPresentationOptions presentationOptions =
(isSendTab && isForeground) ? UNNotificationPresentationOptionNone
: UNNotificationPresentationOptionBanner;
// TODO(crbug.com/408085973): Add PushNotificationDelegate unittest suite.
// Cover critical paths and error cases.
completionHandler(presentationOptions);
}
base::UmaHistogramEnumeration(kAppLaunchSource,
AppLaunchSource::NOTIFICATION);
}
// Executes blocks queued in _runAfterForeground. If multi-profile handling is
// enabled, also notifies the profile-specific PushNotificationClientManager
// for the given sceneState upon readiness.
- (void)handleQueuedBlocksWithSceneState:(SceneState*)sceneState {
NSMutableArray<ProceduralBlock>* blocks = _runAfterForeground;
_runAfterForeground = nil;
for (ProceduralBlock block in blocks) {
block();
}
if (IsMultiProfilePushNotificationHandlingEnabled()) {
if (!sceneState || !sceneState.profileState.profile) {
return;
}
PushNotificationClientManager* clientManager =
GetClientManagerForProfile(sceneState.profileState.profile);
if (!clientManager) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::kAppDidEnterForeground);
return;
}
clientManager->OnSceneActiveForegroundBrowserReady();
}
}
// Notifies the client manager that the scene is "foreground active".
- (void)appDidEnterForeground:(SceneState*)sceneState {
PushNotificationClientManager* appWideClientManager =
self.appWideClientManager;
DCHECK(appWideClientManager);
appWideClientManager->OnSceneActiveForegroundBrowserReady();
[self.metricsRecorder
handleDeliveredNotificationsWithClosure:base::DoNothing()];
__weak PushNotificationDelegate* weakSelf = self;
__weak SceneState* weakSceneState = sceneState;
// Asynchronously processes any queued `_runAfterForeground` blocks.
//
// This is crucial for notification interactions (queued via
// `-executeWhenForeground:`), as handling them might require tearing down the
// current Browser/Profile to switch Profiles. Asynchronous processing ensures
// that all current observers (potentially observing the Browser/Profile being
// torn down) finish their work *before* these objects are destroyed. This
// prevents state modification or destruction while observers are still active
// in the original synchronous call stack.
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf handleQueuedBlocksWithSceneState:weakSceneState];
});
ProfileIOS* profile = sceneState.profileState.profile;
if (IsContentNotificationEnabled(profile)) {
ContentNotificationService* contentNotificationService =
ContentNotificationServiceFactory::GetForProfile(profile);
int maxNauSentPerSession = base::GetFieldTrialParamByFeatureAsInt(
kContentNotificationDeliveredNAU, kDeliveredNAUMaxPerSession,
kDeliveredNAUMaxSendsPerSession);
// Check if there are notifications received in the background to send the
// respective NAUs.
NSUserDefaults* defaults = app_group::GetGroupUserDefaults();
if ([defaults objectForKey:kContentNotificationContentArrayKey] != nil) {
NSMutableArray* contentArray = [[defaults
objectForKey:kContentNotificationContentArrayKey] mutableCopy];
// Report in 5 item increments.
NSMutableArray* uploadedItems = [NSMutableArray array];
for (NSData* item in contentArray) {
ContentNotificationNAUConfiguration* config =
[[ContentNotificationNAUConfiguration alloc] init];
config.actionType = NAUActionTypeDisplayed;
UNNotificationContent* content = [NSKeyedUnarchiver
unarchivedObjectOfClass:UNMutableNotificationContent.class
fromData:item
error:nil];
config.content = content;
contentNotificationService->SendNAUForConfiguration(config);
[uploadedItems addObject:item];
base::UmaHistogramEnumeration(
kContentNotificationActionHistogramName,
NotificationActionType::kNotificationActionTypeDisplayed);
if ((int)uploadedItems.count == maxNauSentPerSession) {
break;
}
}
[contentArray removeObjectsInArray:uploadedItems];
if (contentArray.count > 0) {
[defaults setObject:contentArray
forKey:kContentNotificationContentArrayKey];
} else {
[defaults setObject:nil forKey:kContentNotificationContentArrayKey];
}
}
// Send an NAU on every foreground to report the OS Auth Settings.
[self sendSettingsChangeNAUForProfile:profile];
}
[PushNotificationUtil updateAuthorizationStatusPref];
// For Reactivation Notifications, ask for provisional auth right away.
if (IsFirstRunRecent(base::Days(28)) &&
IsIOSReactivationNotificationsEnabled()) {
UNAuthorizationStatus auth_status =
[PushNotificationUtil getSavedPermissionSettings];
if (auth_status == UNAuthorizationStatusNotDetermined) {
[PushNotificationUtil enableProvisionalPushNotificationPermission:nil];
}
}
}
- (void)sendSettingsChangeNAUForProfile:(ProfileIOS*)profile {
[PushNotificationUtil
getPermissionSettings:base::CallbackToBlock(base::BindOnce(
&SendNAUFConfigurationForProfileWithSettings,
profile->AsWeakPtr()))];
}
- (void)recordLifeCycleEvent:(PushNotificationLifecycleEvent)event {
base::UmaHistogramEnumeration(kLifecycleEventsHistogram, event);
}
- (BOOL)isContentNotificationAvailable:(ProfileIOS*)profile {
return IsContentNotificationEnabled(profile) ||
IsContentNotificationRegistered(profile);
}
// If user has not previously disabled Send Tab notifications, either 1) If user
// has authorized full notification permissions, enables Send Tab notifications
// OR 2) enrolls user in provisional notifications for Send Tab notification
// type.
- (void)setUpAndEnableSendTabNotificationsWithProfile:(ProfileIOS*)profile {
// Refresh the local device info now that the client has a Chime
// Representative Target ID.
syncer::DeviceInfoSyncService* deviceInfoSyncService =
DeviceInfoSyncServiceFactory::GetForProfile(profile);
deviceInfoSyncService->RefreshLocalDeviceInfo();
AuthenticationService* authService =
AuthenticationServiceFactory::GetForProfile(profile);
NSString* gaiaID =
authService->GetPrimaryIdentity(signin::ConsentLevel::kSignin).gaiaID;
// Early return if 1) the user has previously disabled Send Tab push
// notifications, because in that case we don't want to automatically enable
// the notification type or 2) if Send Tab notifications are already enabled.
if (profile->GetPrefs()->GetBoolean(
prefs::kSendTabNotificationsPreviouslyDisabled) ||
push_notification_settings::
GetMobileNotificationPermissionStatusForClient(
PushNotificationClientId::kSendTab, GaiaId(gaiaID))) {
return;
}
if ([PushNotificationUtil getSavedPermissionSettings] ==
UNAuthorizationStatusAuthorized) {
GetApplicationContext()->GetPushNotificationService()->SetPreference(
gaiaID, PushNotificationClientId::kSendTab, true);
} else {
ProvisionalPushNotificationServiceFactory::GetForProfile(profile)
->EnrollUserToProvisionalNotifications(
ProvisionalPushNotificationService::ClientIdState::kEnabled,
{PushNotificationClientId::kSendTab});
}
}
// Runs the given `block` immediately if the app has an active foreground
// scene connected, otherwise stores it to be called when the app is
// foregrounded.
- (void)executeWhenForeground:(ProceduralBlock)block {
if (self.foregroundActiveScene) {
block();
return;
}
if (!_runAfterForeground) {
_runAfterForeground = [[NSMutableArray alloc] init];
}
[_runAfterForeground addObject:block];
}
// Handles a notification response by sending it to the push notification
// client manager.
- (void)handleNotificationResponse:(UNNotificationResponse*)response {
DCHECK_GE(_appState.initStage,
AppInitStage::kBrowserObjectsForBackgroundHandlers);
if (IsMultiProfilePushNotificationHandlingEnabled()) {
[self handleProfileSpecificNotificationResponse:response];
return;
}
// Notifications are intentionally passed on to the `appWideClientManager`
// even when the profile specific one handles them. In a future refacor, if a
// notification is properly handled in a profile specific manager it likely
// should not be passed onto the app wide manager.
self.appWideClientManager->HandleNotificationInteraction(response);
}
// Handles a notification interaction specifically for the multi-Profile case.
//
// It determines the Profile the notification originated from and the target
// scene where the interaction should occur. Based on whether the target scene's
// current Profile matches the notification's originating Profile, it either:
//
// 1. Handles the interaction directly using the current context if the Profiles
// match.
// 2. Initiates a Profile switch for the target scene if the Profiles do not
// match. A continuation callback is provided to the switching mechanism to
// process the interaction once the correct Profile is active.
//
// Failures encountered during Profile validation, scene lookup, manager
// retrieval, or switch initiation are logged to UMA for monitoring.
- (void)handleProfileSpecificNotificationResponse:
(UNNotificationResponse*)response {
CHECK(IsMultiProfilePushNotificationHandlingEnabled());
std::string profileName = GetProfileNameFromUserInfo(
response.notification.request.content.userInfo);
if (profileName.empty()) {
// No profile name was found, so allow app-wide clients the opportunity to
// handle interactions.
self.appWideClientManager->HandleNotificationInteraction(response);
RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
kHandleInteractionInvalidProfileName);
RecordPushNotificationTargetProfileHandlingResult(
PushNotificationTargetProfileHandlingResult::kProfileUnidentifiable);
return;
}
SceneState* targetSceneState =
[self notificationTargetSceneStateForResponse:response];
if (!targetSceneState) {
RecordClientManagerAccessFailure(PushNotificationClientManagerFailurePoint::
kHandleInteractionMissingTargetScene);
targetSceneState = self.foregroundActiveScene;
}
if (!targetSceneState) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::
kHandleInteractionMissingFallbackScene);
RecordPushNotificationTargetProfileHandlingResult(
PushNotificationTargetProfileHandlingResult::kFailureSceneUnavailable);
return;
}
ProfileIOS* sceneProfile = targetSceneState.profileState.profile;
if (!sceneProfile || profileName != sceneProfile->GetProfileName()) {
RecordPushNotificationTargetProfileHandlingResult(
PushNotificationTargetProfileHandlingResult::
kSwitchEnsuredCorrectProfile);
} else {
RecordPushNotificationTargetProfileHandlingResult(
PushNotificationTargetProfileHandlingResult::kCorrectProfileActive);
}
id<ChangeProfileCommands> handler =
HandlerForProtocol(_appState.appCommandDispatcher, ChangeProfileCommands);
CHECK(handler);
[handler changeProfile:profileName
forScene:targetSceneState
reason:ChangeProfileReason::kHandlePushNotification
continuation:CreateNotificationInteractionContinuation(response)];
}
// Shows the app's notification settings, switching to the given `profileName`
// profile if needed.
- (void)openSettingsForNotification:(UNNotification*)notification
profileName:(std::string_view)profileName {
SceneState* sceneState = self.foregroundActiveScene;
CHECK(sceneState);
CHECK(IsMultiProfilePushNotificationHandlingEnabled());
id<ChangeProfileCommands> handler =
HandlerForProtocol(_appState.appCommandDispatcher, ChangeProfileCommands);
__weak __typeof(self) weakSelf = self;
ChangeProfileContinuation continuation = base::BindOnce(
[](__typeof(self) strong_self, UNNotification* notification,
SceneState* new_scene_state, base::OnceClosure completion_closure) {
[strong_self openSettingsForNotification:notification
scene:new_scene_state
completion:std::move(completion_closure)];
},
weakSelf, notification);
[handler changeProfile:profileName
forScene:sceneState
reason:ChangeProfileReason::kHandlePushNotification
continuation:std::move(continuation)];
}
// Shows the app's notification settings in the given `sceneState`, and calls
// `completion` when finished.
- (void)openSettingsForNotification:(UNNotification*)notification
scene:(SceneState*)sceneState
completion:(base::OnceClosure)completion {
CHECK(sceneState);
Browser* browser =
sceneState.browserProviderInterface.mainBrowserProvider.browser;
ProfileIOS* profile = browser->GetProfile();
// Get the clientID for the client that is associated with this notification.
PushNotificationClient* client = [self clientForNotification:notification
profile:profile];
std::optional<PushNotificationClientId> clientID =
(client) ? std::make_optional(client->GetClientId()) : std::nullopt;
CommandDispatcher* dispatcher = browser->GetCommandDispatcher();
id<ApplicationCommands> applicationHandler =
HandlerForProtocol(dispatcher, ApplicationCommands);
id<SettingsCommands> settingsHandler =
HandlerForProtocol(dispatcher, SettingsCommands);
__block base::OnceClosure completion2 = std::move(completion);
[applicationHandler
prepareToPresentModalWithSnackbarDismissal:YES
completion:^{
[settingsHandler
showNotificationsSettingsAndHighlightClient:
clientID];
std::move(completion2).Run();
}];
}
// Returns the client to handle the given `notification`. The client can be
// either profile-scoped (and associated with the given `profile`), or
// app-scoped.
- (PushNotificationClient*)clientForNotification:(UNNotification*)notification
profile:(ProfileIOS*)profile {
if (notification == nil) {
return nullptr;
}
PushNotificationClient* client = nullptr;
if (IsMultiProfilePushNotificationHandlingEnabled()) {
PushNotificationClientManager* clientManager =
GetClientManagerForProfile(profile);
client = clientManager->GetClientForNotification(notification);
}
if (!client) {
client = self.appWideClientManager->GetClientForNotification(notification);
}
return client;
}
// Returns the `SceneState` matching the notification response's target scene,
// if any.
- (SceneState*)notificationTargetSceneStateForResponse:
(UNNotificationResponse*)response {
UIScene* targetScene = response.targetScene;
if (!targetScene) {
RecordClientManagerAccessFailure(
PushNotificationClientManagerFailurePoint::kGetResponseTargetSceneNil);
return nil;
}
std::string targetSceneSessionIdentifier =
SessionIdentifierForScene(targetScene);
for (SceneState* scene in _appState.connectedScenes) {
if (scene.sceneSessionID == targetSceneSessionIdentifier) {
return scene;
}
}
// No matching scene found
return nil;
}
@end