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