/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settingslib.wifi import android.content.ComponentName import android.content.Context import android.content.Intent import android.graphics.drawable.Drawable import android.icu.text.MessageFormat import android.net.wifi.ScanResult import android.net.wifi.WifiConfiguration import android.net.wifi.WifiConfiguration.NetworkSelectionStatus import android.net.wifi.WifiManager import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo import android.os.Bundle import android.os.SystemClock import android.util.Log import android.view.WindowManager import androidx.annotation.VisibleForTesting import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.android.settingslib.R import com.android.settingslib.flags.Flags.newStatusBarIcons import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.asExecutor import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import java.util.Locale import kotlin.coroutines.resume open class WifiUtils { /** * Wrapper the [.getInternetIconResource] for testing compatibility. */ open class InternetIconInjector(protected val context: Context) { /** * Returns the Internet icon for a given RSSI level. * * @param noInternet True if a connected Wi-Fi network cannot access the Internet * @param level The number of bars to show (0-4) */ open fun getIcon(noInternet: Boolean, level: Int): Drawable? { return context.getDrawable(getInternetIconResource(level, noInternet)) } } companion object { private const val TAG = "WifiUtils" private const val INVALID_RSSI = -127 /** * The intent action shows Wi-Fi dialog to connect Wi-Fi network. * * * Input: The calling package should put the chosen * com.android.wifitrackerlib.WifiEntry#getKey() to a string extra in the request bundle into * the [.EXTRA_CHOSEN_WIFI_ENTRY_KEY]. * * * Output: Nothing. */ @JvmField @VisibleForTesting val ACTION_WIFI_DIALOG = "com.android.settings.WIFI_DIALOG" /** * Specify a key that indicates the WifiEntry to be configured. */ @JvmField @VisibleForTesting val EXTRA_CHOSEN_WIFI_ENTRY_KEY = "key_chosen_wifientry_key" /** * The lookup key for a boolean that indicates whether a chosen WifiEntry request to connect to. * `true` means a chosen WifiEntry request to connect to. */ @JvmField @VisibleForTesting val EXTRA_CONNECT_FOR_CALLER = "connect_for_caller" /** * The intent action shows network details settings to allow configuration of Wi-Fi. * * * In some cases, a matching Activity may not exist, so ensure you * safeguard against this. * * * Input: The calling package should put the chosen * com.android.wifitrackerlib.WifiEntry#getKey() to a string extra in the request bundle into * the [.KEY_CHOSEN_WIFIENTRY_KEY]. * * * Output: Nothing. */ const val ACTION_WIFI_DETAILS_SETTINGS = "android.settings.WIFI_DETAILS_SETTINGS" const val KEY_CHOSEN_WIFIENTRY_KEY = "key_chosen_wifientry_key" const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" @JvmField val WIFI_PIE = getIconsBasedOnFlag() private fun getIconsBasedOnFlag(): IntArray { return if (newStatusBarIcons()) { intArrayOf( R.drawable.ic_wifi_0, R.drawable.ic_wifi_1, R.drawable.ic_wifi_2, R.drawable.ic_wifi_3, R.drawable.ic_wifi_4 ) } else { intArrayOf( com.android.internal.R.drawable.ic_wifi_signal_0, com.android.internal.R.drawable.ic_wifi_signal_1, com.android.internal.R.drawable.ic_wifi_signal_2, com.android.internal.R.drawable.ic_wifi_signal_3, com.android.internal.R.drawable.ic_wifi_signal_4 ) } } val NO_INTERNET_WIFI_PIE = getErrorIconsBasedOnFlag() private fun getErrorIconsBasedOnFlag(): IntArray { return if (newStatusBarIcons()) { intArrayOf( R.drawable.ic_wifi_0_error, R.drawable.ic_wifi_1_error, R.drawable.ic_wifi_2_error, R.drawable.ic_wifi_3_error, R.drawable.ic_wifi_4_error ) } else { intArrayOf( R.drawable.ic_no_internet_wifi_signal_0, R.drawable.ic_no_internet_wifi_signal_1, R.drawable.ic_no_internet_wifi_signal_2, R.drawable.ic_no_internet_wifi_signal_3, R.drawable.ic_no_internet_wifi_signal_4 ) } } @JvmStatic fun buildLoggingSummary(accessPoint: AccessPoint, config: WifiConfiguration?): String { val summary = StringBuilder() val info = accessPoint.info // Add RSSI/band information for this config, what was seen up to 6 seconds ago // verbose WiFi Logging is only turned on thru developers settings if (accessPoint.isActive && info != null) { summary.append(" f=" + info.frequency.toString()) } summary.append(" " + getVisibilityStatus(accessPoint)) if (config != null && (config.networkSelectionStatus.networkSelectionStatus != NetworkSelectionStatus.NETWORK_SELECTION_ENABLED) ) { summary.append(" (" + config.networkSelectionStatus.networkStatusString) if (config.networkSelectionStatus.disableTime > 0) { val now = System.currentTimeMillis() val diff = (now - config.networkSelectionStatus.disableTime) / 1000 val sec = diff % 60 // seconds val min = diff / 60 % 60 // minutes val hour = min / 60 % 60 // hours summary.append(", ") if (hour > 0) summary.append(hour.toString() + "h ") summary.append(min.toString() + "m ") summary.append(sec.toString() + "s ") } summary.append(")") } if (config != null) { val networkStatus = config.networkSelectionStatus for (reason in 0..NetworkSelectionStatus.getMaxNetworkSelectionDisableReason()) { if (networkStatus.getDisableReasonCounter(reason) != 0) { summary.append(" ") .append( NetworkSelectionStatus .getNetworkSelectionDisableReasonString(reason) ) .append("=") .append(networkStatus.getDisableReasonCounter(reason)) } } } return summary.toString() } /** * Returns the visibility status of the WifiConfiguration. * * @return autojoin debugging information * TODO: use a string formatter * ["rssi 5Ghz", "num results on 5GHz" / "rssi 5Ghz", "num results on 5GHz"] * For instance [-40,5/-30,2] */ @JvmStatic @VisibleForTesting fun getVisibilityStatus(accessPoint: AccessPoint): String { val info = accessPoint.info val visibility = StringBuilder() val scans24GHz = StringBuilder() val scans5GHz = StringBuilder() val scans60GHz = StringBuilder() var bssid: String? = null if (accessPoint.isActive && info != null) { bssid = info.bssid if (bssid != null) { visibility.append(" ").append(bssid) } visibility.append(" standard = ").append(info.wifiStandard) visibility.append(" rssi=").append(info.rssi) visibility.append(" ") visibility.append(" score=").append(info.getScore()) if (accessPoint.speed != AccessPoint.Speed.NONE) { visibility.append(" speed=").append(accessPoint.speedLabel) } visibility.append(String.format(" tx=%.1f,", info.successfulTxPacketsPerSecond)) visibility.append(String.format("%.1f,", info.retriedTxPacketsPerSecond)) visibility.append(String.format("%.1f ", info.lostTxPacketsPerSecond)) visibility.append(String.format("rx=%.1f", info.successfulRxPacketsPerSecond)) } var maxRssi5 = INVALID_RSSI var maxRssi24 = INVALID_RSSI var maxRssi60 = INVALID_RSSI val maxDisplayedScans = 4 var num5 = 0 // number of scanned BSSID on 5GHz band var num24 = 0 // number of scanned BSSID on 2.4Ghz band var num60 = 0 // number of scanned BSSID on 60Ghz band val numBlockListed = 0 // TODO: sort list by RSSI or age val nowMs = SystemClock.elapsedRealtime() for (result in accessPoint.getScanResults()) { if (result == null) { continue } if (result.frequency >= AccessPoint.LOWER_FREQ_5GHZ && result.frequency <= AccessPoint.HIGHER_FREQ_5GHZ ) { // Strictly speaking: [4915, 5825] num5++ if (result.level > maxRssi5) { maxRssi5 = result.level } if (num5 <= maxDisplayedScans) { scans5GHz.append( verboseScanResultSummary( accessPoint, result, bssid, nowMs ) ) } } else if (result.frequency >= AccessPoint.LOWER_FREQ_24GHZ && result.frequency <= AccessPoint.HIGHER_FREQ_24GHZ ) { // Strictly speaking: [2412, 2482] num24++ if (result.level > maxRssi24) { maxRssi24 = result.level } if (num24 <= maxDisplayedScans) { scans24GHz.append( verboseScanResultSummary( accessPoint, result, bssid, nowMs ) ) } } else if (result.frequency >= AccessPoint.LOWER_FREQ_60GHZ && result.frequency <= AccessPoint.HIGHER_FREQ_60GHZ ) { // Strictly speaking: [60000, 61000] num60++ if (result.level > maxRssi60) { maxRssi60 = result.level } if (num60 <= maxDisplayedScans) { scans60GHz.append( verboseScanResultSummary( accessPoint, result, bssid, nowMs ) ) } } } visibility.append(" [") if (num24 > 0) { visibility.append("(").append(num24).append(")") if (num24 > maxDisplayedScans) { visibility.append("max=").append(maxRssi24).append(",") } visibility.append(scans24GHz.toString()) } visibility.append(";") if (num5 > 0) { visibility.append("(").append(num5).append(")") if (num5 > maxDisplayedScans) { visibility.append("max=").append(maxRssi5).append(",") } visibility.append(scans5GHz.toString()) } visibility.append(";") if (num60 > 0) { visibility.append("(").append(num60).append(")") if (num60 > maxDisplayedScans) { visibility.append("max=").append(maxRssi60).append(",") } visibility.append(scans60GHz.toString()) } if (numBlockListed > 0) { visibility.append("!").append(numBlockListed) } visibility.append("]") return visibility.toString() } @JvmStatic @VisibleForTesting /* package */ fun verboseScanResultSummary( accessPoint: AccessPoint, result: ScanResult, bssid: String?, nowMs: Long ): String { val stringBuilder = StringBuilder() stringBuilder.append(" \n{").append(result.BSSID) if (result.BSSID == bssid) { stringBuilder.append("*") } stringBuilder.append("=").append(result.frequency) stringBuilder.append(",").append(result.level) val speed = getSpecificApSpeed(result, accessPoint.scoredNetworkCache) if (speed != AccessPoint.Speed.NONE) { stringBuilder.append(",") .append(accessPoint.getSpeedLabel(speed)) } val ageSeconds = (nowMs - result.timestamp / 1000).toInt() / 1000 stringBuilder.append(",").append(ageSeconds).append("s") stringBuilder.append("}") return stringBuilder.toString() } @AccessPoint.Speed private fun getSpecificApSpeed( result: ScanResult, scoredNetworkCache: Map ): Int { val timedScore = scoredNetworkCache[result.BSSID] ?: return AccessPoint.Speed.NONE // For debugging purposes we may want to use mRssi rather than result.level as the average // speed wil be determined by mRssi return timedScore.score.calculateBadge(result.level) } @JvmStatic fun getMeteredLabel(context: Context, config: WifiConfiguration): String { // meteredOverride is whether the user manually set the metered setting or not. // meteredHint is whether the network itself is telling us that it is metered return if (config.meteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED || config.meteredHint && !isMeteredOverridden( config ) ) { context.getString(R.string.wifi_metered_label) } else context.getString(R.string.wifi_unmetered_label) } /** * Returns the Internet icon resource for a given RSSI level. * * @param level The number of bars to show (0-4) * @param noInternet True if a connected Wi-Fi network cannot access the Internet */ @JvmStatic fun getInternetIconResource(level: Int, noInternet: Boolean): Int { var wifiLevel = level if (wifiLevel < 0) { Log.e(TAG, "Wi-Fi level is out of range! level:$level") wifiLevel = 0 } else if (level >= WIFI_PIE.size) { Log.e(TAG, "Wi-Fi level is out of range! level:$level") wifiLevel = WIFI_PIE.size - 1 } return if (noInternet) NO_INTERNET_WIFI_PIE[wifiLevel] else WIFI_PIE[wifiLevel] } /** * Returns the Hotspot network icon resource. * * @param deviceType The device type of Hotspot network */ @JvmStatic fun getHotspotIconResource(deviceType: Int): Int { return when (deviceType) { NetworkProviderInfo.DEVICE_TYPE_PHONE -> R.drawable.ic_hotspot_phone NetworkProviderInfo.DEVICE_TYPE_TABLET -> R.drawable.ic_hotspot_tablet NetworkProviderInfo.DEVICE_TYPE_LAPTOP -> R.drawable.ic_hotspot_laptop NetworkProviderInfo.DEVICE_TYPE_WATCH -> R.drawable.ic_hotspot_watch NetworkProviderInfo.DEVICE_TYPE_AUTO -> R.drawable.ic_hotspot_auto else -> R.drawable.ic_hotspot_phone } } @JvmStatic fun isMeteredOverridden(config: WifiConfiguration): Boolean { return config.meteredOverride != WifiConfiguration.METERED_OVERRIDE_NONE } /** * Returns the Intent for Wi-Fi dialog. * * @param key The Wi-Fi entry key * @param connectForCaller True if a chosen WifiEntry request to connect to */ @JvmStatic fun getWifiDialogIntent(key: String?, connectForCaller: Boolean): Intent { val intent = Intent(ACTION_WIFI_DIALOG) intent.putExtra(EXTRA_CHOSEN_WIFI_ENTRY_KEY, key) intent.putExtra(EXTRA_CONNECT_FOR_CALLER, connectForCaller) return intent } /** * Returns the Intent for Wi-Fi network details settings. * * @param key The Wi-Fi entry key */ @JvmStatic fun getWifiDetailsSettingsIntent(key: String?): Intent { val intent = Intent(ACTION_WIFI_DETAILS_SETTINGS) val bundle = Bundle() bundle.putString(KEY_CHOSEN_WIFIENTRY_KEY, key) intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle) return intent } /** * Returns the string of Wi-Fi tethering summary for connected devices. * * @param context The application context * @param connectedDevices The count of connected devices */ @JvmStatic fun getWifiTetherSummaryForConnectedDevices( context: Context, connectedDevices: Int ): String { val msgFormat = MessageFormat( context.resources.getString(R.string.wifi_tether_connected_summary), Locale.getDefault() ) val arguments: MutableMap = HashMap() arguments["count"] = connectedDevices return msgFormat.format(arguments) } @JvmStatic fun checkWepAllowed( context: Context, lifecycleOwner: LifecycleOwner, ssid: String, onAllowed: () -> Unit ) { checkWepAllowed( context, lifecycleOwner.lifecycleScope, ssid, WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW, { intent -> context.startActivity(intent) }, onAllowed ) } @JvmStatic fun checkWepAllowed( context: Context, coroutineScope: CoroutineScope, ssid: String, dialogWindowType: Int, onStartActivity: (intent: Intent) -> Unit, onAllowed: () -> Unit, ): Job = coroutineScope.launch { val wifiManager = context.getSystemService(WifiManager::class.java) ?: return@launch if (wifiManager.queryWepAllowed()) { onAllowed() } else { val intent = Intent(Intent.ACTION_MAIN).apply { component = ComponentName( "com.android.settings", "com.android.settings.network.WepNetworkDialogActivity" ) putExtra(DIALOG_WINDOW_TYPE, dialogWindowType) putExtra(SSID, ssid) }.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) onStartActivity(intent) } } private suspend fun WifiManager.queryWepAllowed(): Boolean = withContext(Dispatchers.Default) { suspendCancellableCoroutine { continuation -> queryWepAllowed(Dispatchers.Default.asExecutor()) { continuation.resume(it) } } } const val SSID = "ssid" const val DIALOG_WINDOW_TYPE = "dialog_window_type" } }