Part 1 of this post was about WiFi scanning on Windows. You can find it here.
Scanning on Android isn’t hard, but there are obstacles. In what is documented as being in the interest of saving battery life, WiFi scanning is throttled on more recent devices to be limited to 4 scans within a 2 minute period. On some of my older devices this limit is not present. While I found that I could turn off the default throttling setting in the developer settings, the more recent devices was still much more limited in how often it could scan. For my purposes (building a personal collection of coordinates and WiFi access points for an embedded device) this has the effect of lowering the number of samples that can be collected with my more recent device.
Because location can be inferred from WiFi information, Android protects WiFi scanning behind the location permission. Even if the application has no interest in location information, it must have the location permission to scan for WiFi information. I do have interest in location information. I want to save the location at which the access points were observed. The permissions that I specify in the manifest include the following.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
</manifest>
In addition to the manifest declarations, the application must explicitly ask the user for permission to track location. Once the application has location permissions, it can start tracking location and performing WiFi scans.
fun getWifi() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Toast.makeText(this, "version> = marshmallow", Toast.LENGTH_SHORT).show();
if (checkSelfPermission( Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "location turned off", Toast.LENGTH_SHORT).show();
var s = arrayOf<String>(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
this.requestPermissions(s, COURSE_LOCATION_REQUEST);
} else {
Toast.makeText(this, "location turned on", Toast.LENGTH_SHORT).show();
getLocationUpdates()
wifiManager.startScan();
}
} else {
Toast.makeText(this, "scanning", Toast.LENGTH_SHORT).show();
getLocationUpdates();
wifiManager.startScan();
}
}
For the location updates, I have asked for new location information if the user’s location has changed by three meters (nine feet) and if at least 10 seconds has passed. I am interest in getting multiple samples of access points from different positions to better localize them. I ask for high precision for the location information. The device will most likely use GPS based positioning, but may use any location source.
@SuppressLint("MissingPermission")
fun getLocationUpdates() {
val locationRequest = LocationRequest.create()?.apply {
interval = 10_000
fastestInterval = 10_000
smallestDisplacement = 3.0f
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult ?: return
for (location in locationResult.locations){
currentLocation = location
// Update UI with location data
// ...
}
}
}
locationRequest?.let {
fusedLocationClient.requestLocationUpdates(
it,
locationCallback,
Looper.getMainLooper())
}
}
To scan for Wifi, I’ll need the wifiManager
class and I’ll need an IntentFilter
. The WifiManager
instance is used to ensure that WiFi is turned on and to request the WiFi scan. The IntentFilter
lateinit var wifiManager:WifiManager
val intentFilter = IntentFilter().also {
it.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
}
I instantiate the WifiManager
in the activity’s onCreate
method. After getting an instance I ensure that WiFi is turned on.
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
if(!wifiManager.isWifiEnabled) {
Toast.makeText(this, "Turning on Wifi...", Toast.LENGTH_LONG).show()
wifiManager.isWifiEnabled = true
}
wifiReceiver = WifiReceiver(wifiManager, this)
wifiInfo = wifiManager.connectionInfo
registerReceiver(this.wifiReceiver, intentFilter)
The WifiReceiver
class above is a class I’ve made that derives from BroadcastReceiver
. I must implement a BroadcastReceiver
with an onReceive()
method. After the OS has scanned for available WiFi, it will notify our app of the availability of the results through this instance. When the results are available, they can be read from WiFiManager.scanResults
. I’m only saving results if I have location information too. If I don’t have location information, the results are discarded. If results are available, I save them to a data class that I’ve called ScanItem
. This class only serves to hold the values. Populated instances of ScanItem
are passed to another class for being persisted to a database.
override fun onReceive(p0: Context?, intent: Intent?) {
var action:String? = intent?.action
if(mainActivity.currentLocation!=null) {
val currentLocation = mainActivity.currentLocation!!;
var result = wifiManager.scanResults
Log.d(TAG, "results received");
val scanResultList: List<ScanItem> = ArrayList<ScanItem>(result.size)
for (r in result) {
var item = ScanItem(r.BSSID)
item.apply {
sessionID = SessionID
clientID = mainActivity.clientID
latitude = currentLocation.latitude
longitude = currentLocation.longitude
if (currentLocation.hasAccuracy()) {
horizontalAccuracy = currentLocation.accuracy
}
if (currentLocation.hasVerticalAccuracy()) {
verticalAccuracy = currentLocation.verticalAccuracyMeters
}
altitude = currentLocation.altitude.toFloat()
BSSID = r.BSSID
SSID = r.SSID
level = r.level
//http://w1.fi/cgit/hostap/tree/wpa_supplicant/ctrl_iface.c?id=7b42862ac87f333b0efb0f0bae822dcdf606bc69#n2169
capabilities = r.capabilities
frequency = r.frequency
datetime = currentLocation.time
locationLabel = mainActivity.locationLabel
}
mainActivity.scanItemDataHelper.insert(item)
}
mainActivity.currentLocation = null
mainActivity.addNewEntryCount(result.size)
Toast.makeText(mainActivity, "scan Saved", Toast.LENGTH_SHORT).show();
}
wifiManager.startScan()
}
}
As soon as I’ve saved all the results, I clear the location and start a new scan. What I’ve done here only works for me because I have disabled scan throttling on my device. By default, more recent Android devices only allow 4 scans within two minutes. It might have been better if I had scheduled the scans to be requested on an interval. But I went with a quick-and-dirty solution since I was implementing this just before getting on the road. I needed to drive a few hundred miles over a few days and I wanted to maximize on the opportunity to collect data.
I was able to reduce a significant data set from several days of scanning to a collection of hashes for the WiFi ID along with a latitude and longitude. My source dataset may contain an access point multiple times, as they are usually visible from multiple locations. In reducing the dataset, for each WiFi ID I got the average of its location (though I removed vehicle Wifi from my dataset. I found Wifi from Tesla’s, VW, and “Tanya’s iPhone” from vehicles on the same path as me for several miles) and exported a 32-bit hash of the WiFi ID, the latitude, and longitude (12-bytes per access point). Using a hash instead of the actual data let’s me reduce the storage size.
I’ve had success in using this to get an embedded device to determine its location. I’ll write more about this in another post. Until then, if you want a brief description of what that involved, you can find it here.
Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Twitter: @j2inet
Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.