1 /* <lambda>null2 * 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 package com.android.settingslib.wifi 17 18 import android.content.ComponentName 19 import android.content.Context 20 import android.content.Intent 21 import android.graphics.drawable.Drawable 22 import android.icu.text.MessageFormat 23 import android.net.wifi.ScanResult 24 import android.net.wifi.WifiConfiguration 25 import android.net.wifi.WifiConfiguration.NetworkSelectionStatus 26 import android.net.wifi.WifiManager 27 import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo 28 import android.os.Bundle 29 import android.os.SystemClock 30 import android.util.Log 31 import android.view.WindowManager 32 import androidx.annotation.VisibleForTesting 33 import androidx.lifecycle.LifecycleOwner 34 import androidx.lifecycle.lifecycleScope 35 import com.android.settingslib.R 36 import com.android.settingslib.flags.Flags.newStatusBarIcons 37 import kotlinx.coroutines.CoroutineScope 38 import kotlinx.coroutines.Dispatchers 39 import kotlinx.coroutines.Job 40 import kotlinx.coroutines.asExecutor 41 import kotlinx.coroutines.launch 42 import kotlinx.coroutines.suspendCancellableCoroutine 43 import kotlinx.coroutines.withContext 44 import java.util.Locale 45 import kotlin.coroutines.resume 46 47 open class WifiUtils { 48 /** 49 * Wrapper the [.getInternetIconResource] for testing compatibility. 50 */ 51 open class InternetIconInjector(protected val context: Context) { 52 /** 53 * Returns the Internet icon for a given RSSI level. 54 * 55 * @param noInternet True if a connected Wi-Fi network cannot access the Internet 56 * @param level The number of bars to show (0-4) 57 */ 58 open fun getIcon(noInternet: Boolean, level: Int): Drawable? { 59 return context.getDrawable(getInternetIconResource(level, noInternet)) 60 } 61 } 62 63 companion object { 64 private const val TAG = "WifiUtils" 65 private const val INVALID_RSSI = -127 66 67 /** 68 * The intent action shows Wi-Fi dialog to connect Wi-Fi network. 69 * 70 * 71 * Input: The calling package should put the chosen 72 * com.android.wifitrackerlib.WifiEntry#getKey() to a string extra in the request bundle into 73 * the [.EXTRA_CHOSEN_WIFI_ENTRY_KEY]. 74 * 75 * 76 * Output: Nothing. 77 */ 78 @JvmField 79 @VisibleForTesting 80 val ACTION_WIFI_DIALOG = "com.android.settings.WIFI_DIALOG" 81 82 /** 83 * Specify a key that indicates the WifiEntry to be configured. 84 */ 85 @JvmField 86 @VisibleForTesting 87 val EXTRA_CHOSEN_WIFI_ENTRY_KEY = "key_chosen_wifientry_key" 88 89 /** 90 * The lookup key for a boolean that indicates whether a chosen WifiEntry request to connect to. 91 * `true` means a chosen WifiEntry request to connect to. 92 */ 93 @JvmField 94 @VisibleForTesting 95 val EXTRA_CONNECT_FOR_CALLER = "connect_for_caller" 96 97 /** 98 * The intent action shows network details settings to allow configuration of Wi-Fi. 99 * 100 * 101 * In some cases, a matching Activity may not exist, so ensure you 102 * safeguard against this. 103 * 104 * 105 * Input: The calling package should put the chosen 106 * com.android.wifitrackerlib.WifiEntry#getKey() to a string extra in the request bundle into 107 * the [.KEY_CHOSEN_WIFIENTRY_KEY]. 108 * 109 * 110 * Output: Nothing. 111 */ 112 const val ACTION_WIFI_DETAILS_SETTINGS = "android.settings.WIFI_DETAILS_SETTINGS" 113 const val KEY_CHOSEN_WIFIENTRY_KEY = "key_chosen_wifientry_key" 114 const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" 115 116 @JvmField 117 val WIFI_PIE = getIconsBasedOnFlag() 118 119 private fun getIconsBasedOnFlag(): IntArray { 120 return if (newStatusBarIcons()) { 121 intArrayOf( 122 R.drawable.ic_wifi_0, 123 R.drawable.ic_wifi_1, 124 R.drawable.ic_wifi_2, 125 R.drawable.ic_wifi_3, 126 R.drawable.ic_wifi_4 127 ) 128 } else { 129 intArrayOf( 130 com.android.internal.R.drawable.ic_wifi_signal_0, 131 com.android.internal.R.drawable.ic_wifi_signal_1, 132 com.android.internal.R.drawable.ic_wifi_signal_2, 133 com.android.internal.R.drawable.ic_wifi_signal_3, 134 com.android.internal.R.drawable.ic_wifi_signal_4 135 ) 136 } 137 } 138 139 val NO_INTERNET_WIFI_PIE = getErrorIconsBasedOnFlag() 140 141 private fun getErrorIconsBasedOnFlag(): IntArray { 142 return if (newStatusBarIcons()) { 143 intArrayOf( 144 R.drawable.ic_wifi_0_error, 145 R.drawable.ic_wifi_1_error, 146 R.drawable.ic_wifi_2_error, 147 R.drawable.ic_wifi_3_error, 148 R.drawable.ic_wifi_4_error 149 ) 150 } else { 151 intArrayOf( 152 R.drawable.ic_no_internet_wifi_signal_0, 153 R.drawable.ic_no_internet_wifi_signal_1, 154 R.drawable.ic_no_internet_wifi_signal_2, 155 R.drawable.ic_no_internet_wifi_signal_3, 156 R.drawable.ic_no_internet_wifi_signal_4 157 ) 158 } 159 } 160 161 @JvmStatic 162 fun buildLoggingSummary(accessPoint: AccessPoint, config: WifiConfiguration?): String { 163 val summary = StringBuilder() 164 val info = accessPoint.info 165 // Add RSSI/band information for this config, what was seen up to 6 seconds ago 166 // verbose WiFi Logging is only turned on thru developers settings 167 if (accessPoint.isActive && info != null) { 168 summary.append(" f=" + info.frequency.toString()) 169 } 170 summary.append(" " + getVisibilityStatus(accessPoint)) 171 if (config != null && (config.networkSelectionStatus.networkSelectionStatus 172 != NetworkSelectionStatus.NETWORK_SELECTION_ENABLED) 173 ) { 174 summary.append(" (" + config.networkSelectionStatus.networkStatusString) 175 if (config.networkSelectionStatus.disableTime > 0) { 176 val now = System.currentTimeMillis() 177 val diff = (now - config.networkSelectionStatus.disableTime) / 1000 178 val sec = diff % 60 // seconds 179 val min = diff / 60 % 60 // minutes 180 val hour = min / 60 % 60 // hours 181 summary.append(", ") 182 if (hour > 0) summary.append(hour.toString() + "h ") 183 summary.append(min.toString() + "m ") 184 summary.append(sec.toString() + "s ") 185 } 186 summary.append(")") 187 } 188 if (config != null) { 189 val networkStatus = config.networkSelectionStatus 190 for (reason in 0..NetworkSelectionStatus.getMaxNetworkSelectionDisableReason()) { 191 if (networkStatus.getDisableReasonCounter(reason) != 0) { 192 summary.append(" ") 193 .append( 194 NetworkSelectionStatus 195 .getNetworkSelectionDisableReasonString(reason) 196 ) 197 .append("=") 198 .append(networkStatus.getDisableReasonCounter(reason)) 199 } 200 } 201 } 202 return summary.toString() 203 } 204 205 /** 206 * Returns the visibility status of the WifiConfiguration. 207 * 208 * @return autojoin debugging information 209 * TODO: use a string formatter 210 * ["rssi 5Ghz", "num results on 5GHz" / "rssi 5Ghz", "num results on 5GHz"] 211 * For instance [-40,5/-30,2] 212 */ 213 @JvmStatic 214 @VisibleForTesting 215 fun getVisibilityStatus(accessPoint: AccessPoint): String { 216 val info = accessPoint.info 217 val visibility = StringBuilder() 218 val scans24GHz = StringBuilder() 219 val scans5GHz = StringBuilder() 220 val scans60GHz = StringBuilder() 221 var bssid: String? = null 222 if (accessPoint.isActive && info != null) { 223 bssid = info.bssid 224 if (bssid != null) { 225 visibility.append(" ").append(bssid) 226 } 227 visibility.append(" standard = ").append(info.wifiStandard) 228 visibility.append(" rssi=").append(info.rssi) 229 visibility.append(" ") 230 visibility.append(" score=").append(info.getScore()) 231 if (accessPoint.speed != AccessPoint.Speed.NONE) { 232 visibility.append(" speed=").append(accessPoint.speedLabel) 233 } 234 visibility.append(String.format(" tx=%.1f,", info.successfulTxPacketsPerSecond)) 235 visibility.append(String.format("%.1f,", info.retriedTxPacketsPerSecond)) 236 visibility.append(String.format("%.1f ", info.lostTxPacketsPerSecond)) 237 visibility.append(String.format("rx=%.1f", info.successfulRxPacketsPerSecond)) 238 } 239 var maxRssi5 = INVALID_RSSI 240 var maxRssi24 = INVALID_RSSI 241 var maxRssi60 = INVALID_RSSI 242 val maxDisplayedScans = 4 243 var num5 = 0 // number of scanned BSSID on 5GHz band 244 var num24 = 0 // number of scanned BSSID on 2.4Ghz band 245 var num60 = 0 // number of scanned BSSID on 60Ghz band 246 val numBlockListed = 0 247 248 // TODO: sort list by RSSI or age 249 val nowMs = SystemClock.elapsedRealtime() 250 for (result in accessPoint.getScanResults()) { 251 if (result == null) { 252 continue 253 } 254 if (result.frequency >= AccessPoint.LOWER_FREQ_5GHZ && 255 result.frequency <= AccessPoint.HIGHER_FREQ_5GHZ 256 ) { 257 // Strictly speaking: [4915, 5825] 258 num5++ 259 if (result.level > maxRssi5) { 260 maxRssi5 = result.level 261 } 262 if (num5 <= maxDisplayedScans) { 263 scans5GHz.append( 264 verboseScanResultSummary( 265 accessPoint, result, bssid, 266 nowMs 267 ) 268 ) 269 } 270 } else if (result.frequency >= AccessPoint.LOWER_FREQ_24GHZ && 271 result.frequency <= AccessPoint.HIGHER_FREQ_24GHZ 272 ) { 273 // Strictly speaking: [2412, 2482] 274 num24++ 275 if (result.level > maxRssi24) { 276 maxRssi24 = result.level 277 } 278 if (num24 <= maxDisplayedScans) { 279 scans24GHz.append( 280 verboseScanResultSummary( 281 accessPoint, result, bssid, 282 nowMs 283 ) 284 ) 285 } 286 } else if (result.frequency >= AccessPoint.LOWER_FREQ_60GHZ && 287 result.frequency <= AccessPoint.HIGHER_FREQ_60GHZ 288 ) { 289 // Strictly speaking: [60000, 61000] 290 num60++ 291 if (result.level > maxRssi60) { 292 maxRssi60 = result.level 293 } 294 if (num60 <= maxDisplayedScans) { 295 scans60GHz.append( 296 verboseScanResultSummary( 297 accessPoint, result, bssid, 298 nowMs 299 ) 300 ) 301 } 302 } 303 } 304 visibility.append(" [") 305 if (num24 > 0) { 306 visibility.append("(").append(num24).append(")") 307 if (num24 > maxDisplayedScans) { 308 visibility.append("max=").append(maxRssi24).append(",") 309 } 310 visibility.append(scans24GHz.toString()) 311 } 312 visibility.append(";") 313 if (num5 > 0) { 314 visibility.append("(").append(num5).append(")") 315 if (num5 > maxDisplayedScans) { 316 visibility.append("max=").append(maxRssi5).append(",") 317 } 318 visibility.append(scans5GHz.toString()) 319 } 320 visibility.append(";") 321 if (num60 > 0) { 322 visibility.append("(").append(num60).append(")") 323 if (num60 > maxDisplayedScans) { 324 visibility.append("max=").append(maxRssi60).append(",") 325 } 326 visibility.append(scans60GHz.toString()) 327 } 328 if (numBlockListed > 0) { 329 visibility.append("!").append(numBlockListed) 330 } 331 visibility.append("]") 332 return visibility.toString() 333 } 334 335 @JvmStatic 336 @VisibleForTesting /* package */ fun verboseScanResultSummary( 337 accessPoint: AccessPoint, 338 result: ScanResult, 339 bssid: String?, 340 nowMs: Long 341 ): String { 342 val stringBuilder = StringBuilder() 343 stringBuilder.append(" \n{").append(result.BSSID) 344 if (result.BSSID == bssid) { 345 stringBuilder.append("*") 346 } 347 stringBuilder.append("=").append(result.frequency) 348 stringBuilder.append(",").append(result.level) 349 val speed = getSpecificApSpeed(result, accessPoint.scoredNetworkCache) 350 if (speed != AccessPoint.Speed.NONE) { 351 stringBuilder.append(",") 352 .append(accessPoint.getSpeedLabel(speed)) 353 } 354 val ageSeconds = (nowMs - result.timestamp / 1000).toInt() / 1000 355 stringBuilder.append(",").append(ageSeconds).append("s") 356 stringBuilder.append("}") 357 return stringBuilder.toString() 358 } 359 360 @AccessPoint.Speed 361 private fun getSpecificApSpeed( 362 result: ScanResult, 363 scoredNetworkCache: Map<String, TimestampedScoredNetwork> 364 ): Int { 365 val timedScore = scoredNetworkCache[result.BSSID] ?: return AccessPoint.Speed.NONE 366 // For debugging purposes we may want to use mRssi rather than result.level as the average 367 // speed wil be determined by mRssi 368 return timedScore.score.calculateBadge(result.level) 369 } 370 371 @JvmStatic 372 fun getMeteredLabel(context: Context, config: WifiConfiguration): String { 373 // meteredOverride is whether the user manually set the metered setting or not. 374 // meteredHint is whether the network itself is telling us that it is metered 375 return if (config.meteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED || 376 config.meteredHint && !isMeteredOverridden( 377 config 378 ) 379 ) { 380 context.getString(R.string.wifi_metered_label) 381 } else context.getString(R.string.wifi_unmetered_label) 382 } 383 384 /** 385 * Returns the Internet icon resource for a given RSSI level. 386 * 387 * @param level The number of bars to show (0-4) 388 * @param noInternet True if a connected Wi-Fi network cannot access the Internet 389 */ 390 @JvmStatic 391 fun getInternetIconResource(level: Int, noInternet: Boolean): Int { 392 var wifiLevel = level 393 if (wifiLevel < 0) { 394 Log.e(TAG, "Wi-Fi level is out of range! level:$level") 395 wifiLevel = 0 396 } else if (level >= WIFI_PIE.size) { 397 Log.e(TAG, "Wi-Fi level is out of range! level:$level") 398 wifiLevel = WIFI_PIE.size - 1 399 } 400 return if (noInternet) NO_INTERNET_WIFI_PIE[wifiLevel] else WIFI_PIE[wifiLevel] 401 } 402 403 /** 404 * Returns the Hotspot network icon resource. 405 * 406 * @param deviceType The device type of Hotspot network 407 */ 408 @JvmStatic 409 fun getHotspotIconResource(deviceType: Int): Int { 410 return when (deviceType) { 411 NetworkProviderInfo.DEVICE_TYPE_PHONE -> R.drawable.ic_hotspot_phone 412 NetworkProviderInfo.DEVICE_TYPE_TABLET -> R.drawable.ic_hotspot_tablet 413 NetworkProviderInfo.DEVICE_TYPE_LAPTOP -> R.drawable.ic_hotspot_laptop 414 NetworkProviderInfo.DEVICE_TYPE_WATCH -> R.drawable.ic_hotspot_watch 415 NetworkProviderInfo.DEVICE_TYPE_AUTO -> R.drawable.ic_hotspot_auto 416 else -> R.drawable.ic_hotspot_phone 417 } 418 } 419 420 @JvmStatic 421 fun isMeteredOverridden(config: WifiConfiguration): Boolean { 422 return config.meteredOverride != WifiConfiguration.METERED_OVERRIDE_NONE 423 } 424 425 /** 426 * Returns the Intent for Wi-Fi dialog. 427 * 428 * @param key The Wi-Fi entry key 429 * @param connectForCaller True if a chosen WifiEntry request to connect to 430 */ 431 @JvmStatic 432 fun getWifiDialogIntent(key: String?, connectForCaller: Boolean): Intent { 433 val intent = Intent(ACTION_WIFI_DIALOG) 434 intent.putExtra(EXTRA_CHOSEN_WIFI_ENTRY_KEY, key) 435 intent.putExtra(EXTRA_CONNECT_FOR_CALLER, connectForCaller) 436 return intent 437 } 438 439 /** 440 * Returns the Intent for Wi-Fi network details settings. 441 * 442 * @param key The Wi-Fi entry key 443 */ 444 @JvmStatic 445 fun getWifiDetailsSettingsIntent(key: String?): Intent { 446 val intent = Intent(ACTION_WIFI_DETAILS_SETTINGS) 447 val bundle = Bundle() 448 bundle.putString(KEY_CHOSEN_WIFIENTRY_KEY, key) 449 intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle) 450 return intent 451 } 452 453 /** 454 * Returns the string of Wi-Fi tethering summary for connected devices. 455 * 456 * @param context The application context 457 * @param connectedDevices The count of connected devices 458 */ 459 @JvmStatic 460 fun getWifiTetherSummaryForConnectedDevices( 461 context: Context, 462 connectedDevices: Int 463 ): String { 464 val msgFormat = MessageFormat( 465 context.resources.getString(R.string.wifi_tether_connected_summary), 466 Locale.getDefault() 467 ) 468 val arguments: MutableMap<String, Any> = HashMap() 469 arguments["count"] = connectedDevices 470 return msgFormat.format(arguments) 471 } 472 473 @JvmStatic 474 fun checkWepAllowed( 475 context: Context, 476 lifecycleOwner: LifecycleOwner, 477 ssid: String, 478 onAllowed: () -> Unit 479 ) { 480 checkWepAllowed( 481 context, 482 lifecycleOwner.lifecycleScope, 483 ssid, 484 WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW, 485 { intent -> context.startActivity(intent) }, 486 onAllowed 487 ) 488 } 489 490 @JvmStatic 491 fun checkWepAllowed( 492 context: Context, 493 coroutineScope: CoroutineScope, 494 ssid: String, 495 dialogWindowType: Int, 496 onStartActivity: (intent: Intent) -> Unit, 497 onAllowed: () -> Unit, 498 ): Job = 499 coroutineScope.launch { 500 val wifiManager = context.getSystemService(WifiManager::class.java) ?: return@launch 501 if (wifiManager.queryWepAllowed()) { 502 onAllowed() 503 } else { 504 val intent = Intent(Intent.ACTION_MAIN).apply { 505 component = ComponentName( 506 "com.android.settings", 507 "com.android.settings.network.WepNetworkDialogActivity" 508 ) 509 putExtra(DIALOG_WINDOW_TYPE, dialogWindowType) 510 putExtra(SSID, ssid) 511 }.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 512 onStartActivity(intent) 513 } 514 } 515 516 private suspend fun WifiManager.queryWepAllowed(): Boolean = 517 withContext(Dispatchers.Default) { 518 suspendCancellableCoroutine { continuation -> 519 queryWepAllowed(Dispatchers.Default.asExecutor()) { 520 continuation.resume(it) 521 } 522 } 523 } 524 525 const val SSID = "ssid" 526 const val DIALOG_WINDOW_TYPE = "dialog_window_type" 527 } 528 } 529