1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.Q; 4 import static android.os.Build.VERSION_CODES.R; 5 import static android.os.Build.VERSION_CODES.S; 6 import static android.os.Build.VERSION_CODES.TIRAMISU; 7 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 8 import static java.util.stream.Collectors.toList; 9 10 import android.app.admin.DevicePolicyManager; 11 import android.content.Context; 12 import android.content.Intent; 13 import android.net.ConnectivityManager; 14 import android.net.DhcpInfo; 15 import android.net.NetworkInfo; 16 import android.net.wifi.ScanResult; 17 import android.net.wifi.SoftApConfiguration; 18 import android.net.wifi.WifiConfiguration; 19 import android.net.wifi.WifiInfo; 20 import android.net.wifi.WifiManager; 21 import android.net.wifi.WifiManager.AddNetworkResult; 22 import android.net.wifi.WifiManager.LocalOnlyConnectionFailureListener; 23 import android.net.wifi.WifiManager.MulticastLock; 24 import android.net.wifi.WifiNetworkSpecifier; 25 import android.net.wifi.WifiSsid; 26 import android.net.wifi.WifiUsabilityStatsEntry; 27 import android.os.Binder; 28 import android.os.Handler; 29 import android.os.Looper; 30 import android.provider.Settings; 31 import android.util.ArraySet; 32 import android.util.Pair; 33 import com.google.common.collect.ImmutableList; 34 import java.lang.reflect.InvocationTargetException; 35 import java.lang.reflect.Method; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.BitSet; 39 import java.util.HashSet; 40 import java.util.LinkedHashMap; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Set; 44 import java.util.concurrent.ConcurrentHashMap; 45 import java.util.concurrent.ConcurrentMap; 46 import java.util.concurrent.Executor; 47 import java.util.concurrent.atomic.AtomicInteger; 48 import org.robolectric.RuntimeEnvironment; 49 import org.robolectric.annotation.HiddenApi; 50 import org.robolectric.annotation.Implementation; 51 import org.robolectric.annotation.Implements; 52 import org.robolectric.annotation.RealObject; 53 import org.robolectric.shadow.api.Shadow; 54 import org.robolectric.util.ReflectionHelpers; 55 56 /** Shadow for {@link android.net.wifi.WifiManager}. */ 57 @Implements(value = WifiManager.class, looseSignatures = true) 58 @SuppressWarnings("AndroidConcurrentHashMap") 59 public class ShadowWifiManager { 60 private static final int LOCAL_HOST = 2130706433; 61 62 private static float sSignalLevelInPercent = 1f; 63 private boolean accessWifiStatePermission = true; 64 private boolean changeWifiStatePermission = true; 65 private int wifiState = WifiManager.WIFI_STATE_ENABLED; 66 private boolean wasSaved = false; 67 private WifiInfo wifiInfo; 68 private List<ScanResult> scanResults; 69 private final Map<Integer, WifiConfiguration> networkIdToConfiguredNetworks = 70 new LinkedHashMap<>(); 71 private Pair<Integer, Boolean> lastEnabledNetwork; 72 private final Set<Integer> enabledNetworks = new HashSet<>(); 73 private DhcpInfo dhcpInfo; 74 private boolean startScanSucceeds = true; 75 private boolean is5GHzBandSupported = false; 76 private boolean isStaApConcurrencySupported = false; 77 private boolean isWpa3SaeSupported = false; 78 private boolean isWpa3SaeH2eSupported = false; 79 private boolean isWpa3SaePublicKeySupported = false; 80 private boolean isWpa3SuiteBSupported = false; 81 private AtomicInteger activeLockCount = new AtomicInteger(0); 82 private final BitSet readOnlyNetworkIds = new BitSet(); 83 private final ConcurrentHashMap<WifiManager.OnWifiUsabilityStatsListener, Executor> 84 wifiUsabilityStatsListeners = new ConcurrentHashMap<>(); 85 private final List<WifiUsabilityScore> usabilityScores = new ArrayList<>(); 86 private Object networkScorer; 87 @RealObject WifiManager wifiManager; 88 private WifiConfiguration apConfig; 89 private SoftApConfiguration softApConfig; 90 private final Object pnoRequestLock = new Object(); 91 private PnoScanRequest outstandingPnoScanRequest = null; 92 93 private final ConcurrentMap<LocalOnlyConnectionFailureListener, Executor> 94 localOnlyConnectionFailureListenerExecutorMap = new ConcurrentHashMap<>(); 95 96 /** 97 * Simulates a connection failure for a specified local network connection. 98 * 99 * @param specifier the {@link WifiNetworkSpecifier} describing the local network connection 100 * attempt 101 * @param failureReason the reason for the network connection failure. This should be one of the 102 * values specified in {@code WifiManager#STATUS_LOCAL_ONLY_CONNECTION_FAILURE_*} 103 */ triggerLocalConnectionFailure(WifiNetworkSpecifier specifier, int failureReason)104 public void triggerLocalConnectionFailure(WifiNetworkSpecifier specifier, int failureReason) { 105 localOnlyConnectionFailureListenerExecutorMap.forEach( 106 (failureListener, executor) -> 107 executor.execute(() -> failureListener.onConnectionFailed(specifier, failureReason))); 108 } 109 110 @Implementation(minSdk = UPSIDE_DOWN_CAKE) addLocalOnlyConnectionFailureListener( Executor executor, LocalOnlyConnectionFailureListener listener)111 protected void addLocalOnlyConnectionFailureListener( 112 Executor executor, LocalOnlyConnectionFailureListener listener) { 113 if (listener == null) { 114 throw new IllegalArgumentException("Listener cannot be null"); 115 } 116 if (executor == null) { 117 throw new IllegalArgumentException("Executor cannot be null"); 118 } 119 localOnlyConnectionFailureListenerExecutorMap.putIfAbsent(listener, executor); 120 } 121 122 @Implementation(minSdk = UPSIDE_DOWN_CAKE) removeLocalOnlyConnectionFailureListener( LocalOnlyConnectionFailureListener listener)123 protected void removeLocalOnlyConnectionFailureListener( 124 LocalOnlyConnectionFailureListener listener) { 125 if (listener == null) { 126 throw new IllegalArgumentException("Listener cannot be null"); 127 } 128 localOnlyConnectionFailureListenerExecutorMap.remove(listener); 129 } 130 131 @Implementation setWifiEnabled(boolean wifiEnabled)132 protected boolean setWifiEnabled(boolean wifiEnabled) { 133 checkAccessWifiStatePermission(); 134 this.wifiState = wifiEnabled ? WifiManager.WIFI_STATE_ENABLED : WifiManager.WIFI_STATE_DISABLED; 135 return true; 136 } 137 setWifiState(int wifiState)138 public void setWifiState(int wifiState) { 139 checkAccessWifiStatePermission(); 140 this.wifiState = wifiState; 141 } 142 143 @Implementation isWifiEnabled()144 protected boolean isWifiEnabled() { 145 checkAccessWifiStatePermission(); 146 return wifiState == WifiManager.WIFI_STATE_ENABLED; 147 } 148 149 @Implementation getWifiState()150 protected int getWifiState() { 151 checkAccessWifiStatePermission(); 152 return wifiState; 153 } 154 155 @Implementation getConnectionInfo()156 protected WifiInfo getConnectionInfo() { 157 checkAccessWifiStatePermission(); 158 if (wifiInfo == null) { 159 wifiInfo = ReflectionHelpers.callConstructor(WifiInfo.class); 160 } 161 return wifiInfo; 162 } 163 164 @Implementation is5GHzBandSupported()165 protected boolean is5GHzBandSupported() { 166 return is5GHzBandSupported; 167 } 168 169 /** Sets whether 5ghz band is supported. */ setIs5GHzBandSupported(boolean is5GHzBandSupported)170 public void setIs5GHzBandSupported(boolean is5GHzBandSupported) { 171 this.is5GHzBandSupported = is5GHzBandSupported; 172 } 173 174 /** Returns last value provided to {@link #setStaApConcurrencySupported}. */ 175 @Implementation(minSdk = R) isStaApConcurrencySupported()176 protected boolean isStaApConcurrencySupported() { 177 return isStaApConcurrencySupported; 178 } 179 180 /** Sets whether STA/AP concurrency is supported. */ setStaApConcurrencySupported(boolean isStaApConcurrencySupported)181 public void setStaApConcurrencySupported(boolean isStaApConcurrencySupported) { 182 this.isStaApConcurrencySupported = isStaApConcurrencySupported; 183 } 184 185 /** Returns last value provided to {@link #setWpa3SaeSupported}. */ 186 @Implementation(minSdk = Q) isWpa3SaeSupported()187 protected boolean isWpa3SaeSupported() { 188 return isWpa3SaeSupported; 189 } 190 191 /** Sets whether WPA3-Personal SAE is supported. */ setWpa3SaeSupported(boolean isWpa3SaeSupported)192 public void setWpa3SaeSupported(boolean isWpa3SaeSupported) { 193 this.isWpa3SaeSupported = isWpa3SaeSupported; 194 } 195 196 /** Returns last value provided to {@link #setWpa3SaePublicKeySupported}. */ 197 @Implementation(minSdk = S) isWpa3SaePublicKeySupported()198 protected boolean isWpa3SaePublicKeySupported() { 199 return isWpa3SaePublicKeySupported; 200 } 201 202 /** Sets whether WPA3 SAE Public Key is supported. */ setWpa3SaePublicKeySupported(boolean isWpa3SaePublicKeySupported)203 public void setWpa3SaePublicKeySupported(boolean isWpa3SaePublicKeySupported) { 204 this.isWpa3SaePublicKeySupported = isWpa3SaePublicKeySupported; 205 } 206 207 /** Returns last value provided to {@link #setWpa3SaeH2eSupported}. */ 208 @Implementation(minSdk = S) isWpa3SaeH2eSupported()209 protected boolean isWpa3SaeH2eSupported() { 210 return isWpa3SaeH2eSupported; 211 } 212 213 /** Sets whether WPA3 SAE Hash-to-Element is supported. */ setWpa3SaeH2eSupported(boolean isWpa3SaeH2eSupported)214 public void setWpa3SaeH2eSupported(boolean isWpa3SaeH2eSupported) { 215 this.isWpa3SaeH2eSupported = isWpa3SaeH2eSupported; 216 } 217 218 /** Returns last value provided to {@link #setWpa3SuiteBSupported}. */ 219 @Implementation(minSdk = Q) isWpa3SuiteBSupported()220 protected boolean isWpa3SuiteBSupported() { 221 return isWpa3SuiteBSupported; 222 } 223 224 /** Sets whether WPA3-Enterprise Suite-B-192 is supported. */ setWpa3SuiteBSupported(boolean isWpa3SuiteBSupported)225 public void setWpa3SuiteBSupported(boolean isWpa3SuiteBSupported) { 226 this.isWpa3SuiteBSupported = isWpa3SuiteBSupported; 227 } 228 229 /** Sets the connection info as the provided {@link WifiInfo}. */ setConnectionInfo(WifiInfo wifiInfo)230 public void setConnectionInfo(WifiInfo wifiInfo) { 231 this.wifiInfo = wifiInfo; 232 } 233 234 /** Sets the return value of {@link #startScan}. */ setStartScanSucceeds(boolean succeeds)235 public void setStartScanSucceeds(boolean succeeds) { 236 this.startScanSucceeds = succeeds; 237 } 238 239 @Implementation getScanResults()240 protected List<ScanResult> getScanResults() { 241 return scanResults; 242 } 243 244 /** 245 * The original implementation allows this to be called by the Device Owner (DO), Profile Owner 246 * (PO), callers with carrier privilege and system apps, but this shadow can be called by all apps 247 * carrying the ACCESS_WIFI_STATE permission. 248 * 249 * <p>This shadow is a wrapper for getConfiguredNetworks() and does not actually check the caller. 250 */ 251 @Implementation(minSdk = S) getCallerConfiguredNetworks()252 protected List<WifiConfiguration> getCallerConfiguredNetworks() { 253 checkAccessWifiStatePermission(); 254 return getConfiguredNetworks(); 255 } 256 257 @Implementation getConfiguredNetworks()258 protected List<WifiConfiguration> getConfiguredNetworks() { 259 final ArrayList<WifiConfiguration> wifiConfigurations = new ArrayList<>(); 260 for (WifiConfiguration wifiConfiguration : networkIdToConfiguredNetworks.values()) { 261 wifiConfigurations.add(wifiConfiguration); 262 } 263 return wifiConfigurations; 264 } 265 266 @Implementation getPrivilegedConfiguredNetworks()267 protected List<WifiConfiguration> getPrivilegedConfiguredNetworks() { 268 return getConfiguredNetworks(); 269 } 270 271 @Implementation addNetwork(WifiConfiguration config)272 protected int addNetwork(WifiConfiguration config) { 273 if (config == null) { 274 return -1; 275 } 276 int networkId = networkIdToConfiguredNetworks.size(); 277 config.networkId = -1; 278 networkIdToConfiguredNetworks.put(networkId, makeCopy(config, networkId)); 279 return networkId; 280 } 281 282 /** 283 * The new version of {@link #addNetwork(WifiConfiguration)} which returns a more detailed failure 284 * codes. The original implementation of this API is limited to Device Owner (DO), Profile Owner 285 * (PO), system app, and privileged apps but this shadow can be called by all apps. 286 */ 287 @Implementation(minSdk = S) addNetworkPrivileged(WifiConfiguration config)288 protected AddNetworkResult addNetworkPrivileged(WifiConfiguration config) { 289 if (config == null) { 290 throw new IllegalArgumentException("config cannot be null"); 291 } 292 293 int networkId = addNetwork(config); 294 return new AddNetworkResult(AddNetworkResult.STATUS_SUCCESS, networkId); 295 } 296 297 @Implementation removeNetwork(int netId)298 protected boolean removeNetwork(int netId) { 299 networkIdToConfiguredNetworks.remove(netId); 300 return true; 301 } 302 303 /** 304 * Removes all configured networks regardless of the app that created the network. Can only be 305 * called by a Device Owner (DO) app. 306 * 307 * @return {@code true} if at least one network is removed, {@code false} otherwise 308 */ 309 @Implementation(minSdk = S) removeNonCallerConfiguredNetworks()310 protected boolean removeNonCallerConfiguredNetworks() { 311 checkChangeWifiStatePermission(); 312 checkDeviceOwner(); 313 int previousSize = networkIdToConfiguredNetworks.size(); 314 networkIdToConfiguredNetworks.clear(); 315 return networkIdToConfiguredNetworks.size() < previousSize; 316 } 317 318 /** 319 * Adds or updates a network which can later be retrieved with {@link #getWifiConfiguration(int)} 320 * method. A null {@param config}, or one with a networkId less than 0, or a networkId that had 321 * its updatePermission removed using the {@link #setUpdateNetworkPermission(int, boolean)} will 322 * return -1, which indicates a failure to update. 323 */ 324 @Implementation updateNetwork(WifiConfiguration config)325 protected int updateNetwork(WifiConfiguration config) { 326 if (config == null || config.networkId < 0 || readOnlyNetworkIds.get(config.networkId)) { 327 return -1; 328 } 329 networkIdToConfiguredNetworks.put(config.networkId, makeCopy(config, config.networkId)); 330 return config.networkId; 331 } 332 333 @Implementation saveConfiguration()334 protected boolean saveConfiguration() { 335 wasSaved = true; 336 return true; 337 } 338 339 @Implementation enableNetwork(int netId, boolean attemptConnect)340 protected boolean enableNetwork(int netId, boolean attemptConnect) { 341 lastEnabledNetwork = new Pair<>(netId, attemptConnect); 342 enabledNetworks.add(netId); 343 return true; 344 } 345 346 @Implementation disableNetwork(int netId)347 protected boolean disableNetwork(int netId) { 348 return enabledNetworks.remove(netId); 349 } 350 351 @Implementation createWifiLock(int lockType, String tag)352 protected WifiManager.WifiLock createWifiLock(int lockType, String tag) { 353 WifiManager.WifiLock wifiLock = ReflectionHelpers.callConstructor(WifiManager.WifiLock.class); 354 shadowOf(wifiLock).setWifiManager(wifiManager); 355 return wifiLock; 356 } 357 358 @Implementation createWifiLock(String tag)359 protected WifiManager.WifiLock createWifiLock(String tag) { 360 return createWifiLock(WifiManager.WIFI_MODE_FULL, tag); 361 } 362 363 @Implementation createMulticastLock(String tag)364 protected MulticastLock createMulticastLock(String tag) { 365 MulticastLock multicastLock = ReflectionHelpers.callConstructor(MulticastLock.class); 366 shadowOf(multicastLock).setWifiManager(wifiManager); 367 return multicastLock; 368 } 369 370 @Implementation calculateSignalLevel(int rssi, int numLevels)371 protected static int calculateSignalLevel(int rssi, int numLevels) { 372 return (int) (sSignalLevelInPercent * (numLevels - 1)); 373 } 374 375 /** 376 * Does nothing and returns the configured success status. 377 * 378 * <p>That is different from the Android implementation which always returns {@code true} up to 379 * and including Android 8, and either {@code true} or {@code false} on Android 9+. 380 * 381 * @return the value configured by {@link #setStartScanSucceeds}, or {@code true} if that method 382 * was never called. 383 */ 384 @Implementation startScan()385 protected boolean startScan() { 386 if (getScanResults() != null && !getScanResults().isEmpty()) { 387 new Handler(Looper.getMainLooper()) 388 .post( 389 () -> { 390 Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); 391 RuntimeEnvironment.getApplication().sendBroadcast(intent); 392 }); 393 } 394 return startScanSucceeds; 395 } 396 397 @Implementation getDhcpInfo()398 protected DhcpInfo getDhcpInfo() { 399 return dhcpInfo; 400 } 401 402 @Implementation isScanAlwaysAvailable()403 protected boolean isScanAlwaysAvailable() { 404 return Settings.Global.getInt( 405 getContext().getContentResolver(), Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 1) 406 == 1; 407 } 408 409 @HiddenApi 410 @Implementation connect(WifiConfiguration wifiConfiguration, WifiManager.ActionListener listener)411 protected void connect(WifiConfiguration wifiConfiguration, WifiManager.ActionListener listener) { 412 WifiInfo wifiInfo = getConnectionInfo(); 413 414 String ssid = 415 isQuoted(wifiConfiguration.SSID) 416 ? stripQuotes(wifiConfiguration.SSID) 417 : wifiConfiguration.SSID; 418 419 ShadowWifiInfo shadowWifiInfo = Shadow.extract(wifiInfo); 420 shadowWifiInfo.setSSID(ssid); 421 shadowWifiInfo.setBSSID(wifiConfiguration.BSSID); 422 shadowWifiInfo.setNetworkId(wifiConfiguration.networkId); 423 setConnectionInfo(wifiInfo); 424 425 // Now that we're "connected" to wifi, update Dhcp and point it to localhost. 426 DhcpInfo dhcpInfo = new DhcpInfo(); 427 dhcpInfo.gateway = LOCAL_HOST; 428 dhcpInfo.ipAddress = LOCAL_HOST; 429 setDhcpInfo(dhcpInfo); 430 431 // Now add the network to ConnectivityManager. 432 NetworkInfo networkInfo = 433 ShadowNetworkInfo.newInstance( 434 NetworkInfo.DetailedState.CONNECTED, 435 ConnectivityManager.TYPE_WIFI, 436 0 /* subType */, 437 true /* isAvailable */, 438 true /* isConnected */); 439 ShadowConnectivityManager connectivityManager = 440 Shadow.extract( 441 RuntimeEnvironment.getApplication().getSystemService(Context.CONNECTIVITY_SERVICE)); 442 connectivityManager.setActiveNetworkInfo(networkInfo); 443 444 if (listener != null) { 445 listener.onSuccess(); 446 } 447 } 448 449 @HiddenApi 450 @Implementation connect(int networkId, WifiManager.ActionListener listener)451 protected void connect(int networkId, WifiManager.ActionListener listener) { 452 WifiConfiguration wifiConfiguration = new WifiConfiguration(); 453 wifiConfiguration.networkId = networkId; 454 wifiConfiguration.SSID = ""; 455 wifiConfiguration.BSSID = ""; 456 connect(wifiConfiguration, listener); 457 } 458 isQuoted(String str)459 private static boolean isQuoted(String str) { 460 if (str == null || str.length() < 2) { 461 return false; 462 } 463 464 return str.charAt(0) == '"' && str.charAt(str.length() - 1) == '"'; 465 } 466 stripQuotes(String str)467 private static String stripQuotes(String str) { 468 return str.substring(1, str.length() - 1); 469 } 470 471 @Implementation reconnect()472 protected boolean reconnect() { 473 WifiConfiguration wifiConfiguration = getMostRecentNetwork(); 474 if (wifiConfiguration == null) { 475 return false; 476 } 477 478 connect(wifiConfiguration, null); 479 return true; 480 } 481 getMostRecentNetwork()482 private WifiConfiguration getMostRecentNetwork() { 483 if (getLastEnabledNetwork() == null) { 484 return null; 485 } 486 487 return getWifiConfiguration(getLastEnabledNetwork().first); 488 } 489 setSignalLevelInPercent(float level)490 public static void setSignalLevelInPercent(float level) { 491 if (level < 0 || level > 1) { 492 throw new IllegalArgumentException("level needs to be between 0 and 1"); 493 } 494 sSignalLevelInPercent = level; 495 } 496 setAccessWifiStatePermission(boolean accessWifiStatePermission)497 public void setAccessWifiStatePermission(boolean accessWifiStatePermission) { 498 this.accessWifiStatePermission = accessWifiStatePermission; 499 } 500 setChangeWifiStatePermission(boolean changeWifiStatePermission)501 public void setChangeWifiStatePermission(boolean changeWifiStatePermission) { 502 this.changeWifiStatePermission = changeWifiStatePermission; 503 } 504 505 /** 506 * Prevents a networkId from being updated using the {@link updateNetwork(WifiConfiguration)} 507 * method. This is to simulate the case where a separate application creates a network, and the 508 * Android security model prevents your application from updating it. 509 */ setUpdateNetworkPermission(int networkId, boolean hasPermission)510 public void setUpdateNetworkPermission(int networkId, boolean hasPermission) { 511 readOnlyNetworkIds.set(networkId, !hasPermission); 512 } 513 setScanResults(List<ScanResult> scanResults)514 public void setScanResults(List<ScanResult> scanResults) { 515 this.scanResults = scanResults; 516 } 517 setDhcpInfo(DhcpInfo dhcpInfo)518 public void setDhcpInfo(DhcpInfo dhcpInfo) { 519 this.dhcpInfo = dhcpInfo; 520 } 521 getLastEnabledNetwork()522 public Pair<Integer, Boolean> getLastEnabledNetwork() { 523 return lastEnabledNetwork; 524 } 525 526 /** Whether the network is enabled or not. */ isNetworkEnabled(int netId)527 public boolean isNetworkEnabled(int netId) { 528 return enabledNetworks.contains(netId); 529 } 530 531 /** Returns the number of WifiLocks and MulticastLocks that are currently acquired. */ getActiveLockCount()532 public int getActiveLockCount() { 533 return activeLockCount.get(); 534 } 535 wasConfigurationSaved()536 public boolean wasConfigurationSaved() { 537 return wasSaved; 538 } 539 setIsScanAlwaysAvailable(boolean isScanAlwaysAvailable)540 public void setIsScanAlwaysAvailable(boolean isScanAlwaysAvailable) { 541 Settings.Global.putInt( 542 getContext().getContentResolver(), 543 Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 544 isScanAlwaysAvailable ? 1 : 0); 545 } 546 checkAccessWifiStatePermission()547 private void checkAccessWifiStatePermission() { 548 if (!accessWifiStatePermission) { 549 throw new SecurityException("Caller does not hold ACCESS_WIFI_STATE permission"); 550 } 551 } 552 checkChangeWifiStatePermission()553 private void checkChangeWifiStatePermission() { 554 if (!changeWifiStatePermission) { 555 throw new SecurityException("Caller does not hold CHANGE_WIFI_STATE permission"); 556 } 557 } 558 checkDeviceOwner()559 private void checkDeviceOwner() { 560 if (!getContext() 561 .getSystemService(DevicePolicyManager.class) 562 .isDeviceOwnerApp(getContext().getPackageName())) { 563 throw new SecurityException("Caller is not device owner"); 564 } 565 } 566 makeCopy(WifiConfiguration config, int networkId)567 private WifiConfiguration makeCopy(WifiConfiguration config, int networkId) { 568 ShadowWifiConfiguration shadowWifiConfiguration = Shadow.extract(config); 569 WifiConfiguration copy = shadowWifiConfiguration.copy(); 570 copy.networkId = networkId; 571 return copy; 572 } 573 getWifiConfiguration(int netId)574 public WifiConfiguration getWifiConfiguration(int netId) { 575 return networkIdToConfiguredNetworks.get(netId); 576 } 577 578 @Implementation(minSdk = Q) 579 @HiddenApi addOnWifiUsabilityStatsListener(Object executorObject, Object listenerObject)580 protected void addOnWifiUsabilityStatsListener(Object executorObject, Object listenerObject) { 581 Executor executor = (Executor) executorObject; 582 WifiManager.OnWifiUsabilityStatsListener listener = 583 (WifiManager.OnWifiUsabilityStatsListener) listenerObject; 584 wifiUsabilityStatsListeners.put(listener, executor); 585 } 586 587 @Implementation(minSdk = Q) 588 @HiddenApi removeOnWifiUsabilityStatsListener(Object listenerObject)589 protected void removeOnWifiUsabilityStatsListener(Object listenerObject) { 590 WifiManager.OnWifiUsabilityStatsListener listener = 591 (WifiManager.OnWifiUsabilityStatsListener) listenerObject; 592 wifiUsabilityStatsListeners.remove(listener); 593 } 594 595 @Implementation(minSdk = Q) 596 @HiddenApi updateWifiUsabilityScore(int seqNum, int score, int predictionHorizonSec)597 protected void updateWifiUsabilityScore(int seqNum, int score, int predictionHorizonSec) { 598 synchronized (usabilityScores) { 599 usabilityScores.add(new WifiUsabilityScore(seqNum, score, predictionHorizonSec)); 600 } 601 } 602 603 /** 604 * Implements setWifiConnectedNetworkScorer() with the generic Object input as 605 * WifiConnectedNetworkScorer is a hidden/System API. 606 */ 607 @Implementation(minSdk = R) 608 @HiddenApi setWifiConnectedNetworkScorer(Object executorObject, Object scorerObject)609 protected boolean setWifiConnectedNetworkScorer(Object executorObject, Object scorerObject) { 610 if (networkScorer == null) { 611 networkScorer = scorerObject; 612 return true; 613 } else { 614 return false; 615 } 616 } 617 618 @Implementation(minSdk = R) 619 @HiddenApi clearWifiConnectedNetworkScorer()620 protected void clearWifiConnectedNetworkScorer() { 621 networkScorer = null; 622 } 623 624 /** Returns if wifi connected betwork scorer enabled */ isWifiConnectedNetworkScorerEnabled()625 public boolean isWifiConnectedNetworkScorerEnabled() { 626 return networkScorer != null; 627 } 628 629 @Implementation setWifiApConfiguration(WifiConfiguration apConfig)630 protected boolean setWifiApConfiguration(WifiConfiguration apConfig) { 631 this.apConfig = apConfig; 632 return true; 633 } 634 635 @Implementation getWifiApConfiguration()636 protected WifiConfiguration getWifiApConfiguration() { 637 return apConfig; 638 } 639 640 @Implementation(minSdk = R) setSoftApConfiguration(SoftApConfiguration softApConfig)641 protected boolean setSoftApConfiguration(SoftApConfiguration softApConfig) { 642 this.softApConfig = softApConfig; 643 return true; 644 } 645 646 @Implementation(minSdk = R) getSoftApConfiguration()647 protected SoftApConfiguration getSoftApConfiguration() { 648 return softApConfig; 649 } 650 651 /** 652 * Returns wifi usability scores previous passed to {@link WifiManager#updateWifiUsabilityScore} 653 */ getUsabilityScores()654 public List<WifiUsabilityScore> getUsabilityScores() { 655 synchronized (usabilityScores) { 656 return ImmutableList.copyOf(usabilityScores); 657 } 658 } 659 660 /** 661 * Clears wifi usability scores previous passed to {@link WifiManager#updateWifiUsabilityScore} 662 */ clearUsabilityScores()663 public void clearUsabilityScores() { 664 synchronized (usabilityScores) { 665 usabilityScores.clear(); 666 } 667 } 668 669 /** 670 * Post Wifi stats to any listeners registered with {@link 671 * WifiManager#addOnWifiUsabilityStatsListener} 672 */ postUsabilityStats( int seqNum, boolean isSameBssidAndFreq, WifiUsabilityStatsEntryBuilder statsBuilder)673 public void postUsabilityStats( 674 int seqNum, boolean isSameBssidAndFreq, WifiUsabilityStatsEntryBuilder statsBuilder) { 675 WifiUsabilityStatsEntry stats = statsBuilder.build(); 676 677 Set<Map.Entry<WifiManager.OnWifiUsabilityStatsListener, Executor>> toNotify = new ArraySet<>(); 678 toNotify.addAll(wifiUsabilityStatsListeners.entrySet()); 679 for (Map.Entry<WifiManager.OnWifiUsabilityStatsListener, Executor> entry : toNotify) { 680 entry 681 .getValue() 682 .execute( 683 new Runnable() { 684 // Using a lambda here means loading the ShadowWifiManager class tries 685 // to load the WifiManager.OnWifiUsabilityStatsListener which fails if 686 // not building against a system API. 687 @Override 688 public void run() { 689 entry.getKey().onWifiUsabilityStats(seqNum, isSameBssidAndFreq, stats); 690 } 691 }); 692 } 693 } 694 getContext()695 private Context getContext() { 696 return ReflectionHelpers.getField(wifiManager, "mContext"); 697 } 698 699 @Implements(WifiManager.WifiLock.class) 700 public static class ShadowWifiLock { 701 private int refCount; 702 private boolean refCounted = true; 703 private boolean locked; 704 private WifiManager wifiManager; 705 public static final int MAX_ACTIVE_LOCKS = 50; 706 setWifiManager(WifiManager wifiManager)707 private void setWifiManager(WifiManager wifiManager) { 708 this.wifiManager = wifiManager; 709 } 710 711 @Implementation acquire()712 protected synchronized void acquire() { 713 if (wifiManager != null) { 714 shadowOf(wifiManager).activeLockCount.getAndIncrement(); 715 } 716 if (refCounted) { 717 if (++refCount >= MAX_ACTIVE_LOCKS) { 718 throw new UnsupportedOperationException("Exceeded maximum number of wifi locks"); 719 } 720 } else { 721 locked = true; 722 } 723 } 724 725 @Implementation release()726 protected synchronized void release() { 727 if (wifiManager != null) { 728 shadowOf(wifiManager).activeLockCount.getAndDecrement(); 729 } 730 if (refCounted) { 731 if (--refCount < 0) throw new RuntimeException("WifiLock under-locked"); 732 } else { 733 locked = false; 734 } 735 } 736 737 @Implementation isHeld()738 protected synchronized boolean isHeld() { 739 return refCounted ? refCount > 0 : locked; 740 } 741 742 @Implementation setReferenceCounted(boolean refCounted)743 protected void setReferenceCounted(boolean refCounted) { 744 this.refCounted = refCounted; 745 } 746 } 747 748 @Implements(MulticastLock.class) 749 public static class ShadowMulticastLock { 750 private int refCount; 751 private boolean refCounted = true; 752 private boolean locked; 753 static final int MAX_ACTIVE_LOCKS = 50; 754 private WifiManager wifiManager; 755 setWifiManager(WifiManager wifiManager)756 private void setWifiManager(WifiManager wifiManager) { 757 this.wifiManager = wifiManager; 758 } 759 760 @Implementation acquire()761 protected void acquire() { 762 if (wifiManager != null) { 763 shadowOf(wifiManager).activeLockCount.getAndIncrement(); 764 } 765 if (refCounted) { 766 if (++refCount >= MAX_ACTIVE_LOCKS) { 767 throw new UnsupportedOperationException("Exceeded maximum number of wifi locks"); 768 } 769 } else { 770 locked = true; 771 } 772 } 773 774 @Implementation release()775 protected synchronized void release() { 776 if (wifiManager != null) { 777 shadowOf(wifiManager).activeLockCount.getAndDecrement(); 778 } 779 if (refCounted) { 780 if (--refCount < 0) throw new RuntimeException("WifiLock under-locked"); 781 } else { 782 locked = false; 783 } 784 } 785 786 @Implementation setReferenceCounted(boolean refCounted)787 protected void setReferenceCounted(boolean refCounted) { 788 this.refCounted = refCounted; 789 } 790 791 @Implementation isHeld()792 protected synchronized boolean isHeld() { 793 return refCounted ? refCount > 0 : locked; 794 } 795 } 796 shadowOf(WifiManager.WifiLock o)797 private static ShadowWifiLock shadowOf(WifiManager.WifiLock o) { 798 return Shadow.extract(o); 799 } 800 shadowOf(WifiManager.MulticastLock o)801 private static ShadowMulticastLock shadowOf(WifiManager.MulticastLock o) { 802 return Shadow.extract(o); 803 } 804 shadowOf(WifiManager o)805 private static ShadowWifiManager shadowOf(WifiManager o) { 806 return Shadow.extract(o); 807 } 808 809 /** Class to record scores passed to WifiManager#updateWifiUsabilityScore */ 810 public static class WifiUsabilityScore { 811 public final int seqNum; 812 public final int score; 813 public final int predictionHorizonSec; 814 WifiUsabilityScore(int seqNum, int score, int predictionHorizonSec)815 private WifiUsabilityScore(int seqNum, int score, int predictionHorizonSec) { 816 this.seqNum = seqNum; 817 this.score = score; 818 this.predictionHorizonSec = predictionHorizonSec; 819 } 820 } 821 822 /** Informs the {@link WifiManager} of a list of PNO {@link ScanResult}. */ networksFoundFromPnoScan(List<ScanResult> scanResults)823 public void networksFoundFromPnoScan(List<ScanResult> scanResults) { 824 synchronized (pnoRequestLock) { 825 List<ScanResult> scanResultsCopy = List.copyOf(scanResults); 826 if (outstandingPnoScanRequest == null 827 || outstandingPnoScanRequest.ssids.stream() 828 .noneMatch( 829 ssid -> 830 scanResultsCopy.stream() 831 .anyMatch(scanResult -> scanResult.getWifiSsid().equals(ssid)))) { 832 return; 833 } 834 Executor executor = outstandingPnoScanRequest.executor; 835 InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback; 836 executor.execute(() -> callback.onScanResultsAvailable(scanResultsCopy)); 837 Intent intent = createPnoScanResultsBroadcastIntent(); 838 getContext().sendBroadcast(intent); 839 executor.execute( 840 () -> 841 callback.onRemoved( 842 InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED)); 843 outstandingPnoScanRequest = null; 844 } 845 } 846 847 // Object needs to be used here since PnoScanResultsCallback is hidden. The looseSignatures spec 848 // requires that all args are of type Object. 849 @Implementation(minSdk = TIRAMISU) 850 @HiddenApi setExternalPnoScanRequest( Object ssids, Object frequencies, Object executor, Object callback)851 protected void setExternalPnoScanRequest( 852 Object ssids, Object frequencies, Object executor, Object callback) { 853 synchronized (pnoRequestLock) { 854 if (callback == null) { 855 throw new IllegalArgumentException("callback cannot be null"); 856 } 857 858 List<WifiSsid> pnoSsids = (List<WifiSsid>) ssids; 859 int[] pnoFrequencies = (int[]) frequencies; 860 Executor pnoExecutor = (Executor) executor; 861 InternalPnoScanResultsCallback pnoCallback = new InternalPnoScanResultsCallback(callback); 862 863 if (pnoExecutor == null) { 864 throw new IllegalArgumentException("executor cannot be null"); 865 } 866 if (pnoSsids == null || pnoSsids.isEmpty()) { 867 // The real WifiServiceImpl throws an IllegalStateException in this case, so keeping it the 868 // same for consistency. 869 throw new IllegalStateException("Ssids can't be null or empty"); 870 } 871 if (pnoSsids.size() > 2) { 872 throw new IllegalArgumentException("Ssid list can't be greater than 2"); 873 } 874 if (pnoFrequencies != null && pnoFrequencies.length > 10) { 875 throw new IllegalArgumentException("Length of frequencies must be smaller than 10"); 876 } 877 int uid = Binder.getCallingUid(); 878 String packageName = getContext().getPackageName(); 879 880 if (outstandingPnoScanRequest != null) { 881 pnoExecutor.execute( 882 () -> 883 pnoCallback.onRegisterFailed( 884 uid == outstandingPnoScanRequest.uid 885 ? InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_ALREADY_REGISTERED 886 : InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY)); 887 return; 888 } 889 890 outstandingPnoScanRequest = 891 new PnoScanRequest(pnoSsids, pnoFrequencies, pnoExecutor, pnoCallback, packageName, uid); 892 pnoExecutor.execute(pnoCallback::onRegisterSuccess); 893 } 894 } 895 896 @Implementation(minSdk = TIRAMISU) 897 @HiddenApi clearExternalPnoScanRequest()898 protected void clearExternalPnoScanRequest() { 899 synchronized (pnoRequestLock) { 900 if (outstandingPnoScanRequest != null 901 && outstandingPnoScanRequest.uid == Binder.getCallingUid()) { 902 InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback; 903 outstandingPnoScanRequest.executor.execute( 904 () -> 905 callback.onRemoved( 906 InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED)); 907 outstandingPnoScanRequest = null; 908 } 909 } 910 } 911 912 private static class PnoScanRequest { 913 private final List<WifiSsid> ssids; 914 private final List<Integer> frequencies; 915 private final Executor executor; 916 private final InternalPnoScanResultsCallback callback; 917 private final String packageName; 918 private final int uid; 919 PnoScanRequest( List<WifiSsid> ssids, int[] frequencies, Executor executor, InternalPnoScanResultsCallback callback, String packageName, int uid)920 private PnoScanRequest( 921 List<WifiSsid> ssids, 922 int[] frequencies, 923 Executor executor, 924 InternalPnoScanResultsCallback callback, 925 String packageName, 926 int uid) { 927 this.ssids = List.copyOf(ssids); 928 this.frequencies = 929 frequencies == null ? List.of() : Arrays.stream(frequencies).boxed().collect(toList()); 930 this.executor = executor; 931 this.callback = callback; 932 this.packageName = packageName; 933 this.uid = uid; 934 } 935 } 936 createPnoScanResultsBroadcastIntent()937 private Intent createPnoScanResultsBroadcastIntent() { 938 Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); 939 intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true); 940 intent.setPackage(outstandingPnoScanRequest.packageName); 941 return intent; 942 } 943 944 private static class InternalPnoScanResultsCallback { 945 static final int REGISTER_PNO_CALLBACK_ALREADY_REGISTERED = 1; 946 static final int REGISTER_PNO_CALLBACK_RESOURCE_BUSY = 2; 947 static final int REMOVE_PNO_CALLBACK_RESULTS_DELIVERED = 1; 948 static final int REMOVE_PNO_CALLBACK_UNREGISTERED = 2; 949 950 final Object callback; 951 final Method availableCallback; 952 final Method successCallback; 953 final Method failedCallback; 954 final Method removedCallback; 955 InternalPnoScanResultsCallback(Object callback)956 InternalPnoScanResultsCallback(Object callback) { 957 this.callback = callback; 958 try { 959 Class<?> pnoCallbackClass = callback.getClass(); 960 availableCallback = pnoCallbackClass.getMethod("onScanResultsAvailable", List.class); 961 successCallback = pnoCallbackClass.getMethod("onRegisterSuccess"); 962 failedCallback = pnoCallbackClass.getMethod("onRegisterFailed", int.class); 963 removedCallback = pnoCallbackClass.getMethod("onRemoved", int.class); 964 } catch (NoSuchMethodException e) { 965 throw new IllegalArgumentException("callback is not of type PnoScanResultsCallback", e); 966 } 967 } 968 onScanResultsAvailable(List<ScanResult> scanResults)969 void onScanResultsAvailable(List<ScanResult> scanResults) { 970 invokeCallback(availableCallback, scanResults); 971 } 972 onRegisterSuccess()973 void onRegisterSuccess() { 974 invokeCallback(successCallback); 975 } 976 onRegisterFailed(int reason)977 void onRegisterFailed(int reason) { 978 invokeCallback(failedCallback, reason); 979 } 980 onRemoved(int reason)981 void onRemoved(int reason) { 982 invokeCallback(removedCallback, reason); 983 } 984 invokeCallback(Method method, Object... args)985 void invokeCallback(Method method, Object... args) { 986 try { 987 method.invoke(callback, args); 988 } catch (IllegalAccessException | InvocationTargetException e) { 989 throw new IllegalStateException("Failed to invoke " + method.getName(), e); 990 } 991 } 992 } 993 } 994