chromium/chrome/browser/ash/net/apn_migrator.cc

// 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.

#include "chrome/browser/ash/net/apn_migrator.h"

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/network_config_service.h"
#include "base/containers/contains.h"
#include "base/values.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/ash/components/network/managed_cellular_pref_handler.h"
#include "chromeos/ash/components/network/managed_network_configuration_handler.h"
#include "chromeos/ash/components/network/metrics/cellular_network_metrics_logger.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_metadata_store.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/services/network_config/public/cpp/cros_network_config_util.h"
#include "components/device_event_log/device_event_log.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash {

namespace {

using chromeos::network_config::mojom::ApnPropertiesPtr;
using chromeos::network_config::mojom::ApnState;
using chromeos::network_config::mojom::ApnType;
using chromeos::network_config::mojom::ManagedApnPropertiesPtr;

std::optional<ApnPropertiesPtr> GetPreRevampApnFromDict(
    const base::Value::Dict* cellular_dict,
    const char* key) {
  const base::Value::Dict* apn_dict =
      chromeos::network_config::GetDictionary(cellular_dict, key);
  if (!apn_dict) {
    return std::nullopt;
  }

  // Pre-revamp APNs with empty kAccessPointName will be ignored as they
  // indicate shill tried to send a NULL APN to modemmanager. If shill
  // uses a custom APN or modem DB APN, the kAccessPointName will be
  // non-empty.
  const std::string* access_point_name =
      apn_dict->FindString(::onc::cellular_apn::kAccessPointName);
  if (!access_point_name || access_point_name->empty()) {
    return std::nullopt;
  }

  return chromeos::network_config::GetApnProperties(
      *apn_dict,
      /*is_apn_revamp_enabled=*/false);
}

std::vector<ApnType> GetMigratedApnTypes(
    const ApnPropertiesPtr& pre_revamp_apn) {
  if (pre_revamp_apn->attach.has_value() &&
      !(*pre_revamp_apn->attach).empty()) {
    return {ApnType::kDefault, ApnType::kAttach};
  }
  return {ApnType::kDefault};
}

}  // namespace

ApnMigrator::ApnMigrator(
    ManagedCellularPrefHandler* managed_cellular_pref_handler,
    ManagedNetworkConfigurationHandler* network_configuration_handler,
    NetworkStateHandler* network_state_handler)
    : managed_cellular_pref_handler_(managed_cellular_pref_handler),
      network_configuration_handler_(network_configuration_handler),
      network_state_handler_(network_state_handler) {
  if (!NetworkHandler::IsInitialized()) {
    return;
  }
  // TODO(b/162365553): Only bind this lazily when CrosNetworkConfig is actually
  // used.
  ash::GetNetworkConfigService(
      remote_cros_network_config_.BindNewPipeAndPassReceiver());
  network_state_handler_observer_.Observe(network_state_handler_.get());
}

ApnMigrator::~ApnMigrator() = default;

void ApnMigrator::NetworkListChanged() {
  NetworkStateHandler::NetworkStateList network_list;
  network_state_handler_->GetVisibleNetworkListByType(
      NetworkTypePattern::Cellular(), &network_list);
  for (const NetworkState* network : network_list) {
    // Only attempt to migrate networks known by Shill.
    if (network->IsNonShillCellularNetwork()) {
      continue;
    }

    if (network->iccid().empty()) {
      // If the network somehow has no iccid, don't attempt to migrate.
      NET_LOG(DEBUG) << "Network has no iccid, not migrating: "
                     << network->guid();
      continue;
    }

    // The network has already been updated in Shill with the correct logic
    // depending on if the flag is enabled or disabled. Finish early so we don't
    // redundantly update Shill.
    if (base::Contains(shill_updated_iccids_, network->iccid())) {
      continue;
    }

    bool has_network_been_migrated =
        managed_cellular_pref_handler_->ContainsApnMigratedIccid(
            network->iccid());
    if (!ash::features::IsApnRevampEnabled()) {
      // If the network has been marked as migrated, but the ApnRevamp flag is
      // disabled, the flag was disabled after being enabled. Clear
      // CustomApnList so that Shill knows to use legacy APN selection logic.
      if (has_network_been_migrated) {
        NET_LOG(EVENT) << "Network has been migrated but the revamp flag is "
                       << "disabled. Clearing CustomAPNList: "
                       << network->iccid();
        network_configuration_handler_->ClearShillProperties(
            network->path(), {shill::kCellularCustomApnListProperty},
            base::BindOnce(&ApnMigrator::OnClearPropertiesSuccess,
                           weak_factory_.GetWeakPtr(), network->iccid()),
            base::BindOnce(&ApnMigrator::OnClearPropertiesFailure,
                           weak_factory_.GetWeakPtr(), network->iccid(),
                           network->guid()));
      }
      continue;
    }

    if (!has_network_been_migrated) {
      NET_LOG(EVENT) << "Network has not been migrated, attempting to migrate: "
                     << network->iccid();
      MigrateNetwork(*network);
      continue;
    }

    // The network has already been migrated, either the last time the flag was
    // on, or this time. Send Shill the revamp APN list.
    if (const base::Value::List* custom_apn_list =
            GetNetworkMetadataStore()->GetCustomApnList(network->guid())) {
      if (!ash::features::IsAllowApnModificationPolicyEnabled() ||
          network_configuration_handler_->AllowApnModification()) {
        NET_LOG(EVENT) << "Network has already been migrated, setting with the "
                       << "populated custom APN list: " << network->iccid();
        SetShillCustomApnListForNetwork(*network, custom_apn_list);
      } else if (!network_configuration_handler_->AllowApnModification()) {
        NET_LOG(EVENT) << "Not setting custom APN list as admin has restricted "
                          "use of custom APNs";
        base::Value::List empty_custom_apn_list;
        SetShillCustomApnListForNetwork(*network, &empty_custom_apn_list);
      }
      continue;
    }

    NET_LOG(EVENT) << "Network has already been migrated, setting with the "
                   << "empty custom APN list: " << network->iccid();
    base::Value::List empty_custom_apn_list;
    SetShillCustomApnListForNetwork(*network, &empty_custom_apn_list);
  }
}

void ApnMigrator::OnClearPropertiesSuccess(const std::string iccid) {
  NET_LOG(EVENT) << "Successfully cleared CustomAPNList for: " << iccid;
  shill_updated_iccids_.emplace(iccid);
}

void ApnMigrator::OnClearPropertiesFailure(const std::string iccid,
                                           const std::string guid,
                                           const std::string& error_name) {
  NET_LOG(ERROR) << "Failed to clear CustomAPNList for: " << iccid;
}

void ApnMigrator::SetShillCustomApnListForNetwork(
    const NetworkState& network,
    const base::Value::List* apn_list) {
  network_configuration_handler_->SetProperties(
      network.path(),
      chromeos::network_config::CustomApnListToOnc(network.guid(), apn_list),
      base::BindOnce(&ApnMigrator::OnSetShillCustomApnListSuccess,
                     weak_factory_.GetWeakPtr(), network.iccid()),
      base::BindOnce(&ApnMigrator::OnSetShillCustomApnListFailure,
                     weak_factory_.GetWeakPtr(), network.iccid(),
                     network.guid()));
}

void ApnMigrator::OnSetShillCustomApnListSuccess(const std::string iccid) {
  // Shill has successfully updated the network with the revamp APN list.
  shill_updated_iccids_.emplace(iccid);
  NET_LOG(EVENT) << "ApnMigrator: Update the custom APN "
                 << "list in Shill for network with ICCID: " << iccid;

  // The network has just been migrated.
  if (!managed_cellular_pref_handler_->ContainsApnMigratedIccid(iccid)) {
    CompleteMigrationAttempt(iccid, /*success=*/true);
  }
}

void ApnMigrator::OnSetShillCustomApnListFailure(
    const std::string iccid,
    const std::string guid,
    const std::string& error_name) {
  NET_LOG(ERROR) << "ApnMigrator: Failed to update the custom APN "
                 << "list in Shill for network: " << guid << ": [" << error_name
                 << ']';

  CompleteMigrationAttempt(iccid, /*success=*/false);
}

void ApnMigrator::MigrateNetwork(const NetworkState& network) {
  DCHECK(ash::features::IsApnRevampEnabled());

  // Return early if the network is already in the process of being migrated.
  if (base::Contains(iccids_in_migration_, network.iccid())) {
    NET_LOG(DEBUG) << "Attempting to migrate network that already has a "
                   << "migration in progress, returning early: "
                   << network.iccid();
    return;
  }

  DCHECK(!managed_cellular_pref_handler_->ContainsApnMigratedIccid(
      network.iccid()));

  // Get the pre-revamp APN list.
  const base::Value::List* custom_apn_list =
      GetNetworkMetadataStore()->GetPreRevampCustomApnList(network.guid());

  // If the pre-revamp APN list is empty, set the revamp list as empty and
  // finish the migration.
  if (!custom_apn_list || custom_apn_list->empty()) {
    NET_LOG(EVENT) << "Pre-revamp APN list is empty, sending empty list to "
                   << "Shill: " << network.iccid();
    base::Value::List empty_apn_list;
    SetShillCustomApnListForNetwork(network, &empty_apn_list);
    return;
  }

  // If the pre-revamp APN list is non-empty, get the network's managed
  // properties, to be used for the migration heuristic. This call is
  // asynchronous; mark the ICCID as migrating so that the network won't
  // be attempted to be migrated again while these properties are being fetched.
  iccids_in_migration_.emplace(network.iccid());

  NET_LOG(EVENT) << "Fetching managed properties for network: "
                 << network.iccid();
  network_configuration_handler_->GetManagedProperties(
      LoginState::Get()->primary_user_hash(), network.path(),
      base::BindOnce(&ApnMigrator::OnGetManagedProperties,
                     weak_factory_.GetWeakPtr(), network.iccid(),
                     network.guid()));
}

void ApnMigrator::OnGetManagedProperties(
    std::string iccid,
    std::string guid,
    const std::string& service_path,
    std::optional<base::Value::Dict> properties,
    std::optional<std::string> error) {
  if (error.has_value()) {
    NET_LOG(ERROR) << "Error fetching managed properties for " << iccid
                   << ", error: " << error.value();
    CompleteMigrationAttempt(iccid, /*success=*/false);
    return;
  }

  if (!properties) {
    NET_LOG(ERROR) << "Error fetching managed properties for " << iccid;
    CompleteMigrationAttempt(iccid, /*success=*/false);
    return;
  }

  const NetworkState* network =
      network_state_handler_->GetNetworkStateFromGuid(guid);
  if (!network) {
    NET_LOG(ERROR) << "Network no longer exists: " << guid;
    CompleteMigrationAttempt(iccid, /*success=*/false);
    return;
  }

  // Get the pre-revamp APN list.
  const base::Value::List* custom_apn_list =
      GetNetworkMetadataStore()->GetPreRevampCustomApnList(guid);

  // At this point, the pre-revamp APN list should not be empty. However, there
  // could be the case where the custom APN list was cleared during the
  // GetManagedProperties() call. If so, set the revamp list as empty and finish
  // the migration.
  if (!custom_apn_list || custom_apn_list->empty()) {
    NET_LOG(EVENT) << "Custom APN list cleared during GetManagedProperties() "
                   << "call, setting Shill with empty list for network: "
                   << guid;
    base::Value::List empty_apn_list;
    SetShillCustomApnListForNetwork(*network, &empty_apn_list);
    return;
  }

  ApnPropertiesPtr pre_revamp_custom_apn =
      chromeos::network_config::GetApnProperties(
          custom_apn_list->front().GetDict(),
          /*is_apn_revamp_enabled=*/false);

  NET_LOG(EVENT) << "pre_revamp_custom_apn: "
                 << pre_revamp_custom_apn->access_point_name;

  const base::Value::Dict* cellular_dict =
      chromeos::network_config::GetDictionary(&properties.value(),
                                              ::onc::network_config::kCellular);
  std::optional<ApnPropertiesPtr> last_connected_attach_apn =
      GetPreRevampApnFromDict(cellular_dict,
                              ::onc::cellular::kLastConnectedAttachApnProperty);
  NET_LOG(EVENT) << "last_connected_attach_apn: "
                 << (last_connected_attach_apn.has_value()
                         ? (*last_connected_attach_apn)->access_point_name
                         : "none");

  std::optional<ApnPropertiesPtr> last_connected_default_apn =
      GetPreRevampApnFromDict(
          cellular_dict, ::onc::cellular::kLastConnectedDefaultApnProperty);
  NET_LOG(EVENT) << "last_connected_default_apn: "
                 << (last_connected_default_apn.has_value()
                         ? (*last_connected_default_apn)->access_point_name
                         : "none");

  const bool is_network_managed = network->IsManagedByPolicy();
  if (is_network_managed && !last_connected_default_apn) {
    ManagedApnPropertiesPtr selected_apn =
        chromeos::network_config::GetManagedApnProperties(
            cellular_dict, ::onc::cellular::kAPN);
    if (selected_apn && pre_revamp_custom_apn->access_point_name ==
                            selected_apn->access_point_name->active_value) {
      NET_LOG(EVENT) << "Managed network's selected APN matches the saved "
                     << "custom APN, migrating APN: " << guid;
      // Ensure the APN is enabled when it's migrated so that it's attempted
      // to be used by the new UI.
      pre_revamp_custom_apn->state = ApnState::kEnabled;
      pre_revamp_custom_apn->apn_types =
          GetMigratedApnTypes(pre_revamp_custom_apn);
      CellularNetworkMetricsLogger::LogManagedCustomApnMigrationType(
          CellularNetworkMetricsLogger::ManagedApnMigrationType::
              kMatchesSelectedApn);
      CreateCustomApn(iccid, guid, std::move(pre_revamp_custom_apn));
    } else {
      NET_LOG(EVENT)
          << "Managed network's selected APN doesn't match the saved custom "
          << "APN, setting Shill with empty list for network: " << guid;
      base::Value::List empty_apn_list;
      CellularNetworkMetricsLogger::LogManagedCustomApnMigrationType(
          CellularNetworkMetricsLogger::ManagedApnMigrationType::
              kDoesNotMatchSelectedApn);
      SetShillCustomApnListForNetwork(*network, &empty_apn_list);
    }
    return;
  }

  NET_LOG(EVENT)
      << "Migrating network with non-managed flow, is network managed: "
      << is_network_managed;

  const bool has_last_connected_attach = last_connected_attach_apn.has_value();
  const bool is_last_connected_attach_custom =
      has_last_connected_attach &&
      pre_revamp_custom_apn->access_point_name ==
          (*last_connected_attach_apn)->access_point_name;

  const bool has_last_connected_default =
      last_connected_default_apn.has_value();
  const bool is_last_connected_default_custom =
      has_last_connected_default &&
      pre_revamp_custom_apn->access_point_name ==
          (*last_connected_default_apn)->access_point_name;

  if (is_last_connected_attach_custom && is_last_connected_default_custom) {
    NET_LOG(EVENT) << "Network's last_connected_default_apn and "
                      "last_connected_attach_apn match the "
                   << "saved custom APN, migrating APN: " << guid
                   << " in the Enabled state with Apn types Attach and Default";

    pre_revamp_custom_apn->state = ApnState::kEnabled;
    pre_revamp_custom_apn->apn_types = {ApnType::kAttach, ApnType::kDefault};
    CellularNetworkMetricsLogger::LogUnmanagedCustomApnMigrationType(
        CellularNetworkMetricsLogger::UnmanagedApnMigrationType::
            kMatchesLastConnectedAttachAndDefault);
    CreateCustomApn(iccid, guid, std::move(pre_revamp_custom_apn));
    return;
  }

  if (!has_last_connected_attach && !has_last_connected_default) {
    std::optional<ApnPropertiesPtr> last_good_apn =
        GetPreRevampApnFromDict(cellular_dict, ::onc::cellular::kLastGoodAPN);

    if (last_good_apn && pre_revamp_custom_apn->access_point_name ==
                             (*last_good_apn)->access_point_name) {
      NET_LOG(EVENT) << "Network's last good APN matches the saved "
                     << "custom APN, migrating APN: " << guid
                     << "in the Enabled state";
      // Ensure the APN is enabled when it's migrated so that it's
      // attempted to be used by the new UI.
      pre_revamp_custom_apn->state = ApnState::kEnabled;
      CellularNetworkMetricsLogger::LogUnmanagedCustomApnMigrationType(
          CellularNetworkMetricsLogger::UnmanagedApnMigrationType::
              kMatchesLastGoodApn);
    } else {
      NET_LOG(EVENT) << "Network's last good APN does not match the saved "
                     << "custom APN, migrating APN: " << guid
                     << "in the Disabled state";
      // The custom APN was last unsuccessful in connecting when the flag was
      // off. Preserve the details of the custom APN but with a state of
      // Disabled.
      pre_revamp_custom_apn->state = ApnState::kDisabled;
      CellularNetworkMetricsLogger::LogUnmanagedCustomApnMigrationType(
          CellularNetworkMetricsLogger::UnmanagedApnMigrationType::
              kDoesNotMatchLastGoodApn);
    }
    pre_revamp_custom_apn->apn_types =
        GetMigratedApnTypes(pre_revamp_custom_apn);
    CreateCustomApn(iccid, guid, std::move(pre_revamp_custom_apn));
    return;
  }

  if (!has_last_connected_attach && is_last_connected_default_custom) {
    NET_LOG(EVENT) << "Network has no last connected attach APN but has "
                   << "a last connected default APN that matches the "
                   << "saved custom APN, migrating APN: " << guid
                   << " in the Enabled state with Apn type Default";

    pre_revamp_custom_apn->state = ApnState::kEnabled;
    pre_revamp_custom_apn->apn_types = {ApnType::kDefault};

    CellularNetworkMetricsLogger::LogUnmanagedCustomApnMigrationType(
        CellularNetworkMetricsLogger::UnmanagedApnMigrationType::
            kMatchesLastConnectedDefaultNoLastConnectedAttach);
    CreateCustomApn(iccid, guid, std::move(pre_revamp_custom_apn));
    return;
  }

  if (has_last_connected_attach && is_last_connected_default_custom) {
    NET_LOG(EVENT)
        << "Only last_connected_default_apn matches "
           "pre_revamp_custom_apn, last_connected_attach_apn exists "
           "but does not match pre_revamp_custom_apn, migrating "
           "last_connected_default_apn APN in enabled state with "
           "Default and Attach type, and last_connected_attach_apn APN in "
           "enabled state with Attach type for network: "
        << guid;
    CellularNetworkMetricsLogger::LogUnmanagedCustomApnMigrationType(
        CellularNetworkMetricsLogger::UnmanagedApnMigrationType::
            kMatchesLastConnectedDefaultOnlyAndAttachExists);

    CreateDefaultThenAttachCustomApns(std::move(*last_connected_attach_apn),
                                      std::move(*last_connected_default_apn),
                                      /*can_use_default_apn_as_attach=*/true,
                                      guid, iccid);
    return;
  }

  if (has_last_connected_default && is_last_connected_attach_custom) {
    NET_LOG(EVENT) << "Only last_connected_attach_apn matches "
                      "pre_revamp_custom_apn, last_connected_default exists "
                      "but does not match pre_revamp_custom_apn";
    CellularNetworkMetricsLogger::LogUnmanagedCustomApnMigrationType(
        CellularNetworkMetricsLogger::UnmanagedApnMigrationType::
            kMatchesLastConnectedAttachOnlyAndDefaultExists);
    CreateDefaultThenAttachCustomApns(std::move(*last_connected_attach_apn),
                                      std::move(*last_connected_default_apn),
                                      /*can_use_default_apn_as_attach=*/false,
                                      guid, iccid);
    return;
  }

  NET_LOG(EVENT) << "Network's last connected default APN and attach APN "
                 << "do not match the saved custom APN, migrating APN: " << guid
                 << " in the Disabled state.";
  pre_revamp_custom_apn->state = ApnState::kDisabled;
  pre_revamp_custom_apn->apn_types = GetMigratedApnTypes(pre_revamp_custom_apn);

  CellularNetworkMetricsLogger::LogUnmanagedCustomApnMigrationType(
      CellularNetworkMetricsLogger::UnmanagedApnMigrationType::
          kNoMatchingConnectedApn);
  CreateCustomApn(iccid, guid, std::move(pre_revamp_custom_apn));
}

void ApnMigrator::CreateDefaultThenAttachCustomApns(
    chromeos::network_config::mojom::ApnPropertiesPtr attach_apn,
    chromeos::network_config::mojom::ApnPropertiesPtr default_apn,
    bool can_use_default_apn_as_attach,
    const std::string& guid,
    const std::string& iccid) {
  DCHECK(!attach_apn.is_null());
  DCHECK(!default_apn.is_null());
  NET_LOG(EVENT) << "Migrating default_apn: " << default_apn->access_point_name
                 << " in the enabled state; default_apn is also type attach: "
                 << can_use_default_apn_as_attach
                 << ". Then, migrate attach_apn: "
                 << attach_apn->access_point_name
                 << " in the enabled state for Network with guid: " << guid
                 << " and iccid: " << iccid;

  attach_apn->state = ApnState::kEnabled;
  attach_apn->apn_types = {ApnType::kAttach};

  default_apn->state = ApnState::kEnabled;
  if (can_use_default_apn_as_attach) {
    default_apn->apn_types = {ApnType::kAttach, ApnType::kDefault};
  } else {
    default_apn->apn_types = {ApnType::kDefault};
  }

  // Migrate default APN first, then attach APN in case both are to
  // be enabled; enabled default APN must be created before enabled attach APN.
  // See b/303565348.
  // Also, |CompleteMigrationAttempt()| should only be called
  // once, when both APNs have been migrated.
  auto create_custom_attach_apn_callback =
      base::BindOnce(&ApnMigrator::CreateCustomApn, weak_factory_.GetWeakPtr(),
                     iccid, guid, std::move(attach_apn), std::nullopt);

  auto on_create_default_apn_callback =
      base::BindOnce(
          [](base::OnceClosure success_callback, const std::string& guid,
             bool success) {
            if (success) {
              NET_LOG(EVENT)
                  << "ApnMigrator: Succeeded migrating custom default APN "
                     "for network guid in the enabled state: "
                  << guid
                  << ", now migrating different custom attach APN in enabled "
                     "state.";
              std::move(success_callback).Run();
              return;
            }
            NET_LOG(ERROR)
                << "ApnMigrator: Failed to create custom default APN for "
                   "network of guid: "
                << guid
                << ", so will not proceed to create associated custom attach "
                   "APN";
          },
          std::move(create_custom_attach_apn_callback), guid);

  CreateCustomApn(iccid, guid, std::move(default_apn),
                  std::move(on_create_default_apn_callback));
}

void ApnMigrator::CreateCustomApn(
    const std::string& iccid,
    const std::string& network_guid,
    chromeos::network_config::mojom::ApnPropertiesPtr apn,
    std::optional<base::OnceCallback<void(bool)>> success_callback) {
  remote_cros_network_config_->CreateCustomApn(
      network_guid, std::move(apn),
      success_callback ? std::move(*success_callback)
                       : base::BindOnce(&ApnMigrator::CompleteMigrationAttempt,
                                        weak_factory_.GetWeakPtr(), iccid));
}

void ApnMigrator::CompleteMigrationAttempt(const std::string& iccid,
                                           bool success) {
  iccids_in_migration_.erase(iccid);

  if (success) {
    NET_LOG(EVENT) << "ApnMigrator: Mark network with ICCID: " << iccid
                   << " as migrated";
    managed_cellular_pref_handler_->AddApnMigratedIccid(iccid);
  }
}

NetworkMetadataStore* ApnMigrator::GetNetworkMetadataStore() {
  if (network_metadata_store_for_testing_) {
    return network_metadata_store_for_testing_;
  }

  return NetworkHandler::Get()->network_metadata_store();
}

}  // namespace ash