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