/* * SPDX-FileCopyrightText: 2013 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.nlp.api; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.Handler; import android.telephony.CellIdentity; import android.telephony.CellIdentityCdma; import android.telephony.CellIdentityGsm; import android.telephony.CellIdentityLte; import android.telephony.CellIdentityWcdma; import android.telephony.CellInfo; import android.telephony.CellInfoCdma; import android.telephony.CellInfoGsm; import android.telephony.CellInfoLte; import android.telephony.CellInfoWcdma; import android.telephony.CellLocation; import android.telephony.CellSignalStrengthCdma; import android.telephony.CellSignalStrengthGsm; import android.telephony.CellSignalStrengthLte; import android.telephony.CellSignalStrengthWcdma; import android.telephony.PhoneStateListener; import android.telephony.SignalStrength; import android.telephony.TelephonyManager; import android.telephony.cdma.CdmaCellLocation; import android.telephony.gsm.GsmCellLocation; import android.util.Log; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; import static android.Manifest.permission.READ_PHONE_STATE; /** * Utility class to support backends that use Cells for geolocation. *

* Due to changes in APIs for cell retrieval, this class will only work on Android 4.2+ * Support for earlier Android versions might be added later... */ @SuppressWarnings({"JavaReflectionMemberAccess", "unused", "WeakerAccess", "deprecation"}) @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @SuppressLint("MissingPermission") public class CellBackendHelper extends AbstractBackendHelper { private final Listener listener; private final TelephonyManager telephonyManager; private final Set cells = new HashSet<>(); private PhoneStateListener phoneStateListener; private boolean supportsCellInfoChanged = true; public static final int MIN_UPDATE_INTERVAL = 30 * 1000; public static final int FALLBACK_UPDATE_INTERVAL = 5 * 60 * 1000; private long lastScan = 0; /** * Create a new instance of {@link CellBackendHelper}. Call this in * {@link LocationBackendService#onCreate()}. * * @throws IllegalArgumentException if either context or listener is null. * @throws IllegalStateException if android version is below 4.2 */ public CellBackendHelper(Context context, Listener listener) { super(context); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) throw new IllegalStateException("Requires Android 4.2+"); if (listener == null) throw new IllegalArgumentException("listener must not be null"); this.listener = listener; this.telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); } private int getMcc() { try { return Integer.parseInt(telephonyManager.getNetworkOperator().substring(0, 3)); } catch (Exception e) { return -1; } } private int getMnc() { try { return Integer.parseInt(telephonyManager.getNetworkOperator().substring(3)); } catch (Exception e) { return -1; } } private static Cell.CellType getCellType(int networkType) { switch (networkType) { case TelephonyManager.NETWORK_TYPE_GPRS: case TelephonyManager.NETWORK_TYPE_EDGE: return Cell.CellType.GSM; case TelephonyManager.NETWORK_TYPE_UMTS: case TelephonyManager.NETWORK_TYPE_HSDPA: case TelephonyManager.NETWORK_TYPE_HSUPA: case TelephonyManager.NETWORK_TYPE_HSPA: case TelephonyManager.NETWORK_TYPE_HSPAP: return Cell.CellType.UMTS; case TelephonyManager.NETWORK_TYPE_LTE: return Cell.CellType.LTE; case TelephonyManager.NETWORK_TYPE_EVDO_0: case TelephonyManager.NETWORK_TYPE_EVDO_A: case TelephonyManager.NETWORK_TYPE_EVDO_B: case TelephonyManager.NETWORK_TYPE_1xRTT: case TelephonyManager.NETWORK_TYPE_EHRPD: case TelephonyManager.NETWORK_TYPE_IDEN: return Cell.CellType.CDMA; } return null; } @SuppressWarnings("ChainOfInstanceofChecks") private Cell parseCellInfo(CellInfo info) { try { if (info instanceof CellInfoGsm) { CellIdentityGsm identity = ((CellInfoGsm) info).getCellIdentity(); if (identity.getMcc() == Integer.MAX_VALUE) return null; CellSignalStrengthGsm strength = ((CellInfoGsm) info).getCellSignalStrength(); return new Cell(Cell.CellType.GSM, identity.getMcc(), identity.getMnc(), identity.getLac(), identity.getCid(), -1, strength.getDbm()); } else if (info instanceof CellInfoCdma) { CellIdentityCdma identity = ((CellInfoCdma) info).getCellIdentity(); CellSignalStrengthCdma strength = ((CellInfoCdma) info).getCellSignalStrength(); return new Cell(Cell.CellType.CDMA, getMcc(), identity.getSystemId(), identity.getNetworkId(), identity.getBasestationId(), -1, strength.getDbm()); } else { return parceCellInfo18(info); } } catch (Exception ignored) { Log.d(TAG, "Failed to parse cell info " + info, ignored); } return null; } @SuppressWarnings({"ChainOfInstanceofChecks", "deprecation"}) @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private Cell parceCellInfo18(CellInfo info) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return null; if (info instanceof CellInfoWcdma) { CellIdentityWcdma identity = ((CellInfoWcdma) info).getCellIdentity(); if (identity.getMcc() == Integer.MAX_VALUE) return null; CellSignalStrengthWcdma strength = ((CellInfoWcdma) info).getCellSignalStrength(); return new Cell(Cell.CellType.UMTS, identity.getMcc(), identity.getMnc(), identity.getLac(), identity.getCid(), identity.getPsc(), strength.getDbm()); } else if (info instanceof CellInfoLte) { CellIdentityLte identity = ((CellInfoLte) info).getCellIdentity(); if (identity.getMcc() == Integer.MAX_VALUE) return null; CellSignalStrengthLte strength = ((CellInfoLte) info).getCellSignalStrength(); return new Cell(Cell.CellType.LTE, identity.getMcc(), identity.getMnc(), identity.getTac(), identity.getCi(), identity.getPci(), strength.getDbm()); } return null; } @SuppressWarnings("deprecation") private Cell parseCellInfo(android.telephony.NeighboringCellInfo info) { try { if (getCellType(info.getNetworkType()) != Cell.CellType.GSM) return null; return new Cell(Cell.CellType.GSM, getMcc(), getMnc(), info.getLac(), info.getCid(), info.getPsc(), info.getRssi()); } catch (Exception ignored) { } return null; } private void onCellsChanged(List cellInfo) { lastScan = System.currentTimeMillis(); if (loadCells(cellInfo)) { listener.onCellsChanged(getCells()); } } /** * This will fix empty MNC since Android 9 with 0-prefixed MNCs. * Issue: https://issuetracker.google.com/issues/113560852 */ @SuppressLint("SoonBlockedPrivateApi") private void fixEmptyMnc(List cellInfo) { if (Build.VERSION.SDK_INT < 28 || cellInfo == null) { return; } String networkOperator = telephonyManager.getNetworkOperator(); if (networkOperator.length() < 5 || networkOperator.charAt(3) != '0') { return; } String mnc = networkOperator.substring(3); for (CellInfo info : cellInfo) { if (!info.isRegistered()) { continue; } Object identity = null; if (info instanceof CellInfoGsm) { identity = ((CellInfoGsm) info).getCellIdentity(); } else if (info instanceof CellInfoWcdma) { identity = ((CellInfoWcdma) info).getCellIdentity(); } else if (info instanceof CellInfoLte) { identity = ((CellInfoLte) info).getCellIdentity(); } if (identity == null) { continue; } try { Field mncField = CellIdentity.class.getDeclaredField("mMncStr"); mncField.setAccessible(true); if (mncField.get(identity) == null) { mncField.set(identity, mnc); } } catch (Exception ignored) { } } } /** * This will fix values returned by {@link TelephonyManager#getAllCellInfo()} as described * here: https://github.com/mozilla/ichnaea/issues/340 */ @SuppressWarnings({"ChainOfInstanceofChecks", "MagicNumber", "ConstantConditions"}) @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private void fixShortMncBug(List cellInfo) { if (cellInfo == null) return; String networkOperator = telephonyManager.getNetworkOperator(); if (networkOperator.length() != 5) return; int realMnc = Integer.parseInt(networkOperator.substring(3)); boolean theBug = false; for (CellInfo info : cellInfo) { if (info instanceof CellInfoCdma) return; if (info.isRegistered()) { Cell cell = parseCellInfo(info); if (cell == null) continue; int infoMnc = cell.getMnc(); if (infoMnc == (realMnc * 10 + 15)) { theBug = true; } } } if (theBug) { for (CellInfo info : cellInfo) { Object identity = null; if (info instanceof CellInfoGsm) identity = ((CellInfoGsm) info).getCellIdentity(); else if (info instanceof CellInfoLte) identity = ((CellInfoLte) info).getCellIdentity(); else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && info instanceof CellInfoWcdma) identity = ((CellInfoWcdma) info).getCellIdentity(); if (identity == null) continue; try { Field mncField = identity.getClass().getDeclaredField("mMnc"); mncField.setAccessible(true); int mnc = (Integer) mncField.get(identity); if (mnc >= 25 && mnc <= 1005) { mnc = (mnc - 15) / 10; mncField.setInt(identity, mnc); } } catch (Exception ignored) { } } } } private boolean hasCid(long cid) { for (Cell cell : cells) { if (cell.getCid() == cid) return true; } return false; } /** * This is to support some broken implementations that do not support {@link TelephonyManager#getAllCellInfo()} */ @SuppressWarnings("ChainOfInstanceofChecks") private CellInfo fromCellLocation(CellLocation cellLocation) { try { if (cellLocation instanceof GsmCellLocation) { GsmCellLocation gsmCellLocation = (GsmCellLocation) cellLocation; CellIdentityGsm identity = CellIdentityGsm.class.getConstructor(int.class, int.class, int.class, int.class) .newInstance(getMcc(), getMnc(), gsmCellLocation.getLac(), gsmCellLocation.getCid()); CellSignalStrengthGsm strength = CellSignalStrengthGsm.class.newInstance(); CellInfoGsm info = CellInfoGsm.class.newInstance(); CellInfoGsm.class.getMethod("setCellIdentity", CellIdentityGsm.class).invoke(info, identity); CellInfoGsm.class.getMethod("setCellSignalStrength", CellSignalStrengthGsm.class).invoke(info, strength); return info; } if (cellLocation instanceof CdmaCellLocation) { CdmaCellLocation cdmaCellLocation = (CdmaCellLocation) cellLocation; CellIdentityCdma identity = CellIdentityCdma.class.getConstructor(int.class, int.class, int.class, int.class, int.class) .newInstance(cdmaCellLocation.getNetworkId(), cdmaCellLocation.getSystemId(), cdmaCellLocation.getBaseStationId(), cdmaCellLocation.getBaseStationLongitude(), cdmaCellLocation.getBaseStationLatitude()); CellSignalStrengthCdma strength = CellSignalStrengthCdma.class.newInstance(); CellInfoCdma info = CellInfoCdma.class.newInstance(); CellInfoCdma.class.getMethod("setCellIdentity", CellIdentityCdma.class).invoke(info, identity); CellInfoCdma.class.getMethod("setCellSignalStrength", CellSignalStrengthCdma.class).invoke(info, strength); return info; } } catch (Exception ignored) { } return null; } @SuppressWarnings("unchecked") @SuppressLint({"DiscouragedPrivateApi", "deprecation"}) private synchronized boolean loadCells(List cellInfo) { int oldHash = cells.hashCode(); cells.clear(); currentDataUsed = false; try { if (cellInfo != null) { fixEmptyMnc(cellInfo); fixShortMncBug(cellInfo); for (CellInfo info : cellInfo) { Cell cell = parseCellInfo(info); if (cell == null) continue; cells.add(cell); } } Method getNeighboringCellInfo = TelephonyManager.class.getDeclaredMethod("getNeighboringCellInfo"); List neighboringCellInfo = (List) getNeighboringCellInfo.invoke(telephonyManager); if (neighboringCellInfo != null) { for (android.telephony.NeighboringCellInfo info : neighboringCellInfo) { if (!hasCid(info.getCid())) { Cell cell = parseCellInfo(info); if (cell == null) continue; cells.add(cell); } } } } catch (Exception ignored) { } if (state == State.DISABLING) state = State.DISABLED; switch (state) { default: case DISABLED: return false; case SCANNING: state = State.WAITING; return cells.hashCode() != oldHash; } } public synchronized Set getCells() { currentDataUsed = true; return new HashSet<>(cells); } /** * Call this in {@link org.microg.nlp.api.LocationBackendService#onOpen()}. */ @Override public synchronized void onOpen() { super.onOpen(); if (phoneStateListener == null) { Handler mainHandler = new Handler(context.getMainLooper()); mainHandler.post(() -> { phoneStateListener = new PhoneStateListener() { @Override public void onCellInfoChanged(List cellInfo) { if (cellInfo != null && !cellInfo.isEmpty()) { onCellsChanged(cellInfo); } else if (supportsCellInfoChanged) { supportsCellInfoChanged = false; onSignalStrengthsChanged(null); } } @Override public void onSignalStrengthsChanged(SignalStrength signalStrength) { if (!supportsCellInfoChanged) { doScan(); } } }; registerPhoneStateListener(); }); } else { registerPhoneStateListener(); } } private synchronized void doScan() { if (lastScan + MIN_UPDATE_INTERVAL > System.currentTimeMillis()) return; if (Build.VERSION.SDK_INT >= 29) { telephonyManager.requestCellInfoUpdate(command -> { Handler mainHandler = new Handler(context.getMainLooper()); mainHandler.post(command); }, new TelephonyManager.CellInfoCallback() { @Override public void onCellInfo(List cellInfo) { handleAllCellInfo(cellInfo); } }); } else { handleAllCellInfo(telephonyManager.getAllCellInfo()); } } private void handleAllCellInfo(List allCellInfo) { if ((allCellInfo == null || allCellInfo.isEmpty()) && telephonyManager.getNetworkType() > 0) { allCellInfo = new ArrayList<>(); CellLocation cellLocation = telephonyManager.getCellLocation(); CellInfo cellInfo = fromCellLocation(cellLocation); if (cellInfo != null) allCellInfo.add(cellInfo); } onCellsChanged(allCellInfo); } private synchronized void registerPhoneStateListener() { try { telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CELL_INFO | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS); } catch (Exception e) { // Can't listen phoneStateListener = null; } } /** * Call this in {@link org.microg.nlp.api.LocationBackendService#onClose()}. */ @Override public synchronized void onClose() { super.onClose(); if (phoneStateListener != null) telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); } @Override public synchronized void onUpdate() { if (!currentDataUsed) { listener.onCellsChanged(getCells()); } else { state = State.SCANNING; if (lastScan + FALLBACK_UPDATE_INTERVAL < System.currentTimeMillis()) { doScan(); } } } @Override public String[] getRequiredPermissions() { return new String[]{READ_PHONE_STATE, ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION}; } public interface Listener { void onCellsChanged(Set cells); } public static class Cell { private CellType type; private int mcc; private int mnc; private int lac; private long cid; private int psc; private int signal; public Cell(CellType type, int mcc, int mnc, int lac, long cid, int psc, int signal) { if (type == null) throw new IllegalArgumentException("Each cell has an type!"); this.type = type; boolean cdma = type == CellType.CDMA; if (mcc < 0 || mcc > 999) throw new IllegalArgumentException("Invalid MCC: " + mcc); this.mcc = mcc; if (cdma ? (mnc < 1 || mnc > 32767) : (mnc < 0 || mnc > 999)) throw new IllegalArgumentException("Invalid MNC: " + mnc); this.mnc = mnc; if (lac < 1 || lac > (cdma ? 65534 : 65533)) throw new IllegalArgumentException("Invalid LAC: " + lac); this.lac = lac; if (cid < 0) throw new IllegalArgumentException("Invalid CID: " + cid); this.cid = cid; this.psc = psc; this.signal = signal; } /** * @return RSCP for UMTS, RSRP for LTE, RSSI for GSM and CDMA */ public int getSignal() { return signal; } public CellType getType() { return type; } public int getMcc() { return mcc; } public int getMnc() { return mnc; } public int getLac() { return lac; } public long getCid() { return cid; } public int getPsc() { return psc; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Cell cell = (Cell) o; if (cid != cell.cid) return false; if (lac != cell.lac) return false; if (mcc != cell.mcc) return false; if (mnc != cell.mnc) return false; if (psc != cell.psc) return false; if (signal != cell.signal) return false; if (type != cell.type) return false; return true; } @Override public int hashCode() { int result = type.hashCode(); result = 31 * result + mcc; result = 31 * result + mnc; result = 31 * result + lac; result = 31 * result + (int) (cid ^ (cid >>> 32)); result = 31 * result + psc; result = 31 * result + signal; return result; } @Override public String toString() { return "Cell{" + "type=" + type + ", mcc=" + mcc + ", mnc=" + mnc + ", lac=" + lac + ", cid=" + cid + (psc != -1 ? (", psc=" + psc) : "") + ", signal=" + signal + '}'; } public enum CellType {GSM, UMTS, LTE, CDMA} } }