package org.gfd.gsmlocation;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.gfd.gsmlocation.db.CellTowerDatabase;
import org.gfd.gsmlocation.model.CellInfo;
import android.content.Context;
import android.telephony.CellIdentityGsm;
import android.telephony.CellIdentityWcdma;
import android.telephony.CellInfoGsm;
import android.telephony.CellInfoWcdma;
import android.telephony.CellLocation;
import android.telephony.NeighboringCellInfo;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
import android.telephony.gsm.GsmCellLocation;
import android.util.LruCache;
/**
* Keep track of the current location, dropping old cells over time. This class works by having a
* "measurement" counter and a timestamp. The measurement counter is increased whenever the signal
* strength and cell reaches a new value. This means that the measurement will stay rather static
* if you are stationary.
* The time counter is used to drop towers that are stalled. This is a
* protective tool to avoid completely broken data.
*/
public class CellbasedLocationProvider {
// The key purpose of this class is pulling all the APIs and trying to turn it into s.th.
// remotely consistent...
// Oh, and tracking the state and everything turns out to be rather easy _if_ you know how to
// do it. Basically: Never discard towers unless they've aged beyond recognition. Advance an
// internal time scale based on the stability of the gsm signal. That's about it.
// About the "magic" number (or magic constants)
// RSSI (receive signal strength indicator) is 0..31.
// The internal cache accepts 16 different measurements. The external cache accepts a max age
// of 16 measurements. This means that going from poorest to best reception will not kill any
// seen towers.
// Time based age: 6h should be enough between cell tower scans (at least for scans that we
// catch!).
// // // // // // // // // // // // // Singleton methods // // // // // // // // // // // // //
// Singletons, while often regarded the bad taste of OOP, solve a single problem:
// make sure that never ever there's a second instance running. Great for stuff like a listener
// that might be resource hungry :-)
/**
* Internal instance for singleton lookup.
*/
private static CellbasedLocationProvider ourInstance = new CellbasedLocationProvider();
/**
* Retrieve the singleton CellbasedLocationProvider instance.
* @return CellbasedLocationProvider the instance.
*/
public static CellbasedLocationProvider getInstance() {
return ourInstance;
}
/**
* Private constructor to disallow foreign instantiation.
*/
private CellbasedLocationProvider() { }
/**
* The measurement clock, incremented whenever the signal state changes significantly.
*/
private AtomicLong measurement = new AtomicLong(0);
/**
* Maximum age of a cell based on the measurement clock.
*/
private long MAX_MEASUREMENT_AGE = 16;
/**
* Maximum age of a cell based on the timestamp age.
*/
private long MAX_TIME_AGE = 6 * 60 * 60 * 1000;
/**
* Reference to the cell tower db.
*/
private CellTowerDatabase db = CellTowerDatabase.getInstance();
/**
* List of cells that were recently available and can be resolved (aka long/lat is set)
*/
private HashSet<CellInfo> recentCells = new HashSet<CellInfo>(17);
/**
* List of recent cells that were available but could not be resolved.
*/
private HashSet<CellInfo> unusedCells = new HashSet<CellInfo>(17);
/**
* The current cell location, needed on signal strength change.
*/
private GsmCellLocation location = null;
/**
* Update the internal list of unused (unresolved) cells.
* @param ci The cell information.
*/
private final void pushUnusedCells(CellInfo ci) {
ci.sanitize();
if (ci.isInvalid()) return ;
synchronized (unusedCells) {
boolean isNew = !unusedCells.remove(ci);
ci.seen = System.currentTimeMillis();
ci.measurement = measurement.get();
unusedCells.add(ci);
if (isNew)
android.util.Log.d("LNLP/Cell/Unresolved", ci.toString());
}
}
/**
* Update the internal list of resolved cell information.
* @param ci The cell information.
*/
private final void pushRecentCells(CellInfo ci) {
synchronized (recentCells) {
boolean isNew = !recentCells.remove(ci);
ci.seen = System.currentTimeMillis();
ci.measurement = measurement.get();
recentCells.add(ci);
if (isNew)
android.util.Log.d("LNLP/Cell", ci.toString());
}
}
/**
* Retrieve the list of recently resolved cells.
* @return Array copy of CellInfo instances.
*/
public CellInfo[] getAll() {
synchronized (recentCells) {
handle(false);
cleanup();
return recentCells.toArray(new CellInfo[recentCells.size()]);
}
}
/**
* All cells that are currently unused (can not resolved).
* @return Array copy of CellInfo instances.
*/
public CellInfo[] getAllUnused() {
synchronized (unusedCells) {
handle(false);
cleanup();
return unusedCells.toArray(new CellInfo[unusedCells.size()]);
}
}
public static class CountryResult {
public int currentCountry = 0;
public int[] countries = null;
}
public CountryResult getCountries() {
int currentCountry = 0;
HashSet<Integer> countries = new HashSet<Integer>(8);
for(CellInfo cellInfo : recentCells) {
if (cellInfo.MCC > 0) {
countries.add(cellInfo.MCC);
if (location != null &&
cellInfo.CID == location.getCid() &&
cellInfo.LAC == location.getLac()) {
currentCountry = cellInfo.MCC;
}
}
}
for(CellInfo cellInfo : unusedCells) {
if (cellInfo.MCC > 0) {
countries.add(cellInfo.MCC);
if (location != null &&
cellInfo.CID == location.getCid() &&
cellInfo.LAC == location.getLac()) {
currentCountry = cellInfo.MCC;
}
}
}
CountryResult result = new CountryResult();
result.currentCountry = currentCountry;
result.countries = new int[countries.size()];
Integer[] ints = countries.toArray(new Integer[countries.size()]);
for (int i = 0; i < result.countries.length; i++) {
result.countries[i] = ints[i];
}
return result;
}
/**
* Clean stalled entries within the recent/unused cell list.
*/
public void cleanup() {
ArrayList<CellInfo> dead = null;
long mThreshold = measurement.get() - MAX_MEASUREMENT_AGE;
long timeThreshold = System.currentTimeMillis() - MAX_TIME_AGE;
for (CellInfo ci : recentCells) {
boolean outdatedByAge = ci.seen <= timeThreshold;
boolean outdatedByMeasurement = ci.measurement <= mThreshold;
if (!outdatedByAge && !outdatedByMeasurement) continue;
if (dead == null) {
dead = new ArrayList<CellInfo>(recentCells.size() + 1);
}
String reason = "Cell outdated ";
if (outdatedByMeasurement && !outdatedByAge) {
reason = "Measurements reached ";
}
if (outdatedByAge && !outdatedByMeasurement) {
reason = "Timeout reached ";
}
android.util.Log.d("LNLP/Cell/Died", reason + ci.toString());
dead.add(ci);
}
if (dead != null) {
recentCells.removeAll(dead);
}
if (dead != null) dead.clear();
for (CellInfo ci : unusedCells) {
boolean outdatedByAge = ci.seen <= timeThreshold;
boolean outdatedByMeasurement = ci.measurement <= mThreshold;
if (!outdatedByAge && !outdatedByMeasurement) continue;
if (dead == null) {
dead = new ArrayList<CellInfo>(unusedCells.size() + 1);
}
String reason = "Cell outdated ";
if (outdatedByMeasurement && !outdatedByAge) {
reason = "Measurements reached ";
}
if (outdatedByAge && !outdatedByMeasurement) {
reason = "Timeout reached ";
}
android.util.Log.d("LNLP/Cell/Unused/Died", reason + ci.toString());
dead.add(ci);
}
if (dead != null) {
unusedCells.removeAll(dead);
}
}
/**
* Add a CellLocation, usually called if the phone switched to a new tower.
* @param icell The new cell location.
*/
public void add(CellLocation icell) {
if (icell == null) {
return;
}
if (!(icell instanceof GsmCellLocation)) {
return;
}
GsmCellLocation cell = (GsmCellLocation) icell;
List<CellInfo> cellInfos = db.query(cell.getCid(), cell.getLac());
if (cellInfos != null && !cellInfos.isEmpty()) {
long measurement = this.measurement.get();
for (CellInfo cellInfo : cellInfos) {
cellInfo.measurement = measurement;
cellInfo.seen = System.currentTimeMillis();
pushRecentCells(cellInfo);
}
} else {
CellInfo ci = new CellInfo();
ci.measurement = this.measurement.get();
ci.lng = 0d;
ci.lat = 0d;
ci.CID = ((GsmCellLocation) icell).getCid();
ci.LAC = ((GsmCellLocation) icell).getLac();
ci.MCC = -1;
ci.MNC = -1;
pushUnusedCells(ci);
}
}
/**
* Add neighbouring cells as generated by the getNeighboringCells API.
* @param neighbours The list of neighbouring cells.
*/
public void addNeighbours(List<NeighboringCellInfo> neighbours) {
if (neighbours == null || neighbours.isEmpty()) return;
for (NeighboringCellInfo neighbour : neighbours) {
List<CellInfo> cellInfos = db.query(neighbour.getCid(), neighbour.getLac());
if (cellInfos != null && !cellInfos.isEmpty()) {
for (CellInfo cellInfo : cellInfos) {
pushRecentCells(cellInfo);
}
} else {
CellInfo ci = new CellInfo();
ci.lng = 0d;
ci.lat = 0d;
ci.CID = neighbour.getCid();
ci.LAC = neighbour.getLac();
ci.MCC = -1;
ci.MNC = -1;
pushUnusedCells(ci);
}
}
}
/**
* Add a list of cells.
* @param inputCellInfos
*/
public void addCells(List<android.telephony.CellInfo> inputCellInfos) {
if (inputCellInfos == null || inputCellInfos.isEmpty()) return;
for (android.telephony.CellInfo inputCellInfo : inputCellInfos) {
List<CellInfo> cellInfos = null;
if (inputCellInfo instanceof CellInfoGsm) {
CellInfoGsm gsm = (CellInfoGsm) inputCellInfo;
CellIdentityGsm id = gsm.getCellIdentity();
cellInfos = db.query(id.getMcc(), id.getMnc(), id.getCid(), id.getLac());
if (cellInfos == null) {
CellInfo ci = new CellInfo();
ci.lng = 0d;
ci.lat = 0d;
ci.CID = id.getCid();
ci.LAC = id.getLac();
ci.MNC = id.getMnc();
ci.MCC = id.getMcc();
pushUnusedCells(ci);
}
}
if (inputCellInfo instanceof CellInfoWcdma) {
CellInfoWcdma wcdma = (CellInfoWcdma) inputCellInfo;
CellIdentityWcdma id = wcdma.getCellIdentity();
cellInfos = db.query(id.getMcc(), id.getMnc(), id.getCid(), id.getLac());
if (cellInfos == null) {
CellInfo ci = new CellInfo();
ci.lng = 0d;
ci.lat = 0d;
ci.CID = id.getCid();
ci.LAC = id.getLac();
ci.MNC = id.getMnc();
ci.MCC = id.getMcc();
pushUnusedCells(ci);
}
}
if (cellInfos == null) continue;
if (!cellInfos.isEmpty()) {
for (CellInfo cellInfo : cellInfos) {
pushRecentCells(cellInfo);
}
}
}
}
/**
* Handle a modem event by trying to pull all information. The parameter inc defines if the
* measurement counter should be increased on success.
* @param inc True if the measurement counter should be increased.
*/
private void handle(boolean inc) {
if (telephonyManager == null) return;
final List<android.telephony.CellInfo> cellInfos = telephonyManager.getAllCellInfo();
final List<NeighboringCellInfo> neighbours = telephonyManager.getNeighboringCellInfo();
final CellLocation cellLocation = telephonyManager.getCellLocation();
if (cellInfos == null || cellInfos.isEmpty()) {
if (neighbours == null || neighbours.isEmpty()) {
if (cellLocation == null || !(cellLocation instanceof GsmCellLocation)) return;
}
}
if (inc) measurement.getAndIncrement();
add(cellLocation);
addNeighbours(neighbours);
addCells(cellInfos);
synchronized (recentCells) {
cleanup();
}
}
/**
* The telephony manager used for modem queries.
*/
private TelephonyManager telephonyManager;
/**
* SignalStringthInfo represents a single CID/LAC with a rssi. Used for lookups / caching.
*/
private static class SignalStringthInfo {
public int CID;
public int LAC;
public int rssi;
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SignalStringthInfo that = (SignalStringthInfo) o;
if (CID != that.CID) return false;
if (LAC != that.LAC) return false;
if (rssi != that.rssi) return false;
return true;
}
public int hashCode() {
int result = CID;
result = 31 * result + LAC;
result = 31 * result + rssi;
return result;
}
public String toString() {
return "SignalStringthInfo{" +
"CID=" + CID +
", LAC=" + LAC +
", rssi=" + rssi +
'}';
}
}
/**
* Initialize the location provider.
* @param ctx The application context.
*/
public void init(Context ctx) {
telephonyManager = (TelephonyManager) ctx.getSystemService(Context.TELEPHONY_SERVICE);
final Context fctx = ctx;
new Thread() {
public void run() {
db.init(fctx);
}
}.start();
/**
* The <b>actual</b> phone listener, handling new modem based events.
*/
final PhoneStateListener listener = new PhoneStateListener() {
/**
* A cache for the last few cell/strength combinations we've seen. This helps to
* determine if we should count a measurement as a "new" measurement or if we should
* simply add whatever we got without incrementing the cell based time.
*/
private LruCache<SignalStringthInfo,SignalStringthInfo> recentSignals =
new LruCache<SignalStringthInfo, SignalStringthInfo>(16);
public void onSignalStrengthsChanged(SignalStrength signalStrength) {
if (location != null && location instanceof GsmCellLocation) {
SignalStringthInfo ssi = new SignalStringthInfo();
ssi.rssi = signalStrength.getGsmSignalStrength();
ssi.CID = ((GsmCellLocation) location).getCid();
ssi.LAC = ((GsmCellLocation) location).getLac();
boolean inc = false;
synchronized (recentSignals) {
inc = recentSignals.remove(ssi) == null;
recentSignals.put(ssi,ssi);
}
if (inc) {
android.util.Log.d("LNLP/Signal/Measurement", ssi.toString());
handle(true);
return;
}
}
handle(false);
}
public void onServiceStateChanged(ServiceState serviceState) {
handle(true);
}
public void onCellLocationChanged(CellLocation location) {
if (!(location instanceof GsmCellLocation)) return;
CellbasedLocationProvider.this.location = (GsmCellLocation) location;
measurement.getAndIncrement();
add(location);
handle(false);
}
public void onDataConnectionStateChanged(int state) {
handle(false);
}
public void onCellInfoChanged(List<android.telephony.CellInfo> cellInfo) {
measurement.getAndIncrement();
addCells(cellInfo);
handle(false);
}
};
telephonyManager.listen(
listener,
PhoneStateListener.LISTEN_SIGNAL_STRENGTHS |
PhoneStateListener.LISTEN_CELL_INFO |
PhoneStateListener.LISTEN_CELL_LOCATION |
PhoneStateListener.LISTEN_DATA_CONNECTION_STATE |
PhoneStateListener.LISTEN_SERVICE_STATE
);
}
}