From 20e3efa555c83f88b1f5a1bf7090ac461943d82c Mon Sep 17 00:00:00 2001 From: Liam Meier Date: Tue, 16 Jul 2024 18:11:38 -0400 Subject: [PATCH 1/3] indoors first pass on android --- sdk/build.gradle | 2 +- sdk/src/main/java/io/radar/sdk/Radar.kt | 105 +++++--- .../main/java/io/radar/sdk/RadarApiClient.kt | 5 +- .../io/radar/sdk/RadarIndoorSurveyManager.kt | 231 ++++++++++++++++++ .../java/io/radar/sdk/RadarLocationManager.kt | 156 ++++++++---- .../java/io/radar/sdk/RadarTrackingOptions.kt | 21 +- .../io/radar/sdk/RadarVerificationManager.kt | 1 + .../java/io/radar/sdk/model/RadarEvent.kt | 2 + 8 files changed, 424 insertions(+), 99 deletions(-) create mode 100644 sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt diff --git a/sdk/build.gradle b/sdk/build.gradle index 57097dc1d..b180aaf03 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -10,7 +10,7 @@ apply plugin: "org.jetbrains.dokka" apply plugin: 'io.radar.mvnpublish' ext { - radarVersion = '3.14.0' + radarVersion = '3.16.0-beta.1' } String buildNumber = ".${System.currentTimeMillis()}" diff --git a/sdk/src/main/java/io/radar/sdk/Radar.kt b/sdk/src/main/java/io/radar/sdk/Radar.kt index a858f4451..8eaab7eb7 100644 --- a/sdk/src/main/java/io/radar/sdk/Radar.kt +++ b/sdk/src/main/java/io/radar/sdk/Radar.kt @@ -273,6 +273,13 @@ object Radar { ) } + // interface RadarIndoorSurveyCallback { + // fun onComplete( + // status: RadarStatus, + // indoorsPayload: String + // ) + // } + /** * The status types for a request. See [](https://radar.com/documentation/sdk/android#foreground-tracking). */ @@ -431,6 +438,7 @@ object Radar { private var verifiedReceiver: RadarVerifiedReceiver? = null internal lateinit var logger: RadarLogger internal lateinit var apiClient: RadarApiClient + internal lateinit var indoorSurveyManager: RadarIndoorSurveyManager internal lateinit var locationManager: RadarLocationManager internal lateinit var beaconManager: RadarBeaconManager private lateinit var logBuffer: RadarLogBuffer @@ -544,6 +552,10 @@ object Radar { this.logger.d("Using Huawei location services") } + if (!this::indoorSurveyManager.isInitialized) { + this.indoorSurveyManager = RadarIndoorSurveyManager(this.context, logger, locationManager, apiClient) + } + val application = this.context as? Application if (fraud) { RadarSettings.setSharing(this.context, false) @@ -825,8 +837,8 @@ object Radar { return } - val callTrackApi = { beacons: Array? -> - apiClient.track(location, stopped, true, RadarLocationSource.FOREGROUND_LOCATION, false, beacons, callback = object : RadarApiClient.RadarTrackApiCallback { + val callTrackApi = { beacons: Array?, indoorsPayload: String -> + apiClient.track(location, stopped, true, RadarLocationSource.FOREGROUND_LOCATION, false, beacons, indoorsPayload, callback = object : RadarApiClient.RadarTrackApiCallback { override fun onComplete( status: RadarStatus, res: JSONObject?, @@ -846,45 +858,58 @@ object Radar { }) } - if (beacons && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - apiClient.searchBeacons(location, 1000, 10, object : RadarApiClient.RadarSearchBeaconsApiCallback { - override fun onComplete(status: RadarStatus, res: JSONObject?, beacons: Array?, uuids: Array?, uids: Array?) { - if (!uuids.isNullOrEmpty() || !uids.isNullOrEmpty()) { - beaconManager.startMonitoringBeaconUUIDs(uuids, uids) - - beaconManager.rangeBeaconUUIDs(uuids, uids, false, object : RadarBeaconCallback { - override fun onComplete(status: RadarStatus, beacons: Array?) { - if (status != RadarStatus.SUCCESS || beacons == null) { - callTrackApi(null) - - return - } - - callTrackApi(beacons) - } - }) - } else if (beacons != null) { - beaconManager.startMonitoringBeacons(beacons) - - beaconManager.rangeBeacons(beacons, false, object : RadarBeaconCallback { - override fun onComplete(status: RadarStatus, beacons: Array?) { - if (status != RadarStatus.SUCCESS || beacons == null) { - callTrackApi(null) - - return - } - - callTrackApi(beacons) - } - }) - } else { - callTrackApi(arrayOf()); - } + logger.i("calling RadarIndoorsSurvey", RadarLogType.SDK_CALL) + // todo: call indoors survey here + indoorSurveyManager.start("WHEREAMI", 10, location, true, callback = object : RadarIndoorSurveyManager.RadarIndoorSurveyCallback { + override fun onComplete(status: RadarStatus, indoorsPayload: String) { + if (status != RadarStatus.SUCCESS) { + callTrackApi(null, "") + } else { + callTrackApi(null, indoorsPayload) } - }, false) - } else { - callTrackApi(null) - } + } + }) + + + // if (beacons && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // apiClient.searchBeacons(location, 1000, 10, object : RadarApiClient.RadarSearchBeaconsApiCallback { + // override fun onComplete(status: RadarStatus, res: JSONObject?, beacons: Array?, uuids: Array?, uids: Array?) { + // if (!uuids.isNullOrEmpty() || !uids.isNullOrEmpty()) { + // beaconManager.startMonitoringBeaconUUIDs(uuids, uids) + + // beaconManager.rangeBeaconUUIDs(uuids, uids, false, object : RadarBeaconCallback { + // override fun onComplete(status: RadarStatus, beacons: Array?) { + // if (status != RadarStatus.SUCCESS || beacons == null) { + // callTrackApi(null) + + // return + // } + + // callTrackApi(beacons) + // } + // }) + // } else if (beacons != null) { + // beaconManager.startMonitoringBeacons(beacons) + + // beaconManager.rangeBeacons(beacons, false, object : RadarBeaconCallback { + // override fun onComplete(status: RadarStatus, beacons: Array?) { + // if (status != RadarStatus.SUCCESS || beacons == null) { + // callTrackApi(null) + + // return + // } + + // callTrackApi(beacons) + // } + // }) + // } else { + // callTrackApi(arrayOf()); + // } + // } + // }, false) + // } else { + // callTrackApi(null) + // } } }) } diff --git a/sdk/src/main/java/io/radar/sdk/RadarApiClient.kt b/sdk/src/main/java/io/radar/sdk/RadarApiClient.kt index 97a100099..96c62b572 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarApiClient.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarApiClient.kt @@ -231,7 +231,7 @@ internal class RadarApiClient( ) } - internal fun track(location: Location, stopped: Boolean, foreground: Boolean, source: RadarLocationSource, replayed: Boolean, beacons: Array?, verified: Boolean = false, integrityToken: String? = null, integrityException: String? = null, encrypted: Boolean? = false, callback: RadarTrackApiCallback? = null) { + internal fun track(location: Location, stopped: Boolean, foreground: Boolean, source: RadarLocationSource, replayed: Boolean, beacons: Array?, indoorsWhereAmIScan: String? = null, verified: Boolean = false, integrityToken: String? = null, integrityException: String? = null, encrypted: Boolean? = false, callback: RadarTrackApiCallback? = null) { val publishableKey = RadarSettings.getPublishableKey(context) if (publishableKey == null) { callback?.onComplete(RadarStatus.ERROR_PUBLISHABLE_KEY) @@ -341,6 +341,9 @@ internal class RadarApiClient( params.putOpt("sharing", RadarUtils.isScreenSharing(context)) params.putOpt("encrypted", encrypted) } + if (indoorsWhereAmIScan != null) { + params.putOpt("indoorsWhereAmIScan", indoorsWhereAmIScan) + } params.putOpt("appId", context.packageName) } catch (e: JSONException) { callback?.onComplete(RadarStatus.ERROR_BAD_REQUEST) diff --git a/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt b/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt new file mode 100644 index 000000000..256f84f28 --- /dev/null +++ b/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt @@ -0,0 +1,231 @@ +package io.radar.sdk + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.le.BluetoothLeScanner +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.location.Location +import android.os.Handler +import android.os.Build +import android.os.Looper +import io.radar.sdk.Radar.RadarBeaconCallback +import io.radar.sdk.Radar.RadarStatus +import io.radar.sdk.model.RadarBeacon +import java.io.ByteArrayOutputStream +import java.net.URLEncoder +import java.util.* +import java.util.zip.GZIPOutputStream + +@SuppressLint("MissingPermission") +internal class RadarIndoorSurveyManager( + private val context: Context, + private val logger: RadarLogger, + private val locationManager: RadarLocationManager, + private val apiClient: RadarApiClient +) : SensorEventListener { + + interface RadarIndoorSurveyCallback { + fun onComplete(status: RadarStatus, payload: String) + } + + private var isScanning = false + private lateinit var placeLabel: String + private lateinit var callback: RadarIndoorSurveyCallback + private val bluetoothReadings = mutableListOf() + private var isWhereAmIScan = false + private lateinit var scanId: String + private var locationAtTimeOfSurveyStart: Location? = null + private var lastMagnetometerData: SensorEvent? = null + + private lateinit var bluetoothAdapter: BluetoothAdapter + private lateinit var bluetoothLeScanner: BluetoothLeScanner + private lateinit var sensorManager: SensorManager + + internal fun start( + placeLabel: String, + surveyLengthSeconds: Int, + knownLocation: Location?, + isWhereAmIScan: Boolean, + callback: RadarIndoorSurveyCallback + ) { + logger.d("start called with placeLabel: $placeLabel, surveyLengthSeconds: $surveyLengthSeconds, isWhereAmIScan: $isWhereAmIScan") + logger.d("isScanning: $isScanning") + + if (isScanning) { + logger.e("Error: start called while already scanning") + callback.onComplete(RadarStatus.ERROR_UNKNOWN, "Error: start called while already scanning") + return + } + + isScanning = true + this.placeLabel = placeLabel + this.callback = callback + this.isWhereAmIScan = isWhereAmIScan + scanId = UUID.randomUUID().toString() + + if (isWhereAmIScan && knownLocation == null) { + logger.e("Error: start called with isWhereAmIScan but no knownLocation") + callback.onComplete(RadarStatus.ERROR_UNKNOWN, "Error: start called with isWhereAmIScan but no knownLocation") + isScanning = false + return + } else if (isWhereAmIScan && knownLocation != null) { + locationAtTimeOfSurveyStart = knownLocation + kickOffMotionAndBluetooth(surveyLengthSeconds) + } else if (!isWhereAmIScan) { + logger.d("Calling RadarLocationManager getLocationWithDesiredAccuracy") + locationManager.getLocation(object : Radar.RadarLocationCallback { + override fun onComplete(status: RadarStatus, location: Location?, stopped: Boolean) { + if (status != RadarStatus.SUCCESS || location == null) { + callback.onComplete(status, "") + isScanning = false + return + } + + logger.d("location: ${location.latitude}, ${location.longitude}") + logger.d(location.toString()) + + locationAtTimeOfSurveyStart = location + kickOffMotionAndBluetooth(surveyLengthSeconds) + } + }) + } + } + + private fun kickOffMotionAndBluetooth(surveyLengthSeconds: Int) { + logger.d("Kicking off SensorManager") + sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) + sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_NORMAL) + + logger.d("Kicking off BluetoothLeScanner") + logger.d("time: ${System.currentTimeMillis() / 1000.0}") + + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner + startScanning() + + Handler(Looper.getMainLooper()).postDelayed({ stopScanning() }, surveyLengthSeconds * 1000L) + } + + private fun startScanning() { + logger.d("startScanning called --- calling startScan") + bluetoothLeScanner.startScan(scanCallback) + } + + private fun stopScanning() { + logger.d("stopScanning called") + logger.d("time: ${System.currentTimeMillis() / 1000.0}") + + bluetoothLeScanner.stopScan(scanCallback) + + val payload = bluetoothReadings.joinToString("\n") + + logger.d("payload length: ${payload.length}") + + val compressedData = gzip(payload) + logger.d("compressedData length: ${compressedData.size}") + + val compressedDataBase64 = Base64.getEncoder().encodeToString(compressedData) + logger.d("compressedDataBase64 length: ${compressedDataBase64.length}") + + logger.d("isWhereAmIScan $isWhereAmIScan") + + if (isWhereAmIScan) { + callback.onComplete(RadarStatus.SUCCESS, compressedDataBase64) + } else { + // TODO: Implement POST request to server using apiClient + } + + logger.d("Calling clear, resetting scanId, etc.") + + bluetoothReadings.clear() + scanId = "" + locationAtTimeOfSurveyStart = null + lastMagnetometerData = null + + logger.d("stopScanning end") + + isScanning = false + } + + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + val device = result.device + val rssi = result.rssi + val scanRecord = result.scanRecord + + val manufacturerId = scanRecord?.manufacturerSpecificData?.keyAt(0)?.toString() ?: "" + val name = device.name ?: "(no name)" + + val timestamp = System.currentTimeMillis() / 1000.0 + + val serviceUuids = scanRecord?.serviceUuids?.joinToString(",") { it.uuid.toString() } ?: "(no services)" + + val verticalAccuracy = locationAtTimeOfSurveyStart?.let { location -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && location.hasVerticalAccuracy() && !location.verticalAccuracyMeters.isNaN()) { + location.verticalAccuracyMeters + } else { + 0.0 + } + } + + val queryItems = listOf( + "time" to timestamp.toString(), + "label" to placeLabel, + "peripheral.identifier" to device.address, + "peripheral.name" to name, + "rssi" to rssi.toString(), + "manufacturerId" to manufacturerId, + "scanId" to scanId, + "serviceUUIDs" to serviceUuids, + "location.coordinate.latitude" to locationAtTimeOfSurveyStart?.latitude.toString(), + "location.coordinate.longitude" to locationAtTimeOfSurveyStart?.longitude.toString(), + "location.horizontalAccuracy" to locationAtTimeOfSurveyStart?.accuracy.toString(), + "location.verticalAccuracy" to verticalAccuracy.toString(), + "location.altitude" to locationAtTimeOfSurveyStart?.altitude.toString(), + "location.ellipsoidalAltitude" to "0.0", // Not available in Android + "location.timestamp" to locationAtTimeOfSurveyStart?.time.toString(), + "location.floor" to "0", // Not available in Android + "sdkVersion" to RadarUtils.sdkVersion, + "deviceType" to "Android", + "deviceMake" to android.os.Build.MANUFACTURER, + "deviceModel" to android.os.Build.MODEL, + "deviceOS" to android.os.Build.VERSION.RELEASE, + "magnetometer.field.x" to lastMagnetometerData?.values?.get(0).toString(), + "magnetometer.field.y" to lastMagnetometerData?.values?.get(1).toString(), + "magnetometer.field.z" to lastMagnetometerData?.values?.get(2).toString(), + "magnetometer.timestamp" to lastMagnetometerData?.timestamp.toString(), + // "magnetometer.field.magnitude" to lastMagnetometerData?.let { + // Math.sqrt(it.values[0].toDouble().pow(2) + it.values[1].toDouble().pow(2) + it.values[2].toDouble().pow(2)) + // }.toString(), + // "isConnectable" to scanRecord?.isConnectable().toString() + ) + + val queryString = queryItems.joinToString("&") { (key, value) -> + "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}" + } + + bluetoothReadings.add("?$queryString") + } + } + + override fun onSensorChanged(event: SensorEvent) { + if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) { + lastMagnetometerData = event + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} + + private fun gzip(input: String): ByteArray { + val bos = ByteArrayOutputStream() + GZIPOutputStream(bos).use { it.write(input.toByteArray(Charsets.UTF_8)) } + return bos.toByteArray() + } +} \ No newline at end of file diff --git a/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt b/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt index 12a23ff78..599353928 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt @@ -15,6 +15,8 @@ import io.radar.sdk.Radar.RadarLogType import io.radar.sdk.Radar.RadarStatus import io.radar.sdk.RadarApiClient.RadarTrackApiCallback import io.radar.sdk.RadarTrackingOptions.RadarTrackingOptionsDesiredAccuracy +import io.radar.sdk.RadarIndoorSurveyManager +import io.radar.sdk.RadarIndoorSurveyManager.RadarIndoorSurveyCallback import io.radar.sdk.model.* import org.json.JSONObject import java.util.* @@ -36,6 +38,8 @@ internal class RadarLocationManager( private var startedInterval = 0 private var startedFastestInterval = 0 private val callbacks = ArrayList() + private var isIndoorScanning = false + private var locationCachedWhileIndoorScanning: Location? = null internal companion object { private const val BUBBLE_MOVING_GEOFENCE_REQUEST_ID = "radar_moving" @@ -229,6 +233,10 @@ internal class RadarLocationManager( this.stopForegroundService() }, 5000) } + + if (options.doIndoorsSurvey) { + logger.d("Starting indoors survey") + } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && RadarForegroundService.started) { this.stopForegroundService() @@ -469,6 +477,13 @@ internal class RadarLocationManager( return } + if (isIndoorScanning) { + locationCachedWhileIndoorScanning = location + logger.d("Skipping location: already indoor scanning | location = $location") + callCallbacks(RadarStatus.ERROR_LOCATION, location) + return + } + val options = Radar.getTrackingOptions() val wasStopped = RadarState.getStopped(context) var stopped: Boolean @@ -601,69 +616,106 @@ internal class RadarLocationManager( logger.d("Sending location | source = $source; location = $location; stopped = $stopped; replayed = $replayed") val locationManager = this + val apiClientCaptured = this.apiClient // Capture apiClient in a local variable + + + val sendLocation = locationCachedWhileIndoorScanning ?: location + + // call RadarIndoorsSurvey start + if (options.doIndoorsSurvey && !isIndoorScanning && !stopped) { + isIndoorScanning = true + Radar.indoorSurveyManager.start("WHEREAMI", 10, sendLocation, true, object : RadarIndoorSurveyCallback { + override fun onComplete(status: RadarStatus, indoorsWhereAmIScan: String) { + logger.d("Indoors survey complete | indoorsWhereAmIScan = $indoorsWhereAmIScan") + isIndoorScanning = false + apiClientCaptured.track(sendLocation, stopped, RadarActivityLifecycleCallbacks.foreground, source, replayed, null, indoorsWhereAmIScan, callback = object : RadarTrackApiCallback { + override fun onComplete( + status: RadarStatus, + res: JSONObject?, + events: Array?, + user: RadarUser?, + nearbyGeofences: Array?, + config: RadarConfig?, + token: RadarVerifiedLocationToken? + ) { + // would we maybe null this out before the api call? + locationCachedWhileIndoorScanning = null + locationManager.replaceSyncedGeofences(nearbyGeofences) + + if (options.foregroundServiceEnabled && foregroundService.updatesOnly) { + locationManager.stopForegroundService() + } - val callTrackApi = { beacons: Array? -> - this.apiClient.track(location, stopped, RadarActivityLifecycleCallbacks.foreground, source, replayed, beacons, callback = object : RadarTrackApiCallback { - override fun onComplete( - status: RadarStatus, - res: JSONObject?, - events: Array?, - user: RadarUser?, - nearbyGeofences: Array?, - config: RadarConfig?, - token: RadarVerifiedLocationToken? - ) { - locationManager.replaceSyncedGeofences(nearbyGeofences) - - if (options.foregroundServiceEnabled && foregroundService.updatesOnly) { - locationManager.stopForegroundService() - } - - updateTrackingFromMeta(config?.meta) + updateTrackingFromMeta(config?.meta) + // update feature settings? + } + }) } }) - } + } else { + val callTrackApi = { beacons: Array? -> + this.apiClient.track(location, stopped, RadarActivityLifecycleCallbacks.foreground, source, replayed, beacons, callback = object : RadarTrackApiCallback { + override fun onComplete( + status: RadarStatus, + res: JSONObject?, + events: Array?, + user: RadarUser?, + nearbyGeofences: Array?, + config: RadarConfig?, + token: RadarVerifiedLocationToken? + ) { + locationManager.replaceSyncedGeofences(nearbyGeofences) + + if (options.foregroundServiceEnabled && foregroundService.updatesOnly) { + locationManager.stopForegroundService() + } + + updateTrackingFromMeta(config?.meta) + } + }) + } + + if (options.beacons && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + && permissionsHelper.bluetoothPermissionsGranted(context)) { + val cache = stopped || source == RadarLocationSource.BEACON_ENTER || source == RadarLocationSource.BEACON_EXIT + this.apiClient.searchBeacons(location, 1000, 10, object : RadarApiClient.RadarSearchBeaconsApiCallback { + override fun onComplete(status: RadarStatus, res: JSONObject?, beacons: Array?, uuids: Array?, uids: Array?) { + if (!uuids.isNullOrEmpty() || !uids.isNullOrEmpty()) { + Radar.beaconManager.startMonitoringBeaconUUIDs(uuids, uids) - if (options.beacons && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - && permissionsHelper.bluetoothPermissionsGranted(context)) { - val cache = stopped || source == RadarLocationSource.BEACON_ENTER || source == RadarLocationSource.BEACON_EXIT - this.apiClient.searchBeacons(location, 1000, 10, object : RadarApiClient.RadarSearchBeaconsApiCallback { - override fun onComplete(status: RadarStatus, res: JSONObject?, beacons: Array?, uuids: Array?, uids: Array?) { - if (!uuids.isNullOrEmpty() || !uids.isNullOrEmpty()) { - Radar.beaconManager.startMonitoringBeaconUUIDs(uuids, uids) + Radar.beaconManager.rangeBeaconUUIDs(uuids, uids, true, object : Radar.RadarBeaconCallback { + override fun onComplete(status: RadarStatus, beacons: Array?) { + if (status != RadarStatus.SUCCESS || beacons == null) { + callTrackApi(null) - Radar.beaconManager.rangeBeaconUUIDs(uuids, uids, true, object : Radar.RadarBeaconCallback { - override fun onComplete(status: RadarStatus, beacons: Array?) { - if (status != RadarStatus.SUCCESS || beacons == null) { - callTrackApi(null) + return + } - return + callTrackApi(beacons) } + }) + } else if (beacons != null) { + Radar.beaconManager.startMonitoringBeacons(beacons) - callTrackApi(beacons) - } - }) - } else if (beacons != null) { - Radar.beaconManager.startMonitoringBeacons(beacons) + Radar.beaconManager.rangeBeacons(beacons, true, object : Radar.RadarBeaconCallback { + override fun onComplete(status: RadarStatus, beacons: Array?) { + if (status != RadarStatus.SUCCESS || beacons == null) { + callTrackApi(null) - Radar.beaconManager.rangeBeacons(beacons, true, object : Radar.RadarBeaconCallback { - override fun onComplete(status: RadarStatus, beacons: Array?) { - if (status != RadarStatus.SUCCESS || beacons == null) { - callTrackApi(null) + return + } - return + callTrackApi(beacons) } - - callTrackApi(beacons) - } - }) - } else { - callTrackApi(arrayOf()) - } - } - }, cache) - } else { - callTrackApi(null) + }) + } else { + callTrackApi(arrayOf()) + } + } + }, cache) + } else { + callTrackApi(null) + } } } diff --git a/sdk/src/main/java/io/radar/sdk/RadarTrackingOptions.kt b/sdk/src/main/java/io/radar/sdk/RadarTrackingOptions.kt index ea41d7ae0..f7b50c287 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarTrackingOptions.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarTrackingOptions.kt @@ -111,7 +111,12 @@ data class RadarTrackingOptions( /** * Determines whether to monitor beacons. */ - var beacons: Boolean + var beacons: Boolean, + + /** + * Determines whether to do an indoor survey + */ + var doIndoorsSurvey: Boolean ) { /** @@ -377,7 +382,8 @@ data class RadarTrackingOptions( syncGeofences = true, syncGeofencesLimit = 0, foregroundServiceEnabled = true, - beacons = false + beacons = false, + doIndoorsSurvey = false ) /** @@ -406,7 +412,8 @@ data class RadarTrackingOptions( syncGeofences = true, syncGeofencesLimit = 10, foregroundServiceEnabled = false, - beacons = false + beacons = false, + doIndoorsSurvey = false ) /** @@ -435,7 +442,8 @@ data class RadarTrackingOptions( syncGeofences = true, syncGeofencesLimit = 10, foregroundServiceEnabled = false, - beacons = false + beacons = false, + doIndoorsSurvey = false ) internal const val KEY_DESIRED_STOPPED_UPDATE_INTERVAL = "desiredStoppedUpdateInterval" @@ -458,6 +466,7 @@ data class RadarTrackingOptions( internal const val KEY_SYNC_GEOFENCES_LIMIT = "syncGeofencesLimit" internal const val KEY_FOREGROUND_SERVICE_ENABLED = "foregroundServiceEnabled" internal const val KEY_BEACONS = "beacons" + internal const val KEY_DO_INDOORS_SURVEY = "doIndoorsSurvey" @JvmStatic fun fromJson(obj: JSONObject): RadarTrackingOptions { @@ -517,7 +526,8 @@ data class RadarTrackingOptions( syncGeofences = obj.optBoolean(KEY_SYNC_GEOFENCES), syncGeofencesLimit = obj.optInt(KEY_SYNC_GEOFENCES_LIMIT, 10), foregroundServiceEnabled = obj.optBoolean(KEY_FOREGROUND_SERVICE_ENABLED, false), - beacons = obj.optBoolean(KEY_BEACONS) + beacons = obj.optBoolean(KEY_BEACONS), + doIndoorsSurvey = obj.optBoolean(KEY_DO_INDOORS_SURVEY, false) ) } } @@ -544,6 +554,7 @@ data class RadarTrackingOptions( obj.put(KEY_SYNC_GEOFENCES_LIMIT, syncGeofencesLimit) obj.put(KEY_FOREGROUND_SERVICE_ENABLED, foregroundServiceEnabled) obj.put(KEY_BEACONS, beacons) + obj.put(KEY_DO_INDOORS_SURVEY, doIndoorsSurvey) return obj } diff --git a/sdk/src/main/java/io/radar/sdk/RadarVerificationManager.kt b/sdk/src/main/java/io/radar/sdk/RadarVerificationManager.kt index fe3b4a0e6..1b14bede7 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarVerificationManager.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarVerificationManager.kt @@ -98,6 +98,7 @@ internal class RadarVerificationManager( Radar.RadarLocationSource.FOREGROUND_LOCATION, false, beacons, + null, true, integrityToken, integrityException, diff --git a/sdk/src/main/java/io/radar/sdk/model/RadarEvent.kt b/sdk/src/main/java/io/radar/sdk/model/RadarEvent.kt index 25cf52a9d..0cc08c9e0 100644 --- a/sdk/src/main/java/io/radar/sdk/model/RadarEvent.kt +++ b/sdk/src/main/java/io/radar/sdk/model/RadarEvent.kt @@ -166,6 +166,8 @@ class RadarEvent( USER_ARRIVED_AT_WRONG_TRIP_DESTINATION, /** `user.failed_fraud` */ USER_FAILED_FRAUD, + /** `user.indoor_location` */ + USER_INDOOR_LOCATION } /** From ed02da1f767b5a40ec6c30c1096cd900850a6b21 Mon Sep 17 00:00:00 2001 From: ShiCheng Lu Date: Wed, 24 Jul 2024 12:30:31 -0400 Subject: [PATCH 2/3] only call ble if API>=21 --- sdk/src/main/java/io/radar/sdk/Radar.kt | 22 +++++++++++-------- .../io/radar/sdk/RadarIndoorSurveyManager.kt | 6 ++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/sdk/src/main/java/io/radar/sdk/Radar.kt b/sdk/src/main/java/io/radar/sdk/Radar.kt index 8eaab7eb7..9ea26fbe3 100644 --- a/sdk/src/main/java/io/radar/sdk/Radar.kt +++ b/sdk/src/main/java/io/radar/sdk/Radar.kt @@ -552,7 +552,7 @@ object Radar { this.logger.d("Using Huawei location services") } - if (!this::indoorSurveyManager.isInitialized) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !this::indoorSurveyManager.isInitialized) { this.indoorSurveyManager = RadarIndoorSurveyManager(this.context, logger, locationManager, apiClient) } @@ -860,15 +860,19 @@ object Radar { logger.i("calling RadarIndoorsSurvey", RadarLogType.SDK_CALL) // todo: call indoors survey here - indoorSurveyManager.start("WHEREAMI", 10, location, true, callback = object : RadarIndoorSurveyManager.RadarIndoorSurveyCallback { - override fun onComplete(status: RadarStatus, indoorsPayload: String) { - if (status != RadarStatus.SUCCESS) { - callTrackApi(null, "") - } else { - callTrackApi(null, indoorsPayload) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + indoorSurveyManager.start("WHEREAMI", 10, location, true, callback = object : RadarIndoorSurveyManager.RadarIndoorSurveyCallback { + override fun onComplete(status: RadarStatus, payload: String) { + if (status != RadarStatus.SUCCESS) { + callTrackApi(null, "") + } else { + callTrackApi(null, payload) + } } - } - }) + }) + } else { + callTrackApi(null, "") + } // if (beacons && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt b/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt index 256f84f28..e6b3812bd 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt @@ -1,7 +1,9 @@ package io.radar.sdk import android.annotation.SuppressLint +import android.annotation.TargetApi import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager import android.bluetooth.le.BluetoothLeScanner import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult @@ -14,6 +16,7 @@ import android.location.Location import android.os.Handler import android.os.Build import android.os.Looper +import androidx.annotation.RequiresApi import io.radar.sdk.Radar.RadarBeaconCallback import io.radar.sdk.Radar.RadarStatus import io.radar.sdk.model.RadarBeacon @@ -23,6 +26,7 @@ import java.util.* import java.util.zip.GZIPOutputStream @SuppressLint("MissingPermission") +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) internal class RadarIndoorSurveyManager( private val context: Context, private val logger: RadarLogger, @@ -106,7 +110,7 @@ internal class RadarIndoorSurveyManager( logger.d("Kicking off BluetoothLeScanner") logger.d("time: ${System.currentTimeMillis() / 1000.0}") - bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + bluetoothAdapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner startScanning() From ab9f9dc6a2a4685cfb4a07045a3fe3ee4811387d Mon Sep 17 00:00:00 2001 From: ShiCheng Lu Date: Wed, 24 Jul 2024 12:52:24 -0400 Subject: [PATCH 3/3] gate indoor survey call with Android API version check --- sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt | 3 ++- sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt b/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt index e6b3812bd..2adeb103c 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarIndoorSurveyManager.kt @@ -16,6 +16,7 @@ import android.location.Location import android.os.Handler import android.os.Build import android.os.Looper +import android.util.Base64 import androidx.annotation.RequiresApi import io.radar.sdk.Radar.RadarBeaconCallback import io.radar.sdk.Radar.RadarStatus @@ -135,7 +136,7 @@ internal class RadarIndoorSurveyManager( val compressedData = gzip(payload) logger.d("compressedData length: ${compressedData.size}") - val compressedDataBase64 = Base64.getEncoder().encodeToString(compressedData) + val compressedDataBase64 = Base64.encodeToString(compressedData, Base64.NO_WRAP) logger.d("compressedDataBase64 length: ${compressedDataBase64.length}") logger.d("isWhereAmIScan $isWhereAmIScan") diff --git a/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt b/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt index 599353928..3b3007701 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt @@ -622,7 +622,7 @@ internal class RadarLocationManager( val sendLocation = locationCachedWhileIndoorScanning ?: location // call RadarIndoorsSurvey start - if (options.doIndoorsSurvey && !isIndoorScanning && !stopped) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && options.doIndoorsSurvey && !isIndoorScanning && !stopped) { isIndoorScanning = true Radar.indoorSurveyManager.start("WHEREAMI", 10, sendLocation, true, object : RadarIndoorSurveyCallback { override fun onComplete(status: RadarStatus, indoorsWhereAmIScan: String) {