Added Three New Functions to the PushNotificationService Interface

In this CL, PushNotificationService interface defined three new
functions. In a future CL, these functions will be implemented
downstream. These functions are necessary for integrating the push
notification infrastructure into the sign-in pipeline while supporting a
multi-profile environment.

This is a multi-part CL:
  => https://crrev.com/c/4013727
  -  https://crrev.com/i/5080653

Bug: 1376956
Change-Id: I8017369dd7df850700c524e9364f4b1de583fd44
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4013727
Reviewed-by: Sylvain Defresne <sdefresne@chromium.org>
Commit-Queue: Daniel White <danieltwhite@google.com>
Cr-Commit-Position: refs/heads/main@{#1083308}
diff --git a/ios/chrome/browser/providers/push_notification/chromium_push_notification.mm b/ios/chrome/browser/providers/push_notification/chromium_push_notification.mm
index 6428d39..86299fa3 100644
--- a/ios/chrome/browser/providers/push_notification/chromium_push_notification.mm
+++ b/ios/chrome/browser/providers/push_notification/chromium_push_notification.mm
@@ -24,6 +24,12 @@
   void RegisterDevice(PushNotificationConfiguration* config,
                       void (^completion_handler)(NSError* error)) final;
   void UnregisterDevice(void (^completion_handler)(NSError* error)) final;
+  bool DeviceTokenIsSet() const final;
+
+ protected:
+  // PushNotificationService implementation.
+  void SetAccountsToDevice(NSArray<NSString*>* account_ids,
+                           CompletionHandler completion_handler) final;
 };
 
 void ChromiumPushNotificationService::RegisterDevice(
@@ -58,6 +64,27 @@
       }));
 }
 
+bool ChromiumPushNotificationService::DeviceTokenIsSet() const {
+  return false;
+}
+
+void ChromiumPushNotificationService::SetAccountsToDevice(
+    NSArray<NSString*>* account_ids,
+    void (^completion_handler)(NSError* error)) {
+  // Chromium does not initialize the device's connection to the push
+  // notification server. As a result, the `completion_handler` is called with
+  // a NSFeatureUnsupportedError.
+
+  base::SequencedTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::BindOnce(^() {
+        NSError* error =
+            [NSError errorWithDomain:kChromiumPushNotificationErrorDomain
+                                code:NSFeatureUnsupportedError
+                            userInfo:nil];
+        completion_handler(error);
+      }));
+}
+
 }  // namespace
 
 std::unique_ptr<PushNotificationService> CreatePushNotificationService() {
diff --git a/ios/chrome/browser/push_notification/BUILD.gn b/ios/chrome/browser/push_notification/BUILD.gn
index 533e8f2..20637ac 100644
--- a/ios/chrome/browser/push_notification/BUILD.gn
+++ b/ios/chrome/browser/push_notification/BUILD.gn
@@ -5,6 +5,8 @@
 source_set("push_notification_service") {
   configs += [ "//build/config/compiler:enable_arc" ]
   sources = [
+    "push_notification_account_context_manager.h",
+    "push_notification_account_context_manager.mm",
     "push_notification_client_manager.h",
     "push_notification_client_manager.mm",
     "push_notification_configuration.h",
diff --git a/ios/chrome/browser/push_notification/push_notification_account_context_manager.h b/ios/chrome/browser/push_notification/push_notification_account_context_manager.h
new file mode 100644
index 0000000..397386e
--- /dev/null
+++ b/ios/chrome/browser/push_notification/push_notification_account_context_manager.h
@@ -0,0 +1,60 @@
+// 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.
+
+#ifndef IOS_CHROME_BROWSER_PUSH_NOTIFICATION_PUSH_NOTIFICATION_ACCOUNT_CONTEXT_MANAGER_H_
+#define IOS_CHROME_BROWSER_PUSH_NOTIFICATION_PUSH_NOTIFICATION_ACCOUNT_CONTEXT_MANAGER_H_
+
+#import <Foundation/Foundation.h>
+
+#import "ios/chrome/browser/browser_state/chrome_browser_state_manager.h"
+
+class BrowserStateInfoCache;
+
+namespace ios {
+class ChromeBrowserStateManager;
+
+}
+
+// This class is intended to store the permissions for each push notification
+// enabled feature for a given account and the number of times the account is
+// signed in across BrowserStates.
+@interface PushNotificationAccountContext : NSObject
+// A dictionary that maps the string value of a push notification client id to
+// the perf service value for that push notification enable feature.
+@property(nonatomic, copy) NSDictionary<NSString*, NSNumber*>* preferenceMap;
+// A counter that stores the number of times a given account is used across
+// BrowserStates.
+@property(nonatomic, readonly) NSUInteger occurrencesAcrossBrowserStates;
+@end
+
+// The purpose of this class is to manage the mapping between GaiaIDs and its
+// context data related to push notifications.
+@interface PushNotificationAccountContextManager : NSObject
+
+// The designated initializer. `BrowserStateInfoCache` must not be nil.
+- (instancetype)initWithChromeBrowserStateManager:
+    (ios::ChromeBrowserStateManager*)manager NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+// Adds the account to the manager if the account is not signed into the device
+// in any BrowserState. This function returns a BOOL value indicating whether
+// the account was added to the manager.
+- (BOOL)addAccount:(NSString*)gaiaID;
+
+// Removes the account from the manager if the account is not signed into the
+// device in any BrowserState. This function returns a BOOL value indicating
+// whether the account was removed from the maanger.
+- (BOOL)removeAccount:(NSString*)gaiaID;
+
+// A dictionary that maps a user's GAIA ID to an object containing the account's
+// preferences for all push notification enabled features and an number
+// representing the number of times the account is signed in across
+// BrowserStates.
+@property(nonatomic, readonly)
+    NSDictionary<NSString*, PushNotificationAccountContext*>* contextMap;
+
+@end
+
+#endif  // IOS_CHROME_BROWSER_PUSH_NOTIFICATION_PUSH_NOTIFICATION_ACCOUNT_CONTEXT_MANAGER_H_
diff --git a/ios/chrome/browser/push_notification/push_notification_account_context_manager.mm b/ios/chrome/browser/push_notification/push_notification_account_context_manager.mm
new file mode 100644
index 0000000..4c28430d
--- /dev/null
+++ b/ios/chrome/browser/push_notification/push_notification_account_context_manager.mm
@@ -0,0 +1,145 @@
+// 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/push_notification_account_context_manager.h"
+
+#import "base/strings/sys_string_conversions.h"
+#import "components/prefs/pref_service.h"
+#import "ios/chrome/browser/browser_state/browser_state_info_cache.h"
+#import "ios/chrome/browser/browser_state/chrome_browser_state.h"
+#import "ios/chrome/browser/browser_state/chrome_browser_state_manager.h"
+#import "ios/chrome/browser/prefs/pref_names.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+@interface PushNotificationAccountContext ()
+
+@property(nonatomic, assign) NSUInteger occurrencesAcrossBrowserStates;
+
+@end
+
+@implementation PushNotificationAccountContext
+@end
+
+@implementation PushNotificationAccountContextManager {
+  // Used to retrieve BrowserStates located at a given path.
+  ios::ChromeBrowserStateManager* _chromeBrowserStateManager;
+  // Maps a GaiaID to the given account's AccountContext object.
+  NSMutableDictionary<NSString*, PushNotificationAccountContext*>* _contextMap;
+}
+
+- (instancetype)initWithChromeBrowserStateManager:
+    (ios::ChromeBrowserStateManager*)manager {
+  self = [super init];
+
+  if (self) {
+    _contextMap = [[NSMutableDictionary alloc] init];
+    _chromeBrowserStateManager = manager;
+    BrowserStateInfoCache* infoCache = manager->GetBrowserStateInfoCache();
+
+    const size_t numberOfBrowserStates = infoCache->GetNumberOfBrowserStates();
+    for (size_t i = 0; i < numberOfBrowserStates; i++) {
+      NSString* gaiaID =
+          base::SysUTF8ToNSString(infoCache->GetGAIAIdOfBrowserStateAtIndex(i));
+      base::FilePath path = infoCache->GetPathOfBrowserStateAtIndex(i);
+      ChromeBrowserState* chromeBrowserState =
+          _chromeBrowserStateManager->GetBrowserState(path);
+      [self addAccount:gaiaID withBrowserState:chromeBrowserState];
+    }
+  }
+
+  return self;
+}
+
+- (BOOL)addAccount:(NSString*)gaiaID {
+  ChromeBrowserState* chromeBrowserState = [self chromeBrowserStateFrom:gaiaID];
+  if (!chromeBrowserState)
+    return NO;
+
+  return [self addAccount:gaiaID
+         withBrowserState:[self chromeBrowserStateFrom:gaiaID]];
+}
+
+- (BOOL)removeAccount:(NSString*)gaiaID {
+  PushNotificationAccountContext* context = _contextMap[gaiaID];
+  DCHECK(context);
+
+  context.occurrencesAcrossBrowserStates--;
+  if (context.occurrencesAcrossBrowserStates > 0)
+    return NO;
+
+  [_contextMap removeObjectForKey:gaiaID];
+  return YES;
+}
+
+#pragma mark - Properties
+
+- (NSDictionary<NSString*, PushNotificationAccountContext*>*)contextMap {
+  return _contextMap;
+}
+
+#pragma mark - Private
+
+// Create a new AccountContext object for the given gaiaID, if the gaia id does
+// not already exist in the dictionary, and maps the gaiaID to the new context
+// object.
+- (BOOL)addAccount:(NSString*)gaiaID
+    withBrowserState:(ChromeBrowserState*)browserState {
+  DCHECK(browserState);
+
+  if (!gaiaID.length)
+    return NO;
+
+  PushNotificationAccountContext* context = _contextMap[gaiaID];
+  if (context) {
+    context.occurrencesAcrossBrowserStates++;
+    return NO;
+  }
+
+  PrefService* prefService = browserState->GetPrefs();
+  NSMutableDictionary<NSString*, NSNumber*>* preferenceMap =
+      [[NSMutableDictionary alloc] init];
+  const base::Value::Dict& permissions =
+      prefService->GetDict(prefs::kFeaturePushNotificationPermissions);
+
+  for (const auto pair : permissions) {
+    preferenceMap[base::SysUTF8ToNSString(pair.first)] =
+        [NSNumber numberWithBool:pair.second.GetBool()];
+  }
+
+  context = [[PushNotificationAccountContext alloc] init];
+  context.preferenceMap = preferenceMap;
+  context.occurrencesAcrossBrowserStates = 1;
+  _contextMap[gaiaID] = context;
+
+  return YES;
+}
+
+// Returns the ChromeBrowserState that has the given gaiaID set as the primary
+// account. TODO(crbug.com/1400732) Implement policy that computes correct
+// permission set. This function naively chooses the first ChromeBrowserState
+// that is associated with the given gaiaID. In a multi-profile environment
+// where the given gaiaID is signed into multiple profiles, it is possible that
+// the push notification enabled features' permissions may be incorrectly
+// applied.
+- (ChromeBrowserState*)chromeBrowserStateFrom:(NSString*)gaiaID {
+  BrowserStateInfoCache* infoCache =
+      _chromeBrowserStateManager->GetBrowserStateInfoCache();
+  const size_t numberOfBrowserStates = infoCache->GetNumberOfBrowserStates();
+  for (size_t i = 0; i < numberOfBrowserStates; i++) {
+    NSString* browserStateGaiaID =
+        base::SysUTF8ToNSString(infoCache->GetGAIAIdOfBrowserStateAtIndex(i));
+
+    if (gaiaID == browserStateGaiaID) {
+      base::FilePath path = infoCache->GetPathOfBrowserStateAtIndex(i);
+      return _chromeBrowserStateManager->GetBrowserState(path);
+    }
+  }
+
+  return nil;
+}
+
+@end
diff --git a/ios/chrome/browser/push_notification/push_notification_configuration.h b/ios/chrome/browser/push_notification/push_notification_configuration.h
index aa38d51..ed3f350 100644
--- a/ios/chrome/browser/push_notification/push_notification_configuration.h
+++ b/ios/chrome/browser/push_notification/push_notification_configuration.h
@@ -7,6 +7,7 @@
 
 #import <Foundation/Foundation.h>
 
+@class PushNotificationAccountContextManager;
 @protocol SingleSignOnService;
 
 using GaiaIdToPushNotificationPreferenceMap =
@@ -28,10 +29,18 @@
 // SingleSignOnService used by PushNotificationService.
 @property(nonatomic, strong) id<SingleSignOnService> ssoService;
 
-// A dictionary that maps a user's GAIA ID to its preferences for all push
-// notification enabled features.
+// DEPRECATED. Please use the `contextManager.contextMap` instead. A dictionary
+// that maps a user's GAIA ID to its preferences for all push notification
+// enabled features.
 @property(nonatomic, copy) GaiaIdToPushNotificationPreferenceMap* preferenceMap;
 
+// A dictionary that maps a user's GAIA ID to an object containing the account's
+// preferences for all push notification enabled features and an number
+// representing the number of times the account is signed in across
+// BrowserStates.
+@property(nonatomic, copy)
+    PushNotificationAccountContextManager* contextManager;
+
 @end
 
-#endif  // IOS_CHROME_BROWSER_PUSH_NOTIFICATION_PUSH_NOTIFICATION_CONFIGURATION_H_
\ No newline at end of file
+#endif  // IOS_CHROME_BROWSER_PUSH_NOTIFICATION_PUSH_NOTIFICATION_CONFIGURATION_H_
diff --git a/ios/chrome/browser/push_notification/push_notification_service.h b/ios/chrome/browser/push_notification/push_notification_service.h
index c5aaa0e8..09e64dc 100644
--- a/ios/chrome/browser/push_notification/push_notification_service.h
+++ b/ios/chrome/browser/push_notification/push_notification_service.h
@@ -12,6 +12,10 @@
 namespace user_prefs {
 class PrefRegistrySyncable;
 }  // namespace user_prefs
+namespace ios {
+class ChromeBrowserStateManager;
+}
+@class PushNotificationAccountContextManager;
 class PushNotificationClientManager;
 
 // Service responsible for establishing connection and interacting
@@ -35,6 +39,21 @@
   // the operation successfully or unsuccessfully completes.
   virtual void UnregisterDevice(CompletionHandler completion_handler) = 0;
 
+  // Returns whether the device has retrieved and stored its APNS device token.
+  virtual bool DeviceTokenIsSet() const;
+
+  // Registers the new account to the push notification server. In a multi
+  // BrowserState environment, the PushNotificationService tracks the signed in
+  // account across BrowserStates.
+  void RegisterAccount(NSString* account_id,
+                       CompletionHandler completion_handler);
+
+  // Unregisters the account from the push notification server. In a multi
+  // BrowserState environment, the account will not be signed out until it's
+  // signed out across BrowserStates.
+  void UnregisterAccount(NSString* account_id,
+                         CompletionHandler completion_handler);
+
   // Updates the current user's push notification preferences with the push
   // notification server.
   void UpdateFeaturePushNotificationPreferences(
@@ -51,10 +70,30 @@
   // Returns PushNotificationService's PushNotificationClientManager.
   PushNotificationClientManager* GetPushNotificationClientManager();
 
+ protected:
+  PushNotificationService(ios::ChromeBrowserStateManager* manager);
+  // Registers the device with the push notification server. By supplying a list
+  // of the GAIA IDs currently logged into Chrome on the device and the device's
+  // APNS token, the server associates the GAIA IDs to the device, which allows
+  // the server to begin sending push notifications to that device. When this
+  // method is called, the server overwrites the accountIDs that are associated
+  // with the device token with the given array of accountIDs. Thus, to
+  // unregister an account from receiving push notifications on the device, this
+  // method should be called with an array of accountIDs that omits the account
+  // that is intended to be unregistered.
+  virtual void SetAccountsToDevice(NSArray<NSString*>* account_ids,
+                                   CompletionHandler completion_handler) {}
+
  private:
   // The PushNotificationClientManager manages all interactions between the
   // system and push notification enabled features.
   std::unique_ptr<PushNotificationClientManager> client_manager_;
+
+  // Stores a mapping of each account's GAIA ID signed into the device to its
+  // context object. This object contains the account's pref service values
+  // pertaining to push notification supported features and the number of times
+  // the given account is signed in across multiple browser states.
+  __strong PushNotificationAccountContextManager* context_manager_;
 };
 
 #endif  // IOS_CHROME_BROWSER_PUSH_NOTIFICATION_PUSH_NOTIFICATION_SERVICE_H_
diff --git a/ios/chrome/browser/push_notification/push_notification_service.mm b/ios/chrome/browser/push_notification/push_notification_service.mm
index 272baf8..1cc868d 100644
--- a/ios/chrome/browser/push_notification/push_notification_service.mm
+++ b/ios/chrome/browser/push_notification/push_notification_service.mm
@@ -7,7 +7,9 @@
 #import "base/strings/string_number_conversions.h"
 #import "base/values.h"
 #import "components/pref_registry/pref_registry_syncable.h"
+#import "ios/chrome/browser/application_context/application_context.h"
 #import "ios/chrome/browser/prefs/pref_names.h"
+#import "ios/chrome/browser/push_notification/push_notification_account_context_manager.h"
 #import "ios/chrome/browser/push_notification/push_notification_client_id.h"
 #import "ios/chrome/browser/push_notification/push_notification_client_manager.h"
 
@@ -16,7 +18,15 @@
 #endif
 
 PushNotificationService::PushNotificationService()
-    : client_manager_(std::make_unique<PushNotificationClientManager>()) {}
+    : PushNotificationService(
+          GetApplicationContext()->GetChromeBrowserStateManager()) {}
+
+PushNotificationService::PushNotificationService(
+    ios::ChromeBrowserStateManager* manager)
+    : client_manager_(std::make_unique<PushNotificationClientManager>()) {
+  context_manager_ = [[PushNotificationAccountContextManager alloc]
+      initWithChromeBrowserStateManager:manager];
+}
 
 PushNotificationService::~PushNotificationService() = default;
 
@@ -25,6 +35,28 @@
   return client_manager_.get();
 }
 
+bool PushNotificationService::DeviceTokenIsSet() const {
+  return false;
+}
+
+void PushNotificationService::RegisterAccount(
+    NSString* account_id,
+    CompletionHandler completion_handler) {
+  if ([context_manager_ addAccount:account_id]) {
+    SetAccountsToDevice(context_manager_.contextMap.allKeys,
+                        completion_handler);
+  }
+}
+
+void PushNotificationService::UnregisterAccount(
+    NSString* account_id,
+    CompletionHandler completion_handler) {
+  if ([context_manager_ removeAccount:account_id]) {
+    SetAccountsToDevice(context_manager_.contextMap.allKeys,
+                        completion_handler);
+  }
+}
+
 void PushNotificationService::RegisterBrowserStatePrefs(
     user_prefs::PrefRegistrySyncable* registry) {
   base::Value::Dict feature_push_notification_permission = base::Value::Dict();
diff --git a/ios/chrome/test/providers/push_notification/test_push_notification.mm b/ios/chrome/test/providers/push_notification/test_push_notification.mm
index 628ccc7..41661499 100644
--- a/ios/chrome/test/providers/push_notification/test_push_notification.mm
+++ b/ios/chrome/test/providers/push_notification/test_push_notification.mm
@@ -27,6 +27,12 @@
   void RegisterDevice(PushNotificationConfiguration* config,
                       void (^completion_handler)(NSError* error)) final;
   void UnregisterDevice(void (^completion_handler)(NSError* error)) final;
+  bool DeviceTokenIsSet() const final;
+
+ protected:
+  // PushNotificationService implementation.
+  void SetAccountsToDevice(NSArray<NSString*>* account_ids,
+                           CompletionHandler completion_handler) final;
 };
 
 void TestPushNotificationService::RegisterDevice(
@@ -59,8 +65,28 @@
       }));
 }
 
+bool TestPushNotificationService::DeviceTokenIsSet() const {
+  return false;
+}
+
+void TestPushNotificationService::SetAccountsToDevice(
+    NSArray<NSString*>* account_ids,
+    void (^completion_handler)(NSError* error)) {
+  // Test implementation does nothing. As a result, the `completion_handler` is
+  // called with a NSFeatureUnsupportedError.
+
+  base::SequencedTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::BindOnce(^() {
+        NSError* error =
+            [NSError errorWithDomain:kTestPushNotificationErrorDomain
+                                code:NSFeatureUnsupportedError
+                            userInfo:nil];
+        completion_handler(error);
+      }));
+}
+
 std::unique_ptr<PushNotificationService> CreatePushNotificationService() {
   return std::make_unique<TestPushNotificationService>();
 }
 }  // namespace provider
-}  // namespace ios
\ No newline at end of file
+}  // namespace ios