<filename>chrome/browser/chromeos/policy/affiliated_invalidation_service_provider_impl.cc
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/chromeos/policy/affiliated_invalidation_service_provider_impl.h"

#include <vector>

#include "base/bind.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/task/post_task.h"
#include "base/timer/timer.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part_chromeos.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/chromeos/policy/browser_policy_connector_chromeos.h"
#include "chrome/browser/device_identity/device_identity_provider.h"
#include "chrome/browser/device_identity/device_oauth2_token_service_factory.h"
#include "chrome/browser/invalidation/profile_invalidation_provider_factory.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/gcm_driver/instance_id/instance_id_driver.h"
#include "components/invalidation/impl/fcm_invalidation_service.h"
#include "components/invalidation/impl/fcm_network_handler.h"
#include "components/invalidation/impl/per_user_topic_subscription_manager.h"
#include "components/invalidation/impl/profile_invalidation_provider.h"
#include "components/invalidation/public/identity_provider.h"
#include "components/invalidation/public/invalidation_handler.h"
#include "components/invalidation/public/invalidation_service.h"
#include "components/invalidation/public/invalidator_state.h"
#include "components/policy/core/common/cloud/cloud_policy_constants.h"
#include "components/user_manager/user.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_service.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"

namespace policy {

namespace {

// Currently InvalidationService sends TRANSIENT_INVALIDATION_ERROR in case if
// topic failing to register, but at the same time InvalidationService has retry
// logic. In case if retry is successful and all the topic succeeded to
// register, InvalidationService sends INVALIDATION_ENABLED. So
// InvalidationServiceObserver should give InvalidationService a chance to
// reregister topic, and after some time if INVALIDATION_ENABLED is not being
// received, manually check with delayed task. If after
// |kCheckInvalidatorStateDelay|, InvalidationService is still in
// TRANSIENT_INVALIDATION_ERROR state, disconnect from it and try to reregister
// all the topics.
constexpr base::TimeDelta kCheckInvalidatorStateDelay =
    base::TimeDelta::FromMinutes(3);

// After reregistering all the topics |kTransientErrorDisconnectLimit| number of
// times, when InvalidationService is failing due to
// TRANSIENT_INVALIDATION_ERROR, stop disconnecting, and let registered topics
// to keep being registered and failing topics to keep being failed. This is the
// best effort behaviour. If |kTransientErrorDisconnectLimit| is too high, at
// some point Firebase Cloud Message will start throttling register request for
// this client.
constexpr int kTransientErrorDisconnectLimit = 3;

invalidation::ProfileInvalidationProvider* GetInvalidationProvider(
    Profile* profile) {
  return invalidation::ProfileInvalidationProviderFactory::GetForProfile(
      profile);
}

}  // namespace

class AffiliatedInvalidationServiceProviderImpl::InvalidationServiceObserver
    : public invalidation::InvalidationHandler {
 public:
  explicit InvalidationServiceObserver(
      AffiliatedInvalidationServiceProviderImpl* parent,
      invalidation::InvalidationService* invalidation_service);
  ~InvalidationServiceObserver() override;

  invalidation::InvalidationService* GetInvalidationService();
  void CheckInvalidatorState();
  bool IsServiceConnected() const;

  // public invalidation::InvalidationHandler:
  void OnInvalidatorStateChange(invalidation::InvalidatorState state) override;
  void OnIncomingInvalidation(
      const invalidation::TopicInvalidationMap& invalidation_map) override;
  std::string GetOwnerName() const override;

 private:
  AffiliatedInvalidationServiceProviderImpl* parent_;
  invalidation::InvalidationService* invalidation_service_;
  bool is_service_connected_;
  bool is_observer_ready_;
  base::OneShotTimer transient_error_retry_timer_;

  // The number of times TRANSIENT_INVALIDATION_ERROR should cause disconnect.
  int transient_error_disconnect_limit_ = kTransientErrorDisconnectLimit;

  DISALLOW_COPY_AND_ASSIGN(InvalidationServiceObserver);
};

AffiliatedInvalidationServiceProviderImpl::InvalidationServiceObserver::
    InvalidationServiceObserver(
        AffiliatedInvalidationServiceProviderImpl* parent,
        invalidation::InvalidationService* invalidation_service)
    : parent_(parent),
      invalidation_service_(invalidation_service),
      is_service_connected_(false),
      is_observer_ready_(false) {
  invalidation_service_->RegisterInvalidationHandler(this);
  is_service_connected_ = invalidation_service->GetInvalidatorState() ==
                          invalidation::INVALIDATIONS_ENABLED;
  is_observer_ready_ = true;
}

AffiliatedInvalidationServiceProviderImpl::InvalidationServiceObserver::
    ~InvalidationServiceObserver() {
  is_observer_ready_ = false;
  invalidation_service_->UnregisterInvalidationHandler(this);
}

invalidation::InvalidationService* AffiliatedInvalidationServiceProviderImpl::
    InvalidationServiceObserver::GetInvalidationService() {
  return invalidation_service_;
}

bool AffiliatedInvalidationServiceProviderImpl::InvalidationServiceObserver::
    IsServiceConnected() const {
  return is_service_connected_;
}

void AffiliatedInvalidationServiceProviderImpl::InvalidationServiceObserver::
    CheckInvalidatorState() {
  // Treat TRANSIENT_INVALIDATION_ERROR as an error, and disconnect the service
  // if connected.
  DCHECK(is_observer_ready_);
  DCHECK(invalidation_service_);
  DCHECK(parent_);

  invalidation::InvalidatorState state =
      invalidation_service_->GetInvalidatorState();
  bool is_service_connected = (state == invalidation::INVALIDATIONS_ENABLED);

  if (is_service_connected_ == is_service_connected)
    return;

  if (state == invalidation::TRANSIENT_INVALIDATION_ERROR) {
    // Do not cause disconnect if the number of disconnections caused by
    // TRANSIENT_INVALIDATION_ERROR is more than the limit.
    if (!transient_error_disconnect_limit_)
      return;
    --transient_error_disconnect_limit_;
  }

  is_service_connected_ = is_service_connected;
  if (is_service_connected_)
    parent_->OnInvalidationServiceConnected(invalidation_service_);
  else
    parent_->OnInvalidationServiceDisconnected(invalidation_service_);
}

void AffiliatedInvalidationServiceProviderImpl::InvalidationServiceObserver::
    OnInvalidatorStateChange(invalidation::InvalidatorState state) {
  if (!is_observer_ready_)
    return;

  // TODO(crbug/1007287): Handle 3 different states of
  // AffiliatedInvalidationServiceProvider properly.
  if (is_service_connected_) {
    // If service is connected, do NOT notify parent in case:
    //   * state == INVALIDATIONS_ENABLED
    //   * state == TRANSIENT_INVALIDATION_ERROR, hopefully will be resolved by
    //     InvalidationService, if not InvalidationService should notify again
    //     with another more severe state.
    bool should_notify = (state != invalidation::INVALIDATIONS_ENABLED &&
                          state != invalidation::TRANSIENT_INVALIDATION_ERROR);

    if (should_notify) {
      is_service_connected_ = false;
      parent_->OnInvalidationServiceDisconnected(invalidation_service_);
    } else if (state == invalidation::TRANSIENT_INVALIDATION_ERROR) {
      transient_error_retry_timer_.Stop();
      transient_error_retry_timer_.Start(
          FROM_HERE, kCheckInvalidatorStateDelay,
          base::BindOnce(&AffiliatedInvalidationServiceProviderImpl::
                             InvalidationServiceObserver::CheckInvalidatorState,
                         base::Unretained(this)));
    }
  } else {
    // If service is disconnected, ONLY notify parent in case:
    //   * state == INVALIDATIONS_ENABLED
    bool should_notify = (state == invalidation::INVALIDATIONS_ENABLED);
    if (should_notify) {
      is_service_connected_ = true;
      parent_->OnInvalidationServiceConnected(invalidation_service_);
    }
  }
}

void AffiliatedInvalidationServiceProviderImpl::InvalidationServiceObserver::
    OnIncomingInvalidation(
        const invalidation::TopicInvalidationMap& invalidation_map) {}

std::string AffiliatedInvalidationServiceProviderImpl::
    InvalidationServiceObserver::GetOwnerName() const {
  return "AffiliatedInvalidationService";
}

AffiliatedInvalidationServiceProviderImpl::
    AffiliatedInvalidationServiceProviderImpl()
    : invalidation_service_(nullptr), consumer_count_(0), is_shut_down_(false) {
  // The AffiliatedInvalidationServiceProviderImpl should be created before any
  // user Profiles.
  DCHECK(g_browser_process->profile_manager()->GetLoadedProfiles().empty());

  // Subscribe to notification about new user profiles becoming available.
  session_observation_.Observe(session_manager::SessionManager::Get());
}

AffiliatedInvalidationServiceProviderImpl::
    ~AffiliatedInvalidationServiceProviderImpl() {
  // Verify that the provider was shut down first.
  DCHECK(is_shut_down_);
}

void AffiliatedInvalidationServiceProviderImpl::OnUserProfileLoaded(
    const AccountId& account_id) {
  DCHECK(!is_shut_down_);
  Profile* profile =
      chromeos::ProfileHelper::Get()->GetProfileByAccountId(account_id);
  invalidation::ProfileInvalidationProvider* invalidation_provider =
      GetInvalidationProvider(profile);
  if (!invalidation_provider) {
    // If the Profile does not support invalidation (e.g. guest, incognito),
    // ignore it.
    return;
  }
  const user_manager::User* user =
      chromeos::ProfileHelper::Get()->GetUserByProfile(profile);
  if (!user || !user->IsAffiliated()) {
    // If the Profile belongs to a user who is not affiliated on the device,
    // ignore it.
    return;
  }

  // Create a state observer for the user's invalidation service.
  invalidation::InvalidationService* invalidation_service;
  invalidation_service =
      invalidation_provider->GetInvalidationServiceForCustomSender(
          policy::kPolicyFCMInvalidationSenderID);
  profile_invalidation_service_observers_.push_back(
      std::make_unique<InvalidationServiceObserver>(this,
                                                    invalidation_service));

  if (profile_invalidation_service_observers_.back()->IsServiceConnected()) {
    // If the invalidation service is connected, check whether to switch to it.
    OnInvalidationServiceConnected(invalidation_service);
  }
}

void AffiliatedInvalidationServiceProviderImpl::RegisterConsumer(
    Consumer* consumer) {
  if (consumers_.HasObserver(consumer) || is_shut_down_)
    return;

  consumers_.AddObserver(consumer);
  ++consumer_count_;

  if (invalidation_service_)
    consumer->OnInvalidationServiceSet(invalidation_service_);
  else if (consumer_count_ == 1)
    FindConnectedInvalidationService();
}

void AffiliatedInvalidationServiceProviderImpl::UnregisterConsumer(
    Consumer* consumer) {
  if (!consumers_.HasObserver(consumer))
    return;

  consumers_.RemoveObserver(consumer);
  --consumer_count_;

  if (invalidation_service_ && consumer_count_ == 0) {
    invalidation_service_ = nullptr;
    DestroyDeviceInvalidationService();
  }
}

void AffiliatedInvalidationServiceProviderImpl::Shutdown() {
  is_shut_down_ = true;

  session_observation_.Reset();
  profile_invalidation_service_observers_.clear();
  device_invalidation_service_observer_.reset();

  if (invalidation_service_) {
    invalidation_service_ = nullptr;
    // Explicitly notify consumers that the invalidation service they were using
    // is no longer available.
    SetInvalidationService(nullptr);
  }

  DestroyDeviceInvalidationService();
}

invalidation::InvalidationService*
AffiliatedInvalidationServiceProviderImpl::GetDeviceInvalidationServiceForTest()
    const {
  return device_invalidation_service_.get();
}

void AffiliatedInvalidationServiceProviderImpl::OnInvalidationServiceConnected(
    invalidation::InvalidationService* invalidation_service) {
  DCHECK(!is_shut_down_);

  if (consumer_count_ == 0) {
    // If there are no consumers, no invalidation service is required.
    return;
  }

  if (!device_invalidation_service_) {
    // The lack of a device-global invalidation service implies that another
    // connected invalidation service is being made available to consumers
    // already. There is no need to switch from that to the service which just
    // connected.
    return;
  }

  // Make the invalidation service that just connected available to consumers.
  invalidation_service_ = nullptr;
  SetInvalidationService(invalidation_service);

  if (invalidation_service_ && device_invalidation_service_ &&
      invalidation_service_ != device_invalidation_service_.get()) {
    // If a different invalidation service is being made available to consumers
    // now, destroy the device-global one.
    DestroyDeviceInvalidationService();
  }
}

void AffiliatedInvalidationServiceProviderImpl::
    OnInvalidationServiceDisconnected(
        invalidation::InvalidationService* invalidation_service) {
  DCHECK(!is_shut_down_);

  if (invalidation_service != invalidation_service_) {
    // If the invalidation service which disconnected was not being made
    // available to consumers, return.
    return;
  }

  // The invalidation service which disconnected was being made available to
  // consumers. Stop making it available.
  DCHECK(consumer_count_);
  invalidation_service_ = nullptr;

  // Try to make another invalidation service available to consumers.
  FindConnectedInvalidationService();

  // If no other connected invalidation service was found, explicitly notify
  // consumers that the invalidation service they were using is no longer
  // available.
  if (!invalidation_service_)
    SetInvalidationService(nullptr);
}

void AffiliatedInvalidationServiceProviderImpl::
    FindConnectedInvalidationService() {
  DCHECK(!invalidation_service_);
  DCHECK(consumer_count_);
  DCHECK(!is_shut_down_);

  for (const auto& observer : profile_invalidation_service_observers_) {
    if (observer->IsServiceConnected()) {
      // If a connected invalidation service belonging to an affiliated
      // logged-in user is found, make it available to consumers.
      DestroyDeviceInvalidationService();
      SetInvalidationService(observer->GetInvalidationService());
      return;
    }
  }

  if (!device_invalidation_service_) {
    // If no other connected invalidation service was found and no device-global
    // invalidation service exists, create one.
    device_invalidation_service_ = InitializeDeviceInvalidationService();
    device_invalidation_service_observer_.reset(new InvalidationServiceObserver(
        this, device_invalidation_service_.get()));
  }

  if (device_invalidation_service_observer_->IsServiceConnected()) {
    // If the device-global invalidation service is connected already, make it
    // available to consumers immediately. Otherwise, the invalidation service
    // will be made available to clients when it successfully connects.
    OnInvalidationServiceConnected(device_invalidation_service_.get());
  }
}

void AffiliatedInvalidationServiceProviderImpl::SetInvalidationService(
    invalidation::InvalidationService* invalidation_service) {
  DCHECK(!invalidation_service_);
  invalidation_service_ = invalidation_service;
  for (auto& observer : consumers_)
    observer.OnInvalidationServiceSet(invalidation_service_);
}

void AffiliatedInvalidationServiceProviderImpl::
    DestroyDeviceInvalidationService() {
  device_invalidation_service_observer_.reset();
  device_invalidation_service_.reset();
  device_identity_provider_.reset();
  device_instance_id_driver_.reset();
}

std::unique_ptr<invalidation::InvalidationService>
AffiliatedInvalidationServiceProviderImpl::
    InitializeDeviceInvalidationService() {
  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory;
  if (g_browser_process->system_network_context_manager()) {
    // system_network_context_manager() can be null during unit tests.
    url_loader_factory = g_browser_process->system_network_context_manager()
                             ->GetSharedURLLoaderFactory();
    DCHECK(url_loader_factory);
  }

  device_identity_provider_ = std::make_unique<DeviceIdentityProvider>(
      DeviceOAuth2TokenServiceFactory::Get());

  device_instance_id_driver_ = std::make_unique<instance_id::InstanceIDDriver>(
      g_browser_process->gcm_driver());

  DCHECK(device_instance_id_driver_);
  auto device_invalidation_service =
      std::make_unique<invalidation::FCMInvalidationService>(
          device_identity_provider_.get(),
          base::BindRepeating(&invalidation::FCMNetworkHandler::Create,
                              g_browser_process->gcm_driver(),
                              device_instance_id_driver_.get()),
          base::BindRepeating(
              &invalidation::PerUserTopicSubscriptionManager::Create,
              device_identity_provider_.get(), g_browser_process->local_state(),
              base::RetainedRef(url_loader_factory)),
          device_instance_id_driver_.get(), g_browser_process->local_state(),
          policy::kPolicyFCMInvalidationSenderID);
  device_invalidation_service->Init();
  return device_invalidation_service;
}

}  // namespace policy
