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