chromium/ash/system/network/active_network_icon.cc

// 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 "ash/system/network/active_network_icon.h"

#include "ash/public/cpp/network_config_service.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/network/network_icon.h"
#include "ash/system/network/network_utils.h"
#include "ash/system/network/tray_network_state_model.h"
#include "ash/system/tray/tray_constants.h"
#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/services/network_config/public/cpp/cros_network_config_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/paint_vector_icon.h"

using chromeos::network_config::mojom::ActivationStateType;
using chromeos::network_config::mojom::ConnectionStateType;
using chromeos::network_config::mojom::DeviceStateProperties;
using chromeos::network_config::mojom::DeviceStateType;
using chromeos::network_config::mojom::FilterType;
using chromeos::network_config::mojom::NetworkFilter;
using chromeos::network_config::mojom::NetworkStateProperties;
using chromeos::network_config::mojom::NetworkType;

namespace ash {

namespace {

const int kPurgeDelayMs = 500;

}  // namespace

ActiveNetworkIcon::ActiveNetworkIcon(TrayNetworkStateModel* model)
    : model_(model) {
  model_->AddObserver(this);
}

ActiveNetworkIcon::~ActiveNetworkIcon() {
  model_->RemoveObserver(this);
}

void ActiveNetworkIcon::GetConnectionStatusStrings(Type type,
                                                   std::u16string* a11y_name,
                                                   std::u16string* a11y_desc,
                                                   std::u16string* tooltip) {
  const NetworkStateProperties* network = nullptr;
  switch (type) {
    case Type::kSingle:
      network = model_->default_network();
      break;
    case Type::kPrimary:
      // TODO(902409): Provide strings for technology or connecting.
      network = model_->default_network();
      break;
    case Type::kCellular:
      network = model_->active_cellular();
      break;
  }

  std::u16string network_name;
  if (network) {
    network_name = network->type == NetworkType::kEthernet
                       ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_ETHERNET)
                       : base::UTF8ToUTF16(network->name);
  }
  // Check for Activating first since activating networks may be connected.
  if (network && network->type == NetworkType::kCellular &&
      network->type_state->get_cellular()->activation_state ==
          ActivationStateType::kActivating) {
    std::u16string activating_string = l10n_util::GetStringFUTF16(
        IDS_ASH_STATUS_TRAY_NETWORK_ACTIVATING, network_name);
    if (a11y_name)
      *a11y_name = activating_string;
    if (a11y_desc)
      *a11y_desc = std::u16string();
    if (tooltip)
      *tooltip = activating_string;
  } else if (network && chromeos::network_config::StateIsConnected(
                            network->connection_state)) {
    std::u16string connected_string;
    if (auto portal_subtext = GetPortalStateSubtext(network->portal_state)) {
      connected_string = l10n_util::GetStringFUTF16(
          IDS_ASH_STATUS_TRAY_NETWORK_PORTAL, network_name, *portal_subtext);
    } else {
      connected_string = l10n_util::GetStringFUTF16(
          IDS_ASH_STATUS_TRAY_NETWORK_CONNECTED, network_name);
    }
    std::u16string signal_strength_string;
    if (chromeos::network_config::NetworkTypeMatchesType(
            network->type, NetworkType::kWireless)) {
      // Retrieve the string describing the signal strength, if it is applicable
      // to |network|.
      int signal_strength =
          chromeos::network_config::GetWirelessSignalStrength(network);
      switch (network_icon::GetSignalStrength(signal_strength)) {
        case network_icon::SignalStrength::NONE:
          break;
        case network_icon::SignalStrength::WEAK:
          signal_strength_string = l10n_util::GetStringUTF16(
              IDS_ASH_STATUS_TRAY_NETWORK_SIGNAL_WEAK);
          break;
        case network_icon::SignalStrength::MEDIUM:
          signal_strength_string = l10n_util::GetStringUTF16(
              IDS_ASH_STATUS_TRAY_NETWORK_SIGNAL_MEDIUM);
          break;
        case network_icon::SignalStrength::STRONG:
          signal_strength_string = l10n_util::GetStringUTF16(
              IDS_ASH_STATUS_TRAY_NETWORK_SIGNAL_STRONG);
          break;
      }
    }
    if (a11y_name)
      *a11y_name = connected_string;
    if (a11y_desc)
      *a11y_desc = signal_strength_string;
    if (tooltip) {
      *tooltip = signal_strength_string.empty()
                     ? connected_string
                     : l10n_util::GetStringFUTF16(
                           IDS_ASH_STATUS_TRAY_NETWORK_CONNECTED_TOOLTIP,
                           connected_string, signal_strength_string);
    }
  } else if (network &&
             network->connection_state == ConnectionStateType::kConnecting) {
    std::u16string connecting_string = l10n_util::GetStringFUTF16(
        IDS_ASH_STATUS_TRAY_NETWORK_CONNECTING, network_name);
    if (a11y_name)
      *a11y_name = connecting_string;
    if (a11y_desc)
      *a11y_desc = std::u16string();
    if (tooltip)
      *tooltip = connecting_string;
  } else {
    if (a11y_name) {
      *a11y_name = l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_NETWORK_NOT_CONNECTED_A11Y);
    }
    if (a11y_desc)
      *a11y_desc = std::u16string();
    if (tooltip) {
      *tooltip = l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_NETWORK_DISCONNECTED_TOOLTIP);
    }
  }
}

gfx::ImageSkia ActiveNetworkIcon::GetImage(
    const ui::ColorProvider* color_provider,
    Type type,
    network_icon::IconType icon_type,
    bool* animating) {
  switch (type) {
    case Type::kSingle:
      return GetSingleImage(color_provider, icon_type, animating);
    case Type::kPrimary:
      return GetDualImagePrimary(color_provider, icon_type, animating);
    case Type::kCellular:
      return GetDualImageCellular(color_provider, icon_type, animating);
  }
  NOTREACHED();
}

gfx::ImageSkia ActiveNetworkIcon::GetSingleImage(
    const ui::ColorProvider* color_provider,
    network_icon::IconType icon_type,
    bool* animating) {
  // If no network, check for cellular initializing.
  const NetworkStateProperties* default_network = model_->default_network();
  if (!default_network && cellular_uninitialized_msg_ != 0) {
    if (animating)
      *animating = true;
    return network_icon::GetConnectingImageForNetworkType(
        color_provider, NetworkType::kCellular, icon_type);
  }
  return GetDefaultImageImpl(color_provider, default_network, icon_type,
                             animating);
}

gfx::ImageSkia ActiveNetworkIcon::GetDualImagePrimary(
    const ui::ColorProvider* color_provider,
    network_icon::IconType icon_type,
    bool* animating) {
  const NetworkStateProperties* default_network = model_->default_network();
  if (default_network && default_network->type == NetworkType::kCellular) {
    if (chromeos::network_config::StateIsConnected(
            default_network->connection_state)) {
      // TODO(902409): Show proper technology badges.
      if (animating)
        *animating = false;
      return gfx::CreateVectorIcon(
          kNetworkBadgeTechnologyLteIcon,
          network_icon::GetDefaultColorForIconType(color_provider, icon_type));
    }
    // If Cellular is connecting, use the active non cellular network.
    return GetDefaultImageImpl(color_provider, model_->active_non_cellular(),
                               icon_type, animating);
  }
  return GetDefaultImageImpl(color_provider, default_network, icon_type,
                             animating);
}

gfx::ImageSkia ActiveNetworkIcon::GetDualImageCellular(
    const ui::ColorProvider* color_provider,
    network_icon::IconType icon_type,
    bool* animating) {
  if (model_->GetDeviceState(NetworkType::kCellular) ==
      DeviceStateType::kUnavailable) {
    if (animating)
      *animating = false;
    return gfx::ImageSkia();
  }

  if (cellular_uninitialized_msg_ != 0) {
    if (animating)
      *animating = true;
    return network_icon::GetConnectingImageForNetworkType(
        color_provider, NetworkType::kCellular, icon_type);
  }

  const NetworkStateProperties* active_cellular = model_->active_cellular();
  if (!active_cellular) {
    if (animating)
      *animating = false;
    // For the `kCellular` icon in the `UnifiedSystemTray`: if the tray is
    // active, the icon type should be used to get the correct color.
    if (icon_type != network_icon::IconType::ICON_TYPE_TRAY_ACTIVE) {
      icon_type = network_icon::IconType::ICON_TYPE_LIST;
    }
    return network_icon::GetDisconnectedImageForNetworkType(
        color_provider, NetworkType::kCellular, icon_type);
  }

  return network_icon::GetImageForNonVirtualNetwork(
      color_provider, active_cellular, icon_type, false /* show_vpn_badge */,
      animating);
}

gfx::ImageSkia ActiveNetworkIcon::GetDefaultImageImpl(
    const ui::ColorProvider* color_provider,
    const NetworkStateProperties* network,
    network_icon::IconType icon_type,
    bool* animating) {
  if (!network) {
    VLOG(1) << __func__ << ": No network";
    return GetDefaultImageForNoNetwork(color_provider, icon_type, animating);
  }

  const NetworkStateProperties* active_vpn = model_->active_vpn();
  // Connected network with a connecting VPN.
  if (chromeos::network_config::StateIsConnected(network->connection_state) &&
      active_vpn &&
      active_vpn->connection_state == ConnectionStateType::kConnecting) {
    if (animating)
      *animating = true;
    VLOG(1) << __func__ << ": Connected with connecting VPN";
    return network_icon::GetConnectedNetworkWithConnectingVpnImage(
        color_provider, network, icon_type);
  }

  // Default behavior: connected or connecting network, possibly with VPN badge.
  bool show_vpn_badge = !!active_vpn;
  VLOG(1) << __func__ << ": Network: " << network->name;
  return network_icon::GetImageForNonVirtualNetwork(
      color_provider, network, icon_type, show_vpn_badge, animating);
}

gfx::ImageSkia ActiveNetworkIcon::GetDefaultImageForNoNetwork(
    const ui::ColorProvider* color_provider,
    network_icon::IconType icon_type,
    bool* animating) {
  if (animating)
    *animating = false;
  if (model_->GetDeviceState(NetworkType::kWiFi) == DeviceStateType::kEnabled) {
    // WiFi is enabled but no connections available.
    return network_icon::GetImageForWiFiNoConnections(color_provider,
                                                      icon_type);
  }
  // WiFi is disabled, show a full icon with a strikethrough.
  return network_icon::GetImageForWiFiEnabledState(
      color_provider, false /* not enabled*/, icon_type);
}

void ActiveNetworkIcon::SetCellularUninitializedMsg() {
  const DeviceStateProperties* cellular =
      model_->GetDevice(NetworkType::kCellular);
  if (cellular && cellular->device_state == DeviceStateType::kUninitialized) {
    cellular_uninitialized_msg_ = IDS_ASH_STATUS_TRAY_INITIALIZING_CELLULAR;
    uninitialized_state_time_ = base::Time::Now();
    return;
  }

  // If cellular is scanning, we want to show a 'connecting' image. However,
  // there may be some cases where cellular's scanning property is true while
  // the device is disabling. In this instance, we don't want to show
  // 'connecting'. Only set cellular_uninitialized_msg_ to scanning if the
  // device is scanning and enabled or enabling.
  if (cellular &&
      (cellular->device_state == DeviceStateType::kEnabled ||
       cellular->device_state == DeviceStateType::kEnabling) &&
      cellular->scanning) {
    cellular_uninitialized_msg_ = IDS_ASH_STATUS_TRAY_MOBILE_SCANNING;
    uninitialized_state_time_ = base::Time::Now();
    return;
  }

  // If cellular is not scanning and cellular device is enabled reset cellular
  // initializing state.
  if (cellular && !cellular->scanning &&
      (cellular->device_state == DeviceStateType::kEnabled ||
       cellular->device_state == DeviceStateType::kEnabling)) {
    cellular_uninitialized_msg_ = 0;
  }

  // There can be a delay between leaving the Initializing state and when
  // a Cellular device shows up, so keep showing the initializing
  // animation for a bit to avoid flashing the disconnect icon.
  const int kInitializingDelaySeconds = 1;
  base::TimeDelta dtime = base::Time::Now() - uninitialized_state_time_;
  if (dtime.InSeconds() >= kInitializingDelaySeconds)
    cellular_uninitialized_msg_ = 0;
}

// TrayNetworkStateObserver

void ActiveNetworkIcon::ActiveNetworkStateChanged() {
  SetCellularUninitializedMsg();
}

void ActiveNetworkIcon::DeviceStateListChanged() {
  SetCellularUninitializedMsg();
}

void ActiveNetworkIcon::NetworkListChanged() {
  if (purge_timer_.IsRunning())
    return;
  purge_timer_.Start(FROM_HERE, base::Milliseconds(kPurgeDelayMs),
                     base::BindOnce(&ActiveNetworkIcon::PurgeNetworkIconCache,
                                    weak_ptr_factory_.GetWeakPtr()));
}

void ActiveNetworkIcon::PurgeNetworkIconCache() {
  model_->cros_network_config()->GetNetworkStateList(
      NetworkFilter::New(FilterType::kVisible, NetworkType::kAll,
                         /*limit=*/0),
      base::BindOnce(
          [](std::vector<
              chromeos::network_config::mojom::NetworkStatePropertiesPtr>
                 networks) {
            std::set<std::string> network_guids;
            for (const auto& iter : networks) {
              network_guids.insert(iter->guid);
            }
            network_icon::PurgeNetworkIconCache(network_guids);
          }));
}

}  // namespace ash