This commit is contained in:
Marvin W 2019-11-26 18:09:53 +01:00
parent 18b16dcc1f
commit 4b5b3251ae
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
20 changed files with 1861 additions and 0 deletions

60
api/build.gradle Normal file
View File

@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
android {
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
pom {
name = 'UnifiedNlp API'
description = 'API interfaces and helpers to create backends for UnifiedNlp'
url = 'https://github.com/microg/UnifiedNlp'
licenses {
license {
name = 'The Apache Software License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id = 'microg'
name = 'microG Team'
}
developer {
id = 'mar-v-in'
name = 'Marvin W.'
}
}
scm {
url = 'https://github.com/microg/UnifiedNlp'
connection = 'scm:git:https://github.com/microg/UnifiedNlp.git'
developerConnection = 'scm:git:ssh://github.com/microg/UnifiedNlp.git'
}
}
from components.release
}
}
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2013, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest package="org.microg.nlp.api"
xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<meta-data
android:name="org.microg.nlp.API_VERSION"
android:value="@string/nlp_api_version"/>
<activity
android:name=".MPermissionHelperActivity"
android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
</application>
</manifest>

View File

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.content.Intent;
import android.location.Location;
import android.location.Address;
interface GeocoderBackend {
void open();
List<Address> getFromLocation(double latitude, double longitude, int maxResults, String locale);
List<Address> getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude,
double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude,
String locale);
void close();
Intent getInitIntent();
Intent getSettingsIntent();
Intent getAboutIntent();
List<Address> getFromLocationWithOptions(double latitude, double longitude, int maxResults,
String locale, in Bundle options);
List<Address> getFromLocationNameWithOptions(String locationName, int maxResults,
double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude,
double upperRightLongitude, String locale, in Bundle options);
}

View File

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import org.microg.nlp.api.LocationCallback;
import android.content.Intent;
import android.location.Location;
interface LocationBackend {
void open(LocationCallback callback);
Location update();
void close();
Intent getInitIntent();
Intent getSettingsIntent();
Intent getAboutIntent();
Location updateWithOptions(in Bundle options);
}

View File

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.location.Location;
interface LocationCallback {
void report(in Location location);
}

View File

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: 2008, The Android Open Source Project
* SPDX-License-Identifier: Apache-2.0
*/
package android.location;
parcelable Address;

View File

@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.content.Context;
import android.util.Log;
@SuppressWarnings("WeakerAccess")
public class AbstractBackendHelper {
public static final String TAG = "BackendHelper";
protected final Context context;
protected State state = State.DISABLED;
protected boolean currentDataUsed = true;
public AbstractBackendHelper(Context context) {
if (context == null)
throw new IllegalArgumentException("context must not be null");
this.context = context;
}
/**
* Call this in {@link org.microg.nlp.api.LocationBackendService#onOpen()}.
*/
public synchronized void onOpen() {
if (state == State.WAITING || state == State.SCANNING) {
Log.w(TAG, "Do not call onOpen if not closed before");
}
currentDataUsed = true;
state = State.WAITING;
}
/**
* Call this in {@link org.microg.nlp.api.LocationBackendService#onClose()}.
*/
public synchronized void onClose() {
if (state == State.DISABLED || state == State.DISABLING) {
Log.w(TAG, "Do not call onClose if not opened before");
return;
}
if (state == State.WAITING) {
state = State.DISABLED;
} else {
state = State.DISABLING;
}
}
/**
* Call this in {@link org.microg.nlp.api.LocationBackendService#update()}.
*/
public synchronized void onUpdate() {
}
public String[] getRequiredPermissions() {
return new String[0];
}
protected enum State {DISABLED, WAITING, SCANNING, DISABLING}
}

View File

@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class AbstractBackendService extends Service {
public static final String TAG = "BackendService";
@Override
public IBinder onBind(Intent intent) {
return getBackend();
}
/**
* Called after a connection was setup
*/
protected void onOpen() {
}
/**
* Called before connection closure
*/
protected void onClose() {
}
protected Intent getInitIntent() {
return null;
}
@SuppressWarnings("SameReturnValue")
protected Intent getSettingsIntent() {
return null;
}
@SuppressWarnings("SameReturnValue")
protected Intent getAboutIntent() {
return null;
}
@Override
public boolean onUnbind(Intent intent) {
try {
disconnect();
} catch (Exception e) {
Log.w(TAG, e);
}
return super.onUnbind(intent);
}
public abstract void disconnect();
protected abstract IBinder getBackend();
protected String getServiceApiVersion() {
return Utils.getServiceApiVersion(this);
}
protected String getSelfApiVersion() {
return Utils.getSelfApiVersion(this);
}
}

View File

@ -0,0 +1,155 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import java.util.HashSet;
import java.util.Set;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.BLUETOOTH;
import static android.Manifest.permission.BLUETOOTH_ADMIN;
/**
* Utility class to support backend using Device for geolocation.
*/
@SuppressWarnings({"MissingPermission", "WeakerAccess", "unused"})
public class BluetoothBackendHelper extends AbstractBackendHelper {
private final static IntentFilter bluetoothBroadcastFilter =
new IntentFilter();
private final Listener listener;
private final BluetoothAdapter bluetoothAdapter;
private final Set<Device> devices = new HashSet<>();
private final BroadcastReceiver bluetoothBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
devices.clear();
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
onBluetoothChanged();
} else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null) {
int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
Device deviceDiscovered = new Device(device.getAddress(), device.getName(), rssi);
devices.add(deviceDiscovered);
}
}
}
};
public BluetoothBackendHelper(Context context, Listener listener){
super(context);
if (listener == null)
throw new IllegalArgumentException("listener must not be null");
this.listener = listener;
this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
bluetoothBroadcastFilter.addAction(BluetoothDevice.ACTION_FOUND);
bluetoothBroadcastFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
bluetoothBroadcastFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
}
public synchronized void onOpen() {
super.onOpen();
bluetoothBroadcastFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
bluetoothBroadcastFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
bluetoothBroadcastFilter.addAction(BluetoothDevice.ACTION_FOUND);
context.registerReceiver(bluetoothBroadcastReceiver, bluetoothBroadcastFilter);
}
public synchronized void onClose() {
super.onClose();
context.unregisterReceiver(bluetoothBroadcastReceiver);
}
public synchronized void onUpdate() {
if (!currentDataUsed) {
listener.onDevicesChanged(getDevices());
} else {
scanBluetooth();
}
}
@Override
public String[] getRequiredPermissions() {
return new String[]{BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_COARSE_LOCATION};
}
private void onBluetoothChanged() {
if (loadBluetooths()) {
listener.onDevicesChanged(getDevices());
}
}
private synchronized boolean scanBluetooth() {
if (state == State.DISABLED)
return false;
if (bluetoothAdapter.isEnabled()) {
state = State.SCANNING;
bluetoothAdapter.startDiscovery();
return true;
}
return false;
}
private synchronized boolean loadBluetooths() {
currentDataUsed = false;
if (state == State.DISABLING)
state = State.DISABLED;
switch (state) {
default:
case DISABLED:
return false;
case SCANNING:
state = State.WAITING;
return true;
}
}
public synchronized Set<Device> getDevices() {
currentDataUsed = true;
return new HashSet<>(devices);
}
public interface Listener {
void onDevicesChanged(Set<Device> device);
}
public static class Device {
private final String bssid;
private final String name;
private final int rssi;
public String getBssid() { return bssid; }
public String getName() {return name; }
public int getRssi() { return rssi; }
public Device(String bssid, String name, int rssi) {
this.bssid = Utils.wellFormedMac(bssid);
this.name = name;
this.rssi = rssi;
}
@Override
public String toString() {
return "Device{" +
"name=" + name +
", bssid=" + bssid +
", rssi=" + rssi +
"}";
}
}
}

View File

@ -0,0 +1,558 @@
/*
* 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 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 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) {
}
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
*/
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) {
fallbackScan();
}
}
};
registerPhoneStateListener();
});
} else {
registerPhoneStateListener();
}
}
private synchronized void fallbackScan() {
if (lastScan + MIN_UPDATE_INTERVAL > System.currentTimeMillis()) return;
List<CellInfo> allCellInfo = telephonyManager.getAllCellInfo();
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()) {
fallbackScan();
}
}
}
@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}
}
}

View File

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
public class Constants {
public static final String ACTION_LOCATION_BACKEND = "org.microg.nlp.LOCATION_BACKEND";
public static final String ACTION_GEOCODER_BACKEND = "org.microg.nlp.GEOCODER_BACKEND";
public static final String ACTION_RELOAD_SETTINGS = "org.microg.nlp.RELOAD_SETTINGS";
public static final String ACTION_FORCE_LOCATION = "org.microg.nlp.FORCE_LOCATION";
public static final String PERMISSION_FORCE_LOCATION = "org.microg.permission.FORCE_COARSE_LOCATION";
public static final String INTENT_EXTRA_LOCATION = "location";
public static final String LOCATION_EXTRA_BACKEND_PROVIDER = "SERVICE_BACKEND_PROVIDER";
public static final String LOCATION_EXTRA_BACKEND_COMPONENT = "SERVICE_BACKEND_COMPONENT";
public static final String LOCATION_EXTRA_OTHER_BACKENDS = "OTHER_BACKEND_RESULTS";
public static final String METADATA_BACKEND_SETTINGS_ACTIVITY = "org.microg.nlp.BACKEND_SETTINGS_ACTIVITY";
public static final String METADATA_BACKEND_ABOUT_ACTIVITY = "org.microg.nlp.BACKEND_ABOUT_ACTIVITY";
public static final String METADATA_BACKEND_INIT_ACTIVITY = "org.microg.nlp.BACKEND_INIT_ACTIVITY";
public static final String METADATA_BACKEND_SUMMARY = "org.microg.nlp.BACKEND_SUMMARY";
public static final String METADATA_API_VERSION = "org.microg.nlp.API_VERSION";
public static final String API_VERSION = "3";
}

View File

@ -0,0 +1,109 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.content.Intent;
import android.location.Address;
import android.os.Bundle;
import android.os.IBinder;
import java.util.List;
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class GeocoderBackendService extends AbstractBackendService {
private final Backend backend = new Backend();
private boolean connected = false;
@Override
protected IBinder getBackend() {
return backend;
}
@Override
public void disconnect() {
if (connected) {
onClose();
connected = false;
}
}
/**
* @param locale The locale, formatted as a String with underscore (eg. en_US) the resulting
* address should be localized in
* @see android.location.Geocoder#getFromLocation(double, double, int)
*/
protected abstract List<Address> getFromLocation(double latitude, double longitude, int maxResults, String locale);
protected List<Address> getFromLocation(double latitude, double longitude, int maxResults, String locale, Bundle options) {
return getFromLocation(latitude, longitude, maxResults, locale);
}
/**
* @param locale The locale, formatted as a String with underscore (eg. en_US) the resulting
* address should be localized in
* @see android.location.Geocoder#getFromLocationName(String, int, double, double, double, double)
*/
protected abstract List<Address> getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, String locale);
protected List<Address> getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, String locale, Bundle options) {
return getFromLocationName(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale);
}
private class Backend extends GeocoderBackend.Stub {
@Override
public void open() {
onOpen();
connected = true;
}
@Override
public List<Address> getFromLocation(double latitude, double longitude, int maxResults, String locale) {
return GeocoderBackendService.this
.getFromLocation(latitude, longitude, maxResults, locale);
}
@Override
public List<Address> getFromLocationWithOptions(double latitude, double longitude, int maxResults, String locale, Bundle options) {
return GeocoderBackendService.this.getFromLocation(latitude, longitude, maxResults, locale, options);
}
@Override
public List<Address> getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, String locale) {
return GeocoderBackendService.this
.getFromLocationName(locationName, maxResults, lowerLeftLatitude,
lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale);
}
@Override
public List<Address> getFromLocationNameWithOptions(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, String locale, Bundle options) {
return GeocoderBackendService.this.getFromLocationName(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, locale, options);
}
@Override
public void close() {
disconnect();
}
@Override
public Intent getInitIntent() {
return GeocoderBackendService.this.getInitIntent();
}
@Override
public Intent getSettingsIntent() {
return GeocoderBackendService.this.getSettingsIntent();
}
@Override
public Intent getAboutIntent() {
return GeocoderBackendService.this.getAboutIntent();
}
}
}

View File

@ -0,0 +1,95 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Build;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
@SuppressWarnings("unused")
public abstract class HelperLocationBackendService extends LocationBackendService {
private boolean opened;
private final Set<AbstractBackendHelper> helpers = new HashSet<>();
public synchronized void addHelper(AbstractBackendHelper helper) {
helpers.add(helper);
if (opened) {
helper.onOpen();
}
}
public synchronized void removeHelpers() {
if (opened) {
for (AbstractBackendHelper helper : helpers) {
helper.onClose();
}
}
helpers.clear();
}
@Override
protected synchronized void onOpen() {
for (AbstractBackendHelper helper : helpers) {
helper.onOpen();
}
opened = true;
}
@Override
protected synchronized void onClose() {
for (AbstractBackendHelper helper : helpers) {
helper.onClose();
}
opened = false;
}
@Override
protected synchronized Location update() {
for (AbstractBackendHelper helper : helpers) {
helper.onUpdate();
}
return null;
}
@Override
protected Intent getInitIntent() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Consider permissions
List<String> perms = new LinkedList<>();
for (AbstractBackendHelper helper : helpers) {
perms.addAll(Arrays.asList(helper.getRequiredPermissions()));
}
// Request background location permission if needed as we are likely to run in background
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q && (perms.contains(ACCESS_COARSE_LOCATION) || perms.contains(ACCESS_FINE_LOCATION))) {
perms.add(ACCESS_BACKGROUND_LOCATION);
}
for (Iterator<String> iterator = perms.iterator(); iterator.hasNext(); ) {
String perm = iterator.next();
if (checkSelfPermission(perm) == PackageManager.PERMISSION_GRANTED) {
iterator.remove();
}
}
if (perms.isEmpty()) return null;
Intent intent = new Intent(this, MPermissionHelperActivity.class);
intent.putExtra(MPermissionHelperActivity.EXTRA_PERMISSIONS, perms.toArray(new String[0]));
return intent;
}
return super.getInitIntent();
}
}

View File

@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class LocationBackendService extends AbstractBackendService {
private LocationCallback callback;
private Location waiting;
/**
* Called, whenever an app requires a location update. This can be a single or a repeated request.
* <p/>
* You may return null if your backend has no newer location available then the last one.
* Do not send the same {@link android.location.Location} twice, if it's not based on updated/refreshed data.
* <p/>
* You can completely ignore this method (means returning null) if you use {@link #report(android.location.Location)}.
*
* @return a new {@link android.location.Location} instance or null if not available.
*/
@SuppressWarnings("SameReturnValue")
protected Location update() {
return null;
}
protected Location update(Bundle options) {
return update();
}
/**
* Directly report a {@link android.location.Location} to the requesting apps. Use this if your updates are based
* on environment changes (eg. cell id change).
*
* @param location the new {@link android.location.Location} instance to be send
*/
public void report(Location location) {
if (callback != null) {
try {
callback.report(location);
} catch (android.os.DeadObjectException e) {
waiting = location;
callback = null;
} catch (RemoteException e) {
waiting = location;
}
} else {
waiting = location;
}
}
/**
* @return true if we're an actively connected backend, false if not
*/
public boolean isConnected() {
return callback != null;
}
@Override
protected IBinder getBackend() {
return new Backend();
}
@Override
public void disconnect() {
if (callback != null) {
onClose();
callback = null;
}
}
private class Backend extends LocationBackend.Stub {
@Override
public void open(LocationCallback callback) throws RemoteException {
LocationBackendService.this.callback = callback;
if (waiting != null) {
callback.report(waiting);
waiting = null;
}
onOpen();
}
@Override
public Location update() {
return LocationBackendService.this.update();
}
@Override
public Location updateWithOptions(Bundle options) {
return LocationBackendService.this.update(options);
}
@Override
public void close() {
disconnect();
}
@Override
public Intent getInitIntent() {
return LocationBackendService.this.getInitIntent();
}
@Override
public Intent getSettingsIntent() {
return LocationBackendService.this.getSettingsIntent();
}
@Override
public Intent getAboutIntent() {
return LocationBackendService.this.getAboutIntent();
}
}
}

View File

@ -0,0 +1,111 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.location.Location;
import android.os.Bundle;
import java.util.Collection;
@SuppressWarnings({"WeakerAccess", "unused"})
public final class LocationHelper {
public static final String EXTRA_AVERAGED_OF = "AVERAGED_OF";
public static final String EXTRA_TOTAL_WEIGHT = "org.microg.nlp.TOTAL_WEIGHT";
public static final String EXTRA_TOTAL_ALTITUDE_WEIGHT = "org.microg.nlp.TOTAL_ALTITUDE_WEIGHT";
public static final String EXTRA_WEIGHT = "org.microg.nlp.WEIGHT";
private LocationHelper() {
}
public static Location create(String source) {
Location l = new Location(source);
l.setTime(System.currentTimeMillis());
return l;
}
public static Location create(String source, double latitude, double longitude, float accuracy) {
Location location = create(source);
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAccuracy(accuracy);
return location;
}
public static Location create(String source, double latitude, double longitude, float altitude, Bundle extras) {
Location location = create(source, latitude, longitude, altitude);
location.setExtras(extras);
return location;
}
public static Location create(String source, double latitude, double longitude, double altitude, float accuracy) {
Location location = create(source, latitude, longitude, accuracy);
location.setAltitude(altitude);
return location;
}
public static Location create(String source, double latitude, double longitude, double altitude, float accuracy, Bundle extras) {
Location location = create(source, latitude, longitude, altitude, accuracy);
location.setExtras(extras);
return location;
}
public static Location create(String source, long time) {
Location location = create(source);
location.setTime(time);
return location;
}
public static Location create(String source, long time, Bundle extras) {
Location location = create(source, time);
location.setExtras(extras);
return location;
}
public static Location average(String source, Collection<Location> locations) {
return weightedAverage(source, locations, LocationBalance.BALANCED, new Bundle());
}
public static Location weightedAverage(String source, Collection<Location> locations, LocationBalance balance, Bundle extras) {
if (locations == null || locations.isEmpty()) {
return null;
}
double total = 0;
double lat = 0;
double lon = 0;
float acc = 0;
double altTotal = 0;
double alt = 0;
for (Location value : locations) {
if (value != null) {
double weight = balance.getWeight(value);
total += weight;
lat += value.getLatitude() * weight;
lon += value.getLongitude() * weight;
acc += value.getAccuracy() * weight;
if (value.hasAltitude()) {
alt += value.getAltitude();
altTotal += weight;
}
}
}
if (extras == null) extras = new Bundle();
extras.putInt(EXTRA_AVERAGED_OF, locations.size());
extras.putDouble(EXTRA_TOTAL_WEIGHT, total);
if (altTotal > 0) {
extras.putDouble(EXTRA_TOTAL_ALTITUDE_WEIGHT, altTotal);
return create(source, lat / total, lon / total, alt / altTotal, (float) (acc / total), extras);
} else {
return create(source, lat / total, lon / total, (float) (acc / total), extras);
}
}
public interface LocationBalance {
LocationBalance BALANCED = location -> 1;
LocationBalance FROM_EXTRA = location -> location.getExtras() == null ? 1 : location.getExtras().getDouble(EXTRA_WEIGHT, 1);
double getWeight(Location location);
}
}

View File

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
@TargetApi(Build.VERSION_CODES.M)
public class MPermissionHelperActivity extends Activity {
public static final String EXTRA_PERMISSIONS = "org.microg.nlp.api.mperms";
private static final int REQUEST_CODE_PERMS = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String[] mperms = getIntent().getStringArrayExtra(EXTRA_PERMISSIONS);
if (mperms == null || mperms.length == 0) {
setResult(RESULT_OK);
finish();
} else {
requestPermissions(mperms, REQUEST_CODE_PERMS);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
boolean ok = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) ok = false;
}
setResult(ok ? RESULT_OK : RESULT_CANCELED);
finish();
}
}

View File

@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.util.Log;
@SuppressWarnings("WeakerAccess")
public class Utils {
/**
* Bring a mac address to the form 01:23:45:AB:CD:EF
*
* @param mac address to be well-formed
* @return well-formed mac address
*/
public static String wellFormedMac(String mac) {
int HEX_RADIX = 16;
int[] bytes = new int[6];
String[] splitAtColon = mac.split(":");
if (splitAtColon.length == 6) {
for (int i = 0; i < 6; ++i) {
bytes[i] = Integer.parseInt(splitAtColon[i], HEX_RADIX);
}
} else {
String[] splitAtLine = mac.split("-");
if (splitAtLine.length == 6) {
for (int i = 0; i < 6; ++i) {
bytes[i] = Integer.parseInt(splitAtLine[i], HEX_RADIX);
}
} else if (mac.length() == 12) {
for (int i = 0; i < 6; ++i) {
bytes[i] = Integer.parseInt(mac.substring(i * 2, (i + 1) * 2), HEX_RADIX);
}
} else if (mac.length() == 17) {
for (int i = 0; i < 6; ++i) {
bytes[i] = Integer.parseInt(mac.substring(i * 3, (i * 3) + 2), HEX_RADIX);
}
} else {
throw new IllegalArgumentException("Can't read this string as mac address");
}
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 6; ++i) {
String hex = Integer.toHexString(bytes[i]);
if (hex.length() == 1) {
hex = "0" + hex;
}
if (sb.length() != 0)
sb.append(":");
sb.append(hex);
}
return sb.toString();
}
public static String getPackageApiVersion(Context context, String packageName) {
PackageManager pm = context.getPackageManager();
ApplicationInfo applicationInfo;
try {
applicationInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
return applicationInfo.metaData == null ? null
: applicationInfo.metaData.getString(Constants.METADATA_API_VERSION);
}
public static String getServiceApiVersion(Context context) {
String apiVersion = getPackageApiVersion(context, "com.google.android.gms");
if (apiVersion == null)
apiVersion = getPackageApiVersion(context, "com.google.android.location");
if (apiVersion == null) apiVersion = getPackageApiVersion(context, "org.microg.nlp");
return apiVersion;
}
public static String getSelfApiVersion(Context context) {
String apiVersion = getPackageApiVersion(context, context.getPackageName());
if (!Constants.API_VERSION.equals(apiVersion)) {
Log.w("VersionUtil", "You did not specify the currently used api version in your manifest.\n" +
"When using gradle + aar, this should be done automatically, if not, add the\n" +
"following to your <application> tag\n" +
"<meta-data android:name=\"" + Constants.METADATA_API_VERSION +
"\" android:value=\"" + Constants.API_VERSION + "\" />");
apiVersion = Constants.API_VERSION;
}
return apiVersion;
}
}

View File

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.content.Context;
@SuppressWarnings("unused")
@Deprecated
public class VersionUtils {
public static String getPackageApiVersion(Context context, String packageName) {
return Utils.getPackageApiVersion(context, packageName);
}
public static String getServiceApiVersion(Context context) {
return Utils.getServiceApiVersion(context);
}
public static String getSelfApiVersion(Context context) {
return Utils.getSelfApiVersion(context);
}
}

View File

@ -0,0 +1,241 @@
/*
* SPDX-FileCopyrightText: 2013, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.nlp.api;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.os.Build;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.ACCESS_WIFI_STATE;
import static android.Manifest.permission.CHANGE_WIFI_STATE;
/**
* Utility class to support backends that use Wi-Fis for geolocation.
*/
@SuppressWarnings({"MissingPermission", "WeakerAccess", "unused"})
public class WiFiBackendHelper extends AbstractBackendHelper {
private final static IntentFilter wifiBroadcastFilter =
new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
private final Listener listener;
private final WifiManager wifiManager;
private final Set<WiFi> wiFis = new HashSet<>();
private final BroadcastReceiver wifiBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onWiFisChanged();
}
};
private boolean ignoreNomap = true;
/**
* Create a new instance of {@link WiFiBackendHelper}. Call this in
* {@link LocationBackendService#onCreate()}.
*
* @throws IllegalArgumentException if either context or listener is null.
*/
public WiFiBackendHelper(Context context, Listener listener) {
super(context);
if (listener == null)
throw new IllegalArgumentException("listener must not be null");
this.listener = listener;
this.wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
}
/**
* Sets whether to ignore the "_nomap" flag on Wi-Fi SSIDs or not.
* <p/>
* Usually, Wi-Fis whose SSID end with "_nomap" are ignored for geolocation. This behaviour can
* be suppressed by {@code setIgnoreNomap(false)}.
* <p/>
* Default is {@code true}.
*/
public void setIgnoreNomap(boolean ignoreNomap) {
this.ignoreNomap = ignoreNomap;
}
/**
* Call this in {@link LocationBackendService#onOpen()}.
*/
public synchronized void onOpen() {
super.onOpen();
context.registerReceiver(wifiBroadcastReceiver, wifiBroadcastFilter);
}
/**
* Call this in {@link LocationBackendService#onClose()}.
*/
public synchronized void onClose() {
super.onClose();
context.unregisterReceiver(wifiBroadcastReceiver);
}
/**
* Call this in {@link LocationBackendService#update()}.
*/
public synchronized void onUpdate() {
if (!currentDataUsed) {
listener.onWiFisChanged(getWiFis());
} else {
scanWiFis();
}
}
@Override
public String[] getRequiredPermissions() {
return new String[]{CHANGE_WIFI_STATE, ACCESS_WIFI_STATE, ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION};
}
private void onWiFisChanged() {
if (loadWiFis()) {
listener.onWiFisChanged(getWiFis());
}
}
@SuppressWarnings("deprecation")
private synchronized boolean scanWiFis() {
if (state == State.DISABLED)
return false;
if (wifiManager.isWifiEnabled() || isScanAlwaysAvailable()) {
state = State.SCANNING;
wifiManager.startScan();
return true;
}
return false;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@SuppressWarnings("deprecation")
private boolean isScanAlwaysAvailable() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
&& wifiManager.isScanAlwaysAvailable();
}
private synchronized boolean loadWiFis() {
int oldHash = wiFis.hashCode();
wiFis.clear();
currentDataUsed = false;
List<ScanResult> scanResults = wifiManager.getScanResults();
for (ScanResult scanResult : scanResults) {
if (ignoreNomap && scanResult.SSID.toLowerCase(Locale.US).endsWith("_nomap")) continue;
wiFis.add(new WiFi(scanResult.BSSID, scanResult.level, frequencyToChannel(scanResult.frequency), scanResult.frequency));
}
if (state == State.DISABLING)
state = State.DISABLED;
switch (state) {
default:
case DISABLED:
return false;
case SCANNING:
state = State.WAITING;
return wiFis.hashCode() != oldHash;
}
}
@SuppressWarnings("MagicNumber")
private static int frequencyToChannel(int freq) {
if (freq >= 2412 && freq <= 2484) {
return (freq - 2412) / 5 + 1;
} else if (freq >= 5170 && freq <= 5825) {
return (freq - 5170) / 5 + 34;
} else {
return -1;
}
}
/**
* @return the latest scan result.
*/
public synchronized Set<WiFi> getWiFis() {
currentDataUsed = true;
return new HashSet<>(wiFis);
}
/**
* Interface to listen for Wi-Fi scan results.
*/
public interface Listener {
/**
* Called when a new set of Wi-Fi's is discovered.
*/
void onWiFisChanged(Set<WiFi> wiFis);
}
/**
* Represents a generic Wi-Fi scan result.
* <p/>
* This does contain the BSSID (mac address) and the RSSI (in dBm) of a Wi-Fi.
* Additional data is not provided, but also not usable for geolocation.
*/
public static class WiFi {
private final String bssid;
private final int rssi;
private final int channel;
private final int frequency;
public String getBssid() {
return bssid;
}
public int getRssi() {
return rssi;
}
public int getChannel() {
return channel;
}
public int getFrequency() {
return frequency;
}
public WiFi(String bssid, int rssi) {
this(bssid, rssi, -1, -1);
}
public WiFi(String bssid, int rssi, Integer channel, Integer frequency) {
this.bssid = Utils.wellFormedMac(bssid);
this.rssi = rssi;
this.channel = channel;
this.frequency = frequency;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WiFi wiFi = (WiFi) o;
if (rssi != wiFi.rssi) return false;
if (channel != wiFi.channel) return false;
if (frequency != wiFi.frequency) return false;
return bssid != null ? bssid.equals(wiFi.bssid) : wiFi.bssid == null;
}
@Override
public int hashCode() {
int result = bssid != null ? bssid.hashCode() : 0;
result = 31 * result + rssi;
result = 31 * result + channel;
result = 31 * result + frequency;
return result;
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2013, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<resources>
<string name="nlp_api_version">3</string>
</resources>