• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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