1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.network.telephony.scan 18 19 import android.content.Context 20 import android.telephony.AccessNetworkConstants.AccessNetworkType 21 import android.telephony.CellInfo 22 import android.telephony.NetworkScanRequest 23 import android.telephony.PhoneCapability 24 import android.telephony.RadioAccessSpecifier 25 import android.telephony.TelephonyManager 26 import android.telephony.TelephonyScanManager 27 import android.util.Log 28 import androidx.lifecycle.LifecycleOwner 29 import com.android.settings.R 30 import com.android.settings.network.telephony.CellInfoUtil.getNetworkTitle 31 import com.android.settings.network.telephony.telephonyManager 32 import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle 33 import java.util.concurrent.Executors 34 import kotlinx.coroutines.Dispatchers 35 import kotlinx.coroutines.channels.awaitClose 36 import kotlinx.coroutines.flow.Flow 37 import kotlinx.coroutines.flow.callbackFlow 38 import kotlinx.coroutines.flow.conflate 39 import kotlinx.coroutines.flow.flowOn 40 import kotlinx.coroutines.flow.onEach 41 42 class NetworkScanRepository(private val context: Context, subId: Int) { 43 enum class NetworkScanState { 44 ACTIVE, COMPLETE, ERROR 45 } 46 47 data class NetworkScanResult( 48 val state: NetworkScanState, 49 val cellInfos: List<CellInfo>, 50 ) 51 52 private val telephonyManager = context.telephonyManager(subId) 53 54 /** TODO: Move this to UI layer, when UI layer migrated to Kotlin. */ launchNetworkScannull55 fun launchNetworkScan(lifecycleOwner: LifecycleOwner, onResult: (NetworkScanResult) -> Unit) = 56 networkScanFlow().collectLatestWithLifecycle(lifecycleOwner, action = onResult) 57 58 data class CellInfoScanKey( 59 val title: String?, 60 val className: String, 61 val isRegistered: Boolean, 62 ) { 63 constructor(cellInfo: CellInfo) : this( 64 title = cellInfo.cellIdentity.getNetworkTitle(), 65 className = cellInfo.javaClass.name, 66 isRegistered = cellInfo.isRegistered, 67 ) 68 } 69 <lambda>null70 fun networkScanFlow(): Flow<NetworkScanResult> = callbackFlow { 71 var state = NetworkScanState.ACTIVE 72 var cellInfos: List<CellInfo> = emptyList() 73 74 val callback = object : TelephonyScanManager.NetworkScanCallback() { 75 override fun onResults(results: List<CellInfo>) { 76 cellInfos = results.distinctBy { CellInfoScanKey(it) } 77 sendResult() 78 } 79 80 override fun onComplete() { 81 state = NetworkScanState.COMPLETE 82 sendResult() 83 // Don't call close() here since onComplete() could happens before onResults() 84 } 85 86 override fun onError(error: Int) { 87 state = NetworkScanState.ERROR 88 sendResult() 89 close() 90 } 91 92 private fun sendResult() { 93 trySend(NetworkScanResult(state, cellInfos)) 94 } 95 } 96 97 val networkScan = telephonyManager.requestNetworkScan( 98 createNetworkScan(), 99 // requestNetworkScan() could call callbacks concurrently, so we use a single thread 100 // to avoid racing conditions. 101 Executors.newSingleThreadExecutor(), 102 callback, 103 ) 104 105 awaitClose { 106 networkScan.stopScan() 107 Log.d(TAG, "network scan stopped") 108 } 109 }.conflate().onEach { Log.d(TAG, "networkScanFlow: $it") }.flowOn(Dispatchers.Default) 110 111 /** Create network scan for allowed network types. */ createNetworkScannull112 private fun createNetworkScan(): NetworkScanRequest { 113 val allowedNetworkTypes = getAllowedNetworkTypes() 114 Log.d(TAG, "createNetworkScan: allowedNetworkTypes = $allowedNetworkTypes") 115 val radioAccessSpecifiers = allowedNetworkTypes 116 .map { RadioAccessSpecifier(it, null, null) } 117 .toTypedArray() 118 return NetworkScanRequest( 119 NetworkScanRequest.SCAN_TYPE_ONE_SHOT, 120 radioAccessSpecifiers, 121 NetworkScanRequest.MIN_SEARCH_PERIODICITY_SEC, // one shot, not used 122 context.resources.getInteger(R.integer.config_network_scan_helper_max_search_time_sec), 123 true, 124 INCREMENTAL_RESULTS_PERIODICITY_SEC, 125 null, 126 ) 127 } 128 getAllowedNetworkTypesnull129 private fun getAllowedNetworkTypes(): List<Int> { 130 val networkTypeBitmap3gpp: Long = 131 telephonyManager.getAllowedNetworkTypesBitmask() and 132 TelephonyManager.NETWORK_STANDARDS_FAMILY_BITMASK_3GPP 133 return buildList { 134 // If the allowed network types are unknown or if they are of the right class, scan for 135 // them; otherwise, skip them to save scan time and prevent users from being shown 136 // networks that they can't connect to. 137 if (networkTypeBitmap3gpp == 0L 138 || networkTypeBitmap3gpp and TelephonyManager.NETWORK_CLASS_BITMASK_2G != 0L 139 ) { 140 add(AccessNetworkType.GERAN) 141 } 142 if (networkTypeBitmap3gpp == 0L 143 || networkTypeBitmap3gpp and TelephonyManager.NETWORK_CLASS_BITMASK_3G != 0L 144 ) { 145 add(AccessNetworkType.UTRAN) 146 } 147 if (networkTypeBitmap3gpp == 0L 148 || networkTypeBitmap3gpp and TelephonyManager.NETWORK_CLASS_BITMASK_4G != 0L 149 ) { 150 add(AccessNetworkType.EUTRAN) 151 } 152 // If a device supports 5G stand-alone then the code below should be re-enabled; however 153 // a device supporting only non-standalone mode cannot perform PLMN selection and camp 154 // on a 5G network, which means that it shouldn't scan for 5G at the expense of battery 155 // as part of the manual network selection process. 156 // 157 if (networkTypeBitmap3gpp == 0L 158 || (networkTypeBitmap3gpp and TelephonyManager.NETWORK_CLASS_BITMASK_5G != 0L && 159 hasNrSaCapability()) 160 ) { 161 add(AccessNetworkType.NGRAN) 162 Log.d(TAG, "radioAccessSpecifiers add NGRAN.") 163 } 164 } 165 } 166 hasNrSaCapabilitynull167 private fun hasNrSaCapability(): Boolean { 168 val phoneCapability = telephonyManager.getPhoneCapability() 169 return PhoneCapability.DEVICE_NR_CAPABILITY_SA in phoneCapability.deviceNrCapabilities 170 } 171 172 companion object { 173 private const val TAG = "NetworkScanRepository" 174 175 private const val INCREMENTAL_RESULTS_PERIODICITY_SEC = 3 176 } 177 } 178