chromium/ios/chrome/browser/push_notification/model/push_notification_util.mm

// 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_util.h"

#import <UIKit/UIKit.h>
#import <UserNotifications/UserNotifications.h>

#import "base/metrics/histogram_functions.h"
#import "base/task/sequenced_task_runner.h"
#import "base/types/cxx23_to_underlying.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/public/features/features.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"

namespace {

using PermissionResponseHandler = void (^)(BOOL granted,
                                           BOOL promptedUser,
                                           NSError* error);
using push_notification::SettingsAuthorizationStatus;

using ProvisionalPermissionResponseHandler = void (^)(BOOL granted,
                                                      NSError* error);

// This enum is used to record the action a user performed when prompted to
// allow push notification permissions.
enum class PermissionPromptAction {
  ACCEPTED,
  DECLINED,
  ERROR,
  kMaxValue = ERROR
};

enum class ProvisionalPermissionAction {
  ENABLED,
  INELIGIBLE,
  ERROR,
  kMaxValue = ERROR
};

// The histogram used to record the outcome of the permission prompt.
const char kEnabledPermissionsHistogram[] =
    "IOS.PushNotification.EnabledPermisisons";

// The histogram used to record the outcome of the provisional notifications
// permission.
const char kProvisionalEnabledPermissionsHistogram[] =
    "IOS.PushNotification.Provisional.EnabledPermissions";

// The histogram used to record the user's push notification authorization
// status.
const char kAuthorizationStatusHistogram[] =
    "IOS.PushNotification.NotificationSettingsAuthorizationStatus";

// The histogram used to record users changes to an authorized push notification
// permission status.
const char kNotificationAutorizationStatusChangedToAuthorized[] =
    "IOS.PushNotification.NotificationAutorizationStatusChangedToAuthorized";

// The histogram used to record users changes to a denied push notification
// permission status.
const char kNotificationAutorizationStatusChangedToDenied[] =
    "IOS.PushNotification.NotificationAutorizationStatusChangedToDenied";
}  // namespace

@implementation PushNotificationUtil

+ (void)registerDeviceWithAPNSWithContentNotificationsAvailable:
    (BOOL)contentNotificationAvailability {
  [PushNotificationUtil
      getPermissionSettings:^(UNNotificationSettings* settings) {
        // Logs the users iOS settings' push notification permission status over
        // time.
        [PushNotificationUtil
            logPermissionSettingsMetrics:settings.authorizationStatus];
        if (settings.authorizationStatus == UNAuthorizationStatusAuthorized ||
            contentNotificationAvailability) {
          [[UIApplication sharedApplication] registerForRemoteNotifications];
        }
      }];
}

+ (void)registerActionableNotifications:
    (NSSet<UNNotificationCategory*>*)categories {
  UNUserNotificationCenter* center =
      UNUserNotificationCenter.currentNotificationCenter;
  [center setNotificationCategories:categories];
}

+ (void)requestPushNotificationPermission:
    (PermissionResponseHandler)completionHandler {
  [PushNotificationUtil getPermissionSettings:^(
                            UNNotificationSettings* settings) {
    [PushNotificationUtil requestPushNotificationPermission:completionHandler
                                         permissionSettings:settings];
  }];
}

+ (void)enableProvisionalPushNotificationPermission:
    (ProvisionalPermissionResponseHandler)completionHandler {
  [PushNotificationUtil
      getPermissionSettings:^(UNNotificationSettings* settings) {
        [PushNotificationUtil
            enableProvisionalPushNotificationPermission:completionHandler
                                     permissionSettings:settings];
      }];
}

+ (void)getPermissionSettings:
    (void (^)(UNNotificationSettings* settings))completionHandler {
  UNUserNotificationCenter* center =
      UNUserNotificationCenter.currentNotificationCenter;
  if (!web::WebThread::IsThreadInitialized(web::WebThread::UI)) {
    // In some circumstances, like when the application is going through a cold
    // startup, this function is called before Chrome threads have been
    // initialized. In this case, the function relies on native infrastructure
    // to schedule and execute the callback on the main thread.
    void (^permissionHandler)(UNNotificationSettings*) =
        ^(UNNotificationSettings* settings) {
          dispatch_async(dispatch_get_main_queue(), ^{
            completionHandler(settings);
          });
        };

    [center getNotificationSettingsWithCompletionHandler:permissionHandler];
    return;
  }

  scoped_refptr<base::SequencedTaskRunner> thread;
  // To avoid unnecessarily posting callbacks to the UI thread, the current
  // thread is used if it is suitable for callback execution.
  if (base::SequencedTaskRunner::HasCurrentDefault()) {
    thread = base::SequencedTaskRunner::GetCurrentDefault();
  } else {
    thread = web::GetUIThreadTaskRunner({});
  }

  void (^permissionHandler)(UNNotificationSettings*) =
      ^(UNNotificationSettings* settings) {
        thread->PostTask(FROM_HERE, base::BindOnce(^{
                           completionHandler(settings);
                         }));
      };

  [center getNotificationSettingsWithCompletionHandler:permissionHandler];
}

// This function returns the value stored in the prefService that represents the
// user's iOS settings permission status for push notifications.
+ (UNAuthorizationStatus)getSavedPermissionSettings {
  ApplicationContext* context = GetApplicationContext();
  PrefService* prefService = context->GetLocalState();
  int previousStatus =
      prefService->GetInteger(prefs::kPushNotificationAuthorizationStatus);
  switch (previousStatus) {
    case (int)SettingsAuthorizationStatus::NOTDETERMINED:
      return UNAuthorizationStatusNotDetermined;
    case (int)SettingsAuthorizationStatus::DENIED:
      return UNAuthorizationStatusDenied;
    case (int)SettingsAuthorizationStatus::AUTHORIZED:
      return UNAuthorizationStatusAuthorized;
    case (int)SettingsAuthorizationStatus::PROVISIONAL:
      return UNAuthorizationStatusProvisional;
    case (int)SettingsAuthorizationStatus::EPHEMERAL:
      return UNAuthorizationStatusEphemeral;
    default:
      return UNAuthorizationStatusNotDetermined;
  }
}

// This function updates the value stored in the prefService that represents the
// user's iOS settings permission status for push notifications. If there is a
// difference between the prefService's previous value and the new value, the
// change is logged to UMA.
+ (void)updateAuthorizationStatusPref:(UNAuthorizationStatus)status {
  ApplicationContext* context = GetApplicationContext();
  PrefService* prefService = context->GetLocalState();
  SettingsAuthorizationStatus previousStatus =
      static_cast<SettingsAuthorizationStatus>(
          prefService->GetInteger(prefs::kPushNotificationAuthorizationStatus));
  BOOL changeWasLogged = [PushNotificationUtil
      logChangeInAuthorizationStatusFrom:previousStatus
                                      to:[PushNotificationUtil
                                             getNotificationSettingsStatusFrom:
                                                 status]];
  if (changeWasLogged) {
    prefService->SetInteger(prefs::kPushNotificationAuthorizationStatus,
                            base::to_underlying(status));
  }
}

#pragma mark - Private

// Displays the push notification permission prompt if the user has not decided
// on the application's permission status.
+ (void)requestPushNotificationPermission:(PermissionResponseHandler)completion
                       permissionSettings:(UNNotificationSettings*)settings {
  if (settings.authorizationStatus != UNAuthorizationStatusNotDetermined) {
    if (completion) {
      completion(
          settings.authorizationStatus == UNAuthorizationStatusAuthorized, NO,
          nil);
    }
    return;
  }
  UNAuthorizationOptions options = UNAuthorizationOptionAlert |
                                   UNAuthorizationOptionBadge |
                                   UNAuthorizationOptionSound;
  UNUserNotificationCenter* center =
      UNUserNotificationCenter.currentNotificationCenter;
  [center requestAuthorizationWithOptions:options
                        completionHandler:^(BOOL granted, NSError* error) {
                          [PushNotificationUtil
                              requestAuthorizationResult:completion
                                                 granted:granted
                                                   error:error];
                        }];
}

// Enrolls the user in provisional notifications.
+ (void)enableProvisionalPushNotificationPermission:
            (ProvisionalPermissionResponseHandler)completion
                                 permissionSettings:
                                     (UNNotificationSettings*)settings {
  if (settings.authorizationStatus != UNAuthorizationStatusNotDetermined) {
    if (completion) {
      completion(
          settings.authorizationStatus == UNAuthorizationStatusProvisional,
          nil);
    }
    base::UmaHistogramEnumeration(kProvisionalEnabledPermissionsHistogram,
                                  ProvisionalPermissionAction::INELIGIBLE);
    return;
  }
  UNAuthorizationOptions options =
      UNAuthorizationOptionProvisional | UNAuthorizationOptionBadge |
      UNAuthorizationOptionAlert | UNAuthorizationOptionSound;
  UNUserNotificationCenter* center =
      UNUserNotificationCenter.currentNotificationCenter;
  [center requestAuthorizationWithOptions:options
                        completionHandler:^(BOOL granted, NSError* error) {
                          [PushNotificationUtil
                              requestProvisionalAuthorizationResult:completion
                                                            granted:granted
                                                              error:error];
                        }];
}

// Reports the push notification permission prompt's outcome to metrics.
+ (void)requestAuthorizationResult:(PermissionResponseHandler)completion
                           granted:(BOOL)granted
                             error:(NSError*)error {
  if (granted) {
    [PushNotificationUtil
        registerDeviceWithAPNSWithContentNotificationsAvailable:NO];
    base::UmaHistogramEnumeration(kEnabledPermissionsHistogram,
                                  PermissionPromptAction::ACCEPTED);
  } else if (!error) {
    base::UmaHistogramEnumeration(kEnabledPermissionsHistogram,
                                  PermissionPromptAction::DECLINED);
  } else {
    base::UmaHistogramEnumeration(kEnabledPermissionsHistogram,
                                  PermissionPromptAction::ERROR);
  }

  if (completion) {
    completion(granted, YES, error);
  }
}

// Reports the push notification permission prompt's outcome to metrics and
// registers the device to APNs.
+ (void)requestProvisionalAuthorizationResult:
            (ProvisionalPermissionResponseHandler)completion
                                      granted:(BOOL)granted
                                        error:(NSError*)error {
  if (granted) {
    [PushNotificationUtil
        registerDeviceWithAPNSWithContentNotificationsAvailable:NO];
    base::UmaHistogramEnumeration(kProvisionalEnabledPermissionsHistogram,
                                  ProvisionalPermissionAction::ENABLED);
  } else if (!granted || error) {
    base::UmaHistogramEnumeration(kProvisionalEnabledPermissionsHistogram,
                                  ProvisionalPermissionAction::ERROR);
  }

  if (completion) {
    completion(granted, error);
  }
}

// Converts an UNAuthorizationStatus enum to a
// push_notification::SettingsAuthorizationStatus enum.
+ (SettingsAuthorizationStatus)getNotificationSettingsStatusFrom:
    (UNAuthorizationStatus)status {
  switch (status) {
    case UNAuthorizationStatusNotDetermined:
      // The authorization status is this case when the user has not yet
      // decided to give Chrome push notification permissions.
      return SettingsAuthorizationStatus::NOTDETERMINED;
    case UNAuthorizationStatusDenied:
      // The authorization status is this case when the user has denied to
      // give Chrome push notification permissions via the push
      // notification iOS system permission prompt or by navigating to the iOS
      // settings and manually enabling it.
      return SettingsAuthorizationStatus::DENIED;
    case UNAuthorizationStatusAuthorized:
      // The authorization status is this case when the user has
      // authorized to give Chrome push notification permissions via the
      // push notification iOS system permission prompt or by navigating to the
      // iOS settings and manually enabling it.
      return SettingsAuthorizationStatus::AUTHORIZED;
    case UNAuthorizationStatusProvisional:
      // The authorization status is this case when Chrome has the ability
      // to send provisional push notifications.
      return SettingsAuthorizationStatus::PROVISIONAL;
    case UNAuthorizationStatusEphemeral:
      // The authorization status is this case Chrome can receive
      // notifications for a limited amount of time.
      return SettingsAuthorizationStatus::EPHEMERAL;
  }
}

// Logs the permission status, stored in iOS settings, the user has given for
// whether Chrome can receive push notifications on the device to UMA.
+ (void)logPermissionSettingsMetrics:
    (UNAuthorizationStatus)authorizationStatus {
  SettingsAuthorizationStatus status = [PushNotificationUtil
      getNotificationSettingsStatusFrom:authorizationStatus];
  base::UmaHistogramEnumeration(kAuthorizationStatusHistogram, status);
}

// This function logs the `previousStatus` to UMA if the push notificaiton
// authorization status that is stored in the prefService is differnet from the
// authorization status currently set on the user's device. The function returns
// YES if the function logs to UMA. Otherwise, it returns NO.
+ (BOOL)logChangeInAuthorizationStatusFrom:
            (SettingsAuthorizationStatus)previousStatus
                                        to:(SettingsAuthorizationStatus)status {
  if (previousStatus == status) {
    return NO;
  }

  if (status == SettingsAuthorizationStatus::AUTHORIZED) {
    base::UmaHistogramEnumeration(
        kNotificationAutorizationStatusChangedToAuthorized, previousStatus);
    return YES;
  }

  if (status == SettingsAuthorizationStatus::DENIED) {
    base::UmaHistogramEnumeration(
        kNotificationAutorizationStatusChangedToDenied, previousStatus);
    return YES;
  }

  return NO;
}

@end