package org.gfd.gsmlocation.db;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.gfd.gsmlocation.R;
import org.gfd.gsmlocation.model.CellInfo;
import org.tukaani.xz.XZInputStream;
import android.content.Context;
import android.telephony.NeighboringCellInfo;
import android.util.Log;
import android.util.LruCache;
public class CellTowerDatabase {
private static CellTowerDatabase ourInstance = new CellTowerDatabase();
public static CellTowerDatabase getInstance() {
return ourInstance;
}
private BCSReader reader = null;
private CellTowerDatabase() {}
/**
* Initialize the DB, possibly copying the content over to another file. Note that this method
* may require considerable amounts of time.
* @param ctx The app context.
*/
public void init(Context ctx) {
final int dbfilesize = ctx.getResources().getInteger(R.integer.dbfile_size);
final String dbfilename = ctx.getResources().getString(R.string.dbfile);
File path = ctx.getDatabasePath("towers");
path.mkdirs();
File db = new File(path + "/db.bcs");
android.util.Log.d("SS/CellTowerDatabase/Init", "Path: " + path);
if (!db.exists() || db.length() < dbfilesize) {
android.util.Log.d("SS/CellTowerDatabase/Init", "Database needs extraction...");
// extract. This can take *quite* some time.
try {
InputStream in = ctx.getAssets().open(dbfilename);
OutputStream out = new BufferedOutputStream(new FileOutputStream(db));
XZInputStream xz = new XZInputStream(in);
byte[] buf = new byte[16 * 1024];
boolean canread = true;
while (canread) {
final int read = xz.read(buf);
if (read > 0) {
out.write(buf, 0, read);
} else {
final int b = xz.read();
if (b == -1) {
canread = false;
} else {
out.write(b);
}
}
}
out.close();
xz.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
android.util.Log.d("SS/CellTowerDatabase/Init", "Database extracted!");
}
android.util.Log.d("SS/CellTowerDatabase/Init", "Opening database");
try {
reader = new BCSReader(
new Class<?>[]{Integer.class, Integer.class, Integer.class, Integer.class},
new Class<?>[]{Double.class, Double.class},
db.getCanonicalPath()
);
} catch (IOException e) {
Log.e("LNLP", "init failed", e);
}
}
/**
* Used internally for caching. HashMap compatible entity class.
*/
private static class QueryArgs {
Integer mcc;
Integer mnc;
int cid;
int lac;
private QueryArgs(Integer mcc, Integer mnc, int cid, int lac) {
this.mcc = mcc;
this.mnc = mnc;
this.cid = cid;
this.lac = lac;
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QueryArgs queryArgs = (QueryArgs) o;
if (cid != queryArgs.cid) return false;
if (lac != queryArgs.lac) return false;
if (mcc != null ? !mcc.equals(queryArgs.mcc) : queryArgs.mcc != null) return false;
if (mnc != null ? !mnc.equals(queryArgs.mnc) : queryArgs.mnc != null) return false;
return true;
}
public int hashCode() {
int result = mcc != null ? mcc.hashCode() : (1 << 16);
result = 31 * result + (mnc != null ? mnc.hashCode() : (1 << 16));
result = 31 * result + cid;
result = 31 * result + lac;
return result;
}
}
/**
* DB negative query cache (not found in db).
*/
private final LruCache<QueryArgs, Boolean> queryResultNegativeCache =
new LruCache<QueryArgs, Boolean>(10000);
/**
* DB positive query cache (found in the db).
*/
private final LruCache<QueryArgs, List<CellInfo>> queryResultCache =
new LruCache<QueryArgs, List<CellInfo>>(10000);
public List<CellInfo> query(final int cid, final int lac) {
return query(null, null, cid, lac);
}
/**
* Perform a (cached) DB query for a given cell tower. Note that MCC and MNC can be null.
* @param mcc
* @param mnc
* @param cid
* @param lac
* @return
*/
public List<CellInfo> query(final Integer mcc, final Integer mnc, final int cid, final int lac) {
if (this.reader == null) return null;
if (cid == NeighboringCellInfo.UNKNOWN_CID || cid == Integer.MAX_VALUE) return null;
if (mcc != null && mcc == Integer.MAX_VALUE) return query(null, mnc, cid, lac);
if (mnc != null && mnc == Integer.MAX_VALUE) return query(mcc, null, cid, lac);
QueryArgs args = new QueryArgs(mcc, mnc, cid, lac);
Boolean negative = queryResultNegativeCache.get(args);
if (negative != null && negative.booleanValue()) return null;
List<CellInfo> cached = queryResultCache.get(args);
if (cached != null) return cached;
List<CellInfo> result = _query(mcc, mnc, cid, lac);
if (result == null) {
queryResultNegativeCache.put(args, true);
return null;
}
result = Collections.unmodifiableList(result);
queryResultCache.put(args, result);
return result;
}
/**
* Internal db query to retrieve all cell tower candidates for a given cid/lac.
* @param mcc
* @param mnc
* @param cid
* @param lac
* @return
*/
private List<CellInfo> _query(Integer mcc, Integer mnc, int cid, int lac) {
if (this.reader == null) return null;
// we need at least CID/LAC
if (cid == NeighboringCellInfo.UNKNOWN_CID) return null;
android.util.Log.d("LNLP/Query", "(" + mcc + "," + mnc + "," + cid + "," + lac + ")");
List<CellInfo> cil = _queryDirect(mcc, mnc, cid, lac);
if (cil == null || cil.size() == 0) {
if (cid > 0xffff) {
_queryDirect(mcc, mnc, cid & 0xffff, lac);
}
}
if (cil != null && cil.size() > 0) {
return cil;
}
if (mcc != null && mnc != null) {
return query(mcc, null, cid, lac);
}
if (mcc != null || mnc != null) {
return query(null,null,cid,lac);
}
return null;
}
private List<CellInfo> _queryDirect(Integer mcc, Integer mnc, int cid, int lac) {
if (mcc != null && mnc != null) {
// try direct lookup
Object[] values;
try {
values = reader.get(lac, cid, mcc, mnc);
} catch (IOException e) {
Log.e("LNLP", "queryDirect failed", e);
return null; // We're broken
}
if (values != null) {
CellInfo ci = new CellInfo();
ci.CID = cid;
ci.LAC = lac;
ci.MCC = mcc;
ci.MNC = mnc;
ci.lng = (Double) values[0];
ci.lat = (Double) values[1];
return Arrays.asList(new CellInfo[]{ci});
}
return null;
}
if (mnc != null && mcc == null) {
// this is special, we can only search for cid + lac and must filter afterwards
BCSReader.BlockEntry[] be;
try {
be = reader.getAll(lac, cid);
} catch (IOException e) {
Log.e("LNLP", "queryDirect failed", e);
return null; // br0ke
}
if (be != null && be.length > 0) {
ArrayList<CellInfo> cil = new ArrayList<CellInfo>();
for (BCSReader.BlockEntry e : be) {
if (((Integer) e.key[3]).equals(mnc)) {
CellInfo ci = new CellInfo();
ci.CID = (Integer) e.key[1];
ci.LAC = (Integer) e.key[0];
ci.MCC = (Integer) e.key[2];
ci.MNC = (Integer) e.key[3];
ci.lng = (Double) e.value[0];
ci.lat = (Double) e.value[1];
cil.add(ci);
}
}
if (!cil.isEmpty()) {
return cil;
}
}
return null;
}
BCSReader.BlockEntry[] be;
if (mcc != null) {
try {
be = reader.getAll(lac, cid, mcc);
} catch (IOException e) {
Log.e("LNLP", "queryDirect failed", e);
return null; // br0ke
}
} else {
try {
be = reader.getAll(lac, cid);
} catch (IOException e) {
Log.e("LNLP", "queryDirect failed", e);
return null; // br0ke
}
}
if (be == null || be.length == 0) {
return null;
}
ArrayList<CellInfo> cil = new ArrayList<CellInfo>();
for (BCSReader.BlockEntry e : be) {
CellInfo ci = new CellInfo();
ci.CID = (Integer) e.key[1];
ci.LAC = (Integer) e.key[0];
ci.MCC = (Integer) e.key[2];
ci.MNC = (Integer) e.key[3];
ci.lng = (Double) e.value[0];
ci.lat = (Double) e.value[1];
cil.add(ci);
}
return cil;
}
}