diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ac24a0b..5d49eb6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,11 +1,15 @@
-
+ package="org.microg.nlp.backend.openwlanmap"
+ android:versionCode="2"
+ android:versionName="0.0.2"
+ >
+
+
+
-
+
+
+
+
+
diff --git a/README.md b/README.md
index 06c5abd..18c7993 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,12 @@ OpenWlanMapNlpBackend
=====================
[UnifiedNlp](https://github.com/microg/android_packages_apps_UnifiedNlp) backend that uses [OpenWlanMap](http://www.openwlanmap.org/) to resolve user location.
-Location calculation is done online and therefor requires internet connection.
+Location calculation is done either online or offline. This can be switched in the settings.
+Online calculation of course requires internet connection.
+Offline calculation won't use any data, but look up the wifi access points in a database on your sd-card only.
+To generate the database a shell script (gen_openwifimap_db.sh) is included.
+
+To contribute to the OpenWlanMap database you can use the available Android app or upload your "wardriving" data manually [here](https://openwlanmap.org/upload.php?lang=). Don't forget to enable the "Publish own data" in the Android apps settings!
Building
--------
@@ -16,6 +21,13 @@ Used libraries
- [libwlocate](http://sourceforge.net/projects/libwlocate/) (included)
+Changes
+-------
+
+0.0.2 - Felix Knecht added offline support
+
+0.0.1 - Initial version by @mar-v-in with online support
+
License
-------
libwlocate is GPLv3, so is OpenWlanMapNlpBackend.
diff --git a/gen_openwifimap_db.sh b/gen_openwifimap_db.sh
new file mode 100755
index 0000000..b5e89e9
--- /dev/null
+++ b/gen_openwifimap_db.sh
@@ -0,0 +1,77 @@
+#! /bin/bash
+#
+# Quick and dirty script to build and install a new
+# wifi APs location database on a phone for microg/nogapps
+# OpenWlanMapNlpBackend.
+#
+
+MAX_LAT="90"
+MIN_LAT="-90"
+MAX_LON="180"
+MIN_LON="-180"
+
+function usage {
+ echo "Calling Sequence:"
+ echo "${0} [options]"
+ echo " Options:"
+ echo " -nDD -(North) Maximum latitude"
+ echo " -sDD -(South) Minimum latitude"
+ echo " -eDD -(East) Maximum longitude"
+ echo " -nDD -(West) Minimum latitude"
+ exit
+}
+
+#Process the arguments
+while getopts n:s:e:w: opt
+do
+ case "$opt" in
+ n) MAX_LAT=$OPTARG;;
+ s) MIN_LAT=$OPTARG;;
+ e) MAX_LON=$OPTARG;;
+ w) MIN_LON=$OPTARG;;
+ \?) usage;;
+ esac
+done
+
+#
+# Get latest wifi AP locations from OpenWLANMap.org
+#
+
+echo 'Getting wifi AP locations from OpenWLANMap.org'
+if [ -e db.tar.bz2 ] ; then
+ rm db.tar.bz2
+fi
+if [ -e db.csv ] ; then
+ mv -f db.csv db.csv.bak
+fi
+wget "http://openwlanmap.org/db.tar.bz2"
+tar --strip-components=1 -xjf db.tar.bz2 db/db.csv
+
+echo 'Building database file'
+if [ -e openwifimap.db ] ; then
+ mv -f openwifimap.db openwifimap.db.bak
+fi
+
+### TODO: Filter all entries with lat or long = 0
+### Those are the _nomap entries
+
+sqlite3 openwifimap.db <${MAX_LAT};
+DELETE FROM APs WHERE latitude<${MIN_LAT};
+DELETE FROM APs WHERE longitude>${MAX_LON};
+DELETE FROM APs WHERE longitude<${MIN_LON};
+CREATE INDEX _idx1 ON APs (bssid);
+VACUUM;
+.quit
+!
+
+#
+# Push the new database to the phone.
+#
+echo 'Pushing database to phone'
+adb push openwifimap.db /sdcard/.nogapps/openwifimap.db.x
+adb shell mv /sdcard/.nogapps/openwifimap.db.x /sdcard/.nogapps/openwifimap.db.new
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5862a54..2ae4736 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1,4 +1,12 @@
OpenWlanMapNlpBackend
+ Network
+ Allow network activity
+ Local
+ Database location
+ Assumed accuracy for database in meters
+ Debug
+ Enable debug log (contains your location)
+ The supplied value for assumed accuracy is not a float
\ No newline at end of file
diff --git a/res/xml/settings.xml b/res/xml/settings.xml
new file mode 100644
index 0000000..82c68cc
--- /dev/null
+++ b/res/xml/settings.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/org/microg/nlp/backend/openwlanmap/BackendService.java b/src/org/microg/nlp/backend/openwlanmap/BackendService.java
index be1f77a..afd3277 100644
--- a/src/org/microg/nlp/backend/openwlanmap/BackendService.java
+++ b/src/org/microg/nlp/backend/openwlanmap/BackendService.java
@@ -1,40 +1,108 @@
package org.microg.nlp.backend.openwlanmap;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.microg.nlp.api.LocationBackendService;
+import org.microg.nlp.api.LocationHelper;
+import org.microg.nlp.backend.openwlanmap.local.WifiLocationFile;
+import org.microg.nlp.backend.openwlanmap.local.WifiReceiver;
+import org.microg.nlp.backend.openwlanmap.local.WifiReceiver.WifiReceivedCallback;
+
import android.content.Context;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
import android.location.Location;
+import android.net.wifi.WifiManager;
+import android.preference.PreferenceManager;
import android.util.Log;
+
import com.vwp.libwlocate.WLocate;
-import org.microg.nlp.api.LocationBackendService;
-import org.microg.nlp.api.LocationHelper;
public class BackendService extends LocationBackendService {
private static final String TAG = BackendService.class.getName();
private WLocate wLocate;
+ private WifiLocationFile wifiLocationFile;
+ private WifiReceiver wifiReceiver;
+ private boolean networkAllowed;
@Override
protected void onOpen() {
- if (wLocate == null) {
- wLocate = new MyWLocate(this);
- } else {
- wLocate.doResume();
- }
+ Log.d(TAG, "onOpen");
+
+ SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ Configuration.fillFromPrefs(sharedPrefs);
+ sharedPrefs.registerOnSharedPreferenceChangeListener(Configuration.listener);
+
+ setOperatingMode();
}
@Override
protected void onClose() {
+ if (Configuration.debugEnabled) Log.d(TAG, "onClose");
+
+ SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ sharedPrefs.unregisterOnSharedPreferenceChangeListener(Configuration.listener);
+
+ cleanupOperatingMode();
+
+ }
+
+ private void setOperatingMode() {
+ this.networkAllowed = Configuration.networkAllowed;
+ if (this.networkAllowed) {
+ if (wLocate == null) {
+ wLocate = new MyWLocate(this);
+ } else {
+ wLocate.doResume();
+ }
+ } else {
+ openDatabase();
+ if (wifiReceiver == null) {
+ wifiReceiver = new WifiReceiver(this, new WifiDBResolver());
+ }
+ registerReceiver(wifiReceiver, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
+ }
+ }
+
+ private void cleanupOperatingMode() {
if (wLocate != null) {
wLocate.doPause();
}
- }
+ if (wifiReceiver != null) {
+ unregisterReceiver(wifiReceiver);
+ }
+ }
@Override
protected Location update() {
+ if (Configuration.debugEnabled) Log.d(TAG, "update");
+
+ if (this.networkAllowed != Configuration.networkAllowed) {
+ if (Configuration.debugEnabled) Log.d(TAG, "Network allowed changed");
+ cleanupOperatingMode();
+ setOperatingMode();
+ }
if (wLocate != null) {
+ if (Configuration.debugEnabled) Log.d(TAG, "Requesting location from net");
wLocate.wloc_request_position(WLocate.FLAG_NO_GPS_ACCESS);
+ return null;
+ }
+
+ if (wifiReceiver != null) {
+ if (Configuration.debugEnabled) Log.d(TAG, "Requesting location from db");
+ wifiReceiver.startScan();
}
+
return null;
}
+ private void openDatabase() {
+ if (wifiLocationFile == null) {
+ wifiLocationFile = new WifiLocationFile();
+ }
+ }
+
private class MyWLocate extends WLocate {
public MyWLocate(Context ctx) throws IllegalArgumentException {
@@ -43,7 +111,7 @@ public MyWLocate(Context ctx) throws IllegalArgumentException {
@Override
protected void wloc_return_position(int ret, double lat, double lon, float radius, short ccode, float cog) {
- Log.d(TAG, String.format("wloc_return_position ret=%d lat=%f lon=%f radius=%f ccode=%d cog=%f", ret, lat, lon, radius, ccode, cog));
+ if (Configuration.debugEnabled) Log.d(TAG, String.format("wloc_return_position ret=%d lat=%f lon=%f radius=%f ccode=%d cog=%f", ret, lat, lon, radius, ccode, cog));
if (ret == WLOC_OK) {
Location location = LocationHelper.create("libwlocate", lat, lon, radius);
if (cog != -1) {
@@ -53,4 +121,42 @@ protected void wloc_return_position(int ret, double lat, double lon, float radiu
}
}
}
+
+ private class WifiDBResolver implements WifiReceivedCallback {
+
+ @Override
+ public void process(List foundBssids) {
+
+ if (foundBssids == null || foundBssids.isEmpty()) {
+ return;
+ }
+ if (wifiLocationFile != null) {
+
+ List locations = new ArrayList(foundBssids.size());
+
+ for (String bssid : foundBssids) {
+ Location result = wifiLocationFile.query(bssid);
+ if (result != null) {
+ locations.add(result);
+ }
+ }
+
+ if (locations.isEmpty()) {
+ return;
+ }
+
+ //TODO fix LocationHelper:average to not calculate with null values
+ //TODO sort out wifis obviously in the wrong spot
+ Location avgLoc = LocationHelper.average("owm", locations);
+
+ if (avgLoc == null) {
+ Log.e(TAG, "Averaging locations did not work.");
+ return;
+ }
+
+ if (Configuration.debugEnabled) Log.d(TAG, "Reporting location: " + avgLoc.toString());
+ report(avgLoc);
+ }
+ }
+ }
}
diff --git a/src/org/microg/nlp/backend/openwlanmap/Configuration.java b/src/org/microg/nlp/backend/openwlanmap/Configuration.java
new file mode 100644
index 0000000..ab499ee
--- /dev/null
+++ b/src/org/microg/nlp/backend/openwlanmap/Configuration.java
@@ -0,0 +1,48 @@
+package org.microg.nlp.backend.openwlanmap;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Environment;
+import android.util.Log;
+
+public class Configuration {
+ private static String TAG = Configuration.class.getName();
+
+ public static boolean networkAllowed;
+
+ public static String dbLocation = Environment.getExternalStorageDirectory().getAbsolutePath() + "/.nogapps/openwifimap.db";
+
+ public static float assumedAccuracy;
+
+ public static ConfigChangedListener listener = new ConfigChangedListener();
+
+ public static boolean debugEnabled;
+
+
+ public static void fillFromPrefs(SharedPreferences sharedPrefs) {
+
+ debugEnabled = sharedPrefs.getBoolean("debugEnabled", false);
+
+ networkAllowed = sharedPrefs.getBoolean("networkAllowed", false);
+ if (debugEnabled) Log.d(TAG, "Network allowed: " + networkAllowed);
+
+ dbLocation = sharedPrefs.getString("databaseLocation", Environment.getExternalStorageDirectory().getAbsolutePath()
+ + "/.nogapps/openwifimap.db");
+
+ try {
+ assumedAccuracy = Float.parseFloat(sharedPrefs.getString("assumedAccuracy", "50"));
+ } catch (NumberFormatException e) {
+ assumedAccuracy = 50;
+ }
+ }
+
+ private static class ConfigChangedListener implements OnSharedPreferenceChangeListener {
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ fillFromPrefs(sharedPreferences);
+ }
+ }
+
+}
diff --git a/src/org/microg/nlp/backend/openwlanmap/PrefsFragment.java b/src/org/microg/nlp/backend/openwlanmap/PrefsFragment.java
new file mode 100644
index 0000000..7d8c8fe
--- /dev/null
+++ b/src/org/microg/nlp/backend/openwlanmap/PrefsFragment.java
@@ -0,0 +1,53 @@
+package org.microg.nlp.backend.openwlanmap;
+
+import org.microg.nlp.backend.openwlanmap.R;
+
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+
+public class PrefsFragment extends PreferenceFragment {
+
+ public PrefsFragment() {
+ super();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.settings);
+
+ CheckBoxPreference allowNetwork = (CheckBoxPreference) this.findPreference("networkAllowed");
+ allowNetwork.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+
+ return switchLocalGroup((Boolean) newValue);
+ }
+ });
+ //get initial state right
+ switchLocalGroup(allowNetwork.isChecked());
+
+
+ EditTextPreference dbLocPreference = (EditTextPreference) this.findPreference("databaseLocation");
+ if (dbLocPreference != null) {
+ //defaultValue doesn't work very well from code so we fill the pref this way
+ if (dbLocPreference.getText() == null || dbLocPreference.getText().isEmpty()) {
+ dbLocPreference.setText(Environment.getExternalStorageDirectory().getAbsolutePath() + "/.nogapps/openwifimap.db");
+ }
+ }
+ }
+
+ private boolean switchLocalGroup(boolean networkAllowed) {
+
+ PreferenceCategory localCategory = (PreferenceCategory) PrefsFragment.this.findPreference("category_local");
+ localCategory.setEnabled(!networkAllowed);
+
+ return true;
+ }
+}
diff --git a/src/org/microg/nlp/backend/openwlanmap/Settings.java b/src/org/microg/nlp/backend/openwlanmap/Settings.java
new file mode 100644
index 0000000..e303a54
--- /dev/null
+++ b/src/org/microg/nlp/backend/openwlanmap/Settings.java
@@ -0,0 +1,16 @@
+package org.microg.nlp.backend.openwlanmap;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class Settings extends Activity {
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getFragmentManager().beginTransaction().replace(android.R.id.content,
+ new PrefsFragment()).commit();
+ }
+}
diff --git a/src/org/microg/nlp/backend/openwlanmap/local/WifiLocationFile.java b/src/org/microg/nlp/backend/openwlanmap/local/WifiLocationFile.java
new file mode 100644
index 0000000..dea7bdc
--- /dev/null
+++ b/src/org/microg/nlp/backend/openwlanmap/local/WifiLocationFile.java
@@ -0,0 +1,145 @@
+package org.microg.nlp.backend.openwlanmap.local;
+
+import java.io.File;
+
+import org.microg.nlp.backend.openwlanmap.Configuration;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.location.Location;
+import android.util.Log;
+import android.util.LruCache;
+
+public class WifiLocationFile {
+ private static final String TABLE_APS = "APs";
+ private static final String COL_BSSSID = "bssid";
+ private static final String COL_LATITUDE = "latitude";
+ private static final String COL_LONGITUDE = "longitude";
+ private static File file;
+ private SQLiteDatabase database;
+
+ protected String TAG = WifiLocationFile.class.getName();
+
+
+ public WifiLocationFile() {
+ openDatabase();
+ }
+
+ /**
+ * DB negative query cache (not found in db).
+ */
+ private LruCache queryResultNegativeCache =
+ new LruCache(1000);
+ /**
+ * DB positive query cache (found in the db).
+ */
+ private LruCache queryResultCache =
+ new LruCache(1000);
+
+
+ private void openDatabase() {
+ if (database == null) {
+ file = new File(Configuration.dbLocation);
+ if (file.exists() && file.canRead()) {
+ database = SQLiteDatabase.openDatabase(file.getAbsolutePath(),
+ null,
+ SQLiteDatabase.NO_LOCALIZED_COLLATORS);
+ } else {
+ Log.e(TAG, "Could not open database at " + Configuration.dbLocation);
+ database = null;
+ }
+ }
+ }
+
+ public void close() {
+ if (database != null) {
+ database.close();
+ database = null;
+ }
+ }
+
+ public boolean exists() {
+ return file.exists() && file.canRead();
+ }
+
+ public String getPath() {
+ return file.getAbsolutePath();
+ }
+
+ private void checkForNewDb() {
+ File newDbFile = new File(Configuration.dbLocation + ".new");
+ if (newDbFile.exists() && newDbFile.canRead()) {
+ if (Configuration.debugEnabled) Log.d(TAG, "New database file detected.");
+ this.close();
+ queryResultCache = new LruCache(1000);
+ queryResultNegativeCache = new LruCache(1000);
+ file.renameTo(new File(Configuration.dbLocation + ".bak"));
+ newDbFile.renameTo(new File(Configuration.dbLocation));
+ openDatabase();
+ }
+ }
+
+ public synchronized Location query(final String bssid) {
+
+ checkForNewDb();
+
+ String normalizedBssid = bssid.replace(":", "");
+
+ if (Configuration.debugEnabled) Log.d(TAG, "Searching for BSSID '" + normalizedBssid + "'");
+
+ Boolean negative = queryResultNegativeCache.get(normalizedBssid);
+ if (negative != null && negative.booleanValue()) return null;
+
+ Location cached = queryResultCache.get(normalizedBssid);
+ if (cached != null) return cached;
+
+ if (database == null) {
+ if (Configuration.debugEnabled) Log.d(TAG, "Unable to open wifi database file.");
+ return null;
+ }
+
+ Location result = null;
+
+ Cursor cursor =
+ database.query(TABLE_APS,
+ new String[]{COL_LATITUDE,
+ COL_LONGITUDE},
+ COL_BSSSID + "=?",
+ new String[]{normalizedBssid},
+ null,
+ null,
+ null);
+ if (cursor != null) {
+ if (Configuration.debugEnabled) Log.d(TAG,"Database contains " + cursor.getCount() + " entries");
+ try {
+ if (cursor.getCount() > 0) {
+ cursor.moveToNext();
+
+ result = new Location("owm");
+ result.setLatitude(cursor.getDouble(cursor.getColumnIndexOrThrow(COL_LATITUDE)));
+ result.setLongitude(cursor.getDouble(cursor.getColumnIndexOrThrow(COL_LONGITUDE)));
+ result.setAccuracy(Configuration.assumedAccuracy);
+
+ if (result.getLatitude() == 0 || result.getLongitude() == 0) {
+ //this is the case for bssids where OWM detected a _nomap or other moving AP
+ if (Configuration.debugEnabled) Log.d(TAG, "BSSID '" + bssid + "' returns 0 values for lat or long. Skipped.");
+ queryResultNegativeCache.put(normalizedBssid, true);
+ return null;
+ }
+
+ queryResultCache.put(normalizedBssid, result);
+ if (Configuration.debugEnabled) Log.d(TAG,"Wifi info found for: " + normalizedBssid);
+
+ return result;
+ }
+ } finally {
+ cursor.close();
+ }
+
+
+ }
+ if (Configuration.debugEnabled) Log.d(TAG,"No Wifi info found for: " + normalizedBssid);
+ queryResultNegativeCache.put(normalizedBssid, true);
+ return null;
+ }
+}
diff --git a/src/org/microg/nlp/backend/openwlanmap/local/WifiReceiver.java b/src/org/microg/nlp/backend/openwlanmap/local/WifiReceiver.java
new file mode 100644
index 0000000..169ae35
--- /dev/null
+++ b/src/org/microg/nlp/backend/openwlanmap/local/WifiReceiver.java
@@ -0,0 +1,78 @@
+package org.microg.nlp.backend.openwlanmap.local;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import org.microg.nlp.backend.openwlanmap.Configuration;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+
+public class WifiReceiver extends BroadcastReceiver {
+
+ private boolean scanStarted = false;
+ private WifiManager wifi;
+ private String TAG = WifiReceiver.class.getName();
+ private WifiReceivedCallback callback;
+
+ public WifiReceiver(Context ctx, WifiReceivedCallback aCallback) {
+ wifi = (WifiManager) ctx.getSystemService(Context.WIFI_SERVICE);
+ callback = aCallback;
+ }
+
+ public void onReceive(Context c, Intent intent) {
+ if (!isScanStarted())
+ return;
+ setScanStarted(false);
+ List configs = wifi.getScanResults();
+
+ if (Configuration.debugEnabled) Log.d(TAG, "Got " + configs.size() + " wifi access points");
+
+ if (configs.size() > 0) {
+
+ List foundBssids = new ArrayList(configs.size());
+
+ for (ScanResult config : configs) {
+ // some strange devices use a dot instead of :
+ final String canonicalBSSID = config.BSSID.toUpperCase(Locale.US).replace(".",":");
+ // ignore APs that have _nomap suffix on SSID
+ if (config.SSID.endsWith("_nomap")) {
+ if (Configuration.debugEnabled) Log.d(TAG, "Ignoring AP '" + config.SSID + "' BSSID: " + canonicalBSSID);
+ } else {
+ foundBssids.add(canonicalBSSID);
+ }
+ }
+
+ callback.process(foundBssids);
+ }
+
+ }
+
+ public boolean isScanStarted() {
+ return scanStarted;
+ }
+
+ public void setScanStarted(boolean scanStarted) {
+ this.scanStarted = scanStarted;
+ }
+
+
+ public interface WifiReceivedCallback {
+
+ void process(List foundBssids);
+
+ }
+
+ public void startScan() {
+ setScanStarted(true);
+ if (!wifi.isWifiEnabled() && !wifi.isScanAlwaysAvailable()) {
+ Log.i(TAG, "Wifi is disabled and we can't scan either. Not doing anything.");
+ }
+ wifi.startScan();
+ }
+}