| // Copyright 2007 The Android Open Source Project |
| |
| package com.android.internal.location; |
| |
| import java.io.DataInput; |
| import java.io.DataInputStream; |
| import java.io.DataOutput; |
| import java.io.DataOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import android.location.Location; |
| import android.location.LocationManager; |
| import android.net.wifi.ScanResult; |
| import android.os.Bundle; |
| import android.util.Log; |
| |
| /** |
| * Data store to cache cell-id and wifi locations from the network |
| * |
| * {@hide} |
| */ |
| public class LocationCache { |
| private static final String TAG = "LocationCache"; |
| |
| // Version of cell cache |
| private static final int CACHE_DB_VERSION = 1; |
| |
| // Don't save cache more than once every minute |
| private static final long SAVE_FREQUENCY = 60 * 1000; |
| |
| // Location of the cache file; |
| private static final String mCellCacheFile = "cache.cell"; |
| private static final String mWifiCacheFile = "cache.wifi"; |
| |
| // Maximum time (in millis) that a record is valid for, before it needs |
| // to be refreshed from the server. |
| private static final long MAX_CELL_REFRESH_RECORD_AGE = 12 * 60 * 60 * 1000; // 12 hours |
| private static final long MAX_WIFI_REFRESH_RECORD_AGE = 48 * 60 * 60 * 1000; // 48 hours |
| |
| // Cache sizes |
| private static final int MAX_CELL_RECORDS = 50; |
| private static final int MAX_WIFI_RECORDS = 200; |
| |
| // Cache constants |
| private static final long CELL_SMOOTHING_WINDOW = 30 * 1000; // 30 seconds |
| private static final int WIFI_MIN_AP_REQUIRED = 2; |
| private static final int WIFI_MAX_MISS_ALLOWED = 5; |
| private static final int MAX_ACCURACY_ALLOWED = 5000; // 5km |
| |
| // Caches |
| private final Cache<Record> mCellCache; |
| private final Cache<Record> mWifiCache; |
| |
| // Currently calculated centroids |
| private final LocationCentroid mCellCentroid = new LocationCentroid(); |
| private final LocationCentroid mWifiCentroid = new LocationCentroid(); |
| |
| // Extra key and values |
| private final String EXTRA_KEY_LOCATION_TYPE = "networkLocationType"; |
| private final String EXTRA_VALUE_LOCATION_TYPE_CELL = "cell"; |
| private final String EXTRA_VALUE_LOCATION_TYPE_WIFI = "wifi"; |
| |
| public LocationCache() { |
| mCellCache = new Cache<Record>(LocationManager.SYSTEM_DIR, mCellCacheFile, |
| MAX_CELL_RECORDS, MAX_CELL_REFRESH_RECORD_AGE); |
| mWifiCache = new Cache<Record>(LocationManager.SYSTEM_DIR, mWifiCacheFile, |
| MAX_WIFI_RECORDS, MAX_WIFI_REFRESH_RECORD_AGE); |
| } |
| |
| /** |
| * Looks up network location on device cache |
| * |
| * @param cellState primary cell state |
| * @param cellHistory history of cell states |
| * @param scanResults wifi scan results |
| * @param result location object to fill if location is found |
| * @return true if cache was able to answer query (successfully or not), false if call to |
| * server is required |
| */ |
| public synchronized boolean lookup(CellState cellState, List<CellState> cellHistory, |
| List<ScanResult> scanResults, Location result) { |
| |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| Log.d(TAG, "including cell:" + (cellState != null) + |
| ", wifi:" + ((scanResults != null)? scanResults.size() : "null")); |
| } |
| |
| long now = System.currentTimeMillis(); |
| |
| mCellCentroid.reset(); |
| mWifiCentroid.reset(); |
| |
| if (cellState != null && cellState.isValid()) { |
| String primaryCellKey = getCellCacheKey(cellState.getMcc(), cellState.getMnc(), |
| cellState.getLac(), cellState.getCid()); |
| Record record = mCellCache.lookup(primaryCellKey); |
| |
| // Relax MCC/MNC condition |
| if (record == null) { |
| primaryCellKey = getCellCacheKey(-1, -1, cellState.getLac(), cellState.getCid()); |
| record = mCellCache.lookup(primaryCellKey); |
| } |
| |
| if (record == null) { |
| // Make a server request if primary cell doesn't exist in DB |
| return false; |
| } |
| |
| if (record.isValid()) { |
| mCellCentroid.addLocation(record.getLat(), record.getLng(), record.getAccuracy(), |
| record.getConfidence()); |
| } |
| } |
| |
| if (cellHistory != null) { |
| for (CellState historicalCell : cellHistory) { |
| // Cell location might need to be smoothed if you are on the border of two cells |
| if (now - historicalCell.getTime() < CELL_SMOOTHING_WINDOW) { |
| String historicalCellKey = getCellCacheKey(historicalCell.getMcc(), |
| historicalCell.getMnc(), historicalCell.getLac(), historicalCell.getCid()); |
| Record record = mCellCache.lookup(historicalCellKey); |
| |
| // Relax MCC/MNC condition |
| if (record == null) { |
| historicalCellKey = getCellCacheKey(-1, -1, historicalCell.getLac(), |
| historicalCell.getCid()); |
| record = mCellCache.lookup(historicalCellKey); |
| } |
| |
| if (record != null && record.isValid()) { |
| mCellCentroid.addLocation(record.getLat(), record.getLng(), |
| record.getAccuracy(), record.getConfidence()); |
| } |
| } |
| } |
| } |
| |
| if (scanResults != null) { |
| int miss = 0; |
| for (ScanResult scanResult : scanResults) { |
| String wifiKey = scanResult.BSSID; |
| Record record = mWifiCache.lookup(wifiKey); |
| if (record == null) { |
| miss++; |
| } else { |
| if (record.isValid()) { |
| mWifiCentroid.addLocation(record.getLat(), record.getLng(), |
| record.getAccuracy(), record.getConfidence()); |
| } |
| } |
| } |
| |
| if (mWifiCentroid.getNumber() >= WIFI_MIN_AP_REQUIRED) { |
| // Try to return best out of the available cell or wifi location |
| } else if (miss > Math.min(WIFI_MAX_MISS_ALLOWED, (scanResults.size()+1)/2)) { |
| // Make a server request |
| return false; |
| } else { |
| // Don't use wifi location, only consider using cell location |
| mWifiCache.save(); |
| mWifiCentroid.reset(); |
| } |
| } |
| |
| if (mCellCentroid.getNumber() > 0) { |
| mCellCache.save(); |
| } |
| if (mWifiCentroid.getNumber() > 0) { |
| mWifiCache.save(); |
| } |
| |
| int cellAccuracy = mCellCentroid.getAccuracy(); |
| int wifiAccuracy = mWifiCentroid.getAccuracy(); |
| |
| int cellConfidence = mCellCentroid.getConfidence(); |
| int wifiConfidence = mWifiCentroid.getConfidence(); |
| |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| Log.d(TAG, "cellAccuracy:" + cellAccuracy+ ", wifiAccuracy:" + wifiAccuracy); |
| } |
| |
| if ((mCellCentroid.getNumber() == 0 || cellAccuracy > MAX_ACCURACY_ALLOWED) && |
| (mWifiCentroid.getNumber() == 0 || wifiAccuracy > MAX_ACCURACY_ALLOWED)) { |
| // Return invalid location if neither location is valid |
| result.setAccuracy(-1); |
| |
| // don't make server request |
| return true; |
| } |
| |
| float[] distance = new float[1]; |
| Location.distanceBetween(mCellCentroid.getCentroidLat(), mCellCentroid.getCentroidLng(), |
| mWifiCentroid.getCentroidLat(), mWifiCentroid.getCentroidLng(), |
| distance); |
| |
| boolean consistent = distance[0] <= (cellAccuracy + wifiAccuracy); |
| |
| boolean useCell = true; |
| |
| if (consistent) { |
| // If consistent locations, use one with greater accuracy |
| useCell = (cellAccuracy <= wifiAccuracy); |
| } else { |
| // If inconsistent locations, use one with greater confidence |
| useCell = (cellConfidence >= wifiConfidence); |
| } |
| |
| if (useCell) { |
| // Use cell results |
| result.setAccuracy(cellAccuracy); |
| result.setLatitude(mCellCentroid.getCentroidLat()); |
| result.setLongitude(mCellCentroid.getCentroidLng()); |
| result.setTime(now); |
| |
| Bundle extras = result.getExtras() == null ? new Bundle() : result.getExtras(); |
| extras.putString(EXTRA_KEY_LOCATION_TYPE, EXTRA_VALUE_LOCATION_TYPE_CELL); |
| result.setExtras(extras); |
| |
| } else { |
| // Use wifi results |
| result.setAccuracy(wifiAccuracy); |
| result.setLatitude(mWifiCentroid.getCentroidLat()); |
| result.setLongitude(mWifiCentroid.getCentroidLng()); |
| result.setTime(now); |
| |
| Bundle extras = result.getExtras() == null ? new Bundle() : result.getExtras(); |
| extras.putString(EXTRA_KEY_LOCATION_TYPE, EXTRA_VALUE_LOCATION_TYPE_WIFI); |
| result.setExtras(extras); |
| |
| } |
| |
| // don't make a server request |
| return true; |
| } |
| |
| public synchronized void insert(int mcc, int mnc, int lac, int cid, double lat, double lng, |
| int accuracy, int confidence, long time) { |
| String key = getCellCacheKey(mcc, mnc, lac, cid); |
| if (accuracy <= 0) { |
| mCellCache.insert(key, new Record()); |
| } else { |
| mCellCache.insert(key, new Record(accuracy, confidence, lat, lng, time)); |
| } |
| } |
| |
| public synchronized void insert(String bssid, double lat, double lng, int accuracy, |
| int confidence, long time) { |
| if (accuracy <= 0) { |
| mWifiCache.insert(bssid, new Record()); |
| } else { |
| mWifiCache.insert(bssid, new Record(accuracy, confidence, lat, lng, time)); |
| } |
| } |
| |
| public synchronized void save() { |
| mCellCache.save(); |
| mWifiCache.save(); |
| } |
| |
| /** |
| * Cell or Wifi location record |
| */ |
| public static class Record { |
| |
| private final double lat; |
| private final double lng; |
| private final int accuracy; |
| private final int confidence; |
| |
| // Time (since the epoch) of original reading. |
| private final long originTime; |
| |
| public static Record read(DataInput dataInput) throws IOException { |
| final int accuracy = dataInput.readInt(); |
| final int confidence = dataInput.readInt(); |
| final double lat = dataInput.readDouble(); |
| final double lng = dataInput.readDouble(); |
| final long readingTime = dataInput.readLong(); |
| return new Record(accuracy, confidence, lat, lng, readingTime); |
| } |
| |
| /** |
| * Creates an "invalid" record indicating there was no location data |
| * available for the given data |
| */ |
| public Record() { |
| this(-1, 0, 0, 0, System.currentTimeMillis()); |
| } |
| |
| /** |
| * Creates a Record |
| * |
| * @param accuracy acuracy in meters. If < 0, then this is an invalid record. |
| * @param confidence confidence (0-100) |
| * @param lat latitude |
| * @param lng longitude |
| * @param time Time of the original location reading from the server |
| */ |
| public Record(int accuracy, int confidence, double lat, double lng, long time) { |
| this.accuracy = accuracy; |
| this.confidence = confidence; |
| this.originTime = time; |
| this.lat = lat; |
| this.lng = lng; |
| } |
| |
| public double getLat() { |
| return lat; |
| } |
| |
| public double getLng() { |
| return lng; |
| } |
| |
| public int getAccuracy() { |
| return accuracy; |
| } |
| |
| public int getConfidence() { |
| return confidence; |
| } |
| |
| public boolean isValid() { |
| return accuracy > 0; |
| } |
| |
| public long getTime() { |
| return originTime; |
| } |
| |
| public void write(DataOutput dataOut) throws IOException { |
| dataOut.writeInt(accuracy); |
| dataOut.writeInt(confidence); |
| dataOut.writeDouble(lat); |
| dataOut.writeDouble(lng); |
| dataOut.writeLong(originTime); |
| } |
| |
| @Override |
| public String toString() { |
| return lat + "," + lng + "," + originTime +"," + accuracy + "," + confidence; |
| } |
| } |
| |
| public class Cache<T> extends LinkedHashMap { |
| private final long mMaxAge; |
| private final int mCapacity; |
| private final String mDir; |
| private final String mFile; |
| private long mLastSaveTime = 0; |
| |
| public Cache(String dir, String file, int capacity, long maxAge) { |
| super(capacity + 1, 1.1f, true); |
| this.mCapacity = capacity; |
| this.mDir = dir; |
| this.mFile = file; |
| this.mMaxAge = maxAge; |
| load(); |
| } |
| |
| private LocationCache.Record lookup(String key) { |
| LocationCache.Record result = (LocationCache.Record) get(key); |
| |
| if (result == null) { |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| Log.d(TAG, "lookup: " + key + " failed"); |
| } |
| return null; |
| } |
| |
| // Cache entry needs refresh |
| if (result.getTime() + mMaxAge < System.currentTimeMillis()) { |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| Log.d(TAG, "lookup: " + key + " expired"); |
| } |
| return null; |
| } |
| |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| Log.d(TAG, "lookup: " + key + " " + result.toString()); |
| } |
| |
| return result; |
| } |
| |
| private void insert(String key, LocationCache.Record record) { |
| remove(key); |
| put(key, record); |
| |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| Log.d(TAG, "insert: " + key + " " + record.toString()); |
| } |
| } |
| |
| @Override |
| protected boolean removeEldestEntry(Map.Entry eldest) { |
| // Remove cache entries when it has more than capacity |
| return size() > mCapacity; |
| } |
| |
| private void load() { |
| FileInputStream istream; |
| try { |
| File f = new File(mDir, mFile); |
| istream = new FileInputStream(f); |
| } catch (FileNotFoundException e) { |
| // No existing DB - return new CellCache |
| return; |
| } |
| |
| DataInputStream dataInput = new DataInputStream(istream); |
| |
| try { |
| int version = dataInput.readUnsignedShort(); |
| if (version != CACHE_DB_VERSION) { |
| // Ignore records - invalid version ID. |
| dataInput.close(); |
| return; |
| } |
| int records = dataInput.readUnsignedShort(); |
| |
| for (int i = 0; i < records; i++) { |
| final String key = dataInput.readUTF(); |
| final LocationCache.Record record = LocationCache.Record.read(dataInput); |
| //Log.d(TAG, key + " " + record.toString()); |
| put(key, record); |
| } |
| |
| dataInput.close(); |
| } catch (IOException e) { |
| // Something's corrupted - return a new CellCache |
| } |
| } |
| |
| private void save() { |
| long now = System.currentTimeMillis(); |
| if (mLastSaveTime != 0 && (now - mLastSaveTime < SAVE_FREQUENCY)) { |
| // Don't save to file more often than SAVE_FREQUENCY |
| return; |
| } |
| |
| FileOutputStream ostream; |
| |
| File systemDir = new File(mDir); |
| if (!systemDir.exists()) { |
| if (!systemDir.mkdirs()) { |
| Log.e(TAG, "Cache.save(): couldn't create directory"); |
| return; |
| } |
| } |
| |
| try { |
| File f = new File(mDir, mFile); |
| ostream = new FileOutputStream(f); |
| } catch (FileNotFoundException e) { |
| Log.d(TAG, "Cache.save(): unable to create cache file", e); |
| return; |
| } |
| |
| DataOutputStream dataOut = new DataOutputStream(ostream); |
| try { |
| dataOut.writeShort(CACHE_DB_VERSION); |
| |
| dataOut.writeShort(size()); |
| |
| for (Iterator iter = entrySet().iterator(); iter.hasNext();) { |
| Map.Entry entry = (Map.Entry) iter.next(); |
| String key = (String) entry.getKey(); |
| LocationCache.Record record = (LocationCache.Record) entry.getValue(); |
| dataOut.writeUTF(key); |
| record.write(dataOut); |
| } |
| |
| dataOut.close(); |
| mLastSaveTime = now; |
| |
| } catch (IOException e) { |
| Log.e(TAG, "Cache.save(): unable to write cache", e); |
| // This should never happen |
| } |
| } |
| } |
| |
| public class LocationCentroid { |
| |
| double mLatSum = 0; |
| double mLngSum = 0; |
| int mNumber = 0; |
| int mConfidence = 0; |
| |
| double mCentroidLat = 0; |
| double mCentroidLng = 0; |
| |
| // Probably never have to calculate centroid for more than 10 locations |
| final static int MAX_SIZE = 10; |
| double[] mLats = new double[MAX_SIZE]; |
| double[] mLngs = new double[MAX_SIZE]; |
| int[] mRadii = new int[MAX_SIZE]; |
| |
| LocationCentroid() { |
| reset(); |
| } |
| |
| public void reset() { |
| mLatSum = 0; |
| mLngSum = 0; |
| mNumber = 0; |
| mConfidence = 0; |
| |
| mCentroidLat = 0; |
| mCentroidLng = 0; |
| |
| for (int i = 0; i < MAX_SIZE; i++) { |
| mLats[i] = 0; |
| mLngs[i] = 0; |
| mRadii[i] = 0; |
| } |
| } |
| |
| public void addLocation(double lat, double lng, int accuracy, int confidence) { |
| if (mNumber < MAX_SIZE && accuracy <= MAX_ACCURACY_ALLOWED) { |
| mLatSum += lat; |
| mLngSum += lng; |
| if (confidence > mConfidence) { |
| mConfidence = confidence; |
| } |
| |
| mLats[mNumber] = lat; |
| mLngs[mNumber] = lng; |
| mRadii[mNumber] = accuracy; |
| mNumber++; |
| } |
| } |
| |
| public int getNumber() { |
| return mNumber; |
| } |
| |
| public double getCentroidLat() { |
| if (mCentroidLat == 0 && mNumber != 0) { |
| mCentroidLat = mLatSum/mNumber; |
| } |
| return mCentroidLat; |
| } |
| |
| public double getCentroidLng() { |
| if (mCentroidLng == 0 && mNumber != 0) { |
| mCentroidLng = mLngSum/mNumber; |
| } |
| return mCentroidLng; |
| } |
| |
| public int getConfidence() { |
| return mConfidence; |
| } |
| |
| public int getAccuracy() { |
| if (mNumber == 0) { |
| return 0; |
| } |
| |
| if (mNumber == 1) { |
| return mRadii[0]; |
| } |
| |
| double cLat = getCentroidLat(); |
| double cLng = getCentroidLng(); |
| |
| int meanDistanceSum = 0; |
| int smallestCircle = MAX_ACCURACY_ALLOWED; |
| int smallestCircleDistance = MAX_ACCURACY_ALLOWED; |
| float[] distance = new float[1]; |
| boolean outlierExists = false; |
| |
| for (int i = 0; i < mNumber; i++) { |
| Location.distanceBetween(cLat, cLng, mLats[i], mLngs[i], distance); |
| meanDistanceSum += (int)distance[0]; |
| if (distance[0] > mRadii[i]) { |
| outlierExists = true; |
| } |
| if (mRadii[i] < smallestCircle) { |
| smallestCircle = mRadii[i]; |
| smallestCircleDistance = (int)distance[0]; |
| } |
| } |
| |
| if (outlierExists) { |
| return meanDistanceSum/mNumber; |
| } else { |
| return Math.max(smallestCircle, smallestCircleDistance); |
| } |
| } |
| |
| } |
| |
| private String getCellCacheKey(int mcc, int mnc, int lac, int cid) { |
| return mcc + ":" + mnc + ":" + lac + ":" + cid; |
| } |
| |
| } |