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; } }