Skip to content

Support for Wi-Fi scanning (a.k.a. Wi-Fi fingerprinting) #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
435 changes: 305 additions & 130 deletions README.md

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions demo/app/App_Resources/Android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,26 @@
android:largeScreens="true"
android:xlargeScreens="true"/>

<!-- Always include this permission if your app needs location access -->
<!-- This permission is for "approximate" location data -->
<!-- Always include this permission if your app needs location / Wi-Fi scans access. -->
<!-- This permission is for "approximate" location data and required for Wi-Fi scans -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Include only if your app benefits from precise location access. -->
<!-- This permission is for "precise" location data -->
<!-- Include only if your app benefits from precise location / Wi-Fi scans access. -->
<!-- This permission is for "precise" location data and required for Wi-Fi scans -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Required only when requesting background location access on
<!-- Required only when requesting background location / Wi-Fi scans access on
Android 10 (API level 29) and higher. -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

<!-- The following two permissions are required if your app wants to receive human activity changes -->
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION"/>
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>

<!-- The following two permissions are required in order to ask and retrieve Wi-Fi scan updates -->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
Expand Down
151 changes: 121 additions & 30 deletions demo/app/home/home-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ a code-behind file. The code-behind is a great place to place your view
logic, and to set up your page’s data binding.
*/

import { NavigatedData, Page, Application } from "@nativescript/core";
import { Application, NavigatedData, Page } from "@nativescript/core";

import { HomeViewModel } from "./home-view-model";

Expand All @@ -13,47 +13,76 @@ import { Resolution } from "nativescript-context-apis/activity-recognition";

import { GeolocationProvider } from "nativescript-context-apis/geolocation";
import { of, Subscription } from "rxjs";
import {
FingerprintGrouping,
WifiScanProvider,
} from "nativescript-context-apis/internal/wifi";

const activityRecognizers = [Resolution.LOW, Resolution.MEDIUM];

let locationSubscription: Subscription;
let wifiScanSubscription: Subscription;
export function onNavigatingTo(args: NavigatedData) {
const page = <Page>args.object;

page.bindingContext = new HomeViewModel();

let preparing = true;
let locationSubscription: Subscription;
Application.on(Application.resumeEvent, () => {
if (!preparing) {
printCurrentLocation().catch((err) => {
console.error(`Could not print current location: ${err}`);
}).then(() => printLocationUpdates()
.then((subscription) => (locationSubscription = subscription))
.catch(
(err) =>
`An error occurred while getting location updates: ${err}`)
).then(() => listenToActivityChanges());
showUpdates().catch((err) =>
console.error("Could not show updates. Reason: ", err)
);
}
});

Application.on(Application.suspendEvent, () => {
if (!preparing) {
if (locationSubscription) {
locationSubscription.unsubscribe();
}
locationSubscription?.unsubscribe();
wifiScanSubscription?.unsubscribe();
stopListeningToChanges();
}
});

printCurrentLocation().catch((err) => {
console.error(`Could not print current location: ${err}`);
}).then(() => printLocationUpdates()
.then((subscription) => (locationSubscription = subscription))
.catch(
(err) => `An error occurred while getting location updates: ${err}`
)
).then(() => listenToActivityChanges(true))
.then(() => preparing = false);
showUpdates().then(() => (preparing = false));
}

async function showUpdates(addListeners = false): Promise<void> {
const steps: Array<() => Promise<any>> = [
() => listenToActivityChanges(addListeners),
() =>
printCurrentLocation().catch((err) => {
console.error("Could not print current location. Reason:", err);
}),
() =>
printWifiScanResult().catch((err) => {
console.error(
"Could not print current nearby wifi scan. Reason:",
err
);
}),
() =>
printLocationUpdates()
.then((subscription) => (locationSubscription = subscription))
.catch((err) =>
console.error(
"An error occurred while getting location updates. Reason:",
err
)
),
() =>
printWifiScanUpdates()
.then((subscription) => (wifiScanSubscription = subscription))
.catch((err) =>
console.error(
"An error occurred while getting wifi scan updates. Reason:",
err
)
),
];
for (const step of steps) {
await step();
}
}

async function printCurrentLocation() {
Expand All @@ -68,6 +97,15 @@ async function printCurrentLocation() {
}
}

async function printWifiScanResult() {
const provider = contextApis.wifiScanProvider;
const ok = await prepareWifiScanProvider(provider);
if (ok) {
const fingerprint = await provider.acquireWifiFingerprint(true);
console.log(`Last wifi scan result: ${JSON.stringify(fingerprint)}`);
}
}

async function printLocationUpdates(): Promise<Subscription> {
const provider = contextApis.geolocationProvider;
const ok = await prepareGeolocationProvider(provider);
Expand All @@ -86,7 +124,31 @@ async function printLocationUpdates(): Promise<Subscription> {
next: (location) =>
console.log(`New location acquired!: ${JSON.stringify(location)}`),
error: (error) =>
console.error(`Location updates could not be acquired: ${error}`)
console.error(`Location updates could not be acquired: ${error}`),
});
}

async function printWifiScanUpdates(): Promise<Subscription> {
const provider = contextApis.wifiScanProvider;
const ok = await prepareWifiScanProvider(provider);

await new Promise((resolve) => setTimeout(resolve, 30000));

const stream = ok
? provider.wifiFingerprintStream({
ensureAlwaysNew: true,
grouping: FingerprintGrouping.NONE,
continueOnFailure: true,
})
: of(null);

return stream.subscribe({
next: (fingerprint) =>
console.log(
`New wifi scan result!: ${JSON.stringify(fingerprint)}`
),
error: (error) =>
console.error(`Wifi scan result could not be acquired: ${error}`),
});
}

Expand All @@ -96,7 +158,9 @@ export async function listenToActivityChanges(addListener = false) {
await listenToActivityChangesFor(recognizerType, addListener);
} catch (err) {
console.error(
`An error occurred while listening to ${recognizerType} res activity changes: ${JSON.stringify(err)}`
`An error occurred while listening to ${recognizerType} res activity changes: ${JSON.stringify(
err
)}`
);
}
}
Expand Down Expand Up @@ -144,7 +208,7 @@ async function listenToActivityChangesFor(
);
}

let _preparing: Promise<any>;
let _preparingGeoProv: Promise<any>;
async function prepareGeolocationProvider(
provider: GeolocationProvider
): Promise<boolean> {
Expand All @@ -154,15 +218,42 @@ async function prepareGeolocationProvider(
}

try {
if (!_preparing) {
_preparing = provider.prepare();
if (!_preparingGeoProv) {
_preparingGeoProv = provider.prepare();
}
await _preparing;
await _preparingGeoProv;
return true;
} catch (e) {
console.error(`GeolocationProvider couldn't be prepared: ${JSON.stringify(e)}`);
console.error(
`GeolocationProvider couldn't be prepared: ${JSON.stringify(e)}`
);
return false;
} finally {
_preparingGeoProv = null;
}
}

let _preparingWifiProv: Promise<any>;
async function prepareWifiScanProvider(
provider: WifiScanProvider
): Promise<boolean> {
const isReady = await provider.isReady();
if (isReady) {
return true;
}

try {
if (!_preparingWifiProv) {
_preparingWifiProv = provider.prepare();
}
await _preparingWifiProv;
return true;
} catch (e) {
console.error(
`WifiScanProvider couldn't be prepared: ${JSON.stringify(e)}`
);
return false;
} finally {
_preparing = null;
_preparingWifiProv = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package es.uji.geotec.contextapis.wifi;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class WifiScanReceiver extends BroadcastReceiver {
private static String tag = "WifiScanReceiver";

private final WifiScanReceiverDelegate delegate;

public WifiScanReceiver(WifiScanReceiverDelegate delegate) {
this.delegate = delegate;
}

@Override
public void onReceive(Context context, Intent intent) {
if (context == null || intent == null) {
return;
}

Log.d(tag, "Wifi scan received!");
delegate.onReceive(context, intent);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package es.uji.geotec.contextapis.wifi;

import es.uji.geotec.contextapis.common.BroadcastReceiverDelegate;

public interface WifiScanReceiverDelegate extends BroadcastReceiverDelegate {
}
5 changes: 5 additions & 0 deletions src/context-apis.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "./activity-recognition";

import { GeolocationProvider, getGeolocationProvider } from "./geolocation";
import { getWifiScanProvider, WifiScanProvider } from "./wifi";

const recognizerTypes = [Resolution.LOW, Resolution.MEDIUM];

Expand All @@ -25,4 +26,8 @@ export class Common extends Observable {
get geolocationProvider(): GeolocationProvider {
return getGeolocationProvider();
}

get wifiScanProvider(): WifiScanProvider {
return getWifiScanProvider();
}
}
88 changes: 88 additions & 0 deletions src/internal/wifi/android/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { WifiScanAdapter } from "../common";
import { WifiFingerprint } from "../fingerprint";
import { enableLocationRequest, isEnabled } from "@nativescript/geolocation";
import { Utils } from "@nativescript/core";
import { getSeenWifiApsFrom } from "./parsers";

export class AndroidWifiScanAdapter implements WifiScanAdapter {
constructor(
private wifiManager: android.net.wifi.WifiManager = Utils.android
.getApplicationContext()
.getSystemService(android.content.Context.WIFI_SERVICE)
) {}

isReady(): Promise<boolean> {
return isEnabled();
}

prepare(): Promise<void> {
return enableLocationRequest(false, true);
}

async acquireWifiFingerprint(ensureIsNew = true): Promise<WifiFingerprint> {
const ready = await this.isReady();
if (!ready) {
throw new Error(
"Could not acquire wifi fingerprint. Insufficient permissions or disabled location services. Please, call prepare() first before requesting a fingerprint."
);
}

try {
await this.setupScanReceiver();
return this.getLastFingerprint(true);
} catch (err) {
if (ensureIsNew) throw err;
return this.getLastFingerprint(false);
}
}

private setupScanReceiver(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const scanReceiver = new es.uji.geotec.contextapis.wifi.WifiScanReceiver(
new es.uji.geotec.contextapis.wifi.WifiScanReceiverDelegate({
onReceive(
context: android.content.Context,
intent: android.content.Intent
) {
Utils.android
.getApplicationContext()
.unregisterReceiver(scanReceiver);
const success = intent.getBooleanExtra(
android.net.wifi.WifiManager.EXTRA_RESULTS_UPDATED,
false
);
if (success) {
resolve();
} else {
reject(new Error("Results not updated"));
}
},
})
);
const intentFilter = new android.content.IntentFilter();
intentFilter.addAction(
android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION
);
Utils.android
.getApplicationContext()
.registerReceiver(scanReceiver, intentFilter);
const success = this.wifiManager.startScan();
if (!success) {
reject(
new Error(
"Could not start scan! Reason: too many requests, device idle or unlikely hardware failure"
)
);
}
});
}

private getLastFingerprint(updateReceived: boolean): WifiFingerprint {
const results = this.wifiManager.getScanResults();
return {
seen: getSeenWifiApsFrom(results),
isNew: updateReceived,
timestamp: new Date(),
};
}
}
Loading