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