• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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