blob: 4863e4aca8ea8731a739e2cb2882176d6e9d5a59 [file] [edit]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package cellular
import (
"bufio"
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"go.chromium.org/luci/common/errors"
"go.chromium.org/infra/cros/recovery/internal/components"
"go.chromium.org/infra/cros/recovery/internal/log"
"go.chromium.org/infra/cros/recovery/tlw"
)
var featureLive = tlw.Cellular_SIMProfileInfo_FEATURE_LIVE_NETWORK
var featureSMS = tlw.Cellular_SIMProfileInfo_FEATURE_SMS
// carrierFeatures is a map of the carriers to the supported features.
// These features are used to determine what tests can be run against which SIMs
// in the lab, see go/cros-cellular-features for more information.
// File bugs against buganizer component: 979102.
var carrierFeatures = map[tlw.Cellular_NetworkProvider][]tlw.Cellular_SIMProfileInfo_Feature{
tlw.Cellular_NETWORK_ATT: {featureLive, featureSMS},
tlw.Cellular_NETWORK_TMOBILE: {featureLive, featureSMS},
tlw.Cellular_NETWORK_TEST: {featureLive},
tlw.Cellular_NETWORK_VERIZON: {featureLive},
tlw.Cellular_NETWORK_SPRINT: {featureLive},
tlw.Cellular_NETWORK_DOCOMO: {featureLive},
tlw.Cellular_NETWORK_SOFTBANK: {featureLive},
tlw.Cellular_NETWORK_KDDI: {featureLive},
tlw.Cellular_NETWORK_RAKUTEN: {featureLive},
tlw.Cellular_NETWORK_VODAFONE: {featureLive},
tlw.Cellular_NETWORK_EE: {featureLive},
tlw.Cellular_NETWORK_AMARISOFT: {featureLive},
tlw.Cellular_NETWORK_ROGER: {featureLive},
tlw.Cellular_NETWORK_BELL: {featureLive},
tlw.Cellular_NETWORK_TELUS: {featureLive},
tlw.Cellular_NETWORK_FI: {featureLive},
tlw.Cellular_NETWORK_CBRS: {featureLive},
tlw.Cellular_NETWORK_LINEMO: {featureLive},
tlw.Cellular_NETWORK_POVO: {featureLive},
tlw.Cellular_NETWORK_HANSHIN: {featureLive},
}
// carrierOperatorIDs is a map of known operator_code to carrier mappings present in
// the lab, these are generally fixed and should only really need to be updated when
// adding new carriers to the lab..
var carrierOperatorIDs = map[string]tlw.Cellular_NetworkProvider{
"00101": tlw.Cellular_NETWORK_AMARISOFT,
"001010": tlw.Cellular_NETWORK_AMARISOFT,
"23415": tlw.Cellular_NETWORK_VODAFONE,
"23430": tlw.Cellular_NETWORK_EE,
"302220": tlw.Cellular_NETWORK_TELUS,
"302720": tlw.Cellular_NETWORK_ROGER,
"310260": tlw.Cellular_NETWORK_TMOBILE,
"311882": tlw.Cellular_NETWORK_TMOBILE,
"310280": tlw.Cellular_NETWORK_ATT,
"310410": tlw.Cellular_NETWORK_ATT,
"311480": tlw.Cellular_NETWORK_VERIZON,
"44010": tlw.Cellular_NETWORK_DOCOMO,
"44011": tlw.Cellular_NETWORK_RAKUTEN,
"44020": tlw.Cellular_NETWORK_SOFTBANK,
"44051": tlw.Cellular_NETWORK_KDDI,
}
const (
adbGetOperatorNumberCmd = "getprop gsm.sim.operator.numeric"
simInfoIccIdKey = "icc_id"
simInfoOperatorNameKey = "display_name"
simInfoSimTypeKey = "is_embedded"
simInfoCardIdKey = "card_id"
)
// SupportedFeaturesForCarrier returns a list of the features supported for an
// individual cellular carrier.
func SupportedFeaturesForCarrier(carrier tlw.Cellular_NetworkProvider) []tlw.Cellular_SIMProfileInfo_Feature {
if f, ok := carrierFeatures[carrier]; ok {
return f
}
return []tlw.Cellular_SIMProfileInfo_Feature{}
}
// simInfo is a simplified version of the JSON output from ModemManager containing the SIM information.
type simInfo struct {
SIM *simDetails `json:"sim,omitempty"`
}
type simProperties struct {
ICCID string `json:"iccid,omitempty"`
EID string `json:"eid,omitempty"`
OperatorName string `json:"operator-name,omitempty"`
OperatorCode string `json:"operator-code,omitempty"`
TypeString string `json:"sim-type,omitempty"`
}
type simDetails struct {
Properties *simProperties `json:"properties,omitempty"`
}
// Type returns the type of SIM represented..
func (s *simInfo) Type() tlw.Cellular_SIMType {
if s == nil || s.SIM == nil || s.SIM.Properties == nil {
return tlw.Cellular_SIM_UNSPECIFIED
}
// If SIM has an EID or is explicitly eSIM then it is 'digital'
if s.EID() != "" || strings.EqualFold(s.SIM.Properties.TypeString, "esim") {
return tlw.Cellular_SIM_DIGITAL
}
return tlw.Cellular_SIM_PHYSICAL
}
// ICCID returns the SIMs ICCID.
func (s *simInfo) ICCID() string {
if s == nil || s.SIM == nil || s.SIM.Properties == nil {
return ""
}
// Modem manager may populate empty properties with "--"
if s.SIM.Properties.ICCID == "--" {
return ""
}
return s.SIM.Properties.ICCID
}
// EID returns the SIMs EID.
func (s *simInfo) EID() string {
if s == nil || s.SIM == nil || s.SIM.Properties == nil {
return ""
}
// Modem manager may populate empty properties with "--"
if s.SIM.Properties.EID == "--" {
return ""
}
return s.SIM.Properties.EID
}
// CarrierName returns the SIMs operator/carrier name.
func (s *simInfo) CarrierName() tlw.Cellular_NetworkProvider {
if s == nil || s.SIM == nil || s.SIM.Properties == nil {
return tlw.Cellular_NETWORK_UNSPECIFIED
}
// First try to determine Carrier Name from OperatorCode as it is more reliable,
// if that fails then fallback to the OperatorName.
oc := s.SIM.Properties.OperatorCode
if _, ok := carrierOperatorIDs[oc]; ok {
return carrierOperatorIDs[oc]
}
on := s.SIM.Properties.OperatorName
switch {
case strings.EqualFold(on, "AT&T"):
return tlw.Cellular_NETWORK_ATT
case strings.EqualFold(on, "VERIZON WIRELESS"):
return tlw.Cellular_NETWORK_VERIZON
case strings.EqualFold(on, "T-MOBILE"):
return tlw.Cellular_NETWORK_TMOBILE
case strings.EqualFold(on, "USIM"):
return tlw.Cellular_NETWORK_AMARISOFT
case strings.EqualFold(on, "NTT DOCOMO"):
return tlw.Cellular_NETWORK_DOCOMO
case strings.EqualFold(on, "EE"):
return tlw.Cellular_NETWORK_EE
case strings.EqualFold(on, "KDDI"):
return tlw.Cellular_NETWORK_KDDI
case strings.EqualFold(on, "RAKUTEN"):
return tlw.Cellular_NETWORK_RAKUTEN
case strings.EqualFold(on, "SOFTBANK"):
return tlw.Cellular_NETWORK_SOFTBANK
case strings.EqualFold(on, "GOOGLE FI"):
return tlw.Cellular_NETWORK_FI
case strings.EqualFold(on, "SPRINT"):
return tlw.Cellular_NETWORK_SPRINT
// vodafone may be 'vodafone UK' or some other variant.
case strings.Contains(strings.ToLower(on), "vodafone"):
return tlw.Cellular_NETWORK_VODAFONE
// If empty, then return unspecified rather than unsupported.
case on == "" || on == "--":
return tlw.Cellular_NETWORK_UNSPECIFIED
default:
return tlw.Cellular_NETWORK_UNSUPPORTED
}
}
type SimAction interface {
SwitchSIMSlot(context.Context, components.Runner, int32) error
GetSIMInfo(context.Context, components.Runner) (*tlw.Cellular_SIMInfo, error)
}
// SwitchToMatchingSIMSlot switches to a SIM slot that matches the predicate.
func SwitchToMatchingSIMSlot(ctx context.Context, runner components.Runner, predicate func(*tlw.Cellular_SIMInfo) bool, modem CellularClient) error {
// Check if the current SIM slot matches before moving on.
simInfo, err := modem.GetSIMInfo(ctx, runner)
if err != nil {
return errors.Annotate(err, "get all sim info: failed to query info for current SIM slot")
}
if predicate(simInfo) {
return nil
}
modemInfo, err := modem.WaitForModemInfo(ctx, runner, 15*time.Second)
if err != nil {
return errors.Annotate(err, "get all sim info: wait for ModemManager to export modem")
}
for i := range modemInfo.SIMSlotCount() {
if err := modem.SwitchSIMSlot(ctx, runner, i+1); err != nil {
return errors.Annotate(err, "get all sim info: switch to requested SIM slot")
}
simInfo, err := modem.GetSIMInfo(ctx, runner)
if err != nil {
return errors.Annotate(err, "get all sim info: failed to query info for sim slot: %d", i+1)
}
if predicate(simInfo) {
return nil
}
}
return errors.Reason("switch to sim slot matching: failed to find a SIM slot matching predicate")
}
// GetAllSIMInfo queries all SIM cards on the DUT and populates their information.
func GetAllSIMInfo(ctx context.Context, runner components.Runner, modem CellularClient) ([]*tlw.Cellular_SIMInfo, error) {
modemInfo, err := modem.WaitForModemInfo(ctx, runner, 15*time.Second)
if err != nil {
return nil, errors.Annotate(err, "get all sim info: wait for ModemManager to export modem")
}
oldSlot := modemInfo.ActiveSIMSlot()
defer func() {
if err := modem.SwitchSIMSlot(ctx, runner, oldSlot); err != nil {
log.Errorf(ctx, "get all sim info: failed to restore original SIM slot: ", err)
}
}()
res := make([]*tlw.Cellular_SIMInfo, 0)
for i := range modemInfo.SIMSlotCount() {
if err := modem.SwitchSIMSlot(ctx, runner, i+1); err != nil {
return nil, errors.Annotate(err, "get all sim info: switch to requested SIM slot")
}
simInfo, err := modem.GetSIMInfo(ctx, runner)
if err != nil {
return nil, errors.Annotate(err, "get all sim info: failed to query info for sim slot: %d", i+1)
}
if simInfo != nil {
res = append(res, simInfo)
}
}
return res, nil
}
// SwitchSIMSlot switches the active SIM slot to the requested index.
func (c *ChromeOSCellular) SwitchSIMSlot(ctx context.Context, runner components.Runner, slotNumber int32) error {
modemInfo, err := c.WaitForModemInfo(ctx, runner, 15*time.Second)
if err != nil {
return errors.Annotate(err, "switch sim slot: wait for ModemManager to export modem")
}
if modemInfo.ActiveSIMSlot() == slotNumber {
log.Debugf(ctx, "sim slot already on %d", slotNumber)
return nil
}
if _, err := runner(ctx, 5*time.Second, fmt.Sprintf("mmcli -m a --set-primary-sim-slot=%d", slotNumber)); err != nil {
return errors.Annotate(err, "call mmcli")
}
predicate := func(m *ModemInfo) error {
if m.ActiveSIMSlot() != slotNumber {
return errors.Reason("active modem slot not equal to %d", slotNumber)
}
return nil
}
// Wait for up to 45 seconds for Modem to re-appear after slot switch.
if _, err := c.WaitForModemInfo(ctx, runner, 120*time.Second, predicate); err != nil {
return errors.Annotate(err, "switch sim slot: wait for ModemManager to export modem")
}
return nil
}
// SwitchSIMSlot switches the active SIM slot to the requested index.
func (a *AndroidCellular) SwitchSIMSlot(ctx context.Context, runner components.Runner, slotNumber int32) error {
activeSimSlot, err := runner(ctx, 5*time.Second, adbGetActiveSimSlotCmd)
if err != nil {
return errors.Annotate(err, "get active sim slot")
}
if activeSimSlot == strconv.Itoa(int(slotNumber)) {
log.Debugf(ctx, "sim slot already on %d", slotNumber)
return nil
}
if _, err := runner(ctx, 5*time.Second, fmt.Sprintf("settings put global multi_sim_data_call %d", slotNumber)); err != nil {
return errors.Annotate(err, "set sim slot")
}
predicate := func(m *ModemInfo) error {
if m.ActiveSIMSlot() != slotNumber {
return errors.Reason("active modem slot not equal to %d", slotNumber)
}
return nil
}
// Wait for up to 2 minutes for Modem to re-appear after slot switch.
if _, err := a.WaitForModemInfo(ctx, runner, 120*time.Second, predicate); err != nil {
return errors.Annotate(err, "switch sim slot: wait for ModemManager to export modem")
}
return nil
}
// GetSIMInfo queries the SIM information in the current slot or returns nil if there is no SIM in the slot.
func (c *ChromeOSCellular) GetSIMInfo(ctx context.Context, runner components.Runner) (*tlw.Cellular_SIMInfo, error) {
modemInfo, err := c.WaitForModemInfo(ctx, runner, 15*time.Second)
if err != nil {
return nil, errors.Annotate(err, "get sim info: wait for ModemManager to export modem")
}
// If this is a pSIM slot with no SIM, then there will be no SIM dbus path.
// No sim detected in the slot -> return with no error.
simID := modemInfo.ActiveSIMID()
if simID == "" {
log.Infof(ctx, "get sim info: no SIM detected in slot: %d, empty sim dbus path", modemInfo.ActiveSIMSlot())
return nil, nil
}
output, err := runner(ctx, 5*time.Second, "mmcli -J -i "+simID)
if err != nil {
return nil, errors.Annotate(err, "get sim info: failed to call mmcli")
}
si, err := parseSIMInfo(ctx, output)
if err != nil {
return nil, errors.Annotate(err, "get sim info: failed parsing mmcli response")
}
// If this is an eSIM slot with no SIM profile, then there will be an empty ICCID even if the SIM dbus exists.
if si.ICCID() == "" {
log.Infof(ctx, "get sim info: no SIM detected in slot: %d, empty sim ICCID", modemInfo.ActiveSIMSlot())
return nil, nil
}
// Verify some required information before returning
if modemInfo.ActiveSIMSlot() == 0 {
return nil, errors.Reason("get sim info: SIM slot ID is empty.")
}
if si.Type() == tlw.Cellular_SIM_UNSPECIFIED {
return nil, errors.Reason("get sim info: SIM type is empty.")
}
return &tlw.Cellular_SIMInfo{
SlotId: modemInfo.ActiveSIMSlot(),
Type: si.Type(),
Eid: si.EID(),
ProfileInfos: []*tlw.Cellular_SIMProfileInfo{
{
Iccid: si.ICCID(),
CarrierName: si.CarrierName(),
OwnNumber: modemInfo.OwnNumber(),
},
},
}, nil
}
// GetSIMInfo queries the SIM information in the current slot or returns nil if there is no SIM in the slot.
func (a *AndroidCellular) GetSIMInfo(ctx context.Context, runner components.Runner) (*tlw.Cellular_SIMInfo, error) {
modemInfo, err := a.WaitForModemInfo(ctx, runner, 15*time.Second)
if err != nil {
return nil, errors.Annotate(err, "get sim info: wait for ModemManager to export modem")
}
// If this is a pSIM slot with no SIM, then there will be no SIM dbus path.
// No sim detected in the slot -> return with no error.
simID := modemInfo.ActiveSIMID()
if simID == "" {
log.Infof(ctx, "get sim info: no SIM detected in slot: %d, empty sim dbus path", modemInfo.ActiveSIMSlot())
return nil, nil
}
output, err := runner(ctx, 5*time.Second, adbGetSimInfoCmd)
if err != nil {
return nil, errors.Annotate(err, "get sim info: failed to query siminfo")
}
operatorNumber, err := runner(ctx, 5*time.Second, adbGetOperatorNumberCmd)
if err != nil {
return nil, errors.Annotate(err, "get sim info: failed to get operator number")
}
properties, err := a.parseSimProperties(simID, output, operatorNumber)
if err != nil {
return nil, errors.Annotate(err, "get sim info: failed parsing mmcli response")
}
si := &simInfo{SIM: &simDetails{Properties: properties}}
// If this is an eSIM slot with no SIM profile, then there will be an empty ICCID even if the SIM dbus exists.
if si.ICCID() == "" {
log.Infof(ctx, "get sim info: no SIM detected in slot: %d, empty sim ICCID", modemInfo.ActiveSIMSlot())
return nil, nil
}
// Verify some required information before returning
if modemInfo.ActiveSIMSlot() == 0 {
return nil, errors.Reason("get sim info: SIM slot ID is empty.")
}
if si.Type() == tlw.Cellular_SIM_UNSPECIFIED {
return nil, errors.Reason("get sim info: SIM type is empty.")
}
return &tlw.Cellular_SIMInfo{
SlotId: modemInfo.ActiveSIMSlot(),
Type: si.Type(),
Eid: si.EID(),
ProfileInfos: []*tlw.Cellular_SIMProfileInfo{
{
Iccid: si.ICCID(),
CarrierName: si.CarrierName(),
OwnNumber: modemInfo.OwnNumber(),
},
},
}, nil
}
// parseSimProperties parse the sim-related output into simProperties.
func (a *AndroidCellular) parseSimProperties(activeSlot, simInfoOutput, operatorNumber string) (*simProperties, error) {
info := &simProperties{OperatorCode: operatorNumber}
scanner := bufio.NewScanner(strings.NewReader(simInfoOutput))
rowData := map[string]string{}
for scanner.Scan() {
row := scanner.Text()
parts := strings.SplitN(row, " ", 3)
if len(parts) < 3 {
continue
}
data := parts[2]
fields := strings.Split(data, ",")
for _, field := range fields {
kv := strings.Split(strings.TrimSpace(field), "=")
if len(kv) == 2 {
key, value := kv[0], kv[1]
rowData[key] = value
}
}
if activeSlot == rowData[simInfoIdKey] {
info.ICCID = rowData[simInfoIccIdKey]
info.OperatorName = rowData[simInfoOperatorNameKey]
eid := ""
simType := "physical"
// "is_embedded=0" means physical sim card.
if rowData[simInfoSimTypeKey] != "0" {
eid = rowData[simInfoCardIdKey]
simType = "esim"
}
info.TypeString = simType
info.EID = eid
}
}
return info, nil
}
// parseSIMInfo unmarshals the SIM properties json output from mmcli.
func parseSIMInfo(ctx context.Context, output string) (*simInfo, error) {
info := &simInfo{}
if err := json.Unmarshal([]byte(output), info); err != nil {
return nil, err
}
return info, nil
}