// Copyright (c) 2012 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 "net/android/network_change_notifier_delegate_android.h"

#include "base/android/jni_array.h"
#include "base/logging.h"
#include "jni/NetworkChangeNotifier_jni.h"
#include "net/android/network_change_notifier_android.h"

using base::android::JavaParamRef;
using base::android::JavaRef;
using base::android::ScopedJavaLocalRef;

namespace net {

namespace {

// Converts a Java side connection type (integer) to
// the native side NetworkChangeNotifier::ConnectionType.
NetworkChangeNotifier::ConnectionType ConvertConnectionType(
    jint connection_type) {
  switch (connection_type) {
    case NetworkChangeNotifier::CONNECTION_UNKNOWN:
    case NetworkChangeNotifier::CONNECTION_ETHERNET:
    case NetworkChangeNotifier::CONNECTION_WIFI:
    case NetworkChangeNotifier::CONNECTION_2G:
    case NetworkChangeNotifier::CONNECTION_3G:
    case NetworkChangeNotifier::CONNECTION_4G:
    case NetworkChangeNotifier::CONNECTION_NONE:
    case NetworkChangeNotifier::CONNECTION_BLUETOOTH:
      break;
    default:
      NOTREACHED() << "Unknown connection type received: " << connection_type;
      return NetworkChangeNotifier::CONNECTION_UNKNOWN;
  }
  return static_cast<NetworkChangeNotifier::ConnectionType>(connection_type);
}

// Converts a Java side connection type (integer) to
// the native side NetworkChangeNotifier::ConnectionType.
NetworkChangeNotifier::ConnectionSubtype ConvertConnectionSubtype(
    jint subtype) {
  DCHECK(subtype >= 0 && subtype <= NetworkChangeNotifier::SUBTYPE_LAST);

  return static_cast<NetworkChangeNotifier::ConnectionSubtype>(subtype);
}

}  // namespace

// static
void NetworkChangeNotifierDelegateAndroid::JavaLongArrayToNetworkMap(
    JNIEnv* env,
    const JavaRef<jlongArray>& long_array,
    NetworkMap* network_map) {
  std::vector<int64_t> int64_list;
  base::android::JavaLongArrayToInt64Vector(env, long_array, &int64_list);
  network_map->clear();
  for (auto i = int64_list.begin(); i != int64_list.end(); ++i) {
    NetworkChangeNotifier::NetworkHandle network_handle = *i;
    CHECK(++i != int64_list.end());
    (*network_map)[network_handle] = static_cast<ConnectionType>(*i);
  }
}

NetworkChangeNotifierDelegateAndroid::NetworkChangeNotifierDelegateAndroid()
    : observers_(new base::ObserverListThreadSafe<Observer>()),
      java_network_change_notifier_(Java_NetworkChangeNotifier_init(
          base::android::AttachCurrentThread())),
      register_network_callback_failed_(
          Java_NetworkChangeNotifier_registerNetworkCallbackFailed(
              base::android::AttachCurrentThread(),
              java_network_change_notifier_)) {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_addNativeObserver(
      env, java_network_change_notifier_, reinterpret_cast<intptr_t>(this));
  SetCurrentConnectionType(
      ConvertConnectionType(Java_NetworkChangeNotifier_getCurrentConnectionType(
          env, java_network_change_notifier_)));
  SetCurrentMaxBandwidth(
      NetworkChangeNotifierAndroid::GetMaxBandwidthMbpsForConnectionSubtype(
          GetCurrentConnectionSubtype()));
  SetCurrentDefaultNetwork(Java_NetworkChangeNotifier_getCurrentDefaultNetId(
      env, java_network_change_notifier_));
  NetworkMap network_map;
  ScopedJavaLocalRef<jlongArray> networks_and_types =
      Java_NetworkChangeNotifier_getCurrentNetworksAndTypes(
          env, java_network_change_notifier_);
  JavaLongArrayToNetworkMap(env, networks_and_types, &network_map);
  SetCurrentNetworksAndTypes(network_map);
}

NetworkChangeNotifierDelegateAndroid::~NetworkChangeNotifierDelegateAndroid() {
  DCHECK(thread_checker_.CalledOnValidThread());
  observers_->AssertEmpty();
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_removeNativeObserver(
      env, java_network_change_notifier_, reinterpret_cast<intptr_t>(this));
}

NetworkChangeNotifier::ConnectionType
NetworkChangeNotifierDelegateAndroid::GetCurrentConnectionType() const {
  base::AutoLock auto_lock(connection_lock_);
  return connection_type_;
}

NetworkChangeNotifier::ConnectionSubtype
NetworkChangeNotifierDelegateAndroid::GetCurrentConnectionSubtype() const {
  DCHECK(thread_checker_.CalledOnValidThread());
  return ConvertConnectionSubtype(
      Java_NetworkChangeNotifier_getCurrentConnectionSubtype(
          base::android::AttachCurrentThread(), java_network_change_notifier_));
}

void NetworkChangeNotifierDelegateAndroid::
    GetCurrentMaxBandwidthAndConnectionType(
        double* max_bandwidth_mbps,
        ConnectionType* connection_type) const {
  base::AutoLock auto_lock(connection_lock_);
  *connection_type = connection_type_;
  *max_bandwidth_mbps = connection_max_bandwidth_;
}

NetworkChangeNotifier::ConnectionType
NetworkChangeNotifierDelegateAndroid::GetNetworkConnectionType(
    NetworkChangeNotifier::NetworkHandle network) const {
  base::AutoLock auto_lock(connection_lock_);
  auto network_entry = network_map_.find(network);
  if (network_entry == network_map_.end())
    return ConnectionType::CONNECTION_UNKNOWN;
  return network_entry->second;
}

NetworkChangeNotifier::NetworkHandle
NetworkChangeNotifierDelegateAndroid::GetCurrentDefaultNetwork() const {
  base::AutoLock auto_lock(connection_lock_);
  return default_network_;
}

void NetworkChangeNotifierDelegateAndroid::GetCurrentlyConnectedNetworks(
    NetworkList* network_list) const {
  network_list->clear();
  base::AutoLock auto_lock(connection_lock_);
  for (auto i : network_map_)
    network_list->push_back(i.first);
}

void NetworkChangeNotifierDelegateAndroid::NotifyConnectionTypeChanged(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint new_connection_type,
    jlong default_netid) {
  DCHECK(thread_checker_.CalledOnValidThread());
  const ConnectionType actual_connection_type = ConvertConnectionType(
      new_connection_type);
  SetCurrentConnectionType(actual_connection_type);
  NetworkHandle default_network = default_netid;
  if (default_network != GetCurrentDefaultNetwork()) {
    SetCurrentDefaultNetwork(default_network);
    bool default_exists;
    {
      base::AutoLock auto_lock(connection_lock_);
      // |default_network| may be an invalid value (i.e. -1) in cases where
      // the device is disconnected or when run on Android versions prior to L,
      // in which case |default_exists| will correctly be false and no
      // OnNetworkMadeDefault notification will be sent.
      default_exists = network_map_.find(default_network) != network_map_.end();
    }
    // Android Lollipop had race conditions where CONNECTIVITY_ACTION intents
    // were sent out before the network was actually made the default.
    // Delay sending the OnNetworkMadeDefault notification until we are
    // actually notified that the network connected in NotifyOfNetworkConnect.
    if (default_exists) {
      observers_->Notify(FROM_HERE, &Observer::OnNetworkMadeDefault,
                         default_network);
    }
  }
  observers_->Notify(FROM_HERE, &Observer::OnConnectionTypeChanged);
}

jint NetworkChangeNotifierDelegateAndroid::GetConnectionType(JNIEnv*,
                                                             jobject) const {
  DCHECK(thread_checker_.CalledOnValidThread());
  return GetCurrentConnectionType();
}

void NetworkChangeNotifierDelegateAndroid::NotifyMaxBandwidthChanged(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint subtype) {
  DCHECK(thread_checker_.CalledOnValidThread());
  double new_max_bandwidth =
      NetworkChangeNotifierAndroid::GetMaxBandwidthMbpsForConnectionSubtype(
          ConvertConnectionSubtype(subtype));
  SetCurrentMaxBandwidth(new_max_bandwidth);
  observers_->Notify(FROM_HERE, &Observer::OnMaxBandwidthChanged,
                     new_max_bandwidth, GetCurrentConnectionType());
}

void NetworkChangeNotifierDelegateAndroid::NotifyOfNetworkConnect(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jlong net_id,
    jint connection_type) {
  DCHECK(thread_checker_.CalledOnValidThread());
  NetworkHandle network = net_id;
  bool already_exists;
  {
    base::AutoLock auto_lock(connection_lock_);
    already_exists = network_map_.find(network) != network_map_.end();
    network_map_[network] = static_cast<ConnectionType>(connection_type);
  }
  // Android Lollipop would send many duplicate notifications.
  // This was later fixed in Android Marshmallow.
  // Deduplicate them here by avoiding sending duplicate notifications.
  if (!already_exists) {
    observers_->Notify(FROM_HERE, &Observer::OnNetworkConnected, network);
    if (network == GetCurrentDefaultNetwork()) {
      observers_->Notify(FROM_HERE, &Observer::OnNetworkMadeDefault, network);
    }
  }
}

void NetworkChangeNotifierDelegateAndroid::NotifyOfNetworkSoonToDisconnect(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jlong net_id) {
  DCHECK(thread_checker_.CalledOnValidThread());
  NetworkHandle network = net_id;
  {
    base::AutoLock auto_lock(connection_lock_);
    if (network_map_.find(network) == network_map_.end())
      return;
  }
  observers_->Notify(FROM_HERE, &Observer::OnNetworkSoonToDisconnect, network);
}

void NetworkChangeNotifierDelegateAndroid::NotifyOfNetworkDisconnect(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jlong net_id) {
  DCHECK(thread_checker_.CalledOnValidThread());
  NetworkHandle network = net_id;
  {
    base::AutoLock auto_lock(connection_lock_);
    if (network == default_network_)
      default_network_ = NetworkChangeNotifier::kInvalidNetworkHandle;
    if (network_map_.erase(network) == 0)
      return;
  }
  observers_->Notify(FROM_HERE, &Observer::OnNetworkDisconnected, network);
}

void NetworkChangeNotifierDelegateAndroid::NotifyPurgeActiveNetworkList(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jlongArray>& active_networks) {
  DCHECK(thread_checker_.CalledOnValidThread());
  NetworkList active_network_list;
  base::android::JavaLongArrayToInt64Vector(env, active_networks,
                                            &active_network_list);
  NetworkList disconnected_networks;
  {
    base::AutoLock auto_lock(connection_lock_);
    for (auto i : network_map_) {
      bool found = false;
      for (auto j : active_network_list) {
        if (j == i.first) {
          found = true;
          break;
        }
      }
      if (!found) {
        disconnected_networks.push_back(i.first);
      }
    }
  }
  for (auto disconnected_network : disconnected_networks)
    NotifyOfNetworkDisconnect(env, obj, disconnected_network);
}

void NetworkChangeNotifierDelegateAndroid::AddObserver(
    Observer* observer) {
  observers_->AddObserver(observer);
}

void NetworkChangeNotifierDelegateAndroid::RemoveObserver(
    Observer* observer) {
  observers_->RemoveObserver(observer);
}

void NetworkChangeNotifierDelegateAndroid::SetCurrentConnectionType(
    ConnectionType new_connection_type) {
  base::AutoLock auto_lock(connection_lock_);
  connection_type_ = new_connection_type;
}

void NetworkChangeNotifierDelegateAndroid::SetCurrentMaxBandwidth(
    double max_bandwidth) {
  base::AutoLock auto_lock(connection_lock_);
  connection_max_bandwidth_ = max_bandwidth;
}

void NetworkChangeNotifierDelegateAndroid::SetCurrentDefaultNetwork(
    NetworkHandle default_network) {
  base::AutoLock auto_lock(connection_lock_);
  default_network_ = default_network;
}

void NetworkChangeNotifierDelegateAndroid::SetCurrentNetworksAndTypes(
    NetworkMap network_map) {
  base::AutoLock auto_lock(connection_lock_);
  network_map_ = network_map;
}

void NetworkChangeNotifierDelegateAndroid::SetOnline() {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_forceConnectivityState(env, true);
}

void NetworkChangeNotifierDelegateAndroid::SetOffline() {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_forceConnectivityState(env, false);
}

void NetworkChangeNotifierDelegateAndroid::FakeNetworkConnected(
    NetworkChangeNotifier::NetworkHandle network,
    ConnectionType type) {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_fakeNetworkConnected(env, network, type);
}

void NetworkChangeNotifierDelegateAndroid::FakeNetworkSoonToBeDisconnected(
    NetworkChangeNotifier::NetworkHandle network) {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_fakeNetworkSoonToBeDisconnected(env, network);
}

void NetworkChangeNotifierDelegateAndroid::FakeNetworkDisconnected(
    NetworkChangeNotifier::NetworkHandle network) {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_fakeNetworkDisconnected(env, network);
}

void NetworkChangeNotifierDelegateAndroid::FakePurgeActiveNetworkList(
    NetworkChangeNotifier::NetworkList networks) {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_fakePurgeActiveNetworkList(
      env, base::android::ToJavaLongArray(env, networks));
}

void NetworkChangeNotifierDelegateAndroid::FakeDefaultNetwork(
    NetworkChangeNotifier::NetworkHandle network,
    ConnectionType type) {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_fakeDefaultNetwork(env, network, type);
}

void NetworkChangeNotifierDelegateAndroid::FakeConnectionSubtypeChanged(
    ConnectionSubtype subtype) {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_NetworkChangeNotifier_fakeConnectionSubtypeChanged(env, subtype);
}

bool NetworkChangeNotifierDelegateAndroid::IsProcessBoundToNetwork() {
  return Java_NetworkChangeNotifier_isProcessBoundToNetwork(
      base::android::AttachCurrentThread());
}

}  // namespace net
