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