diff --git a/.gitignore b/.gitignore index 98b098f8..2ebf5d66 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /captures .externalNativeBuild /buildSrc/build +/build \ No newline at end of file diff --git a/sdk/build.gradle b/sdk/build.gradle index 5bd048d0..a3e20aba 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.5.12' + radarVersion = '3.5.13' } 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 bc9818cc..47c23712 100644 --- a/sdk/src/main/java/io/radar/sdk/Radar.kt +++ b/sdk/src/main/java/io/radar/sdk/Radar.kt @@ -749,13 +749,7 @@ 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 (status != RadarStatus.SUCCESS || beacons == null) { - callTrackApi(null) - - return - } - - if (!uuids.isNullOrEmpty() || !uids.isNullOrEmpty()) { + if (!uuids.isNullOrEmpty() || !uids.isNullOrEmpty()) { beaconManager.startMonitoringBeaconUUIDs(uuids, uids) beaconManager.rangeBeaconUUIDs(uuids, uids, false, object : RadarBeaconCallback { @@ -769,7 +763,7 @@ object Radar { callTrackApi(beacons) } }) - } else { + } else if (beacons != null) { beaconManager.startMonitoringBeacons(beacons) beaconManager.rangeBeacons(beacons, false, object : RadarBeaconCallback { @@ -783,9 +777,11 @@ object Radar { callTrackApi(beacons) } }) + } else { + callTrackApi(null) } } - }) + }, false) } else { callTrackApi(null) } @@ -2625,13 +2621,14 @@ object Radar { } /** - * Sends Radar log events to the server + * Flushes debug logs to the server. */ @JvmStatic internal fun flushLogs() { if (!initialized || !isTestKey()) { return } + val flushable = logBuffer.getFlushableLogsStash() val logs = flushable.get() if (logs.isNotEmpty()) { @@ -2664,10 +2661,11 @@ object Radar { @JvmStatic internal fun isTestKey(): Boolean { val key = RadarSettings.getPublishableKey(this.context) + val userDebug = RadarSettings.getUserDebug(this.context) return if (key == null) { false } else { - key.startsWith("prj_test") || key.startsWith("org_test") + key.startsWith("prj_test") || key.startsWith("org_test") || userDebug } } @@ -2768,12 +2766,12 @@ object Radar { locationManager.handleLocation(location, source) } - internal fun handleBeacons(context: Context, scanResults: ArrayList?) { + internal fun handleBeacons(context: Context, beacons: Array?, source: RadarLocationSource) { if (!initialized) { initialize(context) } - locationManager.handleBeacons(scanResults) + locationManager.handleBeacons(beacons, source) } internal fun handleBootCompleted(context: Context) { diff --git a/sdk/src/main/java/io/radar/sdk/RadarApiClient.kt b/sdk/src/main/java/io/radar/sdk/RadarApiClient.kt index 12b5e78a..9de05697 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarApiClient.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarApiClient.kt @@ -379,6 +379,8 @@ internal class RadarApiClient( } } + RadarSettings.setUserDebug(context, user.debug) + Radar.sendLocation(location, user) if (events.isNotEmpty()) { @@ -721,7 +723,8 @@ internal class RadarApiClient( location: Location, radius: Int, limit: Int?, - callback: RadarSearchBeaconsApiCallback + callback: RadarSearchBeaconsApiCallback, + cache: Boolean ) { val publishableKey = RadarSettings.getPublishableKey(context) if (publishableKey == null) { @@ -730,10 +733,23 @@ internal class RadarApiClient( return } + if (cache && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val lastBeacons = RadarState.getLastBeacons(context) + val lastBeaconUUIDs = RadarState.getLastBeaconUUIDs(context) + val lastBeaconUIDs = RadarState.getLastBeaconUIDs(context) + + logger.d("Using cached search beacons response | lastBeaconUUIDs = ${lastBeaconUUIDs?.joinToString(",")}; lastBeaconUIDs = ${lastBeaconUIDs?.joinToString(",")}") + + callback.onComplete(RadarStatus.SUCCESS, null, lastBeacons, lastBeaconUUIDs, lastBeaconUIDs) + + return + } + val queryParams = StringBuilder() queryParams.append("near=${location.latitude},${location.longitude}") queryParams.append("&radius=${radius}") queryParams.append("&limit=${limit}") + queryParams.append("&installId=${RadarSettings.getInstallId(context)}") val host = RadarSettings.getHost(context) val uri = Uri.parse(host).buildUpon() @@ -746,7 +762,17 @@ internal class RadarApiClient( apiHelper.request(context, "GET", url, headers, null, false, object : RadarApiHelper.RadarApiCallback { override fun onComplete(status: RadarStatus, res: JSONObject?) { if (status != RadarStatus.SUCCESS || res == null) { - callback.onComplete(status) + var lastBeacons: Array? = null + var lastBeaconUUIDs: Array? = null + var lastBeaconUIDs: Array? = null + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + lastBeacons = RadarState.getLastBeacons(context) + lastBeaconUUIDs = RadarState.getLastBeaconUUIDs(context) + lastBeaconUIDs = RadarState.getLastBeaconUIDs(context) + } + + callback.onComplete(status, res, lastBeacons, lastBeaconUUIDs, lastBeaconUIDs) return } @@ -767,6 +793,12 @@ internal class RadarApiClient( }.filter { uid -> uid.isNotEmpty() }.toTypedArray() } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + RadarState.setLastBeacons(context, beacons) + RadarState.setLastBeaconUUIDs(context, uuids) + RadarState.setLastBeaconUIDs(context, uids) + } + callback.onComplete(RadarStatus.SUCCESS, res, beacons, uuids, uids) } }) diff --git a/sdk/src/main/java/io/radar/sdk/RadarBeaconManager.kt b/sdk/src/main/java/io/radar/sdk/RadarBeaconManager.kt index 4373bc3b..a9e88f5f 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarBeaconManager.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarBeaconManager.kt @@ -133,7 +133,7 @@ internal class RadarBeaconManager( } try { - val scanSettings = getScanSettings(ScanSettings.SCAN_MODE_LOW_POWER, 20000) + val scanSettings = getScanSettings(ScanSettings.SCAN_MODE_LOW_POWER, ScanSettings.CALLBACK_TYPE_FIRST_MATCH or ScanSettings.CALLBACK_TYPE_MATCH_LOST, 0, ScanSettings.MATCH_MODE_STICKY) logger.d("Starting monitoring beacons") @@ -236,7 +236,7 @@ internal class RadarBeaconManager( } try { - val scanSettings = getScanSettings(ScanSettings.SCAN_MODE_LOW_POWER, 20000) + val scanSettings = getScanSettings(ScanSettings.SCAN_MODE_LOW_POWER, ScanSettings.CALLBACK_TYPE_FIRST_MATCH or ScanSettings.CALLBACK_TYPE_MATCH_LOST, 0, ScanSettings.MATCH_MODE_STICKY) logger.d("Starting monitoring beacon UUIDs") @@ -357,8 +357,7 @@ internal class RadarBeaconManager( return } - val scanMode = if (background) ScanSettings.SCAN_MODE_LOW_POWER else ScanSettings.SCAN_MODE_LOW_LATENCY - val scanSettings = getScanSettings(scanMode, 0) + val scanSettings = getScanSettings(ScanSettings.SCAN_MODE_LOW_LATENCY, ScanSettings.CALLBACK_TYPE_ALL_MATCHES, 0, ScanSettings.MATCH_MODE_AGGRESSIVE) val beaconManager = this @@ -366,13 +365,13 @@ internal class RadarBeaconManager( override fun onScanResult(callbackType: Int, result: ScanResult?) { super.onScanResult(callbackType, result) - beaconManager.handleScanResult(result) + beaconManager.handleScanResult(callbackType, result) } override fun onBatchScanResults(results: MutableList?) { super.onBatchScanResults(results) - results?.forEach { result -> beaconManager.handleScanResult(result) } + results?.forEach { result -> beaconManager.handleScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH, result) } } override fun onScanFailed(errorCode: Int) { @@ -500,8 +499,7 @@ internal class RadarBeaconManager( return } - val scanMode = if (background) ScanSettings.SCAN_MODE_LOW_POWER else ScanSettings.SCAN_MODE_LOW_LATENCY - val scanSettings = getScanSettings(scanMode, 0) + val scanSettings = getScanSettings(ScanSettings.SCAN_MODE_LOW_LATENCY, ScanSettings.CALLBACK_TYPE_ALL_MATCHES, 0, ScanSettings.MATCH_MODE_AGGRESSIVE) val beaconManager = this @@ -509,13 +507,13 @@ internal class RadarBeaconManager( override fun onScanResult(callbackType: Int, result: ScanResult?) { super.onScanResult(callbackType, result) - beaconManager.handleScanResult(result) + beaconManager.handleScanResult(callbackType, result) } override fun onBatchScanResults(results: MutableList?) { super.onBatchScanResults(results) - results?.forEach { result -> beaconManager.handleScanResult(result) } + results?.forEach { result -> beaconManager.handleScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH, result) } } override fun onScanFailed(errorCode: Int) { @@ -573,25 +571,41 @@ internal class RadarBeaconManager( this.nearbyBeacons.clear() } - internal fun handleScanResults(scanResults: ArrayList?) { - if (scanResults == null || scanResults.isEmpty()) { - logger.d("No scan results to handle") + internal fun handleBeacons(beacons: Array?, source: Radar.RadarLocationSource) { + if (beacons.isNullOrEmpty()) { + logger.d("No beacons to handle") return } - scanResults.forEach { scanResult -> - this.handleScanResult(scanResult, false) + beacons.forEach { beacon -> + if (source == Radar.RadarLocationSource.BEACON_EXIT) { + logger.d("Handling beacon exit | beacon.type = ${beacon.type}; beacon.uuid = ${beacon.uuid}; beacon.major = ${beacon.major}; beacon.minor = ${beacon.minor}; beacon.rssi = ${beacon.rssi}") + + nearbyBeacons.remove(beacon) + } else { + logger.d("Handling beacon entry | beacon.type = ${beacon.type}; beacon.uuid = ${beacon.uuid}; beacon.major = ${beacon.major}; beacon.minor = ${beacon.minor}; beacon.rssi = ${beacon.rssi}") + + nearbyBeacons.add(beacon) + } } } - internal fun handleScanResult(result: ScanResult?, ranging: Boolean = true) { + internal fun handleScanResult(callbackType: Int, result: ScanResult?, ranging: Boolean = true) { logger.d("Handling scan result") result?.scanRecord?.let { scanRecord -> RadarBeaconUtils.getBeacon(result, scanRecord) }?.let { beacon -> logger.d("Ranged beacon | beacon.type = ${beacon.type}; beacon.uuid = ${beacon.uuid}; beacon.major = ${beacon.major}; beacon.minor = ${beacon.minor}; beacon.rssi = ${beacon.rssi}") - nearbyBeacons.add(beacon) + if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { + logger.d("Handling beacon exit | beacon.type = ${beacon.type}; beacon.uuid = ${beacon.uuid}; beacon.major = ${beacon.major}; beacon.minor = ${beacon.minor}; beacon.rssi = ${beacon.rssi}") + + nearbyBeacons.remove(beacon) + } else { + logger.d("Handling beacon entry | beacon.type = ${beacon.type}; beacon.uuid = ${beacon.uuid}; beacon.major = ${beacon.major}; beacon.minor = ${beacon.minor}; beacon.rssi = ${beacon.rssi}") + + nearbyBeacons.add(beacon) + } } if (this.nearbyBeacons.size == this.beacons.size && ranging) { @@ -612,12 +626,12 @@ internal class RadarBeaconManager( return context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) && adapter != null && adapter.bluetoothLeScanner != null } - private fun getScanSettings(scanMode: Int, reportDelay: Long): ScanSettings { + private fun getScanSettings(scanMode: Int, callbackType: Int, reportDelay: Long, matchMode: Int): ScanSettings { return ScanSettings.Builder() .setScanMode(scanMode) - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setCallbackType(callbackType) .setReportDelay(reportDelay) - .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .setMatchMode(matchMode) .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) .build() } diff --git a/sdk/src/main/java/io/radar/sdk/RadarBeaconUtils.kt b/sdk/src/main/java/io/radar/sdk/RadarBeaconUtils.kt index 910b626a..3f3febaf 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarBeaconUtils.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarBeaconUtils.kt @@ -7,6 +7,7 @@ import android.os.Build import android.os.ParcelUuid import androidx.annotation.RequiresApi import io.radar.sdk.model.RadarBeacon +import org.json.JSONObject import java.nio.ByteBuffer import java.util.* @@ -17,6 +18,69 @@ internal object RadarBeaconUtils { private val EDDYSTONE_SERVICE_UUID = ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB") private val HEX = "0123456789abcdef".toCharArray() + fun beaconsForScanResults(scanResults: ArrayList?): Array { + if (scanResults.isNullOrEmpty()) { + return arrayOf() + } + + val beacons = mutableListOf() + + scanResults.forEach { result -> + result.scanRecord?.let { scanRecord -> getBeacon(result, scanRecord) }?.let { beacon -> + beacons.add(beacon) + } + } + + return beacons.toTypedArray() + } + + fun stringSetForBeacons(beacons: Array?): Set? { + if (beacons == null) { + return null + } + + val arr = stringArrayForBeacons(beacons) + + return arr.toSet() + } + + fun stringArrayForBeacons(beacons: Array): Array { + val arr = mutableListOf() + + beacons.forEach { beacon -> + arr.add(beacon.toJson().toString()) + } + + return arr.toTypedArray() + } + + fun beaconsForStringSet(set: Set?): Array? { + if (set == null) { + return null + } + + val arr = set.toTypedArray() + + return beaconsForStringArray(arr) + } + + fun beaconsForStringArray(arr: Array?): Array? { + if (arr == null) { + return null + } + + val beacons = mutableListOf() + + arr.forEach { str -> + val beacon = RadarBeacon.fromJson(JSONObject(str)) + if (beacon != null) { + beacons.add(beacon) + } + } + + return beacons.toTypedArray() + } + fun getScanFilterForBeacon(beacon: RadarBeacon): ScanFilter? { if (beacon.type == RadarBeacon.RadarBeaconType.EDDYSTONE) { val uid = beacon.uuid diff --git a/sdk/src/main/java/io/radar/sdk/RadarJobScheduler.kt b/sdk/src/main/java/io/radar/sdk/RadarJobScheduler.kt index dbca578e..3341bd67 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarJobScheduler.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarJobScheduler.kt @@ -14,6 +14,7 @@ import android.os.PersistableBundle import androidx.annotation.RequiresApi import io.radar.sdk.Radar.RadarLocationSource import io.radar.sdk.Radar.stringForSource +import io.radar.sdk.model.RadarBeacon import java.util.concurrent.atomic.AtomicInteger @RequiresApi(Build.VERSION_CODES.LOLLIPOP) @@ -26,19 +27,20 @@ class RadarJobScheduler : JobService() { private const val EXTRA_PROVIDER = "provider" private const val EXTRA_TIME = "time" private const val EXTRA_SOURCE = "source" - /** - * Base Job ID (Radar's birthday!) - */ - private const val BASE_JOB_ID = 20160525 + private const val EXTRA_BEACONS = "beacons" - /** - * Current number of active jobs - */ - private val counter = AtomicInteger() + private const val BASE_JOB_ID_LOCATIONS = 20160525 // Radar's birthday! + private const val BASE_JOB_ID_BEACONS = 20210216 // Beacons launch date - internal fun scheduleJob(context: Context, location: Location, source: RadarLocationSource) { + private val numActiveLocationJobs = AtomicInteger() + private val numActiveBeaconJobs = AtomicInteger() + + internal fun scheduleJob( + context: Context, + location: Location, + source: RadarLocationSource + ) { if (!Radar.initialized) { - // Radar must be initialized before using Radar.logger or other Radar members Radar.initialize(context) } @@ -52,8 +54,10 @@ class RadarJobScheduler : JobService() { putString(EXTRA_SOURCE, source.name) } + val sourceStr = stringForSource(source) + val settings = RadarSettings.getFeatureSettings(context) - val jobId = BASE_JOB_ID + (counter.incrementAndGet() % settings.maxConcurrentJobs) + val jobId = BASE_JOB_ID_LOCATIONS + (numActiveLocationJobs.incrementAndGet() % settings.maxConcurrentJobs) val jobInfo = JobInfo.Builder(jobId, componentName) .setExtras(extras) @@ -67,90 +71,139 @@ class RadarJobScheduler : JobService() { val jobScheduler = context.getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler val result = jobScheduler.schedule(jobInfo) if (result == JobScheduler.RESULT_SUCCESS) { - Radar.logger.d( - "Scheduling location job |" + - " location = $location;" + - " source = ${stringForSource(source)};" - ) + Radar.logger.d("Scheduling location job | source = $sourceStr; location = $location") } else { - Radar.logger.d( - "Failed to schedule location job |" + - " location = $location;" + - " source = ${stringForSource(source)};" - ) + Radar.logger.d("Failed to schedule location job | source = $sourceStr; location = $location") + } + } + + internal fun scheduleJob( + context: Context, + beacons: Array, + source: RadarLocationSource + ) { + if (!Radar.initialized) { + Radar.initialize(context) + } + + val componentName = ComponentName(context, RadarJobScheduler::class.java) + val beaconsArr = RadarBeaconUtils.stringArrayForBeacons(beacons) + val extras = PersistableBundle().apply { + putStringArray(EXTRA_BEACONS, beaconsArr) + putString(EXTRA_SOURCE, source.name) } + val sourceStr = stringForSource(source) + + val settings = RadarSettings.getFeatureSettings(context) + val jobId = BASE_JOB_ID_BEACONS + (numActiveBeaconJobs.incrementAndGet() % 10) + + val jobInfo = JobInfo.Builder(jobId, componentName) + .setExtras(extras) + .setMinimumLatency(0) + .setOverrideDeadline(0) + .setRequiredNetworkType( + if (settings.schedulerRequiresNetwork) JobInfo.NETWORK_TYPE_ANY else JobInfo.NETWORK_TYPE_NONE + ) + .build() + + val jobScheduler = context.getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler + val result = jobScheduler.schedule(jobInfo) + if (result == JobScheduler.RESULT_SUCCESS) { + Radar.logger.d("Scheduling beacons job | source = $sourceStr; beaconsArr = ${beaconsArr.joinToString(",")}") + } else { + Radar.logger.d("Failed to schedule beacons job | source = $sourceStr; beaconsArr = ${beaconsArr.joinToString(",")}") + } } } override fun onStartJob(params: JobParameters): Boolean { if (!Radar.initialized) { - // Radar must be initialized before using Radar.logger or other Radar members Radar.initialize(this.applicationContext) } val extras = params.extras + val beaconsArr = extras.getStringArray(EXTRA_BEACONS) val latitude = extras.getDouble(EXTRA_LATITUDE) val longitude = extras.getDouble(EXTRA_LONGITUDE) val accuracy = extras.getDouble(EXTRA_ACCURACY).toFloat() val provider = extras.getString(EXTRA_PROVIDER) val time = extras.getLong(EXTRA_TIME) - val location = Location(provider).apply { - this.latitude = latitude - this.longitude = longitude - this.accuracy = accuracy - this.time = time - } + val sourceStr = extras.getString(EXTRA_SOURCE) ?: return false - val sourceStr = extras.getString(EXTRA_SOURCE) - if (Radar.isTestKey()) { - val batteryState = Radar.batteryManager.getBatteryState() - Radar.logger.d( - "Starting location job | " + - "location = $location; " + - "source = ${sourceStr}; " + - "standbyBucket = ${Radar.batteryManager.getAppStandbyBucket()}; " + - "performanceState = ${batteryState.performanceState.name}; " + - "isCharging = ${batteryState.isCharging}; " + - "batteryPercentage = ${batteryState.percent}; " + - "isPowerSaveMode = ${batteryState.powerSaveMode}; " + - "isIgnoringBatteryOptimizations = ${batteryState.isIgnoringBatteryOptimizations}; " + - "locationPowerSaveMode = ${batteryState.getPowerLocationPowerSaveModeString()}; " + - "isDozeMode = ${batteryState.isDeviceIdleMode}" - ) - } + val source = RadarLocationSource.valueOf(sourceStr) - if (sourceStr == null) { - return false - } + if (beaconsArr != null) { + val beacons = RadarBeaconUtils.beaconsForStringArray(beaconsArr) - val source = RadarLocationSource.valueOf(sourceStr) + Radar.logger.d("Starting beacons job | source = $sourceStr; beaconsArr = ${beaconsArr.joinToString(",")}") + + Radar.handleBeacons(this.applicationContext, beacons, source) + + Handler(Looper.getMainLooper()).postDelayed({ + this.jobFinished(params, false) + }, 10000) + + numActiveBeaconJobs.set(0) + + return true + } else { + val location = Location(provider).apply { + this.latitude = latitude + this.longitude = longitude + this.accuracy = accuracy + this.time = time + } + + if (Radar.isTestKey()) { + val batteryState = Radar.batteryManager.getBatteryState() + Radar.logger.d( + "Starting location job | " + + "source = $sourceStr; " + + "location = $location; " + + "standbyBucket = ${Radar.batteryManager.getAppStandbyBucket()}; " + + "performanceState = ${batteryState.performanceState.name}; " + + "isCharging = ${batteryState.isCharging}; " + + "batteryPercentage = ${batteryState.percent}; " + + "isPowerSaveMode = ${batteryState.powerSaveMode}; " + + "isIgnoringBatteryOptimizations = ${batteryState.isIgnoringBatteryOptimizations}; " + + "locationPowerSaveMode = ${batteryState.getPowerLocationPowerSaveModeString()}; " + + "isDozeMode = ${batteryState.isDeviceIdleMode}" + ) + } else { + Radar.logger.d("Starting location job | source = $sourceStr; location = $location") + } + + Radar.handleLocation(this.applicationContext, location, source) - Radar.handleLocation(this.applicationContext, location, source) + Handler(Looper.getMainLooper()).postDelayed({ + this.jobFinished(params, false) + }, 10000) - Handler(Looper.getMainLooper()).postDelayed({ - this.jobFinished(params, false) - }, 10000) + numActiveLocationJobs.set(0) - counter.set(0) - return true + return true + } } override fun onStopJob(params: JobParameters): Boolean { if (!Radar.initialized) { - // Radar must be initialized before using Radar.logger or other Radar members Radar.initialize(this.applicationContext) } - if (Radar.isTestKey()) { - val extras = params.extras - val latitude = extras.getDouble(EXTRA_LATITUDE) - val longitude = extras.getDouble(EXTRA_LONGITUDE) - val accuracy = extras.getDouble(EXTRA_ACCURACY).toFloat() - val provider = extras.getString(EXTRA_PROVIDER) - val time = extras.getLong(EXTRA_TIME) + val extras = params.extras + val beaconsArr = extras.getStringArray(EXTRA_BEACONS) + val latitude = extras.getDouble(EXTRA_LATITUDE) + val longitude = extras.getDouble(EXTRA_LONGITUDE) + val accuracy = extras.getDouble(EXTRA_ACCURACY).toFloat() + val provider = extras.getString(EXTRA_PROVIDER) + val time = extras.getLong(EXTRA_TIME) + val source = extras.getString(EXTRA_SOURCE) + if (beaconsArr != null) { + Radar.logger.d("Stopping beacons job | source = $source; beaconsArr = ${beaconsArr.joinToString(",")}") + } else { val location = Location(provider).apply { this.latitude = latitude this.longitude = longitude @@ -158,9 +211,7 @@ class RadarJobScheduler : JobService() { this.time = time } - val source = extras.getString(EXTRA_SOURCE) - // This may occur, for example, if a new location job is scheduled before the current job finishes executing. - Radar.logger.d("Stopping location job | location = $location; source = $source") + Radar.logger.d("Stopping location job | source = $source; location = $location") } return false diff --git a/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt b/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt index ddc5c123..c022880d 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarLocationManager.kt @@ -2,13 +2,11 @@ package io.radar.sdk import android.annotation.SuppressLint import android.app.NotificationManager -import android.bluetooth.le.ScanResult import android.content.Context import android.content.Intent import android.location.Location import android.os.Build import io.radar.sdk.Radar.RadarLocationCallback -import io.radar.sdk.Radar.RadarLocationServicesProvider.GOOGLE import io.radar.sdk.Radar.RadarLocationServicesProvider.HUAWEI import io.radar.sdk.Radar.RadarLocationSource import io.radar.sdk.Radar.RadarStatus @@ -136,11 +134,11 @@ internal class RadarLocationManager( this.started = false } - internal fun handleBeacons(scanResults: ArrayList?) { + internal fun handleBeacons(beacons: Array?, source: RadarLocationSource) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - logger.d("Handling beacons | scanResults = $scanResults") + logger.d("Handling beacons") - Radar.beaconManager.handleScanResults(scanResults) + Radar.beaconManager.handleBeacons(beacons, source) val lastLocation = RadarState.getLastLocation(context) @@ -148,7 +146,7 @@ internal class RadarLocationManager( logger.d("Not handling beacons, no last location") } - this.handleLocation(lastLocation, RadarLocationSource.BEACON_ENTER) + this.handleLocation(lastLocation, source) } } @@ -444,7 +442,7 @@ internal class RadarLocationManager( val wasStopped = RadarState.getStopped(context) var stopped: Boolean - val force = (source == RadarLocationSource.FOREGROUND_LOCATION || source == RadarLocationSource.MANUAL_LOCATION || source == RadarLocationSource.BEACON_ENTER) + val force = (source == RadarLocationSource.FOREGROUND_LOCATION || source == RadarLocationSource.MANUAL_LOCATION || source == RadarLocationSource.BEACON_ENTER || source == RadarLocationSource.BEACON_EXIT) if (!force && location.accuracy > 1000 && options.desiredAccuracy != RadarTrackingOptionsDesiredAccuracy.LOW) { logger.d("Skipping location: inaccurate | accuracy = ${location.accuracy}") @@ -594,15 +592,10 @@ internal class RadarLocationManager( if (options.beacons && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && permissionsHelper.bluetoothPermissionsGranted(context)) { - Radar.apiClient.searchBeacons(location, 1000, 10, object : RadarApiClient.RadarSearchBeaconsApiCallback { + 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 (status != RadarStatus.SUCCESS || beacons == null) { - callTrackApi(null) - - return - } - - if (!uuids.isNullOrEmpty() || !uids.isNullOrEmpty()) { + if (!uuids.isNullOrEmpty() || !uids.isNullOrEmpty()) { Radar.beaconManager.startMonitoringBeaconUUIDs(uuids, uids) Radar.beaconManager.rangeBeaconUUIDs(uuids, uids, true, object : Radar.RadarBeaconCallback { @@ -616,7 +609,7 @@ internal class RadarLocationManager( callTrackApi(beacons) } }) - } else { + } else if (beacons != null) { Radar.beaconManager.startMonitoringBeacons(beacons) Radar.beaconManager.rangeBeacons(beacons, true, object : Radar.RadarBeaconCallback { @@ -630,9 +623,11 @@ internal class RadarLocationManager( callTrackApi(beacons) } }) - } + } else { + callTrackApi(null) + } } - }) + }, cache) } else { callTrackApi(null) } diff --git a/sdk/src/main/java/io/radar/sdk/RadarLocationReceiver.kt b/sdk/src/main/java/io/radar/sdk/RadarLocationReceiver.kt index 5b681ae3..43465297 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarLocationReceiver.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarLocationReceiver.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.bluetooth.le.BluetoothLeScanner import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -98,7 +99,6 @@ class RadarLocationReceiver : BroadcastReceiver() { @SuppressLint("MissingPermission") override fun onReceive(context: Context, intent: Intent) { if (!Radar.initialized) { - // Radar must be initialized before using Radar.logger Radar.initialize(context) } @@ -137,8 +137,10 @@ class RadarLocationReceiver : BroadcastReceiver() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val bleCallbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1) if (bleCallbackType != -1) { + val source = if (bleCallbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) Radar.RadarLocationSource.BEACON_EXIT else Radar.RadarLocationSource.BEACON_ENTER val scanResults: ArrayList? = intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT) - Radar.handleBeacons(context, scanResults) + val beacons = RadarBeaconUtils.beaconsForScanResults(scanResults) + RadarJobScheduler.scheduleJob(context, beacons, source) } } } diff --git a/sdk/src/main/java/io/radar/sdk/RadarSettings.kt b/sdk/src/main/java/io/radar/sdk/RadarSettings.kt index 4d78bb45..c275c3d4 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarSettings.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarSettings.kt @@ -30,11 +30,11 @@ internal object RadarSettings { private const val KEY_LOG_LEVEL = "log_level" private const val KEY_HOST = "host" private const val KEY_PERMISSIONS_DENIED = "permissions_denied" - private const val KEY_OLD_UPDATE_INTERVAL = "dwell_delay" private const val KEY_OLD_UPDATE_INTERVAL_RESPONSIVE = 60000 private const val KEY_OLD_SYNC_MODE = "sync_mode" private const val KEY_OLD_OFFLINE_MODE = "offline_mode" + private const val KEY_USER_DEBUG = "user_debug" private fun getSharedPreferences(context: Context): SharedPreferences { return context.getSharedPreferences("RadarSDK", Context.MODE_PRIVATE) @@ -253,7 +253,8 @@ internal object RadarSettings { internal fun getLogLevel(context: Context): Radar.RadarLogLevel { val logLevelInt = getSharedPreferences(context).getInt(KEY_LOG_LEVEL, 3) - return Radar.RadarLogLevel.fromInt(logLevelInt) + val userDebug = getUserDebug(context) + return if (userDebug) Radar.RadarLogLevel.DEBUG else Radar.RadarLogLevel.fromInt(logLevelInt) } internal fun setLogLevel(context: Context, level: Radar.RadarLogLevel) { @@ -273,4 +274,12 @@ internal object RadarSettings { return getSharedPreferences(context).getBoolean(KEY_PERMISSIONS_DENIED, false) } + internal fun getUserDebug(context: Context): Boolean { + return getSharedPreferences(context).getBoolean(KEY_USER_DEBUG, true) + } + + internal fun setUserDebug(context: Context, userDebug: Boolean) { + getSharedPreferences(context).edit { putBoolean(KEY_USER_DEBUG, userDebug) } + } + } \ No newline at end of file diff --git a/sdk/src/main/java/io/radar/sdk/RadarState.kt b/sdk/src/main/java/io/radar/sdk/RadarState.kt index 02109539..26b363c0 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarState.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarState.kt @@ -3,7 +3,10 @@ package io.radar.sdk import android.content.Context import android.content.SharedPreferences import android.location.Location +import android.os.Build +import androidx.annotation.RequiresApi import androidx.core.content.edit +import io.radar.sdk.model.RadarBeacon internal object RadarState { @@ -31,6 +34,9 @@ internal object RadarState { private const val KEY_PLACE_ID = "place_id" private const val KEY_REGION_IDS = "region_ids" private const val KEY_BEACON_IDS = "beacon_ids" + private const val KEY_LAST_BEACONS = "last_beacons" + private const val KEY_LAST_BEACON_UUIDS = "last_beacon_uuids" + private const val KEY_LAST_BEACON_UIDS = "last_beacon_uids" private fun getSharedPreferences(context: Context): SharedPreferences { return context.getSharedPreferences("RadarSDK", Context.MODE_PRIVATE) @@ -215,4 +221,36 @@ internal object RadarState { getSharedPreferences(context).edit { putStringSet(KEY_BEACON_IDS, beaconIds) } } + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + internal fun getLastBeacons(context: Context): Array? { + val set = getSharedPreferences(context).getStringSet(KEY_LAST_BEACONS, null) + return RadarBeaconUtils.beaconsForStringSet(set) + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + internal fun setLastBeacons(context: Context, beacons: Array?) { + val set = RadarBeaconUtils.stringSetForBeacons(beacons) + getSharedPreferences(context).edit { putStringSet(KEY_LAST_BEACONS, set) } + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + internal fun getLastBeaconUUIDs(context: Context): Array? { + return getSharedPreferences(context).getStringSet(KEY_LAST_BEACON_UUIDS, null)?.toTypedArray() + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + internal fun setLastBeaconUUIDs(context: Context, uuids: Array?) { + getSharedPreferences(context).edit { putStringSet(KEY_LAST_BEACON_UUIDS, uuids?.toSet()) } + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + internal fun getLastBeaconUIDs(context: Context): Array? { + return getSharedPreferences(context).getStringSet(KEY_LAST_BEACON_UIDS, null)?.toTypedArray() + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + internal fun setLastBeaconUIDs(context: Context, uids: Array?) { + getSharedPreferences(context).edit { putStringSet(KEY_LAST_BEACON_UIDS, uids?.toSet()) } + } + } \ No newline at end of file diff --git a/sdk/src/main/java/io/radar/sdk/RadarUtils.kt b/sdk/src/main/java/io/radar/sdk/RadarUtils.kt index 4eedcdce..46f4d7fd 100644 --- a/sdk/src/main/java/io/radar/sdk/RadarUtils.kt +++ b/sdk/src/main/java/io/radar/sdk/RadarUtils.kt @@ -12,7 +12,6 @@ import android.provider.Settings import androidx.core.content.ContextCompat import androidx.core.content.edit import com.google.android.gms.ads.identifier.AdvertisingIdClient -import io.radar.sdk.model.RadarEvent import java.io.File import java.text.SimpleDateFormat import java.util.* diff --git a/sdk/src/main/java/io/radar/sdk/model/RadarUser.kt b/sdk/src/main/java/io/radar/sdk/model/RadarUser.kt index 907938b4..82dabb37 100644 --- a/sdk/src/main/java/io/radar/sdk/model/RadarUser.kt +++ b/sdk/src/main/java/io/radar/sdk/model/RadarUser.kt @@ -120,7 +120,12 @@ class RadarUser( * A boolean indicating whether the user's location is being mocked, such as in a simulation. May be `false` if * Fraud is not enabled. */ - val mocked: Boolean = false + val mocked: Boolean = false, + + /** + * A boolean indicating whether the user has been "Marked as Debug" in the dashboard. + */ + val debug: Boolean = false ) { /** @@ -152,6 +157,7 @@ class RadarUser( private const val FIELD_SOURCE = "source" private const val FIELD_FRAUD = "fraud" private const val FIELD_TRIP = "trip" + private const val FIELD_DEBUG = "debug" @JvmStatic fun fromJson(obj: JSONObject?): RadarUser? { @@ -197,6 +203,7 @@ class RadarUser( } val fraud = RadarFraud.fromJson(obj.optJSONObject(FIELD_FRAUD)) val trip = RadarTrip.fromJson(obj.optJSONObject(FIELD_TRIP)) + val debug = obj.optBoolean(FIELD_DEBUG) return RadarUser( id, @@ -220,7 +227,8 @@ class RadarUser( source, fraud.proxy, trip, - fraud.mocked + fraud.mocked, + debug ) } } @@ -254,6 +262,7 @@ class RadarUser( obj.putOpt(FIELD_SOURCE, Radar.stringForSource(this.source)) obj.putOpt(FIELD_FRAUD, this.fraud.toJson()) obj.putOpt(FIELD_TRIP, this.trip?.toJson()) + obj.putOpt(FIELD_DEBUG, this.debug) return obj }