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