• 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.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