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(); + } +}