chromium/remoting/ios/app/notification_presenter.mm

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "remoting/ios/app/notification_presenter.h"

#import <MaterialComponents/MaterialDialogs.h>

#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/strings/sys_string_conversions.h"
#include "remoting/base/string_resources.h"
#include "remoting/client/chromoting_client_runtime.h"
#import "remoting/ios/app/notification_dialog_view_controller.h"
#include "remoting/ios/app/view_utils.h"
#import "remoting/ios/facade/remoting_authentication.h"
#import "remoting/ios/facade/remoting_service.h"
#import "remoting/ios/persistence/remoting_preferences.h"
#include "ui/base/l10n/l10n_util.h"

namespace remoting {

namespace {

// This is to make sure notification shows up after the user status toast (see
// UserStatusPresenter).
// TODO(yuweih): Chain this class with UserStatusPresenter on the
// kUserDidUpdate event.
constexpr base::TimeDelta kFetchNotificationDelay = base::Seconds(2);

// The "Don't show again" checkbox only appears if the notification has been
// shown for at least |kTimesShownRequiredToAllowSilencing| times. Note that
// this means the checkbox shows up starting from the
// (|kTimesShownRequiredToAllowSilencing| + 1)th app launch.
constexpr unsigned int kTimesShownRequiredToAllowSilencing = 2u;

enum NotificationUiState : unsigned int {
  NOT_SHOWN = 0,
  SHOWN = 1,
  SILENCED = 2,
};

NotificationUiState NSNumberToUiState(NSNumber* number) {
  return number ? static_cast<NotificationUiState>(number.unsignedIntValue)
                : NOT_SHOWN;
}

NSNumber* UiStateToNSNumber(NotificationUiState state) {
  return @(static_cast<unsigned int>(state));
}

}  // namespace

// static
NotificationPresenter* NotificationPresenter::GetInstance() {
  static base::NoDestructor<NotificationPresenter> instance;
  return instance.get();
}

NotificationPresenter::NotificationPresenter()
    : notification_client_(
          ChromotingClientRuntime::GetInstance()->network_task_runner()) {}

void NotificationPresenter::Start() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!user_update_observer_);

  FetchNotification();
  user_update_observer_ = [NSNotificationCenter.defaultCenter
      addObserverForName:kUserDidUpdate
                  object:nil
                   queue:nil
              usingBlock:^(NSNotification*) {
                // This implicitly captures |this|, but should be fine since
                // |NotificationPresenter| is singleton.
                FetchNotification();
              }];
}

void NotificationPresenter::FetchNotification() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (state_ == State::FETCHED) {
    return;
  }
  UserInfo* user = RemotingService.instance.authentication.user;
  std::string user_email = [user isAuthenticated]
                               ? base::SysNSStringToUTF8(user.userEmail)
                               : std::string();

  state_ = State::FETCHING;
  // If FetchNotification() is called when the timer is already running, this
  // will restart the timer with the new user email.
  fetch_notification_timer_.Start(
      FROM_HERE, kFetchNotificationDelay,
      base::BindOnce(
          [](NotificationPresenter* that, const std::string& user_email) {
            that->notification_client_.GetNotification(
                user_email,
                base::BindOnce(&NotificationPresenter::OnNotificationFetched,
                               base::Unretained(that)));
          },
          base::Unretained(this), user_email));
}

void NotificationPresenter::OnNotificationFetched(
    std::optional<NotificationMessage> notification) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK_EQ(State::FETCHING, state_);

  if (!notification) {
    // No notification for this user. Need to check again when user changes.
    state_ = State::NOT_FETCHED;
    return;
  }

  state_ = State::FETCHED;

  DCHECK(!notification->message_id.empty());

  std::string last_seen_message_id =
      base::SysNSStringToUTF8([RemotingPreferences.instance
          objectForFlag:RemotingFlagLastSeenNotificationMessageId]);
  NotificationUiState ui_state = NSNumberToUiState([RemotingPreferences.instance
      objectForFlag:RemotingFlagNotificationUiState]);
  if (notification->allow_silence && ui_state == SILENCED &&
      last_seen_message_id == notification->message_id) {
    return;
  }

  if (last_seen_message_id != notification->message_id) {
    [RemotingPreferences.instance
        setObject:base::SysUTF8ToNSString(notification->message_id)
          forFlag:RemotingFlagLastSeenNotificationMessageId];
    ui_state = NOT_SHOWN;
    [RemotingPreferences.instance setObject:UiStateToNSNumber(ui_state)
                                    forFlag:RemotingFlagNotificationUiState];
    [RemotingPreferences.instance setObject:@(0u)
                                    forFlag:RemotingFlagNotificationShownTimes];
    [RemotingPreferences.instance synchronizeFlags];
  }

  NSNumber* notificationShownTimesNsNumber = [RemotingPreferences.instance
      objectForFlag:RemotingFlagNotificationShownTimes];
  unsigned int notification_shown_times =
      notificationShownTimesNsNumber
          ? notificationShownTimesNsNumber.unsignedIntValue
          : 0u;

  BOOL allowSilence =
      notification->allow_silence && ui_state == SHOWN &&
      notification_shown_times >= kTimesShownRequiredToAllowSilencing;
  NotificationDialogViewController* dialogVc =
      [[NotificationDialogViewController alloc]
          initWithNotificationMessage:*notification
                         allowSilence:allowSilence];
  [dialogVc presentOnTopVCWithCompletion:^(BOOL isSilenced) {
    NotificationUiState new_ui_state = isSilenced ? SILENCED : SHOWN;
    [RemotingPreferences.instance setObject:UiStateToNSNumber(new_ui_state)
                                    forFlag:RemotingFlagNotificationUiState];
    [RemotingPreferences.instance setObject:@(notification_shown_times + 1)
                                    forFlag:RemotingFlagNotificationShownTimes];
    [RemotingPreferences.instance synchronizeFlags];
  }];
}

}  // namespace remoting