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