UnifiedNlp/api/src/main/java/org/microg/nlp/api/CellBackendHelper.java
2022-01-24 17:09:12 +01:00

579 lines
22 KiB
Java

/*
* 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.
* <p/>
* 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<Cell> 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> 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> 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> 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> 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<android.telephony.NeighboringCellInfo> neighboringCellInfo = (List<android.telephony.NeighboringCellInfo>) 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<Cell> 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> 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> cellInfo) {
handleAllCellInfo(cellInfo);
}
});
} else {
handleAllCellInfo(telephonyManager.getAllCellInfo());
}
}
private void handleAllCellInfo(List<CellInfo> 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<Cell> 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}
}
}