You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android 14+系统5G NSA网络下如何同时获取LTE与NR小区信息?

Android 14+系统5G NSA网络下如何同时获取LTE与NR小区信息?

我太懂这种郁闷了!在5G NSA组网下,Android系统居然把LTE锚点小区和NR小区的信息交替返回——单次调用allCellInfo只能拿到其中一组,自己写的APP明明逻辑没问题,结果却和G-NET这类工具的表现不一样,换成SA模式就一切正常,这明显是系统底层的快照机制搞的鬼!

为什么会出现这种情况?

NSA(非独立组网)的核心是5G NR依附在LTE锚点小区上工作,Android系统在这种模式下,并没有把LTE和NR的小区信息合并在同一个allCellInfo返回结果里,而是分成了两个独立的快照。每次调用allCellInfo只能获取到当前的「活跃快照」——要么是LTE锚点的全套信息,要么是NR的信息,所以你轮询的时候会看到两者交替出现。

可行的解决思路

核心思路就是合并多次获取的结果,维护一个去重的缓存列表,具体可以这么做:

1. 不要每次获取到新信息就清空列表,而是合并结果

你之前的代码每次轮询就清空列表再添加新数据,自然只能看到当前快照的内容。正确的做法是维护一个全局缓存:

  • 把每次获取到的新条目(不管是LTE还是NR)加入缓存
  • 通过小区唯一标识(比如CI+PCI)判断是否已经存在,避免重复添加
  • 给每个缓存条目设置过期时间,定期清理失效的旧数据,避免缓存膨胀

2. 改用TelephonyCallback替代主动轮询(更高效)

主动轮询不仅耗电,还容易错过快照切换的时机。Android 10+提供了TelephonyManager.registerTelephonyCallback接口,通过回调接收小区信息变化,能更精准地捕捉到交替出现的LTE和NR快照。

修改后的代码示例

@RequiresApi(Build.VERSION_CODES.Q)
class MainActivity : ComponentActivity() {
    private lateinit var telephonyManager: TelephonyManager
    // 维护缓存的小区信息,包含去重后的LTE和NR条目
    private val cachedCellInfos = mutableStateListOf<CachedCellInfo>()
    // 小区信息过期时间(可根据实际网络情况调整)
    private val CELL_EXPIRY_DURATION = 5000L

    data class CachedCellInfo(val cellInfo: CellInfo, val timestamp: Long)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
        requestPermissionsIfNeeded()
        setContent {
            MaterialTheme {
                CellInfoScreen(cachedCellInfos.map { it.cellInfo })
            }
        }
        setupTelephonyCallback()
        // 启动定期清理过期条目的任务
        startExpiryCleanup()
    }

    private fun requestPermissionsIfNeeded() {
        val permissions = arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.READ_PHONE_STATE
        )
        ActivityCompat.requestPermissions(this, permissions, 1)
    }

    private fun setupTelephonyCallback() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            val telephonyCallback = object : TelephonyCallback(), TelephonyCallback.CellInfoListener {
                override fun onCellInfoChanged(cellInfoList: MutableList<CellInfo>?) {
                    cellInfoList?.forEach { newCellInfo ->
                        // 检查缓存中是否已有相同小区
                        val isAlreadyCached = cachedCellInfos.any { cached ->
                            isSameCell(cached.cellInfo, newCellInfo)
                        }
                        if (!isAlreadyCached) {
                            cachedCellInfos.add(CachedCellInfo(newCellInfo, System.currentTimeMillis()))
                        } else {
                            // 若已存在,更新时间戳避免被误清理
                            cachedCellInfos.find { isSameCell(it.cellInfo, newCellInfo) }?.timestamp = System.currentTimeMillis()
                        }
                    }
                }
            }
            // 注册回调,用主线程处理结果
            telephonyManager.registerTelephonyCallback(mainExecutor, telephonyCallback)
        } else {
            // 兼容Q-R版本的轮询方案(Android14+可忽略)
            startRepeatingUpdates()
        }
    }

    // 判断两个CellInfo是否为同一个小区
    private fun isSameCell(a: CellInfo, b: CellInfo): Boolean {
        if (a.javaClass != b.javaClass) return false
        return when (a) {
            is CellInfoLte -> {
                val bLte = b as CellInfoLte
                a.cellIdentity.ci == bLte.cellIdentity.ci && a.cellIdentity.pci == bLte.cellIdentity.pci
            }
            is CellInfoNr -> {
                val bNr = b as CellInfoNr
                a.cellIdentity.nci == bNr.cellIdentity.nci && a.cellIdentity.pci == bNr.cellIdentity.pci
            }
            is CellInfoWcdma -> {
                val bWcdma = b as CellInfoWcdma
                a.cellIdentity.cid == bWcdma.cellIdentity.cid && a.cellIdentity.pci == bWcdma.cellIdentity.pci
            }
            is CellInfoGsm -> {
                val bGsm = b as CellInfoGsm
                a.cellIdentity.cid == bGsm.cellIdentity.cid && a.cellIdentity.arfcn == bGsm.cellIdentity.arfcn
            }
            else -> false
        }
    }

    private fun startExpiryCleanup() {
        lifecycleScope.launch {
            while (isActive) {
                val currentTime = System.currentTimeMillis()
                // 清理超过过期时间的旧条目
                cachedCellInfos.removeAll { currentTime - it.timestamp > CELL_EXPIRY_DURATION }
                delay(1000) // 每秒清理一次
            }
        }
    }

    // 兼容旧版本的轮询方案(Android14+可忽略)
    private fun startRepeatingUpdates() {
        lifecycleScope.launch {
            while (isActive) {
                val info = telephonyManager.allCellInfo
                info?.forEach { newCellInfo ->
                    val isAlreadyCached = cachedCellInfos.any { cached ->
                        isSameCell(cached.cellInfo, newCellInfo)
                    }
                    if (!isAlreadyCached) {
                        cachedCellInfos.add(CachedCellInfo(newCellInfo, System.currentTimeMillis()))
                    }
                }
                delay(300)
            }
        }
    }
}

@Composable
fun CellInfoScreen(cellInfoList: List<CellInfo>) {
    LazyColumn {
        items(cellInfoList.size) { index ->
            val cellInfo = cellInfoList[index]
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(4.dp),
                elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
            ) {
                Column(modifier = Modifier.padding(8.dp)) {
                    Text("Type: ${cellInfo.javaClass.simpleName}")
                    Text("Registered: ${cellInfo.isRegistered}")
                    Text("CI: ${getCi(cellInfo)}")
                    Text("PCI: ${getPci(cellInfo)}")
                    Text("TAC: ${getTac(cellInfo)}")
                    Text("ARFCN: ${getArfcn(cellInfo)}")
                    Text("Signal: ${getSignalStrength(cellInfo)}")
                }
            }
        }
    }
}

// 以下工具方法保留原逻辑即可
fun getCi(info: CellInfo): Int? = when (info) {
    is CellInfoLte -> info.cellIdentity.ci
    is CellInfoNr -> info.cellIdentity.nci.toInt()
    is CellInfoWcdma -> info.cellIdentity.cid
    is CellInfoGsm -> info.cellIdentity.cid
    else -> null
}

fun getPci(info: CellInfo): Int? = when (info) {
    is CellInfoLte -> info.cellIdentity.pci
    is CellInfoNr -> info.cellIdentity.pci
    else -> null
}

fun getTac(info: CellInfo): Int? = when (info) {
    is CellInfoLte -> info.cellIdentity.tac
    is CellInfoNr -> info.cellIdentity.tac
    else -> null
}

fun getArfcn(info: CellInfo): Int? = when (info) {
    is CellInfoLte -> info.cellIdentity.earfcn
    is CellInfoNr -> info.cellIdentity.nrarfcn
    is CellInfoWcdma -> info.cellIdentity.uarfcn
    is CellInfoGsm -> info.cellIdentity.arfcn
    else -> null
}

fun getSignalStrength(info: CellInfo): String = when (info) {
    is CellInfoLte -> "${info.cellSignalStrength.dbm} dBm"
    is CellInfoNr -> "${info.cellSignalStrength.dbm} dBm"
    is CellInfoWcdma -> "${info.cellSignalStrength.dbm} dBm"
    is CellInfoGsm -> "${info.cellSignalStrength.dbm} dBm"
    else -> "N/A"
}

代码关键点说明

  1. 缓存管理:用CachedCellInfo封装小区信息和时间戳,既能合并LTE和NR数据,又能通过时间戳清理旧数据
  2. 去重逻辑:isSameCell方法通过小区唯一标识判断是否重复,避免缓存中出现同一个小区的多条记录
  3. 高效回调:用TelephonyCallback替代轮询,系统会在小区信息变化时主动通知,比主动轮询更省电、更及时
  4. 权限检查:确保ACCESS_FINE_LOCATION和READ_PHONE_STATE权限已被用户授予,这是获取小区信息的必要条件

额外注意事项

  • 可以根据实际网络情况调整过期时间和清理间隔,如果小区切换频繁,可适当缩短过期时间
  • NSA下的这个现象是Android系统底层机制导致的,SA模式下因为是独立组网,单次调用就能拿到所有小区信息,所以不会有这个问题

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 12:14:36