diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 0000000..31a1b70
--- /dev/null
+++ b/api/build.gradle
@@ -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
+ }
+ }
+ }
+}
diff --git a/api/src/main/AndroidManifest.xml b/api/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3328631
--- /dev/null
+++ b/api/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/api/src/main/aidl/org/microg/nlp/api/GeocoderBackend.aidl b/api/src/main/aidl/org/microg/nlp/api/GeocoderBackend.aidl
new file mode 100644
index 0000000..fa4536e
--- /dev/null
+++ b/api/src/main/aidl/org/microg/nlp/api/GeocoderBackend.aidl
@@ -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
getFromLocation(double latitude, double longitude, int maxResults, String locale);
+ List getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude,
+ double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude,
+ String locale);
+ void close();
+ Intent getInitIntent();
+ Intent getSettingsIntent();
+ Intent getAboutIntent();
+ List getFromLocationWithOptions(double latitude, double longitude, int maxResults,
+ String locale, in Bundle options);
+ List getFromLocationNameWithOptions(String locationName, int maxResults,
+ double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude,
+ double upperRightLongitude, String locale, in Bundle options);
+}
diff --git a/api/src/main/aidl/org/microg/nlp/api/LocationBackend.aidl b/api/src/main/aidl/org/microg/nlp/api/LocationBackend.aidl
new file mode 100644
index 0000000..3f126ab
--- /dev/null
+++ b/api/src/main/aidl/org/microg/nlp/api/LocationBackend.aidl
@@ -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);
+}
diff --git a/api/src/main/aidl/org/microg/nlp/api/LocationCallback.aidl b/api/src/main/aidl/org/microg/nlp/api/LocationCallback.aidl
new file mode 100644
index 0000000..a425242
--- /dev/null
+++ b/api/src/main/aidl/org/microg/nlp/api/LocationCallback.aidl
@@ -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);
+}
diff --git a/api/src/main/java/android/location/Address.aidl b/api/src/main/java/android/location/Address.aidl
new file mode 100644
index 0000000..e63c8e2
--- /dev/null
+++ b/api/src/main/java/android/location/Address.aidl
@@ -0,0 +1,7 @@
+/*
+ * SPDX-FileCopyrightText: 2008, The Android Open Source Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package android.location;
+parcelable Address;
diff --git a/api/src/main/java/org/microg/nlp/api/AbstractBackendHelper.java b/api/src/main/java/org/microg/nlp/api/AbstractBackendHelper.java
new file mode 100644
index 0000000..bfae4c4
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/AbstractBackendHelper.java
@@ -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}
+}
diff --git a/api/src/main/java/org/microg/nlp/api/AbstractBackendService.java b/api/src/main/java/org/microg/nlp/api/AbstractBackendService.java
new file mode 100644
index 0000000..d893c40
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/AbstractBackendService.java
@@ -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);
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/BluetoothBackendHelper.java b/api/src/main/java/org/microg/nlp/api/BluetoothBackendHelper.java
new file mode 100644
index 0000000..843d984
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/BluetoothBackendHelper.java
@@ -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 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 getDevices() {
+ currentDataUsed = true;
+ return new HashSet<>(devices);
+ }
+
+ public interface Listener {
+ void onDevicesChanged(Set 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 +
+ "}";
+ }
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/CellBackendHelper.java b/api/src/main/java/org/microg/nlp/api/CellBackendHelper.java
new file mode 100644
index 0000000..468ffa8
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/CellBackendHelper.java
@@ -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.
+ *
+ * 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) {
+ }
+ 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
+ */
+ 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) {
+ fallbackScan();
+ }
+ }
+ };
+ registerPhoneStateListener();
+ });
+ } else {
+ registerPhoneStateListener();
+ }
+ }
+
+ private synchronized void fallbackScan() {
+ if (lastScan + MIN_UPDATE_INTERVAL > System.currentTimeMillis()) return;
+ List 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 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}
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/Constants.java b/api/src/main/java/org/microg/nlp/api/Constants.java
new file mode 100644
index 0000000..1c352d8
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/Constants.java
@@ -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";
+}
diff --git a/api/src/main/java/org/microg/nlp/api/GeocoderBackendService.java b/api/src/main/java/org/microg/nlp/api/GeocoderBackendService.java
new file mode 100644
index 0000000..1cea334
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/GeocoderBackendService.java
@@ -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 getFromLocation(double latitude, double longitude, int maxResults, String locale);
+
+ protected List 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 getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, String locale);
+
+
+ protected List 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 getFromLocation(double latitude, double longitude, int maxResults, String locale) {
+ return GeocoderBackendService.this
+ .getFromLocation(latitude, longitude, maxResults, locale);
+ }
+
+ @Override
+ public List getFromLocationWithOptions(double latitude, double longitude, int maxResults, String locale, Bundle options) {
+ return GeocoderBackendService.this.getFromLocation(latitude, longitude, maxResults, locale, options);
+ }
+
+
+ @Override
+ public List 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 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();
+ }
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java b/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java
new file mode 100644
index 0000000..d775cac
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/HelperLocationBackendService.java
@@ -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 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 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 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();
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/LocationBackendService.java b/api/src/main/java/org/microg/nlp/api/LocationBackendService.java
new file mode 100644
index 0000000..83c8274
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/LocationBackendService.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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();
+ }
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/LocationHelper.java b/api/src/main/java/org/microg/nlp/api/LocationHelper.java
new file mode 100644
index 0000000..b287b55
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/LocationHelper.java
@@ -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 locations) {
+ return weightedAverage(source, locations, LocationBalance.BALANCED, new Bundle());
+ }
+
+ public static Location weightedAverage(String source, Collection 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);
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/MPermissionHelperActivity.java b/api/src/main/java/org/microg/nlp/api/MPermissionHelperActivity.java
new file mode 100644
index 0000000..d89f530
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/MPermissionHelperActivity.java
@@ -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();
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/Utils.java b/api/src/main/java/org/microg/nlp/api/Utils.java
new file mode 100644
index 0000000..8e14192
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/Utils.java
@@ -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 tag\n" +
+ "");
+ apiVersion = Constants.API_VERSION;
+ }
+ return apiVersion;
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/VersionUtils.java b/api/src/main/java/org/microg/nlp/api/VersionUtils.java
new file mode 100644
index 0000000..ec1a922
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/VersionUtils.java
@@ -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);
+ }
+}
diff --git a/api/src/main/java/org/microg/nlp/api/WiFiBackendHelper.java b/api/src/main/java/org/microg/nlp/api/WiFiBackendHelper.java
new file mode 100644
index 0000000..c336044
--- /dev/null
+++ b/api/src/main/java/org/microg/nlp/api/WiFiBackendHelper.java
@@ -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 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.
+ *
+ * Usually, Wi-Fis whose SSID end with "_nomap" are ignored for geolocation. This behaviour can
+ * be suppressed by {@code setIgnoreNomap(false)}.
+ *
+ * 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 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 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 wiFis);
+ }
+
+ /**
+ * Represents a generic Wi-Fi scan result.
+ *
+ * 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;
+ }
+ }
+}
diff --git a/api/src/main/res/values/version.xml b/api/src/main/res/values/version.xml
new file mode 100644
index 0000000..55af080
--- /dev/null
+++ b/api/src/main/res/values/version.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ 3
+
\ No newline at end of file
| | |