1 /* 2 * Copyright (C) 2017 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 17 package com.android.settingslib.wifi; 18 19 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLED; 20 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.getMaxNetworkSelectionDisableReason; 21 22 import android.content.Context; 23 import android.content.Intent; 24 import android.graphics.drawable.Drawable; 25 import android.icu.text.MessageFormat; 26 import android.net.wifi.ScanResult; 27 import android.net.wifi.WifiConfiguration; 28 import android.net.wifi.WifiConfiguration.NetworkSelectionStatus; 29 import android.net.wifi.WifiInfo; 30 import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo; 31 import android.os.Bundle; 32 import android.os.SystemClock; 33 import android.util.Log; 34 35 import androidx.annotation.VisibleForTesting; 36 37 import com.android.settingslib.R; 38 39 import java.util.HashMap; 40 import java.util.Locale; 41 import java.util.Map; 42 43 public class WifiUtils { 44 45 private static final String TAG = "WifiUtils"; 46 47 private static final int INVALID_RSSI = -127; 48 49 /** 50 * The intent action shows Wi-Fi dialog to connect Wi-Fi network. 51 * <p> 52 * Input: The calling package should put the chosen 53 * com.android.wifitrackerlib.WifiEntry#getKey() to a string extra in the request bundle into 54 * the {@link #EXTRA_CHOSEN_WIFI_ENTRY_KEY}. 55 * <p> 56 * Output: Nothing. 57 */ 58 @VisibleForTesting 59 static final String ACTION_WIFI_DIALOG = "com.android.settings.WIFI_DIALOG"; 60 61 /** 62 * Specify a key that indicates the WifiEntry to be configured. 63 */ 64 @VisibleForTesting 65 static final String EXTRA_CHOSEN_WIFI_ENTRY_KEY = "key_chosen_wifientry_key"; 66 67 /** 68 * The lookup key for a boolean that indicates whether a chosen WifiEntry request to connect to. 69 * {@code true} means a chosen WifiEntry request to connect to. 70 */ 71 @VisibleForTesting 72 static final String EXTRA_CONNECT_FOR_CALLER = "connect_for_caller"; 73 74 /** 75 * The intent action shows network details settings to allow configuration of Wi-Fi. 76 * <p> 77 * In some cases, a matching Activity may not exist, so ensure you 78 * safeguard against this. 79 * <p> 80 * Input: The calling package should put the chosen 81 * com.android.wifitrackerlib.WifiEntry#getKey() to a string extra in the request bundle into 82 * the {@link #KEY_CHOSEN_WIFIENTRY_KEY}. 83 * <p> 84 * Output: Nothing. 85 */ 86 public static final String ACTION_WIFI_DETAILS_SETTINGS = 87 "android.settings.WIFI_DETAILS_SETTINGS"; 88 public static final String KEY_CHOSEN_WIFIENTRY_KEY = "key_chosen_wifientry_key"; 89 public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"; 90 91 static final int[] WIFI_PIE = { 92 com.android.internal.R.drawable.ic_wifi_signal_0, 93 com.android.internal.R.drawable.ic_wifi_signal_1, 94 com.android.internal.R.drawable.ic_wifi_signal_2, 95 com.android.internal.R.drawable.ic_wifi_signal_3, 96 com.android.internal.R.drawable.ic_wifi_signal_4 97 }; 98 99 static final int[] NO_INTERNET_WIFI_PIE = { 100 R.drawable.ic_no_internet_wifi_signal_0, 101 R.drawable.ic_no_internet_wifi_signal_1, 102 R.drawable.ic_no_internet_wifi_signal_2, 103 R.drawable.ic_no_internet_wifi_signal_3, 104 R.drawable.ic_no_internet_wifi_signal_4 105 }; 106 buildLoggingSummary(AccessPoint accessPoint, WifiConfiguration config)107 public static String buildLoggingSummary(AccessPoint accessPoint, WifiConfiguration config) { 108 final StringBuilder summary = new StringBuilder(); 109 final WifiInfo info = accessPoint.getInfo(); 110 // Add RSSI/band information for this config, what was seen up to 6 seconds ago 111 // verbose WiFi Logging is only turned on thru developers settings 112 if (accessPoint.isActive() && info != null) { 113 summary.append(" f=" + Integer.toString(info.getFrequency())); 114 } 115 summary.append(" " + getVisibilityStatus(accessPoint)); 116 if (config != null 117 && (config.getNetworkSelectionStatus().getNetworkSelectionStatus() 118 != NETWORK_SELECTION_ENABLED)) { 119 summary.append(" (" + config.getNetworkSelectionStatus().getNetworkStatusString()); 120 if (config.getNetworkSelectionStatus().getDisableTime() > 0) { 121 long now = System.currentTimeMillis(); 122 long diff = (now - config.getNetworkSelectionStatus().getDisableTime()) / 1000; 123 long sec = diff % 60; //seconds 124 long min = (diff / 60) % 60; //minutes 125 long hour = (min / 60) % 60; //hours 126 summary.append(", "); 127 if (hour > 0) summary.append(Long.toString(hour) + "h "); 128 summary.append(Long.toString(min) + "m "); 129 summary.append(Long.toString(sec) + "s "); 130 } 131 summary.append(")"); 132 } 133 134 if (config != null) { 135 NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus(); 136 for (int reason = 0; reason <= getMaxNetworkSelectionDisableReason(); reason++) { 137 if (networkStatus.getDisableReasonCounter(reason) != 0) { 138 summary.append(" ") 139 .append(NetworkSelectionStatus 140 .getNetworkSelectionDisableReasonString(reason)) 141 .append("=") 142 .append(networkStatus.getDisableReasonCounter(reason)); 143 } 144 } 145 } 146 147 return summary.toString(); 148 } 149 150 /** 151 * Returns the visibility status of the WifiConfiguration. 152 * 153 * @return autojoin debugging information 154 * TODO: use a string formatter 155 * ["rssi 5Ghz", "num results on 5GHz" / "rssi 5Ghz", "num results on 5GHz"] 156 * For instance [-40,5/-30,2] 157 */ 158 @VisibleForTesting getVisibilityStatus(AccessPoint accessPoint)159 static String getVisibilityStatus(AccessPoint accessPoint) { 160 final WifiInfo info = accessPoint.getInfo(); 161 StringBuilder visibility = new StringBuilder(); 162 StringBuilder scans24GHz = new StringBuilder(); 163 StringBuilder scans5GHz = new StringBuilder(); 164 StringBuilder scans60GHz = new StringBuilder(); 165 String bssid = null; 166 167 if (accessPoint.isActive() && info != null) { 168 bssid = info.getBSSID(); 169 if (bssid != null) { 170 visibility.append(" ").append(bssid); 171 } 172 visibility.append(" standard = ").append(info.getWifiStandard()); 173 visibility.append(" rssi=").append(info.getRssi()); 174 visibility.append(" "); 175 visibility.append(" score=").append(info.getScore()); 176 if (accessPoint.getSpeed() != AccessPoint.Speed.NONE) { 177 visibility.append(" speed=").append(accessPoint.getSpeedLabel()); 178 } 179 visibility.append(String.format(" tx=%.1f,", info.getSuccessfulTxPacketsPerSecond())); 180 visibility.append(String.format("%.1f,", info.getRetriedTxPacketsPerSecond())); 181 visibility.append(String.format("%.1f ", info.getLostTxPacketsPerSecond())); 182 visibility.append(String.format("rx=%.1f", info.getSuccessfulRxPacketsPerSecond())); 183 } 184 185 int maxRssi5 = INVALID_RSSI; 186 int maxRssi24 = INVALID_RSSI; 187 int maxRssi60 = INVALID_RSSI; 188 final int maxDisplayedScans = 4; 189 int num5 = 0; // number of scanned BSSID on 5GHz band 190 int num24 = 0; // number of scanned BSSID on 2.4Ghz band 191 int num60 = 0; // number of scanned BSSID on 60Ghz band 192 int numBlockListed = 0; 193 194 // TODO: sort list by RSSI or age 195 long nowMs = SystemClock.elapsedRealtime(); 196 for (ScanResult result : accessPoint.getScanResults()) { 197 if (result == null) { 198 continue; 199 } 200 if (result.frequency >= AccessPoint.LOWER_FREQ_5GHZ 201 && result.frequency <= AccessPoint.HIGHER_FREQ_5GHZ) { 202 // Strictly speaking: [4915, 5825] 203 num5++; 204 205 if (result.level > maxRssi5) { 206 maxRssi5 = result.level; 207 } 208 if (num5 <= maxDisplayedScans) { 209 scans5GHz.append( 210 verboseScanResultSummary(accessPoint, result, bssid, 211 nowMs)); 212 } 213 } else if (result.frequency >= AccessPoint.LOWER_FREQ_24GHZ 214 && result.frequency <= AccessPoint.HIGHER_FREQ_24GHZ) { 215 // Strictly speaking: [2412, 2482] 216 num24++; 217 218 if (result.level > maxRssi24) { 219 maxRssi24 = result.level; 220 } 221 if (num24 <= maxDisplayedScans) { 222 scans24GHz.append( 223 verboseScanResultSummary(accessPoint, result, bssid, 224 nowMs)); 225 } 226 } else if (result.frequency >= AccessPoint.LOWER_FREQ_60GHZ 227 && result.frequency <= AccessPoint.HIGHER_FREQ_60GHZ) { 228 // Strictly speaking: [60000, 61000] 229 num60++; 230 231 if (result.level > maxRssi60) { 232 maxRssi60 = result.level; 233 } 234 if (num60 <= maxDisplayedScans) { 235 scans60GHz.append( 236 verboseScanResultSummary(accessPoint, result, bssid, 237 nowMs)); 238 } 239 } 240 } 241 visibility.append(" ["); 242 if (num24 > 0) { 243 visibility.append("(").append(num24).append(")"); 244 if (num24 > maxDisplayedScans) { 245 visibility.append("max=").append(maxRssi24).append(","); 246 } 247 visibility.append(scans24GHz.toString()); 248 } 249 visibility.append(";"); 250 if (num5 > 0) { 251 visibility.append("(").append(num5).append(")"); 252 if (num5 > maxDisplayedScans) { 253 visibility.append("max=").append(maxRssi5).append(","); 254 } 255 visibility.append(scans5GHz.toString()); 256 } 257 visibility.append(";"); 258 if (num60 > 0) { 259 visibility.append("(").append(num60).append(")"); 260 if (num60 > maxDisplayedScans) { 261 visibility.append("max=").append(maxRssi60).append(","); 262 } 263 visibility.append(scans60GHz.toString()); 264 } 265 if (numBlockListed > 0) { 266 visibility.append("!").append(numBlockListed); 267 } 268 visibility.append("]"); 269 270 return visibility.toString(); 271 } 272 273 @VisibleForTesting verboseScanResultSummary(AccessPoint accessPoint, ScanResult result, String bssid, long nowMs)274 /* package */ static String verboseScanResultSummary(AccessPoint accessPoint, ScanResult result, 275 String bssid, long nowMs) { 276 StringBuilder stringBuilder = new StringBuilder(); 277 stringBuilder.append(" \n{").append(result.BSSID); 278 if (result.BSSID.equals(bssid)) { 279 stringBuilder.append("*"); 280 } 281 stringBuilder.append("=").append(result.frequency); 282 stringBuilder.append(",").append(result.level); 283 int speed = getSpecificApSpeed(result, accessPoint.getScoredNetworkCache()); 284 if (speed != AccessPoint.Speed.NONE) { 285 stringBuilder.append(",") 286 .append(accessPoint.getSpeedLabel(speed)); 287 } 288 int ageSeconds = (int) (nowMs - result.timestamp / 1000) / 1000; 289 stringBuilder.append(",").append(ageSeconds).append("s"); 290 stringBuilder.append("}"); 291 return stringBuilder.toString(); 292 } 293 294 @AccessPoint.Speed getSpecificApSpeed(ScanResult result, Map<String, TimestampedScoredNetwork> scoredNetworkCache)295 private static int getSpecificApSpeed(ScanResult result, 296 Map<String, TimestampedScoredNetwork> scoredNetworkCache) { 297 TimestampedScoredNetwork timedScore = scoredNetworkCache.get(result.BSSID); 298 if (timedScore == null) { 299 return AccessPoint.Speed.NONE; 300 } 301 // For debugging purposes we may want to use mRssi rather than result.level as the average 302 // speed wil be determined by mRssi 303 return timedScore.getScore().calculateBadge(result.level); 304 } 305 getMeteredLabel(Context context, WifiConfiguration config)306 public static String getMeteredLabel(Context context, WifiConfiguration config) { 307 // meteredOverride is whether the user manually set the metered setting or not. 308 // meteredHint is whether the network itself is telling us that it is metered 309 if (config.meteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED 310 || (config.meteredHint && !isMeteredOverridden(config))) { 311 return context.getString(R.string.wifi_metered_label); 312 } 313 return context.getString(R.string.wifi_unmetered_label); 314 } 315 316 /** 317 * Returns the Internet icon resource for a given RSSI level. 318 * 319 * @param level The number of bars to show (0-4) 320 * @param noInternet True if a connected Wi-Fi network cannot access the Internet 321 */ getInternetIconResource(int level, boolean noInternet)322 public static int getInternetIconResource(int level, boolean noInternet) { 323 int wifiLevel = level; 324 if (wifiLevel < 0) { 325 Log.e(TAG, "Wi-Fi level is out of range! level:" + level); 326 wifiLevel = 0; 327 } else if (level >= WIFI_PIE.length) { 328 Log.e(TAG, "Wi-Fi level is out of range! level:" + level); 329 wifiLevel = WIFI_PIE.length - 1; 330 } 331 return noInternet ? NO_INTERNET_WIFI_PIE[wifiLevel] : WIFI_PIE[wifiLevel]; 332 } 333 334 /** 335 * Returns the Hotspot network icon resource. 336 * 337 * @param deviceType The device type of Hotspot network 338 */ getHotspotIconResource(int deviceType)339 public static int getHotspotIconResource(int deviceType) { 340 return switch (deviceType) { 341 case NetworkProviderInfo.DEVICE_TYPE_PHONE -> R.drawable.ic_hotspot_phone; 342 case NetworkProviderInfo.DEVICE_TYPE_TABLET -> R.drawable.ic_hotspot_tablet; 343 case NetworkProviderInfo.DEVICE_TYPE_LAPTOP -> R.drawable.ic_hotspot_laptop; 344 case NetworkProviderInfo.DEVICE_TYPE_WATCH -> R.drawable.ic_hotspot_watch; 345 case NetworkProviderInfo.DEVICE_TYPE_AUTO -> R.drawable.ic_hotspot_auto; 346 default -> R.drawable.ic_hotspot_phone; // Return phone icon as default. 347 }; 348 } 349 350 /** 351 * Wrapper the {@link #getInternetIconResource} for testing compatibility. 352 */ 353 public static class InternetIconInjector { 354 355 protected final Context mContext; 356 InternetIconInjector(Context context)357 public InternetIconInjector(Context context) { 358 mContext = context; 359 } 360 361 /** 362 * Returns the Internet icon for a given RSSI level. 363 * 364 * @param noInternet True if a connected Wi-Fi network cannot access the Internet 365 * @param level The number of bars to show (0-4) 366 */ getIcon(boolean noInternet, int level)367 public Drawable getIcon(boolean noInternet, int level) { 368 return mContext.getDrawable(WifiUtils.getInternetIconResource(level, noInternet)); 369 } 370 } 371 isMeteredOverridden(WifiConfiguration config)372 public static boolean isMeteredOverridden(WifiConfiguration config) { 373 return config.meteredOverride != WifiConfiguration.METERED_OVERRIDE_NONE; 374 } 375 376 /** 377 * Returns the Intent for Wi-Fi dialog. 378 * 379 * @param key The Wi-Fi entry key 380 * @param connectForCaller True if a chosen WifiEntry request to connect to 381 */ getWifiDialogIntent(String key, boolean connectForCaller)382 public static Intent getWifiDialogIntent(String key, boolean connectForCaller) { 383 final Intent intent = new Intent(ACTION_WIFI_DIALOG); 384 intent.putExtra(EXTRA_CHOSEN_WIFI_ENTRY_KEY, key); 385 intent.putExtra(EXTRA_CONNECT_FOR_CALLER, connectForCaller); 386 return intent; 387 } 388 389 /** 390 * Returns the Intent for Wi-Fi network details settings. 391 * 392 * @param key The Wi-Fi entry key 393 */ getWifiDetailsSettingsIntent(String key)394 public static Intent getWifiDetailsSettingsIntent(String key) { 395 final Intent intent = new Intent(ACTION_WIFI_DETAILS_SETTINGS); 396 final Bundle bundle = new Bundle(); 397 bundle.putString(KEY_CHOSEN_WIFIENTRY_KEY, key); 398 intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle); 399 return intent; 400 } 401 402 /** 403 * Returns the string of Wi-Fi tethering summary for connected devices. 404 * 405 * @param context The application context 406 * @param connectedDevices The count of connected devices 407 */ getWifiTetherSummaryForConnectedDevices(Context context, int connectedDevices)408 public static String getWifiTetherSummaryForConnectedDevices(Context context, 409 int connectedDevices) { 410 MessageFormat msgFormat = new MessageFormat( 411 context.getResources().getString(R.string.wifi_tether_connected_summary), 412 Locale.getDefault()); 413 Map<String, Object> arguments = new HashMap<>(); 414 arguments.put("count", connectedDevices); 415 return msgFormat.format(arguments); 416 } 417 } 418