WiFi Scanning Part 2: Scanning on Android

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.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.